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     */
017    package org.apache.commons.scxml.env.jexl;
018    
019    import java.io.Serializable;
020    import java.util.ArrayList;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.regex.Pattern;
025    
026    import org.apache.commons.jexl.Expression;
027    import org.apache.commons.jexl.ExpressionFactory;
028    import org.apache.commons.scxml.Context;
029    import org.apache.commons.scxml.Evaluator;
030    import org.apache.commons.scxml.SCXMLExpressionException;
031    import org.w3c.dom.Node;
032    
033    /**
034     * Evaluator implementation enabling use of JEXL expressions in
035     * SCXML documents.
036     *
037     */
038    public class JexlEvaluator implements Evaluator, Serializable {
039    
040        /** Serial version UID. */
041        private static final long serialVersionUID = 1L;
042    
043        /** Error message if evaluation context is not a JexlContext. */
044        private static final String ERR_CTX_TYPE = "Error evaluating JEXL "
045            + "expression, Context must be a org.apache.commons.jexl.JexlContext";
046    
047        /** Pattern for recognizing the SCXML In() special predicate. */
048        private static Pattern inFct = Pattern.compile("In\\(");
049        /** Pattern for recognizing the Commons SCXML Data() builtin function. */
050        private static Pattern dataFct = Pattern.compile("Data\\(");
051    
052        /** Constructor. */
053        public JexlEvaluator() {
054            super();
055        }
056    
057        /**
058         * Evaluate an expression.
059         *
060         * @param ctx variable context
061         * @param expr expression
062         * @return a result of the evaluation
063         * @throws SCXMLExpressionException For a malformed expression
064         * @see Evaluator#eval(Context, String)
065         */
066        public Object eval(final Context ctx, final String expr)
067        throws SCXMLExpressionException {
068            if (expr == null) {
069                return null;
070            }
071            JexlContext jexlCtx = null;
072            if (ctx instanceof JexlContext) {
073                jexlCtx = (JexlContext) ctx;
074            } else {
075                throw new SCXMLExpressionException(ERR_CTX_TYPE);
076            }
077            Expression exp = null;
078            try {
079                String evalExpr = inFct.matcher(expr).
080                    replaceAll("_builtin.isMember(_ALL_STATES, ");
081                evalExpr = dataFct.matcher(evalExpr).
082                    replaceAll("_builtin.data(_ALL_NAMESPACES, ");
083                exp = ExpressionFactory.createExpression(evalExpr);
084                return exp.evaluate(getEffectiveContext(jexlCtx));
085            } catch (Exception e) {
086                throw new SCXMLExpressionException("eval('" + expr + "'):"
087                    + e.getMessage(), e);
088            }
089        }
090    
091        /**
092         * @see Evaluator#evalCond(Context, String)
093         */
094        public Boolean evalCond(final Context ctx, final String expr)
095        throws SCXMLExpressionException {
096            if (expr == null) {
097                return null;
098            }
099            JexlContext jexlCtx = null;
100            if (ctx instanceof JexlContext) {
101                jexlCtx = (JexlContext) ctx;
102            } else {
103                throw new SCXMLExpressionException(ERR_CTX_TYPE);
104            }
105            Expression exp = null;
106            try {
107                String evalExpr = inFct.matcher(expr).
108                    replaceAll("_builtin.isMember(_ALL_STATES, ");
109                evalExpr = dataFct.matcher(evalExpr).
110                    replaceAll("_builtin.data(_ALL_NAMESPACES, ");
111                exp = ExpressionFactory.createExpression(evalExpr);
112                return (Boolean) exp.evaluate(getEffectiveContext(jexlCtx));
113            } catch (Exception e) {
114                throw new SCXMLExpressionException("eval('" + expr + "'):"
115                    + e.getMessage(), e);
116            }
117        }
118    
119        /**
120         * @see Evaluator#evalLocation(Context, String)
121         */
122        public Node evalLocation(final Context ctx, final String expr)
123        throws SCXMLExpressionException {
124            if (expr == null) {
125                return null;
126            }
127            JexlContext jexlCtx = null;
128            if (ctx instanceof JexlContext) {
129                jexlCtx = (JexlContext) ctx;
130            } else {
131                throw new SCXMLExpressionException(ERR_CTX_TYPE);
132            }
133            Expression exp = null;
134            try {
135                String evalExpr = inFct.matcher(expr).
136                    replaceAll("_builtin.isMember(_ALL_STATES, ");
137                evalExpr = dataFct.matcher(evalExpr).
138                    replaceFirst("_builtin.dataNode(_ALL_NAMESPACES, ");
139                evalExpr = dataFct.matcher(evalExpr).
140                    replaceAll("_builtin.data(_ALL_NAMESPACES, ");
141                exp = ExpressionFactory.createExpression(evalExpr);
142                return (Node) exp.evaluate(getEffectiveContext(jexlCtx));
143            } catch (Exception e) {
144                throw new SCXMLExpressionException("eval('" + expr + "'):"
145                    + e.getMessage(), e);
146            }
147        }
148    
149        /**
150         * Create a new child context.
151         *
152         * @param parent parent context
153         * @return new child context
154         * @see Evaluator#newContext(Context)
155         */
156        public Context newContext(final Context parent) {
157            return new JexlContext(parent);
158        }
159    
160        /**
161         * Create a new context which is the summation of contexts from the
162         * current state to document root, child has priority over parent
163         * in scoping rules.
164         *
165         * @param nodeCtx The JexlContext for this state.
166         * @return The effective JexlContext for the path leading up to
167         *         document root.
168         */
169        private JexlContext getEffectiveContext(final JexlContext nodeCtx) {
170            List contexts = new ArrayList();
171            // trace path to root
172            JexlContext currentCtx = nodeCtx;
173            while (currentCtx != null) {
174                contexts.add(currentCtx);
175                currentCtx = (JexlContext) currentCtx.getParent();
176            }
177            Map vars = new HashMap();
178            // summation of the contexts, parent first, child wins
179            for (int i = contexts.size() - 1; i > -1; i--) {
180                vars.putAll(((JexlContext) contexts.get(i)).getVars());
181            }
182            return new JexlContext(vars);
183        }
184    
185    }
186