/*
* Copyright (c) 2012, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* 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.wso2.carbon.humantask.core.engine.runtime.xpath;
import net.sf.saxon.value.DurationValue;
import net.sf.saxon.xpath.XPathEvaluator;
import net.sf.saxon.xpath.XPathFactoryImpl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.*;
import org.wso2.carbon.humantask.core.engine.runtime.api.EvaluationContext;
import org.wso2.carbon.humantask.core.engine.runtime.api.ExpressionLanguageRuntime;
import org.wso2.carbon.humantask.core.engine.runtime.api.HumanTaskRuntimeException;
import org.wso2.carbon.humantask.core.utils.DOMUtils;
import org.wso2.carbon.humantask.core.utils.Duration;
import org.wso2.carbon.humantask.core.utils.ISO8601DateParser;
import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.util.*;
/**
* Implementation for XPath 2.0
*/
public class XPathExpressionRuntime implements ExpressionLanguageRuntime {
private static final Log log = LogFactory.getLog(ExpressionLanguageRuntime.class);
/**
* Xpath 2 name space
*/
public static final String ns = XPath2Constants.WSHT_EXP_LANG_XPATH20;
private static final String JAVAX_XML_XPATH_XPATH_FACTORY = "javax.xml.xpath.XPathFactory:";
private static final String NET_SF_SAXON_XPATH_XPATH_FACTORY_IMPL = "net.sf.saxon.xpath.XPathFactoryImpl";
/**
* Evaluate XPath expression
*
* @param exp XPath expression string
* @param evalCtx Evaluation context containing all the required context information
* @return Return List of selected nodes or string
*/
@Override
public List evaluate(String exp, EvaluationContext evalCtx) {
List result;
Object someRes;
try {
someRes = evaluate(exp, evalCtx, XPathConstants.NODESET);
} catch (Exception e) {
someRes = evaluate(exp, evalCtx, XPathConstants.STRING);
}
if (someRes instanceof List) {
result = (List) someRes;
if (log.isDebugEnabled()) {
log.debug("Returned list of size " + result.size());
}
if ((result.size() == 1) && !(result.get(0) instanceof Node)) {
// Dealing with a Java class
Object simpleType = result.get(0);
// Dates get a separate treatment as we don't want to call toString on them
String textVal;
if (simpleType instanceof Date) {
textVal = ISO8601DateParser.format((Date) simpleType);
} else if (simpleType instanceof DurationValue) {
textVal = ((DurationValue) simpleType).getStringValue();
} else {
textVal = simpleType.toString();
}
// Wrapping in a document
Document document = DOMUtils.newDocument();
// Giving our node a parent just in case it's an LValue expression
Element wrapper = document.createElement("wrapper");
Text text = document.createTextNode(textVal);
wrapper.appendChild(text);
document.appendChild(wrapper);
result = Collections.singletonList(text);
}
} else if (someRes instanceof NodeList) {
NodeList retVal = (NodeList) someRes;
if (log.isDebugEnabled()) {
log.debug("Returned node list of size " + retVal.getLength());
}
result = new ArrayList(retVal.getLength());
for (int m = 0; m < retVal.getLength(); ++m) {
Node val = retVal.item(m);
if (val.getNodeType() == Node.DOCUMENT_NODE) {
val = ((Document) val).getDocumentElement();
}
result.add(val);
}
} else if (someRes instanceof String) {
// Wrapping in a document
Document document = DOMUtils.newDocument();
Element wrapper = document.createElement("wrapper");
Text text = document.createTextNode((String) someRes);
wrapper.appendChild(text);
document.appendChild(wrapper);
result = Collections.singletonList(text);
} else {
result = null;
}
return result;
}
private Object evaluate(String exp, EvaluationContext evalCtx, QName type) {
try {
XPathFactory xpf = new XPathFactoryImpl();
JaxpFunctionResolver funcResolve = new JaxpFunctionResolver(evalCtx);
xpf.setXPathFunctionResolver(funcResolve);
XPathEvaluator xpe = (XPathEvaluator) xpf.newXPath();
xpe.setXPathFunctionResolver(funcResolve);
xpe.setBackwardsCompatible(true);
xpe.setNamespaceContext(evalCtx.getNameSpaceContextOfTask());
XPathExpression xpathExpression = xpe.compile(exp);
Node contextNode = evalCtx.getRootNode() == null ? DOMUtils.newDocument() : evalCtx.getRootNode();
Object evalResult = xpathExpression.evaluate(contextNode, type);
if (evalResult != null && log.isDebugEnabled()) {
log.debug("Expression " + exp + " generate result " + evalResult + " - type=" +
evalResult.getClass().getName());
if (evalCtx.getRootNode() != null) {
log.debug("Was using context node " + DOMUtils.domToString(evalCtx.getRootNode()));
}
}
return evalResult;
} catch (XPathFactoryConfigurationException e) {
log.error("Exception occurred while creating XPathFactory.", e);
throw new XPathProcessingException("Exception occurred while creating XPathFactory.", e);
} catch (XPathExpressionException e) {
String msg = "Error evaluating XPath expression: " + exp;
log.error(msg, e);
throw new XPathProcessingException(msg, e);
} catch (ParserConfigurationException e) {
String msg = "XML Parsing error.";
log.error(msg, e);
throw new XPathProcessingException(msg, e);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new XPathProcessingException(e.getMessage(), e);
}
}
/**
* Evaluate given XPath string and returns result as a string
*
* @param exp XPath expression string
* @param evalCtx Evaluation context containing all the required context information
* @return String
*/
@Override
public String evaluateAsString(String exp, EvaluationContext evalCtx) {
return (String) evaluate(exp, evalCtx, XPathConstants.STRING);
}
/**
* Evaluate given XPath string and returns the result as Date
*
* @param exp XPath expression string
* @param evalCtx Evaluation context containing all the required context information
* @return Calendar
*/
@Override
public Calendar evaluateAsDate(String exp, EvaluationContext evalCtx) {
List literal = toList(evaluate(exp, evalCtx));
if (literal.size() == 0) {
throw new IllegalArgumentException("No results for expression:" + exp);
}
if (literal.size() > 1) {
throw new IllegalArgumentException("Multiple results for expression:" + exp);
}
Object date = literal.get(0);
if (date instanceof Calendar) {
return (Calendar) date;
}
if (date instanceof Date) {
Calendar cal = Calendar.getInstance();
cal.setTime((Date) date);
return cal;
}
if (date instanceof Element) {
date = ((Element) date).getTextContent();
}
if (date instanceof Text) {
date = ((Text) date).getTextContent();
}
try {
return ISO8601DateParser.parseCal(date.toString());
} catch (Exception ex) {
String errmsg = "Invalid date format: " + literal;
log.error(errmsg, ex);
throw new IllegalArgumentException(errmsg, ex);
}
}
/**
* Evaluate given XPath string and returns the result as Date
*
* @param exp XPath expression string
* @param evalCtx Evaluation context containing all the required context information
* @return Duration
*/
@Override
public Duration evaluateAsDuration(String exp, EvaluationContext evalCtx) {
String literal = exp;
if (!Duration.isValidExpression(exp)) {
literal = this.evaluateAsString(exp, evalCtx);
//TODO exp should be evaluateAsString in the first place. If it is not an xpath then it should return the
// exp string itself
}
try {
return new Duration(literal);
} catch (Exception ex) {
String errmsg = "Invalid duration: " + exp;
//String errmsg = "Invalid duration: " + literal;
log.error(errmsg, ex);
throw new IllegalArgumentException(errmsg, ex);
}
}
/**
* Evaluate given XPath string and returns the result as boolean
*
* @param exp XPath expression
* @param evalCtx Evaluation context containing all the required context information
* @return boolean
*/
@Override
public boolean evaluateAsBoolean(String exp, EvaluationContext evalCtx) {
return (Boolean) evaluate(exp, evalCtx, XPathConstants.BOOLEAN);
}
/**
* Evaluate given XPath and returns the results as a java.lang.Number
*
* @param exp XPath expression
* @param evalCtx Evaluation context containing all the required context information
* @return Number
*/
@Override
public Number evaluateAsNumber(String exp, EvaluationContext evalCtx) {
return (Number) evaluate(exp, evalCtx, XPathConstants.NUMBER);
}
/**
* Evaluate the expression returns an OMElement
*
* @param exp Expresion
* @param partName Name of the part
* @param evalCtx EvaluationContext
* @return Part as an Node
*/
@Override
public Node evaluateAsPart(String exp, String partName, EvaluationContext evalCtx) {
Document document = DOMUtils.newDocument();
Node node = document.createElement(partName);
List<Node> nodeList = evaluate(exp, evalCtx);
if (nodeList.size() == 0) {
String errMsg = "0 nodes selected for the expression: " + exp;
log.error(errMsg);
throw new HumanTaskRuntimeException(errMsg);
} else if (nodeList.size() > 1) {
String errMsg = "More than one nodes are selected for the expression: " + exp;
log.error(errMsg);
throw new HumanTaskRuntimeException(errMsg);
}
Node partNode = nodeList.get(0);
replaceElement((Element) node, (Element) partNode);
return node;
}
private Element replaceElement(Element lval, Element src) {
Document doc = lval.getOwnerDocument();
NodeList nl = src.getChildNodes();
for (int i = 0; i < nl.getLength(); ++i) {
lval.appendChild(doc.importNode(nl.item(i), true));
}
NamedNodeMap attrs = src.getAttributes();
for (int i = 0; i < attrs.getLength(); ++i) {
Attr attr = (Attr) attrs.item(i);
if (!attr.getName().startsWith("xmlns")) {
lval.setAttributeNodeNS((Attr) doc.importNode(attrs.item(i), true));
// Case of qualified attribute values, we're forced to add corresponding namespace declaration manually
int colonIdx = attr.getValue().indexOf(":");
if (colonIdx > 0) {
String prefix = attr.getValue().substring(0, colonIdx);
String attrValNs = src.lookupPrefix(prefix);
if (attrValNs != null) {
lval.setAttributeNS(DOMUtils.NS_URI_XMLNS, "xmlns:" + prefix, attrValNs);
}
}
}
}
return lval;
}
/**
* Somewhat eases the pain of dealing with both Lists and Nodelists by converting either
* passed as parameter to a List.
*
* @param nl a NodeList or a List
* @return a List
*/
public static List<Node> toList(Object nl) {
if (nl == null) {
return null;
}
if (nl instanceof List) {
return (List<Node>) nl;
}
NodeList cnl = (NodeList) nl;
LinkedList<Node> ll = new LinkedList<Node>();
for (int m = 0; m < cnl.getLength(); m++) {
ll.add(cnl.item(m));
}
return ll;
}
}