This is an automated email from the ASF dual-hosted git repository.

jlmonteiro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git


The following commit(s) were added to refs/heads/main by this push:
     new 6871226ff1 feat(tck): Add Jakarta Messaging 3.1 TCK with some fixes 
(#1712)
6871226ff1 is described below

commit 6871226ff1f686c1db62bbff3e5b34eedb3d86b5
Author: Jean-Louis Monteiro <[email protected]>
AuthorDate: Wed Mar 4 11:35:09 2026 +0100

    feat(tck): Add Jakarta Messaging 3.1 TCK with some fixes (#1712)
    
    * feat(tck): adding Jakarta Messaging 3.1 TCK (#1713)
    
    * feat(client): update JMS version metadata to Jakarta Messaging 3.1 (#1718)
    
    ActiveMQ reports JMS version as 1.1 (JMS 1.1 era). Since the project
    now uses jakarta.jms (Jakarta Messaging 3.1), the metadata must match.
    
    - getJMSVersion(): "1.1" → "3.1"
    - getJMSMajorVersion(): 1 → 3
    
    * fix(client): use correct exception types per Jakarta Messaging spec 
(#1722) (#1724) (#1719)
    
    Several methods threw wrong exception types, causing TCK failures:
    
    - MessageProducer.send() with null destination on an unbound producer
      threw UnsupportedOperationException instead of InvalidDestinationException
    - JMSProducer.send() with null message threw NPE instead of
      MessageFormatRuntimeException
    - checkClientIDWasManuallySpecified() threw JMSException instead of
      jakarta.jms.IllegalStateException for durable subscribers without
      a client ID
    
    * fix(client): validate inputs before UnsupportedOperationException in 
shared consumer methods (#1719)
    
    The shared consumer methods (createSharedConsumer, 
createSharedDurableConsumer)
    threw UnsupportedOperationException immediately without validating inputs.
    Per the JMS spec, parameter validation must happen first:
    
    - Session: null topic now throws InvalidDestinationException, invalid
      selector now throws InvalidSelectorException
    - JMSContext: null topic now throws InvalidDestinationRuntimeException
    
    This fixes the TCK invalidDestinationExceptionTests and
    invalidSelectorExceptionTopicTests for Session, and the corresponding
    JMSContext tests.
    
    fix(client): always include JMSXDeliveryCount in message property names 
(#1720)
    
    getPropertyNames() only included JMSXDeliveryCount when
    redeliveryCounter != 0. Since JMSXDeliveryCount = redeliveryCounter + 1,
    it is always >= 1 on a received message and should always be enumerated.
    
    The TCK sets 17 custom properties and expects getPropertyNames() to
    return 18 (17 + JMSXDeliveryCount).
    
    * fix(tests): the spec requires InvalidDestinationException when passing 
null topic. So updating the call but we could instead adapt the expected 
exception. Anyways, this is a placeholder for the tests of the new features for 
JMS
---
 .../org/apache/activemq/ActiveMQConnection.java    |   2 +-
 .../activemq/ActiveMQConnectionMetaData.java       |   4 +-
 .../java/org/apache/activemq/ActiveMQContext.java  |  19 ++-
 .../apache/activemq/ActiveMQMessageProducer.java   |   2 +-
 .../java/org/apache/activemq/ActiveMQProducer.java |   3 +
 .../java/org/apache/activemq/ActiveMQSession.java  |  23 +++
 .../apache/activemq/command/ActiveMQMessage.java   |   4 +-
 .../activemq-jakarta-messaging-tck/.gitignore      |   2 +
 .../activemq-jakarta-messaging-tck/README.adoc     |  73 ++++++++
 .../activemq-jakarta-messaging-tck/pom.xml         | 131 ++++++++++++++
 .../activemq-jakarta-messaging-tck/run_tck.sh      | 168 ++++++++++++++++++
 .../activemq/tck/JNDIInitialContextFactory.java    | 188 +++++++++++++++++++++
 .../activemq-jakarta-messaging-tck/ts.jte          | 110 ++++++++++++
 .../activemq-jakarta-messaging-tck/ts.jtx          |  24 +++
 activemq-tooling/pom.xml                           |   1 +
 .../activemq/command/ActiveMQMessageTest.java      |   7 +-
 .../activemq/jms2/ActiveMQJMS2ContextTest.java     |   8 +-
 pom.xml                                            |   1 +
 18 files changed, 755 insertions(+), 15 deletions(-)

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 b9f0eebf9a..8600861675 100644
--- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java
+++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java
@@ -1362,7 +1362,7 @@ public class ActiveMQConnection implements Connection, 
TopicConnection, QueueCon
      */
     public void checkClientIDWasManuallySpecified() throws JMSException {
         if (!userSpecifiedClientID) {
-            throw new JMSException("You cannot create a durable subscriber 
without specifying a unique clientID on a Connection");
+            throw new IllegalStateException("You cannot create a durable 
subscriber without specifying a unique clientID on a Connection");
         }
     }
 
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionMetaData.java
 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionMetaData.java
index 0e01e298ed..ade33c062d 100644
--- 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionMetaData.java
+++ 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnectionMetaData.java
@@ -73,7 +73,7 @@ public final class ActiveMQConnectionMetaData implements 
ConnectionMetaData {
      */
     @Override
     public String getJMSVersion() {
-        return "1.1";
+        return "3.1";
     }
 
     /**
@@ -83,7 +83,7 @@ public final class ActiveMQConnectionMetaData implements 
ConnectionMetaData {
      */
     @Override
     public int getJMSMajorVersion() {
-        return 1;
+        return 3;
     }
 
     /**
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQContext.java 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQContext.java
index 0e4295c37c..72ef853313 100644
--- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQContext.java
+++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQContext.java
@@ -25,6 +25,7 @@ import jakarta.jms.ConnectionMetaData;
 import jakarta.jms.Destination;
 import jakarta.jms.ExceptionListener;
 import jakarta.jms.IllegalStateRuntimeException;
+import jakarta.jms.InvalidDestinationRuntimeException;
 import jakarta.jms.JMSConsumer;
 import jakarta.jms.JMSContext;
 import jakarta.jms.JMSException;
@@ -442,21 +443,37 @@ public class ActiveMQContext implements JMSContext {
 
     @Override
     public JMSConsumer createSharedDurableConsumer(Topic topic, String name) {
+        checkContextState();
+        if (topic == null) {
+            throw new InvalidDestinationRuntimeException("Topic cannot be 
null");
+        }
         throw new 
UnsupportedOperationException("createSharedDurableConsumer(topic, name) is not 
supported");
     }
 
     @Override
     public JMSConsumer createSharedDurableConsumer(Topic topic, String name, 
String messageSelector) {
-        throw new UnsupportedOperationException("createDurableConsumer(topic, 
name, messageSelector) is not supported");
+        checkContextState();
+        if (topic == null) {
+            throw new InvalidDestinationRuntimeException("Topic cannot be 
null");
+        }
+        throw new 
UnsupportedOperationException("createSharedDurableConsumer(topic, name, 
messageSelector) is not supported");
     }
 
     @Override
     public JMSConsumer createSharedConsumer(Topic topic, String 
sharedSubscriptionName) {
+        checkContextState();
+        if (topic == null) {
+            throw new InvalidDestinationRuntimeException("Topic cannot be 
null");
+        }
         throw new UnsupportedOperationException("createSharedConsumer(topic, 
sharedSubscriptionName) is not supported");
     }
 
     @Override
     public JMSConsumer createSharedConsumer(Topic topic, String 
sharedSubscriptionName, String messageSelector) {
+        checkContextState();
+        if (topic == null) {
+            throw new InvalidDestinationRuntimeException("Topic cannot be 
null");
+        }
         throw new UnsupportedOperationException("createSharedConsumer(topic, 
sharedSubscriptionName, messageSelector) is not supported");
     }
 
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java
 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java
index 185ebffd41..fbd093f10f 100644
--- 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java
+++ 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java
@@ -294,7 +294,7 @@ public class ActiveMQMessageProducer extends 
ActiveMQMessageProducerSupport impl
         checkClosed();
         if (destination == null) {
             if (info.getDestination() == null) {
-                throw new UnsupportedOperationException("A destination must be 
specified.");
+                throw new InvalidDestinationException("A destination must be 
specified.");
             }
             throw new InvalidDestinationException("Don't understand null 
destinations");
         }
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java
index dc7311981e..abf7493024 100644
--- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java
+++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java
@@ -64,6 +64,9 @@ public class ActiveMQProducer implements JMSProducer {
 
     @Override
     public JMSProducer send(Destination destination, Message message) {
+        if (message == null) {
+            throw new MessageFormatRuntimeException("Message must not be 
null");
+        }
         try {
             if(this.correlationId != null) {
                 message.setJMSCorrelationID(this.correlationId);
diff --git 
a/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java 
b/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java
index 766005f287..dc20685041 100644
--- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java
+++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java
@@ -58,6 +58,7 @@ import jakarta.jms.TopicSession;
 import jakarta.jms.TopicSubscriber;
 import jakarta.jms.TransactionRolledBackException;
 
+import org.apache.activemq.selector.SelectorParser;
 import org.apache.activemq.blob.BlobDownloader;
 import org.apache.activemq.blob.BlobTransferPolicy;
 import org.apache.activemq.blob.BlobUploader;
@@ -1386,11 +1387,22 @@ public class ActiveMQSession implements Session, 
QueueSession, TopicSession, Sta
    
     @Override
     public MessageConsumer createSharedConsumer(Topic topic, String 
sharedSubscriptionName) throws JMSException {
+        checkClosed();
+        if (topic == null) {
+            throw new InvalidDestinationException("Topic cannot be null");
+        }
         throw new UnsupportedOperationException("createSharedConsumer(Topic, 
sharedSubscriptionName) is not supported");
     }
 
     @Override
     public MessageConsumer createSharedConsumer(Topic topic, String 
sharedSubscriptionName, String messageSelector) throws JMSException {
+        checkClosed();
+        if (topic == null) {
+            throw new InvalidDestinationException("Topic cannot be null");
+        }
+        if (messageSelector != null && !messageSelector.trim().isEmpty()) {
+            SelectorParser.parse(messageSelector);
+        }
         throw new UnsupportedOperationException("createSharedConsumer(Topic, 
sharedSubscriptionName, messageSelector) is not supported");
     }
 
@@ -1408,11 +1420,22 @@ public class ActiveMQSession implements Session, 
QueueSession, TopicSession, Sta
 
     @Override
     public MessageConsumer createSharedDurableConsumer(Topic topic, String 
name) throws JMSException {
+        checkClosed();
+        if (topic == null) {
+            throw new InvalidDestinationException("Topic cannot be null");
+        }
         throw new 
UnsupportedOperationException("createSharedDurableConsumer(Topic, name) is not 
supported");
     }
 
     @Override
     public MessageConsumer createSharedDurableConsumer(Topic topic, String 
name, String messageSelector) throws JMSException {
+        checkClosed();
+        if (topic == null) {
+            throw new InvalidDestinationException("Topic cannot be null");
+        }
+        if (messageSelector != null && !messageSelector.trim().isEmpty()) {
+            SelectorParser.parse(messageSelector);
+        }
         throw new 
UnsupportedOperationException("createSharedDurableConsumer(Topic, name, 
messageSelector) is not supported");
     }
 
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 2b9c86dc17..f9eb371e58 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
@@ -314,9 +314,7 @@ public class ActiveMQMessage extends Message implements 
org.apache.activemq.Mess
     public Enumeration getPropertyNames() throws JMSException {
         try {
             Vector<String> result = new 
Vector<String>(this.getProperties().keySet());
-            if( getRedeliveryCounter()!=0 ) {
-                result.add("JMSXDeliveryCount");
-            }
+            result.add("JMSXDeliveryCount");
             if( getGroupID()!=null ) {
                 result.add("JMSXGroupID");
             }
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/.gitignore 
b/activemq-tooling/activemq-jakarta-messaging-tck/.gitignore
new file mode 100644
index 0000000000..6c978fb3ea
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/.gitignore
@@ -0,0 +1,2 @@
+jakarta-messaging-tck-*.zip
+target/
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/README.adoc 
b/activemq-tooling/activemq-jakarta-messaging-tck/README.adoc
new file mode 100644
index 0000000000..7064dc8eb5
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/README.adoc
@@ -0,0 +1,73 @@
+////
+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.
+////
+= Jakarta Messaging TCK 3.1.0 Runner
+:toc:
+
+This module runs the official 
https://jakarta.ee/specifications/messaging/3.1/[Jakarta Messaging 3.1.0 TCK] 
against Apache ActiveMQ using an embedded broker.
+
+== Prerequisites
+
+* **JDK 17 or 21** — the TCK harness does not compile on JDK 25 (`javax.rmi` 
removed)
+* **Apache Ant** — must be on `PATH` (e.g. `apt-get install ant`)
+* **curl** and **unzip** — for downloading and extracting the TCK
+
+== How It Works
+
+The module builds a shaded (fat) JAR containing the ActiveMQ broker, client, 
KahaDB store, and a custom `JNDIInitialContextFactory`.
+The `run_tck.sh` script then:
+
+1. Downloads the TCK ZIP from `download.eclipse.org` (cached after first 
download)
+2. Verifies the SHA-256 checksum
+3. Extracts and patches the TCK source for JDK 17+ compatibility
+4. Builds the TCK test classes with Ant
+5. Runs all tests via `ant runclient`
+
+Each forked test JVM starts an embedded ActiveMQ broker over `vm://localhost` 
(non-persistent, no JMX) through the JNDI factory.
+
+== Build
+
+[source,bash]
+----
+mvn clean package -pl activemq-tooling/activemq-jakarta-messaging-tck -am 
-DskipTests
+----
+
+== Run the TCK
+
+[source,bash]
+----
+mvn verify -pl activemq-tooling/activemq-jakarta-messaging-tck -Prun-tck
+----
+
+If your default JDK is 25, point to a 17 or 21 installation:
+
+[source,bash]
+----
+JAVA_HOME=/path/to/jdk17 mvn verify -pl 
activemq-tooling/activemq-jakarta-messaging-tck -Prun-tck
+----
+
+Results are written to `target/tck-report/` and `target/tck-work/`.
+
+== Configuration
+
+`ts.jte`::
+TCK environment properties — classpaths, JNDI factory, timeout factor, test 
execution command.
+`jms.home` and `jms.classes` are appended automatically by the script.
+
+`ts.jtx`::
+Test exclusion list.
+Add entries in the format `com/sun/ts/tests/jms/path/Class#method` to skip 
known-failing tests.
+
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/pom.xml 
b/activemq-tooling/activemq-jakarta-messaging-tck/pom.xml
new file mode 100644
index 0000000000..b587a79f9f
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/pom.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.activemq.tooling</groupId>
+    <artifactId>activemq-tooling</artifactId>
+    <version>6.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>activemq-jakarta-messaging-tck</artifactId>
+  <packaging>jar</packaging>
+  <name>ActiveMQ :: Tooling :: Jakarta Messaging TCK Runner</name>
+  <description>Runs the Jakarta Messaging 3.1.0 TCK against 
ActiveMQ</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.activemq</groupId>
+      <artifactId>activemq-broker</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.activemq</groupId>
+      <artifactId>activemq-client</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.activemq</groupId>
+      <artifactId>activemq-kahadb-store</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j2-impl</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <skipTests>true</skipTests>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+              <transformers>
+                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                <transformer 
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
+                  <resource>META-INF/spring.handlers</resource>
+                </transformer>
+                <transformer 
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
+                  <resource>META-INF/spring.schemas</resource>
+                </transformer>
+                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
+                  <projectName>Apache ActiveMQ</projectName>
+                </transformer>
+              </transformers>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>run-tck</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>run-tck</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <configuration>
+                  <executable>bash</executable>
+                  <workingDirectory>${project.basedir}</workingDirectory>
+                  <arguments>
+                    <argument>run_tck.sh</argument>
+                    <argument>ts.jte</argument>
+                  </arguments>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+</project>
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/run_tck.sh 
b/activemq-tooling/activemq-jakarta-messaging-tck/run_tck.sh
new file mode 100755
index 0000000000..e8cdd2f993
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/run_tck.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+# 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.
+
+set -euo pipefail
+
+# Force C/POSIX locale to avoid issues with locale-specific number formatting
+# (e.g. French locale uses comma as decimal separator which breaks JavaTest 
harness)
+export LANG=en_US.UTF-8
+export LC_ALL=en_US.UTF-8
+
+JTE_FILE="${1:?Usage: run_tck.sh <ts.jte>}"
+
+TCK_VERSION="3.1.0"
+TCK_SPEC_VERSION="3.1"
+TCK_ZIP="jakarta-messaging-tck-${TCK_VERSION}.zip"
+TCK_URL="https://download.eclipse.org/jakartaee/messaging/${TCK_SPEC_VERSION}/${TCK_ZIP}";
+TCK_SHA256="3ea9e4d9eb6c7ebd2f60f920c1e76ea2e2928540b0eee78120a18453d5851644"
+
+TARGET_DIR="$(pwd)/target"
+TCK_DIR="${TARGET_DIR}/messaging-tck"
+SHADED_JAR=$(find "${TARGET_DIR}" -maxdepth 1 -name 
"activemq-jakarta-messaging-tck-*.jar" ! -name "*-sources*" ! -name 
"*-javadoc*" ! -name "original-*" | head -1)
+
+if [ -z "${SHADED_JAR}" ]; then
+    echo "ERROR: Shaded JAR not found in ${TARGET_DIR}. Run 'mvn package' 
first."
+    exit 1
+fi
+echo "Using shaded JAR: ${SHADED_JAR}"
+
+# Download TCK if not already present
+if [ ! -f "${TCK_ZIP}" ]; then
+    echo "Downloading Jakarta Messaging TCK ${TCK_VERSION}..."
+    curl -fSL -o "${TCK_ZIP}" "${TCK_URL}"
+fi
+
+# Verify checksum
+echo "Verifying SHA-256 checksum..."
+if command -v sha256sum &>/dev/null; then
+    ACTUAL_SHA256=$(sha256sum "${TCK_ZIP}" | awk '{print $1}')
+else
+    ACTUAL_SHA256=$(shasum -a 256 "${TCK_ZIP}" | awk '{print $1}')
+fi
+if [ "${ACTUAL_SHA256}" != "${TCK_SHA256}" ]; then
+    echo "ERROR: SHA-256 checksum mismatch!"
+    echo "  Expected: ${TCK_SHA256}"
+    echo "  Actual:   ${ACTUAL_SHA256}"
+    echo "Deleting corrupt download."
+    rm -f "${TCK_ZIP}"
+    exit 1
+fi
+echo "Checksum verified."
+
+# Extract TCK
+if [ ! -d "${TCK_DIR}" ]; then
+    echo "Extracting TCK to ${TCK_DIR}..."
+    mkdir -p "${TCK_DIR}"
+    unzip -q -o "${TCK_ZIP}" -d "${TCK_DIR}"
+fi
+
+# Find the extracted TCK root (may be nested in a subdirectory)
+TCK_ROOT=$(find "${TCK_DIR}" -mindepth 1 -maxdepth 1 -type d -name 
"messaging-tck" | head -1)
+if [ -z "${TCK_ROOT}" ]; then
+    # Try without nesting
+    if [ -d "${TCK_DIR}/bin" ]; then
+        TCK_ROOT="${TCK_DIR}"
+    else
+        echo "ERROR: Could not locate TCK root directory under ${TCK_DIR}"
+        ls -la "${TCK_DIR}"
+        exit 1
+    fi
+fi
+echo "TCK root: ${TCK_ROOT}"
+
+# Copy JTE and JTX configuration files
+echo "Configuring TCK..."
+cp "${JTE_FILE}" "${TCK_ROOT}/bin/ts.jte"
+if [ -f "ts.jtx" ]; then
+    cp "ts.jtx" "${TCK_ROOT}/bin/ts.jtx"
+fi
+
+# Append dynamic paths to ts.jte
+{
+    echo ""
+    echo "jms.home=${TCK_ROOT}"
+    echo "jms.classes=${SHADED_JAR}"
+} >> "${TCK_ROOT}/bin/ts.jte"
+
+# Ensure Ant is available (use system Ant; TCK does not bundle one)
+if ! command -v ant &>/dev/null; then
+    echo "ERROR: Apache Ant is required but not found on PATH."
+    echo "Install it with: apt-get install ant  (or equivalent for your OS)"
+    exit 1
+fi
+
+# ANT_HOME must be set for the TCK harness classpath (${ant.home}/lib/ant.jar)
+if [ -z "${ANT_HOME:-}" ]; then
+    # Derive ANT_HOME from the ant binary location
+    ANT_BIN="$(command -v ant)"
+    ANT_REAL="$(readlink -f "${ANT_BIN}" 2>/dev/null || realpath "${ANT_BIN}" 
2>/dev/null || echo "${ANT_BIN}")"
+    export ANT_HOME="${ANT_REAL%/bin/ant}"
+    # On Debian/Ubuntu, ant is a shell script in /usr/bin but jars are in 
/usr/share/ant
+    if [ ! -f "${ANT_HOME}/lib/ant.jar" ] && [ -f "/usr/share/ant/lib/ant.jar" 
]; then
+        export ANT_HOME="/usr/share/ant"
+    fi
+fi
+
+echo "Using ANT_HOME: ${ANT_HOME}"
+echo "Ant version: $(ant -version)"
+
+# Create required temp directories and files
+mkdir -p /tmp/ri_admin_objects "${TARGET_DIR}/tck-work" 
"${TARGET_DIR}/tck-report" 2>/dev/null || true
+# Create password file for RI porting compatibility
+echo "admin" > /tmp/ripassword
+
+# Patch TCK source for JDK 17+ compatibility
+# javax.rmi.PortableRemoteObject was removed in JDK 14+
+TSNAMING="${TCK_ROOT}/src/com/sun/ts/lib/util/TSNamingContext.java"
+if grep -q "javax.rmi.PortableRemoteObject" "${TSNAMING}" 2>/dev/null; then
+    echo "Patching TSNamingContext.java for JDK 17+ compatibility..."
+    sed -i.bak 's|import javax\.rmi\.PortableRemoteObject;|// Removed: 
javax.rmi unavailable on JDK 17+|' "${TSNAMING}"
+    sed -i.bak 's|return c == null ? o : PortableRemoteObject\.narrow(o, 
c);|return c == null ? o : c.cast(o);|' "${TSNAMING}"
+    rm -f "${TSNAMING}.bak"
+fi
+
+# Build the TCK test classes first
+echo ""
+echo "============================================"
+echo "Building Jakarta Messaging TCK ${TCK_VERSION}"
+echo "============================================"
+echo ""
+
+cd "${TCK_ROOT}/bin"
+ant build.all 2>&1 | tail -20
+echo "TCK build complete."
+
+# Run the TCK tests using same-JVM mode to avoid security manager issues on 
Java 17+
+echo ""
+echo "============================================"
+echo "Running Jakarta Messaging TCK ${TCK_VERSION}"
+echo "============================================"
+echo ""
+
+cd "${TCK_ROOT}/src/com/sun/ts/tests/jms"
+ant \
+    -Dwork.dir="${TARGET_DIR}/tck-work" \
+    -Dreport.dir="${TARGET_DIR}/tck-report" \
+    runclient
+TCK_EXIT=$?
+
+echo ""
+echo "============================================"
+echo "TCK execution finished with exit code: ${TCK_EXIT}"
+echo "Report: ${TARGET_DIR}/tck-report"
+echo "============================================"
+
+exit ${TCK_EXIT}
diff --git 
a/activemq-tooling/activemq-jakarta-messaging-tck/src/main/java/org/apache/activemq/tck/JNDIInitialContextFactory.java
 
b/activemq-tooling/activemq-jakarta-messaging-tck/src/main/java/org/apache/activemq/tck/JNDIInitialContextFactory.java
new file mode 100644
index 0000000000..a95a86eeb2
--- /dev/null
+++ 
b/activemq-tooling/activemq-jakarta-messaging-tck/src/main/java/org/apache/activemq/tck/JNDIInitialContextFactory.java
@@ -0,0 +1,188 @@
+/**
+ * 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.tck;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.spi.InitialContextFactory;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.command.ActiveMQTopic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JNDI InitialContextFactory for the Jakarta Messaging 3.1.0 TCK.
+ * <p>
+ * Starts an embedded ActiveMQ broker (non-persistent, no JMX) on first lookup
+ * and provides the JNDI names required by the TCK test harness.
+ */
+public class JNDIInitialContextFactory implements InitialContextFactory {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(JNDIInitialContextFactory.class);
+
+    private static final String BROKER_URL = "vm://localhost";
+
+    private static volatile BrokerService broker;
+    private static final Object BROKER_LOCK = new Object();
+
+    private static final Set<String> QUEUE_NAMES = Set.of(
+        "MY_QUEUE", "MY_QUEUE2",
+        "testQ0", "testQ1", "testQ2",
+        "testQueue2", "Q2"
+    );
+
+    private static final Set<String> TOPIC_NAMES = Set.of(
+        "MY_TOPIC", "MY_TOPIC2",
+        "testT0", "testT1", "testT2"
+    );
+
+    private static final Set<String> CONNECTION_FACTORY_NAMES = Set.of(
+        "MyConnectionFactory",
+        "MyQueueConnectionFactory",
+        "MyTopicConnectionFactory"
+    );
+
+    private static final String DURABLE_SUB_CF = 
"DURABLE_SUB_CONNECTION_FACTORY";
+
+    @Override
+    public Context getInitialContext(final Hashtable<?, ?> environment) throws 
NamingException {
+        ensureBrokerStarted();
+
+        final Map<String, Object> bindings = new ConcurrentHashMap<>();
+
+        // Connection factories
+        for (final String name : CONNECTION_FACTORY_NAMES) {
+            bindings.put(name, createConnectionFactory(null));
+        }
+        bindings.put(DURABLE_SUB_CF, createConnectionFactory("cts"));
+
+        // Queues
+        for (final String name : QUEUE_NAMES) {
+            bindings.put(name, new ActiveMQQueue(name));
+        }
+
+        // Topics
+        for (final String name : TOPIC_NAMES) {
+            bindings.put(name, new ActiveMQTopic(name));
+        }
+
+        return createContextProxy(bindings);
+    }
+
+    private static ActiveMQConnectionFactory createConnectionFactory(final 
String clientId) {
+        final ActiveMQConnectionFactory factory = new 
ActiveMQConnectionFactory(BROKER_URL);
+        factory.setNestedMapAndListEnabled(false);
+        if (clientId != null) {
+            factory.setClientID(clientId);
+        }
+        return factory;
+    }
+
+    private static void ensureBrokerStarted() {
+        if (broker != null) {
+            return;
+        }
+        synchronized (BROKER_LOCK) {
+            if (broker != null) {
+                return;
+            }
+            try {
+                final BrokerService bs = new BrokerService();
+                bs.setBrokerName("localhost");
+                bs.setPersistent(false);
+                bs.setUseJmx(false);
+                bs.setAdvisorySupport(false);
+                bs.start();
+                bs.waitUntilStarted();
+                broker = bs;
+                LOG.info("Embedded ActiveMQ broker started for TCK");
+
+                Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                    try {
+                        broker.stop();
+                        broker.waitUntilStopped();
+                    } catch (final Exception e) {
+                        LOG.warn("Error stopping embedded broker", e);
+                    }
+                }, "activemq-tck-shutdown"));
+            } catch (final Exception e) {
+                throw new RuntimeException("Failed to start embedded ActiveMQ 
broker", e);
+            }
+        }
+    }
+
+    private static Context createContextProxy(final Map<String, Object> 
bindings) {
+        return (Context) Proxy.newProxyInstance(
+            JNDIInitialContextFactory.class.getClassLoader(),
+            new Class<?>[]{Context.class},
+            new ContextInvocationHandler(bindings)
+        );
+    }
+
+    private static final class ContextInvocationHandler implements 
InvocationHandler {
+        private final Map<String, Object> bindings;
+
+        ContextInvocationHandler(final Map<String, Object> bindings) {
+            this.bindings = bindings;
+        }
+
+        @Override
+        public Object invoke(final Object proxy, final Method method, final 
Object[] args) throws Throwable {
+            final String methodName = method.getName();
+
+            if ("lookup".equals(methodName) && args != null && args.length == 
1) {
+                final String name = args[0] instanceof javax.naming.Name
+                    ? ((javax.naming.Name) args[0]).toString()
+                    : (String) args[0];
+                final Object result = bindings.get(name);
+                if (result == null) {
+                    throw new NamingException("Name not found: " + name);
+                }
+                return result;
+            }
+
+            if ("close".equals(methodName)) {
+                return null;
+            }
+
+            if ("toString".equals(methodName)) {
+                return "ActiveMQ TCK JNDI Context" + bindings.keySet();
+            }
+
+            if ("hashCode".equals(methodName)) {
+                return System.identityHashCode(proxy);
+            }
+
+            if ("equals".equals(methodName)) {
+                return proxy == args[0];
+            }
+
+            throw new NamingException("Operation not supported: " + 
methodName);
+        }
+    }
+}
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/ts.jte 
b/activemq-tooling/activemq-jakarta-messaging-tck/ts.jte
new file mode 100644
index 0000000000..d98f4df1d0
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/ts.jte
@@ -0,0 +1,110 @@
+#
+# 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.
+#
+
+###############################################################################
+# Jakarta Messaging TCK 3.1.0 - Apache ActiveMQ configuration
+#
+# jms.home and jms.classes are appended dynamically by run_tck.sh
+###############################################################################
+
+# ---- Classpaths (harness, compile, runtime) --------------------------------
+ts.harness.classpath=${ts.home}/lib/javatest.jar${pathsep}${ts.home}/lib/tsharness.jar${pathsep}${ts.home}/lib/jmstck.jar${pathsep}${ant.home}/lib/ant.jar
+ts.classpath=${jms.classes}${pathsep}${ts.home}/lib/tsharness.jar${pathsep}${ts.home}/lib/jmstck.jar
+ts.run.classpath=${ts.home}/lib/tsharness.jar${pathsep}${ts.home}/lib/sigtest.jar${pathsep}${ts.home}/lib/jmstck.jar${pathsep}${ts.home}/classes${pathsep}${ts.home}/bin${pathsep}${jms.classes}
+
+# ---- Signature test settings -----------------------------------------------
+jimage.dir=${ts.home}/tmp/jdk-bundles
+sigTestClasspath=${jms.classes}${pathsep}${jimage.dir}/java.base${pathsep}${jimage.dir}/java.rmi${pathsep}${jimage.dir}/java.sql${pathsep}${jimage.dir}/java.naming
+
+# ---- Harness settings ------------------------------------------------------
+harness.temp.directory=${ts.home}/tmp
+harness.log.port=2001
+harness.log.traceflag=true
+harness.log.delayseconds=1
+harness.executeMode=2
+harness.socket.retry.count=10
+work.dir=/tmp/JTwork
+report.dir=/tmp/JTreport
+if.existing.work.report.dirs=auto
+
+# ---- Timezone --------------------------------------------------------------
+tz=UTC
+
+# ---- Timeout factor --------------------------------------------------------
+javatest.timeout.factor=2
+
+# ---- Build level (1=compile only) ------------------------------------------
+build.level=1
+
+# ---- Display (Unix) --------------------------------------------------------
+ts.display=:0.0
+
+# ---- Deliverable class -----------------------------------------------------
+deliverable.class=com.sun.ts.lib.deliverable.jms.JMSDeliverable
+
+# ---- Test execution: run tests in same JVM as harness ----------------------
+# This avoids forking a process with -Djava.security.manager which is
+# unsupported on Java 17+.
+command.testExecuteSameJVM=com.sun.ts.lib.harness.ExecuteTSTestSameJVMCmd \
+        $testExecuteClass $testExecuteArgs
+
+# ---- Test execution: forked JVM (no security manager) ----------------------
+command.testExecute=com.sun.ts.lib.harness.ExecTSTestCmd \
+        CLASSPATH=${ts.run.classpath} \
+        DISPLAY="${ts.display}" \
+        HOME="${user.home}" \
+        windir=${windir} \
+        SYSTEMROOT=${SYSTEMROOT} \
+        ${JAVA_HOME}/bin/java \
+        
-Djava.naming.factory.initial=org.apache.activemq.tck.JNDIInitialContextFactory 
\
+        -Ddeliverable.class=${deliverable.class} \
+        $testExecuteClass $testExecuteArgs
+
+env.ts_unix.menu=true
+env.ts_win32.menu=true
+
+# ---- JMS porting class (uses JNDI to look up administered objects) ---------
+porting.ts.jmsObjects.class.1=com.sun.ts.lib.implementation.sun.jms.SunRIJMSObjects
+
+# ---- JNDI configuration ---------------------------------------------------
+java.naming.factory.initial=org.apache.activemq.tck.JNDIInitialContextFactory
+
+# ---- JMS TCK settings -----------------------------------------------------
+jms_timeout=10000
+user=guest
+password=guest
+
+# ---- Platform mode (standalone = no Jakarta EE container) ------------------
+platform.mode=standalone
+
+# ---- TCK library name -----------------------------------------------------
+tslib.name=jmstck
+
+# ---- Implementation under test (use 'ri' porting layer) -------------------
+impl.vi=ri
+
+# ---- RI / vendor configuration (paths not used for ActiveMQ) --------------
+ri.home=${jms.home}
+ri.jars=${jms.classes}
+admin.user=admin
+admin.pass=admin
+admin.pass.file=/tmp/ripassword
+jndi.fs.dir=/tmp/ri_admin_objects
+jndi.factory.initial="java.naming.factory.initial=org.apache.activemq.tck.JNDIInitialContextFactory"
+jndi.provider.url="java.naming.provider.url=file:///${jndi.fs.dir}"
+
+# jms.home and jms.classes are appended dynamically by run_tck.sh
diff --git a/activemq-tooling/activemq-jakarta-messaging-tck/ts.jtx 
b/activemq-tooling/activemq-jakarta-messaging-tck/ts.jtx
new file mode 100644
index 0000000000..b55c2b40dc
--- /dev/null
+++ b/activemq-tooling/activemq-jakarta-messaging-tck/ts.jtx
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+
+# Jakarta Messaging TCK 3.1.0 - Test Exclusions for Apache ActiveMQ
+#
+# This file lists TCK tests to exclude from the run.
+# Format: <full-test-path>#<test-method>
+
+# TCK bug: variable never assigned before use
+com/sun/ts/tests/jms/core/messageFormatRuntimeExceptionTests#messageFormatRuntimeExceptionTests_from_standalone
diff --git a/activemq-tooling/pom.xml b/activemq-tooling/pom.xml
index 30070148e5..1160e3fb05 100644
--- a/activemq-tooling/pom.xml
+++ b/activemq-tooling/pom.xml
@@ -36,5 +36,6 @@
     <module>activemq-maven-plugin</module>
     <module>activemq-junit</module>
     <module>activemq-joram-jms-tests</module>
+    <module>activemq-jakarta-messaging-tck</module>
   </modules>
 </project>
diff --git 
a/activemq-unit-tests/src/test/java/org/apache/activemq/command/ActiveMQMessageTest.java
 
b/activemq-unit-tests/src/test/java/org/apache/activemq/command/ActiveMQMessageTest.java
index d560814578..87f1a54228 100644
--- 
a/activemq-unit-tests/src/test/java/org/apache/activemq/command/ActiveMQMessageTest.java
+++ 
b/activemq-unit-tests/src/test/java/org/apache/activemq/command/ActiveMQMessageTest.java
@@ -411,9 +411,10 @@ public class ActiveMQMessageTest extends TestCase {
             found3 |= element.equals(name3);
         }
         assertTrue("prop name1 found", found1);
-        // spec compliance, only non JMS (and JMSX) props returned
-        assertFalse("prop name2 not found", found2);
-        assertFalse("prop name4 not found", found3);
+        // JMSXDeliveryCount is always present per Jakarta Messaging 3.1 spec
+        assertTrue("prop name2 found", found2);
+        // JMS standard header fields (like JMSRedelivered) are not returned
+        assertFalse("prop name3 not found", found3);
     }
 
     @SuppressWarnings("rawtypes")
diff --git 
a/activemq-unit-tests/src/test/java/org/apache/activemq/jms2/ActiveMQJMS2ContextTest.java
 
b/activemq-unit-tests/src/test/java/org/apache/activemq/jms2/ActiveMQJMS2ContextTest.java
index cf1e0fa594..a1948be12e 100644
--- 
a/activemq-unit-tests/src/test/java/org/apache/activemq/jms2/ActiveMQJMS2ContextTest.java
+++ 
b/activemq-unit-tests/src/test/java/org/apache/activemq/jms2/ActiveMQJMS2ContextTest.java
@@ -268,22 +268,22 @@ public class ActiveMQJMS2ContextTest extends 
ActiveMQJMS2TestBase {
 
     @Test(expected = UnsupportedOperationException.class)
     public void testSessionSharedConsumer() throws JMSException {
-        session.createSharedConsumer(null, null);
+        session.createSharedConsumer(session.createTopic("test"), null);
     }
 
     @Test(expected = UnsupportedOperationException.class)
     public void testSessionSharedConsumerSelector() throws JMSException {
-        session.createSharedConsumer(null, null, null);
+        session.createSharedConsumer(session.createTopic("test"), null, null);
     }
 
     @Test(expected = UnsupportedOperationException.class)
     public void testSessionSharedDurableConsumer() throws JMSException {
-        session.createSharedDurableConsumer(null, null);
+        session.createSharedDurableConsumer(session.createTopic("test"), null);
     }
 
     @Test(expected = UnsupportedOperationException.class)
     public void testSessionSharedDurableConsumerSelector() throws JMSException 
{
-        session.createSharedDurableConsumer(null, null, null);
+        session.createSharedDurableConsumer(session.createTopic("test"), null, 
null);
     }
 
     @Test(expected = UnsupportedOperationException.class)
diff --git a/pom.xml b/pom.xml
index aebbf31047..b60c502cd4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1221,6 +1221,7 @@
             <exclude>**/webapp/decorators/footer.jsp</exclude>
             <exclude>**/docs/img/*.svg</exclude>
             <exclude>**/testJdbcConfig/**/*</exclude>
+            <exclude>**/jakarta-messaging-tck-*.zip</exclude>
           </excludes>
         </configuration>
       </plugin>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact



Reply via email to