AMQP-111: add AnonymousQueue and a load of changes to namespace to support it

This commit is contained in:
Dave Syer
2011-03-22 17:28:48 +00:00
parent d236fc71a0
commit 04f9a88240
15 changed files with 254 additions and 100 deletions

View File

@@ -29,33 +29,45 @@ import java.util.Map;
*/
public abstract class AbstractExchange implements Exchange {
protected String name;
private final String name;
private boolean durable = false;
private final boolean durable;
private boolean autoDelete = false;
private final boolean autoDelete;
private Map<String, Object> arguments = null;
private final Map<String, Object> arguments;
/**
* Construct a new Exchange for bean usage.
* @param name the name of the exchange.
*/
public AbstractExchange(String name) {
this.name = name;
this(name, false, false);
}
/**
* Construct a new Exchange, given a name, durability flag, and auto-delete flag.
* Construct a new Exchange, given a name, durability flag, auto-delete flag.
* @param name the name of the exchange.
* @param durable true if we are declaring a durable exchange (the exchange will survive a server restart)
* @param autoDelete true if the server should delete the exchange when it is no longer in use
*/
public AbstractExchange(String name, boolean durable, boolean autoDelete) {
this(name, durable, autoDelete, null);
}
/**
* Construct a new Exchange, given a name, durability flag, and auto-delete flag, and arguments.
* @param name the name of the exchange.
* @param durable true if we are declaring a durable exchange (the exchange will survive a server restart)
* @param autoDelete true if the server should delete the exchange when it is no longer in use
* @param arguments the arguments used to declare the exchange
*/
public AbstractExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments) {
super();
this.name = name;
this.durable = durable;
this.autoDelete = autoDelete;
this.arguments = arguments;
}
public abstract String getType();
@@ -68,27 +80,10 @@ public abstract class AbstractExchange implements Exchange {
return durable;
}
/**
* Set the durability of this exchange definition.
* @param durable true if describing a durable exchange (the exchange will survive a server restart)
*/
public void setDurable(boolean durable) {
this.durable = durable;
}
public boolean isAutoDelete() {
return autoDelete;
}
/**
* Set the auto-delete lifecycle of this exchange.
* An non-auto-deleted exchange lasts until the server is shut down.
* @param autoDelete true if the server should delete the exchange when it is no longer in use.
*/
public void setAutoDelete(boolean autoDelete) {
this.autoDelete = autoDelete;
}
/**
* Return the collection of arbitrary arguments to use when declaring an exchange.
* @return the collection of arbitrary arguments to use when declaring an exchange.
@@ -97,14 +92,6 @@ public abstract class AbstractExchange implements Exchange {
return arguments;
}
/**
* Set the collection of arbitrary arguments to use when declaring an exchange.
* @param arguments A collection of arbitrary arguments to use when declaring an exchange.
*/
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
@Override
public String toString() {
return "Exchange [name=" + name +

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.amqp.core;
import java.util.UUID;
/**
* @author Dave Syer
*
*/
public class AnonymousQueue extends Queue {
public AnonymousQueue() {
super(UUID.randomUUID().toString(), false, true, true);
}
}

View File

@@ -1,43 +1,85 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.amqp.core;
import java.util.Map;
/**
* Simple container collecting information to describe a queue.
* Used in conjunction with AmqpAdmin.
* Simple container collecting information to describe a queue. Used in conjunction with AmqpAdmin.
*
* @author Mark Pollack
* @see AmqpAdmin
*/
public class Queue {
public class Queue {
private final String name;
private volatile boolean durable;
private final boolean durable;
private volatile boolean exclusive;
private final boolean exclusive;
private volatile boolean autoDelete;
private volatile java.util.Map<java.lang.String,java.lang.Object> arguments;
private final boolean autoDelete;
private final java.util.Map<java.lang.String, java.lang.Object> arguments;
/**
* The queue is non-durable, non-exclusive and non auto-delete.
*
* @param name the name of the queue.
*/
public Queue(String name) {
this(name, false, false, false);
}
/**
* Construct a new queue, given a name and durability flag. The queue is non-exclusive and non auto-delete.
*
* @param name the name of the queue.
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
*/
public Queue(String name, boolean durable) {
this(name, durable, false, false, null);
}
/**
* Construct a new queue, given a name, durability, exclusive and auto-delete flags.
* @param name the name of the queue.
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (the queue will only be used by the declarer's
* connection)
* @param autoDelete true if the server should delete the queue when it is no longer in use
*/
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete) {
this(name, durable, exclusive, autoDelete, null);
}
/**
* Construct a new queue, given a name, durability flag, and auto-delete flag, and arguments.
* @param name the name of the queue.
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (the queue will only be used by the declarer's
* connection)
* @param autoDelete true if the server should delete the queue when it is no longer in use
* @param arguments the arguments used to declare the queue
*/
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) {
super();
this.name = name;
this.durable = durable;
this.exclusive = exclusive;
this.autoDelete = autoDelete;
this.arguments = arguments;
}
public String getName() {
@@ -48,40 +90,22 @@ public class Queue {
return this.durable;
}
public void setDurable(boolean durable) {
this.durable = durable;
}
public boolean isExclusive() {
return this.exclusive;
}
public void setExclusive(boolean exclusive) {
this.exclusive = exclusive;
}
public boolean isAutoDelete() {
return this.autoDelete;
}
public void setAutoDelete(boolean autoDelete) {
this.autoDelete = autoDelete;
}
public java.util.Map<java.lang.String, java.lang.Object> getArguments() {
return this.arguments;
}
public void setArguments(java.util.Map<java.lang.String, java.lang.Object> arguments) {
this.arguments = arguments;
}
@Override
public String toString() {
return "Queue [name=" + name + ", durable=" + durable + ", autoDelete="
+ autoDelete + ", exclusive=" + exclusive + ", arguments="
+ arguments + "]";
return "Queue [name=" + name + ", durable=" + durable + ", autoDelete=" + autoDelete + ", exclusive="
+ exclusive + ", arguments=" + arguments + "]";
}
}

View File

@@ -48,6 +48,7 @@ public abstract class AbstractRabbitConfiguration extends AbstractAmqpConfigurat
public AmqpAdmin amqpAdmin() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate().getConnectionFactory());
rabbitAdmin.setAutoStartup(true);
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}

View File

@@ -13,12 +13,16 @@
package org.springframework.amqp.rabbit.config;
import java.util.List;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
@@ -45,6 +49,8 @@ class ListenerContainerParser implements BeanDefinitionParser {
private static final String QUEUE_NAMES_ATTRIBUTE = "queue-names";
private static final String QUEUES_ATTRIBUTE = "queues";
private static final String REF_ATTRIBUTE = "ref";
private static final String METHOD_ATTRIBUTE = "method";
@@ -139,19 +145,41 @@ class ListenerContainerParser implements BeanDefinitionParser {
listenerDef.setBeanClassName("org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter");
containerDef.getPropertyValues().add("messageListener", listenerDef);
String containerBeanName = listenerEle.getAttribute(ID_ATTRIBUTE);
String containerBeanName = containerEle.getAttribute(ID_ATTRIBUTE);
// If no bean id is given auto generate one using the ReaderContext's BeanNameGenerator
if (!StringUtils.hasText(containerBeanName)) {
containerBeanName = parserContext.getReaderContext().generateBeanName(containerDef);
}
String queueNames = listenerEle.getAttribute(QUEUE_NAMES_ATTRIBUTE);
if (!StringUtils.hasText(queueNames)) {
parserContext.getReaderContext().error("Listener 'queue-names' attribute contains empty value.",
if (!NamespaceUtils.isAttributeDefined(listenerEle, QUEUE_NAMES_ATTRIBUTE)
&& !NamespaceUtils.isAttributeDefined(listenerEle, QUEUES_ATTRIBUTE)) {
parserContext.getReaderContext().error("Listener 'queue-names' or 'queues' attribute must be provided.",
listenerEle);
}
containerDef.getPropertyValues().add("queueNames",
StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(queueNames)));
if (NamespaceUtils.isAttributeDefined(listenerEle, QUEUE_NAMES_ATTRIBUTE)
&& NamespaceUtils.isAttributeDefined(listenerEle, QUEUES_ATTRIBUTE)) {
parserContext.getReaderContext().error("Listener 'queue-names' or 'queues' attribute must be provided but not both.",
listenerEle);
}
String queueNames = listenerEle.getAttribute(QUEUE_NAMES_ATTRIBUTE);
if (StringUtils.hasText(queueNames)) {
String[] names = StringUtils.commaDelimitedListToStringArray(queueNames);
List<TypedStringValue> values = new ManagedList<TypedStringValue>();
for (int i = 0; i < names.length; i++) {
values.add(new TypedStringValue(names[i].trim()));
}
containerDef.getPropertyValues().add("queueNames", values);
}
String queues = listenerEle.getAttribute(QUEUES_ATTRIBUTE);
if (StringUtils.hasText(queues)) {
String[] names = StringUtils.commaDelimitedListToStringArray(queues);
List<RuntimeBeanReference> values = new ManagedList<RuntimeBeanReference>();
for (int i = 0; i < names.length; i++) {
values.add(new RuntimeBeanReference(names[i].trim()));
}
containerDef.getPropertyValues().add("queues", values);
}
// Register the listener and fire event
parserContext.registerBeanComponent(new BeanComponentDefinition(containerDef, containerBeanName));

View File

@@ -76,6 +76,17 @@ public abstract class NamespaceUtils {
Conventions.attributeNameToPropertyName(attributeName));
}
/**
* Checks the attribute to see if it is defined in the given element.
*
* @param element the XML element where the attribute should be defined
* @param attributeName the name of the attribute whose value will be used as a constructor argument
*/
public static boolean isAttributeDefined(Element element, String attributeName) {
String value = element.getAttribute(attributeName);
return (StringUtils.hasText(value));
}
/**
* Populates the bean definition constructor argument with the value of that attribute if it is defined in the given
* element.

View File

@@ -16,6 +16,7 @@
package org.springframework.amqp.rabbit.config;
import org.springframework.amqp.core.AnonymousQueue;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
@@ -35,11 +36,18 @@ public class QueueParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return Queue.class;
if (NamespaceUtils.isAttributeDefined(element, NAME_ATTRIBUTE)) {
return Queue.class;
} else {
return AnonymousQueue.class;
}
}
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
if (!NamespaceUtils.isAttributeDefined(element, NAME_ATTRIBUTE) && !NamespaceUtils.isAttributeDefined(element, ID_ATTRIBUTE)) {
parserContext.getReaderContext().error("Queue must have either id or name (or both)", element);
}
NamespaceUtils.addConstructorArgValueIfAttributeDefined(builder, element, NAME_ATTRIBUTE);
}

View File

@@ -134,8 +134,8 @@ public class SingleConnectionFactory implements ConnectionFactory, DisposableBea
}
this.connection = new SharedConnectionProxy(this.targetConnection);
}
this.listener.onCreate(connection);
}
this.listener.onCreate(connection);
return this.connection;
}

View File

@@ -124,10 +124,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Initiali
return channel.queueDeclare();
}
});
Queue queue = new Queue(declareOk.getQueue());
queue.setExclusive(true);
queue.setAutoDelete(true);
queue.setDurable(false);
Queue queue = new Queue(declareOk.getQueue(), true, true, false);
return queue;
}
@@ -247,6 +244,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Initiali
return;
}
logger.debug("Initializing declarations");
final Collection<Exchange> exchanges = applicationContext.getBeansOfType(Exchange.class).values();
final Collection<Queue> queues = applicationContext.getBeansOfType(Queue.class).values();
final Collection<Binding> bindings = applicationContext.getBeansOfType(Binding.class).values();
@@ -258,6 +256,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Initiali
return null;
}
});
logger.debug("Declarations finished");
}

View File

@@ -21,7 +21,15 @@
<xsd:element ref="queue-arguments" minOccurs="0"
maxOccurs="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string">
<xsd:attribute name="id" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The id of the queue in case it is different than the name. Clients can receive or listen for messages by referring to the
queue itself, or to its name.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="name" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of the queue. Clients can receive or listen for messages by referring to the
@@ -361,6 +369,13 @@
<xsd:sequence>
<xsd:element name="listener" type="listenerType" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Optional bean id for the container.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="connection-factory" type="xsd:string" default="rabbitConnectionFactory">
<xsd:annotation>
<xsd:documentation><![CDATA[
@@ -484,10 +499,17 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="queue-names" type="xsd:string" use="required">
<xsd:attribute name="queue-names" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The queue names for this listener as a comma-separated list. Required.
The queue names for this listener as a comma-separated list. Either this or queues is required.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="queues" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The queues (bean references) for this listener as a comma-separated list. Either this or queue-names is requires.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -24,11 +24,13 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.core.io.ClassPathResource;
/**
@@ -41,20 +43,28 @@ public final class ListenerContainerParserTests {
@Before
public void setUp() throws Exception {
beanFactory = new XmlBeanFactory(new ClassPathResource(getClass().getSimpleName() + "-context.xml", getClass()));
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver());
}
@Test
public void testParse() throws Exception {
SimpleMessageListenerContainer container = beanFactory.getBean(SimpleMessageListenerContainer.class);
public void testParseWithQueueNames() throws Exception {
SimpleMessageListenerContainer container = beanFactory.getBean("container1", SimpleMessageListenerContainer.class);
assertEquals(AcknowledgeMode.MANUAL, container.getAcknowledgeMode());
assertEquals(beanFactory.getBean(ConnectionFactory.class), container.getConnectionFactory());
assertEquals(MessageListenerAdapter.class, container.getMessageListener().getClass());
DirectFieldAccessor listenerAccessor = new DirectFieldAccessor(container.getMessageListener());
assertEquals(beanFactory.getBean(TestBean.class), listenerAccessor.getPropertyValue("delegate"));
assertEquals("handle", listenerAccessor.getPropertyValue("defaultListenerMethod"));
assertEquals("[foo, bar]", Arrays.asList(container.getQueueNames()).toString());
Queue queue = beanFactory.getBean("bar", Queue.class);
assertEquals("[foo, "+queue.getName()+"]", Arrays.asList(container.getQueueNames()).toString());
}
@Test
public void testParseWithQueues() throws Exception {
SimpleMessageListenerContainer container = beanFactory.getBean("container2", SimpleMessageListenerContainer.class);
Queue queue = beanFactory.getBean("bar", Queue.class);
assertEquals("[foo, "+queue.getName()+"]", Arrays.asList(container.getQueueNames()).toString());
}
static class TestBean {
public void handle(String s) {

View File

@@ -18,11 +18,14 @@ package org.springframework.amqp.rabbit.config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.amqp.core.AnonymousQueue;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
@@ -43,8 +46,26 @@ public final class RabbitNamespaceHandlerTests {
}
@Test
public void testParse() throws Exception {
assertNotNull(beanFactory.getBean("foo", Queue.class));
public void testQueue() throws Exception {
Queue queue = beanFactory.getBean("foo", Queue.class);
assertNotNull(queue);
assertEquals("foo", queue.getName());
}
@Test
public void testAliasQueue() throws Exception {
Queue queue = beanFactory.getBean("spam", Queue.class);
assertNotNull(queue);
assertNotSame("spam", queue.getName());
assertEquals("bar", queue.getName());
}
@Test
public void testAnonymousQueue() throws Exception {
Queue queue = beanFactory.getBean("bucket", Queue.class);
assertNotNull(queue);
assertNotSame("bucket", queue.getName());
assertTrue(queue instanceof AnonymousQueue);
}
@Test
@@ -58,8 +79,8 @@ public final class RabbitNamespaceHandlerTests {
@Test
public void testBindings() throws Exception {
Map<String, Binding> bindings = beanFactory.getBeansOfType(Binding.class);
// 2 for each exchange type
assertEquals(8, bindings.size());
// 4 for each exchange type
assertEquals(16, bindings.size());
}
@Test

View File

@@ -38,7 +38,8 @@ public class MessageListenerBrokerInterruptionIntegrationTests {
private static Log logger = LogFactory.getLog(MessageListenerBrokerInterruptionIntegrationTests.class);
private Queue queue = new Queue("test.queue");
// Ensure queue is durable, or it won't survive the broker restart
private Queue queue = new Queue("test.queue", true);
private int concurrentConsumers = 2;
@@ -70,8 +71,6 @@ public class MessageListenerBrokerInterruptionIntegrationTests {
public MessageListenerBrokerInterruptionIntegrationTests() throws Exception {
FileUtils.deleteDirectory(new File("target/rabbitmq"));
// Ensure queue is durable, or it won't survive the broker restart
queue.setDurable(true);
brokerIsRunning.setPort(BrokerTestUtils.getAdminPort());
logger.debug("Setting up broker");
brokerAdmin = BrokerTestUtils.getRabbitBrokerAdmin();

View File

@@ -7,10 +7,14 @@
<rabbit:queue name="foo" />
<rabbit:queue name="bar" />
<rabbit:queue id="bar" />
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" concurrency="5">
<rabbit:listener id="testListener" queue-names="foo, bar" ref="testBean" method="handle"/>
<rabbit:listener-container id="container1" connection-factory="connectionFactory" acknowledge="manual" concurrency="5">
<rabbit:listener id="testListener" queue-names="foo, #{bar.name}" ref="testBean" method="handle"/>
</rabbit:listener-container>
<rabbit:listener-container id="container2" connection-factory="connectionFactory" acknowledge="manual" concurrency="5">
<rabbit:listener id="testListener" queues="foo, bar" ref="testBean" method="handle"/>
</rabbit:listener-container>
<bean class="org.springframework.amqp.rabbit.core.RabbitAdmin">

View File

@@ -10,6 +10,8 @@
<bindings>
<binding queue="foo" key="foo" />
<binding queue="bar" />
<binding queue="spam" />
<binding queue="bucket" />
</bindings>
</direct-exchange>
@@ -18,6 +20,8 @@
<bindings>
<binding queue="foo" pattern="foo.#" />
<binding queue="bar" pattern="bar.#" />
<binding queue="spam" pattern="spam.#"/>
<binding queue="bucket" pattern="bucket.#"/>
</bindings>
</topic-exchange>
@@ -26,6 +30,8 @@
<bindings>
<binding queue="foo" />
<binding queue="bar" />
<binding queue="spam" />
<binding queue="bucket" />
</bindings>
</fanout-exchange>
@@ -34,12 +40,16 @@
<bindings>
<binding queue="foo" key="type" value="foo" />
<binding queue="bar" key="type" value="bar" />
<binding queue="spam" key="type" value="spam" />
<binding queue="bucket" key="type" value="bucket" />
</bindings>
</headers-exchange>
<rabbit:queue name="foo" />
<rabbit:queue name="bar" />
<rabbit:queue id="spam" name="bar" />
<rabbit:queue id="bucket" />
<rabbit:admin id="admin-test" connection-factory="connectionFactory"/>