/* * Copyright (c) 2010-2013 Evolveum * * 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 com.evolveum.midpoint.model.common.expression.script.xpath; import com.evolveum.midpoint.model.common.expression.ExpressionSyntaxException; import com.evolveum.midpoint.model.common.expression.ExpressionVariables; import com.evolveum.midpoint.model.common.expression.functions.FunctionLibrary; import com.evolveum.midpoint.model.common.expression.script.ScriptEvaluator; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismConstants; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.PrismValue; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.util.PrismUtil; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.prism.xml.XsdTypeMapper; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.ExceptionUtil; import com.evolveum.midpoint.schema.util.ObjectResolver; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ScriptExpressionEvaluatorType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ScriptExpressionReturnTypeType; import com.evolveum.prism.xml.ns._public.types_3.PolyStringType; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import javax.xml.namespace.QName; import javax.xml.xpath.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; /** * @author Radovan Semancik */ public class XPathScriptEvaluator implements ScriptEvaluator { public static String XPATH_LANGUAGE_URL = "http://www.w3.org/TR/xpath/"; private XPathFactory factory = XPathFactory.newInstance(); private PrismContext prismContext; public XPathScriptEvaluator(PrismContext prismContext) { this.prismContext = prismContext; } @Override public <T, V extends PrismValue> List<V> evaluate(ScriptExpressionEvaluatorType expressionType, ExpressionVariables variables, ItemDefinition outputDefinition, Function<Object, Object> additionalConvertor, ScriptExpressionReturnTypeType suggestedReturnType, ObjectResolver objectResolver, Collection<FunctionLibrary> functions, String contextDescription, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, ExpressionSyntaxException { String codeString = expressionType.getCode(); if (codeString == null) { throw new ExpressionEvaluationException("No script code in " + contextDescription); } Class<T> type = null; if (outputDefinition != null) { QName xsdReturnType = outputDefinition.getTypeName(); type = XsdTypeMapper.toJavaType(xsdReturnType); // may return null if unknown } if (type == null) { type = (Class<T>) Element.class; // actually, if outputDefinition is null, the return value is of no interest for us } QName returnType = determineRerturnType(type, expressionType, outputDefinition, suggestedReturnType); Object evaluatedExpression = evaluate(returnType, codeString, variables, objectResolver, functions, contextDescription, result); List<V> propertyValues; boolean scalar = !outputDefinition.isMultiValue(); if (expressionType.getReturnType() != null) { scalar = isScalar(expressionType.getReturnType()); } else if (suggestedReturnType != null) { scalar = isScalar(suggestedReturnType); } if (scalar) { if (evaluatedExpression instanceof NodeList) { NodeList evaluatedExpressionNodeList = (NodeList)evaluatedExpression; if (evaluatedExpressionNodeList.getLength() > 1) { throw new ExpressionEvaluationException("Expected scalar expression result but got a list result with "+evaluatedExpressionNodeList.getLength()+" elements in "+contextDescription); } if (evaluatedExpressionNodeList.getLength() == 0) { evaluatedExpression = null; } else { evaluatedExpression = evaluatedExpressionNodeList.item(0); } } propertyValues = new ArrayList<V>(1); V pval = convertScalar(type, returnType, evaluatedExpression, contextDescription); if (pval instanceof PrismPropertyValue && !isNothing(((PrismPropertyValue<T>)pval).getValue())) { propertyValues.add(pval); } } else { if (!(evaluatedExpression instanceof NodeList)) { throw new IllegalStateException("The expression " + contextDescription + " resulted in " + evaluatedExpression.getClass().getName() + " while exprecting NodeList in "+contextDescription); } propertyValues = convertList(type, (NodeList) evaluatedExpression, contextDescription); } return (List<V>) PrismValue.cloneCollection(propertyValues); } private boolean isScalar(ScriptExpressionReturnTypeType returnType) { if (returnType == ScriptExpressionReturnTypeType.SCALAR) { return true; } else { return false; } } private Object evaluate(QName returnType, String code, ExpressionVariables variables, ObjectResolver objectResolver, Collection<FunctionLibrary> functions, String contextDescription, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, ExpressionSyntaxException { XPathExpressionCodeHolder codeHolder = new XPathExpressionCodeHolder(code); //System.out.println("code " + code); XPath xpath = factory.newXPath(); XPathVariableResolver variableResolver = new LazyXPathVariableResolver(variables, objectResolver, contextDescription, prismContext, result); xpath.setXPathVariableResolver(variableResolver); xpath.setNamespaceContext(new MidPointNamespaceContext(codeHolder.getNamespaceMap())); xpath.setXPathFunctionResolver(getFunctionResolver(functions)); XPathExpression expr; try { expr = xpath.compile(codeHolder.getExpressionAsString()); } catch (Exception e) { Throwable originalException = ExceptionUtil.lookForTunneledException(e); if (originalException != null && originalException instanceof ObjectNotFoundException) { throw (ObjectNotFoundException) originalException; } if (originalException != null && originalException instanceof ExpressionSyntaxException) { throw (ExpressionSyntaxException) originalException; } if (e instanceof XPathExpressionException) { throw createExpressionEvaluationException(e, contextDescription); } if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new SystemException(e.getMessage(), e); } Object rootNode; try { rootNode = determineRootNode(variableResolver, contextDescription); } catch (SchemaException e) { throw new ExpressionSyntaxException(e.getMessage(), e); } Object evaluatedExpression; try { evaluatedExpression = expr.evaluate(rootNode, returnType); } catch (Exception e) { Throwable originalException = ExceptionUtil.lookForTunneledException(e); if (originalException != null && originalException instanceof ObjectNotFoundException) { throw (ObjectNotFoundException) originalException; } if (originalException != null && originalException instanceof ExpressionSyntaxException) { throw (ExpressionSyntaxException) originalException; } if (e instanceof XPathExpressionException) { throw createExpressionEvaluationException(e, contextDescription); } if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new SystemException(e.getMessage(), e); } if (evaluatedExpression == null) { return null; } return evaluatedExpression; } private ExpressionEvaluationException createExpressionEvaluationException(Exception e, String contextDescription) { return new ExpressionEvaluationException(lookForMessage(e) + " in " + contextDescription, e); } public static String lookForMessage(Throwable e) { // the net.sf.saxon.trans.XPathException lies. It has meaningless message. skip it. if (e instanceof net.sf.saxon.trans.XPathException && e.getCause() != null) { return lookForMessage(e.getCause()); } if (e.getMessage() != null) { return e.getMessage(); } if (e.getCause() != null) { return lookForMessage(e.getCause()); } return null; } /** * Kind of convenience magic. Try few obvious variables and set them as the root node * for evaluation. This allow to use "fullName" instead of "$user/fullName". */ private Object determineRootNode(XPathVariableResolver variableResolver, String contextDescription) throws SchemaException { Object rootNode = variableResolver.resolveVariable(null); if (rootNode == null) { // Add empty document instead of null so the expressions don't die with exception. // This is necessary e.g. on deletes in sync when there may be nothing to evaluate. return DOMUtil.getDocument(); } else { return LazyXPathVariableResolver.convertToXml(rootNode, null, prismContext, contextDescription); } } private <T> QName determineRerturnType(Class<T> type, ScriptExpressionEvaluatorType expressionType, ItemDefinition outputDefinition, ScriptExpressionReturnTypeType suggestedReturnType) throws ExpressionEvaluationException { if (expressionType.getReturnType() == ScriptExpressionReturnTypeType.LIST || suggestedReturnType == ScriptExpressionReturnTypeType.LIST) { return XPathConstants.NODESET; } if (expressionType.getReturnType() == ScriptExpressionReturnTypeType.SCALAR) { return toXPathReturnType(outputDefinition.getTypeName()); } if (suggestedReturnType == ScriptExpressionReturnTypeType.LIST) { return XPathConstants.NODESET; } if (suggestedReturnType == ScriptExpressionReturnTypeType.SCALAR) { return toXPathReturnType(outputDefinition.getTypeName()); } if (outputDefinition.isMultiValue()) { return XPathConstants.NODESET; } else { return toXPathReturnType(outputDefinition.getTypeName()); } } private QName toXPathReturnType(QName xsdTypeName) throws ExpressionEvaluationException { if (xsdTypeName.equals(DOMUtil.XSD_STRING)) { return XPathConstants.STRING; } if (xsdTypeName.equals(DOMUtil.XSD_FLOAT)) { return XPathConstants.NUMBER; } if (xsdTypeName.equals(DOMUtil.XSD_DOUBLE)) { return XPathConstants.NUMBER; } if (xsdTypeName.equals(DOMUtil.XSD_INT)) { return XPathConstants.NUMBER; } if (xsdTypeName.equals(DOMUtil.XSD_INTEGER)) { return XPathConstants.NUMBER; } if (xsdTypeName.equals(DOMUtil.XSD_LONG)) { return XPathConstants.NUMBER; } if (xsdTypeName.equals(DOMUtil.XSD_BOOLEAN)) { return XPathConstants.BOOLEAN; } if (xsdTypeName.equals(DOMUtil.XSD_DATETIME)) { return XPathConstants.STRING; } if (xsdTypeName.equals(PolyStringType.COMPLEX_TYPE)) { return XPathConstants.STRING; } throw new ExpressionEvaluationException("Unsupported return type " + xsdTypeName); } /* if (type.equals(String.class)) { return XPathConstants.STRING; } if (type.equals(Double.class) || type.equals(double.class)) { return XPathConstants.NUMBER; } if (type.equals(Integer.class) || type.equals(int.class)) { return XPathConstants.NUMBER; } if (type.equals(Long.class) || type.equals(long.class)) { return XPathConstants.NUMBER; } if (type.equals(Boolean.class) || type.equals(boolean.class)) { return XPathConstants.BOOLEAN; } if (type.equals(NodeList.class)) { if (expressionType.getReturnType() == ScriptExpressionReturnTypeType.SCALAR) { // FIXME: is this OK? return XPathConstants.STRING; } else { return XPathConstants.NODESET; } } if (type.equals(Node.class)) { return XPathConstants.NODE; } if (type.equals(PolyString.class) || type.equals(PolyStringType.class)) { return XPathConstants.STRING; } throw new ExpressionEvaluationException("Unsupported return type " + type); } */ private <T, V extends PrismValue> V convertScalar(Class<T> type, QName returnType, Object value, String contextDescription) throws ExpressionEvaluationException { if (value instanceof ObjectReferenceType){ return (V) ((ObjectReferenceType) value).asReferenceValue(); } if (type.isAssignableFrom(value.getClass())) { return (V) new PrismPropertyValue<T>((T) value); } try { T resultValue = null; if (value instanceof String) { resultValue = XmlTypeConverter.toJavaValue((String) value, type); } else if (value instanceof Boolean) { resultValue = (T)value; } else if (value instanceof Element) { resultValue = XmlTypeConverter.convertValueElementAsScalar((Element) value, type); } else { throw new ExpressionEvaluationException("Unexpected scalar return type " + value.getClass().getName()); } if (returnType.equals(PrismConstants.POLYSTRING_TYPE_QNAME) && resultValue instanceof String) { resultValue = (T) new PolyString((String)resultValue); } PrismUtil.recomputeRealValue(resultValue, prismContext); return (V) new PrismPropertyValue<T>(resultValue); } catch (SchemaException e) { throw new ExpressionEvaluationException("Error converting result of " + contextDescription + ": " + e.getMessage(), e); } catch (IllegalArgumentException e) { throw new ExpressionEvaluationException("Error converting result of " + contextDescription + ": " + e.getMessage(), e); } } private <T, V extends PrismValue> List<V> convertList(Class<T> type, NodeList valueNodes, String contextDescription) throws ExpressionEvaluationException { List<V> values = new ArrayList<V>(); if (valueNodes == null) { return values; } try { List<T> list = XmlTypeConverter.convertValueElementAsList(valueNodes, type); for (T item : list) { if (item instanceof ObjectReferenceType){ values.add((V)((ObjectReferenceType) item).asReferenceValue()); } if (isNothing(item)) { continue; } values.add((V) new PrismPropertyValue<T>(item)); } return values; } catch (SchemaException e) { throw new ExpressionEvaluationException("Error converting return value of " + contextDescription + ": " + e.getMessage(), e); } catch (IllegalArgumentException e) { throw new ExpressionEvaluationException("Error converting return value of " + contextDescription + ": " + e.getMessage(), e); } } private <T> boolean isNothing(T value) { return value == null || ((value instanceof String) && ((String) value).isEmpty()); } private XPathFunctionResolver getFunctionResolver(Collection<FunctionLibrary> functions) { return new ReflectionXPathFunctionResolver(functions); } @Override public String getLanguageName() { return "XPath 2.0"; } @Override public String getLanguageUrl() { return XPATH_LANGUAGE_URL; } }