/* * 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.transport.amqp.protocol; import static org.apache.activemq.transport.amqp.AmqpSupport.COPY; import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_FILTER_IDS; import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_NAME; import static org.apache.activemq.transport.amqp.AmqpSupport.LIFETIME_POLICY; import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_FILTER_IDS; import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_NAME; import static org.apache.activemq.transport.amqp.AmqpSupport.createDestination; import static org.apache.activemq.transport.amqp.AmqpSupport.findFilter; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jms.InvalidSelectorException; import org.apache.activemq.command.ActiveMQDestination; import org.apache.activemq.command.ActiveMQTempDestination; import org.apache.activemq.command.ConsumerId; import org.apache.activemq.command.ConsumerInfo; import org.apache.activemq.command.ExceptionResponse; import org.apache.activemq.command.LocalTransactionId; import org.apache.activemq.command.ProducerId; import org.apache.activemq.command.ProducerInfo; import org.apache.activemq.command.RemoveInfo; import org.apache.activemq.command.Response; import org.apache.activemq.command.SessionId; import org.apache.activemq.command.SessionInfo; import org.apache.activemq.command.TransactionId; import org.apache.activemq.selector.SelectorParser; import org.apache.activemq.transport.amqp.AmqpProtocolConverter; import org.apache.activemq.transport.amqp.AmqpProtocolException; import org.apache.activemq.transport.amqp.AmqpSupport; import org.apache.activemq.transport.amqp.ResponseHandler; import org.apache.activemq.util.IntrospectionSupport; import org.apache.qpid.proton.amqp.DescribedType; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.messaging.DeleteOnClose; import org.apache.qpid.proton.amqp.messaging.Target; import org.apache.qpid.proton.amqp.messaging.TerminusDurability; import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy; import org.apache.qpid.proton.amqp.transport.AmqpError; import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.engine.Receiver; import org.apache.qpid.proton.engine.Sender; import org.apache.qpid.proton.engine.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Wraps the AMQP Session and provides the services needed to manage the remote * peer requests for link establishment. */ public class AmqpSession implements AmqpResource { private static final Logger LOG = LoggerFactory.getLogger(AmqpSession.class); private final Map<ConsumerId, AmqpSender> consumers = new HashMap<>(); private final AmqpConnection connection; private final Session protonSession; private final SessionId sessionId; private boolean enlisted; private long nextProducerId = 0; private long nextConsumerId = 0; /** * Create new AmqpSession instance whose parent is the given AmqpConnection. * * @param connection * the parent connection for this session. * @param sessionId * the ActiveMQ SessionId that is used to identify this session. * @param session * the AMQP Session that this class manages. */ public AmqpSession(AmqpConnection connection, SessionId sessionId, Session session) { this.connection = connection; this.sessionId = sessionId; this.protonSession = session; } @Override public void open() { LOG.debug("Session {} opened", getSessionId()); getEndpoint().setContext(this); getEndpoint().setIncomingCapacity(Integer.MAX_VALUE); getEndpoint().open(); connection.sendToActiveMQ(new SessionInfo(getSessionId())); } @Override public void close() { LOG.debug("Session {} closed", getSessionId()); connection.sendToActiveMQ(new RemoveInfo(getSessionId()), new ResponseHandler() { @Override public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException { getEndpoint().setContext(null); getEndpoint().close(); getEndpoint().free(); } }); } /** * Commits all pending work for all resources managed under this session. * * @param txId * The specific TransactionId that is being committed. * * @throws Exception if an error occurs while attempting to commit work. */ public void commit(LocalTransactionId txId) throws Exception { for (AmqpSender consumer : consumers.values()) { consumer.commit(txId); } enlisted = false; } /** * Rolls back any pending work being down under this session. * * @param txId * The specific TransactionId that is being rolled back. * * @throws Exception if an error occurs while attempting to roll back work. */ public void rollback(LocalTransactionId txId) throws Exception { for (AmqpSender consumer : consumers.values()) { consumer.rollback(txId); } enlisted = false; } /** * Used to direct all Session managed Senders to push any queued Messages * out to the remote peer. * * @throws Exception if an error occurs while flushing the messages. */ public void flushPendingMessages() throws Exception { for (AmqpSender consumer : consumers.values()) { consumer.pumpOutbound(); } } public void createCoordinator(final Receiver protonReceiver) throws Exception { AmqpTransactionCoordinator txCoordinator = new AmqpTransactionCoordinator(this, protonReceiver); txCoordinator.flow(connection.getConfiguredReceiverCredit()); txCoordinator.open(); } public void createReceiver(final Receiver protonReceiver) throws Exception { org.apache.qpid.proton.amqp.transport.Target remoteTarget = protonReceiver.getRemoteTarget(); ProducerInfo producerInfo = new ProducerInfo(getNextProducerId()); final AmqpReceiver receiver = new AmqpReceiver(this, protonReceiver, producerInfo); LOG.debug("opening new receiver {} on link: {}", producerInfo.getProducerId(), protonReceiver.getName()); try { Target target = (Target) remoteTarget; ActiveMQDestination destination = null; String targetNodeName = target.getAddress(); if (target.getDynamic()) { destination = connection.createTemporaryDestination(protonReceiver, target.getCapabilities()); Map<Symbol, Object> dynamicNodeProperties = new HashMap<>(); dynamicNodeProperties.put(LIFETIME_POLICY, DeleteOnClose.getInstance()); // Currently we only support temporary destinations with delete on close lifetime policy. Target actualTarget = new Target(); actualTarget.setAddress(destination.getQualifiedName()); actualTarget.setCapabilities(AmqpSupport.getDestinationTypeSymbol(destination)); actualTarget.setDynamic(true); actualTarget.setDynamicNodeProperties(dynamicNodeProperties); protonReceiver.setTarget(actualTarget); receiver.addCloseAction(new Runnable() { @Override public void run() { connection.deleteTemporaryDestination((ActiveMQTempDestination) receiver.getDestination()); } }); } else if (targetNodeName != null && !targetNodeName.isEmpty()) { destination = createDestination(remoteTarget); if (destination.isTemporary()) { String connectionId = ((ActiveMQTempDestination) destination).getConnectionId(); if (connectionId == null) { throw new AmqpProtocolException(AmqpError.PRECONDITION_FAILED.toString(), "Not a broker created temp destination"); } } } Symbol[] remoteDesiredCapabilities = protonReceiver.getRemoteDesiredCapabilities(); if (remoteDesiredCapabilities != null) { List<Symbol> list = Arrays.asList(remoteDesiredCapabilities); if (list.contains(AmqpSupport.DELAYED_DELIVERY)) { protonReceiver.setOfferedCapabilities(new Symbol[] { AmqpSupport.DELAYED_DELIVERY }); } } receiver.setDestination(destination); connection.sendToActiveMQ(producerInfo, new ResponseHandler() { @Override public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException { if (response.isException()) { ErrorCondition error = null; Throwable exception = ((ExceptionResponse) response).getException(); if (exception instanceof SecurityException) { error = new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, exception.getMessage()); } else { error = new ErrorCondition(AmqpError.INTERNAL_ERROR, exception.getMessage()); } receiver.close(error); } else { receiver.flow(connection.getConfiguredReceiverCredit()); receiver.open(); } pumpProtonToSocket(); } }); } catch (AmqpProtocolException exception) { receiver.close(new ErrorCondition(Symbol.getSymbol(exception.getSymbolicName()), exception.getMessage())); } } @SuppressWarnings("unchecked") public void createSender(final Sender protonSender) throws Exception { org.apache.qpid.proton.amqp.messaging.Source source = (org.apache.qpid.proton.amqp.messaging.Source) protonSender.getRemoteSource(); ConsumerInfo consumerInfo = new ConsumerInfo(getNextConsumerId()); final AmqpSender sender = new AmqpSender(this, protonSender, consumerInfo); LOG.debug("opening new sender {} on link: {}", consumerInfo.getConsumerId(), protonSender.getName()); try { final Map<Symbol, Object> supportedFilters = new HashMap<>(); protonSender.setContext(sender); boolean noLocal = false; String selector = null; if (source != null) { Map.Entry<Symbol, DescribedType> filter = findFilter(source.getFilter(), JMS_SELECTOR_FILTER_IDS); if (filter != null) { selector = filter.getValue().getDescribed().toString(); // Validate the Selector. try { SelectorParser.parse(selector); } catch (InvalidSelectorException e) { sender.close(new ErrorCondition(AmqpError.INVALID_FIELD, e.getMessage())); return; } supportedFilters.put(filter.getKey(), filter.getValue()); } filter = findFilter(source.getFilter(), NO_LOCAL_FILTER_IDS); if (filter != null) { noLocal = true; supportedFilters.put(filter.getKey(), filter.getValue()); } } ActiveMQDestination destination; if (source == null) { // Attempt to recover previous subscription ConsumerInfo storedInfo = connection.lookupSubscription(protonSender.getName()); if (storedInfo != null) { destination = storedInfo.getDestination(); source = new org.apache.qpid.proton.amqp.messaging.Source(); source.setAddress(destination.getQualifiedName()); source.setDurable(TerminusDurability.UNSETTLED_STATE); source.setExpiryPolicy(TerminusExpiryPolicy.NEVER); source.setDistributionMode(COPY); if (storedInfo.isNoLocal()) { supportedFilters.put(NO_LOCAL_NAME, AmqpNoLocalFilter.NO_LOCAL); } if (storedInfo.getSelector() != null && !storedInfo.getSelector().trim().equals("")) { supportedFilters.put(JMS_SELECTOR_NAME, new AmqpJmsSelectorFilter(storedInfo.getSelector())); } } else { sender.close(new ErrorCondition(AmqpError.NOT_FOUND, "Unknown subscription link: " + protonSender.getName())); return; } } else if (source.getDynamic()) { destination = connection.createTemporaryDestination(protonSender, source.getCapabilities()); Map<Symbol, Object> dynamicNodeProperties = new HashMap<>(); dynamicNodeProperties.put(LIFETIME_POLICY, DeleteOnClose.getInstance()); // Currently we only support temporary destinations with delete on close lifetime policy. source = new org.apache.qpid.proton.amqp.messaging.Source(); source.setAddress(destination.getQualifiedName()); source.setCapabilities(AmqpSupport.getDestinationTypeSymbol(destination)); source.setDynamic(true); source.setDynamicNodeProperties(dynamicNodeProperties); sender.addCloseAction(new Runnable() { @Override public void run() { connection.deleteTemporaryDestination((ActiveMQTempDestination) sender.getDestination()); } }); } else { destination = createDestination(source); if (destination.isTemporary()) { String connectionId = ((ActiveMQTempDestination) destination).getConnectionId(); if (connectionId == null) { throw new AmqpProtocolException(AmqpError.INVALID_FIELD.toString(), "Not a broker created temp destination"); } } } source.setFilter(supportedFilters.isEmpty() ? null : supportedFilters); protonSender.setSource(source); int senderCredit = protonSender.getRemoteCredit(); // Allows the options on the destination to configure the consumerInfo if (destination.getOptions() != null) { Map<String, Object> options = IntrospectionSupport.extractProperties( new HashMap<String, Object>(destination.getOptions()), "consumer."); IntrospectionSupport.setProperties(consumerInfo, options); if (options.size() > 0) { String msg = "There are " + options.size() + " consumer options that couldn't be set on the consumer." + " Check the options are spelled correctly." + " Unknown parameters=[" + options + "]." + " This consumer cannot be started."; LOG.warn(msg); throw new AmqpProtocolException(AmqpError.INVALID_FIELD.toString(), msg); } } consumerInfo.setSelector(selector); consumerInfo.setNoRangeAcks(true); consumerInfo.setDestination(destination); consumerInfo.setPrefetchSize(senderCredit >= 0 ? senderCredit : 0); consumerInfo.setDispatchAsync(true); consumerInfo.setNoLocal(noLocal); if (source.getDistributionMode() == COPY && destination.isQueue()) { consumerInfo.setBrowser(true); } if ((TerminusDurability.UNSETTLED_STATE.equals(source.getDurable()) || TerminusDurability.CONFIGURATION.equals(source.getDurable())) && destination.isTopic()) { consumerInfo.setSubscriptionName(protonSender.getName()); } connection.sendToActiveMQ(consumerInfo, new ResponseHandler() { @Override public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException { if (response.isException()) { ErrorCondition error = null; Throwable exception = ((ExceptionResponse) response).getException(); if (exception instanceof SecurityException) { error = new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, exception.getMessage()); } else if (exception instanceof InvalidSelectorException) { error = new ErrorCondition(AmqpError.INVALID_FIELD, exception.getMessage()); } else { error = new ErrorCondition(AmqpError.INTERNAL_ERROR, exception.getMessage()); } sender.close(error); } else { sender.open(); } pumpProtonToSocket(); } }); } catch (AmqpProtocolException e) { sender.close(new ErrorCondition(Symbol.getSymbol(e.getSymbolicName()), e.getMessage())); } } /** * Send all pending work out to the remote peer. */ public void pumpProtonToSocket() { connection.pumpProtonToSocket(); } public void registerSender(ConsumerId consumerId, AmqpSender sender) { consumers.put(consumerId, sender); connection.registerSender(consumerId, sender); } public void unregisterSender(ConsumerId consumerId) { consumers.remove(consumerId); connection.unregisterSender(consumerId); } public void enlist(TransactionId txId) { if (!enlisted) { connection.getTxCoordinator(txId).enlist(this); enlisted = true; } } //----- Configuration accessors ------------------------------------------// public AmqpConnection getConnection() { return connection; } public SessionId getSessionId() { return sessionId; } public Session getEndpoint() { return protonSession; } public long getMaxFrameSize() { return connection.getMaxFrameSize(); } //----- Internal Implementation ------------------------------------------// private ConsumerId getNextConsumerId() { return new ConsumerId(sessionId, nextConsumerId++); } private ProducerId getNextProducerId() { return new ProducerId(sessionId, nextProducerId++); } }