/**
* 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.core;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.milyn.SmooksException;
import org.milyn.cdr.Parameter;
import org.milyn.cdr.SmooksConfigurationException;
import org.milyn.cdr.SmooksResourceConfiguration;
import org.milyn.cdr.annotation.AnnotationConstants;
import org.milyn.cdr.annotation.AppContext;
import org.milyn.cdr.annotation.Config;
import org.milyn.cdr.annotation.ConfigParam;
import org.milyn.cdr.annotation.ConfigParam.Use;
import org.milyn.container.ApplicationContext;
import org.milyn.container.ExecutionContext;
import org.milyn.delivery.annotation.Initialize;
import org.milyn.delivery.annotation.VisitAfterIf;
import org.milyn.delivery.annotation.VisitBeforeIf;
import org.milyn.delivery.dom.DOMElementVisitor;
import org.milyn.delivery.ordering.Consumer;
import org.milyn.delivery.ordering.Producer;
import org.milyn.delivery.sax.SAXElement;
import org.milyn.delivery.sax.SAXVisitAfter;
import org.milyn.delivery.sax.SAXVisitBefore;
import org.milyn.event.report.annotation.VisitAfterReport;
import org.milyn.event.report.annotation.VisitBeforeReport;
import org.milyn.expression.MVELExpressionEvaluator;
import org.milyn.javabean.DataDecodeException;
import org.milyn.javabean.DataDecoder;
import org.milyn.javabean.context.BeanContext;
import org.milyn.javabean.context.BeanIdStore;
import org.milyn.javabean.repository.BeanId;
import org.milyn.smooks.mule.core.message.MVELEvaluatingMessagePropertyValue;
import org.milyn.smooks.mule.core.message.MessageProperty;
import org.milyn.smooks.mule.core.message.MessagePropertyValue;
import org.milyn.smooks.mule.core.message.StaticMessagePropertyValue;
import org.milyn.util.CollectionsUtil;
import org.milyn.xml.DomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* The MuleDispatcher is a Smooks Resource visitor to dispatch message
* parts to endpoints in Mule.
* <p/>
* <h3>Usage:</h3>
* A MuleDispatcher is defined as follows:
* <pre>
* <resource-config selector="order">
* <resource>org.milyn.smooks.mule.MuleDispatcher</resource>
* <param name="endpointName">order</param>
* <param name="beanId">order</param>
* </resource-config>
* </pre>
*
* <b>Required parameters:</b>
* <property name="endpointName" value="someEndpoint" />
*
* <b>Optional parameters:</b>
* <param name="beanId">someBeanId</param>
* <param name="resultBeanId">someBeanIdToBindTheResultTo</param>
* <param name="messagePropertiesBeanId">someBeanIdToGetTheMessagePropertiesFrom</param>
* <param name="messageProperties" >
* <property name="somePropertyName" value="somePropertyValue" type="someType" />
* </param>
* </pre>
*
* <h3>Description of configuration parameters</h3>
* <ul>
* <li><i>endpointName</i> - The name of the endpoint which will be used when routing the message. If no endpoint can be found under that name then an exception is thrown.
* <li><i>expression</i> - A MVEL script. The result of the script is used as the message payload. This overrides the beanId property.
* <li><i>beanId</i> - The bean id of the bean which will be used as the message payload. The context of the MVEL script contains all the beans of the bean map.
* <li><i>resultBeanId</i> - If the endpoint returns a result then the payload of that result is bounded to the resultBeanId.
* When the resultBeanId isn't set then the result is discarded. If the resultBeanId is set then the
* endpoint is forced to distpatch synchronous.
* <li><i>messagePropertiesBeanId</i> -
* <li><i>messageProperties</i> - Properties that will be set on the message before dispatching.
* The MuleDispatcher messageProperties attribute section explains how to set the message properties
* </ul>
*
* <h3>MuleDispatcher messageProperties attribute</h3>
* Message properties are defined as folows:
* <pre>
* <resource-config selector="order">
* <resource>org.milyn.smooks.mule.MuleDispatcher</resource>
* <param name="endpointName">testEndpoint</param>
* <param name="messageBeanId">test</param>
* <param name="messageProperties">
* <property name="prop1" value="prop1Value" />
* <property name="prop2">"prop2Value"</property>
* <property name="intProp" value="10" type="Integer" />
* <property name="dateProp" value="2008-07-11 12:30:56" type="DateTime" />
* </param>
* </resource-config>
*
* <resource-config selector="decoder:DateTime">
* <resource>org.milyn.javabean.decoders.DateDecoder</resource>
* <param name="format">yyyy-MM-dd HH:mm:ss</param>
* </resource-config>
* </pre>
*
* The following points explain the four properties:
* <ol>
* <li>The first property uses a attribute to set the value.
* <li>The second property takes the property element content and parses it as a MVEL expression.
* <li>The third property uses the type attribute to define a Integer type. This will convert the value into an Integer. This uses the standard DataDecoder feature from Smooks.
* <li>The fourth property uses the type attribute to define a custom configured Date type. This will convert the value into a Date. The configuration is set in the resource-config with the "decoder:DateTime" selector. This uses the standard DataDecoder feature from Smooks.
* </ol>
*
* <h3>Description of property attributes</h3>
* <ul>
* <li><i>name</i> - The message property name (required)
* <li><i>value</i> - The value of the property. The property element content can also be set. If both are set then the value attribute is used.
* <li><i>type</i> - The type of the property value. This uses the standard DataDecoder feature from Smooks. DataDecoders can be configured using
* a "decoder:decoderName" resource-config selector. The decoderName must be set as type then. (Default: String)
* </ul>
*
* <h3>Description of the property element content</h3>
* The property element content is parsed as a MVEL expression. This gives
* great flexibility. The MVEL context contains all the beans from the bean map.
* The type and value attribute are ignored when the element content is set.
*
* @author <a href="mailto:maurice@zeijen.net">Maurice Zeijen</a>
*/
@VisitBeforeIf(condition = "parameters.containsKey('executeBefore') && parameters.executeBefore.value == 'true'")
@VisitAfterIf(condition = "!parameters.containsKey('executeBefore') || parameters.executeBefore.value != 'true'")
@VisitBeforeReport(summary = "Dispatch to endpoint '${resource.parameters.endpointName}'.", detailTemplate = "reporting/MuleDispatcher.html")
@VisitAfterReport(summary = "Dispatch to endpoint '${resource.parameters.endpointName}'.", detailTemplate = "reporting/MuleDispatcher.html")
public class MuleDispatcher implements DOMElementVisitor, SAXVisitBefore, SAXVisitAfter, Consumer, Producer {
private static final String MESSAGE_PROPERTIES_PARAMETER_TYPE = "type";
private static final String MESSAGE_PROPERTIES_PARAMETER_VALUE = "value";
private static final String MESSAGE_PROPERTIES_PARAMETER_DECODER = "decoder";
private static final String MESSAGE_PROPERTIES_ATTRIBUTE_NAME = "name";
private static final String MESSAGE_PROPERTIES_NODE_NAME = "property";
private static final String MESSAGE_EXPRESSION_NODE_NAME = "expression";
public static final String PARAMETER_MESSAGE_PROPERTIES = "messageProperties";
private static final Logger log = LoggerFactory.getLogger(MuleDispatcher.class);
@ConfigParam(use=Use.REQUIRED)
private String endpointName;
@ConfigParam(name="beanId", use=Use.OPTIONAL)
private String beanIdName;
private BeanId beanId;
@ConfigParam(name="resultBeanId", use=Use.OPTIONAL)
private String resultBeanIdName;
private BeanId resultBeanId;
@ConfigParam(name="messagePropertiesBeanId", use=Use.OPTIONAL)
private String messagePropertiesBeanIdName;
private BeanId messagePropertiesBeanId;
@ConfigParam(defaultVal = AnnotationConstants.NULL_STRING)
private MVELExpressionEvaluator expression;
private List<MessageProperty> staticMessageProperties;
@ConfigParam(defaultVal = "false")
private boolean copyOriginalMessageProperties;
@ConfigParam(defaultVal = "true")
private boolean overrideOriginalMessageProperties;
@ConfigParam(defaultVal = "true")
private boolean ignorePropertiesWithNullValues;
@ConfigParam(defaultVal = "false")
private boolean copyOriginalMessageAttachments;
@Config
private SmooksResourceConfiguration config;
@AppContext
private ApplicationContext appContext;
@Initialize
public void initialize() {
BeanIdStore beanIdStore = appContext.getBeanIdStore();
if(beanIdName != null) {
beanId = beanIdStore.register(beanIdName);
}
if(resultBeanIdName != null) {
resultBeanId = beanIdStore.register(resultBeanIdName);
}
if(messagePropertiesBeanIdName != null) {
messagePropertiesBeanId = beanIdStore.register(messagePropertiesBeanIdName);
}
}
/**
* {@inheritDoc}
*/
public void visitBefore(Element element, ExecutionContext executionContext) throws SmooksException {
dispatch(executionContext);
}
/**
* {@inheritDoc}
*/
public void visitBefore(SAXElement element, ExecutionContext executionContext) throws SmooksException, IOException {
dispatch(executionContext);
}
/**
* {@inheritDoc}
*/
public void visitAfter(Element element, ExecutionContext executionContext) throws SmooksException {
dispatch(executionContext);
}
/**
* {@inheritDoc}
*/
public void visitAfter(SAXElement element, ExecutionContext executionContext) throws SmooksException, IOException {
dispatch(executionContext);
}
/**
* Dispatches a message to an endpoint
*
* @param executionContext
*/
private void dispatch(ExecutionContext executionContext) {
Object payload = null;
BeanContext beanRepository = executionContext.getBeanContext();
if(expression != null) {
Map<?, ?> beanMap = beanRepository.getBeanMap();
payload = expression.getValue(beanMap);
} else if(beanIdName != null) {
payload = beanRepository.getBean(beanId);
}
if(log.isInfoEnabled()) {
String payloadMsg;
if(payload == null) {
payloadMsg = " with no payload";
} else {
payloadMsg = " with a " + payload.getClass().getName() + " payload";
if(expression != null) {
payloadMsg += " from an expression";
} else {
payloadMsg += " from beanId '" + beanIdName + "'";
}
}
log.info("Dispatching Mule message to endpoint '" + endpointName + "'" + payloadMsg );
}
NamedEndpointMuleDispatcher dispatcher = (NamedEndpointMuleDispatcher) executionContext.getAttribute(NamedEndpointMuleDispatcher.SMOOKS_CONTEXT);
if(dispatcher == null) {
throw new IllegalStateException("The executionContext doesn't have the MuleDispatcher object as the attribute with the key '" + NamedEndpointMuleDispatcher.SMOOKS_CONTEXT + "'");
}
boolean forceSynchronous = resultBeanId != null;
Map<String, Object> messageProperties = createMessagePropertiesMap(executionContext);
Object result = dispatcher.dispatch(endpointName, payload, messageProperties, forceSynchronous, copyOriginalMessageProperties, overrideOriginalMessageProperties, ignorePropertiesWithNullValues, copyOriginalMessageAttachments);
if(result != null && resultBeanId != null) {
if(log.isInfoEnabled()) {
log.info("Received result from endpoint '" + endpointName + "'. Adding it to the bean map under beanId '" + resultBeanId.getName()+ "'");
}
beanRepository.addBean(resultBeanId, result);
}
}
/**
* Creates the message properties map from the static message properties and the possible
* properties from the Map found under the messageProperties bean Id.
*
* @param executionContext
* @return
*/
@SuppressWarnings("unchecked")
private Map<String, Object> createMessagePropertiesMap(ExecutionContext executionContext) {
HashMap<String, Object> props = evaluateStaticMessageProperties(executionContext);
if(messagePropertiesBeanId != null) {
BeanContext beanContext = executionContext.getBeanContext();
Map<String, Object> bProperties = (Map<String, Object>) beanContext.getBean(messagePropertiesBeanId);
if(bProperties == null) {
throw new SmooksConfigurationException("No properties map could be found under the beanId '" + messagePropertiesBeanId.getName() + "'");
}
props.putAll(bProperties);
}
return props;
}
private HashMap<String, Object> evaluateStaticMessageProperties(ExecutionContext executionContext) {
HashMap<String, Object> props = new HashMap<String, Object>();
for(MessageProperty messageProperty : getStaticMessageProperties(executionContext) ) {
props.put(messageProperty.getName(), messageProperty.getValue(executionContext));
}
return props;
}
/**
* Processes the static message properties.
* Because these are static it is only done the first time. From that point on
* a cached result is used.
*
* @param executionContext
* @return
*/
private List<MessageProperty> getStaticMessageProperties(ExecutionContext executionContext) {
if(staticMessageProperties == null) {
List<MessageProperty> lMessageProperties = new ArrayList<MessageProperty>();
Parameter messagePropertiesParam = config.getParameter(PARAMETER_MESSAGE_PROPERTIES);
if (messagePropertiesParam != null) {
Element messagePropertiesParamElement = messagePropertiesParam.getXml();
if(messagePropertiesParamElement != null) {
boolean extendedConfig = messagePropertiesParamElement.getNamespaceURI().equals(Constants.MULE_SMOOKS_NAMESPACE);
if(extendedConfig) {
resolvePropertiesExtendedConfig(executionContext, lMessageProperties, messagePropertiesParamElement);
} else {
resolvePropertiesNormalConfig(executionContext, lMessageProperties, messagePropertiesParamElement);
}
} else {
log.error("Sorry, the Javabean populator bindings must be available as XML DOM. Please configure using XML.");
}
}
staticMessageProperties = lMessageProperties;
}
return staticMessageProperties;
}
/**
* @param executionContext
* @param lMessageProperties
* @param messagePropertiesParamElement
*/
private void resolvePropertiesNormalConfig(ExecutionContext executionContext, List<MessageProperty> lMessageProperties, Element messagePropertiesParamElement) {
NodeList properties = messagePropertiesParamElement.getElementsByTagName(MESSAGE_PROPERTIES_NODE_NAME);
for (int i = 0; properties != null && i < properties.getLength(); i++) {
Element node = (Element)properties.item(i);
String name = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_ATTRIBUTE_NAME);
if(StringUtils.isBlank(name)) {
throw new SmooksConfigurationException("The 'name' attribute isn't a defined or empty for the message property: " + node);
}
name = name.trim();
MessagePropertyValue messageValue = null;
String rawValue = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_PARAMETER_VALUE);
if(rawValue == null) {
String expression = DomUtils.getAllText(node, true);
if(StringUtils.isNotBlank(expression)) {
MVELExpressionEvaluator evaluator = new MVELExpressionEvaluator();
try {
evaluator.setExpression(expression);
} catch (RuntimeException e) {
throw new RuntimeException("Exception while setting the expression on the MVELExpressionEvaluator", e);
}
messageValue = new MVELEvaluatingMessagePropertyValue(evaluator);
}
} else {
rawValue = rawValue.trim();
Object value = null;
String type = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_PARAMETER_TYPE);
if(type != null) {
type = type.trim();
value = getDecoder(executionContext, type).decode(rawValue);
} else {
value = rawValue;
}
messageValue = new StaticMessagePropertyValue(value);
}
lMessageProperties.add(new MessageProperty(name, messageValue));
}
}
/**
* @param executionContext
* @param lMessageProperties
* @param messagePropertiesParamElement
*/
private void resolvePropertiesExtendedConfig(ExecutionContext executionContext, List<MessageProperty> lMessageProperties, Element messagePropertiesParamElement) {
NodeList properties = messagePropertiesParamElement.getElementsByTagNameNS(Constants.MULE_SMOOKS_NAMESPACE, MESSAGE_PROPERTIES_NODE_NAME);
for (int i = 0; properties != null && i < properties.getLength(); i++) {
Element node = (Element)properties.item(i);
String name = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_ATTRIBUTE_NAME);
if(StringUtils.isBlank(name)) {
throw new SmooksConfigurationException("The 'name' attribute isn't a defined or empty for the message property: " + node);
}
name = name.trim();
MessagePropertyValue messageValue = null;
String rawValue = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_PARAMETER_VALUE);
if(rawValue == null) {
Element expressionElement = (Element) DomUtils.getElement(node, MESSAGE_EXPRESSION_NODE_NAME, 1, Constants.MULE_SMOOKS_NAMESPACE);
String expression = DomUtils.getAllText(expressionElement, true);
if(StringUtils.isNotBlank(expression)) {
MVELExpressionEvaluator evaluator = new MVELExpressionEvaluator();
try {
evaluator.setExpression(expression);
} catch (RuntimeException e) {
throw new RuntimeException("Exception while setting the expression on the MVELExpressionEvaluator", e);
}
messageValue = new MVELEvaluatingMessagePropertyValue(evaluator);
}
} else {
rawValue = rawValue.trim();
Object value = null;
String decoder = DomUtils.getAttributeValue(node, MESSAGE_PROPERTIES_PARAMETER_DECODER);
if(decoder != null) {
decoder = decoder.trim();
value = getDecoder(executionContext, decoder).decode(rawValue);
} else {
value = rawValue;
}
messageValue = new StaticMessagePropertyValue(value);
}
lMessageProperties.add(new MessageProperty(name, messageValue));
}
}
/**
* Retrieves the decoder of a certain type.
*
* @param executionContext
* @param type
* @return
* @throws DataDecodeException
*/
private DataDecoder getDecoder(ExecutionContext executionContext, String type) throws DataDecodeException {
@SuppressWarnings("unchecked")
List<DataDecoder> decoders = executionContext.getDeliveryConfig().getObjects("decoder:" + type);
DataDecoder decoder;
if (decoders == null || decoders.isEmpty()) {
decoder = DataDecoder.Factory.create(type);
} else if (!(decoders.get(0) instanceof DataDecoder)) {
throw new DataDecodeException("Configured decoder '" + type + ":" + decoders.get(0).getClass().getName() + "' is not an instance of " + DataDecoder.class.getName());
} else {
decoder = decoders.get(0);
}
return decoder;
}
/* (non-Javadoc)
* @see org.milyn.delivery.ordering.Consumer#consumes(java.lang.Object)
*/
public boolean consumes(Object object) {
if(object.toString().equals(beanIdName)) {
return true;
}
if(object.toString().equals(messagePropertiesBeanIdName)) {
return true;
}
return false;
}
/* (non-Javadoc)
* @see org.milyn.delivery.ordering.Producer#getProducts()
*/
public Set<? extends Object> getProducts() {
if(resultBeanIdName == null) {
return Collections.emptySet();
}
return CollectionsUtil.toSet(resultBeanIdName);
}
}