/**
*
* Copyright 2004 Protique Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**/
package org.codehaus.activemq;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.management.JMSConsumerStatsImpl;
import org.codehaus.activemq.management.StatsCapable;
import org.codehaus.activemq.message.ActiveMQDestination;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.util.MemoryBoundedQueue;
import org.codehaus.activemq.selector.SelectorParser;
import javax.jms.IllegalStateException;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.management.j2ee.statistics.Stats;
/**
* A client uses a <CODE>MessageConsumer</CODE> object to receive messages from a destination. A <CODE>
* MessageConsumer</CODE> object is created by passing a <CODE>Destination</CODE> object to a message-consumer
* creation method supplied by a session.
* <P>
* <CODE>MessageConsumer</CODE> is the parent interface for all message consumers.
* <P>
* A message consumer can be created with a message selector. A message selector allows the client to restrict the
* messages delivered to the message consumer to those that match the selector.
* <P>
* A client may either synchronously receive a message consumer's messages or have the consumer asynchronously deliver
* them as they arrive.
* <P>
* For synchronous receipt, a client can request the next message from a message consumer using one of its <CODE>
* receive</CODE> methods. There are several variations of <CODE>receive</CODE> that allow a client to poll or wait
* for the next message.
* <P>
* For asynchronous delivery, a client can register a <CODE>MessageListener</CODE> object with a message consumer. As
* messages arrive at the message consumer, it delivers them by calling the <CODE>MessageListener</CODE>'s<CODE>
* onMessage</CODE> method.
* <P>
* It is a client programming error for a <CODE>MessageListener</CODE> to throw an exception.
*
* @version $Revision: 1.6 $
* @see javax.jms.MessageConsumer
* @see javax.jms.QueueReceiver
* @see javax.jms.TopicSubscriber
* @see javax.jms.Session
*/
public class ActiveMQMessageConsumer implements MessageConsumer, StatsCapable, Closeable {
private static final Log log = LogFactory.getLog(ActiveMQMessageConsumer.class);
protected ActiveMQSession session;
protected String consumerId;
protected MemoryBoundedQueue messageQueue;
protected String messageSelector;
private MessageListener messageListener;
protected String consumerName;
protected ActiveMQDestination destination;
private boolean closed;
protected int consumerNumber;
protected int prefetchNumber;
protected long startTime;
protected boolean noLocal;
protected boolean browser;
private Thread accessThread;
private Object messageListenerGuard;
private JMSConsumerStatsImpl stats;
/**
* Create a MessageConsumer
*
* @param theSession
* @param dest
* @param name
* @param selector
* @param cnum
* @param prefetch
* @param noLocalValue
* @param browserValue
* @throws JMSException
*/
protected ActiveMQMessageConsumer(ActiveMQSession theSession, ActiveMQDestination dest, String name,
String selector, int cnum, int prefetch, boolean noLocalValue, boolean browserValue) throws JMSException {
if (dest == null) {
throw new InvalidDestinationException("Do not understand a null destination");
}
if (dest.isTemporary() && theSession.connection.isJ2EEcompliant()) {
//validate that the destination comes from this Connection
String physicalName = dest.getPhysicalName();
if (physicalName == null) {
throw new IllegalArgumentException("Physical name of Destination should be valid: " + dest);
}
String clientID = theSession.connection.getInitializedClientID();
if (physicalName.indexOf(clientID) < 0) {
throw new InvalidDestinationException("Cannot use a Temporary destination from another Connection");
}
if( dest.isDeleted() ) {
throw new InvalidDestinationException("Cannot use a Temporary destination that has been deleted");
}
}
dest.incrementConsumerCounter();
if (selector != null) {
selector = selector.trim();
if (selector.length() > 0) {
// Validate that the selector
new SelectorParser().parse(selector);
}
}
this.session = theSession;
this.destination = dest;
this.consumerName = name;
this.messageSelector = selector;
this.consumerNumber = cnum;
this.prefetchNumber = prefetch;
this.noLocal = noLocalValue;
this.browser = browserValue;
this.startTime = System.currentTimeMillis();
this.messageListenerGuard = new Object();
String queueName = theSession.connection.clientID + ":" + name;
queueName += ":" + cnum;
this.messageQueue = theSession.connection.getMemoryBoundedQueue(queueName);
this.stats = new JMSConsumerStatsImpl(theSession.getSessionStats(), dest);
this.session.addConsumer(this);
}
/**
* @return the memory used by the internal queue for this MessageConsumer
*/
public long getLocalMemoryUsage(){
return this.messageQueue.getLocalMemoryUsedByThisQueue();
}
/**
* @return the number of messages enqueued by this consumer awaiting dispatch
*/
public int size(){
return this.messageQueue.size();
}
/**
* @return Stats for this MessageConsumer
*/
public Stats getStats() {
return stats;
}
/**
* @return Stats for this MessageConsumer
*/
public JMSConsumerStatsImpl getConsumerStats() {
return stats;
}
/**
* @return pretty print of this consumer
*/
public String toString() {
return "MessageConsumer: " + consumerId;
}
/**
* @return Returns the prefetchNumber.
*/
public int getPrefetchNumber() {
return prefetchNumber;
}
/**
* @param prefetchNumber The prefetchNumber to set.
*/
public void setPrefetchNumber(int prefetchNumber) {
this.prefetchNumber = prefetchNumber;
}
/**
* Gets this message consumer's message selector expression.
*
* @return this message consumer's message selector, or null if no message selector exists for the message consumer
* (that is, if the message selector was not set or was set to null or the empty string)
* @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
*/
public String getMessageSelector() throws JMSException {
checkClosed();
return this.messageSelector;
}
/**
* Gets the message consumer's <CODE>MessageListener</CODE>.
*
* @return the listener for the message consumer, or null if no listener is set
* @throws JMSException if the JMS provider fails to get the message listener due to some internal error.
* @see javax.jms.MessageConsumer#setMessageListener(javax.jms.MessageListener)
*/
public MessageListener getMessageListener() throws JMSException {
checkClosed();
return this.messageListener;
}
/**
* Sets the message consumer's <CODE>MessageListener</CODE>.
* <P>
* Setting the message listener to null is the equivalent of unsetting the message listener for the message
* consumer.
* <P>
* The effect of calling <CODE>MessageConsumer.setMessageListener</CODE> while messages are being consumed by an
* existing listener or the consumer is being used to consume messages synchronously is undefined.
*
* @param listener the listener to which the messages are to be delivered
* @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
* @see javax.jms.MessageConsumer#getMessageListener()
*/
public void setMessageListener(MessageListener listener) throws JMSException {
checkClosed();
this.messageListener = listener;
if (listener != null) {
session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_ASYNC);
}
}
/**
* Receives the next message produced for this message consumer.
* <P>
* This call blocks indefinitely until a message is produced or until this message consumer is closed.
* <P>
* If this <CODE>receive</CODE> is done within a transaction, the consumer retains the message until the
* transaction commits.
*
* @return the next message produced for this message consumer, or null if this message consumer is concurrently
* closed
* @throws JMSException
*/
public Message receive() throws JMSException {
checkClosed();
session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
try {
this.accessThread = Thread.currentThread();
ActiveMQMessage message = (ActiveMQMessage) messageQueue.dequeue();
this.accessThread = null;
if (message != null) {
boolean expired = message.isExpired();
messageDelivered(message, true,expired);
if (!expired){
message = message.shallowCopy();
}else {
message = (ActiveMQMessage) receiveNoWait(); //this will remove any other expired messages held in the queue
}
}
return message;
}
catch (InterruptedException ioe) {
return null;
}
}
/**
* Receives the next message that arrives within the specified timeout interval.
* <P>
* This call blocks until a message arrives, the timeout expires, or this message consumer is closed. A <CODE>
* timeout</CODE> of zero never expires, and the call blocks indefinitely.
*
* @param timeout the timeout value (in milliseconds)
* @return the next message produced for this message consumer, or null if the timeout expires or this message
* consumer is concurrently closed
* @throws JMSException
*/
public Message receive(long timeout) throws JMSException {
checkClosed();
session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
try {
if (timeout == 0) {
return this.receive();
}
this.accessThread = Thread.currentThread();
ActiveMQMessage message = (ActiveMQMessage) messageQueue.dequeue(timeout);
this.accessThread = null;
if (message != null) {
boolean expired = message.isExpired();
messageDelivered(message, true,expired);
if (!expired){
message = message.shallowCopy();
}else {
message = (ActiveMQMessage) receiveNoWait(); //this will remove any other expired messages held in the queue
}
}
return message;
}
catch (InterruptedException ioe) {
return null;
}
}
/**
* Receives the next message if one is immediately available.
*
* @return the next message produced for this message consumer, or null if one is not available
* @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
*/
public Message receiveNoWait() throws JMSException {
checkClosed();
session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
try {
ActiveMQMessage message = null;
//iterate through an scrub delivered but expired messages
while ((message = (ActiveMQMessage)messageQueue.dequeueNoWait())!= null){
boolean expired = message.isExpired();
messageDelivered(message,true,expired);
if (!expired){
return message.shallowCopy();
}
}
}
catch (InterruptedException ioe) {
throw new JMSException("Queue is interrupted: " + ioe.getMessage());
}
return null;
}
/**
* Closes the message consumer.
* <P>
* Since a provider may allocate some resources on behalf of a <CODE>MessageConsumer</CODE> outside the Java
* virtual machine, clients should close them when they are not needed. Relying on garbage collection to eventually
* reclaim these resources may not be timely enough.
* <P>
* This call blocks until a <CODE>receive</CODE> or message listener in progress has completed. A blocked message
* consumer <CODE>receive</CODE> call returns null when this message consumer is closed.
*
* @throws JMSException if the JMS provider fails to close the consumer due to some internal error.
*/
public void close() throws JMSException {
try {
this.accessThread.interrupt();
}
catch (NullPointerException npe) {
}
catch (SecurityException se) {
}
if( destination !=null )
destination.decrementConsumerCounter();
this.session.removeConsumer(this);
messageQueue.close();
closed = true;
}
/**
* @return true if this is a durable topic subscriber
*/
public boolean isDurableSubscriber() {
return this instanceof ActiveMQTopicSubscriber && consumerName != null && consumerName.length() > 0;
}
/**
* @throws IllegalStateException
*/
protected void checkClosed() throws IllegalStateException {
if (closed) {
throw new IllegalStateException("The Consumer is closed");
}
}
/**
* Process a Message - passing either to the queue or message listener
*
* @param message
*/
protected void processMessage(ActiveMQMessage message) {
message.setConsumerId(this.consumerId);
MessageListener listener = null;
synchronized (messageListenerGuard) {
listener = this.messageListener;
}
try {
if (!closed) {
if (listener != null) {
boolean expired = message.isExpired();
messageDelivered(message, true, expired);
if (!expired) {
listener.onMessage(message.shallowCopy());
}
}
else {
this.messageQueue.enqueue(message);
}
}
else {
messageDelivered(message,false,false);
}
}
catch (Exception e) {
log.warn("could not process message: " + message, e);
messageDelivered(message, false, false);
}
}
/**
* @return Returns the consumerId.
*/
protected String getConsumerId() {
return consumerId;
}
/**
* @param consumerId The consumerId to set.
*/
protected void setConsumerId(String consumerId) {
this.consumerId = consumerId;
}
/**
* @return the consumer name - used for durable consumers
*/
protected String getConsumerName() {
return this.consumerName;
}
/**
* Set the name of the Consumer - used for durable subscribers
*
* @param value
*/
protected void setConsumerName(String value) {
this.consumerName = value;
}
/**
* @return the locally unique Consumer Number
*/
protected int getConsumerNumber() {
return this.consumerNumber;
}
/**
* Set the locally unique consumer number
*
* @param value
*/
protected void setConsumerNumber(int value) {
this.consumerNumber = value;
}
/**
* @return true if this consumer does not accept locally produced messages
*/
protected boolean isNoLocal() {
return this.noLocal;
}
/**
* Retrive is a browser
*
* @return true if a browser
*/
protected boolean isBrowser() {
return this.browser;
}
/**
* Set true if only a Browser
*
* @param value
* @see ActiveMQQueueBrowser
*/
protected void setBrowser(boolean value) {
this.browser = value;
}
/**
* @return ActiveMQDestination
*/
protected ActiveMQDestination getDestination() {
return this.destination;
}
/**
* @return the startTime
*/
protected long getStartTime() {
return startTime;
}
protected void clearMessagesInProgress() {
messageQueue.clear();
}
private void messageDelivered(ActiveMQMessage message, boolean messageRead,boolean messageExpired) {
boolean read = browser ? false : messageRead;
if (message != null) {
message.setTransientConsumed(!isDurableSubscriber() && message.getJMSActiveMQDestination().isTopic());
this.session.messageDelivered((isDurableSubscriber() || destination.isQueue()), message, read,messageExpired);
if (messageRead) {
stats.onMessage(message);
}
}
}
}