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