001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.transport.amqp.protocol;
018
019import static org.apache.activemq.transport.amqp.AmqpSupport.COPY;
020import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_FILTER_IDS;
021import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_NAME;
022import static org.apache.activemq.transport.amqp.AmqpSupport.LIFETIME_POLICY;
023import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_FILTER_IDS;
024import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_NAME;
025import static org.apache.activemq.transport.amqp.AmqpSupport.createDestination;
026import static org.apache.activemq.transport.amqp.AmqpSupport.findFilter;
027
028import java.io.IOException;
029import java.util.Arrays;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import javax.jms.InvalidSelectorException;
035
036import org.apache.activemq.command.ActiveMQDestination;
037import org.apache.activemq.command.ActiveMQTempDestination;
038import org.apache.activemq.command.ConsumerId;
039import org.apache.activemq.command.ConsumerInfo;
040import org.apache.activemq.command.ExceptionResponse;
041import org.apache.activemq.command.LocalTransactionId;
042import org.apache.activemq.command.ProducerId;
043import org.apache.activemq.command.ProducerInfo;
044import org.apache.activemq.command.RemoveInfo;
045import org.apache.activemq.command.Response;
046import org.apache.activemq.command.SessionId;
047import org.apache.activemq.command.SessionInfo;
048import org.apache.activemq.command.TransactionId;
049import org.apache.activemq.selector.SelectorParser;
050import org.apache.activemq.transport.amqp.AmqpProtocolConverter;
051import org.apache.activemq.transport.amqp.AmqpProtocolException;
052import org.apache.activemq.transport.amqp.AmqpSupport;
053import org.apache.activemq.transport.amqp.ResponseHandler;
054import org.apache.activemq.util.IntrospectionSupport;
055import org.apache.qpid.proton.amqp.DescribedType;
056import org.apache.qpid.proton.amqp.Symbol;
057import org.apache.qpid.proton.amqp.messaging.DeleteOnClose;
058import org.apache.qpid.proton.amqp.messaging.Target;
059import org.apache.qpid.proton.amqp.messaging.TerminusDurability;
060import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
061import org.apache.qpid.proton.amqp.transport.AmqpError;
062import org.apache.qpid.proton.amqp.transport.ErrorCondition;
063import org.apache.qpid.proton.engine.Receiver;
064import org.apache.qpid.proton.engine.Sender;
065import org.apache.qpid.proton.engine.Session;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069/**
070 * Wraps the AMQP Session and provides the services needed to manage the remote
071 * peer requests for link establishment.
072 */
073public class AmqpSession implements AmqpResource {
074
075    private static final Logger LOG = LoggerFactory.getLogger(AmqpSession.class);
076
077    private final Map<ConsumerId, AmqpSender> consumers = new HashMap<>();
078
079    private final AmqpConnection connection;
080    private final Session protonSession;
081    private final SessionId sessionId;
082
083    private boolean enlisted;
084    private long nextProducerId = 0;
085    private long nextConsumerId = 0;
086
087    /**
088     * Create new AmqpSession instance whose parent is the given AmqpConnection.
089     *
090     * @param connection
091     *        the parent connection for this session.
092     * @param sessionId
093     *        the ActiveMQ SessionId that is used to identify this session.
094     * @param session
095     *        the AMQP Session that this class manages.
096     */
097    public AmqpSession(AmqpConnection connection, SessionId sessionId, Session session) {
098        this.connection = connection;
099        this.sessionId = sessionId;
100        this.protonSession = session;
101    }
102
103    @Override
104    public void open() {
105        LOG.debug("Session {} opened", getSessionId());
106
107        getEndpoint().setContext(this);
108        getEndpoint().setIncomingCapacity(Integer.MAX_VALUE);
109        getEndpoint().open();
110
111        connection.sendToActiveMQ(new SessionInfo(getSessionId()));
112    }
113
114    @Override
115    public void close() {
116        LOG.debug("Session {} closed", getSessionId());
117
118        connection.sendToActiveMQ(new RemoveInfo(getSessionId()), new ResponseHandler() {
119
120            @Override
121            public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
122                getEndpoint().setContext(null);
123                getEndpoint().close();
124                getEndpoint().free();
125            }
126        });
127    }
128
129    /**
130     * Commits all pending work for all resources managed under this session.
131     *
132     * @param txId
133     *      The specific TransactionId that is being committed.
134     *
135     * @throws Exception if an error occurs while attempting to commit work.
136     */
137    public void commit(LocalTransactionId txId) throws Exception {
138        for (AmqpSender consumer : consumers.values()) {
139            consumer.commit(txId);
140        }
141
142        enlisted = false;
143    }
144
145    /**
146     * Rolls back any pending work being down under this session.
147     *
148     * @param txId
149     *      The specific TransactionId that is being rolled back.
150     *
151     * @throws Exception if an error occurs while attempting to roll back work.
152     */
153    public void rollback(LocalTransactionId txId) throws Exception {
154        for (AmqpSender consumer : consumers.values()) {
155            consumer.rollback(txId);
156        }
157
158        enlisted = false;
159    }
160
161    /**
162     * Used to direct all Session managed Senders to push any queued Messages
163     * out to the remote peer.
164     *
165     * @throws Exception if an error occurs while flushing the messages.
166     */
167    public void flushPendingMessages() throws Exception {
168        for (AmqpSender consumer : consumers.values()) {
169            consumer.pumpOutbound();
170        }
171    }
172
173    public void createCoordinator(final Receiver protonReceiver) throws Exception {
174        AmqpTransactionCoordinator txCoordinator = new AmqpTransactionCoordinator(this, protonReceiver);
175        txCoordinator.flow(connection.getConfiguredReceiverCredit());
176        txCoordinator.open();
177    }
178
179    public void createReceiver(final Receiver protonReceiver) throws Exception {
180        org.apache.qpid.proton.amqp.transport.Target remoteTarget = protonReceiver.getRemoteTarget();
181
182        ProducerInfo producerInfo = new ProducerInfo(getNextProducerId());
183        final AmqpReceiver receiver = new AmqpReceiver(this, protonReceiver, producerInfo);
184
185        LOG.debug("opening new receiver {} on link: {}", producerInfo.getProducerId(), protonReceiver.getName());
186
187        try {
188            Target target = (Target) remoteTarget;
189            ActiveMQDestination destination = null;
190            String targetNodeName = target.getAddress();
191
192            if (target.getDynamic()) {
193                destination = connection.createTemporaryDestination(protonReceiver, target.getCapabilities());
194
195                Map<Symbol, Object> dynamicNodeProperties = new HashMap<>();
196                dynamicNodeProperties.put(LIFETIME_POLICY, DeleteOnClose.getInstance());
197
198                // Currently we only support temporary destinations with delete on close lifetime policy.
199                Target actualTarget = new Target();
200                actualTarget.setAddress(destination.getQualifiedName());
201                actualTarget.setCapabilities(AmqpSupport.getDestinationTypeSymbol(destination));
202                actualTarget.setDynamic(true);
203                actualTarget.setDynamicNodeProperties(dynamicNodeProperties);
204
205                protonReceiver.setTarget(actualTarget);
206                receiver.addCloseAction(new Runnable() {
207
208                    @Override
209                    public void run() {
210                        connection.deleteTemporaryDestination((ActiveMQTempDestination) receiver.getDestination());
211                    }
212                });
213            } else if (targetNodeName != null && !targetNodeName.isEmpty()) {
214                destination = createDestination(remoteTarget);
215                if (destination.isTemporary()) {
216                    String connectionId = ((ActiveMQTempDestination) destination).getConnectionId();
217                    if (connectionId == null) {
218                        throw new AmqpProtocolException(AmqpError.PRECONDITION_FAILED.toString(), "Not a broker created temp destination");
219                    }
220                }
221            }
222
223            Symbol[] remoteDesiredCapabilities = protonReceiver.getRemoteDesiredCapabilities();
224            if (remoteDesiredCapabilities != null) {
225                List<Symbol> list = Arrays.asList(remoteDesiredCapabilities);
226                if (list.contains(AmqpSupport.DELAYED_DELIVERY)) {
227                    protonReceiver.setOfferedCapabilities(new Symbol[] { AmqpSupport.DELAYED_DELIVERY });
228                }
229            }
230
231            receiver.setDestination(destination);
232            connection.sendToActiveMQ(producerInfo, new ResponseHandler() {
233                @Override
234                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
235                    if (response.isException()) {
236                        ErrorCondition error = null;
237                        Throwable exception = ((ExceptionResponse) response).getException();
238                        if (exception instanceof SecurityException) {
239                            error = new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, exception.getMessage());
240                        } else {
241                            error = new ErrorCondition(AmqpError.INTERNAL_ERROR, exception.getMessage());
242                        }
243
244                        receiver.close(error);
245                    } else {
246                        receiver.flow(connection.getConfiguredReceiverCredit());
247                        receiver.open();
248                    }
249                    pumpProtonToSocket();
250                }
251            });
252
253        } catch (AmqpProtocolException exception) {
254            receiver.close(new ErrorCondition(Symbol.getSymbol(exception.getSymbolicName()), exception.getMessage()));
255        }
256    }
257
258    @SuppressWarnings("unchecked")
259    public void createSender(final Sender protonSender) throws Exception {
260        org.apache.qpid.proton.amqp.messaging.Source source = (org.apache.qpid.proton.amqp.messaging.Source) protonSender.getRemoteSource();
261
262        ConsumerInfo consumerInfo = new ConsumerInfo(getNextConsumerId());
263        final AmqpSender sender = new AmqpSender(this, protonSender, consumerInfo);
264
265        LOG.debug("opening new sender {} on link: {}", consumerInfo.getConsumerId(), protonSender.getName());
266
267        try {
268            final Map<Symbol, Object> supportedFilters = new HashMap<>();
269            protonSender.setContext(sender);
270
271            boolean noLocal = false;
272            String selector = null;
273
274            if (source != null) {
275                Map.Entry<Symbol, DescribedType> filter = findFilter(source.getFilter(), JMS_SELECTOR_FILTER_IDS);
276                if (filter != null) {
277                    selector = filter.getValue().getDescribed().toString();
278                    // Validate the Selector.
279                    try {
280                        SelectorParser.parse(selector);
281                    } catch (InvalidSelectorException e) {
282                        sender.close(new ErrorCondition(AmqpError.INVALID_FIELD, e.getMessage()));
283                        return;
284                    }
285
286                    supportedFilters.put(filter.getKey(), filter.getValue());
287                }
288
289                filter = findFilter(source.getFilter(), NO_LOCAL_FILTER_IDS);
290                if (filter != null) {
291                    noLocal = true;
292                    supportedFilters.put(filter.getKey(), filter.getValue());
293                }
294            }
295
296            ActiveMQDestination destination;
297            if (source == null) {
298                // Attempt to recover previous subscription
299                ConsumerInfo storedInfo = connection.lookupSubscription(protonSender.getName());
300
301                if (storedInfo != null) {
302                    destination = storedInfo.getDestination();
303
304                    source = new org.apache.qpid.proton.amqp.messaging.Source();
305                    source.setAddress(destination.getQualifiedName());
306                    source.setDurable(TerminusDurability.UNSETTLED_STATE);
307                    source.setExpiryPolicy(TerminusExpiryPolicy.NEVER);
308                    source.setDistributionMode(COPY);
309
310                    if (storedInfo.isNoLocal()) {
311                        supportedFilters.put(NO_LOCAL_NAME, AmqpNoLocalFilter.NO_LOCAL);
312                    }
313
314                    if (storedInfo.getSelector() != null && !storedInfo.getSelector().trim().equals("")) {
315                        supportedFilters.put(JMS_SELECTOR_NAME, new AmqpJmsSelectorFilter(storedInfo.getSelector()));
316                    }
317                } else {
318                    sender.close(new ErrorCondition(AmqpError.NOT_FOUND, "Unknown subscription link: " + protonSender.getName()));
319                    return;
320                }
321            } else if (source.getDynamic()) {
322                destination = connection.createTemporaryDestination(protonSender, source.getCapabilities());
323
324                Map<Symbol, Object> dynamicNodeProperties = new HashMap<>();
325                dynamicNodeProperties.put(LIFETIME_POLICY, DeleteOnClose.getInstance());
326
327                // Currently we only support temporary destinations with delete on close lifetime policy.
328                source = new org.apache.qpid.proton.amqp.messaging.Source();
329                source.setAddress(destination.getQualifiedName());
330                source.setCapabilities(AmqpSupport.getDestinationTypeSymbol(destination));
331                source.setDynamic(true);
332                source.setDynamicNodeProperties(dynamicNodeProperties);
333
334                sender.addCloseAction(new Runnable() {
335
336                    @Override
337                    public void run() {
338                        connection.deleteTemporaryDestination((ActiveMQTempDestination) sender.getDestination());
339                    }
340                });
341            } else {
342                destination = createDestination(source);
343                if (destination.isTemporary()) {
344                    String connectionId = ((ActiveMQTempDestination) destination).getConnectionId();
345                    if (connectionId == null) {
346                        throw new AmqpProtocolException(AmqpError.INVALID_FIELD.toString(), "Not a broker created temp destination");
347                    }
348                }
349            }
350
351            source.setFilter(supportedFilters.isEmpty() ? null : supportedFilters);
352            protonSender.setSource(source);
353
354            int senderCredit = protonSender.getRemoteCredit();
355
356            // Allows the options on the destination to configure the consumerInfo
357            if (destination.getOptions() != null) {
358                Map<String, Object> options = IntrospectionSupport.extractProperties(
359                    new HashMap<String, Object>(destination.getOptions()), "consumer.");
360                IntrospectionSupport.setProperties(consumerInfo, options);
361                if (options.size() > 0) {
362                    String msg = "There are " + options.size()
363                        + " consumer options that couldn't be set on the consumer."
364                        + " Check the options are spelled correctly."
365                        + " Unknown parameters=[" + options + "]."
366                        + " This consumer cannot be started.";
367                    LOG.warn(msg);
368                    throw new AmqpProtocolException(AmqpError.INVALID_FIELD.toString(), msg);
369                }
370            }
371
372            consumerInfo.setSelector(selector);
373            consumerInfo.setNoRangeAcks(true);
374            consumerInfo.setDestination(destination);
375            consumerInfo.setPrefetchSize(senderCredit >= 0 ? senderCredit : 0);
376            consumerInfo.setDispatchAsync(true);
377            consumerInfo.setNoLocal(noLocal);
378
379            if (source.getDistributionMode() == COPY && destination.isQueue()) {
380                consumerInfo.setBrowser(true);
381            }
382
383            if ((TerminusDurability.UNSETTLED_STATE.equals(source.getDurable()) ||
384                 TerminusDurability.CONFIGURATION.equals(source.getDurable())) && destination.isTopic()) {
385                consumerInfo.setSubscriptionName(protonSender.getName());
386            }
387
388            connection.sendToActiveMQ(consumerInfo, new ResponseHandler() {
389                @Override
390                public void onResponse(AmqpProtocolConverter converter, Response response) throws IOException {
391                    if (response.isException()) {
392                        ErrorCondition error = null;
393                        Throwable exception = ((ExceptionResponse) response).getException();
394                        if (exception instanceof SecurityException) {
395                            error = new ErrorCondition(AmqpError.UNAUTHORIZED_ACCESS, exception.getMessage());
396                        } else if (exception instanceof InvalidSelectorException) {
397                            error = new ErrorCondition(AmqpError.INVALID_FIELD, exception.getMessage());
398                        } else {
399                            error = new ErrorCondition(AmqpError.INTERNAL_ERROR, exception.getMessage());
400                        }
401
402                        sender.close(error);
403                    } else {
404                        sender.open();
405                    }
406                    pumpProtonToSocket();
407                }
408            });
409
410        } catch (AmqpProtocolException e) {
411            sender.close(new ErrorCondition(Symbol.getSymbol(e.getSymbolicName()), e.getMessage()));
412        }
413    }
414
415    /**
416     * Send all pending work out to the remote peer.
417     */
418    public void pumpProtonToSocket() {
419        connection.pumpProtonToSocket();
420    }
421
422    public void registerSender(ConsumerId consumerId, AmqpSender sender) {
423        consumers.put(consumerId, sender);
424        connection.registerSender(consumerId, sender);
425    }
426
427    public void unregisterSender(ConsumerId consumerId) {
428        consumers.remove(consumerId);
429        connection.unregisterSender(consumerId);
430    }
431
432    public void enlist(TransactionId txId) {
433        if (!enlisted) {
434            connection.getTxCoordinator(txId).enlist(this);
435            enlisted = true;
436        }
437    }
438
439    //----- Configuration accessors ------------------------------------------//
440
441    public AmqpConnection getConnection() {
442        return connection;
443    }
444
445    public SessionId getSessionId() {
446        return sessionId;
447    }
448
449    public Session getEndpoint() {
450        return protonSession;
451    }
452
453    public long getMaxFrameSize() {
454        return connection.getMaxFrameSize();
455    }
456
457    //----- Internal Implementation ------------------------------------------//
458
459    private ConsumerId getNextConsumerId() {
460        return new ConsumerId(sessionId, nextConsumerId++);
461    }
462
463    private ProducerId getNextProducerId() {
464        return new ProducerId(sessionId, nextProducerId++);
465    }
466}