/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 io.hawtjms.provider.amqp;
import io.hawtjms.jms.JmsDestination;
import io.hawtjms.jms.message.JmsInboundMessageDispatch;
import io.hawtjms.jms.message.JmsMessage;
import io.hawtjms.jms.meta.JmsConsumerId;
import io.hawtjms.jms.meta.JmsConsumerInfo;
import io.hawtjms.jms.meta.JmsMessageId;
import io.hawtjms.provider.AsyncResult;
import io.hawtjms.provider.ProviderConstants.ACK_TYPE;
import io.hawtjms.provider.ProviderListener;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.jms.JMSException;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.DescribedType;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.Modified;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.amqp.messaging.TerminusDurability;
import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
import org.apache.qpid.proton.amqp.transaction.TransactionalState;
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
import org.apache.qpid.proton.jms.EncodedMessage;
import org.apache.qpid.proton.jms.InboundTransformer;
import org.apache.qpid.proton.jms.JMSMappingInboundTransformer;
import org.fusesource.hawtbuf.Buffer;
import org.fusesource.hawtbuf.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AMQP Consumer object that is used to manage JMS MessageConsumer semantics.
*/
public class AmqpConsumer extends AbstractAmqpResource<JmsConsumerInfo, Receiver> {
private static final Logger LOG = LoggerFactory.getLogger(AmqpConsumer.class);
protected static final Symbol COPY = Symbol.getSymbol("copy");
protected static final Symbol JMS_NO_LOCAL_SYMBOL = Symbol.valueOf("no-local");
protected static final Symbol JMS_SELECTOR_SYMBOL = Symbol.valueOf("jms-selector");
protected final AmqpSession session;
protected final InboundTransformer inboundTransformer =
new JMSMappingInboundTransformer(AmqpJMSVendor.INSTANCE);;
protected final Map<JmsMessageId, Delivery> delivered = new LinkedHashMap<JmsMessageId, Delivery>();
protected boolean presettle;
private final ByteArrayOutputStream streamBuffer = new ByteArrayOutputStream();
private final byte incomingBuffer[] = new byte[1024 * 64];
public AmqpConsumer(AmqpSession session, JmsConsumerInfo info) {
super(info);
this.session = session;
// Add a shortcut back to this Consumer for quicker lookups
this.info.getConsumerId().setProviderHint(this);
}
/**
* Starts the consumer by setting the link credit to the given prefetch value.
*/
public void start(AsyncResult<Void> request) {
this.endpoint.flow(info.getPrefetchSize());
request.onSuccess();
}
@Override
protected void doOpen() {
JmsDestination destination = info.getDestination();
String subscription = session.getQualifiedName(destination);
Source source = new Source();
source.setAddress(subscription);
Target target = new Target();
configureSource(source);
String receiverName = getConsumerId() + ":" + subscription;
if (info.getSubscriptionName() != null && !info.getSubscriptionName().isEmpty()) {
// In the case of Durable Topic Subscriptions the client must use the same
// receiver name which is derived from the subscription name property.
receiverName = info.getSubscriptionName();
}
endpoint = session.getProtonSession().receiver(receiverName);
endpoint.setSource(source);
endpoint.setTarget(target);
if (isPresettle()) {
endpoint.setSenderSettleMode(SenderSettleMode.SETTLED);
} else {
endpoint.setSenderSettleMode(SenderSettleMode.UNSETTLED);
}
endpoint.setReceiverSettleMode(ReceiverSettleMode.FIRST);
}
@Override
public void opened() {
this.session.addResource(this);
super.opened();
}
@Override
public void closed() {
this.session.removeResource(this);
super.closed();
}
protected void configureSource(Source source) {
Map<Symbol, DescribedType> filters = new HashMap<Symbol, DescribedType>();
if (info.getSubscriptionName() != null && !info.getSubscriptionName().isEmpty()) {
source.setExpiryPolicy(TerminusExpiryPolicy.NEVER);
source.setDurable(TerminusDurability.UNSETTLED_STATE);
source.setDistributionMode(COPY);
} else {
source.setDurable(TerminusDurability.NONE);
source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH);
}
if (info.isNoLocal()) {
filters.put(JMS_NO_LOCAL_SYMBOL, AmqpJmsNoLocalType.NO_LOCAL);
}
if (info.getSelector() != null && !info.getSelector().trim().equals("")) {
filters.put(JMS_SELECTOR_SYMBOL, new AmqpJmsSelectorType(info.getSelector()));
}
if (!filters.isEmpty()) {
source.setFilter(filters);
}
}
/**
* Called to acknowledge all messages that have been marked as delivered but
* have not yet been marked consumed. Usually this is called as part of an
* client acknowledge session operation.
*
* Only messages that have already been acknowledged as delivered by the JMS
* framework will be in the delivered Map. This means that the link credit
* would already have been given for these so we just need to settle them.
*/
public void acknowledge() {
LOG.trace("Session Acknowledge for consumer: {}", info.getConsumerId());
for (Delivery delivery : delivered.values()) {
delivery.disposition(Accepted.getInstance());
delivery.settle();
}
delivered.clear();
}
/**
* Called to acknowledge a given delivery. Depending on the Ack Mode that
* the consumer was created with this method can acknowledge more than just
* the target delivery.
*
* @param envelope
* the delivery that is to be acknowledged.
* @param ackType
* the type of acknowledgment to perform.
*/
public void acknowledge(JmsInboundMessageDispatch envelope, ACK_TYPE ackType) {
JmsMessageId messageId = envelope.getMessage().getFacade().getMessageId();
Delivery delivery = null;
if (messageId.getProviderHint() instanceof Delivery) {
delivery = (Delivery) messageId.getProviderHint();
} else {
delivery = delivered.get(messageId);
if (delivery == null) {
LOG.warn("Received Ack for unknown message: {}", messageId);
return;
}
}
if (ackType.equals(ACK_TYPE.DELIVERED)) {
LOG.debug("Delivered Ack of message: {}", messageId);
if (session.isTransacted()) {
Binary txnId = session.getTransactionContext().getAmqpTransactionId();
if (txnId != null) {
TransactionalState txState = new TransactionalState();
txState.setOutcome(Accepted.getInstance());
txState.setTxnId(txnId);
delivery.disposition(txState);
session.getTransactionContext().registerTxConsumer(this);
}
}
if (!isPresettle()) {
delivered.put(messageId, delivery);
}
sendFlowIfNeeded();
} else if (ackType.equals(ACK_TYPE.CONSUMED)) {
// A Consumer may not always send a delivered ACK so we need to check to
// ensure we don't add to much credit to the link.
if (isPresettle() || delivered.remove(messageId) == null) {
sendFlowIfNeeded();
}
LOG.debug("Consumed Ack of message: {}", messageId);
if (!delivery.isSettled()) {
delivery.disposition(Accepted.getInstance());
delivery.settle();
}
} else if (ackType.equals(ACK_TYPE.REDELIVERED)) {
Modified disposition = new Modified();
disposition.setUndeliverableHere(false);
disposition.setDeliveryFailed(true);
delivery.disposition(disposition);
delivery.settle();
} else if (ackType.equals(ACK_TYPE.POISONED)) {
deliveryFailed(delivery, false);
} else {
LOG.warn("Unsupporeted Ack Type for message: {}", messageId);
}
}
/**
* We only send more credits as the credit window dwindles to a certain point and
* then we open the window back up to full prefetch size.
*/
private void sendFlowIfNeeded() {
if (info.getPrefetchSize() == 0) {
return;
}
int currentCredit = endpoint.getCredit();
if (currentCredit <= info.getPrefetchSize() * 0.2) {
endpoint.flow(info.getPrefetchSize() - currentCredit);
}
}
/**
* Recovers all previously delivered but not acknowledged messages.
*/
public void recover() {
LOG.debug("Session Recover for consumer: {}", info.getConsumerId());
for (Delivery delivery : delivered.values()) {
// TODO - increment redelivery counter and apply connection redelivery policy
// to those messages that are past max redlivery.
JmsInboundMessageDispatch envelope = (JmsInboundMessageDispatch) delivery.getContext();
envelope.onMessageRedelivered();
deliver(envelope);
}
delivered.clear();
}
/**
* For a consumer whose prefetch value is set to zero this method will attempt to solicite
* a new message dispatch from the broker.
*
* @param timeout
*/
public void pull(long timeout) {
if (info.getPrefetchSize() == 0 && endpoint.getCredit() == 0) {
// expand the credit window by one.
endpoint.flow(1);
}
}
@Override
public void processDeliveryUpdates() throws IOException {
Delivery incoming = null;
do {
incoming = endpoint.current();
if (incoming != null && incoming.isReadable() && !incoming.isPartial()) {
LOG.trace("{} has incoming Message(s).", this);
processDelivery(incoming);
endpoint.advance();
} else {
LOG.trace("{} has a partial incoming Message(s), deferring.", this);
incoming = null;
}
} while (incoming != null);
}
private void processDelivery(Delivery incoming) {
EncodedMessage encoded = readIncomingMessage(incoming);
JmsMessage message = null;
try {
message = (JmsMessage) inboundTransformer.transform(encoded);
} catch (Exception e) {
LOG.warn("Error on transform: {}", e.getMessage());
// TODO - We could signal provider error but not sure we want to fail
// the connection just because we can't convert the message.
deliveryFailed(incoming, true);
return;
}
try {
message.setJMSDestination(info.getDestination());
} catch (JMSException e) {
LOG.warn("Error on transform: {}", e.getMessage());
// TODO - We could signal provider error but not sure we want to fail
// the connection just because we can't convert the message.
deliveryFailed(incoming, true);
return;
}
// Store link to delivery in the hint for use in acknowledge requests.
message.getFacade().getMessageId().setProviderHint(incoming);
JmsInboundMessageDispatch envelope = new JmsInboundMessageDispatch();
envelope.setMessage(message);
envelope.setConsumerId(info.getConsumerId());
envelope.setProviderHint(incoming);
// Store reference to envelope in delivery context for recovery
incoming.setContext(envelope);
deliver(envelope);
}
@Override
protected void doClose() {
}
public AmqpSession getSession() {
return this.session;
}
public JmsConsumerId getConsumerId() {
return this.info.getConsumerId();
}
public Receiver getProtonReceiver() {
return this.endpoint;
}
public boolean isBrowser() {
return false;
}
public boolean isPresettle() {
return presettle;
}
public void setPresettle(boolean presettle) {
this.presettle = presettle;
}
@Override
public String toString() {
return "AmqpConsumer { " + this.info.getConsumerId() + " }";
}
protected void deliveryFailed(Delivery incoming, boolean expandCredit) {
Modified disposition = new Modified();
disposition.setUndeliverableHere(true);
disposition.setDeliveryFailed(true);
incoming.disposition(disposition);
incoming.settle();
if (expandCredit) {
endpoint.flow(1);
}
}
protected void deliver(JmsInboundMessageDispatch envelope) {
ProviderListener listener = session.getProvider().getProviderListener();
if (listener != null) {
if (envelope.getMessage() != null) {
LOG.debug("Dispatching received message: {}", envelope.getMessage().getFacade().getMessageId());
} else {
LOG.debug("Dispatching end of browse to: {}", envelope.getConsumerId());
}
listener.onMessage(envelope);
} else {
LOG.error("Provider listener is not set, message will be dropped.");
}
}
protected EncodedMessage readIncomingMessage(Delivery incoming) {
Buffer buffer;
int count;
while ((count = endpoint.recv(incomingBuffer, 0, incomingBuffer.length)) > 0) {
streamBuffer.write(incomingBuffer, 0, count);
}
buffer = streamBuffer.toBuffer();
try {
return new EncodedMessage(incoming.getMessageFormat(), buffer.data, buffer.offset, buffer.length);
} finally {
streamBuffer.reset();
}
}
public void preCommit() {
}
public void preRollback() {
}
/**
* Ensures that all delivered messages are marked as settled locally before the TX state
* is cleared and the next TX started.
*/
public void postCommit() {
for (Delivery delivery : delivered.values()) {
delivery.settle();
}
this.delivered.clear();
}
/**
* Redeliver Acknowledge all previously delivered messages and clear state to prepare for
* the next TX to start.
*/
public void postRollback() {
for (Delivery delivery : delivered.values()) {
JmsInboundMessageDispatch envelope = (JmsInboundMessageDispatch) delivery.getContext();
acknowledge(envelope, ACK_TYPE.REDELIVERED);
}
this.delivered.clear();
}
}