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.jexl2;
018
019 import java.io.BufferedReader;
020 import java.io.File;
021 import java.io.FileReader;
022 import java.io.IOException;
023 import java.io.InputStreamReader;
024 import java.io.StringReader;
025 import java.io.Reader;
026 import java.net.URL;
027 import java.net.URLConnection;
028 import java.lang.ref.SoftReference;
029 import java.util.ArrayList;
030 import java.util.Map;
031 import java.util.Set;
032 import java.util.Collections;
033 import java.util.LinkedHashMap;
034 import java.util.LinkedHashSet;
035 import java.util.List;
036 import java.util.Map.Entry;
037 import org.apache.commons.logging.Log;
038 import org.apache.commons.logging.LogFactory;
039
040 import org.apache.commons.jexl2.parser.ParseException;
041 import org.apache.commons.jexl2.parser.Parser;
042 import org.apache.commons.jexl2.parser.JexlNode;
043 import org.apache.commons.jexl2.parser.TokenMgrError;
044 import org.apache.commons.jexl2.parser.ASTJexlScript;
045
046 import org.apache.commons.jexl2.introspection.Uberspect;
047 import org.apache.commons.jexl2.introspection.UberspectImpl;
048 import org.apache.commons.jexl2.introspection.JexlMethod;
049 import org.apache.commons.jexl2.parser.ASTArrayAccess;
050 import org.apache.commons.jexl2.parser.ASTIdentifier;
051 import org.apache.commons.jexl2.parser.ASTReference;
052
053 /**
054 * <p>
055 * Creates and evaluates Expression and Script objects.
056 * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
057 * <ul>
058 * <li>Introspection, see {@link Uberspect}</li>
059 * <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
060 * <li>Error reporting</li>
061 * <li>Logging</li>
062 * </ul>
063 * </p>
064 * <p>The <code>setSilent</code> and <code>setLenient</code> methods allow to fine-tune an engine instance behavior
065 * according to various error control needs. The lenient/strict flag tells the engine when and if null as operand is
066 * considered an error, the silent/verbose flag tells the engine what to do with the error
067 * (log as warning or throw exception).
068 * </p>
069 * <ul>
070 * <li>When "silent" & "lenient":
071 * <p> 0 & null should be indicators of "default" values so that even in an case of error,
072 * something meaningfull can still be inferred; may be convenient for configurations.
073 * </p>
074 * </li>
075 * <li>When "silent" & "strict":
076 * <p>One should probably consider using null as an error case - ie, every object
077 * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
078 * can be used to workaround exceptional cases.
079 * Use case could be configuration with no implicit values or defaults.
080 * </p>
081 * </li>
082 * <li>When "verbose" & "lenient":
083 * <p>The error control grain is roughly on par with JEXL 1.0</p>
084 * </li>
085 * <li>When "verbose" & "strict":
086 * <p>The finest error control grain is obtained; it is the closest to Java code -
087 * still augmented by "script" capabilities regarding automated conversions & type matching.
088 * </p>
089 * </li>
090 * </ul>
091 * <p>
092 * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
093 * The {@link JexlException} are thrown in "non-silent" mode but since these are
094 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
095 * </p>
096 * @since 2.0
097 */
098 public class JexlEngine {
099 /**
100 * An empty/static/non-mutable JexlContext used instead of null context.
101 */
102 public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
103 /** {@inheritDoc} */
104 public Object get(String name) {
105 return null;
106 }
107
108 /** {@inheritDoc} */
109 public boolean has(String name) {
110 return false;
111 }
112
113 /** {@inheritDoc} */
114 public void set(String name, Object value) {
115 throw new UnsupportedOperationException("Not supported in void context.");
116 }
117 };
118
119 /**
120 * Gets the default instance of Uberspect.
121 * <p>This is lazily initialized to avoid building a default instance if there
122 * is no use for it. The main reason for not using the default Uberspect instance is to
123 * be able to use a (low level) introspector created with a given logger
124 * instead of the default one.</p>
125 * <p>Implemented as on demand holder idiom.</p>
126 */
127 private static final class UberspectHolder {
128 /** The default uberspector that handles all introspection patterns. */
129 private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
130
131 /** Non-instantiable. */
132 private UberspectHolder() {
133 }
134 }
135 /**
136 * The Uberspect instance.
137 */
138 protected final Uberspect uberspect;
139 /**
140 * The JexlArithmetic instance.
141 */
142 protected final JexlArithmetic arithmetic;
143 /**
144 * The Log to which all JexlEngine messages will be logged.
145 */
146 protected final Log logger;
147 /**
148 * The singleton ExpressionFactory also holds a single instance of
149 * {@link Parser}.
150 * When parsing expressions, ExpressionFactory synchronizes on Parser.
151 */
152 protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
153 /**
154 * Whether expressions evaluated by this engine will throw exceptions (false) or
155 * return null (true) on errors. Default is false.
156 */
157 // TODO could this be private?
158 protected volatile boolean silent = false;
159 /**
160 * Whether error messages will carry debugging information.
161 */
162 // TODO could this be private?
163 protected volatile boolean debug = true;
164 /**
165 * The map of 'prefix:function' to object implementing the functions.
166 */
167 // TODO this could probably be private; is it threadsafe?
168 protected Map<String, Object> functions = Collections.emptyMap();
169 /**
170 * The expression cache.
171 */
172 // TODO is this thread-safe? Could it be made private?
173 protected SoftCache<String, ASTJexlScript> cache = null;
174 /**
175 * The default cache load factor.
176 */
177 private static final float LOAD_FACTOR = 0.75f;
178
179 /**
180 * Creates an engine with default arguments.
181 */
182 public JexlEngine() {
183 this(null, null, null, null);
184 }
185
186 /**
187 * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
188 * a function map and logger.
189 * @param anUberspect to allow different introspection behaviour
190 * @param anArithmetic to allow different arithmetic behaviour
191 * @param theFunctions an optional map of functions (@link setFunctions)
192 * @param log the logger for various messages
193 */
194 public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
195 this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
196 if (log == null) {
197 log = LogFactory.getLog(JexlEngine.class);
198 }
199 this.logger = log;
200 this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
201 if (theFunctions != null) {
202 this.functions = theFunctions;
203 }
204 }
205
206 /**
207 * Gets the default instance of Uberspect.
208 * <p>This is lazily initialized to avoid building a default instance if there
209 * is no use for it. The main reason for not using the default Uberspect instance is to
210 * be able to use a (low level) introspector created with a given logger
211 * instead of the default one.</p>
212 * @param logger the logger to use for the underlying Uberspect
213 * @return Uberspect the default uberspector instance.
214 */
215 public static Uberspect getUberspect(Log logger) {
216 if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
217 return UberspectHolder.UBERSPECT;
218 }
219 return new UberspectImpl(logger);
220 }
221
222 /**
223 * Gets this engine underlying uberspect.
224 * @return the uberspect
225 */
226 public Uberspect getUberspect() {
227 return uberspect;
228 }
229
230 /**
231 * Gets this engine underlying arithmetic.
232 * @return the arithmetic
233 * @since 2.1
234 */
235 public JexlArithmetic getArithmetic() {
236 return arithmetic;
237 }
238
239 /**
240 * Sets whether this engine reports debugging information when error occurs.
241 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
242 * initialization code before expression creation & evaluation.</p>
243 * @see JexlEngine#setSilent
244 * @see JexlEngine#setLenient
245 * @param flag true implies debug is on, false implies debug is off.
246 */
247 public void setDebug(boolean flag) {
248 this.debug = flag;
249 }
250
251 /**
252 * Checks whether this engine is in debug mode.
253 * @return true if debug is on, false otherwise
254 */
255 public boolean isDebug() {
256 return this.debug;
257 }
258
259 /**
260 * Sets whether this engine throws JexlException during evaluation when an error is triggered.
261 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
262 * initialization code before expression creation & evaluation.</p>
263 * @see JexlEngine#setDebug
264 * @see JexlEngine#setLenient
265 * @param flag true means no JexlException will occur, false allows them
266 */
267 public void setSilent(boolean flag) {
268 this.silent = flag;
269 }
270
271 /**
272 * Checks whether this engine throws JexlException during evaluation.
273 * @return true if silent, false (default) otherwise
274 */
275 public boolean isSilent() {
276 return this.silent;
277 }
278
279 /**
280 * Sets whether this engine considers unknown variables, methods and constructors as errors or evaluates them
281 * as null or zero.
282 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
283 * initialization code before expression creation & evaluation.</p>
284 * <p>As of 2.1, you can use a JexlThreadedArithmetic instance to allow the JexlArithmetic
285 * leniency behavior to be independently specified per thread, whilst still using a single engine.</p>
286 * @see JexlEngine#setSilent
287 * @see JexlEngine#setDebug
288 * @param flag true means no JexlException will occur, false allows them
289 */
290 @SuppressWarnings("deprecation")
291 public void setLenient(boolean flag) {
292 if (arithmetic instanceof JexlThreadedArithmetic) {
293 JexlThreadedArithmetic.setLenient(Boolean.valueOf(flag));
294 } else {
295 this.arithmetic.setLenient(flag);
296 }
297 }
298
299 /**
300 * Checks whether this engine considers unknown variables, methods and constructors as errors.
301 * @return true if lenient, false if strict
302 */
303 public boolean isLenient() {
304 return arithmetic.isLenient();
305 }
306
307 /**
308 * Sets whether this engine behaves in strict or lenient mode.
309 * Equivalent to setLenient(!flag).
310 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
311 * initialization code before expression creation & evaluation.</p>
312 * @param flag true for strict, false for lenient
313 * @since 2.1
314 */
315 public final void setStrict(boolean flag) {
316 setLenient(!flag);
317 }
318
319 /**
320 * Checks whether this engine behaves in strict or lenient mode.
321 * Equivalent to !isLenient().
322 * @return true for strict, false for lenient
323 * @since 2.1
324 */
325 public final boolean isStrict() {
326 return !isLenient();
327 }
328
329 /**
330 * Sets the class loader used to discover classes in 'new' expressions.
331 * <p>This method should be called as an optional step of the JexlEngine
332 * initialization code before expression creation & evaluation.</p>
333 * @param loader the class loader to use
334 */
335 public void setClassLoader(ClassLoader loader) {
336 uberspect.setClassLoader(loader);
337 }
338
339 /**
340 * Sets a cache for expressions of the defined size.
341 * <p>The cache will contain at most <code>size</code> expressions. Note that
342 * all JEXL caches are held through SoftReferences and may be garbage-collected.</p>
343 * @param size if not strictly positive, no cache is used.
344 */
345 public void setCache(int size) {
346 // since the cache is only used during parse, use same sync object
347 synchronized (parser) {
348 if (size <= 0) {
349 cache = null;
350 } else if (cache == null || cache.size() != size) {
351 cache = new SoftCache<String, ASTJexlScript>(size);
352 }
353 }
354 }
355
356 /**
357 * Sets the map of function namespaces.
358 * <p>
359 * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
360 * initialization code before expression creation & evaluation.
361 * </p>
362 * <p>
363 * Each entry key is used as a prefix, each entry value used as a bean implementing
364 * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
365 * a registered bean named 'nsx' that implements method 'method' in that map.
366 * If all methods are static, you may use the bean class instead of an instance as value.
367 * </p>
368 * <p>
369 * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
370 * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
371 * to carry the information used by the namespace to avoid variable space pollution and strongly type
372 * the constructor with this specialized JexlContext.
373 * </p>
374 * <p>
375 * The key or prefix allows to retrieve the bean that plays the role of the namespace.
376 * If the prefix is null, the namespace is the top-level namespace allowing to define
377 * top-level user defined functions ( ie: myfunc(...) )
378 * </p>
379 * <p>Note that the JexlContext is also used to try to solve top-level functions. This allows ObjectContext
380 * derived instances to call methods on the wrapped object.</p>
381 * @param funcs the map of functions that should not mutate after the call; if null
382 * is passed, the empty collection is used.
383 */
384 public void setFunctions(Map<String, Object> funcs) {
385 functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
386 }
387
388 /**
389 * Retrieves the map of function namespaces.
390 *
391 * @return the map passed in setFunctions or the empty map if the
392 * original was null.
393 */
394 public Map<String, Object> getFunctions() {
395 return functions;
396 }
397
398 /**
399 * An overridable through covariant return Expression creator.
400 * @param text the script text
401 * @param tree the parse AST tree
402 * @return the script instance
403 */
404 protected Expression createExpression(ASTJexlScript tree, String text) {
405 return new ExpressionImpl(this, text, tree);
406 }
407
408 /**
409 * Creates an Expression from a String containing valid
410 * JEXL syntax. This method parses the expression which
411 * must contain either a reference or an expression.
412 * @param expression A String containing valid JEXL syntax
413 * @return An Expression object which can be evaluated with a JexlContext
414 * @throws JexlException An exception can be thrown if there is a problem
415 * parsing this expression, or if the expression is neither an
416 * expression nor a reference.
417 */
418 public Expression createExpression(String expression) {
419 return createExpression(expression, null);
420 }
421
422 /**
423 * Creates an Expression from a String containing valid
424 * JEXL syntax. This method parses the expression which
425 * must contain either a reference or an expression.
426 * @param expression A String containing valid JEXL syntax
427 * @return An Expression object which can be evaluated with a JexlContext
428 * @param info An info structure to carry debugging information if needed
429 * @throws JexlException An exception can be thrown if there is a problem
430 * parsing this expression, or if the expression is neither an
431 * expression or a reference.
432 */
433 public Expression createExpression(String expression, JexlInfo info) {
434 // Parse the expression
435 ASTJexlScript tree = parse(expression, info, null);
436 if (tree.jjtGetNumChildren() > 1) {
437 logger.warn("The JEXL Expression created will be a reference"
438 + " to the first expression from the supplied script: \"" + expression + "\" ");
439 }
440 return createExpression(tree, expression);
441 }
442
443 /**
444 * Creates a Script from a String containing valid JEXL syntax.
445 * This method parses the script which validates the syntax.
446 *
447 * @param scriptText A String containing valid JEXL syntax
448 * @return A {@link Script} which can be executed using a {@link JexlContext}.
449 * @throws JexlException if there is a problem parsing the script.
450 */
451 public Script createScript(String scriptText) {
452 return createScript(scriptText, null, null);
453 }
454
455 /**
456 * Creates a Script from a String containing valid JEXL syntax.
457 * This method parses the script which validates the syntax.
458 *
459 * @param scriptText A String containing valid JEXL syntax
460 * @param info An info structure to carry debugging information if needed
461 * @return A {@link Script} which can be executed using a {@link JexlContext}.
462 * @throws JexlException if there is a problem parsing the script.
463 * @deprecated Use {@link #createScript(String, JexlInfo, String[])}
464 */
465 @Deprecated
466 public Script createScript(String scriptText, JexlInfo info) {
467 if (scriptText == null) {
468 throw new NullPointerException("scriptText is null");
469 }
470 // Parse the expression
471 ASTJexlScript tree = parse(scriptText, info);
472 return createScript(tree, scriptText);
473 }
474
475 /**
476 * Creates a Script from a String containing valid JEXL syntax.
477 * This method parses the script which validates the syntax.
478 *
479 * @param scriptText A String containing valid JEXL syntax
480 * @param names the script parameter names
481 * @return A {@link Script} which can be executed using a {@link JexlContext}.
482 * @throws JexlException if there is a problem parsing the script.
483 */
484 public Script createScript(String scriptText, String... names) {
485 return createScript(scriptText, null, names);
486 }
487
488 /**
489 * Creates a Script from a String containing valid JEXL syntax.
490 * This method parses the script which validates the syntax.
491 * It uses an array of parameter names that will be resolved during parsing;
492 * a corresponding array of arguments containing values should be used during evaluation.
493 *
494 * @param scriptText A String containing valid JEXL syntax
495 * @param info An info structure to carry debugging information if needed
496 * @param names the script parameter names
497 * @return A {@link Script} which can be executed using a {@link JexlContext}.
498 * @throws JexlException if there is a problem parsing the script.
499 * @since 2.1
500 */
501 public Script createScript(String scriptText, JexlInfo info, String[] names) {
502 if (scriptText == null) {
503 throw new NullPointerException("scriptText is null");
504 }
505 // Parse the expression
506 ASTJexlScript tree = parse(scriptText, info, new Scope(names));
507 return createScript(tree, scriptText);
508 }
509
510 /**
511 * An overridable through covariant return Script creator.
512 * @param text the script text
513 * @param tree the parse AST tree
514 * @return the script instance
515 */
516 protected Script createScript(ASTJexlScript tree, String text) {
517 return new ExpressionImpl(this, text, tree);
518 }
519
520 /**
521 * Creates a Script from a {@link File} containing valid JEXL syntax.
522 * This method parses the script and validates the syntax.
523 *
524 * @param scriptFile A {@link File} containing valid JEXL syntax.
525 * Must not be null. Must be a readable file.
526 * @return A {@link Script} which can be executed with a
527 * {@link JexlContext}.
528 * @throws IOException if there is a problem reading the script.
529 * @throws JexlException if there is a problem parsing the script.
530 */
531 public Script createScript(File scriptFile) throws IOException {
532 if (scriptFile == null) {
533 throw new NullPointerException("scriptFile is null");
534 }
535 if (!scriptFile.canRead()) {
536 throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
537 }
538 BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
539 JexlInfo info = null;
540 if (debug) {
541 info = createInfo(scriptFile.getName(), 0, 0);
542 }
543 return createScript(readerToString(reader), info, null);
544 }
545
546 /**
547 * Creates a Script from a {@link URL} containing valid JEXL syntax.
548 * This method parses the script and validates the syntax.
549 *
550 * @param scriptUrl A {@link URL} containing valid JEXL syntax.
551 * Must not be null. Must be a readable file.
552 * @return A {@link Script} which can be executed with a
553 * {@link JexlContext}.
554 * @throws IOException if there is a problem reading the script.
555 * @throws JexlException if there is a problem parsing the script.
556 */
557 public Script createScript(URL scriptUrl) throws IOException {
558 if (scriptUrl == null) {
559 throw new NullPointerException("scriptUrl is null");
560 }
561 URLConnection connection = scriptUrl.openConnection();
562
563 BufferedReader reader = new BufferedReader(
564 new InputStreamReader(connection.getInputStream()));
565 JexlInfo info = null;
566 if (debug) {
567 info = createInfo(scriptUrl.toString(), 0, 0);
568 }
569 return createScript(readerToString(reader), info, null);
570 }
571
572 /**
573 * Accesses properties of a bean using an expression.
574 * <p>
575 * jexl.get(myobject, "foo.bar"); should equate to
576 * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
577 * </p>
578 * <p>
579 * If the JEXL engine is silent, errors will be logged through its logger as warning.
580 * </p>
581 * @param bean the bean to get properties from
582 * @param expr the property expression
583 * @return the value of the property
584 * @throws JexlException if there is an error parsing the expression or during evaluation
585 */
586 public Object getProperty(Object bean, String expr) {
587 return getProperty(null, bean, expr);
588 }
589
590 /**
591 * Accesses properties of a bean using an expression.
592 * <p>
593 * If the JEXL engine is silent, errors will be logged through its logger as warning.
594 * </p>
595 * @param context the evaluation context
596 * @param bean the bean to get properties from
597 * @param expr the property expression
598 * @return the value of the property
599 * @throws JexlException if there is an error parsing the expression or during evaluation
600 */
601 public Object getProperty(JexlContext context, Object bean, String expr) {
602 if (context == null) {
603 context = EMPTY_CONTEXT;
604 }
605 // synthetize expr using register
606 expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
607 try {
608 parser.ALLOW_REGISTERS = true;
609 Scope frame = new Scope("#0");
610 ASTJexlScript script = parse(expr, null, frame);
611 JexlNode node = script.jjtGetChild(0);
612 Interpreter interpreter = createInterpreter(context);
613 // set frame
614 interpreter.setFrame(script.createFrame(bean));
615 return node.jjtAccept(interpreter, null);
616 } catch (JexlException xjexl) {
617 if (silent) {
618 logger.warn(xjexl.getMessage(), xjexl.getCause());
619 return null;
620 }
621 throw xjexl;
622 } finally {
623 parser.ALLOW_REGISTERS = false;
624 }
625 }
626
627 /**
628 * Assign properties of a bean using an expression.
629 * <p>
630 * jexl.set(myobject, "foo.bar", 10); should equate to
631 * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
632 * </p>
633 * <p>
634 * If the JEXL engine is silent, errors will be logged through its logger as warning.
635 * </p>
636 * @param bean the bean to set properties in
637 * @param expr the property expression
638 * @param value the value of the property
639 * @throws JexlException if there is an error parsing the expression or during evaluation
640 */
641 public void setProperty(Object bean, String expr, Object value) {
642 setProperty(null, bean, expr, value);
643 }
644
645 /**
646 * Assign properties of a bean using an expression.
647 * <p>
648 * If the JEXL engine is silent, errors will be logged through its logger as warning.
649 * </p>
650 * @param context the evaluation context
651 * @param bean the bean to set properties in
652 * @param expr the property expression
653 * @param value the value of the property
654 * @throws JexlException if there is an error parsing the expression or during evaluation
655 */
656 public void setProperty(JexlContext context, Object bean, String expr, Object value) {
657 if (context == null) {
658 context = EMPTY_CONTEXT;
659 }
660 // synthetize expr using registers
661 expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + "#1" + ";";
662 try {
663 parser.ALLOW_REGISTERS = true;
664 Scope frame = new Scope("#0", "#1");
665 ASTJexlScript script = parse(expr, null, frame);
666 JexlNode node = script.jjtGetChild(0);
667 Interpreter interpreter = createInterpreter(context);
668 // set the registers
669 interpreter.setFrame(script.createFrame(bean, value));
670 node.jjtAccept(interpreter, null);
671 } catch (JexlException xjexl) {
672 if (silent) {
673 logger.warn(xjexl.getMessage(), xjexl.getCause());
674 return;
675 }
676 throw xjexl;
677 } finally {
678 parser.ALLOW_REGISTERS = false;
679 }
680 }
681
682 /**
683 * Invokes an object's method by name and arguments.
684 * @param obj the method's invoker object
685 * @param meth the method's name
686 * @param args the method's arguments
687 * @return the method returned value or null if it failed and engine is silent
688 * @throws JexlException if method could not be found or failed and engine is not silent
689 */
690 public Object invokeMethod(Object obj, String meth, Object... args) {
691 JexlException xjexl = null;
692 Object result = null;
693 JexlInfo info = debugInfo();
694 try {
695 JexlMethod method = uberspect.getMethod(obj, meth, args, info);
696 if (method == null && arithmetic.narrowArguments(args)) {
697 method = uberspect.getMethod(obj, meth, args, info);
698 }
699 if (method != null) {
700 result = method.invoke(obj, args);
701 } else {
702 xjexl = new JexlException(info, "failed finding method " + meth);
703 }
704 } catch (Exception xany) {
705 xjexl = new JexlException(info, "failed executing method " + meth, xany);
706 } finally {
707 if (xjexl != null) {
708 if (silent) {
709 logger.warn(xjexl.getMessage(), xjexl.getCause());
710 return null;
711 }
712 throw xjexl;
713 }
714 }
715 return result;
716 }
717
718 /**
719 * Creates a new instance of an object using the most appropriate constructor
720 * based on the arguments.
721 * @param <T> the type of object
722 * @param clazz the class to instantiate
723 * @param args the constructor arguments
724 * @return the created object instance or null on failure when silent
725 */
726 public <T> T newInstance(Class<? extends T> clazz, Object... args) {
727 return clazz.cast(doCreateInstance(clazz, args));
728 }
729
730 /**
731 * Creates a new instance of an object using the most appropriate constructor
732 * based on the arguments.
733 * @param clazz the name of the class to instantiate resolved through this engine's class loader
734 * @param args the constructor arguments
735 * @return the created object instance or null on failure when silent
736 */
737 public Object newInstance(String clazz, Object... args) {
738 return doCreateInstance(clazz, args);
739 }
740
741 /**
742 * Creates a new instance of an object using the most appropriate constructor
743 * based on the arguments.
744 * @param clazz the class to instantiate
745 * @param args the constructor arguments
746 * @return the created object instance or null on failure when silent
747 */
748 protected Object doCreateInstance(Object clazz, Object... args) {
749 JexlException xjexl = null;
750 Object result = null;
751 JexlInfo info = debugInfo();
752 try {
753 JexlMethod ctor = uberspect.getConstructorMethod(clazz, args, info);
754 if (ctor == null && arithmetic.narrowArguments(args)) {
755 ctor = uberspect.getConstructorMethod(clazz, args, info);
756 }
757 if (ctor != null) {
758 result = ctor.invoke(clazz, args);
759 } else {
760 xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
761 }
762 } catch (Exception xany) {
763 xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
764 } finally {
765 if (xjexl != null) {
766 if (silent) {
767 logger.warn(xjexl.getMessage(), xjexl.getCause());
768 return null;
769 }
770 throw xjexl;
771 }
772 }
773 return result;
774 }
775
776 /**
777 * Creates an interpreter.
778 * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
779 * @return an Interpreter
780 */
781 protected Interpreter createInterpreter(JexlContext context) {
782 return createInterpreter(context, isStrict(), isSilent());
783 }
784
785 /**
786 * Creates an interpreter.
787 * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
788 * @param strictFlag whether the interpreter runs in strict mode
789 * @param silentFlag whether the interpreter runs in silent mode
790 * @return an Interpreter
791 * @since 2.1
792 */
793 protected Interpreter createInterpreter(JexlContext context, boolean strictFlag, boolean silentFlag) {
794 return new Interpreter(this, context == null ? EMPTY_CONTEXT : context, strictFlag, silentFlag);
795 }
796
797 /**
798 * A soft reference on cache.
799 * <p>The cache is held through a soft reference, allowing it to be GCed under
800 * memory pressure.</p>
801 * @param <K> the cache key entry type
802 * @param <V> the cache key value type
803 */
804 protected class SoftCache<K, V> {
805 /**
806 * The cache size.
807 */
808 private final int size;
809 /**
810 * The soft reference to the cache map.
811 */
812 private SoftReference<Map<K, V>> ref = null;
813
814 /**
815 * Creates a new instance of a soft cache.
816 * @param theSize the cache size
817 */
818 SoftCache(int theSize) {
819 size = theSize;
820 }
821
822 /**
823 * Returns the cache size.
824 * @return the cache size
825 */
826 int size() {
827 return size;
828 }
829
830 /**
831 * Clears the cache.
832 */
833 void clear() {
834 ref = null;
835 }
836
837 /**
838 * Produces the cache entry set.
839 * @return the cache entry set
840 */
841 Set<Entry<K, V>> entrySet() {
842 Map<K, V> map = ref != null ? ref.get() : null;
843 return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
844 }
845
846 /**
847 * Gets a value from cache.
848 * @param key the cache entry key
849 * @return the cache entry value
850 */
851 V get(K key) {
852 final Map<K, V> map = ref != null ? ref.get() : null;
853 return map != null ? map.get(key) : null;
854 }
855
856 /**
857 * Puts a value in cache.
858 * @param key the cache entry key
859 * @param script the cache entry value
860 */
861 void put(K key, V script) {
862 Map<K, V> map = ref != null ? ref.get() : null;
863 if (map == null) {
864 map = createCache(size);
865 ref = new SoftReference<Map<K, V>>(map);
866 }
867 map.put(key, script);
868 }
869 }
870
871 /**
872 * Creates a cache.
873 * @param <K> the key type
874 * @param <V> the value type
875 * @param cacheSize the cache size, must be > 0
876 * @return a Map usable as a cache bounded to the given size
877 */
878 protected <K, V> Map<K, V> createCache(final int cacheSize) {
879 return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
880 /** Serial version UID. */
881 private static final long serialVersionUID = 1L;
882
883 @Override
884 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
885 return size() > cacheSize;
886 }
887 };
888 }
889
890 /**
891 * Clears the expression cache.
892 * @since 2.1
893 */
894 public void clearCache() {
895 synchronized (parser) {
896 cache.clear();
897 }
898 }
899
900 /**
901 * Gets the list of variables accessed by a script.
902 * <p>This method will visit all nodes of a script and extract all variables whether they
903 * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
904 * @param script the script
905 * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
906 * or the empty set if no variables are used
907 * @since 2.1
908 */
909 public Set<List<String>> getVariables(Script script) {
910 if (script instanceof ExpressionImpl) {
911 Set<List<String>> refs = new LinkedHashSet<List<String>>();
912 getVariables(((ExpressionImpl) script).script, refs, null);
913 return refs;
914 } else {
915 return Collections.<List<String>>emptySet();
916 }
917 }
918
919 /**
920 * Fills up the list of variables accessed by a node.
921 * @param node the node
922 * @param refs the set of variable being filled
923 * @param ref the current variable being filled
924 * @since 2.1
925 */
926 protected void getVariables(JexlNode node, Set<List<String>> refs, List<String> ref) {
927 boolean array = node instanceof ASTArrayAccess;
928 boolean reference = node instanceof ASTReference;
929 int num = node.jjtGetNumChildren();
930 if (array || reference) {
931 List<String> var = ref != null ? ref : new ArrayList<String>();
932 boolean varf = true;
933 for (int i = 0; i < num; ++i) {
934 JexlNode child = node.jjtGetChild(i);
935 if (array) {
936 if (child instanceof ASTReference && child.jjtGetNumChildren() == 1) {
937 JexlNode desc = child.jjtGetChild(0);
938 if (varf && desc.isConstant()) {
939 String image = desc.image;
940 if (image == null) {
941 var.add(new Debugger().data(desc));
942 } else {
943 var.add(image);
944 }
945 } else if (desc instanceof ASTIdentifier) {
946 if (((ASTIdentifier) desc).getRegister() < 0) {
947 List<String> di = new ArrayList<String>(1);
948 di.add(desc.image);
949 refs.add(di);
950 }
951 var = new ArrayList<String>();
952 varf = false;
953 }
954 continue;
955 } else if (child instanceof ASTIdentifier) {
956 if (i == 0 && (((ASTIdentifier) child).getRegister() < 0)) {
957 var.add(child.image);
958 }
959 continue;
960 }
961 } else {//if (reference) {
962 if (child instanceof ASTIdentifier) {
963 if (((ASTIdentifier) child).getRegister() < 0) {
964 var.add(child.image);
965 }
966 continue;
967 }
968 }
969 getVariables(child, refs, var);
970 }
971 if (!var.isEmpty() && var != ref) {
972 refs.add(var);
973 }
974 } else {
975 for (int i = 0; i < num; ++i) {
976 getVariables(node.jjtGetChild(i), refs, null);
977 }
978 }
979 }
980
981 /**
982 * Gets the array of parameters from a script.
983 * @param script the script
984 * @return the parameters which may be empty (but not null) if no parameters were defined
985 * @since 2.1
986 */
987 protected String[] getParameters(Script script) {
988 if (script instanceof ExpressionImpl) {
989 return ((ExpressionImpl) script).getParameters();
990 } else {
991 return new String[0];
992 }
993 }
994
995 /**
996 * Gets the array of local variable from a script.
997 * @param script the script
998 * @return the local variables array which may be empty (but not null) if no local variables were defined
999 * @since 2.1
1000 */
1001 protected String[] getLocalVariables(Script script) {
1002 if (script instanceof ExpressionImpl) {
1003 return ((ExpressionImpl) script).getLocalVariables();
1004 } else {
1005 return new String[0];
1006 }
1007 }
1008
1009 /**
1010 * A script scope, stores the declaration of parameters and local variables.
1011 * @since 2.1
1012 */
1013 public static final class Scope {
1014 /**
1015 * The number of parameters.
1016 */
1017 private final int parms;
1018 /**
1019 * The map of named registers aka script parameters.
1020 * Each parameter is associated to a register and is materialized as an offset in the registers array used
1021 * during evaluation.
1022 */
1023 private Map<String, Integer> namedRegisters = null;
1024
1025 /**
1026 * Creates a new scope with a list of parameters.
1027 * @param parameters the list of parameters
1028 */
1029 public Scope(String... parameters) {
1030 if (parameters != null) {
1031 parms = parameters.length;
1032 namedRegisters = new LinkedHashMap<String, Integer>();
1033 for (int p = 0; p < parms; ++p) {
1034 namedRegisters.put(parameters[p], Integer.valueOf(p));
1035 }
1036 } else {
1037 parms = 0;
1038 }
1039 }
1040
1041 @Override
1042 public int hashCode() {
1043 return namedRegisters == null ? 0 : parms ^ namedRegisters.hashCode();
1044 }
1045
1046 @Override
1047 public boolean equals(Object o) {
1048 return o instanceof Scope && equals((Scope) o);
1049 }
1050
1051 /**
1052 * Whether this frame is equal to another.
1053 * @param frame the frame to compare to
1054 * @return true if equal, false otherwise
1055 */
1056 public boolean equals(Scope frame) {
1057 if (this == frame) {
1058 return true;
1059 } else if (frame == null || parms != frame.parms) {
1060 return false;
1061 } else if (namedRegisters == null) {
1062 return frame.namedRegisters == null;
1063 } else {
1064 return namedRegisters.equals(frame.namedRegisters);
1065 }
1066 }
1067
1068 /**
1069 * Checks whether an identifier is a local variable or argument, ie stored in a register.
1070 * @param name the register name
1071 * @return the register index
1072 */
1073 public Integer getRegister(String name) {
1074 return namedRegisters != null ? namedRegisters.get(name) : null;
1075 }
1076
1077 /**
1078 * Declares a local variable.
1079 * <p>
1080 * This method creates an new entry in the named register map.
1081 * </p>
1082 * @param name the variable name
1083 * @return the register index storing this variable
1084 */
1085 public Integer declareVariable(String name) {
1086 if (namedRegisters == null) {
1087 namedRegisters = new LinkedHashMap<String, Integer>();
1088 }
1089 Integer register = namedRegisters.get(name);
1090 if (register == null) {
1091 register = Integer.valueOf(namedRegisters.size());
1092 namedRegisters.put(name, register);
1093 }
1094 return register;
1095 }
1096
1097 /**
1098 * Creates a frame by copying values up to the number of parameters.
1099 * @param values the argument values
1100 * @return the arguments array
1101 */
1102 public Frame createFrame(Object... values) {
1103 if (namedRegisters != null) {
1104 Object[] arguments = new Object[namedRegisters.size()];
1105 if (values != null) {
1106 System.arraycopy(values, 0, arguments, 0, Math.min(parms, values.length));
1107 }
1108 return new Frame(arguments, namedRegisters.keySet().toArray(new String[0]));
1109 } else {
1110 return null;
1111 }
1112 }
1113
1114 /**
1115 * Gets the (maximum) number of arguments this script expects.
1116 * @return the number of parameters
1117 */
1118 public int getArgCount() {
1119 return parms;
1120 }
1121
1122 /**
1123 * Gets this script registers, i.e. parameters and local variables.
1124 * @return the register names
1125 */
1126 public String[] getRegisters() {
1127 return namedRegisters != null ? namedRegisters.keySet().toArray(new String[0]) : new String[0];
1128 }
1129
1130 /**
1131 * Gets this script parameters, i.e. registers assigned before creating local variables.
1132 * @return the parameter names
1133 */
1134 public String[] getParameters() {
1135 if (namedRegisters != null && parms > 0) {
1136 String[] pa = new String[parms];
1137 int p = 0;
1138 for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
1139 if (entry.getValue().intValue() < parms) {
1140 pa[p++] = entry.getKey();
1141 }
1142 }
1143 return pa;
1144 } else {
1145 return null;
1146 }
1147 }
1148
1149 /**
1150 * Gets this script local variable, i.e. registers assigned to local variables.
1151 * @return the parameter names
1152 */
1153 public String[] getLocalVariables() {
1154 if (namedRegisters != null && parms > 0) {
1155 String[] pa = new String[parms];
1156 int p = 0;
1157 for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
1158 if (entry.getValue().intValue() >= parms) {
1159 pa[p++] = entry.getKey();
1160 }
1161 }
1162 return pa;
1163 } else {
1164 return null;
1165 }
1166 }
1167 }
1168
1169 /**
1170 * A call frame, created from a scope, stores the arguments and local variables as "registers".
1171 * @since 2.1
1172 */
1173 public static final class Frame {
1174 /** Registers or arguments. */
1175 private Object[] registers = null;
1176 /** Parameter and argument names if any. */
1177 private String[] parameters = null;
1178
1179 /**
1180 * Creates a new frame.
1181 * @param r the registers
1182 * @param p the parameters
1183 */
1184 Frame(Object[] r, String[] p) {
1185 registers = r;
1186 parameters = p;
1187 }
1188
1189 /**
1190 * @return the registers
1191 */
1192 public Object[] getRegisters() {
1193 return registers;
1194 }
1195
1196 /**
1197 * @return the parameters
1198 */
1199 public String[] getParameters() {
1200 return parameters;
1201 }
1202 }
1203
1204 /**
1205 * Parses an expression.
1206 * @param expression the expression to parse
1207 * @param info debug information structure
1208 * @return the parsed tree
1209 * @throws JexlException if any error occured during parsing
1210 * @deprecated Use {@link #parse(CharSequence, JexlInfo, Scope)} instead
1211 */
1212 @Deprecated
1213 protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
1214 return parse(expression, info, null);
1215 }
1216
1217 /**
1218 * Parses an expression.
1219 * @param expression the expression to parse
1220 * @param info debug information structure
1221 * @param frame the script frame to use
1222 * @return the parsed tree
1223 * @throws JexlException if any error occured during parsing
1224 */
1225 protected ASTJexlScript parse(CharSequence expression, JexlInfo info, Scope frame) {
1226 String expr = cleanExpression(expression);
1227 ASTJexlScript script = null;
1228 JexlInfo dbgInfo = null;
1229 synchronized (parser) {
1230 if (cache != null) {
1231 script = cache.get(expr);
1232 if (script != null) {
1233 Scope f = script.getScope();
1234 if ((f == null && frame == null) || (f != null && f.equals(frame))) {
1235 return script;
1236 }
1237 }
1238 }
1239 try {
1240 Reader reader = new StringReader(expr);
1241 // use first calling method of JexlEngine as debug info
1242 if (info == null) {
1243 dbgInfo = debugInfo();
1244 } else {
1245 dbgInfo = info.debugInfo();
1246 }
1247 parser.setFrame(frame);
1248 script = parser.parse(reader, dbgInfo);
1249 // reaccess in case local variables have been declared
1250 frame = parser.getFrame();
1251 if (frame != null) {
1252 script.setScope(frame);
1253 }
1254 if (cache != null) {
1255 cache.put(expr, script);
1256 }
1257 } catch (TokenMgrError xtme) {
1258 throw new JexlException.Tokenization(dbgInfo, expression, xtme);
1259 } catch (ParseException xparse) {
1260 throw new JexlException.Parsing(dbgInfo, expression, xparse);
1261 } finally {
1262 parser.setFrame(null);
1263 }
1264 }
1265 return script;
1266 }
1267
1268 /**
1269 * Creates a JexlInfo instance.
1270 * @param fn url/file name
1271 * @param l line number
1272 * @param c column number
1273 * @return a JexlInfo instance
1274 */
1275 protected JexlInfo createInfo(String fn, int l, int c) {
1276 return new DebugInfo(fn, l, c);
1277 }
1278
1279 /**
1280 * Creates and fills up debugging information.
1281 * <p>This gathers the class, method and line number of the first calling method
1282 * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
1283 * @return an Info if debug is set, null otherwise
1284 */
1285 protected JexlInfo debugInfo() {
1286 DebugInfo info = null;
1287 if (debug) {
1288 Throwable xinfo = new Throwable();
1289 xinfo.fillInStackTrace();
1290 StackTraceElement[] stack = xinfo.getStackTrace();
1291 StackTraceElement se = null;
1292 Class<?> clazz = getClass();
1293 for (int s = 1; s < stack.length; ++s, se = null) {
1294 se = stack[s];
1295 String className = se.getClassName();
1296 if (!className.equals(clazz.getName())) {
1297 // go deeper if called from JexlEngine or UnifiedJEXL
1298 if (className.equals(JexlEngine.class.getName())) {
1299 clazz = JexlEngine.class;
1300 } else if (className.equals(UnifiedJEXL.class.getName())) {
1301 clazz = UnifiedJEXL.class;
1302 } else {
1303 break;
1304 }
1305 }
1306 }
1307 if (se != null) {
1308 info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0).debugInfo();
1309 }
1310 }
1311 return info;
1312 }
1313
1314 /**
1315 * Trims the expression from front & ending spaces.
1316 * @param str expression to clean
1317 * @return trimmed expression ending in a semi-colon
1318 */
1319 public static String cleanExpression(CharSequence str) {
1320 if (str != null) {
1321 int start = 0;
1322 int end = str.length();
1323 if (end > 0) {
1324 // trim front spaces
1325 while (start < end && str.charAt(start) == ' ') {
1326 ++start;
1327 }
1328 // trim ending spaces
1329 while (end > 0 && str.charAt(end - 1) == ' ') {
1330 --end;
1331 }
1332 return str.subSequence(start, end).toString();
1333 }
1334 return "";
1335 }
1336 return null;
1337 }
1338
1339 /**
1340 * Read from a reader into a local buffer and return a String with
1341 * the contents of the reader.
1342 * @param scriptReader to be read.
1343 * @return the contents of the reader as a String.
1344 * @throws IOException on any error reading the reader.
1345 */
1346 public static String readerToString(Reader scriptReader) throws IOException {
1347 StringBuilder buffer = new StringBuilder();
1348 BufferedReader reader;
1349 if (scriptReader instanceof BufferedReader) {
1350 reader = (BufferedReader) scriptReader;
1351 } else {
1352 reader = new BufferedReader(scriptReader);
1353 }
1354 try {
1355 String line;
1356 while ((line = reader.readLine()) != null) {
1357 buffer.append(line).append('\n');
1358 }
1359 return buffer.toString();
1360 } finally {
1361 try {
1362 reader.close();
1363 } catch (IOException xio) {
1364 // ignore
1365 }
1366 }
1367
1368 }
1369 }