/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and others contributors as indicated
* by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2005-2006, JBoss Inc.
*/
package org.jboss.soa.esb.actions.converters;
import org.apache.log4j.Logger;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.ActionLifecycleException;
import org.jboss.soa.esb.actions.ActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.ActionUtils;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.helpers.KeyValuePair;
import org.jboss.soa.esb.lifecycle.LifecycleResourceException;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.message.Body;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.message.body.content.BytesBody;
import org.jboss.soa.esb.services.transform.TransformationException;
import org.jboss.soa.esb.services.transform.TransformationService;
import org.jboss.soa.esb.smooks.resource.SmooksResource;
import org.milyn.Smooks;
import org.milyn.SmooksUtil;
import org.milyn.container.ExecutionContext;
import org.milyn.payload.StringResult;
import org.milyn.payload.StringSource;
import org.milyn.profile.DefaultProfileSet;
import org.milyn.profile.ProfileStore;
import org.milyn.profile.UnknownProfileMemberException;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
/**
* Smooks Transformer.
* <p/>
* This processor hooks the <a href="http://milyn.codehaus.org/Smooks">Milyn Smooks</a>
* Data Transformation/Processing Engine into a message processing pipeline.
*
* <p/>
* A wide range of source (XML, CSV, EDI etc) and target (XML, Java, CSV, EDI etc) formats
* are supported.
*
* <h3>Transformation Configuration</h3>
* This action works in one of 2 ways:
* <ol>
* <li>Out of a Smooks resource configuration whose URL is specified directly on the action via the
* "resource-config" property. If no URI scheme ("http", "file" etc) is specified on the
* resource config, the resource is assumed to reside on the local classpath.
* </p>
* Example :</p>
* <property name="<b>resource-config</b>" value="/smooks-res.xml" />
* </li>
* <li>Out of a centralised Smooks resource configuration datasource managed by the Transformation Admin Console.
* This datasource is configured in the smooks.esb deployment. See the "console.url" property in the
* "smooks.esb.properties" file.
* </li>
* </ol>
*
* If both of the above are specified, the action will use the locally specified config defined on the "resource-config"
* property. If neither are specified, an error will result.
*
* <p/>
* This transformer also supports Smooks profiles as follows:
* <pre>
* <action name="transformAB" class="<b>org.jboss.soa.esb.actions.converters.SmooksTransformer</b>">
* <property name="<b>from</b>" value="A" />
* <property name="<b>from-type</b>" value="text/xml:messageAtA" />
* <property name="<b>to</b>" value="B" />
* <property name="<b>to-type</b>" value="text/xml:messageAtB" />
* </action>
* </pre>
*
* <h3>Transformation Input/Output Configuration</h3>
* This action gets the transformation input, and sets the transformation output
* based on the "input-location" and "output-location" configuration properties.
* These properties name the {@link Body Message.Body} location where the transformation input
* and output are attached.
* <p/>
* If either these properties are not set, the action class defaults that value
* to being "{@link Body#DEFAULT_LOCATION defaultEntry}". In other words, if "input-location" is not configured
* on the action, the action will attempt to load the transformation input from the
* {@link Body Message.Body} location named "{@link Body#DEFAULT_LOCATION defaultEntry}". If the "output-location"
* is not configured on the action, the action will set the transformation result/output
* in the {@link Body Message.Body} location named "{@link Body#DEFAULT_LOCATION defaultEntry}".
*
* <h3>Java Transformation Input/Output Configuration</h3>
* This action supports source (XML, CSV, EDI etc) to Java object transformation/binding. See the
* "Transform_*" quickstarts for examples of this and also check out the
* <a href="http://milyn.codehaus.org/Tutorials">Smooks Tutorials</a>.
* <p/>
* The constructed Java object model can be used as part of a
* <a href="http://milyn.codehaus.org/Model+Driven+Transformation">model driven transform</a>, or can
* simply be used by other ESB action instances that follow the SmooksTransformer in an action
* pipeline.
* <p/>
* Such Java object graphs are available to subsequent pipeline action instances because they are
* attached to the ESB Message output by this action and input to the following action(s). They are bound
* to the Message instance Body
* ({@link Body#add(String, Object) Message.getBody().add(String key, Object object)}) under a key based
* directly on the objects "beanId"
* <a href="http://milyn.codehaus.org/javadoc/smooks-cartridges/javabean/org/milyn/javabean/BeanPopulator.html">as defined in the Smooks Javabean config</a>.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
* @since Version 4.0
* @deprecated Use {@link org.jboss.soa.esb.smooks.SmooksAction}.
*/
public class SmooksTransformer implements TransformationService, ActionPipelineProcessor {
/**
* Action config.
*/
private ConfigTree actionConfig;
/**
* Key for storing/accessing any potential message Body bean HashMaps as populated
* by the Smooks Javabean Cartridge.
* @deprecated The Smooks {@link org.milyn.container.ExecutionContext} is
* attached to the message and can be accessed through the {@link }
*/
public static final String EXTRACTED_BEANS_HASH = "EXTRACTED_BEANS_HASH";
/**
* Action config Smooks configuration key.
*/
public static final String RESOURCE_CONFIG = "resource-config";
/**
* Config key for the message body location on which the input message is attached.
*/
public static final String INPUT_LOCATION = "input-location";
/**
* Config key for the message body location on which the output message is attached.
*/
public static final String OUTPUT_LOCATION = "output-location";
/**
* Config key for the name of the Smooks Execution context object to be
* output to the "output-location".
*/
public static final String JAVA_OUTPUT = "java-output-location";
public static final String FROM = "from";
public static final String FROM_TYPE = "from-type";
public static final String TO = "to";
public static final String TO_TYPE = "to-type";
public static final String UPDATE_TOPIC="update-topic";
private static Logger logger = Logger.getLogger(SmooksTransformer.class);
private Smooks smooks;
private MessagePayloadProxy payloadProxy;
private String inputLocation;
private String outputLocation;
private String javaOutputLocation;
private String defaultMessageFromType;
private String defaultMessageFrom;
private String defaultMessageToType;
private String defaultMessageTo;
/**
* Public constructor.
* @param propertiesTree Action Properties.
* @throws ConfigurationException Action not properly configured.
*/
public SmooksTransformer(ConfigTree propertiesTree) throws ConfigurationException {
List<KeyValuePair> properties = propertiesTree.attributesAsList();
createPayloadProxy(properties, propertiesTree);
if(javaOutputLocation != null) {
javaOutputLocation = javaOutputLocation.trim();
}
// Get the default message flow properties (can be overriden by the message properties)...
defaultMessageFromType = KeyValuePair.getValue(FROM_TYPE, properties);
if(defaultMessageFromType != null && defaultMessageFromType.trim().equals("")) {
throw new ConfigurationException("Empty '" + FROM_TYPE + "' config attribute supplied.");
}
defaultMessageToType = KeyValuePair.getValue(TO_TYPE, properties);
if(defaultMessageToType != null && defaultMessageToType.trim().equals("")) {
throw new ConfigurationException("Empty '" + TO_TYPE + "' config attribute supplied.");
}
defaultMessageFrom = KeyValuePair.getValue(FROM, properties);
if(defaultMessageFrom != null && defaultMessageFrom.trim().equals("")) {
throw new ConfigurationException("Empty '" + FROM + "' config attribute supplied.");
}
defaultMessageTo = KeyValuePair.getValue(TO, properties);
if(defaultMessageTo != null && defaultMessageTo.trim().equals("")) {
throw new ConfigurationException("Empty '" + TO + "' config attribute supplied.");
}
actionConfig = propertiesTree;
}
private void createPayloadProxy(List<KeyValuePair> properties, ConfigTree propertiesTree) {
// if no input location given, then assume default location in message body.
inputLocation = KeyValuePair.getValue(INPUT_LOCATION, properties);
// if no output location given, then assume default location in message body.
outputLocation = KeyValuePair.getValue(OUTPUT_LOCATION, properties);
javaOutputLocation = KeyValuePair.getValue(JAVA_OUTPUT, properties);
String[] legacyGetLocations;
String[] legacySetLocations;
if(inputLocation != null) {
legacyGetLocations = new String[] {inputLocation, BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA};
} else {
legacyGetLocations = new String[] {BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA};
}
if(outputLocation != null) {
legacySetLocations = new String[] {outputLocation, ActionUtils.POST_ACTION_DATA};
} else {
legacySetLocations = new String[] {ActionUtils.POST_ACTION_DATA};
}
payloadProxy = new MessagePayloadProxy(propertiesTree, legacyGetLocations, legacySetLocations);
}
/**
* Initialise the Smooks instance.
* @throws ActionLifecycleException Failed to load Smooks configurations.
*/
public void initialise() throws ActionLifecycleException {
String resourceConfig = actionConfig.getAttribute(RESOURCE_CONFIG);
// If there's a Smooks resource config specified on the action config, this instance
// of the SmooksTransformer will use that configuration. Otherwise there needs to be a
// centralised (console based) config specified in the smooks.esb.properties. If not,
// we have an error!
if(resourceConfig != null) {
try {
smooks = SmooksResource.createSmooksResource(resourceConfig);
} catch (final LifecycleResourceException lre) {
throw new ActionLifecycleException("Unexpected exception creating smooks lifecycle resource", lre);
} catch (final IOException ie) {
throw new ActionLifecycleException("Unexpected IO exception accessing smooks resource: " + resourceConfig, ie);
} catch (final SAXException saxe) {
throw new ActionLifecycleException("Unexpected exception parsing smooks resource: " + resourceConfig, saxe);
}
} else {
throw new ActionLifecycleException("Invalid " + getClass().getSimpleName() + " action configuration. No 'resource-config' specified on the action.");
}
logger.info("Smooks configurations are now loaded.");
}
public void destroy() throws ActionLifecycleException {
SmooksResource.closeSmooksResource(smooks);
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.services.transform.TransformationService#transform(org.jboss.soa.esb.message.Message)
*/
public Message transform(Message message) throws TransformationException {
try {
return process(message);
} catch (ActionProcessingException e) {
throw new TransformationException(e);
}
}
/* (non-Javadoc)
* @see org.jboss.soa.esb.actions.ActionProcessor#process(java.lang.Object)
*/
public Message process(Message message) throws ActionProcessingException {
String messageProfile = "";
long startTime = System.nanoTime();
Object payload = null;
try {
payload = payloadProxy.getPayload(message);
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
try {
if(payload instanceof byte[]) {
payload = new String((byte[])payload, "UTF-8");
}
if(payload == null) {
logger.warn("Null message payload. Returning message unmodified.");
} else if(payload instanceof String) {
long start = System.currentTimeMillis();
ExecutionContext executionContext;
// Register the message profile with Smooks (if there is one and it's not already registered)...
messageProfile = registerMessageProfile(message, smooks);
// Filter and Serialise...
if(messageProfile == null) {
// Not using profiles on this transformation.
executionContext = smooks.createExecutionContext();
} else {
executionContext = smooks.createExecutionContext(messageProfile);
}
StringResult result = new StringResult();
smooks.filterSource(executionContext, new StringSource((String) payload), result);
HashMap beanHash = new HashMap(executionContext.getBeanContext().getBeanMap());
if(beanHash != null) {
message.getBody().add(EXTRACTED_BEANS_HASH, beanHash); // Backward compatibility.
} else {
message.getBody().remove(EXTRACTED_BEANS_HASH); // Backward compatibility.
}
if(logger.isDebugEnabled()) {
long timeTaken = System.currentTimeMillis() - start;
logger.debug("Transformed message for profile [" + messageProfile + "]. Time taken: "
+ timeTaken + ". Message in:\n[" + payload.toString()+ "]. \nMessage out:\n[" + result.toString() + "].");
}
setTransformationOutput(message, result.toString(), executionContext);
} else {
logger.warn("Only java.lang.String payload types supported. Input message was of type [" + payload.getClass().getName() + "]. Returning message untransformed.");
}
} catch(Throwable thrown) {
throw new ActionProcessingException("Message transformation failed.", thrown);
}
// TODO: Cater for more message input types e.g. InputStream, DOM Document...
// TODO: Cater for more message output types e.g. InputStream, DOM Document...
return message;
}
private void setTransformationOutput(Message message, String transformedMessage, ExecutionContext executionContext) throws ActionProcessingException {
// Set the transformation text output...
try {
payloadProxy.setPayload(message, transformedMessage);
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
// Set the transformation Java output. Will be the individual
// java objects directly on the message and (optionally) the map itself...
Map beanMap = executionContext.getBeanContext().getBeanMap();
if(beanMap != null) {
Iterator<Map.Entry> beans = beanMap.entrySet().iterator();
while (beans.hasNext()) {
Map.Entry entry = beans.next();
String key = (String) entry.getKey();
Object value = entry.getValue();
if(value != null) {
if(message.getBody().get(key) != null) {
logger.debug("Outputting Java object to '" + key + "'. Overwritting existing value.");
}
message.getBody().add(key, value);
}
}
}
// Now the map itself, if configured for output....
if(javaOutputLocation != null) {
if(beanMap != null) {
String location = javaOutputLocation;
if(location.equals("$default")) {
location = Body.DEFAULT_LOCATION;
}
if(message.getBody().get(location) != null) {
logger.debug("Outputting Java object Map to '" + location + "'. Overwritting existing value.");
}
message.getBody().add(location, beanMap);
} else {
logger.debug("Transformation Javabean spec '" + javaOutputLocation + "' doesn't evaluate to any bean map for the current message.");
}
}
}
/**
* Register the Message Exchange as a profile within Smooks.
* @param message The message.
* @param smooks The Smooks instance.
* @return The Smooks "profile" string that uniquely identifies the message flow associated
* with the message.
* @throws ActionProcessingException Failed to register the message flow for the message.
*/
private String registerMessageProfile(Message message, Smooks smooks) throws ActionProcessingException {
String messageProfile;
String messageFromType;
String messageFrom;
String messageToType;
String messageTo;
// Get the routing info from the message...
messageFrom = (String)message.getProperties().getProperty(FROM, defaultMessageFrom);
messageTo = (String)message.getProperties().getProperty(TO, defaultMessageTo);
// Get the message typing info from the message...
messageFromType = (String)message.getProperties().getProperty(FROM_TYPE, defaultMessageFromType);
messageToType = (String)message.getProperties().getProperty(TO_TYPE, defaultMessageToType);
// Construct the message profile string for use with Smooks. This is basically the
// name of the Message Exchange on which transformations are to be performed...
messageProfile = getMessageProfileString(messageFromType, messageFrom, messageToType, messageTo);
// If this transformation instance requires profiling, make sure the profile is registered on the
// Smooks instance.
if(messageProfile != null) {
// Register this message flow if it isn't already registered...
try {
ProfileStore profileStore = smooks.getApplicationContext().getProfileStore();
profileStore.getProfileSet(messageProfile);
} catch(UnknownProfileMemberException e) {
String[] profiles = getMessageProfiles(messageFromType, messageFrom, messageToType, messageTo);
synchronized (SmooksTransformer.class) {
// Register the message flow within the Smooks context....
logger.info("Registering JBoss ESB Message-Exchange as Smooks Profile: [" + messageProfile + "]. Profiles: [" + Arrays.asList(profiles) + "]");
SmooksUtil.registerProfileSet(DefaultProfileSet.create( messageProfile, profiles ), smooks);
}
}
}
return messageProfile;
}
/**
* Get the profile list based on the supplied message flow properties.
* @param messageFromType The type string for the message source.
* @param messageFrom The Message Exchange Participant name for the message source.
* @param messageToType The type string for the message target.
* @param messageTo The Message Exchange Participant name for the message target.
* @return The list of profiles.
*/
protected static String[] getMessageProfiles(String messageFromType, String messageFrom, String messageToType, String messageTo) {
List<String> profiles = new ArrayList<String>();
String[] profileArray;
if(messageFromType != null) {
profiles.add(FROM_TYPE + ":" + messageFromType);
}
if(messageFrom != null) {
profiles.add(FROM + ":" + messageFrom);
}
if(messageToType != null) {
profiles.add(TO_TYPE + ":" + messageToType);
}
if(messageTo != null) {
profiles.add(TO + ":" + messageTo);
}
profileArray = new String[profiles.size()];
profiles.toArray(profileArray);
return profileArray;
}
/**
* Construct the Smooks profile string based on the supplied message flow properties.
* @param messageFromType The type string for the message source.
* @param messageFrom The EPR string for the message source.
* @param messageToType The type string for the message target.
* @param messageTo The EPR srting for the message target.
* @return Smooks profile string for the message flow.
*/
protected static String getMessageProfileString(String messageFromType, String messageFrom, String messageToType, String messageTo) {
StringBuffer string = new StringBuffer();
if(messageFromType != null) {
string.append(FROM_TYPE + ":" + messageFromType);
string.append((messageFrom!=null || messageToType!=null || messageTo!=null?":":""));
}
if(messageFrom != null) {
string.append(FROM + ":" + messageFrom);
string.append((messageToType!=null || messageTo!=null?":":""));
}
if(messageToType != null) {
string.append(TO_TYPE + ":" + messageToType);
string.append((messageTo!=null?":":""));
}
if(messageTo != null) {
string.append(TO + ":" + messageTo);
}
if(string.length() == 0) {
return null;
}
return string.toString();
}
public void processException(final Message message, final Throwable th) {
}
public void processSuccess(final Message message) {
}
}