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}