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 &lt;script&gt; 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}