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.jsp;
018    
019    import java.io.Serializable;
020    import java.lang.reflect.Method;
021    import java.util.Map;
022    import java.util.Set;
023    import java.util.regex.Pattern;
024    
025    import javax.servlet.jsp.el.ELException;
026    import javax.servlet.jsp.el.ExpressionEvaluator;
027    import javax.servlet.jsp.el.FunctionMapper;
028    import javax.servlet.jsp.el.VariableResolver;
029    
030    import org.apache.commons.el.ExpressionEvaluatorImpl;
031    import org.apache.commons.logging.Log;
032    import org.apache.commons.logging.LogFactory;
033    import org.apache.commons.scxml.Builtin;
034    import org.apache.commons.scxml.Context;
035    import org.apache.commons.scxml.Evaluator;
036    import org.apache.commons.scxml.SCXMLExpressionException;
037    import org.w3c.dom.Node;
038    
039    /**
040     * Evaluator implementation enabling use of EL expressions in
041     * SCXML documents.
042     *
043     */
044    public class ELEvaluator implements Evaluator, Serializable {
045    
046        /** Serial version UID. */
047        private static final long serialVersionUID = 1L;
048        /** Implementation independent log category. */
049        private Log log = LogFactory.getLog(Evaluator.class);
050        /** Function Mapper for SCXML builtin functions. */
051        private FunctionMapper builtinFnMapper = new BuiltinFunctionMapper();
052        /** User provided function mapper, we delegate to this mapper if
053            we encounter a function that is not built into SCXML. */
054        private FunctionMapper fnMapper;
055        /** Pattern for recognizing the SCXML In() special predicate. */
056        private static Pattern inFct = Pattern.compile("In\\(");
057        /** Pattern for recognizing the Commons SCXML Data() builtin function. */
058        private static Pattern dataFct = Pattern.compile("Data\\(");
059    
060        /** The expression evaluator implementation for the JSP/EL environment. */
061        private transient ExpressionEvaluator ee = null;
062    
063        /**
064         * Constructor.
065         */
066        public ELEvaluator() {
067            ee = new ExpressionEvaluatorImpl();
068        }
069    
070        /**
071         * Constructor for EL evaluator that supports user-defined functions.
072         *
073         * @param fnMapper The function mapper for this Evaluator.
074         * @see javax.servlet.jsp.el.FunctionMapper
075         */
076        public ELEvaluator(final FunctionMapper fnMapper) {
077            ee = new ExpressionEvaluatorImpl();
078            this.fnMapper = fnMapper;
079        }
080    
081        /**
082         * Evaluate an expression.
083         *
084         * @param ctx variable context
085         * @param expr expression
086         * @return a result of the evaluation
087         * @throws SCXMLExpressionException For a malformed expression
088         * @see Evaluator#eval(Context, String)
089         */
090        public Object eval(final Context ctx, final String expr)
091        throws SCXMLExpressionException {
092            if (expr == null) {
093                return null;
094            }
095            VariableResolver vr = null;
096            if (ctx instanceof VariableResolver) {
097                vr = (VariableResolver) ctx;
098            } else {
099                vr = new ContextWrapper(ctx);
100            }
101            try {
102                String evalExpr = inFct.matcher(expr).
103                    replaceAll("In(_ALL_STATES, ");
104                evalExpr = dataFct.matcher(evalExpr).
105                    replaceAll("Data(_ALL_NAMESPACES, ");
106                Object rslt = getEvaluator().evaluate(evalExpr, Object.class, vr,
107                    builtinFnMapper);
108                if (log.isTraceEnabled()) {
109                    log.trace(expr + " = " + String.valueOf(rslt));
110                }
111                return rslt;
112            } catch (ELException e) {
113                throw new SCXMLExpressionException("eval('" + expr + "'):"
114                    + e.getMessage(), e);
115            }
116        }
117    
118        /**
119         * @see Evaluator#evalCond(Context, String)
120         */
121        public Boolean evalCond(final Context ctx, final String expr)
122        throws SCXMLExpressionException {
123            if (expr == null) {
124                return null;
125            }
126            VariableResolver vr = null;
127            if (ctx instanceof VariableResolver) {
128                vr = (VariableResolver) ctx;
129            } else {
130                vr = new ContextWrapper(ctx);
131            }
132            try {
133                String evalExpr = inFct.matcher(expr).
134                    replaceAll("In(_ALL_STATES, ");
135                evalExpr = dataFct.matcher(evalExpr).
136                    replaceAll("Data(_ALL_NAMESPACES, ");
137                Boolean rslt = (Boolean) getEvaluator().evaluate(evalExpr,
138                    Boolean.class, vr, builtinFnMapper);
139                if (log.isDebugEnabled()) {
140                    log.debug(expr + " = " + String.valueOf(rslt));
141                }
142                return rslt;
143            } catch (ELException e) {
144                throw new SCXMLExpressionException("eval('" + expr + "'):"
145                    + e.getMessage(), e);
146            }
147        }
148    
149        /**
150         * @see Evaluator#evalLocation(Context, String)
151         */
152        public Node evalLocation(final Context ctx, final String expr)
153        throws SCXMLExpressionException {
154            if (expr == null) {
155                return null;
156            }
157            VariableResolver vr = null;
158            if (ctx instanceof VariableResolver) {
159                vr = (VariableResolver) ctx;
160            } else {
161                vr = new ContextWrapper(ctx);
162            }
163            try {
164                String evalExpr = inFct.matcher(expr).
165                    replaceAll("In(_ALL_STATES, ");
166                evalExpr = dataFct.matcher(evalExpr).
167                    replaceAll("Data(_ALL_NAMESPACES, ");
168                evalExpr = dataFct.matcher(evalExpr).
169                    replaceFirst("LData(");
170                Node rslt = (Node) getEvaluator().evaluate(evalExpr, Node.class,
171                    vr, builtinFnMapper);
172                if (log.isDebugEnabled()) {
173                    log.debug(expr + " = " + String.valueOf(rslt));
174                }
175                return rslt;
176            } catch (ELException e) {
177                throw new SCXMLExpressionException("eval('" + expr + "'):"
178                    + e.getMessage(), e);
179            }
180        }
181    
182        /**
183         * Create a new child context.
184         *
185         * @param parent parent context
186         * @return new child context
187         * @see Evaluator#newContext(Context)
188         */
189        public Context newContext(final Context parent) {
190            return new ELContext(parent);
191        }
192    
193        /**
194         * Set the log used by this <code>Evaluator</code> instance.
195         *
196         * @param log The new log.
197         */
198        protected void setLog(final Log log) {
199            this.log = log;
200        }
201    
202        /**
203         * Get the log used by this <code>Evaluator</code> instance.
204         *
205         * @return Log The log being used.
206         */
207        protected Log getLog() {
208            return log;
209        }
210    
211        /**
212         * Get the <code>ExpressionEvaluator</code>, with lazy initialization.
213         *
214         * @return Log The log being used.
215         */
216        private ExpressionEvaluator getEvaluator() {
217            if (ee == null) {
218                ee = new ExpressionEvaluatorImpl();
219            }
220            return ee;
221        }
222    
223        /**
224         * A Context wrapper that implements VariableResolver.
225         */
226        static class ContextWrapper implements VariableResolver, Serializable {
227            /** Serial version UID. */
228            private static final long serialVersionUID = 1L;
229            /** Context to be wrapped. */
230            private Context ctx = null;
231            /** The log. */
232            private Log log = LogFactory.getLog(ContextWrapper.class);
233            /**
234             * Constructor.
235             * @param ctx The Context to be wrapped.
236             */
237            ContextWrapper(final Context ctx) {
238                this.ctx = ctx;
239            }
240            /** @see VariableResolver#resolveVariable(String) */
241            public Object resolveVariable(final String pName) throws ELException {
242                Object rslt = ctx.get(pName);
243                if (rslt == null) {
244                    log.info("Variable \"" + pName + "\" does not exist!");
245                }
246                return rslt;
247            }
248        }
249    
250        /**
251         * A simple function mapper for SCXML defined functions.
252         */
253        class BuiltinFunctionMapper implements FunctionMapper, Serializable {
254            /** Serial version UID. */
255            private static final long serialVersionUID = 1L;
256            /** The log. */
257            private Log log = LogFactory.getLog(BuiltinFunctionMapper.class);
258            /**
259             * @see FunctionMapper#resolveFunction(String, String)
260             */
261            public Method resolveFunction(final String prefix,
262                    final String localName) {
263                if (localName.equals("In")) {
264                    Class[] attrs = new Class[] {Set.class, String.class};
265                    try {
266                        return Builtin.class.getMethod("isMember", attrs);
267                    } catch (SecurityException e) {
268                        log.error("resolving isMember(Set, String)", e);
269                    } catch (NoSuchMethodException e) {
270                        log.error("resolving isMember(Set, String)", e);
271                    }
272                } else if (localName.equals("Data")) {
273                    // rvalue in expressions, coerce to String
274                    Class[] attrs =
275                        new Class[] {Map.class, Object.class, String.class};
276                    try {
277                        return Builtin.class.getMethod("data", attrs);
278                    } catch (SecurityException e) {
279                        log.error("resolving data(Node, String)", e);
280                    } catch (NoSuchMethodException e) {
281                        log.error("resolving data(Node, String)", e);
282                    }
283                } else if (localName.equals("LData")) {
284                    // lvalue in expressions, retain as Node
285                    Class[] attrs =
286                        new Class[] {Map.class, Object.class, String.class};
287                    try {
288                        return Builtin.class.getMethod("dataNode", attrs);
289                    } catch (SecurityException e) {
290                        log.error("resolving data(Node, String)", e);
291                    } catch (NoSuchMethodException e) {
292                        log.error("resolving data(Node, String)", e);
293                    }
294                } else if (fnMapper != null) {
295                    return fnMapper.resolveFunction(prefix, localName);
296                }
297                return null;
298            }
299        }
300    
301        /**
302         * Get the FunctionMapper for builtin SCXML/Commons SCXML functions.
303         *
304         * @return builtinFnMapper The FunctionMapper
305         */
306        protected FunctionMapper getBuiltinFnMapper() {
307            return builtinFnMapper;
308        }
309    
310    }
311