diff --git a/docs/user-manual/en/filter-expressions.md b/docs/user-manual/en/filter-expressions.md index ea7224ebb4..88b8b1b76f 100644 --- a/docs/user-manual/en/filter-expressions.md +++ b/docs/user-manual/en/filter-expressions.md @@ -49,3 +49,25 @@ refer to attributes of the core message in an expression: Any other identifiers used in core filter expressions will be assumed to be properties of the message. + +The JMS spec states that a String property should not get converted to a +numeric when used in a selector. So for example, if a message has the `age` +property set to String `21` then the following selector should not match +it: `age > 18`. Since Apache ActiveMQ Artemis supports STOMP clients which +can only send messages with string properties, that restriction is a bit +limiting. Therefore, if you want your filter expressions to auto-convert String +properties the the appropriate number type, just prefix it with +`convert_string_expressions:`. If you changed the filter expression in the +previous example to be `convert_string_expressions:age > 18`, then it would +match the aforementioned message. + +The JMS spec also states that property identifiers (and therefore the +identifiers which are valid for use in a filter expression) are an, +"unlimited-length sequence of letters and digits, the first of which must be +a letter. A letter is any character for which the method +`Character.isJavaLetter` returns `true`. This includes `_` and `$`. A letter +or digit is any character for which the method `Character.isJavaLetterOrDigit` +returns `true`." This constraint means that hyphens (i.e. `-`) cannot be used. +However, this constraint can be overcome by using the `hyphenated_props:` +prefix. For example, if a message had the `foo-bar` property set to `0` then +the filter expression `hyphenated_props:foo-bar = 0` would match it. \ No newline at end of file diff --git a/docs/user-manual/en/protocols-interoperability.md b/docs/user-manual/en/protocols-interoperability.md index abfdcf89fb..5c19311aab 100644 --- a/docs/user-manual/en/protocols-interoperability.md +++ b/docs/user-manual/en/protocols-interoperability.md @@ -311,6 +311,13 @@ seconds. > users can use heart-beats to maintain the life cycle of stomp > connections. +### Selector/Filter expressions + +Stomp subscribers can specify an expression used to select or filter +what the subscriber receives using the `selector` header. The filter +expression syntax follows the *core filter syntax* described in the +[Filter Expressions](filter-expressions.md) documentation. + ### Stomp and JMS interoperability #### Using JMS destinations diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/stomp/StompTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/stomp/StompTest.java index 41b5c35e70..81558987e9 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/stomp/StompTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/stomp/StompTest.java @@ -31,6 +31,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.api.core.client.ClientMessage; +import org.apache.activemq.artemis.api.core.client.ClientProducer; +import org.apache.activemq.artemis.api.core.client.ClientSession; +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; +import org.apache.activemq.artemis.api.core.client.ServerLocator; import org.apache.activemq.artemis.api.core.management.ResourceNames; import org.apache.activemq.artemis.api.jms.ActiveMQJMSClient; import org.apache.activemq.artemis.core.protocol.stomp.Stomp; @@ -435,6 +441,35 @@ public class StompTest extends StompTestBase { Assert.assertEquals("bar", "123", message.getStringProperty("bar")); } + @Test + public void testSendMessageWithCustomHeadersAndHyphenatedSelector() throws Exception { + + MessageConsumer consumer = session.createConsumer(queue, "hyphenated_props:b-ar = '123'"); + + String frame = "CONNECT\n" + "login: brianm\n" + "passcode: wombats\n\n" + Stomp.NULL; + sendFrame(frame); + + frame = receiveFrame(10000); + Assert.assertTrue(frame.startsWith("CONNECTED")); + + frame = "SEND\n" + "foo:abc\n" + + "b-ar:123\n" + + "destination:" + + getQueuePrefix() + + getQueueName() + + "\n\n" + + "Hello World" + + Stomp.NULL; + + sendFrame(frame); + + TextMessage message = (TextMessage) consumer.receive(1000); + Assert.assertNotNull(message); + Assert.assertEquals("Hello World", message.getText()); + Assert.assertEquals("foo", "abc", message.getStringProperty("foo")); + Assert.assertEquals("b-ar", "123", message.getStringProperty("b-ar")); + } + @Test public void testSendMessageWithStandardHeaders() throws Exception { @@ -755,6 +790,49 @@ public class StompTest extends StompTestBase { sendFrame(frame); } + + @Test + public void testSubscribeWithAutoAckAndHyphenatedSelector() throws Exception { + + String frame = "CONNECT\n" + "login: brianm\n" + "passcode: wombats\n\n" + Stomp.NULL; + sendFrame(frame); + + frame = receiveFrame(100000); + Assert.assertTrue(frame.startsWith("CONNECTED")); + + frame = "SUBSCRIBE\n" + "destination:" + + getQueuePrefix() + + getQueueName() + + "\n" + + "selector: hyphenated_props:foo-bar = 'zzz'\n" + + "ack:auto\n\n" + + Stomp.NULL; + sendFrame(frame); + + ServerLocator serverLocator = addServerLocator(ActiveMQClient.createServerLocator("vm://0")); + ClientSessionFactory clientSessionFactory = serverLocator.createSessionFactory(); + ClientSession clientSession = clientSessionFactory.createSession(true, true); + ClientProducer producer = clientSession.createProducer(getQueuePrefix() + getQueueName()); + + ClientMessage ignoredMessage = clientSession.createMessage(false); + ignoredMessage.putStringProperty("foo-bar", "1234"); + ignoredMessage.getBodyBuffer().writeNullableSimpleString(SimpleString.toSimpleString("Ignored message")); + + ClientMessage realMessage = clientSession.createMessage(false); + realMessage.putStringProperty("foo-bar", "zzz"); + realMessage.getBodyBuffer().writeNullableSimpleString(SimpleString.toSimpleString("Real message")); + + producer.send(ignoredMessage); + producer.send(realMessage); + + frame = receiveFrame(10000); + Assert.assertTrue(frame.startsWith("MESSAGE")); + Assert.assertTrue("Should have received the real message but got: " + frame, frame.indexOf("Real message") > 0); + + frame = "DISCONNECT\n" + "\n\n" + Stomp.NULL; + sendFrame(frame); + } + @Test public void testSubscribeWithClientAck() throws Exception {