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.stomp;
018
019import java.io.BufferedReader;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.Map;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.atomic.AtomicBoolean;
030
031import javax.jms.JMSException;
032
033import org.apache.activemq.ActiveMQPrefetchPolicy;
034import org.apache.activemq.advisory.AdvisorySupport;
035import org.apache.activemq.broker.BrokerContext;
036import org.apache.activemq.broker.BrokerContextAware;
037import org.apache.activemq.command.ActiveMQDestination;
038import org.apache.activemq.command.ActiveMQMessage;
039import org.apache.activemq.command.ActiveMQTempQueue;
040import org.apache.activemq.command.ActiveMQTempTopic;
041import org.apache.activemq.command.Command;
042import org.apache.activemq.command.CommandTypes;
043import org.apache.activemq.command.ConnectionError;
044import org.apache.activemq.command.ConnectionId;
045import org.apache.activemq.command.ConnectionInfo;
046import org.apache.activemq.command.ConsumerControl;
047import org.apache.activemq.command.ConsumerId;
048import org.apache.activemq.command.ConsumerInfo;
049import org.apache.activemq.command.DestinationInfo;
050import org.apache.activemq.command.ExceptionResponse;
051import org.apache.activemq.command.LocalTransactionId;
052import org.apache.activemq.command.MessageAck;
053import org.apache.activemq.command.MessageDispatch;
054import org.apache.activemq.command.MessageId;
055import org.apache.activemq.command.ProducerId;
056import org.apache.activemq.command.ProducerInfo;
057import org.apache.activemq.command.RemoveSubscriptionInfo;
058import org.apache.activemq.command.Response;
059import org.apache.activemq.command.SessionId;
060import org.apache.activemq.command.SessionInfo;
061import org.apache.activemq.command.ShutdownInfo;
062import org.apache.activemq.command.TransactionId;
063import org.apache.activemq.command.TransactionInfo;
064import org.apache.activemq.util.ByteArrayOutputStream;
065import org.apache.activemq.util.FactoryFinder;
066import org.apache.activemq.util.IOExceptionSupport;
067import org.apache.activemq.util.IdGenerator;
068import org.apache.activemq.util.IntrospectionSupport;
069import org.apache.activemq.util.LongSequenceGenerator;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073/**
074 * @author <a href="http://hiramchirino.com">chirino</a>
075 */
076public class ProtocolConverter {
077
078    private static final Logger LOG = LoggerFactory.getLogger(ProtocolConverter.class);
079
080    private static final IdGenerator CONNECTION_ID_GENERATOR = new IdGenerator();
081
082    private static final String BROKER_VERSION;
083    private static final StompFrame ping = new StompFrame(Stomp.Commands.KEEPALIVE);
084
085    static {
086        InputStream in = null;
087        String version = "5.6.0";
088        if ((in = ProtocolConverter.class.getResourceAsStream("/org/apache/activemq/version.txt")) != null) {
089            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
090            try {
091                version = reader.readLine();
092            } catch(Exception e) {
093            }
094        }
095        BROKER_VERSION = version;
096    }
097
098    private final ConnectionId connectionId = new ConnectionId(CONNECTION_ID_GENERATOR.generateId());
099    private final SessionId sessionId = new SessionId(connectionId, -1);
100    private final ProducerId producerId = new ProducerId(sessionId, 1);
101
102    private final LongSequenceGenerator consumerIdGenerator = new LongSequenceGenerator();
103    private final LongSequenceGenerator messageIdGenerator = new LongSequenceGenerator();
104    private final LongSequenceGenerator transactionIdGenerator = new LongSequenceGenerator();
105    private final LongSequenceGenerator tempDestinationGenerator = new LongSequenceGenerator();
106
107    private final ConcurrentHashMap<Integer, ResponseHandler> resposeHandlers = new ConcurrentHashMap<Integer, ResponseHandler>();
108    private final ConcurrentHashMap<ConsumerId, StompSubscription> subscriptionsByConsumerId = new ConcurrentHashMap<ConsumerId, StompSubscription>();
109    private final ConcurrentHashMap<String, StompSubscription> subscriptions = new ConcurrentHashMap<String, StompSubscription>();
110    private final ConcurrentHashMap<String, ActiveMQDestination> tempDestinations = new ConcurrentHashMap<String, ActiveMQDestination>();
111    private final ConcurrentHashMap<String, String> tempDestinationAmqToStompMap = new ConcurrentHashMap<String, String>();
112    private final Map<String, LocalTransactionId> transactions = new ConcurrentHashMap<String, LocalTransactionId>();
113    private final StompTransport stompTransport;
114
115    private final ConcurrentHashMap<String, AckEntry> pedingAcks = new ConcurrentHashMap<String, AckEntry>();
116    private final IdGenerator ACK_ID_GENERATOR = new IdGenerator();
117
118    private final Object commnadIdMutex = new Object();
119    private int lastCommandId;
120    private final AtomicBoolean connected = new AtomicBoolean(false);
121    private final FrameTranslator frameTranslator = new LegacyFrameTranslator();
122    private final FactoryFinder FRAME_TRANSLATOR_FINDER = new FactoryFinder("META-INF/services/org/apache/activemq/transport/frametranslator/");
123    private final BrokerContext brokerContext;
124    private String version = "1.0";
125    private long hbReadInterval;
126    private long hbWriteInterval;
127    private float hbGracePeriodMultiplier = 1.0f;
128    private String defaultHeartBeat = Stomp.DEFAULT_HEART_BEAT;
129
130    private static class AckEntry {
131
132        private final String messageId;
133        private final StompSubscription subscription;
134
135        public AckEntry(String messageId, StompSubscription subscription) {
136            this.messageId = messageId;
137            this.subscription = subscription;
138        }
139
140        public MessageAck onMessageAck(TransactionId transactionId) {
141            return subscription.onStompMessageAck(messageId, transactionId);
142        }
143
144        public MessageAck onMessageNack(TransactionId transactionId) throws ProtocolException {
145            return subscription.onStompMessageNack(messageId, transactionId);
146        }
147
148        public String getMessageId() {
149            return this.messageId;
150        }
151
152        @SuppressWarnings("unused")
153        public StompSubscription getSubscription() {
154            return this.subscription;
155        }
156    }
157
158    public ProtocolConverter(StompTransport stompTransport, BrokerContext brokerContext) {
159        this.stompTransport = stompTransport;
160        this.brokerContext = brokerContext;
161    }
162
163    protected int generateCommandId() {
164        synchronized (commnadIdMutex) {
165            return lastCommandId++;
166        }
167    }
168
169    protected ResponseHandler createResponseHandler(final StompFrame command) {
170        final String receiptId = command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
171        if (receiptId != null) {
172            return new ResponseHandler() {
173                @Override
174                public void onResponse(ProtocolConverter converter, Response response) throws IOException {
175                    if (response.isException()) {
176                        // Generally a command can fail.. but that does not invalidate the connection.
177                        // We report back the failure but we don't close the connection.
178                        Throwable exception = ((ExceptionResponse)response).getException();
179                        handleException(exception, command);
180                    } else {
181                        StompFrame sc = new StompFrame();
182                        sc.setAction(Stomp.Responses.RECEIPT);
183                        sc.setHeaders(new HashMap<String, String>(1));
184                        sc.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
185                        stompTransport.sendToStomp(sc);
186                    }
187                }
188            };
189        }
190        return null;
191    }
192
193    protected void sendToActiveMQ(Command command, ResponseHandler handler) {
194        command.setCommandId(generateCommandId());
195        if (handler != null) {
196            command.setResponseRequired(true);
197            resposeHandlers.put(Integer.valueOf(command.getCommandId()), handler);
198        }
199        stompTransport.sendToActiveMQ(command);
200    }
201
202    protected void sendToStomp(StompFrame command) throws IOException {
203        stompTransport.sendToStomp(command);
204    }
205
206    protected FrameTranslator findTranslator(String header) {
207        return findTranslator(header, null, false);
208    }
209
210    protected FrameTranslator findTranslator(String header, ActiveMQDestination destination, boolean advisory) {
211        FrameTranslator translator = frameTranslator;
212        try {
213            if (header != null) {
214                translator = (FrameTranslator) FRAME_TRANSLATOR_FINDER.newInstance(header);
215            } else {
216                if (destination != null && (advisory || AdvisorySupport.isAdvisoryTopic(destination))) {
217                    translator = new JmsFrameTranslator();
218                }
219            }
220        } catch (Exception ignore) {
221            // if anything goes wrong use the default translator
222        }
223
224        if (translator instanceof BrokerContextAware) {
225            ((BrokerContextAware)translator).setBrokerContext(brokerContext);
226        }
227
228        return translator;
229    }
230
231    /**
232     * Convert a STOMP command
233     *
234     * @param command
235     */
236    public void onStompCommand(StompFrame command) throws IOException, JMSException {
237        try {
238
239            if (command.getClass() == StompFrameError.class) {
240                throw ((StompFrameError)command).getException();
241            }
242
243            String action = command.getAction();
244            if (action.startsWith(Stomp.Commands.SEND)) {
245                onStompSend(command);
246            } else if (action.startsWith(Stomp.Commands.ACK)) {
247                onStompAck(command);
248            } else if (action.startsWith(Stomp.Commands.NACK)) {
249                onStompNack(command);
250            } else if (action.startsWith(Stomp.Commands.BEGIN)) {
251                onStompBegin(command);
252            } else if (action.startsWith(Stomp.Commands.COMMIT)) {
253                onStompCommit(command);
254            } else if (action.startsWith(Stomp.Commands.ABORT)) {
255                onStompAbort(command);
256            } else if (action.startsWith(Stomp.Commands.SUBSCRIBE)) {
257                onStompSubscribe(command);
258            } else if (action.startsWith(Stomp.Commands.UNSUBSCRIBE)) {
259                onStompUnsubscribe(command);
260            } else if (action.startsWith(Stomp.Commands.CONNECT) ||
261                       action.startsWith(Stomp.Commands.STOMP)) {
262                onStompConnect(command);
263            } else if (action.startsWith(Stomp.Commands.DISCONNECT)) {
264                onStompDisconnect(command);
265            } else {
266                throw new ProtocolException("Unknown STOMP action: " + action);
267            }
268
269        } catch (ProtocolException e) {
270            handleException(e, command);
271            // Some protocol errors can cause the connection to get closed.
272            if (e.isFatal()) {
273               getStompTransport().onException(e);
274            }
275        }
276    }
277
278    protected void handleException(Throwable exception, StompFrame command) throws IOException {
279        LOG.warn("Exception occurred processing: \n" + command + ": " + exception.toString());
280        if (LOG.isDebugEnabled()) {
281            LOG.debug("Exception detail", exception);
282        }
283
284        // Let the stomp client know about any protocol errors.
285        ByteArrayOutputStream baos = new ByteArrayOutputStream();
286        PrintWriter stream = new PrintWriter(new OutputStreamWriter(baos, "UTF-8"));
287        exception.printStackTrace(stream);
288        stream.close();
289
290        HashMap<String, String> headers = new HashMap<String, String>();
291        headers.put(Stomp.Headers.Error.MESSAGE, exception.getMessage());
292        headers.put(Stomp.Headers.CONTENT_TYPE, "text/plain");
293
294        if (command != null) {
295            final String receiptId = command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
296            if (receiptId != null) {
297                headers.put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
298            }
299        }
300
301        StompFrame errorMessage = new StompFrame(Stomp.Responses.ERROR, headers, baos.toByteArray());
302        sendToStomp(errorMessage);
303    }
304
305    protected void onStompSend(StompFrame command) throws IOException, JMSException {
306        checkConnected();
307
308        Map<String, String> headers = command.getHeaders();
309        String destination = headers.get(Stomp.Headers.Send.DESTINATION);
310        if (destination == null) {
311            throw new ProtocolException("SEND received without a Destination specified!");
312        }
313
314        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
315        headers.remove("transaction");
316
317        ActiveMQMessage message = convertMessage(command);
318
319        message.setProducerId(producerId);
320        MessageId id = new MessageId(producerId, messageIdGenerator.getNextSequenceId());
321        message.setMessageId(id);
322
323        if (stompTx != null) {
324            TransactionId activemqTx = transactions.get(stompTx);
325            if (activemqTx == null) {
326                throw new ProtocolException("Invalid transaction id: " + stompTx);
327            }
328            message.setTransactionId(activemqTx);
329        }
330
331        message.onSend();
332        sendToActiveMQ(message, createResponseHandler(command));
333    }
334
335    protected void onStompNack(StompFrame command) throws ProtocolException {
336
337        checkConnected();
338
339        if (this.version.equals(Stomp.V1_0)) {
340            throw new ProtocolException("NACK received but connection is in v1.0 mode.");
341        }
342
343        Map<String, String> headers = command.getHeaders();
344
345        String subscriptionId = headers.get(Stomp.Headers.Ack.SUBSCRIPTION);
346        if (subscriptionId == null && !this.version.equals(Stomp.V1_2)) {
347            throw new ProtocolException("NACK received without a subscription id for acknowledge!");
348        }
349
350        String messageId = headers.get(Stomp.Headers.Ack.MESSAGE_ID);
351        if (messageId == null && !this.version.equals(Stomp.V1_2)) {
352            throw new ProtocolException("NACK received without a message-id to acknowledge!");
353        }
354
355        String ackId = headers.get(Stomp.Headers.Ack.ACK_ID);
356        if (ackId == null && this.version.equals(Stomp.V1_2)) {
357            throw new ProtocolException("NACK received without an ack header to acknowledge!");
358        }
359
360        TransactionId activemqTx = null;
361        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
362        if (stompTx != null) {
363            activemqTx = transactions.get(stompTx);
364            if (activemqTx == null) {
365                throw new ProtocolException("Invalid transaction id: " + stompTx);
366            }
367        }
368
369        boolean nacked = false;
370
371        if (ackId != null) {
372            AckEntry pendingAck = this.pedingAcks.remove(ackId);
373            if (pendingAck != null) {
374                messageId = pendingAck.getMessageId();
375                MessageAck ack = pendingAck.onMessageNack(activemqTx);
376                if (ack != null) {
377                    sendToActiveMQ(ack, createResponseHandler(command));
378                    nacked = true;
379                }
380            }
381        } else if (subscriptionId != null) {
382            StompSubscription sub = this.subscriptions.get(subscriptionId);
383            if (sub != null) {
384                MessageAck ack = sub.onStompMessageNack(messageId, activemqTx);
385                if (ack != null) {
386                    sendToActiveMQ(ack, createResponseHandler(command));
387                    nacked = true;
388                }
389            }
390        }
391
392        if (!nacked) {
393            throw new ProtocolException("Unexpected NACK received for message-id [" + messageId + "]");
394        }
395    }
396
397    protected void onStompAck(StompFrame command) throws ProtocolException {
398        checkConnected();
399
400        Map<String, String> headers = command.getHeaders();
401        String messageId = headers.get(Stomp.Headers.Ack.MESSAGE_ID);
402        if (messageId == null && !(this.version.equals(Stomp.V1_2))) {
403            throw new ProtocolException("ACK received without a message-id to acknowledge!");
404        }
405
406        String subscriptionId = headers.get(Stomp.Headers.Ack.SUBSCRIPTION);
407        if (subscriptionId == null && this.version.equals(Stomp.V1_1)) {
408            throw new ProtocolException("ACK received without a subscription id for acknowledge!");
409        }
410
411        String ackId = headers.get(Stomp.Headers.Ack.ACK_ID);
412        if (ackId == null && this.version.equals(Stomp.V1_2)) {
413            throw new ProtocolException("ACK received without a ack id for acknowledge!");
414        }
415
416        TransactionId activemqTx = null;
417        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
418        if (stompTx != null) {
419            activemqTx = transactions.get(stompTx);
420            if (activemqTx == null) {
421                throw new ProtocolException("Invalid transaction id: " + stompTx);
422            }
423        }
424
425        boolean acked = false;
426
427        if (ackId != null) {
428            AckEntry pendingAck = this.pedingAcks.remove(ackId);
429            if (pendingAck != null) {
430                messageId = pendingAck.getMessageId();
431                MessageAck ack = pendingAck.onMessageAck(activemqTx);
432                if (ack != null) {
433                    sendToActiveMQ(ack, createResponseHandler(command));
434                    acked = true;
435                }
436            }
437
438        } else if (subscriptionId != null) {
439            StompSubscription sub = this.subscriptions.get(subscriptionId);
440            if (sub != null) {
441                MessageAck ack = sub.onStompMessageAck(messageId, activemqTx);
442                if (ack != null) {
443                    sendToActiveMQ(ack, createResponseHandler(command));
444                    acked = true;
445                }
446            }
447        } else {
448            // STOMP v1.0: acking with just a message id is very bogus since the same message id
449            // could have been sent to 2 different subscriptions on the same Stomp connection.
450            // For example, when 2 subs are created on the same topic.
451            for (StompSubscription sub : subscriptionsByConsumerId.values()) {
452                MessageAck ack = sub.onStompMessageAck(messageId, activemqTx);
453                if (ack != null) {
454                    sendToActiveMQ(ack, createResponseHandler(command));
455                    acked = true;
456                    break;
457                }
458            }
459        }
460
461        if (!acked) {
462            throw new ProtocolException("Unexpected ACK received for message-id [" + messageId + "]");
463        }
464    }
465
466    protected void onStompBegin(StompFrame command) throws ProtocolException {
467        checkConnected();
468
469        Map<String, String> headers = command.getHeaders();
470
471        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
472
473        if (!headers.containsKey(Stomp.Headers.TRANSACTION)) {
474            throw new ProtocolException("Must specify the transaction you are beginning");
475        }
476
477        if (transactions.get(stompTx) != null) {
478            throw new ProtocolException("The transaction was already started: " + stompTx);
479        }
480
481        LocalTransactionId activemqTx = new LocalTransactionId(connectionId, transactionIdGenerator.getNextSequenceId());
482        transactions.put(stompTx, activemqTx);
483
484        TransactionInfo tx = new TransactionInfo();
485        tx.setConnectionId(connectionId);
486        tx.setTransactionId(activemqTx);
487        tx.setType(TransactionInfo.BEGIN);
488
489        sendToActiveMQ(tx, createResponseHandler(command));
490    }
491
492    protected void onStompCommit(StompFrame command) throws ProtocolException {
493        checkConnected();
494
495        Map<String, String> headers = command.getHeaders();
496
497        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
498        if (stompTx == null) {
499            throw new ProtocolException("Must specify the transaction you are committing");
500        }
501
502        TransactionId activemqTx = transactions.remove(stompTx);
503        if (activemqTx == null) {
504            throw new ProtocolException("Invalid transaction id: " + stompTx);
505        }
506
507        for (StompSubscription sub : subscriptionsByConsumerId.values()) {
508            sub.onStompCommit(activemqTx);
509        }
510
511        pedingAcks.clear();
512
513        TransactionInfo tx = new TransactionInfo();
514        tx.setConnectionId(connectionId);
515        tx.setTransactionId(activemqTx);
516        tx.setType(TransactionInfo.COMMIT_ONE_PHASE);
517
518        sendToActiveMQ(tx, createResponseHandler(command));
519    }
520
521    protected void onStompAbort(StompFrame command) throws ProtocolException {
522        checkConnected();
523        Map<String, String> headers = command.getHeaders();
524
525        String stompTx = headers.get(Stomp.Headers.TRANSACTION);
526        if (stompTx == null) {
527            throw new ProtocolException("Must specify the transaction you are committing");
528        }
529
530        TransactionId activemqTx = transactions.remove(stompTx);
531        if (activemqTx == null) {
532            throw new ProtocolException("Invalid transaction id: " + stompTx);
533        }
534        for (StompSubscription sub : subscriptionsByConsumerId.values()) {
535            try {
536                sub.onStompAbort(activemqTx);
537            } catch (Exception e) {
538                throw new ProtocolException("Transaction abort failed", false, e);
539            }
540        }
541
542        pedingAcks.clear();
543
544        TransactionInfo tx = new TransactionInfo();
545        tx.setConnectionId(connectionId);
546        tx.setTransactionId(activemqTx);
547        tx.setType(TransactionInfo.ROLLBACK);
548
549        sendToActiveMQ(tx, createResponseHandler(command));
550    }
551
552    protected void onStompSubscribe(StompFrame command) throws ProtocolException {
553        checkConnected();
554        FrameTranslator translator = findTranslator(command.getHeaders().get(Stomp.Headers.TRANSFORMATION));
555        Map<String, String> headers = command.getHeaders();
556
557        String subscriptionId = headers.get(Stomp.Headers.Subscribe.ID);
558        String destination = headers.get(Stomp.Headers.Subscribe.DESTINATION);
559
560        if (this.version.equals(Stomp.V1_1) && subscriptionId == null) {
561            throw new ProtocolException("SUBSCRIBE received without a subscription id!");
562        }
563
564        final ActiveMQDestination actualDest = translator.convertDestination(this, destination, true);
565
566        if (actualDest == null) {
567            throw new ProtocolException("Invalid 'null' Destination.");
568        }
569
570        final ConsumerId id = new ConsumerId(sessionId, consumerIdGenerator.getNextSequenceId());
571        ConsumerInfo consumerInfo = new ConsumerInfo(id);
572        consumerInfo.setPrefetchSize(actualDest.isQueue() ?
573                ActiveMQPrefetchPolicy.DEFAULT_QUEUE_PREFETCH :
574                headers.containsKey("activemq.subscriptionName") ?
575                        ActiveMQPrefetchPolicy.DEFAULT_DURABLE_TOPIC_PREFETCH : ActiveMQPrefetchPolicy.DEFAULT_TOPIC_PREFETCH);
576        consumerInfo.setDispatchAsync(true);
577
578        String browser = headers.get(Stomp.Headers.Subscribe.BROWSER);
579        if (browser != null && browser.equals(Stomp.TRUE)) {
580
581            if (this.version.equals(Stomp.V1_0)) {
582                throw new ProtocolException("Queue Browser feature only valid for Stomp v1.1+ clients!");
583            }
584
585            consumerInfo.setBrowser(true);
586            consumerInfo.setPrefetchSize(ActiveMQPrefetchPolicy.DEFAULT_QUEUE_BROWSER_PREFETCH);
587        }
588
589        String selector = headers.remove(Stomp.Headers.Subscribe.SELECTOR);
590        if (selector != null) {
591            consumerInfo.setSelector("convert_string_expressions:" + selector);
592        }
593
594        IntrospectionSupport.setProperties(consumerInfo, headers, "activemq.");
595
596        if (actualDest.isQueue() && consumerInfo.getSubscriptionName() != null) {
597            throw new ProtocolException("Invalid Subscription: cannot durably subscribe to a Queue destination!");
598        }
599
600        consumerInfo.setDestination(translator.convertDestination(this, destination, true));
601
602        StompSubscription stompSubscription;
603        if (!consumerInfo.isBrowser()) {
604            stompSubscription = new StompSubscription(this, subscriptionId, consumerInfo, headers.get(Stomp.Headers.TRANSFORMATION));
605        } else {
606            stompSubscription = new StompQueueBrowserSubscription(this, subscriptionId, consumerInfo, headers.get(Stomp.Headers.TRANSFORMATION));
607        }
608        stompSubscription.setDestination(actualDest);
609
610        String ackMode = headers.get(Stomp.Headers.Subscribe.ACK_MODE);
611        if (Stomp.Headers.Subscribe.AckModeValues.CLIENT.equals(ackMode)) {
612            stompSubscription.setAckMode(StompSubscription.CLIENT_ACK);
613        } else if (Stomp.Headers.Subscribe.AckModeValues.INDIVIDUAL.equals(ackMode)) {
614            stompSubscription.setAckMode(StompSubscription.INDIVIDUAL_ACK);
615        } else {
616            stompSubscription.setAckMode(StompSubscription.AUTO_ACK);
617        }
618
619        subscriptionsByConsumerId.put(id, stompSubscription);
620        // Stomp v1.0 doesn't need to set this header so we avoid an NPE if not set.
621        if (subscriptionId != null) {
622            subscriptions.put(subscriptionId, stompSubscription);
623        }
624
625        final String receiptId = command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
626        if (receiptId != null && consumerInfo.getPrefetchSize() > 0) {
627
628            final StompFrame cmd = command;
629            final int prefetch = consumerInfo.getPrefetchSize();
630
631            // Since dispatch could beat the receipt we set prefetch to zero to start and then
632            // once we've sent our Receipt we are safe to turn on dispatch if the response isn't
633            // an error message.
634            consumerInfo.setPrefetchSize(0);
635
636            final ResponseHandler handler = new ResponseHandler() {
637                @Override
638                public void onResponse(ProtocolConverter converter, Response response) throws IOException {
639                    if (response.isException()) {
640                        // Generally a command can fail.. but that does not invalidate the connection.
641                        // We report back the failure but we don't close the connection.
642                        Throwable exception = ((ExceptionResponse)response).getException();
643                        handleException(exception, cmd);
644                    } else {
645                        StompFrame sc = new StompFrame();
646                        sc.setAction(Stomp.Responses.RECEIPT);
647                        sc.setHeaders(new HashMap<String, String>(1));
648                        sc.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
649                        stompTransport.sendToStomp(sc);
650
651                        ConsumerControl control = new ConsumerControl();
652                        control.setPrefetch(prefetch);
653                        control.setDestination(actualDest);
654                        control.setConsumerId(id);
655
656                        sendToActiveMQ(control, null);
657                    }
658                }
659            };
660
661            sendToActiveMQ(consumerInfo, handler);
662        } else {
663            sendToActiveMQ(consumerInfo, createResponseHandler(command));
664        }
665    }
666
667    protected void onStompUnsubscribe(StompFrame command) throws ProtocolException {
668        checkConnected();
669        Map<String, String> headers = command.getHeaders();
670
671        ActiveMQDestination destination = null;
672        Object o = headers.get(Stomp.Headers.Unsubscribe.DESTINATION);
673        if (o != null) {
674            destination = findTranslator(command.getHeaders().get(Stomp.Headers.TRANSFORMATION)).convertDestination(this, (String)o, true);
675        }
676
677        String subscriptionId = headers.get(Stomp.Headers.Unsubscribe.ID);
678        if (this.version.equals(Stomp.V1_1) && subscriptionId == null) {
679            throw new ProtocolException("UNSUBSCRIBE received without a subscription id!");
680        }
681
682        if (subscriptionId == null && destination == null) {
683            throw new ProtocolException("Must specify the subscriptionId or the destination you are unsubscribing from");
684        }
685
686        // check if it is a durable subscription
687        String durable = command.getHeaders().get("activemq.subscriptionName");
688        String clientId = durable;
689        if (this.version.equals(Stomp.V1_1)) {
690            clientId = connectionInfo.getClientId();
691        }
692
693        if (durable != null) {
694            RemoveSubscriptionInfo info = new RemoveSubscriptionInfo();
695            info.setClientId(clientId);
696            info.setSubscriptionName(durable);
697            info.setConnectionId(connectionId);
698            sendToActiveMQ(info, createResponseHandler(command));
699            return;
700        }
701
702        if (subscriptionId != null) {
703
704            StompSubscription sub = this.subscriptions.remove(subscriptionId);
705            if (sub != null) {
706                sendToActiveMQ(sub.getConsumerInfo().createRemoveCommand(), createResponseHandler(command));
707                return;
708            }
709
710        } else {
711
712            // Unsubscribing using a destination is a bit weird if multiple subscriptions
713            // are created with the same destination.
714            for (Iterator<StompSubscription> iter = subscriptionsByConsumerId.values().iterator(); iter.hasNext();) {
715                StompSubscription sub = iter.next();
716                if (destination != null && destination.equals(sub.getDestination())) {
717                    sendToActiveMQ(sub.getConsumerInfo().createRemoveCommand(), createResponseHandler(command));
718                    iter.remove();
719                    return;
720                }
721            }
722        }
723
724        throw new ProtocolException("No subscription matched.");
725    }
726
727    ConnectionInfo connectionInfo = new ConnectionInfo();
728
729    protected void onStompConnect(final StompFrame command) throws ProtocolException {
730
731        if (connected.get()) {
732            throw new ProtocolException("Already connected.");
733        }
734
735        final Map<String, String> headers = command.getHeaders();
736
737        // allow anyone to login for now
738        String login = headers.get(Stomp.Headers.Connect.LOGIN);
739        String passcode = headers.get(Stomp.Headers.Connect.PASSCODE);
740        String clientId = headers.get(Stomp.Headers.Connect.CLIENT_ID);
741        String heartBeat = headers.get(Stomp.Headers.Connect.HEART_BEAT);
742
743        if (heartBeat == null) {
744            heartBeat = defaultHeartBeat;
745        }
746
747        this.version = StompCodec.detectVersion(headers);
748
749        configureInactivityMonitor(heartBeat.trim());
750
751        IntrospectionSupport.setProperties(connectionInfo, headers, "activemq.");
752        connectionInfo.setConnectionId(connectionId);
753        if (clientId != null) {
754            connectionInfo.setClientId(clientId);
755        } else {
756            connectionInfo.setClientId("" + connectionInfo.getConnectionId().toString());
757        }
758
759        connectionInfo.setResponseRequired(true);
760        connectionInfo.setUserName(login);
761        connectionInfo.setPassword(passcode);
762        connectionInfo.setTransportContext(command.getTransportContext());
763
764        sendToActiveMQ(connectionInfo, new ResponseHandler() {
765            @Override
766            public void onResponse(ProtocolConverter converter, Response response) throws IOException {
767
768                if (response.isException()) {
769                    // If the connection attempt fails we close the socket.
770                    Throwable exception = ((ExceptionResponse)response).getException();
771                    handleException(exception, command);
772                    getStompTransport().onException(IOExceptionSupport.create(exception));
773                    return;
774                }
775
776                final SessionInfo sessionInfo = new SessionInfo(sessionId);
777                sendToActiveMQ(sessionInfo, null);
778
779                final ProducerInfo producerInfo = new ProducerInfo(producerId);
780                sendToActiveMQ(producerInfo, new ResponseHandler() {
781                    @Override
782                    public void onResponse(ProtocolConverter converter, Response response) throws IOException {
783
784                        if (response.isException()) {
785                            // If the connection attempt fails we close the socket.
786                            Throwable exception = ((ExceptionResponse)response).getException();
787                            handleException(exception, command);
788                            getStompTransport().onException(IOExceptionSupport.create(exception));
789                        }
790
791                        connected.set(true);
792                        HashMap<String, String> responseHeaders = new HashMap<String, String>();
793
794                        responseHeaders.put(Stomp.Headers.Connected.SESSION, connectionInfo.getClientId());
795                        String requestId = headers.get(Stomp.Headers.Connect.REQUEST_ID);
796                        if (requestId == null) {
797                            // TODO legacy
798                            requestId = headers.get(Stomp.Headers.RECEIPT_REQUESTED);
799                        }
800                        if (requestId != null) {
801                            // TODO legacy
802                            responseHeaders.put(Stomp.Headers.Connected.RESPONSE_ID, requestId);
803                            responseHeaders.put(Stomp.Headers.Response.RECEIPT_ID, requestId);
804                        }
805
806                        responseHeaders.put(Stomp.Headers.Connected.VERSION, version);
807                        responseHeaders.put(Stomp.Headers.Connected.HEART_BEAT,
808                                            String.format("%d,%d", hbWriteInterval, hbReadInterval));
809                        responseHeaders.put(Stomp.Headers.Connected.SERVER, "ActiveMQ/"+BROKER_VERSION);
810
811                        StompFrame sc = new StompFrame();
812                        sc.setAction(Stomp.Responses.CONNECTED);
813                        sc.setHeaders(responseHeaders);
814                        sendToStomp(sc);
815
816                        StompWireFormat format = stompTransport.getWireFormat();
817                        if (format != null) {
818                            format.setStompVersion(version);
819                        }
820                    }
821                });
822            }
823        });
824    }
825
826    protected void onStompDisconnect(StompFrame command) throws ProtocolException {
827        if (connected.get()) {
828            sendToActiveMQ(connectionInfo.createRemoveCommand(), createResponseHandler(command));
829            sendToActiveMQ(new ShutdownInfo(), createResponseHandler(command));
830            connected.set(false);
831        }
832    }
833
834    protected void checkConnected() throws ProtocolException {
835        if (!connected.get()) {
836            throw new ProtocolException("Not connected.");
837        }
838    }
839
840    /**
841     * Dispatch a ActiveMQ command
842     *
843     * @param command
844     * @throws IOException
845     */
846    public void onActiveMQCommand(Command command) throws IOException, JMSException {
847        if (command.isResponse()) {
848            Response response = (Response)command;
849            ResponseHandler rh = resposeHandlers.remove(Integer.valueOf(response.getCorrelationId()));
850            if (rh != null) {
851                rh.onResponse(this, response);
852            } else {
853                // Pass down any unexpected errors. Should this close the connection?
854                if (response.isException()) {
855                    Throwable exception = ((ExceptionResponse)response).getException();
856                    handleException(exception, null);
857                }
858            }
859        } else if (command.isMessageDispatch()) {
860            MessageDispatch md = (MessageDispatch)command;
861            StompSubscription sub = subscriptionsByConsumerId.get(md.getConsumerId());
862            if (sub != null) {
863                String ackId = null;
864                if (version.equals(Stomp.V1_2) && sub.getAckMode() != Stomp.Headers.Subscribe.AckModeValues.AUTO && md.getMessage() != null) {
865                    AckEntry pendingAck = new AckEntry(md.getMessage().getMessageId().toString(), sub);
866                    ackId = this.ACK_ID_GENERATOR.generateId();
867                    this.pedingAcks.put(ackId, pendingAck);
868                }
869                try {
870                    sub.onMessageDispatch(md, ackId);
871                } catch (Exception ex) {
872                    if (ackId != null) {
873                        this.pedingAcks.remove(ackId);
874                    }
875                }
876            }
877        } else if (command.getDataStructureType() == CommandTypes.KEEP_ALIVE_INFO) {
878            stompTransport.sendToStomp(ping);
879        } else if (command.getDataStructureType() == ConnectionError.DATA_STRUCTURE_TYPE) {
880            // Pass down any unexpected async errors. Should this close the connection?
881            Throwable exception = ((ConnectionError)command).getException();
882            handleException(exception, null);
883        }
884    }
885
886    public ActiveMQMessage convertMessage(StompFrame command) throws IOException, JMSException {
887        ActiveMQMessage msg = findTranslator(command.getHeaders().get(Stomp.Headers.TRANSFORMATION)).convertFrame(this, command);
888        return msg;
889    }
890
891    public StompFrame convertMessage(ActiveMQMessage message, boolean ignoreTransformation) throws IOException, JMSException {
892        if (ignoreTransformation == true) {
893            return frameTranslator.convertMessage(this, message);
894        } else {
895            FrameTranslator translator = findTranslator(
896                message.getStringProperty(Stomp.Headers.TRANSFORMATION), message.getDestination(), message.isAdvisory());
897            return translator.convertMessage(this, message);
898        }
899    }
900
901    public StompTransport getStompTransport() {
902        return stompTransport;
903    }
904
905    public ActiveMQDestination createTempDestination(String name, boolean topic) {
906        ActiveMQDestination rc = tempDestinations.get(name);
907        if( rc == null ) {
908            if (topic) {
909                rc = new ActiveMQTempTopic(connectionId, tempDestinationGenerator.getNextSequenceId());
910            } else {
911                rc = new ActiveMQTempQueue(connectionId, tempDestinationGenerator.getNextSequenceId());
912            }
913            sendToActiveMQ(new DestinationInfo(connectionId, DestinationInfo.ADD_OPERATION_TYPE, rc), null);
914            tempDestinations.put(name, rc);
915            tempDestinationAmqToStompMap.put(rc.getQualifiedName(), name);
916        }
917        return rc;
918    }
919
920    public String getCreatedTempDestinationName(ActiveMQDestination destination) {
921        return tempDestinationAmqToStompMap.get(destination.getQualifiedName());
922    }
923
924    public String getDefaultHeartBeat() {
925        return defaultHeartBeat;
926    }
927
928    public void setDefaultHeartBeat(String defaultHeartBeat) {
929        this.defaultHeartBeat = defaultHeartBeat;
930    }
931
932    /**
933     * @return the hbGracePeriodMultiplier
934     */
935    public float getHbGracePeriodMultiplier() {
936        return hbGracePeriodMultiplier;
937    }
938
939    /**
940     * @param hbGracePeriodMultiplier the hbGracePeriodMultiplier to set
941     */
942    public void setHbGracePeriodMultiplier(float hbGracePeriodMultiplier) {
943        this.hbGracePeriodMultiplier = hbGracePeriodMultiplier;
944    }
945
946    protected void configureInactivityMonitor(String heartBeatConfig) throws ProtocolException {
947
948        String[] keepAliveOpts = heartBeatConfig.split(Stomp.COMMA);
949
950        if (keepAliveOpts == null || keepAliveOpts.length != 2) {
951            throw new ProtocolException("Invalid heart-beat header:" + heartBeatConfig, true);
952        } else {
953
954            try {
955                hbReadInterval = (Long.parseLong(keepAliveOpts[0]));
956                hbWriteInterval = Long.parseLong(keepAliveOpts[1]);
957            } catch(NumberFormatException e) {
958                throw new ProtocolException("Invalid heart-beat header:" + heartBeatConfig, true);
959            }
960
961            try {
962                StompInactivityMonitor monitor = this.stompTransport.getInactivityMonitor();
963                monitor.setReadCheckTime((long) (hbReadInterval * hbGracePeriodMultiplier));
964                monitor.setInitialDelayTime(Math.min(hbReadInterval, hbWriteInterval));
965                monitor.setWriteCheckTime(hbWriteInterval);
966                monitor.startMonitoring();
967            } catch(Exception ex) {
968                hbReadInterval = 0;
969                hbWriteInterval = 0;
970            }
971
972            if (LOG.isDebugEnabled()) {
973                LOG.debug("Stomp Connect heartbeat conf RW[" + hbReadInterval + "," + hbWriteInterval + "]");
974            }
975        }
976    }
977
978    protected void sendReceipt(StompFrame command) {
979        final String receiptId = command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
980        if (receiptId != null) {
981            StompFrame sc = new StompFrame();
982            sc.setAction(Stomp.Responses.RECEIPT);
983            sc.setHeaders(new HashMap<String, String>(1));
984            sc.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
985            try {
986                sendToStomp(sc);
987            } catch (IOException e) {
988                LOG.warn("Could not send a receipt for " + command, e);
989            }
990        }
991    }
992}