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}