/*
* This file is part of the WfMOpen project.
* Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: XSLTTool.java 2904 2009-01-28 22:21:36Z mlipp $
*
* $Log$
* Revision 1.5 2006/09/29 12:32:10 drmlipp
* Consistently using WfMOpen as projct name now.
*
* Revision 1.4 2006/03/08 14:46:44 drmlipp
* Synchronized with 1.3.3p5.
*
* Revision 1.3 2005/08/11 15:16:27 drmlipp
* Fixed problem with XSLTTool and namespaces.
*
* Revision 1.2.2.1 2005/08/09 15:46:10 drmlipp
* Fixed problem with XSLTTool and namespaces in output parameter
* mappings.
*
* Revision 1.2 2005/01/21 09:37:35 drmlipp
* Fixed log level.
*
* Revision 1.1.1.2 2004/08/18 15:17:38 drmlipp
* Update to 1.2
*
* Revision 1.28 2004/07/01 13:44:34 lipp
* Minor caching improvement.
*
* Revision 1.27 2004/03/31 19:36:20 lipp
* Completed implementation of Activity.abandon(String).
*
* Revision 1.26 2004/02/26 09:42:36 lipp
* Fixed startElement parameters.
*
* Revision 1.25 2004/02/20 09:53:01 lipp
* Fixed exception handling.
*
* Revision 1.24 2004/02/19 13:10:32 lipp
* Clarified start-/endDocument usage in SAXEventBuffers.
*
* Revision 1.23 2004/02/17 15:28:51 lipp
* Various improvements.
*
* Revision 1.22 2004/02/16 15:38:51 lipp
* Fixed handling of non-well formed result values.
*
* Revision 1.21 2004/02/13 10:01:34 lipp
* Changed result type for result provider to Map which is more
* appropriate.
*
* Revision 1.20 2004/01/28 14:55:09 lipp
* Minor corrections.
*
* Revision 1.19 2004/01/28 09:25:49 montag
* additional testcase added.
*
* Revision 1.18 2004/01/28 08:34:33 montag
* new functionality for merging multiple xml documents in one.
*
* Revision 1.17 2004/01/27 17:38:30 montag
* BodyFiller corrected (handling of missing endDocument event).
*
* Revision 1.16 2004/01/27 12:11:08 montag
* XSLTTool now implements ResultProvider interface.
*
* Revision 1.15 2003/07/14 09:01:19 montag
* error corrected setting the result type
* if there is no output mapping defined.
*
* Revision 1.14 2003/07/14 07:55:09 montag
* clean up the code.
*
* Revision 1.13 2003/07/11 15:08:45 montag
* create transformer during setting of xslt
* and cache it for repeated tool invocation.
*
* Revision 1.12 2003/07/11 14:35:04 montag
* remove code for file handling for
* the xslt parameter.
*
* Revision 1.11 2003/07/11 14:07:09 montag
* return SAXEventBufferImpl if no output
* mapping is defined.
*
* Revision 1.10 2003/07/10 14:42:13 montag
* xlst transformation als SAXEventBuffer.
*
* Revision 1.9 2003/07/04 09:09:09 montag
* debug logging inserted.
*
* Revision 1.8 2003/07/03 12:53:50 montag
* url handling corrected.
*
* Revision 1.7 2003/07/02 15:01:59 montag
* documentation completed.
*
* Revision 1.6 2003/07/02 13:50:09 montag
* Property XSLT no longer mandatory.
*
* Revision 1.5 2003/07/02 11:53:13 montag
* Support for additional IN parameters.
*
* Revision 1.4 2003/07/02 09:30:38 montag
* handling of complex out parameters
* with no defined mapping fixed.
*
* Revision 1.3 2003/07/02 07:33:50 montag
* first working version of the XSLTTool.
*
* Revision 1.2 2003/07/01 15:49:55 montag
* jaxen-dom for XSLTTool.
*
* Revision 1.1 2003/07/01 14:28:43 montag
* initial of XSLTTool.
*
*
*/
package de.danet.an.workflow.tools;
import java.io.Serializable;
import java.io.StringWriter;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.RemoteException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TemplatesHandler;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.jaxen.JaxenException;
import org.jaxen.XPath;
import org.jaxen.jdom.JDOMXPath;
import org.jdom.Document;
import org.jdom.input.SAXHandler;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.XMLFilterImpl;
import de.danet.an.util.XMLUtil;
import de.danet.an.util.sax.BodyFilter;
import de.danet.an.workflow.util.SAXEventBufferImpl;
import de.danet.an.workflow.util.XPDLUtil;
import de.danet.an.workflow.api.Activity;
import de.danet.an.workflow.api.FormalParameter;
import de.danet.an.workflow.api.SAXEventBuffer;
import de.danet.an.workflow.spis.aii.ApplicationNotStoppedException;
import de.danet.an.workflow.spis.aii.CannotExecuteException;
import de.danet.an.workflow.spis.aii.ResultProvider;
import de.danet.an.workflow.spis.aii.ToolAgent;
import de.danet.an.workflow.spis.aii.XMLArgumentTypeProvider;
/**
* This class provides a tool that performs a transformation
* according to the JAXP 1.1 specification.
*
* @author <a href="mailto:montag@danet.de"></a>
* @version $Revision: 2904 $
*/
public class XSLTTool
implements ToolAgent, XMLArgumentTypeProvider,
ResultProvider, Serializable {
private static final org.apache.commons.logging.Log logger
= org.apache.commons.logging.LogFactory.getLog(XSLTTool.class);
// hold the mapping of return parameter name and eventually the XPath
// expression (intialized from property)
private Map returnParamInfo = new HashMap();
// holds the Transformer source property for the stylesheet
private SAXEventBufferImpl xsltSource = null;
// holds sax transformer factory
private SAXTransformerFactory saxTransFactCache = null;
// holds the parsed stylesheet
private Templates templatesCache = null;
/** The result container. */
private ThreadLocal result = new ThreadLocal ();
/**
* A filter that suppresses any characters before the first
* startElement. Normally there shouldn't be any, but somehow
* sometimes there are.
*/
private class BodyCleaner extends XMLFilterImpl {
private boolean gotFirst = false;
public BodyCleaner(ContentHandler contentHandler) {
setContentHandler(contentHandler);
}
public void characters(char[] charArray, int start, int length)
throws SAXException {
if (gotFirst) {
getContentHandler().characters (charArray, start, length);
}
}
public void startElement(String nsUri, String lname, String qname,
Attributes attributes) throws SAXException {
gotFirst = true;
getContentHandler().startElement (nsUri, lname, qname, attributes);
}
}
/**
* Creates an instance of <code>XSLTTool</code>
* with all attributes initialized to default values.
*/
public XSLTTool() {
}
private SAXTransformerFactory saxTransFact ()
throws TransformerConfigurationException {
synchronized (result) {
if (saxTransFactCache == null) {
saxTransFactCache = (SAXTransformerFactory)
TransformerFactory.newInstance();
}
return saxTransFactCache;
}
}
/**
* Set the definition of XSLT. It is used to invoke the stylesheet
* dynamically.
* on windows, something like
* "file:/C:/Entwicklung/WfMOpen/systemtest/tools/testXSLT.xsl"
* on unix, something like "file:/usr/dir/test.xsl"
*
* @param xsltLocation the location of given XSL as an URL.
*/
public void setXSLT(String xsltLocation) {
try {
if ((xsltLocation == null) || xsltLocation.equals("")) {
return;
}
// check for URL
URL url = null;
try {
url = new URL(xsltLocation);
} catch (MalformedURLException mue) {
// url == null
}
if (url != null) {
String systemID = url.toExternalForm();
if (logger.isDebugEnabled()) {
logger.debug("systemID = " + systemID);
}
xsltSource
= convertToSAXEventBuffer(new StreamSource(systemID));
} else {
logger.error("Error setting xslt: "
+ xsltLocation + "is no valid url");
}
} catch (Exception e) {
logger.error("Error setting xslt: " + e.getMessage (), e);
}
}
/**
* Create a SaxBufferEventImpl from a given source.
* @param source The source.
* @return SaxBufferEventImpl for the source
* @throws TransformerConfigurationException
* @throws TransformerException
*/
private SAXEventBufferImpl convertToSAXEventBuffer(Source source)
throws TransformerConfigurationException, TransformerException {
SAXEventBufferImpl b = new SAXEventBufferImpl();
SAXResult result = new SAXResult(b);
TransformerFactory.newInstance().newTransformer()
.transform(source, result);
return b;
}
/**
* Set the definition of XSLT.
*
* @param xslt the given XSLT definition
*/
public void setXSLT(SAXEventBuffer xslt) {
xsltSource = (SAXEventBufferImpl)xslt;
}
/**
* Get a transformer handler for this tool.
*/
private Templates templates() throws CannotExecuteException {
try {
synchronized (result) {
if (templatesCache == null && xsltSource != null) {
TemplatesHandler templHand
= saxTransFact().newTemplatesHandler();
xsltSource.emit(templHand);
// Now the TemplatesHandler contains the xslt information
templatesCache = templHand.getTemplates();
}
}
return templatesCache;
} catch (TransformerConfigurationException e) {
String msg = "Error creating TransformerHandler: " + e.getMessage();
logger.error(msg, e);
throw new CannotExecuteException (msg);
} catch (SAXException e) {
String msg = "Error creating TransformerHandler: " + e.getMessage();
logger.error(msg, e);
throw new CannotExecuteException (msg);
}
}
/**
* Set the xml definition of output mappings. It is used to convert the
* result of the transformation process.
*
* @param outputMappings the given xml as JDOM Element.
*/
public void setMappings(org.w3c.dom.Element outputMappings) {
try {
// retrieve namespaces
org.w3c.dom.NodeList namespacesNodeList = outputMappings
.getElementsByTagNameNS (XPDLUtil.XPDL_EXTN_NS, "Namespaces");
if (namespacesNodeList.getLength() == 0) {
namespacesNodeList = outputMappings.getElementsByTagNameNS
(XPDLUtil.XPDL_EXTN_V1_1_NS, "Namespaces");
}
org.w3c.dom.NodeList nsNodeList = null;
if (namespacesNodeList.getLength() > 0) {
org.w3c.dom.Element namespacesNode
= (org.w3c.dom.Element)namespacesNodeList.item(0);
nsNodeList = namespacesNode.getElementsByTagNameNS
(namespacesNode.getNamespaceURI(), "Namespace");
}
org.w3c.dom.NodeList paramNodeList = outputMappings
.getElementsByTagNameNS (XPDLUtil.XPDL_EXTN_NS, "Parameter");
if (paramNodeList.getLength() == 0) {
paramNodeList = outputMappings.getElementsByTagNameNS
(XPDLUtil.XPDL_EXTN_V1_1_NS, "Parameter");
}
for (int i = 0; i < paramNodeList.getLength(); i++) {
org.w3c.dom.Element param
= (org.w3c.dom.Element)paramNodeList.item(i);
String name = param.getAttribute("Name");
XPath xpath = new JDOMXPath(param.getAttribute("Select"));
if (nsNodeList != null) {
for (int j = 0; j < nsNodeList.getLength(); j++) {
org.w3c.dom.Element ns
= (org.w3c.dom.Element)nsNodeList.item(j);
String prefix = ns.getAttribute("Prefix");
String uri = ns.getAttribute("Uri");
xpath.addNamespace(prefix, uri);
}
}
returnParamInfo.put(name, xpath);
}
} catch (Exception e) {
// if any error ocurred, outputMappings is still null.
logger.error("error in setting XML for output mappings!", e);
}
}
// Implementation of de.danet.an.workflow.spis.aii.XMLArgumentTypeProvider
/**
* Return the requested type for XML arguments.
* @return one of <code>XML_AS_W3C_DOM</code>,
* <code>XML_AS_JDOM</code> or <code>XML_AS_SAX</code>
*/
public int requestedXMLArgumentType() {
return XMLArgumentTypeProvider.XML_AS_SAX;
}
// Implementation of de.danet.an.workflow.spis.aii.ToolAgent
/**
* Describe <code>invoke</code> method here.
*
* @param activity a <code>WfActivity</code> value
* @param formPars the formal parameters.
* @param map a <code>Map</code> value
* @throws CannotExecuteException if an error occurs
* @throws RemoteException if a system level error occurs
*/
public void invoke(Activity activity, FormalParameter[] formPars, Map map)
throws CannotExecuteException, RemoteException {
try {
result.set (invokeOperation(formPars, map));
} catch (JaxenException je) {
logger.error (je.getMessage());
logger.debug (je.getMessage(), je);
throw new CannotExecuteException (je.getMessage());
} finally {
if (logger.isDebugEnabled()) {
logger.debug ("Finished invocation of " + activity.uniqueKey());
}
}
}
/**
* Invoke the transformation through the JAXP 1.1 API.
* The first input paramater of type <code>SAXEventBufferImpl</code>
* will be treaten as the content to be transformed; additional input
* parameters will be handled as additional parameter to the
* transformation process.<p>
* If no stylesheet is explicitly set, the transformation simply "echoes"
* the source to the result.<p>
* The result of the transformation is of type <code>DOMResult</code>.
* After the transformation, the output parameters are filled with the
* result, possibly applying a mapping afterward. The return values are
* written in the new constructed process data using the key of the out
* formal parameters.
* If there is at least one mapping defined, any return value of complex
* type will be filled with an element object. If there is no output
* mapping defined, then there must at least one output parameter with
* a complex type; in this case an object of type SAXEventBufferImpl is
* created and the result is set with tihs object.
*
* @param formPars formal parameters
* @param map actual parameters
* @return the new process data with the result of web services operation
* included.
* @throws WSIFDynamicInvokerException if any errors in invoking web
* service occurred.
*/
private Map invokeOperation(FormalParameter[] formPars, Map map)
throws JaxenException, CannotExecuteException {
// Determine the content to be transformed
boolean firstInFound = false;
String newRootTag = null;
// Holds all additional parameter for the transformation
Map transformParameters = new HashMap();
// Holds all content for the transformation
List contentParameters = new ArrayList();
for (int i = 0; i < formPars.length; i++) {
if (formPars[i].mode() != FormalParameter.Mode.OUT) {
// IN or INOUT
String key = formPars[i].id();
Object value = map.get(key);
// check first IN parameter for operational mode
if (!firstInFound) {
if (value instanceof String) {
newRootTag = (String)value;
}
firstInFound = true;
}
if (value instanceof SAXEventBuffer) {
// add new content
contentParameters.add(value);
} else {
// add new parameter
transformParameters.put(key, value);
}
}
}
SAXEventBufferImpl seb = new SAXEventBufferImpl ();
try {
SAXResult saxResult = new SAXResult (new BodyCleaner(seb));
Templates tmplts = templates ();
TransformerHandler th = null;
if (tmplts == null) {
th = saxTransFact().newTransformerHandler();
} else {
th = saxTransFact().newTransformerHandler(tmplts);
}
setTransformerParameters(th.getTransformer(), transformParameters);
th.setResult(saxResult);
if (newRootTag == null) {
// simple input tree
((SAXEventBuffer)contentParameters.get(0)).emit(th);
} else {
// create new start element
th.startDocument();
th.startElement
("", newRootTag, newRootTag, new AttributesImpl());
// content
Iterator it = contentParameters.iterator();
while (it.hasNext()) {
SAXEventBuffer c = (SAXEventBuffer)it.next();
c.emit(new BodyFilter(th));
}
th.endElement("", newRootTag, newRootTag);
th.endDocument();
}
seb.pack();
} catch (TransformerConfigurationException e) {
String msg = "Error creating TransformerHandler: " + e.getMessage();
logger.error(msg, e);
throw new CannotExecuteException (msg);
} catch (SAXException se) {
String msg = "Error during transformation: " + se.getMessage();
logger.error (msg, se);
throw new CannotExecuteException (msg);
}
if (logger.isDebugEnabled()) {
logTransformationResult (seb);
}
// assemble the result, do additional mapping
Map resData = new HashMap ();
Document jdomRes = null;
for (int i = 0; i < formPars.length; i++) {
if (logger.isDebugEnabled()) {
logger.debug("formPars[i].id() = " + formPars[i].id());
logger.debug("formPars[i].type() = " + formPars[i].type());
logger.debug("formPars[i].mode() = " + formPars[i].mode());
}
if (formPars[i].mode() == FormalParameter.Mode.IN) {
continue;
}
XPath path = (XPath)returnParamInfo.get(formPars[i].id());
if (logger.isDebugEnabled()) {
logger.debug("path = " + path);
}
if (path == null) {
resData.put(formPars[i].id(), seb);
continue;
}
// Handle path expression
if (jdomRes == null) {
try {
SAXHandler hdlr = new SAXHandler ();
seb.emit (hdlr);
jdomRes = hdlr.getDocument();
} catch (SAXException e) {
String msg = "Problem converting SAX to JDOM: "
+ e.getMessage ();
logger.error (msg, e);
throw new CannotExecuteException (msg);
}
}
if (formPars[i].type().equals(String.class)) {
String value = path.stringValueOf(jdomRes);
if (logger.isDebugEnabled()) {
logger.debug("value = " + value);
}
resData.put(formPars[i].id(), value);
} else if (formPars[i].type().equals(Date.class)) {
String sval = path.stringValueOf(jdomRes);
try {
Date value = XMLUtil.parseXsdDateTime(sval);
if (logger.isDebugEnabled()) {
logger.debug("value = " + value);
}
resData.put(formPars[i].id(), value);
} catch (ParseException e) {
throw (CannotExecuteException)
(new CannotExecuteException
("Problem parsing " + sval + " as xsd datetime: "
+ e.getMessage())).initCause(e);
}
} else if (formPars[i].type().equals(Double.class)) {
String sval = path.stringValueOf(jdomRes);
try {
double value = XMLUtil.parseXsdDouble(sval);
if (logger.isDebugEnabled()) {
logger.debug("value = " + value);
}
resData.put(formPars[i].id(), new Double(value));
} catch (NumberFormatException e) {
throw (CannotExecuteException)
(new CannotExecuteException
("Problem parsing " + sval + " as xsd double: "
+ e.getMessage())).initCause(e);
}
} else if (formPars[i].type().equals(Boolean.class)) {
String sval = path.stringValueOf(jdomRes);
try {
boolean value = XMLUtil.parseXsdBoolean(sval);
if (logger.isDebugEnabled()) {
logger.debug("value = " + value);
}
resData.put(formPars[i].id(), new Boolean(value));
} catch (ParseException e) {
throw (CannotExecuteException)
(new CannotExecuteException
("Problem parsing " + sval + " as xsd boolean: "
+ e.getMessage())).initCause(e);
}
} else if ((formPars[i].type() instanceof Class)
&& Number.class.isAssignableFrom
((Class)formPars[i].type())) {
Number n = path.numberValueOf(jdomRes);
if (formPars[i].type().equals (Long.class)
&& !(n instanceof Long)) {
n = new Long (n.longValue());
}
resData.put(formPars[i].id(), n);
} else {
List selected = path.selectNodes (jdomRes);
resData.put(formPars[i].id(), selected);
}
}
return resData;
}
/**
* Sets the additional parameter for the transformation process.
* @param transformer the transformer
* @param transformParameters the additional transform parameters
*/
private void setTransformerParameters(Transformer transformer,
Map transformParameters) {
// set additional parameters
transformer.clearParameters();
if (!transformParameters.isEmpty()) {
Iterator it = transformParameters.keySet().iterator();
while(it.hasNext()) {
String key = (String)it.next();
Object value = transformParameters.get(key);
if (logger.isDebugEnabled()) {
logger.debug("Set additional transformer parameter: "
+ " (" + key + ", " + value + ")");
}
transformer.setParameter(key, value);
}
}
}
/**
* Describe <code>terminate</code> method here.
*
* @param activity a <code>WfActivity</code> value
* @throws ApplicationNotStoppedException if the application could
* not be terminated.
*/
public void terminate(Activity activity)
throws ApplicationNotStoppedException {
throw new ApplicationNotStoppedException
("Terminate not implemented for XSLTTool.");
}
/**
* Return the result evaluated during {@link ToolAgent#invoke
* <code>invoke</code>}. The method will only be called once after
* each invoke, i.e. the attribute holding the result be be
* cleared in this method.
*
* @return the result data or <code>null</code> if the invocation
* does not return any data.
*/
public Object result () {
Map res = (Map)result.get();
result.set (null);
return res;
}
/**
* Log the result of the transformtion to System.out.
* @param domResult the dom result
*/
private void logTransformationResult(SAXEventBuffer seb) {
try {
StringWriter sw = new StringWriter();
StreamResult streamResult = new StreamResult(sw);
SAXTransformerFactory tf
= (SAXTransformerFactory)TransformerFactory.newInstance();
TransformerHandler serializer = tf.newTransformerHandler();
serializer.getTransformer()
.setOutputProperty(OutputKeys.ENCODING,"ISO-8859-1");
serializer.getTransformer()
.setOutputProperty(OutputKeys.INDENT,"yes");
serializer.setResult (streamResult);
seb.emit (serializer);
logger.debug("TransformationResult:\n" + sw);
} catch (Exception e) {
logger.warn(e);
}
}
}