/**
* 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.stomp;
import static io.hawtjms.provider.stomp.StompConstants.ACK;
import static io.hawtjms.provider.stomp.StompConstants.ACK_ID;
import static io.hawtjms.provider.stomp.StompConstants.ACK_MODE;
import static io.hawtjms.provider.stomp.StompConstants.DESTINATION;
import static io.hawtjms.provider.stomp.StompConstants.ID;
import static io.hawtjms.provider.stomp.StompConstants.MESSAGE_ID;
import static io.hawtjms.provider.stomp.StompConstants.SELECTOR;
import static io.hawtjms.provider.stomp.StompConstants.SUBSCRIBE;
import static io.hawtjms.provider.stomp.StompConstants.SUBSCRIPTION;
import static io.hawtjms.provider.stomp.StompConstants.UNSUBSCRIBE;
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.jms.meta.JmsSessionId;
import io.hawtjms.provider.AsyncResult;
import io.hawtjms.provider.ProviderConstants.ACK_TYPE;
import io.hawtjms.provider.ProviderListener;
import io.hawtjms.provider.stomp.adapters.StompServerAdapter;
import java.io.IOException;
import java.util.LinkedList;
import javax.jms.JMSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* STOMP Consumer class used to manage the subscription process for
* STOMP and handle the various ACK modes and mappings to JMS.
*/
public class StompConsumer {
private static final Logger LOG = LoggerFactory.getLogger(StompConsumer.class);
protected final JmsConsumerInfo consumerInfo;
protected final StompSession session;
protected final StompConnection connection;
protected final StompServerAdapter adapter;
protected boolean started;
protected final LinkedList<JmsInboundMessageDispatch> delivered =
new LinkedList<JmsInboundMessageDispatch>();
/**
* Create a new STOMP Consumer that maps a STOMP subscription to a JMS Framework
* MessageConsumer.
*
* @param session
* the session that acts as this consumer's parent.
* @param consumerInfo
* the information object that defines this consumer instance.
*/
public StompConsumer(StompSession session, JmsConsumerInfo consumerInfo) {
this.consumerInfo = consumerInfo;
this.session = session;
this.connection = session.getConnection();
this.adapter = connection.getServerAdapter();
this.consumerInfo.getConsumerId().setProviderHint(this);
}
/**
* Places the consumer in the started state. Messages delivered prior to a consumer being
* started should be held and only dispatched after start.
*/
public void start() {
this.started = true;
}
/**
* Performs the actual subscription by creating an appropriate SUBSCRIBE
* frame and sending it to the server.
*
* @param request
* the request that initiated this operation.
*
* @throws JMSException if the subscription requested is not supported or invalid.
* @throws IOException if an error occurs while sending the frame.
*/
public void subscribe(AsyncResult<Void> request) throws JMSException, IOException {
StompFrame subscribe = new StompFrame(SUBSCRIBE);
subscribe.setProperty(ID, consumerInfo.getConsumerId().toString());
subscribe.setProperty(DESTINATION, adapter.toStompDestination(consumerInfo.getDestination()));
// For either the Auto case or the Client Ack case we use client so we can control the flow
// of messages based on prefetch and delivery.
// TODO - We could add an Individual Ack Mode to the JMS frameworks and let the provider
// error out if that's not supported.
subscribe.setProperty(ACK_MODE, "client");
if (consumerInfo.getSelector() != null) {
subscribe.setProperty(SELECTOR, consumerInfo.getSelector());
}
adapter.addSubscribeHeaders(subscribe, consumerInfo);
connection.request(subscribe, request);
}
/**
* Close the consumer by sending an UNSUBSCRIBE command to the remote peer and then
* removing the consumer from the session's state information.
*
* @param request
* the request that initiated this operation/
*/
public void close(AsyncResult<Void> request) throws IOException {
session.removeConsumer(getConsumerId());
StompFrame frame = new StompFrame(UNSUBSCRIBE);
frame.setProperty(ID, consumerInfo.getConsumerId().toString());
connection.request(frame, request);
}
/**
* Handles any incoming messages for this consumer. The Frame is converted into the
* appropriate JmsMessage type and fired to the provider listener.
*
* @param message
* the incoming STOMP MESSAGE frame to dispatch.
*
* @throws JMSException if an error occurs while processing the frame.
*/
public void processMessage(StompFrame message) throws JMSException {
JmsMessage converted = adapter.convertToJmsMessage(message);
JmsInboundMessageDispatch envelope = new JmsInboundMessageDispatch();
envelope.setConsumerId(consumerInfo.getConsumerId());
envelope.setMessage(converted);
envelope.setProviderHint(message);
connection.getProvider().getProviderListener().onMessage(envelope);
}
/**
* 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.
*
* @param request
* the request that awaits completion of this action.
*
* @throws IOException if an error occurs while sending the ACK frame.
*/
public void acknowledge(AsyncResult<Void> request) throws IOException {
LOG.trace("Session Acknowledge for consumer: {}", getConsumerId());
// STOMP client Ack messages are cumulative so one frame is all we need.
if (!delivered.isEmpty()) {
JmsInboundMessageDispatch envelope = delivered.getLast();
acknowledge(envelope, ACK_TYPE.CONSUMED, request);
delivered.clear();
} else {
request.onSuccess();
}
}
/**
* Acknowledge the message that was delivered in the given envelope based on the
* given acknowledgment type.
*
* @param envelope
* the envelope that contains the delivery information for the message.
* @param ackType
* the type of acknowledge operation that should be performed.
* @param request
* the asynchronous request awaiting completion of this operation.
*
* @throws IOException if an error occurs while writing the frame.
*/
public void acknowledge(JmsInboundMessageDispatch envelope, ACK_TYPE ackType, AsyncResult<Void> request) throws IOException {
StompFrame messageFrame = (StompFrame) envelope.getProviderHint();
JmsMessageId messageId = envelope.getMessage().getFacade().getMessageId();
if (ackType.equals(ACK_TYPE.DELIVERED)) {
LOG.debug("Delivered Ack of message: {}", messageId);
delivered.add(envelope);
StompFrame credit = adapter.createCreditFrame(messageFrame);
if (credit != null) {
connection.send(credit);
}
request.onSuccess();
} else if (ackType.equals(ACK_TYPE.CONSUMED)) {
LOG.debug("Consumed Ack of message: {}", messageId);
delivered.remove(envelope);
StompFrame ack = new StompFrame(ACK);
ack.setProperty(MESSAGE_ID, messageId.toString());
ack.setProperty(SUBSCRIPTION, getConsumerId().toString());
String ackHeader = messageFrame.getProperty(ACK_ID);
if (ackHeader != null) {
ack.setProperty(ID, ackHeader);
}
// TODO - Transaction.
connection.request(ack, request);
return;
} else if (ackType.equals(ACK_TYPE.REDELIVERED)) {
LOG.debug("Redelivered Ack of message: {}", messageId);
request.onSuccess();
} else if (ackType.equals(ACK_TYPE.POISONED)) {
LOG.debug("Poisoned Ack of message: {}", messageId);
request.onSuccess();
} else {
LOG.warn("Unsupporeted Ack Type for message: {}", messageId);
request.onFailure(new JMSException("Failed to send ack for: " + messageId));
}
}
public JmsConsumerId getConsumerId() {
return this.consumerInfo.getConsumerId();
}
public JmsSessionId getSessionId() {
return this.consumerInfo.getParentId();
}
public StompSession getSession() {
return this.session;
}
public boolean isStarted() {
return started;
}
public boolean isBrowser() {
return false;
}
//---------- Internal helper methods -------------------------------------//
protected void deliver(JmsInboundMessageDispatch envelope) {
ProviderListener listener = connection.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.");
}
}
}