/* * 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.artemis.jms.client; import javax.jms.ConnectionConsumer; import javax.jms.ConnectionMetaData; import javax.jms.Destination; import javax.jms.ExceptionListener; import javax.jms.IllegalStateException; import javax.jms.InvalidClientIDException; import javax.jms.JMSException; import javax.jms.JMSRuntimeException; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueSession; import javax.jms.ServerSessionPool; import javax.jms.Session; import javax.jms.Topic; import javax.jms.TopicConnection; import javax.jms.TopicSession; import java.lang.ref.WeakReference; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.client.ClientSession; import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; import org.apache.activemq.artemis.api.core.client.FailoverEventListener; import org.apache.activemq.artemis.api.core.client.FailoverEventType; import org.apache.activemq.artemis.api.core.client.SessionFailureListener; import org.apache.activemq.artemis.api.jms.ActiveMQJMSConstants; import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal; import org.apache.activemq.artemis.core.version.Version; import org.apache.activemq.artemis.reader.MessageUtil; import org.apache.activemq.artemis.utils.ActiveMQThreadFactory; import org.apache.activemq.artemis.utils.UUIDGenerator; import org.apache.activemq.artemis.utils.VersionLoader; import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet; /** * ActiveMQ Artemis implementation of a JMS Connection. * <p> * The flat implementation of {@link TopicConnection} and {@link QueueConnection} is per design, * following the common usage of these as one flat API in JMS 1.1. */ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl implements TopicConnection, QueueConnection { // Constants ------------------------------------------------------------------------------------ public static final int TYPE_GENERIC_CONNECTION = 0; public static final int TYPE_QUEUE_CONNECTION = 1; public static final int TYPE_TOPIC_CONNECTION = 2; public static final String EXCEPTION_FAILOVER = "FAILOVER"; public static final String EXCEPTION_DISCONNECT = "DISCONNECT"; public static final SimpleString CONNECTION_ID_PROPERTY_NAME = MessageUtil.CONNECTION_ID_PROPERTY_NAME; // Static --------------------------------------------------------------------------------------- // Attributes ----------------------------------------------------------------------------------- private final int connectionType; private final Set<ActiveMQSession> sessions = new ConcurrentHashSet<>(); private final Set<SimpleString> tempQueues = new ConcurrentHashSet<>(); private final Set<SimpleString> knownDestinations = new ConcurrentHashSet<>(); private volatile boolean hasNoLocal; private volatile ExceptionListener exceptionListener; private volatile FailoverEventListener failoverEventListener; private volatile boolean justCreated = true; private volatile ConnectionMetaData metaData; private volatile boolean closed; private volatile boolean started; private String clientID; private final ClientSessionFactory sessionFactory; private final SimpleString uid; private final String username; private final String password; private final SessionFailureListener listener = new JMSFailureListener(this); private final FailoverEventListener failoverListener = new FailoverEventListenerImpl(this); private final ExecutorService failoverListenerExecutor = Executors.newFixedThreadPool(1, ActiveMQThreadFactory.defaultThreadFactory()); private final Version thisVersion; private final int dupsOKBatchSize; private final int transactionBatchSize; private ClientSession initialSession; private final Exception creationStack; private ActiveMQConnectionFactory factoryReference; private final ConnectionFactoryOptions options; // Constructors --------------------------------------------------------------------------------- public ActiveMQConnection(final ConnectionFactoryOptions options, final String username, final String password, final int connectionType, final String clientID, final int dupsOKBatchSize, final int transactionBatchSize, final ClientSessionFactory sessionFactory) { this.options = options; this.username = username; this.password = password; this.connectionType = connectionType; this.clientID = clientID; this.sessionFactory = sessionFactory; uid = UUIDGenerator.getInstance().generateSimpleStringUUID(); thisVersion = VersionLoader.getVersion(); this.dupsOKBatchSize = dupsOKBatchSize; this.transactionBatchSize = transactionBatchSize; creationStack = new Exception(); } /** * This internal method serves basically the Resource Adapter. * The resource adapter plays with an XASession and a non XASession. * When there is no enlisted transaction, the EE specification mandates that the commit should * be done as if it was a nonXA Session (i.e. SessionTransacted). * For that reason we have this method to force that nonXASession, since the JMS Javadoc * mandates createSession to return a XASession. */ public Session createNonXASession(final boolean transacted, final int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, acknowledgeMode, ActiveMQConnection.TYPE_GENERIC_CONNECTION); } /** * This internal method serves basically the Resource Adapter. * The resource adapter plays with an XASession and a non XASession. * When there is no enlisted transaction, the EE specification mandates that the commit should * be done as if it was a nonXA Session (i.e. SessionTransacted). * For that reason we have this method to force that nonXASession, since the JMS Javadoc * mandates createSession to return a XASession. */ public Session createNonXATopicSession(final boolean transacted, final int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, acknowledgeMode, ActiveMQConnection.TYPE_TOPIC_CONNECTION); } /** * This internal method serves basically the Resource Adapter. * The resource adapter plays with an XASession and a non XASession. * When there is no enlisted transaction, the EE specification mandates that the commit should * be done as if it was a nonXA Session (i.e. SessionTransacted). * For that reason we have this method to force that nonXASession, since the JMS Javadoc * mandates createSession to return a XASession. */ public Session createNonXAQueueSession(final boolean transacted, final int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, acknowledgeMode, ActiveMQConnection.TYPE_QUEUE_CONNECTION); } // Connection implementation -------------------------------------------------------------------- @Override public synchronized Session createSession(final boolean transacted, final int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, checkAck(transacted, acknowledgeMode), ActiveMQConnection.TYPE_GENERIC_CONNECTION); } @Override public String getClientID() throws JMSException { checkClosed(); return clientID; } @Override public void setClientID(final String clientID) throws JMSException { checkClosed(); if (this.clientID != null) { throw new IllegalStateException("Client id has already been set"); } if (!justCreated) { throw new IllegalStateException("setClientID can only be called directly after the connection is created"); } try { initialSession.addUniqueMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, clientID); } catch (ActiveMQException e) { if (e.getType() == ActiveMQExceptionType.DUPLICATE_METADATA) { throw new InvalidClientIDException("clientID=" + clientID + " was already set into another connection"); } } this.clientID = clientID; try { this.addSessionMetaData(initialSession); } catch (ActiveMQException e) { JMSException ex = new JMSException("Internal error setting metadata jms-client-id"); ex.setLinkedException(e); ex.initCause(e); throw ex; } justCreated = false; } @Override public ConnectionMetaData getMetaData() throws JMSException { checkClosed(); justCreated = false; if (metaData == null) { metaData = new ActiveMQConnectionMetaData(thisVersion); } return metaData; } @Override public ExceptionListener getExceptionListener() throws JMSException { checkClosed(); justCreated = false; return exceptionListener; } @Override public void setExceptionListener(final ExceptionListener listener) throws JMSException { checkClosed(); exceptionListener = listener; justCreated = false; } @Override public synchronized void start() throws JMSException { checkClosed(); for (ActiveMQSession session : sessions) { session.start(); } justCreated = false; started = true; } public synchronized void signalStopToAllSessions() { for (ActiveMQSession session : sessions) { ClientSession coreSession = session.getCoreSession(); if (coreSession instanceof ClientSessionInternal) { ClientSessionInternal internalSession = (ClientSessionInternal) coreSession; internalSession.setStopSignal(); } } } @Override public synchronized void stop() throws JMSException { threadAwareContext.assertNotMessageListenerThread(); checkClosed(); for (ActiveMQSession session : sessions) { session.stop(); } justCreated = false; started = false; } @Override public final synchronized void close() throws JMSException { threadAwareContext.assertNotCompletionListenerThread(); threadAwareContext.assertNotMessageListenerThread(); if (closed) { return; } sessionFactory.close(); try { for (ActiveMQSession session : new HashSet<>(sessions)) { session.close(); } try { if (!tempQueues.isEmpty()) { // Remove any temporary queues for (SimpleString queueName : tempQueues) { if (!initialSession.isClosed()) { try { initialSession.deleteQueue(queueName); } catch (ActiveMQException ignore) { // Exception on deleting queue shouldn't prevent close from completing } } } } } finally { if (initialSession != null) { initialSession.close(); } } AccessController.doPrivileged(new PrivilegedAction() { @Override public Object run() { failoverListenerExecutor.shutdown(); return null; } }); closed = true; } catch (ActiveMQException e) { throw JMSExceptionHelper.convertFromActiveMQException(e); } } @Override public ConnectionConsumer createConnectionConsumer(final Destination destination, final String messageSelector, final ServerSessionPool sessionPool, final int maxMessages) throws JMSException { checkClosed(); checkTempQueues(destination); // We offer a RA, so no need to implement this for MDBs return null; } private void checkTempQueues(Destination destination) throws JMSException { ActiveMQDestination jbdest = (ActiveMQDestination) destination; if (jbdest.isTemporary() && !containsTemporaryQueue(jbdest.getSimpleAddress())) { throw new JMSException("Can not create consumer for temporary destination " + destination + " from another JMS connection"); } } @Override public ConnectionConsumer createDurableConnectionConsumer(final Topic topic, final String subscriptionName, final String messageSelector, final ServerSessionPool sessionPool, final int maxMessages) throws JMSException { checkClosed(); // As spec. section 4.11 if (connectionType == ActiveMQConnection.TYPE_QUEUE_CONNECTION) { String msg = "Cannot create a durable connection consumer on a QueueConnection"; throw new javax.jms.IllegalStateException(msg); } checkTempQueues(topic); // We offer RA, so no need for this return null; } @Override public Session createSession(int sessionMode) throws JMSException { checkClosed(); return createSessionInternal(false, sessionMode == Session.SESSION_TRANSACTED, sessionMode, ActiveMQSession.TYPE_GENERIC_SESSION); } @Override public Session createSession() throws JMSException { checkClosed(); return createSessionInternal(false, false, Session.AUTO_ACKNOWLEDGE, ActiveMQSession.TYPE_GENERIC_SESSION); } // QueueConnection implementation --------------------------------------------------------------- @Override public QueueSession createQueueSession(final boolean transacted, int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, checkAck(transacted, acknowledgeMode), ActiveMQSession.TYPE_QUEUE_SESSION); } /** * I'm keeping this as static as the same check will be done within RA. * This is to conform with TCK Tests where we must return ackMode exactly as they want if transacted=false */ public static int checkAck(boolean transacted, int acknowledgeMode) { if (!transacted && acknowledgeMode == Session.SESSION_TRANSACTED) { return Session.AUTO_ACKNOWLEDGE; } return acknowledgeMode; } @Override public ConnectionConsumer createConnectionConsumer(final Queue queue, final String messageSelector, final ServerSessionPool sessionPool, final int maxMessages) throws JMSException { checkClosed(); checkTempQueues(queue); return null; } // TopicConnection implementation --------------------------------------------------------------- @Override public TopicSession createTopicSession(final boolean transacted, final int acknowledgeMode) throws JMSException { checkClosed(); return createSessionInternal(false, transacted, checkAck(transacted, acknowledgeMode), ActiveMQSession.TYPE_TOPIC_SESSION); } @Override public ConnectionConsumer createConnectionConsumer(final Topic topic, final String messageSelector, final ServerSessionPool sessionPool, final int maxMessages) throws JMSException { checkClosed(); checkTempQueues(topic); return null; } @Override public ConnectionConsumer createSharedConnectionConsumer(Topic topic, String subscriptionName, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { return null; // we offer RA } @Override public ConnectionConsumer createSharedDurableConnectionConsumer(Topic topic, String subscriptionName, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { return null; // we offer RA } // Public --------------------------------------------------------------------------------------- /** * Sets a FailureListener for the session which is notified if a failure occurs on the session. * * @param listener the listener to add * @throws JMSException */ public void setFailoverListener(final FailoverEventListener listener) throws JMSException { checkClosed(); justCreated = false; this.failoverEventListener = listener; } /** * @return {@link FailoverEventListener} the current failover event listener for this connection * @throws JMSException */ public FailoverEventListener getFailoverListener() throws JMSException { checkClosed(); justCreated = false; return failoverEventListener; } public void addTemporaryQueue(final SimpleString queueAddress) { tempQueues.add(queueAddress); knownDestinations.add(queueAddress); } public void removeTemporaryQueue(final SimpleString queueAddress) { tempQueues.remove(queueAddress); } public void addKnownDestination(final SimpleString address) { knownDestinations.add(address); } public boolean containsKnownDestination(final SimpleString address) { return knownDestinations.contains(address); } public boolean containsTemporaryQueue(final SimpleString queueAddress) { return tempQueues.contains(queueAddress); } public boolean hasNoLocal() { return hasNoLocal; } public void setHasNoLocal() { hasNoLocal = true; } public SimpleString getUID() { return uid; } public void removeSession(final ActiveMQSession session) { sessions.remove(session); } public ClientSession getInitialSession() { return initialSession; } // Package protected ---------------------------------------------------------------------------- // Protected ------------------------------------------------------------------------------------ // In case the user forgets to close the connection manually @Override protected final void finalize() throws Throwable { if (!closed) { if (this.factoryReference.isFinalizeChecks()) { ActiveMQJMSClientLogger.LOGGER.connectionLeftOpen(creationStack); } close(); } } protected boolean isXA() { return false; } protected final ActiveMQSession createSessionInternal(final boolean isXA, final boolean transacted, int acknowledgeMode, final int type) throws JMSException { if (transacted) { acknowledgeMode = Session.SESSION_TRANSACTED; } try { ClientSession session; if (acknowledgeMode == Session.SESSION_TRANSACTED) { session = sessionFactory.createSession(username, password, isXA, false, false, sessionFactory.getServerLocator().isPreAcknowledge(), transactionBatchSize); } else if (acknowledgeMode == Session.AUTO_ACKNOWLEDGE) { session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), 0); } else if (acknowledgeMode == Session.DUPS_OK_ACKNOWLEDGE) { session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), dupsOKBatchSize); } else if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE) { session = sessionFactory.createSession(username, password, isXA, true, false, sessionFactory.getServerLocator().isPreAcknowledge(), transactionBatchSize); } else if (acknowledgeMode == ActiveMQJMSConstants.INDIVIDUAL_ACKNOWLEDGE) { session = sessionFactory.createSession(username, password, isXA, true, false, false, transactionBatchSize); } else if (acknowledgeMode == ActiveMQJMSConstants.PRE_ACKNOWLEDGE) { session = sessionFactory.createSession(username, password, isXA, true, false, true, transactionBatchSize); } else { throw new JMSRuntimeException("Invalid ackmode: " + acknowledgeMode); } justCreated = false; // Setting multiple times on different sessions doesn't matter since RemotingConnection // maintains // a set (no duplicates) session.addFailureListener(listener); session.addFailoverListener(failoverListener); ActiveMQSession jbs = createAMQSession(isXA, transacted, acknowledgeMode, session, type); sessions.add(jbs); if (started) { session.start(); } this.addSessionMetaData(session); return jbs; } catch (ActiveMQException e) { throw JMSExceptionHelper.convertFromActiveMQException(e); } } public ClientSessionFactory getSessionFactory() { return sessionFactory; } // Private -------------------------------------------------------------------------------------- /** * @param transacted * @param acknowledgeMode * @param session * @param type * @return */ protected ActiveMQSession createAMQSession(boolean isXA, boolean transacted, int acknowledgeMode, ClientSession session, int type) { if (isXA) { return new ActiveMQXASession(options, this, transacted, true, acknowledgeMode, session, type); } else { return new ActiveMQSession(options, this, transacted, false, acknowledgeMode, session, type); } } protected final void checkClosed() throws JMSException { if (closed) { throw new IllegalStateException("Connection is closed"); } } public void authorize() throws JMSException { try { initialSession = sessionFactory.createSession(username, password, false, false, false, false, 0); addSessionMetaData(initialSession); initialSession.addFailureListener(listener); initialSession.addFailoverListener(failoverListener); } catch (ActiveMQException me) { throw JMSExceptionHelper.convertFromActiveMQException(me); } } private void addSessionMetaData(ClientSession session) throws ActiveMQException { session.addMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY, ""); if (clientID != null) { session.addMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, clientID); } } public void setReference(ActiveMQConnectionFactory factory) { this.factoryReference = factory; } public boolean isStarted() { return started; } public String getDeserializationBlackList() { return this.factoryReference.getDeserializationBlackList(); } public String getDeserializationWhiteList() { return this.factoryReference.getDeserializationWhiteList(); } // Inner classes -------------------------------------------------------------------------------- private static class JMSFailureListener implements SessionFailureListener { private final WeakReference<ActiveMQConnection> connectionRef; JMSFailureListener(final ActiveMQConnection connection) { connectionRef = new WeakReference<>(connection); } @Override public synchronized void connectionFailed(final ActiveMQException me, boolean failedOver) { if (me == null) { return; } ActiveMQConnection conn = connectionRef.get(); if (conn != null) { try { final ExceptionListener exceptionListener = conn.getExceptionListener(); if (exceptionListener != null) { final JMSException je = new JMSException(me.toString(), failedOver ? EXCEPTION_FAILOVER : EXCEPTION_DISCONNECT); je.initCause(me); new Thread(new Runnable() { @Override public void run() { exceptionListener.onException(je); } }).start(); } } catch (JMSException e) { if (!conn.closed) { ActiveMQJMSClientLogger.LOGGER.errorCallingExcListener(e); } } } } @Override public void connectionFailed(final ActiveMQException me, boolean failedOver, String scaleDownTargetNodeID) { connectionFailed(me, failedOver); } @Override public void beforeReconnect(final ActiveMQException me) { } } private static class FailoverEventListenerImpl implements FailoverEventListener { private final WeakReference<ActiveMQConnection> connectionRef; FailoverEventListenerImpl(final ActiveMQConnection connection) { connectionRef = new WeakReference<>(connection); } @Override public void failoverEvent(final FailoverEventType eventType) { ActiveMQConnection conn = connectionRef.get(); if (conn != null) { try { final FailoverEventListener failoverListener = conn.getFailoverListener(); if (failoverListener != null) { conn.failoverListenerExecutor.execute(new Runnable() { @Override public void run() { failoverListener.failoverEvent(eventType); } }); } } catch (JMSException e) { if (!conn.closed) { ActiveMQJMSClientLogger.LOGGER.errorCallingFailoverListener(e); } } } } } }