/** * Copyright (C) 2008 Maurice Zeijen <maurice@zeijen.net> * * 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.milyn.smooks.mule; import static org.milyn.smooks.mule.Constants.MESSAGE_PROPERTY_KEY_EXECUTION_CONTEXT; import static org.milyn.smooks.mule.Constants.MESSAGE_PROPERTY_KEY_PROFILE; import static org.mule.config.i18n.MessageFactory.createStaticMessage; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang.StringUtils; import org.milyn.Smooks; import org.milyn.container.ExecutionContext; import org.milyn.container.plugin.PayloadProcessor; import org.milyn.container.plugin.ResultType; import org.milyn.event.report.HtmlReportGenerator; import org.milyn.smooks.mule.core.AttachmentException; import org.milyn.smooks.mule.core.ExecutionContextUtil; import org.milyn.smooks.mule.core.MuleDispatcher; import org.milyn.smooks.mule.core.NamedEndpointMuleDispatcher; import org.mule.DefaultMuleMessage; import org.mule.api.MuleException; import org.mule.api.MuleMessage; import org.mule.api.MuleSession; import org.mule.api.endpoint.ImmutableEndpoint; import org.mule.api.endpoint.OutboundEndpoint; import org.mule.api.lifecycle.InitialisationException; import org.mule.api.routing.CouldNotRouteOutboundMessageException; import org.mule.api.routing.RoutingException; import org.mule.api.transport.PropertyScope; import org.mule.config.i18n.Message; import org.mule.routing.outbound.AbstractOutboundRouter; import org.mule.routing.outbound.FilteringOutboundRouter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; /** * The Router intended to be used with the Mule ESB. * <p/> * <h3>Usage:</h3> * Declare the router in the Mule configuration file: * <pre> * <outbound> * <smooks:router configFile="/smooks-config.xml"> * <jms:outbound-endpoint name="order" queue="order.queue"/> * <jms:outbound-endpoint name="order-important" queue="order.important.queue"/> * </smooks:router> * </outbound></pre> * * <h3>Description of configuration attributes/properties</h3> * <ul> * <li><i>configFile</i> - The Smooks configuration file. Can be a path on the file system or on the classpath. * <li><i>profile</i> - The Smooks profile to execute. If a profile name was found on the message then that one is used. * <li><i>profileMessagePropertyKey</i> - The name of the message property which could contain a possible profile name. * If the property is set and the value is a string then that value is used as profile name. * Default "MessageProfile". * <li><i>executionContextAsMessageProperty</i> - If set to "true" then the attributes map of the Smooks execution context is added to the message * properties of every message that gets created by this router. The property key is defined with * the executionContextMessagePropertyKey property. Default is "false" * <li><i>executionContextMessagePropertyKey</i> - The property key under which the execution context is put. Default is "SmooksExecutionContext" * <li><i>excludeNonSerializables</i> - if true, non serializable attributes from the Smooks ExecutionContext will no be included. Default is true. * <li><i>reportPath</i> - specifies the path and file name for generating a Smooks Execution Report. This is a development tool. * </ul> * * <h3>Defining Endpoints</h3> * As normal with Mule router you must at least define one endpoint to route the messages to. For this router it is * also required that you define the name of the router. That name is used as reference within the Smooks configuration * so that it knows which endpoint to use. * * <h3>Using the Smooks MuleDispatcher</h3> * Within the Smooks Router the {@link org.milyn.smooks.mule.core.MuleDispatcher} resource is used to actually route the message parts * to one of the defined endpoints (using the endpoint name). The following example demonstrates how to configure a MuleDispatcher: * * <pre> * <resource-config selector="order"> * <resource>org.milyn.smooks.mule.MuleDispatcher</resource> * <param name="endpointName">orderEndpoint</param> * <param name="beanId">orderBean</param> * </resource-config> * </pre> * * In this example Smooks calls the {@link MuleDispatcher} when it is in the visit after phase of the "order" element. The {@link MuleDispatcher} * creates a new Mule message and it will use the object found under the beanId "orderBean" in the Smooks BeanMap as it's payload. It * then dispatches that message to the endpoint with the name "orderEndpoint". * <p/> * More information on the configuration options of the {@link MuleDispatcher} can be found in the javadoc of the {@link MuleDispatcher} itself. * * <h3>Accessing the inbound MuleMessage within Smooks</h3> * The inbound MuleMessage is added to the Smooks bean context under the key <b>MULE_MESSAGE</b>. This means that you can access the Message from within * the Smooks filter process. * * <h3>Accessing Smooks ExecutionContext attributes</h3> * When the {@link MuleDispatcher} dispatches a message and if the "executionContextAsMessageProperty" property * is set to <code>true</code> then the MuleDispatcher will make the attributes that have been set in the * ExecutionContext at that moment available for other actions in the Mule ESB by setting the attributes map as a property of the message. * The attributes can be accessed by using the key defined under the property "executionContextMessagePropertyKey". Default * "SmooksExecutionContext" is used, which is set under the constant {@link Constants#MESSAGE_PROPERTY_KEY_EXECUTION_CONTEXT}. * An example of accessing the attributes map is: * <pre> * message.getProperty( Constants.MESSAGE_PROPERTY_KEY_EXECUTION_CONTEXT ); * </pre> * * @author <a href="mailto:maurice@zeijen.net">Maurice Zeijen</a> * */ public class Router extends FilteringOutboundRouter { private static final Logger log = LoggerFactory.getLogger(Router.class); private static final long serialVersionUID = 1L; /* * Smooks payload processor */ private PayloadProcessor payloadProcessor; /* * Smooks instance */ private Smooks smooks; /* * Filename for smooks configuration. Default is smooks-config.xml */ private String configFile; /* * The smooks profile to be used */ private String profile; /* * The key name where the message profile can be located */ private String profileMessagePropertyKey = MESSAGE_PROPERTY_KEY_PROFILE; /* * If true, then the execution context is set as property on the message */ private boolean executionContextAsMessageProperty = false; /* * The key name of the execution context message property */ private String executionContextMessagePropertyKey = MESSAGE_PROPERTY_KEY_EXECUTION_CONTEXT; /* * If true, non serializable attributes from the Smooks ExecutionContext will no be included. Default is true. */ private boolean excludeNonSerializables = true; /* * Path where the Smooks Report will be generated. */ private String reportPath; private final Map<String, OutboundEndpoint> endpointMap = new HashMap<String, OutboundEndpoint>(); @Override public void initialise() throws InitialisationException { // Create the Smooks instance smooks = createSmooksInstance(); // Create the Smooks payload processor payloadProcessor = new PayloadProcessor( smooks, ResultType.NORESULT ); initialiseEndpointMap(); } private void initialiseEndpointMap() { if (logger.isDebugEnabled()) { logger.debug("Initializing endpoint map"); } for(Object e : getEndpoints()) { OutboundEndpoint endpoint = (OutboundEndpoint) e; String name = endpoint.getName(); if(StringUtils.isEmpty(name)) { throw new IllegalArgumentException("The outbound endpoint list may only contain endpoints which have a name"); } if (logger.isDebugEnabled()) { logger.debug("Registered endpoint '" + name + "' in endpoint map."); } endpointMap.put(name, endpoint); } } public String getConfigFile() { return configFile; } /** * @return the profile */ public String getProfile() { return profile; } /** * @param profile the profile to set */ public void setProfile(String profile) { this.profile = profile; } /** * @return the profileMessagePropertyKey */ public String getProfileMessagePropertyKey() { return profileMessagePropertyKey; } /** * @param profileMessagePropertyKey the profileMessagePropertyKey to set */ public void setProfileMessagePropertyKey(String profileMessagePropertyKey) { this.profileMessagePropertyKey = profileMessagePropertyKey; } /** * @return the executionContextAsMessageProperty */ public boolean isExecutionContextAsMessageProperty() { return executionContextAsMessageProperty; } /** * @return the executionContextMessagePropertyKey */ public String getExecutionContextMessagePropertyKey() { return executionContextMessagePropertyKey; } /** * @return the excludeNonSerializables */ public boolean isExcludeNonSerializables() { return excludeNonSerializables; } /** * @return the reportPath */ public String getReportPath() { return reportPath; } public void setConfigFile( final String configFile ) { this.configFile = configFile; } /** * @param executionContextMessageProperty the setExecutionContextMessageProperty to set */ public void setExecutionContextAsMessageProperty( boolean executionContextMessageProperty) { this.executionContextAsMessageProperty = executionContextMessageProperty; } /** * @param executionContextMessagePropertyKey the executionContextMessagePropertyKey to set */ public void setExecutionContextMessagePropertyKey( String executionContextMessagePropertyKey) { if ( executionContextMessagePropertyKey == null ) { throw new IllegalArgumentException( "'executionContextMessagePropertyKey' can not be set to null." ); } if ( executionContextMessagePropertyKey.length() == 0 ) { throw new IllegalArgumentException( "'executionContextMessagePropertyKey' can not be set to an empty string." ); } this.executionContextMessagePropertyKey = executionContextMessagePropertyKey; } /** * @param excludeNonSerializables - If true, non serializable attributes from the Smooks ExecutionContext will no be included. Default is true. */ public void setExcludeNonSerializables( boolean excludeNonSerializables ) { this.excludeNonSerializables = excludeNonSerializables; } public void setReportPath( final String reportPath ) { this.reportPath = reportPath; } @Override public MuleMessage route(MuleMessage message, MuleSession session) throws RoutingException { if (logger.isDebugEnabled()) { logger.debug("Routing message '" + message.toString() + "'"); } // Retrieve the payload from the message Object payload = message.getPayload(); // Create Smooks ExecutionContext. ExecutionContext executionContext; String profile = retrieveProfile(message); if(profile != null) { if (logger.isDebugEnabled()) { logger.debug("Creating execution context with profile '" + profile + "'"); } executionContext = smooks.createExecutionContext(profile); } else { if (logger.isDebugEnabled()) { logger.debug("Creating execution context"); } executionContext = smooks.createExecutionContext(); } // Create the dispatcher which handles the dispatching of messages Dispatcher dispatcher = new Dispatcher(session, message, executionContext); // make the dispatcher available for Smooks executionContext.setAttribute(NamedEndpointMuleDispatcher.SMOOKS_CONTEXT, dispatcher); // Make the inbound Mule message accessable via the Smooks bean context executionContext.getBeanContext().addBean(org.milyn.smooks.mule.core.Constants.SMOOKS_BEAN_MULE_MESSAGE, message); // Add smooks reporting if configured addReportingSupport(message, executionContext ); if (logger.isDebugEnabled()) { logger.debug("Processing message '" + message.toString() + "'"); } // Use the Smooks PayloadProcessor to execute the routing.... payloadProcessor.process( payload, executionContext ); return null; } private String retrieveProfile(MuleMessage message) { Object messageProfile = message.getProperty(profileMessagePropertyKey); if(messageProfile != null && messageProfile instanceof String) { return (String) messageProfile; } return profile; } private Smooks createSmooksInstance() throws InitialisationException { if ( configFile == null ) { final Message errorMsg = createStaticMessage( "'smooksConfigFile' parameter must be specified" ); throw new InitialisationException( errorMsg, this ); } try { return new Smooks ( configFile ); } catch ( final IOException e) { final Message errorMsg = createStaticMessage( "IOException while trying to get smooks instance: " ); throw new InitialisationException( errorMsg, e, this); } catch ( final SAXException e) { final Message errorMsg = createStaticMessage( "SAXException while trying to get smooks instance: " ); throw new InitialisationException( errorMsg, e, this); } } private void addReportingSupport(final MuleMessage message, final ExecutionContext executionContext ) throws RoutingException { if( reportPath != null ) { try { log.info( "Using Smooks Reporting. Will generate report in file [" + reportPath + "]. Do not use in production evironment as this will have negative impact on performance!"); executionContext.setEventListener( new HtmlReportGenerator( reportPath ) ); } catch ( final IOException e) { final Message errorMsg = createStaticMessage( "Failed to create HtmlReportGenerator instance." ); throw new RoutingException(errorMsg, message, (ImmutableEndpoint)null, e); } } } private class Dispatcher implements NamedEndpointMuleDispatcher { private final ExecutionContext executionContext; private final MuleSession muleSession; private final MuleMessage inboundMessage; public Dispatcher(MuleSession muleSession, MuleMessage inboundMessage, ExecutionContext executionContext) { this.muleSession = muleSession; this.inboundMessage = inboundMessage; this.executionContext = executionContext; } public Object dispatch(String endpointName, Object payload, Map<String, Object> newMessageProperties, boolean forceSynchronous, boolean copyOriginalMessageProperties, boolean overrideOriginalMessageProperties, boolean ignorePropertiesWithNullValues, boolean copyOriginalMessageAttachments) { OutboundEndpoint outboundEndpoint = endpointMap.get(endpointName); if(outboundEndpoint == null) { throw new IllegalArgumentException("The outbound endpoint with the name '" + endpointName + "' isn't declared in the outbound endpoint map"); } Map<String, Object> allMessageProperties = new HashMap<String, Object>(); if(newMessageProperties != null) { allMessageProperties.putAll(newMessageProperties); } if(copyOriginalMessageProperties) { copyOriginalMessageProperties(allMessageProperties, overrideOriginalMessageProperties, ignorePropertiesWithNullValues); } if(ignorePropertiesWithNullValues) { filterNullValues(allMessageProperties); } MuleMessage outboundMessage = new DefaultMuleMessage(payload, allMessageProperties); outboundMessage.setCorrelationId(inboundMessage.getUniqueId()); if(copyOriginalMessageAttachments) { copyOriginalMessageAttachments(outboundMessage); } if(executionContextAsMessageProperty) { // Set the Smooks Excecution properties on the Mule Message object outboundMessage.setProperty(executionContextMessagePropertyKey, ExecutionContextUtil.getAtrributesMap(executionContext, excludeNonSerializables) ); } MuleMessage resultMessage = dispatch(outboundEndpoint, outboundMessage, forceSynchronous); Object result = null; if(resultMessage != null) { result = resultMessage.getPayload(); } return result; } public MuleMessage dispatch(OutboundEndpoint endpoint, MuleMessage message, boolean forceSynchronous) { try { boolean synchr = endpoint.isSynchronous() || forceSynchronous; if(synchr) { return Router.this.send(muleSession, message, endpoint); } else { Router.this.dispatch(muleSession, message, endpoint); } } catch (MuleException e) { throw new RuntimeException(new CouldNotRouteOutboundMessageException(message, endpoint, e)); } return null; } private void filterNullValues(Map<String, Object> allMessageProperties) { Iterator<Entry<String, Object>> entryIterator = allMessageProperties.entrySet().iterator(); while(entryIterator.hasNext()) { Entry<String, Object> entry = entryIterator.next(); if(entry.getValue() == null) { entryIterator.remove(); } } } private void copyOriginalMessageProperties( Map<String, Object> allMessageProperties, boolean overrideOriginalMessageProperties, boolean ignorePropertiesWithNullValues) { for(Object propertyNameObj : inboundMessage.getPropertyNames(PropertyScope.INBOUND)) { String propertyName = (String) propertyNameObj; if(!overrideOriginalMessageProperties || !allMessageProperties.containsKey(propertyName)) { Object value = inboundMessage.getProperty(propertyName, PropertyScope.INBOUND); if(!ignorePropertiesWithNullValues || value != null ) { allMessageProperties.put(propertyName, value); } } } } private void copyOriginalMessageAttachments(MuleMessage outboundMessage){ for(Object attachmentNameObj : inboundMessage.getAttachmentNames()) { String attachmentName = (String) attachmentNameObj; try { outboundMessage.addAttachment(attachmentName, inboundMessage.getAttachment(attachmentName)); } catch (Exception e) { throw new AttachmentException("Exception while trying to add the attachment '" + attachmentName + "' to the outbound message.", e); } } } } }