diff --git a/activemq-broker/src/test/java/org/apache/activemq/bugs/AMQ7085Test.java b/activemq-broker/src/test/java/org/apache/activemq/bugs/AMQ7085Test.java index 630fb560f5e..e4381ca35c8 100644 --- a/activemq-broker/src/test/java/org/apache/activemq/bugs/AMQ7085Test.java +++ b/activemq-broker/src/test/java/org/apache/activemq/bugs/AMQ7085Test.java @@ -62,7 +62,7 @@ public void setUp() throws Exception { final Message toSend = session.createMessage(); toSend.setStringProperty("foo", "bar"); final MessageProducer producer = session.createProducer(queue); - producer.send(queue, toSend); + producer.send(toSend); } finally { conn.close(); } 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..bd75cb97811 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQConnection.java @@ -609,6 +609,8 @@ public void start() throws JMSException { */ @Override public void stop() throws JMSException { + ActiveMQSession.checkNotInCompletionListenerCallback("stop"); + ActiveMQSession.checkNotInMessageListenerCallback("stop"); doStop(true); } @@ -677,6 +679,8 @@ void doStop(boolean checkClosed) throws JMSException { */ @Override public void close() throws JMSException { + ActiveMQSession.checkNotInCompletionListenerCallback("close"); + ActiveMQSession.checkNotInMessageListenerCallback("close"); try { // If we were running, lets stop first. if (!closed.get() && !transportFailed.get()) { @@ -1362,7 +1366,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/ActiveMQMessageConsumer.java b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageConsumer.java index a6bf1b20952..469e04d43e8 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageConsumer.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageConsumer.java @@ -1454,7 +1454,12 @@ public void dispatch(MessageDispatch md) { try { boolean expired = isConsumerExpiryCheckEnabled() && message.isExpired(); if (!expired) { - listener.onMessage(message); + ActiveMQSession.IN_MESSAGE_LISTENER_CALLBACK.set(true); + try { + listener.onMessage(message); + } finally { + ActiveMQSession.IN_MESSAGE_LISTENER_CALLBACK.set(false); + } } afterMessageIsConsumed(md, expired); } catch (RuntimeException e) { 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..3a99cc02198 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducer.java @@ -168,6 +168,7 @@ public Destination getDestination() throws JMSException { */ @Override public void close() throws JMSException { + ActiveMQSession.checkNotInCompletionListenerCallback("close"); if (!closed) { dispose(); this.session.asyncSendPacket(info.createRemoveCommand()); @@ -197,6 +198,35 @@ protected void checkClosed() throws IllegalStateException { } } + @Override + public void send(Message message) throws JMSException { + checkClosed(); + if (info.getDestination() == null) { + throw new UnsupportedOperationException("A destination must be specified."); + } + super.send(message); + } + + @Override + public void send(Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + checkClosed(); + if (info.getDestination() == null) { + throw new UnsupportedOperationException("A destination must be specified."); + } + validateDeliveryMode(deliveryMode); + validatePriority(priority); + super.send(message, deliveryMode, priority, timeToLive); + } + + @Override + public void send(Destination destination, Message message) throws JMSException { + checkClosed(); + if (info.getDestination() != null) { + throw new UnsupportedOperationException("This producer was created with a specific destination. Use send(Message) variants."); + } + super.send(destination, message); + } + /** * Sends a message to a destination for an unidentified message producer, * specifying delivery mode, priority and time to live. @@ -221,42 +251,133 @@ protected void checkClosed() throws IllegalStateException { */ @Override public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { - this.send(destination, message, deliveryMode, priority, timeToLive, (AsyncCallback)null); + validateDeliveryMode(deliveryMode); + validatePriority(priority); + this.send(destination, message, deliveryMode, priority, timeToLive, (AsyncCallback) null); } /** * * @param message the message to send - * @param CompletionListener to callback + * @param completionListener to callback * @throws JMSException if the JMS provider fails to send the message due to * some internal error. - * @throws UnsupportedOperationException if an invalid destination is - * specified. - * @throws InvalidDestinationException if a client uses this method with an - * invalid destination. + * @throws UnsupportedOperationException if called on an anonymous producer (no fixed destination) * @see jakarta.jms.Session#createProducer * @since 2.0 */ @Override public void send(Message message, CompletionListener completionListener) throws JMSException { - throw new UnsupportedOperationException("send(Message, CompletionListener) is not supported"); + checkClosed(); + if (completionListener == null) { + throw new IllegalArgumentException("CompletionListener must not be null"); + } + if (info.getDestination() == null) { + throw new UnsupportedOperationException("A destination must be specified."); + } + this.doSendWithCompletionListener(info.getDestination(), message, this.defaultDeliveryMode, + this.defaultPriority, this.defaultTimeToLive, + getDisableMessageID(), getDisableMessageTimestamp(), completionListener); } @Override public void send(Message message, int deliveryMode, int priority, long timeToLive, CompletionListener completionListener) throws JMSException { - throw new UnsupportedOperationException("send(Message, deliveryMode, priority, timetoLive, CompletionListener) is not supported"); + checkClosed(); + if (completionListener == null) { + throw new IllegalArgumentException("CompletionListener must not be null"); + } + if (info.getDestination() == null) { + throw new UnsupportedOperationException("A destination must be specified."); + } + validateDeliveryMode(deliveryMode); + validatePriority(priority); + this.doSendWithCompletionListener(info.getDestination(), message, deliveryMode, priority, timeToLive, + getDisableMessageID(), getDisableMessageTimestamp(), completionListener); } @Override public void send(Destination destination, Message message, CompletionListener completionListener) throws JMSException { - throw new UnsupportedOperationException("send(Destination, Message, CompletionListener) is not supported"); + checkClosed(); + if (info.getDestination() != null) { + throw new UnsupportedOperationException("This producer was created with a specific destination. Use send(Message, CompletionListener) variants."); + } + if (completionListener == null) { + throw new IllegalArgumentException("CompletionListener must not be null"); + } + if (destination == null) { + throw new InvalidDestinationException("Don't understand null destinations"); + } + this.doSendWithCompletionListener(ActiveMQDestination.transform(destination), message, + this.defaultDeliveryMode, this.defaultPriority, this.defaultTimeToLive, + getDisableMessageID(), getDisableMessageTimestamp(), completionListener); } @Override public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, CompletionListener completionListener) throws JMSException { - throw new UnsupportedOperationException("send(Destination, Message, deliveryMode, priority, timetoLive, CompletionListener) is not supported"); + checkClosed(); + if (info.getDestination() != null) { + throw new UnsupportedOperationException("This producer was created with a specific destination. Use send(Message, CompletionListener) variants."); + } + if (completionListener == null) { + throw new IllegalArgumentException("CompletionListener must not be null"); + } + if (destination == null) { + throw new InvalidDestinationException("Don't understand null destinations"); + } + validateDeliveryMode(deliveryMode); + validatePriority(priority); + this.doSendWithCompletionListener(ActiveMQDestination.transform(destination), message, + deliveryMode, priority, timeToLive, + getDisableMessageID(), getDisableMessageTimestamp(), completionListener); + } + + public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, + boolean disableMessageID, boolean disableMessageTimestamp, + CompletionListener completionListener) throws JMSException { + checkClosed(); + if (info.getDestination() != null) { + throw new UnsupportedOperationException("This producer was created with a specific destination. Use send(Message, CompletionListener) variants."); + } + if (completionListener == null) { + throw new IllegalArgumentException("CompletionListener must not be null"); + } + if (destination == null) { + throw new InvalidDestinationException("Don't understand null destinations"); + } + validateDeliveryMode(deliveryMode); + validatePriority(priority); + this.doSendWithCompletionListener(ActiveMQDestination.transform(destination), message, + deliveryMode, priority, timeToLive, disableMessageID, disableMessageTimestamp, completionListener); + } + + private void doSendWithCompletionListener(final ActiveMQDestination dest, Message message, + final int deliveryMode, final int priority, final long timeToLive, + final boolean disableMessageID, final boolean disableMessageTimestamp, + final CompletionListener completionListener) throws JMSException { + if (dest == null) { + throw new JMSException("No destination specified"); + } + + if (transformer != null) { + final Message transformedMessage = transformer.producerTransform(session, this, message); + if (transformedMessage != null) { + message = transformedMessage; + } + } + + if (producerWindow != null) { + try { + producerWindow.waitForSpace(); + } catch (InterruptedException e) { + throw new JMSException("Send aborted due to thread interrupt."); + } + } + + this.session.send(this, dest, message, deliveryMode, priority, timeToLive, + disableMessageID, disableMessageTimestamp, producerWindow, sendTimeout, completionListener); + stats.onMessage(); } public void send(Message message, AsyncCallback onComplete) throws JMSException { @@ -294,7 +415,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/ActiveMQMessageProducerSupport.java b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducerSupport.java index 5816d70e30c..5e36a15f55f 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducerSupport.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQMessageProducerSupport.java @@ -336,6 +336,18 @@ public void send(Destination destination, Message message) throws JMSException { protected abstract void checkClosed() throws IllegalStateException; + protected static void validateDeliveryMode(final int deliveryMode) throws JMSException { + if (deliveryMode != DeliveryMode.PERSISTENT && deliveryMode != DeliveryMode.NON_PERSISTENT) { + throw new JMSException("Invalid delivery mode: " + deliveryMode); + } + } + + protected static void validatePriority(final int priority) throws JMSException { + if (priority < 0 || priority > 9) { + throw new JMSException("Invalid priority: " + priority + " (must be 0-9)"); + } + } + /** * @return the sendTimeout */ 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..ae574565e0e 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQProducer.java @@ -57,6 +57,8 @@ public class ActiveMQProducer implements JMSProducer { // Properties applied to all messages on a per-JMS producer instance basis private Map messageProperties = null; + private CompletionListener completionListener = null; + ActiveMQProducer(ActiveMQContext activemqContext, ActiveMQMessageProducer activemqMessageProducer) { this.activemqContext = activemqContext; this.activemqMessageProducer = activemqMessageProducer; @@ -64,6 +66,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); @@ -87,7 +92,12 @@ public JMSProducer send(Destination destination, Message message) { } } - activemqMessageProducer.send(destination, message, getDeliveryMode(), getPriority(), getTimeToLive(), getDisableMessageID(), getDisableMessageTimestamp(), null); + if (completionListener != null) { + activemqMessageProducer.send(destination, message, getDeliveryMode(), getPriority(), getTimeToLive(), + getDisableMessageID(), getDisableMessageTimestamp(), completionListener); + } else { + activemqMessageProducer.send(destination, message, getDeliveryMode(), getPriority(), getTimeToLive(), getDisableMessageID(), getDisableMessageTimestamp(), (AsyncCallback) null); + } } catch (JMSException e) { throw JMSExceptionSupport.convertToJMSRuntimeException(e); } @@ -253,12 +263,13 @@ public long getDeliveryDelay() { @Override public JMSProducer setAsync(CompletionListener completionListener) { - throw new UnsupportedOperationException("setAsync(CompletionListener) is not supported"); + this.completionListener = completionListener; + return this; } @Override public CompletionListener getAsync() { - throw new UnsupportedOperationException("getAsync() is not supported"); + return this.completionListener; } @Override 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..4aaa6940693 100644 --- a/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java +++ b/activemq-client/src/main/java/org/apache/activemq/ActiveMQSession.java @@ -26,13 +26,20 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import jakarta.jms.BytesMessage; +import jakarta.jms.CompletionListener; import jakarta.jms.Destination; import jakarta.jms.IllegalStateException; +import jakarta.jms.IllegalStateRuntimeException; import jakarta.jms.InvalidDestinationException; import jakarta.jms.InvalidSelectorException; import jakarta.jms.JMSException; @@ -58,6 +65,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; @@ -235,6 +243,20 @@ public static interface DeliveryListener { private BlobTransferPolicy blobTransferPolicy; private long lastDeliveredSequenceId = -2; + // Single-threaded executor for async send: ensures one CompletionListener callback at a time + // and that callbacks are invoked in the same order as the corresponding send calls + // per Jakarta Messaging 3.1 spec section 7.3.8 + private final ExecutorService asyncSendExecutor = Executors.newSingleThreadExecutor( + r -> new Thread(r, "ActiveMQ async-send")); + + // Set to true on the executor thread while a CompletionListener callback is executing. + // Used to detect illegal session operations (close/commit/rollback) from within a callback. + static final ThreadLocal IN_COMPLETION_LISTENER_CALLBACK = ThreadLocal.withInitial(() -> false); + + // Set to true on the dispatch thread while a MessageListener.onMessage() callback is executing. + // Used to detect illegal connection/session operations from within a MessageListener callback. + static final ThreadLocal IN_MESSAGE_LISTENER_CALLBACK = ThreadLocal.withInitial(() -> false); + /** * Construct the Session * @@ -576,6 +598,7 @@ public int getAcknowledgeMode() throws JMSException { @Override public void commit() throws JMSException { checkClosed(); + checkNotInCompletionListenerCallback("commit"); if (!getTransacted()) { throw new jakarta.jms.IllegalStateException("Not a transacted session"); } @@ -597,6 +620,7 @@ public void commit() throws JMSException { @Override public void rollback() throws JMSException { checkClosed(); + checkNotInCompletionListenerCallback("rollback"); if (!getTransacted()) { throw new jakarta.jms.IllegalStateException("Not a transacted session"); } @@ -636,6 +660,8 @@ public void rollback() throws JMSException { */ @Override public void close() throws JMSException { + checkNotInCompletionListenerCallback("close"); + checkNotInMessageListenerCallback("close"); if (!closed) { if (getTransactionContext().isInXATransaction()) { if (!synchronizationRegistered) { @@ -724,6 +750,15 @@ public synchronized void dispose() throws JMSException { if (!closed) { try { + // Shutdown async send executor and wait for any in-progress callbacks to finish + // per Jakarta Messaging 3.1 spec section 7.3.5 + asyncSendExecutor.shutdown(); + try { + asyncSendExecutor.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.debug("Interrupted while waiting for async send executor to terminate", e); + } executor.close(); for (Iterator iter = consumers.iterator(); iter.hasNext();) { @@ -1053,7 +1088,12 @@ public void run() { } LOG.trace("{} onMessage({})", this, message.getMessageId()); - messageListener.onMessage(message); + IN_MESSAGE_LISTENER_CALLBACK.set(true); + try { + messageListener.onMessage(message); + } finally { + IN_MESSAGE_LISTENER_CALLBACK.set(false); + } } catch (Throwable e) { if (!isClosed()) { @@ -1386,11 +1426,13 @@ public Topic createTopic(String topicName) throws JMSException { @Override public MessageConsumer createSharedConsumer(Topic topic, String sharedSubscriptionName) throws JMSException { + checkClosed(); throw new UnsupportedOperationException("createSharedConsumer(Topic, sharedSubscriptionName) is not supported"); } @Override public MessageConsumer createSharedConsumer(Topic topic, String sharedSubscriptionName, String messageSelector) throws JMSException { + checkClosed(); throw new UnsupportedOperationException("createSharedConsumer(Topic, sharedSubscriptionName, messageSelector) is not supported"); } @@ -1408,11 +1450,13 @@ public MessageConsumer createDurableConsumer(Topic topic, String name, String me @Override public MessageConsumer createSharedDurableConsumer(Topic topic, String name) throws JMSException { + checkClosed(); throw new UnsupportedOperationException("createSharedDurableConsumer(Topic, name) is not supported"); } @Override public MessageConsumer createSharedDurableConsumer(Topic topic, String name, String messageSelector) throws JMSException { + checkClosed(); throw new UnsupportedOperationException("createSharedDurableConsumer(Topic, name, messageSelector) is not supported"); } @@ -2339,4 +2383,151 @@ private static void setForeignMessageDeliveryTime(final Message foreignMessage, foreignMessage.setJMSDeliveryTime(deliveryTime); } } + + /** + * Sends a message with a CompletionListener for async notification per Jakarta Messaging 3.1 spec section 7.3. + *

+ * The wire-level send is performed synchronously (inside sendMutex to preserve ordering). The + * CompletionListener is then invoked on a dedicated single-threaded executor, ensuring: + *

    + *
  • Callbacks are not called on the sender's thread (spec 7.3.8)
  • + *
  • Only one callback executes at a time (spec 7.3.8)
  • + *
  • Callbacks are in the same order as the corresponding send calls (spec 7.3.8)
  • + *
+ * The sender thread blocks until the send completes and the callback has been invoked. + */ + protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, + int deliveryMode, int priority, long timeToLive, + boolean disableMessageID, boolean disableMessageTimestamp, + MemoryUsage producerWindow, int sendTimeout, + CompletionListener completionListener) throws JMSException { + + checkClosed(); + if (destination.isTemporary() && connection.isDeleted(destination)) { + throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination); + } + + final ActiveMQMessage msg; + final Message originalMessage = message; + + synchronized (sendMutex) { + doStartTransaction(); + if (transactionContext.isRollbackOnly()) { + throw new IllegalStateException("transaction marked rollback only"); + } + final TransactionId txid = transactionContext.getTransactionId(); + final long sequenceNumber = producer.getMessageSequence(); + + // Set the "JMS" header fields on the original message, see 1.1 spec section 3.4.11 + message.setJMSDeliveryMode(deliveryMode); + final long timeStamp = System.currentTimeMillis(); + final long expiration = timeToLive > 0 ? timeToLive + timeStamp : 0L; + + if (!(message instanceof ActiveMQMessage)) { + setForeignMessageDeliveryTime(message, timeStamp); + } else { + message.setJMSDeliveryTime(timeStamp); + } + if (!disableMessageTimestamp && !producer.getDisableMessageTimestamp()) { + message.setJMSTimestamp(timeStamp); + } else { + message.setJMSTimestamp(0L); + } + message.setJMSExpiration(expiration); + message.setJMSPriority(priority); + message.setJMSRedelivered(false); + + // Transform to our own message format + ActiveMQMessage amqMsg = ActiveMQMessageTransformation.transformMessage(message, connection); + amqMsg.setDestination(destination); + amqMsg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber)); + + // Propagate the message id and destination back to the original message + if (amqMsg != message) { + message.setJMSMessageID(amqMsg.getMessageId().toString()); + message.setJMSDestination(destination); + } + amqMsg.setBrokerPath(null); + amqMsg.setTransactionId(txid); + + // Always copy when sending async so the user can safely modify the message after send() + // returns without affecting the in-flight message + msg = (ActiveMQMessage) amqMsg.copy(); + msg.setConnection(connection); + msg.onSend(); + msg.setProducerId(msg.getMessageId().getProducerId()); + + if (LOG.isTraceEnabled()) { + LOG.trace(getSessionId() + " async sending message: " + msg); + } + + // Perform the wire-level send synchronously while holding sendMutex. + // This ensures messages are delivered to the broker in send order. + try { + this.connection.syncSendPacket(msg); + } catch (JMSException sendEx) { + // Send failed - invoke onException on executor thread (not sender thread) + final Future future = asyncSendExecutor.submit(() -> { + IN_COMPLETION_LISTENER_CALLBACK.set(true); + try { + completionListener.onException(originalMessage, sendEx); + } finally { + IN_COMPLETION_LISTENER_CALLBACK.set(false); + } + }); + awaitAsyncSendFuture(future, originalMessage, completionListener); + return; + } + } + + // Send succeeded - invoke onCompletion on executor thread (not sender thread) per spec 7.3.8 + final Future future = asyncSendExecutor.submit(() -> { + IN_COMPLETION_LISTENER_CALLBACK.set(true); + try { + completionListener.onCompletion(originalMessage); + } catch (Exception e) { + // Per spec 7.3.2, exceptions thrown by the callback are swallowed + LOG.warn("CompletionListener.onCompletion threw an exception", e); + } finally { + IN_COMPLETION_LISTENER_CALLBACK.set(false); + } + }); + awaitAsyncSendFuture(future, originalMessage, completionListener); + } + + private void awaitAsyncSendFuture(final Future future, final Message originalMessage, + final CompletionListener completionListener) throws JMSException { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new JMSException("Async send interrupted while waiting for CompletionListener"); + } catch (ExecutionException e) { + // Should not happen since we catch all exceptions inside the submitted task + LOG.warn("Unexpected error executing CompletionListener", e.getCause()); + } + } + + /** + * Throws {@link jakarta.jms.IllegalStateException} if the current thread is executing a + * CompletionListener callback, per Jakarta Messaging 3.1 spec section 7.3.5. + * The classic JMS API uses checked IllegalStateException (not the runtime variant). + */ + static void checkNotInCompletionListenerCallback(final String operation) throws jakarta.jms.IllegalStateException { + if (Boolean.TRUE.equals(IN_COMPLETION_LISTENER_CALLBACK.get())) { + throw new jakarta.jms.IllegalStateException( + "Cannot call " + operation + "() from within a CompletionListener callback"); + } + } + + /** + * Throws {@link jakarta.jms.IllegalStateException} if the current thread is executing a + * MessageListener.onMessage() callback, per Jakarta Messaging spec section 4.4. + */ + static void checkNotInMessageListenerCallback(final String operation) throws jakarta.jms.IllegalStateException { + if (Boolean.TRUE.equals(IN_MESSAGE_LISTENER_CALLBACK.get())) { + throw new jakarta.jms.IllegalStateException( + "Cannot call " + operation + "() from within a MessageListener callback"); + } + } } 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/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/JMSConsumerTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/JMSConsumerTest.java index 66df1d4647a..57dbb2ae629 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/JMSConsumerTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/JMSConsumerTest.java @@ -495,8 +495,8 @@ public void onMessage(Message m) { counter.incrementAndGet(); if (counter.get() == 2) { sendDone.await(); - connection.close(); got2Done.countDown(); + return; // Don't acknowledge - message stays unacked (CLIENT_ACK mode) } tm.acknowledge(); } catch (Throwable e) { @@ -511,6 +511,8 @@ public void onMessage(Message m) { // Wait for first 2 messages to arrive. assertTrue(got2Done.await(100000, TimeUnit.MILLISECONDS)); + // Close connection from main thread (spec: Connection.close() from MessageListener throws ISE) + connection.close(); // Re-start connection. connection = (ActiveMQConnection)factory.createConnection(); @@ -584,8 +586,9 @@ public void onMessage(Message m) { m.acknowledge(); if (counter.get() == 2) { sendDone.await(); - connection.close(); got2Done.countDown(); + // Don't call connection.close() from MessageListener - spec violation (throws ISE) + // Main thread will close the connection after this latch } } catch (Throwable e) { e.printStackTrace(); @@ -599,6 +602,8 @@ public void onMessage(Message m) { // Wait for first 2 messages to arrive. assertTrue(got2Done.await(100000, TimeUnit.MILLISECONDS)); + // Close connection from main thread (spec: Connection.close() from MessageListener throws ISE) + connection.close(); // Re-start connection. connection = (ActiveMQConnection)factory.createConnection(); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/JmsQueueBrowserExpirationTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/JmsQueueBrowserExpirationTest.java index 901fbca7e64..2f6345d0781 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/JmsQueueBrowserExpirationTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/JmsQueueBrowserExpirationTest.java @@ -130,7 +130,7 @@ public void testDoNotReceiveExpiredMessage() throws Exception { producer.setTimeToLive(WAIT_TIME); TextMessage message = session.createTextMessage("Test message"); - producer.send(producerQueue, message); + producer.send(message); int count = getMessageCount(producerQueue, session); assertEquals(1, count); @@ -165,6 +165,9 @@ private int browse(ActiveMQQueue queue, Connection connection) throws JMSExcepti int browsed = 0; while (enumeration.hasMoreElements()) { TextMessage m = (TextMessage) enumeration.nextElement(); + if (m == null) { + continue; // message expired during browse + } browsed++; LOG.debug("B[{}]: {}", browsed, m.getText()); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyMemoryUsageTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyMemoryUsageTest.java index 34ccf3d3aa2..27fbd5f2e9d 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyMemoryUsageTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyMemoryUsageTest.java @@ -102,7 +102,7 @@ public void testMemoryUsageBodyIncrease() throws Exception { BytesMessage sendMessageP = session.createBytesMessage(); byte[] origBody = new byte[1*1024]; sendMessageP.writeBytes(origBody); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); } QueueViewMBean queueViewMBean = getProxyToQueue(queueName); @@ -124,7 +124,7 @@ public void testMemoryUsageBodyDecrease() throws Exception { BytesMessage sendMessageP = session.createBytesMessage(); byte[] origBody = new byte[1*1024*1024]; sendMessageP.writeBytes(origBody); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); } QueueViewMBean queueViewMBean = getProxyToQueue(queueName); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyTest.java index ae50d26d9aa..01c430fcb0c 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/policy/MessageInterceptorStrategyTest.java @@ -96,11 +96,11 @@ public void testForceDeliveryModePersistent() throws Exception { Queue queue = createQueue("mis.forceDeliveryMode.true"); Message sendMessageP = session.createTextMessage("forceDeliveryMode=true"); producer.setDeliveryMode(DeliveryMode.PERSISTENT); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); Message sendMessageNP = session.createTextMessage("forceDeliveryMode=true"); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - producer.send(queue, sendMessageNP); + producer.send(sendMessageNP); queueBrowser = session.createBrowser(queue); Enumeration browseEnumeration = queueBrowser.getEnumeration(); @@ -124,11 +124,11 @@ public void testForceDeliveryModeNonPersistent() throws Exception { Queue queue = createQueue("mis.forceDeliveryMode.false"); Message sendMessageP = session.createTextMessage("forceDeliveryMode=false"); producer.setDeliveryMode(DeliveryMode.PERSISTENT); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); Message sendMessageNP = session.createTextMessage("forceDeliveryMode=false"); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - producer.send(queue, sendMessageNP); + producer.send(sendMessageNP); queueBrowser = session.createBrowser(queue); Enumeration browseEnumeration = queueBrowser.getEnumeration(); @@ -152,7 +152,7 @@ public void testForceExpirationDisabled() throws Exception { Queue queue = createQueue("mis.forceExpiration.zero"); Message sendMessageP = session.createTextMessage("expiration=zero"); producer.setTimeToLive(0l); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); queueBrowser = session.createBrowser(queue); Enumeration browseEnumeration = queueBrowser.getEnumeration(); @@ -178,7 +178,7 @@ public void testForceExpirationZeroOverride() throws Exception { Queue queue = createQueue("mis.forceExpiration.100k"); Message sendMessageP = session.createTextMessage("expiration=zero"); producer.setTimeToLive(100_000l); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); queueBrowser = session.createBrowser(queue); Enumeration browseEnumeration = queueBrowser.getEnumeration(); @@ -202,7 +202,7 @@ public void testForceExpirationZeroOverrideDLQ() throws Exception { Queue queue = createQueue("mis.forceExpiration.zero-no-dlq-expiry"); Message sendMessageP = session.createTextMessage("expiration=zero-no-dlq-expiry"); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); Thread.sleep(250l); @@ -242,7 +242,7 @@ public void testForceExpirationCeilingOverride() throws Exception { Queue queue = createQueue("mis.forceExpiration.maxValue"); Message sendMessageP = session.createTextMessage("expiration=ceiling"); producer.setTimeToLive(expiryTime); - producer.send(queue, sendMessageP); + producer.send(sendMessageP); queueBrowser = session.createBrowser(queue); Enumeration browseEnumeration = queueBrowser.getEnumeration(); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ3934Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ3934Test.java index ab019889944..4553e6db402 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ3934Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ3934Test.java @@ -71,7 +71,7 @@ public void sendMessage() throws Exception { final Destination queue = session.createQueue(TEST_QUEUE); final Message toSend = session.createMessage(); final MessageProducer producer = session.createProducer(queue); - producer.send(queue, toSend); + producer.send(toSend); } finally { conn.close(); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4083Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4083Test.java index f395605c897..e4a64562581 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4083Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4083Test.java @@ -463,12 +463,12 @@ public void testConsumeExpiredQueueAndDlq() throws Exception { String msgBody = new String(new byte[20*1024]); for (int i = 0; i < data.length; i++) { Message message = session.createTextMessage(msgBody); - producerExpire.send(queue, message); + producerExpire.send(message); } for (int i = 0; i < data.length; i++) { Message message = session.createTextMessage(msgBody); - producerNormal.send(queue, message); + producerNormal.send(message); } ArrayList messages = new ArrayList(); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4361Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4361Test.java index f8c49e01aff..a2c57b01ade 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4361Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4361Test.java @@ -113,7 +113,7 @@ public void run() { lastLoop.set(System.currentTimeMillis()); ObjectMessage objMsg = session.createObjectMessage(); objMsg.setObject(data); - producer.send(destination, objMsg); + producer.send(objMsg); } } catch (Exception e) { publishException.set(e); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4530Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4530Test.java index 2bfe77617ac..1ab98fb003e 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4530Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4530Test.java @@ -78,7 +78,7 @@ public void sendMessage() throws Exception { final Message toSend = session.createMessage(); toSend.setStringProperty(KEY, VALUE); final MessageProducer producer = session.createProducer(queue); - producer.send(queue, toSend); + producer.send(toSend); } finally { conn.close(); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4930Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4930Test.java index b0521ba611d..0e1e13badec 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4930Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ4930Test.java @@ -83,7 +83,7 @@ public void doTestBrowsePending(int deliveryMode) throws Exception { bytesMessage.writeBytes(new byte[messageSize]); for (int i = 0; i < messageCount; i++) { - producer.send(bigQueue, bytesMessage); + producer.send(bytesMessage); } final QueueViewMBean queueViewMBean = (QueueViewMBean) diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ6059Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ6059Test.java index 5e06ca90b96..871942a5b97 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ6059Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ6059Test.java @@ -173,7 +173,7 @@ private void sendMessage(Destination destination) throws Exception { connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(destination); - producer.send(destination, session.createTextMessage("DLQ message"), DeliveryMode.PERSISTENT, 4, 1000); + producer.send(session.createTextMessage("DLQ message"), DeliveryMode.PERSISTENT, 4, 1000); connection.stop(); LOG.info("### Send message that will expire."); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ7270Test.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ7270Test.java index 8b173f25d84..939f5d6ff93 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ7270Test.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/AMQ7270Test.java @@ -67,7 +67,7 @@ public void testConcurrentCopyMatchingPageSizeOk() throws Exception { for (int i = 0; i < messageCount; i++) { bytesMessage.setIntProperty("id", i); - producer.send(activeMQQueue, bytesMessage); + producer.send(bytesMessage); } final QueueViewMBean queueViewMBean = (QueueViewMBean) diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/MemoryUsageBlockResumeTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/MemoryUsageBlockResumeTest.java index 2f59fcf3b2a..13d5a0ad52c 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/MemoryUsageBlockResumeTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/bugs/MemoryUsageBlockResumeTest.java @@ -112,7 +112,7 @@ public void run() { producer.setDeliveryMode(deliveryMode); for (int idx = 0; idx < toSend; ++idx) { Message message = session.createTextMessage(new String(buf) + idx); - producer.send(destination, message); + producer.send(message); messagesSent.incrementAndGet(); LOG.info("After little:" + idx + ", System Memory Usage " + broker.getSystemUsage().getMemoryUsage().getPercentUsage()); } @@ -132,7 +132,7 @@ public void run() { producer.setDeliveryMode(deliveryMode); for (int idx = 0; idx < toSend; ++idx) { Message message = session.createTextMessage(new String(buf) + idx); - producer.send(destination, message); + producer.send(message); messagesSent.incrementAndGet(); LOG.info("After little:" + idx + ", System Memory Usage " + broker.getSystemUsage().getMemoryUsage().getPercentUsage()); } 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..0eecf33fb74 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 @@ -301,7 +301,7 @@ public void testProducerSendMessageCompletionListener() throws JMSException { messageProducer.send(session.createQueue(methodNameDestinationName), null, (CompletionListener)null); } - @Test(expected = UnsupportedOperationException.class) + @Test(expected = IllegalArgumentException.class) public void testProducerSendMessageQoSParamsCompletionListener() throws JMSException { messageProducer.send(null, 1, 4, 0l, null); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/OpenTypeSupportTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/OpenTypeSupportTest.java index 627fa32e1c9..28ffbad2231 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/OpenTypeSupportTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/OpenTypeSupportTest.java @@ -77,7 +77,7 @@ private static void sendMessage() throws JMSException { BytesMessage toSend = session.createBytesMessage(); toSend.writeBytes(BYTESMESSAGE_TEXT.getBytes()); MessageProducer producer = session.createProducer(queue); - producer.send(queue, toSend); + producer.send(toSend); } finally { conn.close(); } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/TotalMessageCountTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/TotalMessageCountTest.java index bb51e303675..6175bc4e7f8 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/TotalMessageCountTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/jmx/TotalMessageCountTest.java @@ -105,7 +105,7 @@ private void sendMessage() throws JMSException { Destination queue = session.createQueue(TESTQUEUE); TextMessage msg = session.createTextMessage("This is a message."); MessageProducer producer = session.createProducer(queue); - producer.send(queue, msg); + producer.send(msg); LOG.info("Message sent to " + TESTQUEUE); } finally { conn.close(); diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/transport/failover/InitalReconnectDelayTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/failover/InitalReconnectDelayTest.java index c12cbf4755d..008f9c1eacc 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/transport/failover/InitalReconnectDelayTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/failover/InitalReconnectDelayTest.java @@ -129,7 +129,7 @@ public void transportResumed() { LOG.info("Attempting to send... failover should throw on disconnect"); try { - producer.send(destination, message); + producer.send(message); fail("Expect IOException to bubble up on send"); } catch (jakarta.jms.IllegalStateException producerClosed) { } diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/usecases/TopicDurableConnectStatsTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/usecases/TopicDurableConnectStatsTest.java index 16aeeb9209e..ba9606d0920 100644 --- a/activemq-unit-tests/src/test/java/org/apache/activemq/usecases/TopicDurableConnectStatsTest.java +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/usecases/TopicDurableConnectStatsTest.java @@ -187,7 +187,7 @@ public void testPendingTopicStat() throws Exception { TextMessage message = producerSessions.createTextMessage(createMessageText(i)); message.setJMSExpiration(0); message.setStringProperty("filter", "true"); - producer.send(topic, message); + producer.send(message); producerSessions.commit(); } 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