View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.scxml2.env.xpath;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  
25  import org.apache.commons.jxpath.ClassFunctions;
26  import org.apache.commons.jxpath.FunctionLibrary;
27  import org.apache.commons.jxpath.Functions;
28  import org.apache.commons.jxpath.JXPathContext;
29  import org.apache.commons.jxpath.JXPathException;
30  import org.apache.commons.jxpath.PackageFunctions;
31  import org.apache.commons.jxpath.ri.model.NodePointer;
32  import org.apache.commons.jxpath.ri.model.VariablePointer;
33  import org.apache.commons.scxml2.Context;
34  import org.apache.commons.scxml2.Evaluator;
35  import org.apache.commons.scxml2.EvaluatorProvider;
36  import org.apache.commons.scxml2.SCXMLExpressionException;
37  import org.apache.commons.scxml2.env.EffectiveContextMap;
38  import org.apache.commons.scxml2.model.SCXML;
39  import org.w3c.dom.Attr;
40  import org.w3c.dom.CharacterData;
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  import org.w3c.dom.NodeList;
44  
45  /**
46   * <p>An {@link Evaluator} implementation for XPath environments.</p>
47   *
48   * <p>Does not support the &lt;script&gt; module, throws
49   * {@link UnsupportedOperationException} if attempted.</p>
50   */
51  public class XPathEvaluator implements Evaluator, Serializable {
52  
53      /** Serial version UID. */
54      private static final long serialVersionUID = -3578920670869493294L;
55  
56      public static final String SUPPORTED_DATA_MODEL = Evaluator.XPATH_DATA_MODEL;
57  
58      /**
59       * Internal 'marker' list used for collecting the NodePointer results of an {@link #evalLocation(Context, String)}
60       */
61      private static class NodePointerList extends ArrayList<NodePointer> {
62      }
63  
64      public static class XPathEvaluatorProvider implements EvaluatorProvider {
65  
66          @Override
67          public String getSupportedDatamodel() {
68              return SUPPORTED_DATA_MODEL;
69          }
70  
71          @Override
72          public Evaluator getEvaluator() {
73              return new XPathEvaluator();
74          }
75  
76          @Override
77          public Evaluator getEvaluator(final SCXML document) {
78              return new XPathEvaluator();
79          }
80      }
81  
82      private static final JXPathContext jxpathRootContext = JXPathContext.newContext(null);
83  
84      static {
85          FunctionLibrary xpathFunctions = new FunctionLibrary();
86          xpathFunctions.addFunctions(new ClassFunctions(XPathFunctions.class, null));
87          // also restore default generic JXPath functions
88          xpathFunctions.addFunctions(new PackageFunctions("", null));
89          jxpathRootContext.setFunctions(xpathFunctions);
90      }
91  
92      private JXPathContext jxpathContext;
93  
94      /**
95       * No argument constructor.
96       */
97      public XPathEvaluator() {
98          jxpathContext = jxpathRootContext;
99      }
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 }