package org.kisst.jms;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.kisst.gft.LogService;
import org.kisst.props4j.Props;
import org.kisst.util.TemplateUtil;
import org.kisst.util.TimeWindowList;
import org.kisst.util.exception.FunctionalException;
import org.kisst.util.exception.HasDetails;
import org.kisst.util.exception.MappedStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings("deprecation")
public class JmsListener implements Runnable {
private final static Logger logger=LoggerFactory.getLogger(JmsListener.class);
private final JmsSystem system;
private MessageHandler handler;
private final Props props;
public final String queue;
public final String errorqueue;
public final boolean useJmsPropsForErrorMessages;
//public final String retryqueue;
private final int receiveErrorRetries;
private final int receiveErrorRetryDelay;
private final long interval;
private final TimeWindowList forbiddenTimes;
private Session session = null;
private Queue destination = null;
private MessageConsumer consumer = null;
private boolean browseMode=true;
private boolean running=false;
private String messageId=null;
Thread thread;
public JmsListener(JmsSystem system, MessageHandler handler, Props props, Object context) {
this.system=system;
this.props=props;
this.handler=handler;
this.interval=props.getLong("interval",5000);
this.queue=TemplateUtil.processTemplate(props.getString("queue"), context);
this.errorqueue=TemplateUtil.processTemplate(props.getString("errorqueue"), context);
//this.retryqueue=TemplateUtil.processTemplate(props.getString("retryqueue"), context);
this.receiveErrorRetries = props.getInt("receiveErrorRetries", 1000);
this.receiveErrorRetryDelay = props.getInt("receiveErrorRetryDelay", 60000);
this.useJmsPropsForErrorMessages=props.getBoolean("useJmsPropsForErrorMessages", true);
String timewindow=props.getString("forbiddenTimes", null);
if (timewindow==null)
this.forbiddenTimes=null;
else
this.forbiddenTimes=new TimeWindowList(timewindow);
}
public String getStatus() {
if (messageId!=null)
return "WORKING "+messageId;
if (! running)
return "STOPPED";
if (browseMode)
return "PAUZED";
return "LISTENING";
}
public boolean isActive() { return running && ! browseMode; }
public String toString() { return "JmsListener("+queue+")"; }
public boolean isForbiddenTime() {
return forbiddenTimes!=null && forbiddenTimes.isTimeInWindow();
}
public synchronized void start() {
logger.info("Starting Listener {}",this);
if (thread!=null)
logger.warn("Listener already started");
else {
running=true;
thread=new Thread(this);
thread.start();
}
}
public void stop() {
logger.info("Stopping Listener {}",this);
running=false;
Thread t=thread;
if (t!=null) {
try {
t.join();
} catch (InterruptedException e) { throw new RuntimeException(e);}
logger.info("Stopped Listener {}",this);
}
}
public void run() {
// Always start in browse mode, to check for stop messages
// If one would receive the stop message, other threads/machines might temporarily
// not see it, and pick-up the next message
logger.info("Started Listener on queue {}",queue);
enterBrowseMode();
try {
while (running) {
Message message=null;
message = getMessage();
if (message!=null) {
try {
this.messageId=message.getJMSMessageID();
if (logger.isDebugEnabled())
logger.debug("handling message {}",message.getJMSMessageID());
handleMessage(message);
}
finally {
this.messageId=null;
}
}
}
}
catch (Throwable e) { // DLL link errors
logger.error("Unrecoverable error during listening, stopped listening", e);
String hostName="unknown";
try {
hostName = java.net.InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e1) {/* ignore, is only nice to have info for logging purposes */ }
LogService.log("error", "StoppingListener", "Listener", hostName, "Unrecoverable error during listening, stopped listening thread "+thread.getName()+" on host "+ hostName+": "+e.getMessage());
if (props.getBoolean("exitOnUnrecoverableListenerError", false))
System.exit(1);
}
finally {
thread=null;
running=false;
logger.info("Stopped listening to queue {}", queue);
closeSession();
}
}
private Message getMessage() throws JMSException {
if (isForbiddenTime()) {
logger.debug("Sleeping for {} millisecs because of forbiddenTime", interval);
sleepSomeTime(interval);
return null;
}
int retryCount=0;
try {
if (checkBrowseMode())
return null;
openConsumer();
Message message = consumer.receive(interval);
if (message!=null) {
if (ControlMessage.isStopMessage(message)) {
logger.info("Received a stop message on queue {}, rolling back the stop message",queue);
session.rollback(); // put the message back on the queue
closeSession(); // recover the session so it will see the stop message
enterBrowseMode();
return null;
}
else if (ControlMessage.isStartMessage(message)) {
logger.info("Ignoring a received a start message on queue {}, because is already started",queue);
session.commit(); // remove the start message
return null;
}
return message;
}
retryCount=0;
}
catch (Exception e) {
logger.error("Error when receiving JMS message on queue "+queue, e);
if (retryCount++ > receiveErrorRetries)
throw new RuntimeException("too many receive retries for queue "+queue);
closeSession();
logger.info("sleeping for "+receiveErrorRetryDelay+" milliseconds for retrying listening to "+queue);
sleepSomeTime(receiveErrorRetryDelay);
}
return null;
}
private void enterBrowseMode() {
logger.info("Entering browse mode on queue {}",queue);
browseMode=true;
}
/**
* Will browse the top of the queue for starting or stopping messages
* If multiple starting/stopping message are available all but the last will be removed
* If the last message is a start message it will be removed as well
*/
private boolean checkBrowseMode() {
if (! browseMode)
return false;
QueueBrowser browser=null;
ArrayList<String> removeList=new ArrayList<String>();
try {
openSession();
browser = session.createBrowser(destination);
logger.debug("browsing for a message");
Enumeration<?> e = browser.getEnumeration();
Message lastMessage=null;
while (e.hasMoreElements()) {
Message msg = (Message) e.nextElement();
if (logger.isDebugEnabled())
logger.debug("browsing message with id {}, content {}",msg.getJMSMessageID(),((TextMessage) msg).getText());
if (ControlMessage.isStopMessage(msg) || ControlMessage.isStartMessage(msg)) {
if (lastMessage!=null) {
logger.info("Removing control message from queue {} because there is a newer control messages",queue);
removeList.add(lastMessage.getJMSMessageID());
}
lastMessage=msg;
}
else
break;
}
if (lastMessage==null) // there were no control message on queue, no reason to stay in browsemode
browseMode=false;
else if (ControlMessage.isStopMessage(lastMessage)) {
sleepSomeTime(this.interval);
}
else if (ControlMessage.isStartMessage(lastMessage)) { // startMessage may be removed
logger.info("Removing start message from queue {} because it is the last control message",queue);
removeList.add(lastMessage.getJMSMessageID());
browseMode=false;
}
closeSession();
if (!removeMessages(removeList)) {
logger.warn("Could not remove some messages, so staying in browse mode",queue);
browseMode=true;
}
if (! browseMode)
logger.info("Exiting browse mode on queue {}",queue);
return browseMode;
}
catch (JMSException e) { throw JmsUtil.wrapJMSException(e); }
finally {
try {
if (browser!=null)
browser.close();
}
catch (JMSException e) { throw JmsUtil.wrapJMSException(e); }
}
}
private boolean removeMessages(ArrayList<String> removeList) {
try {
closeSession();
openSession();
//consumer.close();
for (String msgid:removeList) {
String selector = "JMSMessageID='" +msgid+ "'";
logger.debug("Removing message [{}] from queue {} ",selector, queue);
MessageConsumer cons2=null;
try {
cons2 = session.createConsumer(destination, selector);
Message msg = cons2.receive(2000);
if (msg==null) {
logger.warn("Could not find for removal a message with id {} ",msgid);
return false;
}
if (logger.isInfoEnabled()) {
String body = ((TextMessage)msg).getText();
logger.debug("Removing message [{}] from queue {} with content: "+body,selector, queue);
}
session.commit();
}
finally {
if (cons2!=null)
cons2.close();
}
}
closeSession();
}
catch (JMSException e) { throw JmsUtil.wrapJMSException(e); }
return true;
}
private void sleepSomeTime(long delay) {
try {
Thread.sleep(delay);
}
catch (InterruptedException e1) { throw new RuntimeException(e1); }
}
private void closeSession() {
if (session==null)
return;
try {
if (consumer!=null)
consumer.close();
}
catch (Exception e) {
logger.warn("Ignoring error when trying to close already suspicious consumer",e);
}
consumer=null;
try {
session.close();
}
catch (Exception e) {
logger.warn("Ignoring error when trying to close already suspicious session",e);
}
session=null;
}
private void openSession() throws JMSException {
if (session!=null)
return;
session = system.getConnection().createSession(true, Session.SESSION_TRANSACTED);
destination = session.createQueue(queue);
consumer=null;
}
private void openConsumer() throws JMSException {
if (consumer!=null)
return;
openSession();
consumer = session.createConsumer(destination);
}
private void handleMessage(Message message) {
boolean messageHandled = false;
try {
logger.debug("Handling {}",message.getJMSMessageID());
handler.handle(new JmsMessage(message));
messageHandled = true;
}
catch (Exception e) {
try {
String code="TECHERR";
if (e instanceof FunctionalException)
code="FUNCERR";
String text=null;
if (message instanceof TextMessage)
text=code+": "+e.getMessage()+". When handling JMS message "+((TextMessage) message).getText();
else
text=code+": "+e.getMessage()+". When handling JMS message of type "+ message.getClass();
if (e instanceof HasDetails)
text+=((HasDetails)e).getDetails();
logger.error(text,e);
if (e instanceof JMSException) {
Exception linked = ((JMSException) e).getLinkedException();
if (e!=null)
logger.error("LinkedException: ", linked);
}
putInErrorQueue(message, e);
messageHandled=true;
}
catch (JMSException e2) { throw JmsUtil.wrapJMSException(e2); }
}
finally {
// The check for messageHandled is necessary because a Throwable error will not put the message on the error queue
// so the message should not be committed in this case.
if (message!=null && messageHandled) {
try {
logger.debug("committing session with message {}", message.getJMSMessageID());
session.commit();
}
catch (JMSException e) { throw JmsUtil.wrapJMSException(e); }
}
}
}
private void putInErrorQueue(Message message, Exception e) throws JMSException {
String equeue=errorqueue;
//if (e instanceof RetryableException)
// queue=retryqueue;
Destination errordestination = session.createQueue(equeue+system.sendParams);
MessageProducer producer = session.createProducer(errordestination);
Message errmsg=JmsUtil.cloneMessage(session, message);
if (useJmsPropsForErrorMessages && e instanceof MappedStateException) {
JmsUtil.prepareDestinationForJmsHeaders(errordestination);
Object prevTry = errmsg.getObjectProperty("state_TIME");
String prevdate="unknowndate";
if (prevTry instanceof String)
prevdate=(String) prevTry;
MappedStateException me = (MappedStateException) e;
me.addState("INPUT_QUEUE", queue);
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
String timestamp=formatter.format(new Date());
for (String key: me.getKeys()) {
String value=null;
try{
value=me.getState(key);
key="state_"+key;
if (value==null || value.length()==0)
continue;
Object oldValue=errmsg.getObjectProperty(key);
if (oldValue!=null) {
if (key.equals("state_ACTION") || key.equals("state_ERROR") || ! oldValue.equals(value) )
errmsg.setObjectProperty(key+"_"+prevdate, oldValue);// remember old value with a key that should be unique
}
errmsg.setStringProperty(key, value);
}
catch (RuntimeException e2) { logger.warn("could not set JMS property ["+key+"] to value ["+value+"]",e2); }
}
errmsg.setStringProperty("state_TIME", timestamp);
}
producer.send(errmsg);
producer.close();
String id = errmsg.getJMSMessageID();
if (id.startsWith("ID:"))
id=id.substring(3);
logger.info("message sent to error queue {} with id {}",equeue,id);
}
}