diff --git a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java index b9f0eebf9ac..d84ce028ba0 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java @@ -141,6 +141,12 @@ public class ActiveMQConnection implements Connection, TopicConnection, QueueCon private RedeliveryPolicyMap redeliveryPolicyMap; private MessageTransformer transformer; + /** + * If set to true, strict Jakarta Messaging 3.1 compliance is enforced. + * This strictly rejects non-standard property types such as Character, Map, and List. + */ + private boolean strictCompliance = false; + private boolean disableTimeStampsByDefault; private boolean optimizedMessageDispatch = true; private boolean copyMessageOnSend = true; @@ -889,7 +895,7 @@ public ConnectionConsumer createSharedDurableConnectionConsumer(Topic topic, Str int maxMessages) throws JMSException { throw new UnsupportedOperationException("createSharedConnectionConsumer() is not supported"); } - + // Properties // ------------------------------------------------------------------------- @@ -1035,6 +1041,17 @@ public void setNestedMapAndListEnabled(boolean structuredMapsEnabled) { this.nestedMapAndListEnabled = structuredMapsEnabled; } + public boolean isStrictCompliance() { + return strictCompliance; + } + + /** + * Sets whether strict Jakarta Messaging compliance is enforced for this connection. + */ + public void setStrictCompliance(boolean strictCompliance) { + this.strictCompliance = strictCompliance; + } + public boolean isExclusiveConsumer() { return exclusiveConsumer; } diff --git a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionFactory.java b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionFactory.java index ae57d2624b6..b36f4a0d9da 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionFactory.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionFactory.java @@ -123,6 +123,12 @@ public class ActiveMQConnectionFactory extends JNDIBaseStorable implements Conne private BlobTransferPolicy blobTransferPolicy = new BlobTransferPolicy(); private MessageTransformer transformer; + /** + * If set to true, strict Jakarta Messaging 3.1 compliance is enforced. + * This strictly rejects non-standard property types such as Character, Map, and List. + */ + private boolean strictCompliance = false; + private boolean disableTimeStampsByDefault; private boolean optimizedMessageDispatch = true; private long optimizeAcknowledgeTimeOut = 300; @@ -408,6 +414,10 @@ protected ActiveMQConnection createActiveMQConnection(String userName, String pa protected ActiveMQConnection createActiveMQConnection(Transport transport, JMSStatsImpl stats) throws Exception { ActiveMQConnection connection = new ActiveMQConnection(transport, getClientIdGenerator(), getConnectionIdGenerator(), stats); + + // Copy the compliance flags from the Factory to the Connection + connection.setStrictCompliance(isStrictCompliance()); + return connection; } @@ -1000,6 +1010,17 @@ public void setNestedMapAndListEnabled(boolean structuredMapsEnabled) { this.nestedMapAndListEnabled = structuredMapsEnabled; } + public boolean isStrictCompliance() { + return strictCompliance; + } + + /** + * If this flag is set then the connection follows strict Jakarta Messaging compliance. + */ + public void setStrictCompliance(boolean strictCompliance) { + this.strictCompliance = strictCompliance; + } + public String getClientIDPrefix() { return clientIDPrefix; } diff --git a/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQMessage.java b/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQMessage.java index 2b9c86dc17b..75cdd56e592 100644 --- a/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQMessage.java +++ b/activemq-client/src/main/java/org/apache/activemq/command/ActiveMQMessage.java @@ -529,9 +529,18 @@ protected void checkValidObject(Object value) throws MessageFormatException { boolean valid = value instanceof Boolean || value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long; valid = valid || value instanceof Float || value instanceof Double || value instanceof Character || value instanceof String || value == null; - if (!valid) { + ActiveMQConnection conn = getConnection(); + + // strict rejection for Character + if (valid && conn != null && conn.isStrictCompliance() && value instanceof Character) { + throw new MessageFormatException("Character type not allowed under strict Jakarta 3.1 compliance"); + } - ActiveMQConnection conn = getConnection(); + if (!valid) { + // Check strict compliance at validation time without side-effecting the 'nested' flag + if (conn != null && conn.isStrictCompliance()) { + throw new MessageFormatException("Only objectified primitive objects and String types are allowed under strict Jakarta 3.1 compliance but was: " + value + " type: " + value.getClass()); + } // conn is null if we are in the broker rather than a JMS client if (conn == null || conn.isNestedMapAndListEnabled()) { if (!(value instanceof Map || value instanceof List)) { diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/ActiveMQMessagePropertyTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/ActiveMQMessagePropertyTest.java new file mode 100644 index 00000000000..1180c825a9a --- /dev/null +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/ActiveMQMessagePropertyTest.java @@ -0,0 +1,111 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.activemq; + +import org.apache.activemq.broker.BrokerService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import jakarta.jms.Connection; +import jakarta.jms.Message; +import jakarta.jms.MessageFormatException; +import jakarta.jms.Session; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.activemq.command.DataStructureTestSupport.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +public class ActiveMQMessagePropertyTest { + + private BrokerService broker; + private String connectionUri; + + @Before + public void setUp() throws Exception { + broker = new BrokerService(); + broker.setPersistent(false); + broker.setUseJmx(false); + broker.addConnector("vm://localhost"); + broker.start(); + broker.waitUntilStarted(); + connectionUri = broker.getTransportConnectors().get(0).getPublishableConnectString(); + } + + @After + public void tearDown() throws Exception { + if (broker != null) { + broker.stop(); + broker.waitUntilStopped(); + } + } + + @Test + public void testStrictComplianceMasterSwitch() throws Exception { + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://localhost"); + + // Turn on strict compliance + factory.setStrictCompliance(true); + // Explicitly turn on the legacy flag to prove strict mode overrides it + factory.setNestedMapAndListEnabled(true); + + Connection connection = factory.createConnection(); + connection.start(); + + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + Message message = session.createMessage(); + + // Verify standard types work + message.setStringProperty("validString", "test"); // Should pass + + // Verify Character is rejected + try { + message.setObjectProperty("invalidChar", 'A'); + fail("Should have rejected Character under strict compliance"); + } catch (MessageFormatException e) { + // Expected + } + + // Verify Map is rejected (even though nestedMapAndListEnabled is true) + try { + Map map = new HashMap<>(); + message.setObjectProperty("invalidMap", map); + fail("Should have rejected Map under strict compliance"); + } catch (MessageFormatException e) { + // Expected + } + + connection.close(); + } + + @Test + public void testLegacyModeStillAllowsCharacter() throws Exception { + ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri); + + // Default is strictCompliance = false + try (Connection connection = factory.createConnection(); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + + Message message = session.createMessage(); + message.setObjectProperty("charProp", 'A'); // Should pass + assertEquals('A', message.getObjectProperty("charProp")); + } + } +}