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..86008616759 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 QueueSession createQueueSession(boolean transacted, int acknowledgeMode) */ 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 0e01e298ed9..ade33c062d1 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 @@ private ActiveMQConnectionMetaData() { */ @Override public String getJMSVersion() { - return "1.1"; + return "3.1"; } /** @@ -83,7 +83,7 @@ public String getJMSVersion() { */ @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 0e4295c37c0..72ef853313b 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.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 JMSConsumer createDurableConsumer(Topic topic, String name, String messag @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 185ebffd41b..fbd093f10f2 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 void send(Destination destination, Message message, int deliveryMode, int 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 dc7311981e4..abf74930242 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 766005f2872..dc206850415 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.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 Topic createTopic(String topicName) throws JMSException { @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 MessageConsumer createDurableConsumer(Topic topic, String name, String me @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 2b9c86dc17b..f9eb371e58e 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 boolean propertyExists(String name) throws JMSException { public Enumeration getPropertyNames() throws JMSException { try { Vector result = new Vector(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 00000000000..6c978fb3eaa --- /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 00000000000..7064dc8eb50 --- /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 00000000000..b587a79f9f5 --- /dev/null +++ b/activemq-tooling/activemq-jakarta-messaging-tck/pom.xml @@ -0,0 +1,131 @@ + + + + + 4.0.0 + + + org.apache.activemq.tooling + activemq-tooling + 6.3.0-SNAPSHOT + + + activemq-jakarta-messaging-tck + jar + ActiveMQ :: Tooling :: Jakarta Messaging TCK Runner + Runs the Jakarta Messaging 3.1.0 TCK against ActiveMQ + + + + org.apache.activemq + activemq-broker + + + org.apache.activemq + activemq-client + + + org.apache.activemq + activemq-kahadb-store + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + META-INF/spring.handlers + + + META-INF/spring.schemas + + + Apache ActiveMQ + + + + + + + + + + + + run-tck + + + + org.codehaus.mojo + exec-maven-plugin + + + run-tck + verify + + exec + + + bash + ${project.basedir} + + run_tck.sh + ts.jte + + + + + + + + + + + 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 00000000000..e8cdd2f9936 --- /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 }" + +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 00000000000..a95a86eeb23 --- /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. + *

+ * 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 QUEUE_NAMES = Set.of( + "MY_QUEUE", "MY_QUEUE2", + "testQ0", "testQ1", "testQ2", + "testQueue2", "Q2" + ); + + private static final Set TOPIC_NAMES = Set.of( + "MY_TOPIC", "MY_TOPIC2", + "testT0", "testT1", "testT2" + ); + + private static final Set 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 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 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 bindings; + + ContextInvocationHandler(final Map 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 00000000000..d98f4df1d07 --- /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 00000000000..b55c2b40dcc --- /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: # + +# 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 30070148e52..1160e3fb059 100644 --- a/activemq-tooling/pom.xml +++ b/activemq-tooling/pom.xml @@ -36,5 +36,6 @@ activemq-maven-plugin activemq-junit activemq-joram-jms-tests + activemq-jakarta-messaging-tck 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 d5608145786..87f1a54228a 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 void testGetPropertyNames() throws JMSException { 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 cf1e0fa594c..a1948be12ee 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 void testSessionDurableConsumerSelectorNoLocal() throws JMSException { @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 58a3aaf78a0..48f2751cc30 100644 --- a/pom.xml +++ b/pom.xml @@ -1207,6 +1207,7 @@ **/webapp/decorators/footer.jsp **/docs/img/*.svg **/testJdbcConfig/**/* + **/jakarta-messaging-tck-*.zip