/* * Copyright 2013 The Sculptor Project Team, including the original * author or authors. * * 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.sculptor.framework.consumer; import javax.annotation.Resource; import javax.ejb.MessageDrivenContext; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.TextMessage; import org.sculptor.framework.context.ServiceContext; import org.sculptor.framework.context.ServiceContextFactory; import org.sculptor.framework.context.ServiceContextStore; import org.sculptor.framework.errorhandling.ApplicationException; import org.sculptor.framework.errorhandling.InvalidMessageException; import org.sculptor.framework.errorhandling.LogMessage; import org.sculptor.framework.errorhandling.MessageException; import org.sculptor.framework.errorhandling.SystemException; import org.sculptor.framework.errorhandling.UnexpectedRuntimeException; import org.sculptor.framework.errorhandling.ValidationException; import org.sculptor.framework.xml.XmlMappingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a base class for Pure EJB3 implementation of a Consumer. Subclass * must override the {@link #consume(String)} method. * <p> * Some error handling is done by this class. Exceptions to indicate an invalid * message can be thrown by subclass. See: * http://www.enterpriseintegrationpatterns.com/InvalidMessageChannel.html Note * that invalid message channel is not the same as dead letter channel. Invalid * messages are for example those that can't be parsed or contain some data that * doesn't fulfill the message contract (schema etc). Invalid messages are sent * to an invalid message queue. The transaction is committed, to avoid returning * it back to the receive queue again. This means that validation to detect * invalid message must be done before and modifications of persistent objects. * Subclass may define invalid message exceptions by overriding * {@link #isInvalidMessageException(Exception)}. */ public abstract class AbstractMessageBean { private final Logger log = LoggerFactory.getLogger(getClass()); @Resource private MessageDrivenContext mdbContext; public AbstractMessageBean() { } protected Logger getLog() { return log; } public void onMessage(Message msg) { if (!checkSupportedMessageTypes(msg)) { return; } try { ServiceContext serviceContext = createServiceContext(); serviceContext.setProperty("jms", Boolean.TRUE); boolean redelivered = isJmsRedelivered(msg); serviceContext.setProperty("jmsRedelivered", redelivered); serviceContext.setProperty("jmsMessageID", getJMSMessageID(msg)); ServiceContextStore.set(serviceContext); String reply = consume(serviceContext, getMessageText(msg)); sendReply(msg, reply); } catch (RuntimeException e) { handleRuntimeException(msg, e); } catch (ApplicationException e) { if (isInvalidMessageException(e)) { handleInvalidMessageException(msg, e); } else { handleApplicationException(e); } } } protected void handleRuntimeException(Message msg, RuntimeException e) { SystemException excToUse = SystemException.unwrapSystemException(e); if (excToUse == null) { excToUse = new UnexpectedRuntimeException(e.getMessage()); } if (isInvalidMessageException(excToUse)) { handleInvalidMessageException(msg, excToUse); } else { // re-throw which will cause rollback throw e; } } protected void handleInvalidMessageException(Message msg, SystemException e) { if (!e.isLogged()) { log.error((new LogMessage(e)).toString(), e); e.setLogged(true); } sendToInvalidMessageQueue(msg); String invalidMessageReply = formatInvalidMessageReply(e, msg); sendReply(msg, invalidMessageReply); } protected void handleInvalidMessageException(Message msg, ApplicationException e) { if (!e.isLogged()) { log.error((new LogMessage(e)).toString(), e); e.setLogged(true); } sendToInvalidMessageQueue(msg); String invalidMessageReply = formatInvalidMessageReply(e, msg); sendReply(msg, invalidMessageReply); } protected void handleApplicationException(ApplicationException e) { if (log.isDebugEnabled() && !e.isLogged()) { LogMessage logMessage = new LogMessage(e); log.debug(logMessage.toString(), e); e.setLogged(true); } mdbContext.setRollbackOnly(); } protected boolean isJmsRedelivered(Message msg) { try { return msg.getJMSRedelivered(); } catch (JMSException e) { return false; } } protected String getJMSMessageID(Message msg) { try { return msg.getJMSMessageID(); } catch (JMSException e) { return null; } } /** * Subclass may override to define invalid message exceptions. Default is * {@link org.sculptor.framework.errorhandling.InvalidMessageException} * , * {@link org.sculptor.framework.errorhandling.XmlMappingException} * and * {@link org.sculptor.framework.errorhandling.ValidationException} */ protected boolean isInvalidMessageException(Exception e) { return (e instanceof InvalidMessageException || e instanceof XmlMappingException || e instanceof ValidationException); } protected String formatInvalidMessageReply(ApplicationException e, Message msg) { return formatInvalidMessageReply(e.getErrorCode(), e.getMessage(), msg); } protected String formatInvalidMessageReply(SystemException e, Message msg) { return formatInvalidMessageReply(e.getErrorCode(), e.getMessage(), msg); } protected String formatInvalidMessageReply(String errorCode, String excMessage, Message msg) { StringBuilder result = new StringBuilder(); result.append(errorCode); result.append("\n"); result.append(excMessage); result.append(getMessageText(msg)); return result.toString(); } public String consume(ServiceContext ctx, String textMessage) throws ApplicationException { return consume(textMessage); } /** * @param textMessage * the incoming text message * @return the reply, return null if there is no reply */ public abstract String consume(String textMessage) throws ApplicationException; /** * Only TextMessages are supported by default. Subclass may override, but * then {@link #getMessageText(Message)} must also be implemented to support * other message types. */ protected boolean checkSupportedMessageTypes(Message msg) { if (msg instanceof TextMessage) { return true; } else { try { log.error("Unsupported message type: " + msg.getJMSType()); } catch (JMSException ignore) { } sendToInvalidMessageQueue(msg); return false; } } protected String getMessageText(Message msg) throws MessageException { try { if (msg instanceof TextMessage) { return ((TextMessage) msg).getText(); } else { throw new IllegalArgumentException("Unsupported message type: " + msg.getJMSType()); } } catch (JMSException e) { throw new MessageException("Failure when getting text from JMS message: " + e.getMessage(), e); } } protected ServiceContext createServiceContext() { ServiceContext defaultCtx = ServiceContextFactory.createServiceContext(getMessageConsumerBeanId()); return new ServiceContext(getMessageConsumerBeanId(), defaultCtx.getSessionId(), defaultCtx.getApplicationId(), defaultCtx.getRoles()); } protected String getMessageConsumerBeanId() { return getClass().getSimpleName(); } protected MessageSender getMessageSender() { if (getJmsConnection() == null) { throw new IllegalStateException("Need JMS connection to be able to send messages."); } return new MessageSenderImpl(getJmsConnection()); } protected void sendToInvalidMessageQueue(Message msg) { if (getJmsConnection() == null || getInvalidMessageQueue() == null) { getLog().info( "No JmsConnection or queue, message that was not sent to InvalidMessageQueue:\n" + safeGetMessageText(msg)); return; } String messageText = ""; try { messageText = getMessageText(msg); getMessageSender().sendMessage(getInvalidMessageQueue(), messageText, getJMSMessageID(msg)); } catch (Exception e) { getLog().error("Can't send to InvalidMessageQueue: " + e.getMessage(), e); getLog().info("Message that was not sent to InvalidMessageQueue:\n" + messageText); } finally { closeConnection(); } } private String safeGetMessageText(Message msg) { try { return getMessageText(msg); } catch (Exception e) { return ""; } } protected void sendReply(Message msg, String reply) throws org.sculptor.framework.errorhandling.MessageException { try { if (reply == null) { reply = defaultOkReply(); } Destination replyTo = msg.getJMSReplyTo(); if (replyTo != null && getJmsConnection() != null) { getMessageSender().sendMessage(replyTo, reply, getJMSMessageID(msg)); } } catch (JMSException e) { throw new org.sculptor.framework.errorhandling.MessageException( "Failure when sending repy: " + reply + "\n" + e.getMessage(), e); } finally { closeConnection(); } } protected String defaultOkReply() { return "OK"; } /** * Subclass need to implement this to be able to send messages, such as * reply and send to invalid message queue. */ protected Connection getJmsConnection() { return null; } /** * Subclass need to implement this to be able to send messages, such as * reply and send to invalid message queue. */ protected void closeConnection() { } /** * Subclass need to implement this to be able to send to invalid message * queue. You could override {@link #sendToInvalidMessageQueue} to implement * some other error handling for invalid messages. */ protected Destination getInvalidMessageQueue() { return null; } }