package org.openamq.client; import edu.emory.mathcs.backport.java.util.concurrent.SynchronousQueue; import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit; import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean; import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.openamq.AMQException; import org.openamq.client.message.MessageFactoryRegistry; import org.openamq.client.message.UnprocessedMessage; import org.openamq.client.message.AbstractJMSMessage; import org.openamq.client.message.AMQMessage; import org.openamq.client.protocol.AMQProtocolHandler; import org.openamq.client.state.listener.SpecificMethodFrameListener; import org.openamq.framing.*; import org.openamq.jms.MessageConsumer; import org.openamq.jms.Session; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; public class BasicMessageConsumer extends Closeable implements MessageConsumer { private static final Logger _logger = LoggerFactory.getLogger(BasicMessageConsumer.class); /** * The connection being used by this consumer */ private AMQConnection _connection; private String _messageSelector; private boolean _noLocal; private AMQDestination _destination; /** * When true indicates that a blocking receive call is in progress */ private final AtomicBoolean _receiving = new AtomicBoolean(false); /** * Holds an atomic reference to the listener installed. */ private final AtomicReference _messageListener = new AtomicReference(); /** * The consumer tag allows us to close the consumer by sending a jmsCancel method to the * broker */ private String _consumerTag; /** * We need to know the channel id when constructing frames */ private int _channelId; /** * Used in the blocking receive methods to receive a message from * the Session thread. Argument true indicates we want strict FIFO semantics */ private final SynchronousQueue _synchronousQueue = new SynchronousQueue(true); private MessageFactoryRegistry _messageFactory; private AMQSession _session; private AMQProtocolHandler _protocolHandler; /** * We need to store the "raw" field table so that we can resubscribe in the event of failover being required */ private FieldTable _rawSelectorFieldTable; /** * We store the prefetch field in order to be able to reuse it when resubscribing in the event of failover */ private int _prefetch; /** * We store the exclusive field in order to be able to reuse it when resubscribing in the event of failover */ private boolean _exclusive; /** * The acknowledge mode in force for this consumer. Note that the AMQP protocol allows different ack modes * per consumer whereas JMS defines this at the session level, hence why we associate it with the consumer in our * implementation. */ private int _acknowledgeMode; /** * Number of messages unacknowledged in DUPS_OK_ACKNOWLEDGE mode */ private int _outstanding; /** * Tag of last message delievered, whoch should be acknowledged on commit in * transaction mode. */ private long _lastDeliveryTag; BasicMessageConsumer(int channelId, AMQConnection connection, AMQDestination destination, String messageSelector, boolean noLocal, MessageFactoryRegistry messageFactory, AMQSession session, AMQProtocolHandler protocolHandler, FieldTable rawSelectorFieldTable, int prefetch, boolean exclusive, int acknowledgeMode) { _channelId = channelId; _connection = connection; _messageSelector = messageSelector; _noLocal = noLocal; _destination = destination; _messageFactory = messageFactory; _session = session; _protocolHandler = protocolHandler; _rawSelectorFieldTable =rawSelectorFieldTable; _prefetch = prefetch; _exclusive = exclusive; _acknowledgeMode = acknowledgeMode; } public AMQDestination getDestination() { return _destination; } public String getMessageSelector() throws JMSException { return _messageSelector; } public MessageListener getMessageListener() throws JMSException { return (MessageListener) _messageListener.get(); } public int getAcknowledgeMode() { return _acknowledgeMode; } private boolean isMessageListenerSet() { return _messageListener.get() != null; } public void setMessageListener(MessageListener messageListener) throws JMSException { checkNotClosed(); //if the current listener is non-null and the session is not stopped, then //it is an error to call this method. //i.e. it is only valid to call this method if // // (a) the session is stopped, in which case the dispatcher is not running // OR // (b) the listener is null AND we are not receiving synchronously at present // if(_session.isStopped()) { _messageListener.set(messageListener); _logger.debug("Message listener set for destination " + _destination); } else { if (_receiving.get()) { throw new javax.jms.IllegalStateException("Another thread is already receiving synchronously."); } if (!_messageListener.compareAndSet(null, messageListener)) { throw new javax.jms.IllegalStateException("Attempt to alter listener while session is started."); } _logger.debug("Message listener set for destination " + _destination); if (messageListener != null) { //handle case where connection has already been started, and the dispatcher is blocked //doing a put on the _synchronousQueue Object msg = _synchronousQueue.poll(); if (msg != null) { AbstractJMSMessage jmsMsg = (AbstractJMSMessage) msg; messageListener.onMessage(jmsMsg); postDeliver(jmsMsg); } } } } private void acquireReceiving() throws JMSException { if (!_receiving.compareAndSet(false, true)) { throw new javax.jms.IllegalStateException("Another thread is already receiving."); } if (isMessageListenerSet()) { throw new javax.jms.IllegalStateException("A listener has already been set."); } } private void releaseReceiving(){ _receiving.set(false); } public FieldTable getRawSelectorFieldTable() { return _rawSelectorFieldTable; } public int getPrefetch() { return _prefetch; } public boolean isNoLocal() { return _noLocal; } public boolean isExclusive() { return _exclusive; } public Message receive() throws JMSException { return receive(0); } public Message receive(long l) throws JMSException { checkNotClosed(); acquireReceiving(); try { Object o = null; if (l > 0) { o = _synchronousQueue.poll(l, TimeUnit.MILLISECONDS); } else { o = _synchronousQueue.take(); } final AbstractJMSMessage m = returnMessageOrThrow(o); postDeliver(m); return m; } catch (InterruptedException e) { return null; } finally { releaseReceiving(); } } public Message receiveNoWait() throws JMSException { checkNotClosed(); acquireReceiving(); try { Object o = _synchronousQueue.poll(); final AbstractJMSMessage m = returnMessageOrThrow(o); postDeliver(m); return m; } finally { releaseReceiving(); } } /** * We can get back either a Message or an exception from the queue. This method examines the argument and deals * with it by throwing it (if an exception) or returning it (in any other case). * @param o * @return a message only if o is a Message * @throws JMSException if the argument is a throwable. If it is a JMSException it is rethrown as is, but if not * a JMSException is created with the linked exception set appropriately */ private AbstractJMSMessage returnMessageOrThrow(Object o) throws JMSException { // errors are passed via the queue too since there is no way of interrupting the poll() via the API. if (o instanceof Throwable) { JMSException e = new JMSException("Message consumer forcibly closed due to error: " + o); if (o instanceof Exception) { e.setLinkedException((Exception) o); } throw e; } else { return (AbstractJMSMessage) o; } } public void close() throws JMSException { synchronized (_connection.getFailoverMutex()) { if (!_closed.getAndSet(true)) { final AMQFrame cancelFrame = BasicCancelBody.createAMQFrame(_channelId, _consumerTag, false); try { _protocolHandler.writeCommandFrameAndWaitForReply(cancelFrame, new SpecificMethodFrameListener(_channelId, BasicCancelOkBody.class)); } catch (AMQException e) { _logger.error("Error closing consumer: " + e, e); throw new JMSException("Error closing consumer: " + e); } deregisterConsumer(); } } } /** * Called when you need to invalidate a consumer. Used for example when failover has occurred and the * client has vetoed automatic resubscription. * The caller must hold the failover mutex. */ void markClosed() { _closed.set(true); deregisterConsumer(); } /** * Called from the AMQSession when a message has arrived for this consumer. This methods handles both the case * of a message listener or a synchronous receive() caller. * @param messageFrame the raw unprocessed mesage * @param channelId channel on which this message was sent */ void notifyMessage(UnprocessedMessage messageFrame, int channelId) { if (_logger.isDebugEnabled()) { _logger.debug("notifyMessage called with message number " + messageFrame.deliverBody.deliveryTag); } try { AbstractJMSMessage jmsMessage = _messageFactory.createMessage(messageFrame.deliverBody.deliveryTag, messageFrame.deliverBody.redelivered, messageFrame.contentHeader, messageFrame.bodies); _logger.debug("Message is of type: " + jmsMessage.getClass().getName()); preDeliver(jmsMessage); if (isMessageListenerSet()) { //we do not need a lock around the test above, and the dispatch below as it is invalid //for an application to alter an installed listener while the session is started getMessageListener().onMessage(jmsMessage); postDeliver(jmsMessage); } else { _synchronousQueue.put(jmsMessage); } } catch (Exception e) { _logger.error("Caught exception (dump follows) - ignoring...", e); } } private void preDeliver(AbstractJMSMessage msg) { switch (_acknowledgeMode) { case Session.PRE_ACKNOWLEDGE: _session.acknowledgeMessage(msg.getDeliveryTag(), false); break; case Session.CLIENT_ACKNOWLEDGE: // we set the session so that when the user calls acknowledge() it can call the method on session // to send out the appropriate frame msg.setAMQSession(_session); break; } } private void postDeliver(AbstractJMSMessage msg) { switch (_acknowledgeMode) { case Session.DUPS_OK_ACKNOWLEDGE: if(++_outstanding >= _prefetch) { _session.acknowledgeMessage(msg.getDeliveryTag(), true); } break; case Session.AUTO_ACKNOWLEDGE: _session.acknowledgeMessage(msg.getDeliveryTag(), false); break; case Session.SESSION_TRANSACTED: _lastDeliveryTag = msg.getDeliveryTag(); break; } } void commit() { if(_lastDeliveryTag >= 0) { _session.acknowledgeMessage(_lastDeliveryTag, true); _lastDeliveryTag = -1; } } void notifyError(Throwable cause) { _closed.set(true); // we have no way of propagating the exception to a message listener - a JMS limitation - so we // deal with the case where we have a synchronous receive() waiting for a message to arrive if (!isMessageListenerSet()) { // offer only succeeds if there is a thread waiting for an item from the queue if (_synchronousQueue.offer(cause)) { _logger.debug("Passed exception to synchronous queue for propagation to receive()"); } } deregisterConsumer(); } /** * Perform cleanup to deregister this consumer. This occurs when closing the consumer in both the clean * case and in the case of an error occurring. */ private void deregisterConsumer() { _session.deregisterConsumer(_consumerTag); } public String getConsumerTag() { return _consumerTag; } public void setConsumerTag(String consumerTag) { _consumerTag = consumerTag; } }