/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY 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 along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.soa.esb.actions.converters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.log4j.Logger;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.ActionUtils;
import org.jboss.soa.esb.actions.converters.xstream.conf.FieldAliasConf;
import org.jboss.soa.esb.actions.converters.xstream.conf.ImplicitCollectionConf;
import org.jboss.soa.esb.actions.converters.xstream.conf.XStreamConfigurator;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.helpers.KeyValuePair;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
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.util.ClassUtil;
import org.jboss.soa.esb.util.XPathUtil;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.DomReader;
/**
* XML to Object processor that uses Uses the <a href="http://xstream.codehaus.org/">XStream</a> .
* <p/>
* Sample Action Configuration:
* <pre>{@code
* <action name="doCustomer" class="XStreamObject">
* <property name="class-alias" value="Customer"/>
* <property name="incoming-type" value="CustomerProcessor"/>
* <property name="exclude-package" value="false"/>
* <property name="root-node" value="/root/Customer"/>
* <property name="aliases">
* <alias name="aliasName" class="className" />
* <alias name="aliasName" class="className" />
* ...
* </property>
* <property name="fieldAliases">
* <field-alias alias="aliasName" class="className" fieldName="fieldName"/>
* <field-alias alias="aliasName" class="className" fieldName="fieldName"/>
* ...
* </property>
* <property name="attributeAliases">
* <attribute-alias name="aliasName" class="className"/>
* <attribute-alias name="aliasName" class="className"/>
* ...
* </property>
* <property name="implicit-collections">
* <implicit-collection class="className" fieldName="fieldName" fieldType="java.lang.String" />
* <implicit-collection class="className" fieldName="fieldName" fieldType="java.lang.Integer"/>
* ...
* </property>
* <property name="converters">
* <converter class="className" />
* <converter class="className" />
* ...
* </action>
* }</pre>
* <p/>
* <lu>
* <li><i>class-alias</i> Optional. Class alias for the 'incoming-type'.</li>
* <li><i>incoming-type</i> Required. Class of the incoming type.</li>
* <li><i>exclude-package-type</i> Optional, defaults to true. Determines whether package name should be removed from the incoming type.</li>
* <li><i>root-node</i> Optional. Specifies an XPath expression to be used to determine the root node that XStream will use.</li>
* <li><i>aliases</i> Optional. Specifies extra class aliases.</li>
* <li><i>fieldAliases</i> Optional. Specifies field aliases.</li>
* <li><i>implicit-collections</i> Optional. Specifies implicit collections which are when you have an xml element that is a place holder for a collection of other elements.
* In this case you are telling XStream to not include the holder element but instead place its element into the the 'fieldName' in the target class.
* 'className' is the collection type.
* 'fieldType' is the type the elements in the collection.</li>
* <li><i>converters</i> Optional. Specifies converters that will be registered with XStream.</li>
* </lu>
*
* The XML root element is either set from the "class-alias" property or the classes full name. In the later case, the class package is
* excluded unless "exclude-package" is set to "false"/"no".
*
* @author danielmarchant
* @author Daniel Bevenius
* @since Version 4.0
*/
public class XStreamToObject extends AbstractObjectXStream {
private static Logger logger = Logger.getLogger(XStreamToObject.class);
// class related variables
private Class<?> incomingType;
// action related variables
private Map<String,String> aliases;
private Map<String,String> attributeAliases;
private List<FieldAliasConf> fieldAliases;
private List<String> converters;
private List<ImplicitCollectionConf> implicitCollections;
private MessagePayloadProxy payloadProxy;
/**
* Public constructor.
* @param properties Action Properties.
* @throws ConfigurationException Action not properly configured.
*/
public XStreamToObject(ConfigTree properties) {
this(properties.getName(), properties.attributesAsList());
XStreamConfigurator xstreamConfig = new XStreamConfigurator(properties);
aliases = getAliases( properties, "alias" );
fieldAliases = xstreamConfig.getFieldAliases();
attributeAliases = getAliases( properties, "attribute-alias" );
converters = getConverters( properties, "converter" );
implicitCollections = xstreamConfig.getImplicitCollections();
payloadProxy = new MessagePayloadProxy(properties,
new String[] {BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA},
new String[] {ActionUtils.POST_ACTION_DATA});
}
/**
* Public constructor.
* @param actionName Action name.
* @param properties Action Properties.
* @throws ConfigurationException Action not properly configured.
*/
protected XStreamToObject(String actionName, List<KeyValuePair> properties) {
super(actionName,properties);
String incomingTypeStr = KeyValuePair.getValue("incoming-type", properties);
try {
incomingType = ClassUtil.forName(incomingTypeStr, getClass());
} catch (ClassNotFoundException e) {
logger.error("Could not find : " + incomingTypeStr,e);
}
}
/**
* Processes the message by using the giving class-processor.
*
*/
public Message process(Message message) throws ActionProcessingException {
Object object;
try {
object = payloadProxy.getPayload(message);
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
try {
Object toObject = incomingType.newInstance();
toObject = fromXmlToObject( object.toString(), toObject );
payloadProxy.setPayload(message, toObject);
} catch (InstantiationException e) {
logger.error( e );
throw new ActionProcessingException("Could not invoke for Arg: " + getName(),e );
} catch (IllegalAccessException e) {
logger.error( e );
throw new ActionProcessingException("Could not access for Arg: " + getName(),e );
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
return message;
}
/**
* Will extract the alias elements from the passed-in conifgTree
*
* @param configTree the configuration for this class
*
* @return Map<String,String> either an empty map or a map containing the alias name
* as its key and the corresponding value is the class to map
* it to.
*/
protected Map<String,String> getAliases( final ConfigTree configTree, final String childName)
{
Map<String,String> aliases = new HashMap<String,String>();
ConfigTree[] children = configTree.getChildren( childName );
if ( children != null ) {
for ( ConfigTree alias : children )
aliases.put( alias.getAttribute( "name" ), alias.getAttribute( "class" ) );
}
return aliases;
}
/**
* Will extract the alias elements from the passed-in conifgTree
*
* @param configTree the configuration for this class
*
* @return Map<String,String> either an empty map or a map containing the alias name
* as its key and the corresponding value is the class to map
* it to.
*/
protected List<FieldAliasConf> getFieldAliases( final ConfigTree configTree, final String childName)
{
List<FieldAliasConf> aliases = new ArrayList<FieldAliasConf>();
ConfigTree[] children = configTree.getChildren( childName );
if ( children != null )
{
for ( ConfigTree alias : children )
{
aliases.add(new FieldAliasConf(alias.getAttribute("alias"), alias.getAttribute("class"), alias.getAttribute("fieldName")));
}
}
return aliases;
}
/**
* Will extract the converter elements from the passed-in conifgTree
*
* @param configTree the configuration for this class
*
* @return Map<String,String> either an empty map or a map containing the converter class
*/
protected List<String> getConverters( final ConfigTree configTree, final String childName )
{
List<String> converters = new ArrayList<String>();
ConfigTree[] children = configTree.getChildren( childName );
if ( children != null ) {
for ( ConfigTree converter : children )
converters.add( converter.getAttribute( "class" ) );
}
return converters;
}
/**
* Added the aliases contained in the passed-in map to the
* passed-in XStream object
*
* @param aliases Map of aliases.
* @throws ActionProcessingException
*/
protected void addAliases( Map<String, String> aliases, XStream xstream) throws ActionProcessingException
{
if ( aliases == null )
return;
Set<Map.Entry<String,String>> set = aliases.entrySet();
for (Map.Entry me : set ) {
String className = (String) me.getValue();
try {
Class<?> clazz = ClassUtil.forName( className, getClass() );
xstream.alias((String)me.getKey(), clazz );
} catch (ClassNotFoundException e) {
logger.error("ClassNotFoundException: ", e);
throw new ActionProcessingException("Could not add alias : " + (String)me.getKey() + ", class : " + className ,e );
}
}
}
/**
* Registers the converters contained in the passed in list
*
* @param converters which should be registered with XStream
* @param xstream
* @throws ActionProcessingException
*/
protected void addConverters( List<String> converters, XStream xstream) throws ActionProcessingException
{
if ( converters == null )
return;
for( String converterClass : converters )
{
if ( converterClass == null )
continue;
try {
Class<?> clazz = ClassUtil.forName( converterClass, getClass() );
xstream.registerConverter((Converter)clazz.newInstance());
} catch (ClassNotFoundException e) {
logger.error("ClassNotFoundException: ", e);
throw new ActionProcessingException("Could not register converter : " + converterClass.getClass().getName(),e );
} catch (InstantiationException e)
{
logger.error("InstantiationException: ", e);
} catch (IllegalAccessException e)
{
logger.error("IllegalAccessException: ", e);
}
}
}
/**
* Added the aliases contained in the passed-in map to the
* passed-in XStream object
*
* @param aliases Map of aliases.
* @throws ActionProcessingException
*/
protected void addAttributeAliases( Map<String, String> aliases, XStream xstream) throws ActionProcessingException
{
if ( aliases == null )
return;
Set<Map.Entry<String,String>> set = aliases.entrySet();
for (Map.Entry me : set ) {
String className = (String) me.getValue();
try {
Class<?> clazz = ClassUtil.forName( className, getClass() );
xstream.useAttributeFor( (String)me.getKey(), clazz );
} catch (ClassNotFoundException e) {
logger.error("ClassNotFoundException: ", e);
throw new ActionProcessingException("Could not add alias : " + (String)me.getKey() + ", class : " + className ,e );
}
}
}
/**
*
* @param xml the xml String
* @param root an instance of the type of the root element
* @throws ActionProcessingException
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
*/
protected Object fromXmlToObject(String xml, Object root ) throws ActionProcessingException
{
HierarchicalStreamReader reader = null;
try
{
reader = new DomReader( getRootElement( xml, rootNodeName ) );
XStream xstream = new XStream( new DomDriver() );
xstream.alias(getAlias(incomingType), incomingType);
addAliases( aliases, xstream );
XStreamConfigurator.addFieldAliases(fieldAliases, xstream);
addAttributeAliases( attributeAliases, xstream );
addConverters( converters, xstream );
XStreamConfigurator.addImplicitCollections(implicitCollections, xstream);
return xstream.unmarshal( reader, root );
}
finally
{
if ( reader != null) reader.close();
}
}
/*
* Simply delegates to XPathUtil and catches exceptions specific
* to that class and rethrows an ActionProcessingException
*/
private Element getRootElement( String xml, String xPathExpression ) throws ActionProcessingException
{
try
{
return XPathUtil.getNodeFromXPathExpression( xml, xPathExpression );
}
catch (ParserConfigurationException e)
{
logger.error( "ParserConfigurationException:", e );
throw new ActionProcessingException( e );
}
catch (SAXException e)
{
logger.error( "SAXException : ", e );
throw new ActionProcessingException( e );
}
catch (IOException e)
{
logger.error( "IOException: ", e );
throw new ActionProcessingException( e );
}
catch (XPathExpressionException e)
{
logger.error( "XPathExpressionException", e );
throw new ActionProcessingException( e );
}
}
}