/**
*
* 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.activemq.service.impl;
import java.util.ArrayList;
import java.util.List;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.broker.BrokerClient;
import org.activemq.broker.BrokerConnector;
import org.activemq.filter.Filter;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.BrokerInfo;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.MessageAck;
import org.activemq.service.DeadLetterPolicy;
import org.activemq.service.Dispatcher;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageIdentity;
import org.activemq.service.QueueList;
import org.activemq.service.QueueListEntry;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.SubscriberEntry;
import org.activemq.service.Subscription;
import org.activemq.service.TransactionManager;
import org.activemq.service.TransactionTask;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;
/**
* A Subscription holds messages to be dispatched to a a Client Consumer
*
* @version $Revision: 1.1.1.1 $
*/
public class SubscriptionImpl implements Subscription {
private static final Log log = LogFactory.getLog(SubscriptionImpl.class);
private String clientId;
private String subscriberName;
private ActiveMQDestination destination;
private String selector;
private int prefetchLimit;
private boolean noLocal;
private int consumerNumber;
private String consumerId;
private boolean browser;
protected Dispatcher dispatch;
protected String brokerName;
protected String clusterName;
protected MessageIdentity lastMessageIdentity;
Filter filter;
protected SynchronizedInt unconsumedMessagesDispatched = new SynchronizedInt(0);
QueueList messagePtrs = new DefaultQueueList();
private boolean usePrefetch = false;
private SubscriberEntry subscriberEntry;
private ConsumerInfo activeConsumer;
private BrokerClient activeClient;
private RedeliveryPolicy redeliveryPolicy;
private DeadLetterPolicy deadLetterPolicy;
private SynchronizedBoolean active = new SynchronizedBoolean(false);
Object lock = new Object();
/**
* Create a Subscription object that holds messages to be dispatched to a Consumer
*
* @param dispatcher
* @param client
* @param info
* @param filter
* @param redeliveryPolicy
* @param deadLetterPolicy
*/
public SubscriptionImpl(Dispatcher dispatcher, BrokerClient client, ConsumerInfo info, Filter filter,
RedeliveryPolicy redeliveryPolicy, DeadLetterPolicy deadLetterPolicy) {
this.dispatch = dispatcher;
this.filter = filter;
this.redeliveryPolicy = redeliveryPolicy;
this.deadLetterPolicy = deadLetterPolicy;
setActiveConsumer(client, info);
}
/**
* Set the active consumer info
*
* @param client
* @param info
*/
public void setActiveConsumer(BrokerClient client, ConsumerInfo info) {
if (info != null) {
this.clientId = info.getClientId();
this.subscriberName = info.getConsumerName();
this.noLocal = info.isNoLocal();
this.destination = info.getDestination();
this.selector = info.getSelector();
this.prefetchLimit = info.getPrefetchNumber();
this.consumerNumber = info.getConsumerNo();
this.consumerId = info.getConsumerId();
this.browser = info.isBrowser();
}
this.activeClient = client;
this.activeConsumer = info;
if (client != null) {
BrokerConnector brokerConnector = client.getBrokerConnector();
if (brokerConnector != null) {
BrokerInfo brokerInfo = brokerConnector.getBrokerInfo();
if (brokerInfo != null) {
brokerName = brokerInfo.getBrokerName();
clusterName = brokerInfo.getClusterName();
}
}
}
}
/**
* @return pretty print of the Subscription
*/
public String toString() {
String str = "SubscriptionImpl(" + super.hashCode() + ")[" + consumerId + "]" + clientId + ": "
+ subscriberName + " : " + destination;
return str;
}
/**
* Called when the Subscription is discarded
*
* @throws JMSException
*/
public void clear() throws JMSException {
synchronized (lock) {
QueueListEntry entry = messagePtrs.getFirstEntry();
while (entry != null) {
MessagePointer pointer = (MessagePointer) entry.getElement();
pointer.clear();
entry = messagePtrs.getNextEntry(entry);
}
messagePtrs.clear();
}
}
/**
* Called when an active subscriber has closed. This resets all MessagePtrs
*
* @throws JMSException
*/
public void reset() throws JMSException {
synchronized (lock) {
QueueListEntry entry = messagePtrs.getFirstEntry();
while (entry != null) {
MessagePointer pointer = (MessagePointer) entry.getElement();
if (pointer.isDispatched() && !pointer.isDeleted()) {
pointer.reset();
pointer.setRedelivered(true);
}
else {
break;
}
entry = messagePtrs.getNextEntry(entry);
}
}
}
/**
* @return Returns the clientId.
*/
public String getClientId() {
return clientId;
}
/**
* @param clientId The clientId to set.
*/
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* @return Returns the filter.
*/
public Filter getFilter() {
return filter;
}
/**
* @param filter The filter to set.
*/
public void setFilter(Filter filter) {
this.filter = filter;
}
public boolean isWildcard() {
return filter.isWildcard();
}
public String getPersistentKey() {
// not required other than for persistent topic subscriptions
return null;
}
public boolean isSameDurableSubscription(ConsumerInfo info) throws JMSException {
if (isDurableTopic()) {
return equal(clientId, info.getClientId()) && equal(subscriberName, info.getConsumerName());
}
return false;
}
/**
* @return Returns the noLocal.
*/
public boolean isNoLocal() {
return noLocal;
}
/**
* @param noLocal The noLocal to set.
*/
public void setNoLocal(boolean noLocal) {
this.noLocal = noLocal;
}
/**
* @return Returns the subscriberName.
*/
public String getSubscriberName() {
return subscriberName;
}
/**
* @param subscriberName The subscriberName to set.
*/
public void setSubscriberName(String subscriberName) {
this.subscriberName = subscriberName;
}
public RedeliveryPolicy getRedeliveryPolicy() {
return redeliveryPolicy;
}
public void setRedeliveryPolicy(RedeliveryPolicy redeliveryPolicy) {
this.redeliveryPolicy = redeliveryPolicy;
}
/**
* determines if the Subscription is interested in the message
*
* @param message
* @return true if this Subscription will accept the message
* @throws JMSException
*/
public boolean isTarget(ActiveMQMessage message) throws JMSException {
boolean result = false;
if (message != null) {
if (activeClient == null || brokerName == null || clusterName == null
|| !activeClient.isClusteredConnection() || !message.isEntryCluster(clusterName)
|| message.isEntryBroker(brokerName)) {
result = message.isDispatchedFromDLQ() || filter.matches(message);
// lets check that we don't have no-local enabled
if (noLocal && result) {
if (clientIDsEqual(message)) {
result = false;
}
}
}
}
return result;
}
/**
* If the Subscription is a target for the message, the subscription will add a reference to the message and
* register an interest in the message to the container
*
* @param container
* @param message
* @throws JMSException
*/
public void addMessage(MessageContainer container, ActiveMQMessage message) throws JMSException {
//log.info("###### Adding to subscription: " + this + " message: " + message);
if (log.isDebugEnabled()) {
log.debug("Adding to subscription: " + this + " message: " + message);
}
MessagePointer pointer = new MessagePointer(container, message);
synchronized (lock) {
messagePtrs.add(pointer);
}
dispatch.wakeup(this);
lastMessageIdentity = message.getJMSMessageIdentity();
}
/**
* Indicates a message has been delivered to a MessageConsumer
*
* @param ack
* @throws JMSException
*/
public void messageConsumed(final MessageAck ack) throws JMSException {
//remove up to this message
int count = 0;
boolean found = false;
synchronized (lock) {
QueueListEntry entry = messagePtrs.getFirstEntry();
while (entry != null) {
final MessagePointer pointer = (MessagePointer) entry.getElement();
count++;
// If in transaction: only consume the message acked.
// If not in transaction: consume all previously delivered messages.
if (!ack.isPartOfTransaction() || pointer.getMessageIdentity().equals(ack.getMessageIdentity())) {
if ((ack.isExpired() || ack.isMessageRead()) && !browser) {
pointer.delete(ack);//delete message from the container (if possible)
}
if (!ack.isMessageRead() && !browser) {
// It was a NACK.
pointer.reset();
pointer.setRedelivered(true);
}
else {
unconsumedMessagesDispatched.decrement();
// We may have to undo the delivery..
TransactionManager.getContexTransaction().addPostRollbackTask(new TransactionTask() {
public void execute() throws Throwable {
unconsumedMessagesDispatched.increment();
pointer.reset();
pointer.setRedelivered(true);
dispatch.wakeup(SubscriptionImpl.this);
}
});
final QueueListEntry theEntry = entry;
TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask() {
public void execute() throws Throwable {
messagePtrs.remove(theEntry);
if ((ack.isExpired() || ack.isMessageRead()) && !browser) {
if (ack.isExpired() && !pointer.getContainer().isDeadLetterQueue()) {
ActiveMQMessage msg = pointer.getContainer().getMessage(
pointer.getMessageIdentity());
if (msg != null) {
deadLetterPolicy.sendToDeadLetter(msg);
}
}
}
}
});
}
if (pointer.getMessageIdentity().equals(ack.getMessageIdentity())) {
found = true;
break;
}
}
entry = messagePtrs.getNextEntry(entry);
}
}
if (!found && log.isDebugEnabled()) {
log.debug("Did not find a matching message for identity: " + ack.getMessageIdentity());
}
dispatch.wakeup(this);
}
/**
* Retrieve messages to dispatch
*
* @return the messages to dispatch
* @throws JMSException
*/
public ActiveMQMessage[] getMessagesToDispatch() throws JMSException {
if (usePrefetch) {
return getMessagesWithPrefetch();
}
List tmpList = new ArrayList();
synchronized (lock) {
QueueListEntry entry = messagePtrs.getFirstEntry();
while (entry != null) {
MessagePointer pointer = (MessagePointer) entry.getElement();
if (!pointer.isDispatched()) {
ActiveMQMessage msg = pointer.getContainer().getMessage(pointer.getMessageIdentity());
if (msg != null) {
if (pointer.isDispatched() || pointer.isRedelivered()) {
//already dispatched - so mark as redelivered
msg.setJMSRedelivered(true);
if (redeliveryPolicy.isBackOffMode()
&& msg.getDeliveryCount() < redeliveryPolicy.getMaximumRetryCount()) {
long sleepTime = redeliveryPolicy.getInitialRedeliveryTimeout();
sleepTime *= (msg.getDeliveryCount() * redeliveryPolicy.getBackOffIncreaseRate());
try {
Thread.sleep(sleepTime);
}
catch (InterruptedException e) {
}
}
//incremenent delivery count
msg.incrementDeliveryCount();
}
if (!pointer.getContainer().isDeadLetterQueue()
&& (msg.isExpired() || msg.getDeliveryCount() >= redeliveryPolicy
.getMaximumRetryCount())) {
if (msg.isExpired()) {
log.warn("Message: " + msg + " has expired");
}
else {
log.warn("Message: " + msg + " exceeded retry count: " + msg.getDeliveryCount());
}
deadLetterPolicy.sendToDeadLetter(msg);
QueueListEntry discarded = entry;
entry = messagePtrs.getPrevEntry(discarded);
messagePtrs.remove(discarded);
}
else {
pointer.setDispatched(true);
msg.setDispatchedFromDLQ(pointer.getContainer().isDeadLetterQueue());
tmpList.add(msg);
}
}
else {
//the message is probably expired
log.info("Message probably expired: " + msg);
QueueListEntry discarded = entry;
entry = messagePtrs.getPrevEntry(discarded);
messagePtrs.remove(discarded);
if (msg != null) {
deadLetterPolicy.sendToDeadLetter(msg);
}
}
}
entry = messagePtrs.getNextEntry(entry);
}
}
ActiveMQMessage[] messages = new ActiveMQMessage[tmpList.size()];
return (ActiveMQMessage[]) tmpList.toArray(messages);
}
public SubscriberEntry getSubscriptionEntry() {
if (subscriberEntry == null) {
subscriberEntry = createSubscriptionEntry();
}
return subscriberEntry;
}
public boolean isLocalSubscription() {
if (activeClient != null) {
return !(activeClient.isClusteredConnection() || activeClient.isBrokerConnection());
}
return true;
}
// Implementation methods
//-------------------------------------------------------------------------
protected SubscriberEntry createSubscriptionEntry() {
SubscriberEntry answer = new SubscriberEntry();
answer.setClientID(clientId);
answer.setConsumerName(subscriberName);
answer.setDestination(destination.getPhysicalName());
answer.setSelector(selector);
return answer;
}
protected ActiveMQMessage[] getMessagesWithPrefetch() throws JMSException {
List tmpList = new ArrayList();
synchronized (lock) {
QueueListEntry entry = messagePtrs.getFirstEntry();
int count = 0;
int maxNumberToDispatch = prefetchLimit - unconsumedMessagesDispatched.get();
while (entry != null && count < maxNumberToDispatch) {
MessagePointer pointer = (MessagePointer) entry.getElement();
if (!pointer.isDispatched()) {
ActiveMQMessage msg = pointer.getContainer().getMessage(pointer.getMessageIdentity());
if (msg != null && !msg.isExpired()) {
if (pointer.isDispatched() || pointer.isRedelivered()) {
//already dispatched - so mark as redelivered
msg.setJMSRedelivered(true);
}
pointer.setDispatched(true);
tmpList.add(msg);
unconsumedMessagesDispatched.increment();
count++;
}
else {
//the message is probably expired
log.info("Message probably expired: " + msg);
QueueListEntry discarded = entry;
entry = messagePtrs.getPrevEntry(discarded);
messagePtrs.remove(discarded);
if (msg != null) {
deadLetterPolicy.sendToDeadLetter(msg);
}
}
}
entry = messagePtrs.getNextEntry(entry);
}
}
/**
* if (tmpList.isEmpty() && ! messagePtrs.isEmpty()) { System.out.println("### Nothing to dispatch but
* messagePtrs still has: " + messagePtrs.size() + " to dispatch, prefetchLimit: " + prefetchLimit + "
* unconsumedMessagesDispatched: " + unconsumedMessagesDispatched.get() + " maxNumberToDispatch: " +
* maxNumberToDispatch); MessagePointer first = (MessagePointer) messagePtrs.getFirst(); System.out.println("###
* First: " + first + " dispatched: " + first.isDispatched() + " id: " + first.getMessageIdentity()); } else {
* if (! tmpList.isEmpty()) { System.out.println("### dispatching: " + tmpList.size() + " items = " + tmpList); } }
*/
ActiveMQMessage[] messages = new ActiveMQMessage[tmpList.size()];
return (ActiveMQMessage[]) tmpList.toArray(messages);
}
/**
* Indicates the Subscription it's reached it's pre-fetch limit
*
* @return true/false
* @throws JMSException
*/
public boolean isAtPrefetchLimit() throws JMSException {
if (usePrefetch) {
int underlivedMessageCount = messagePtrs.size() - unconsumedMessagesDispatched.get();
return underlivedMessageCount >= prefetchLimit;
}
else {
return false;
}
}
/**
* Indicates if this Subscription has more messages to send to the Consumer
*
* @return true if more messages available to dispatch
*/
public boolean isReadyToDispatch() throws JMSException {
/** TODO we may have dispatched messags inside messagePtrs */
boolean answer = active.get() && messagePtrs.size() > 0;
return answer;
}
/**
* @return Returns the destination.
*/
public ActiveMQDestination getDestination() {
return destination;
}
/**
* @return Returns the selector.
*/
public String getSelector() {
return selector;
}
/**
* @return Returns the active.
*/
public boolean isActive() {
return active.get();
}
/**
* @param newActive The active to set.
* @throws JMSException
*/
public void setActive(boolean newActive) throws JMSException {
synchronized (active.getLock()) {
active.set(newActive);
}
if (!newActive) {
reset();
}
}
/**
* @return Returns the consumerNumber.
*/
public int getConsumerNumber() {
return consumerNumber;
}
/**
* @return the consumer Id for the active consumer
*/
public String getConsumerId() {
return consumerId;
}
/**
* Indicates the Subscriber is a Durable Subscriber
*
* @return true if the subscriber is a durable topic
* @throws JMSException
*/
public boolean isDurableTopic() throws JMSException {
return destination.isTopic() && subscriberName != null && subscriberName.length() > 0;
}
/**
* Indicates the consumer is a browser only
*
* @return true if a Browser
* @throws JMSException
*/
public boolean isBrowser() throws JMSException {
return browser;
}
public MessageIdentity getLastMessageIdentity() throws JMSException {
return lastMessageIdentity;
}
public void setLastMessageIdentifier(MessageIdentity messageIdentity) throws JMSException {
this.lastMessageIdentity = messageIdentity;
}
protected boolean clientIDsEqual(ActiveMQMessage message) {
String msgClientID = message.getJMSClientID();
String subClientID = clientId;
if (msgClientID == null || subClientID == null) {
return false;
}
else {
return msgClientID.equals(subClientID);
}
}
protected static final boolean equal(Object left, Object right) {
return left == right || (left != null && right != null && left.equals(right));
}
}