001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.scxml2.env.xpath; 018 019import java.io.Serializable; 020import java.util.ArrayList; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.jxpath.ClassFunctions; 026import org.apache.commons.jxpath.FunctionLibrary; 027import org.apache.commons.jxpath.Functions; 028import org.apache.commons.jxpath.JXPathContext; 029import org.apache.commons.jxpath.JXPathException; 030import org.apache.commons.jxpath.PackageFunctions; 031import org.apache.commons.jxpath.ri.model.NodePointer; 032import org.apache.commons.jxpath.ri.model.VariablePointer; 033import org.apache.commons.scxml2.Context; 034import org.apache.commons.scxml2.Evaluator; 035import org.apache.commons.scxml2.EvaluatorProvider; 036import org.apache.commons.scxml2.SCXMLExpressionException; 037import org.apache.commons.scxml2.env.EffectiveContextMap; 038import org.apache.commons.scxml2.model.SCXML; 039import org.w3c.dom.Attr; 040import org.w3c.dom.CharacterData; 041import org.w3c.dom.Element; 042import org.w3c.dom.Node; 043import org.w3c.dom.NodeList; 044 045/** 046 * <p>An {@link Evaluator} implementation for XPath environments.</p> 047 * 048 * <p>Does not support the <script> module, throws 049 * {@link UnsupportedOperationException} if attempted.</p> 050 */ 051public class XPathEvaluator implements Evaluator, Serializable { 052 053 /** Serial version UID. */ 054 private static final long serialVersionUID = -3578920670869493294L; 055 056 public static final String SUPPORTED_DATA_MODEL = Evaluator.XPATH_DATA_MODEL; 057 058 /** 059 * Internal 'marker' list used for collecting the NodePointer results of an {@link #evalLocation(Context, String)} 060 */ 061 private static class NodePointerList extends ArrayList<NodePointer> { 062 } 063 064 public static class XPathEvaluatorProvider implements EvaluatorProvider { 065 066 @Override 067 public String getSupportedDatamodel() { 068 return SUPPORTED_DATA_MODEL; 069 } 070 071 @Override 072 public Evaluator getEvaluator() { 073 return new XPathEvaluator(); 074 } 075 076 @Override 077 public Evaluator getEvaluator(final SCXML document) { 078 return new XPathEvaluator(); 079 } 080 } 081 082 private static final JXPathContext jxpathRootContext = JXPathContext.newContext(null); 083 084 static { 085 FunctionLibrary xpathFunctions = new FunctionLibrary(); 086 xpathFunctions.addFunctions(new ClassFunctions(XPathFunctions.class, null)); 087 // also restore default generic JXPath functions 088 xpathFunctions.addFunctions(new PackageFunctions("", null)); 089 jxpathRootContext.setFunctions(xpathFunctions); 090 } 091 092 private JXPathContext jxpathContext; 093 094 /** 095 * No argument constructor. 096 */ 097 public XPathEvaluator() { 098 jxpathContext = jxpathRootContext; 099 } 100 101 /** 102 * Constructor supporting user-defined JXPath {@link Functions}. 103 * 104 * @param functions The user-defined JXPath functions to use. 105 */ 106 public XPathEvaluator(final Functions functions) { 107 jxpathContext = JXPathContext.newContext(jxpathRootContext, null); 108 jxpathContext.setFunctions(functions); 109 } 110 111 @Override 112 public String getSupportedDatamodel() { 113 return SUPPORTED_DATA_MODEL; 114 } 115 116 /** 117 * @see Evaluator#eval(Context, String) 118 */ 119 @Override 120 public Object eval(final Context ctx, final String expr) 121 throws SCXMLExpressionException { 122 try { 123 List list = getContext(ctx).selectNodes(expr); 124 if (list.isEmpty()) { 125 return null; 126 } 127 else if (list.size() == 1) { 128 return list.get(0); 129 } 130 return list; 131 } catch (JXPathException xee) { 132 throw new SCXMLExpressionException(xee.getMessage(), xee); 133 } 134 } 135 136 /** 137 * @see Evaluator#evalCond(Context, String) 138 */ 139 @Override 140 public Boolean evalCond(final Context ctx, final String expr) 141 throws SCXMLExpressionException { 142 try { 143 return (Boolean)getContext(ctx).getValue(expr, Boolean.class); 144 } catch (JXPathException xee) { 145 throw new SCXMLExpressionException(xee.getMessage(), xee); 146 } 147 } 148 149 /** 150 * @see Evaluator#evalLocation(Context, String) 151 */ 152 @Override 153 public Object evalLocation(final Context ctx, final String expr) throws SCXMLExpressionException { 154 JXPathContext context = getContext(ctx); 155 try { 156 Iterator iterator = context.iteratePointers(expr); 157 Object pointer; 158 NodePointerList pointerList = null; 159 while (iterator.hasNext()) { 160 pointer = iterator.next(); 161 if (pointer != null && pointer instanceof NodePointer && ((NodePointer)pointer).getNode() != null) { 162 if (pointerList == null) { 163 pointerList = new NodePointerList(); 164 } 165 pointerList.add((NodePointer)pointer); 166 } 167 } 168 return pointerList; 169 } catch (JXPathException xee) { 170 throw new SCXMLExpressionException(xee.getMessage(), xee); 171 } 172 } 173 174 /** 175 * @see Evaluator#evalAssign(Context, String, Object, AssignType, String) 176 */ 177 public void evalAssign(final Context ctx, final String location, final Object data, final AssignType type, 178 final String attr) throws SCXMLExpressionException { 179 180 Object loc = evalLocation(ctx, location); 181 if (isXPathLocation(ctx, loc)) { 182 assign(ctx, loc, data, type, attr); 183 } 184 else { 185 throw new SCXMLExpressionException("evalAssign - cannot resolve location: '" + location + "'"); 186 } 187 } 188 189 /** 190 * @see Evaluator#evalScript(Context, String) 191 */ 192 public Object evalScript(Context ctx, String script) 193 throws SCXMLExpressionException { 194 throw new UnsupportedOperationException("Scripts are not supported by the XPathEvaluator"); 195 } 196 197 /** 198 * @see Evaluator#newContext(Context) 199 */ 200 @Override 201 public Context newContext(final Context parent) { 202 return new XPathContext(parent); 203 } 204 205 /** 206 * Determine if an {@link Evaluator#evalLocation(Context, String)} returned result represents an XPath location 207 * @param ctx variable context 208 * @param data result data from {@link Evaluator#evalLocation(Context, String)} 209 * @return true if the data represents an XPath location 210 */ 211 @SuppressWarnings("unused") 212 public boolean isXPathLocation(final Context ctx, Object data) { 213 return data instanceof NodePointerList; 214 } 215 216 /** 217 * Assigns data to a location 218 * 219 * @param ctx variable context 220 * @param location location expression 221 * @param data the data to assign. 222 * @param type the type of assignment to perform, null assumes {@link Evaluator.AssignType#REPLACE_CHILDREN} 223 * @param attr the name of the attribute to add when using type {@link Evaluator.AssignType#ADD_ATTRIBUTE} 224 * @throws SCXMLExpressionException A malformed expression exception 225 * @see Evaluator#evalAssign(Context, String, Object, Evaluator.AssignType, String) 226 */ 227 public void assign(final Context ctx, final Object location, final Object data, final AssignType type, 228 final String attr) throws SCXMLExpressionException { 229 if (!isXPathLocation(ctx, location)) { 230 throw new SCXMLExpressionException("assign requires a NodePointerList as location but is of type: " + 231 (location==null ? "(null)" : location.getClass().getName())); 232 } 233 for (NodePointer pointer : (NodePointerList)location) { 234 Object node = pointer.getNode(); 235 if (node != null) { 236 if (node instanceof Node) { 237 assign(ctx, (Node)node, pointer.asPath(), data, type != null ? type : AssignType.REPLACE_CHILDREN, attr); 238 } 239 else if (pointer instanceof VariablePointer) { 240 if (type == AssignType.DELETE) { 241 pointer.remove(); 242 } 243 VariablePointer vp = (VariablePointer)pointer; 244 Object variable = vp.getNode(); 245 if (variable instanceof Node) { 246 assign(ctx, (Node)variable, pointer.asPath(), data, type != null ? type : AssignType.REPLACE_CHILDREN, attr); 247 } 248 else if (type == null || type == AssignType.REPLACE) { 249 String variableName = vp.getName().getName(); 250 if (data instanceof CharacterData) { 251 ctx.set(variableName, ((CharacterData)data).getNodeValue()); 252 } 253 else { 254 ctx.set(variableName, data); 255 } 256 } 257 else { 258 throw new SCXMLExpressionException("Unsupported assign type +" + 259 type.name()+" for XPath variable "+pointer.asPath()); 260 } 261 } 262 else { 263 throw new SCXMLExpressionException("Unsupported XPath location pointer " + 264 pointer.getClass().getName()+" for location "+pointer.asPath()); 265 } 266 } 267 // else: silent ignore - NodePointerList should not have pointers without node 268 } 269 } 270 271 @SuppressWarnings("unused") 272 protected void assign(final Context ctx, final Node node, final String nodePath, final Object data, 273 final AssignType type, final String attr) throws SCXMLExpressionException { 274 275 if (type == AssignType.DELETE) { 276 node.getParentNode().removeChild(node); 277 } 278 else if (node instanceof Element) { 279 Element element = (Element)node; 280 if (type == AssignType.ADD_ATTRIBUTE) { 281 if (attr == null) { 282 throw new SCXMLExpressionException("Missing required attribute name for adding attribute at " + 283 nodePath); 284 } 285 if (data == null) { 286 throw new SCXMLExpressionException("Missing required data value for adding attribute " + 287 attr + " to location " + nodePath); 288 } 289 element.setAttribute(attr, data.toString()); 290 } 291 else { 292 Node dataNode = null; 293 if (type != AssignType.REPLACE_CHILDREN) { 294 if (data == null) { 295 throw new SCXMLExpressionException("Missing required data value for assign type "+type.name()); 296 } 297 dataNode = data instanceof Node 298 ? element.getOwnerDocument().importNode((Node)data, true) 299 : element.getOwnerDocument().createTextNode(data.toString()); 300 } 301 switch (type) { 302 case REPLACE_CHILDREN: 303 // quick way to delete all children 304 element.setTextContent(null); 305 if (data instanceof Node) { 306 element.appendChild(element.getOwnerDocument().importNode((Node)data, true)); 307 } 308 else if (data instanceof List) { 309 for (Object dataElement : (List)data) { 310 if (dataElement instanceof Node) { 311 element.appendChild(element.getOwnerDocument().importNode((Node)dataElement, true)); 312 } 313 else if (dataElement != null) { 314 element.appendChild(element.getOwnerDocument().createTextNode(dataElement.toString())); 315 } 316 } 317 } 318 else if (data instanceof NodeList) { 319 NodeList list = (NodeList)data; 320 for (int i = 0, size = list.getLength(); i < size; i++) 321 element.appendChild(element.getOwnerDocument().importNode(list.item(i), true)); 322 } 323 else { 324 element.appendChild(element.getOwnerDocument().createTextNode(data.toString())); 325 } 326 // else if data == null: already taken care of above 327 break; 328 case FIRST_CHILD: 329 element.insertBefore(dataNode, element.getFirstChild()); 330 break; 331 case LAST_CHILD: 332 element.appendChild(dataNode); 333 break; 334 case PREVIOUS_SIBLING: 335 element.getParentNode().insertBefore(dataNode, element); 336 break; 337 case NEXT_SIBLING: 338 element.getParentNode().insertBefore(dataNode, element.getNextSibling()); 339 break; 340 case REPLACE: 341 element.getParentNode().replaceChild(dataNode, element); 342 break; 343 } 344 } 345 } 346 else if (node instanceof CharacterData) { 347 if (type != AssignType.REPLACE) { 348 throw new SCXMLExpressionException("Assign type "+ type.name() + 349 " not supported for character data node at " + nodePath); 350 } 351 ((CharacterData)node).setData(data.toString()); 352 } 353 else if (node instanceof Attr) { 354 if (type != AssignType.REPLACE) { 355 throw new SCXMLExpressionException("Assign type "+ type.name() + 356 " not supported for node attribute at " + nodePath); 357 } 358 ((Attr)node).setValue(data.toString()); 359 } 360 else { 361 throw new SCXMLExpressionException("Unsupported assign location Node type "+node.getNodeType()); 362 } 363 } 364 365 366 @SuppressWarnings("unchecked") 367 protected JXPathContext getContext(final Context ctx) throws SCXMLExpressionException { 368 JXPathContext context = JXPathContext.newContext(jxpathContext, new EffectiveContextMap(ctx)); 369 context.setVariables(new ContextVariables(ctx)); 370 Map<String, String> namespaces = (Map<String, String>) ctx.get(Context.NAMESPACES_KEY); 371 if (namespaces != null) { 372 for (String prefix : namespaces.keySet()) { 373 context.registerNamespace(prefix, namespaces.get(prefix)); 374 } 375 } 376 return context; 377 } 378}