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.lang.reflect.Constructor;
030 import java.util.Map;
031 import java.util.Set;
032 import java.util.Collections;
033 import java.util.Map.Entry;
034 import org.apache.commons.logging.Log;
035 import org.apache.commons.logging.LogFactory;
036
037 import org.apache.commons.jexl2.parser.ParseException;
038 import org.apache.commons.jexl2.parser.Parser;
039 import org.apache.commons.jexl2.parser.JexlNode;
040 import org.apache.commons.jexl2.parser.TokenMgrError;
041 import org.apache.commons.jexl2.parser.ASTJexlScript;
042
043 import org.apache.commons.jexl2.introspection.Uberspect;
044 import org.apache.commons.jexl2.introspection.UberspectImpl;
045 import org.apache.commons.jexl2.introspection.JexlMethod;
046
047 /**
048 * <p>
049 * Creates and evaluates Expression and Script objects.
050 * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
051 * <ul>
052 * <li>Introspection, see {@link Uberspect}</li>
053 * <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
054 * <li>Error reporting</li>
055 * <li>Logging</li>
056 * </ul>
057 * </p>
058 * <p>The <code>setSilent</code> and <code>setLenient</code> methods allow to fine-tune an engine instance behavior
059 * according to various error control needs. The lenient/strict flag tells the engine when and if null as operand is
060 * considered an error, the silent/verbose flag tells the engine what to do with the error
061 * (log as warning or throw exception).
062 * </p>
063 * <ul>
064 * <li>When "silent" & "lenient":
065 * <p> 0 & null should be indicators of "default" values so that even in an case of error,
066 * something meaningfull can still be inferred; may be convenient for configurations.
067 * </p>
068 * </li>
069 * <li>When "silent" & "strict":
070 * <p>One should probably consider using null as an error case - ie, every object
071 * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
072 * can be used to workaround exceptional cases.
073 * Use case could be configuration with no implicit values or defaults.
074 * </p>
075 * </li>
076 * <li>When "verbose" & "lenient":
077 * <p>The error control grain is roughly on par with JEXL 1.0</p>
078 * </li>
079 * <li>When "verbose" & "strict":
080 * <p>The finest error control grain is obtained; it is the closest to Java code -
081 * still augmented by "script" capabilities regarding automated conversions & type matching.
082 * </p>
083 * </li>
084 * </ul>
085 * <p>
086 * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
087 * The {@link JexlException} are thrown in "non-silent" mode but since these are
088 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
089 * </p>
090 * @since 2.0
091 */
092 public class JexlEngine {
093 /**
094 * An empty/static/non-mutable JexlContext used instead of null context.
095 */
096 public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
097 /** {@inheritDoc} */
098 public Object get(String name) {
099 return null;
100 }
101 /** {@inheritDoc} */
102 public boolean has(String name) {
103 return false;
104 }
105 /** {@inheritDoc} */
106 public void set(String name, Object value) {
107 throw new UnsupportedOperationException("Not supported in void context.");
108 }
109 };
110
111 /**
112 * Gets the default instance of Uberspect.
113 * <p>This is lazily initialized to avoid building a default instance if there
114 * is no use for it. The main reason for not using the default Uberspect instance is to
115 * be able to use a (low level) introspector created with a given logger
116 * instead of the default one.</p>
117 * <p>Implemented as on demand holder idiom.</p>
118 */
119 private static final class UberspectHolder {
120 /** The default uberspector that handles all introspection patterns. */
121 private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
122 /** Non-instantiable. */
123 private UberspectHolder() {}
124 }
125
126 /**
127 * The Uberspect instance.
128 */
129 protected final Uberspect uberspect;
130 /**
131 * The JexlArithmetic instance.
132 */
133 protected final JexlArithmetic arithmetic;
134 /**
135 * The Log to which all JexlEngine messages will be logged.
136 */
137 protected final Log logger;
138 /**
139 * The singleton ExpressionFactory also holds a single instance of
140 * {@link Parser}.
141 * When parsing expressions, ExpressionFactory synchronizes on Parser.
142 */
143 protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
144 /**
145 * Whether expressions evaluated by this engine will throw exceptions (false) or
146 * return null (true). Default is false.
147 */
148 protected boolean silent = false;
149 /**
150 * Whether error messages will carry debugging information.
151 */
152 protected boolean debug = true;
153 /**
154 * The map of 'prefix:function' to object implementing the function.
155 */
156 protected Map<String, Object> functions = Collections.emptyMap();
157 /**
158 * The expression cache.
159 */
160 protected SoftCache<String, ASTJexlScript> cache = null;
161 /**
162 * The default cache load factor.
163 */
164 private static final float LOAD_FACTOR = 0.75f;
165
166 /**
167 * Creates an engine with default arguments.
168 */
169 public JexlEngine() {
170 this(null, null, null, null);
171 }
172
173 /**
174 * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
175 * a function map and logger.
176 * @param anUberspect to allow different introspection behaviour
177 * @param anArithmetic to allow different arithmetic behaviour
178 * @param theFunctions an optional map of functions (@link setFunctions)
179 * @param log the logger for various messages
180 */
181 public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
182 this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
183 if (log == null) {
184 log = LogFactory.getLog(JexlEngine.class);
185 }
186 this.logger = log;
187 this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
188 if (theFunctions != null) {
189 this.functions = theFunctions;
190 }
191 }
192
193
194 /**
195 * Gets the default instance of Uberspect.
196 * <p>This is lazily initialized to avoid building a default instance if there
197 * is no use for it. The main reason for not using the default Uberspect instance is to
198 * be able to use a (low level) introspector created with a given logger
199 * instead of the default one.</p>
200 * @param logger the logger to use for the underlying Uberspect
201 * @return Uberspect the default uberspector instance.
202 */
203 public static Uberspect getUberspect(Log logger) {
204 if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
205 return UberspectHolder.UBERSPECT;
206 }
207 return new UberspectImpl(logger);
208 }
209
210 /**
211 * Gets this engine underlying uberspect.
212 * @return the uberspect
213 */
214 public Uberspect getUberspect() {
215 return uberspect;
216 }
217
218 /**
219 * Sets whether this engine reports debugging information when error occurs.
220 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
221 * initialization code before expression creation & evaluation.</p>
222 * @see JexlEngine#setSilent
223 * @see JexlEngine#setLenient
224 * @param flag true implies debug is on, false implies debug is off.
225 */
226 public void setDebug(boolean flag) {
227 this.debug = flag;
228 }
229
230 /**
231 * Checks whether this engine is in debug mode.
232 * @return true if debug is on, false otherwise
233 */
234 public boolean isDebug() {
235 return this.debug;
236 }
237
238 /**
239 * Sets whether this engine throws JexlException during evaluation when an error is triggered.
240 * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
241 * initialization code before expression creation & evaluation.</p>
242 * @see JexlEngine#setDebug
243 * @see JexlEngine#setLenient
244 * @param flag true means no JexlException will occur, false allows them
245 */
246 public void setSilent(boolean flag) {
247 this.silent = flag;
248 }
249
250 /**
251 * Checks whether this engine throws JexlException during evaluation.
252 * @return true if silent, false (default) otherwise
253 */
254 public boolean isSilent() {
255 return this.silent;
256 }
257
258 /**
259 * Sets whether this engine triggers errors during evaluation when null is used as
260 * an operand.
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#setSilent
264 * @see JexlEngine#setDebug
265 * @param flag true means no JexlException will occur, false allows them
266 */
267 public void setLenient(boolean flag) {
268 this.arithmetic.setLenient(flag);
269 }
270
271 /**
272 * Checks whether this engine triggers errors during evaluation when null is used as
273 * an operand.
274 * @return true if lenient, false if strict
275 */
276 public boolean isLenient() {
277 return this.arithmetic.isLenient();
278 }
279
280 /**
281 * Sets the class loader used to discover classes in 'new' expressions.
282 * <p>This method should be called as an optional step of the JexlEngine
283 * initialization code before expression creation & evaluation.</p>
284 * @param loader the class loader to use
285 */
286 public void setClassLoader(ClassLoader loader) {
287 uberspect.setClassLoader(loader);
288 }
289
290 /**
291 * Sets a cache for expressions of the defined size.
292 * <p>The cache will contain at most <code>size</code> expressions. Note that
293 * all JEXL caches are held through SoftReferences and may be garbage-collected.</p>
294 * @param size if not strictly positive, no cache is used.
295 */
296 public void setCache(int size) {
297 // since the cache is only used during parse, use same sync object
298 synchronized (parser) {
299 if (size <= 0) {
300 cache = null;
301 } else if (cache == null || cache.size() != size) {
302 cache = new SoftCache<String, ASTJexlScript>(size);
303 }
304 }
305 }
306
307 /**
308 * Sets the map of function namespaces.
309 * <p>
310 * 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.
312 * </p>
313 * <p>
314 * Each entry key is used as a prefix, each entry value used as a bean implementing
315 * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
316 * a registered bean named 'nsx' that implements method 'method' in that map.
317 * If all methods are static, you may use the bean class instead of an instance as value.
318 * </p>
319 * <p>
320 * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
321 * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
322 * to carry the information used by the namespace to avoid variable space pollution and strongly type
323 * the constructor with this specialized JexlContext.
324 * </p>
325 * <p>
326 * The key or prefix allows to retrieve the bean that plays the role of the namespace.
327 * If the prefix is null, the namespace is the top-level namespace allowing to define
328 * top-level user defined functions ( ie: myfunc(...) )
329 * </p>
330 * @param funcs the map of functions that should not mutate after the call; if null
331 * is passed, the empty collection is used.
332 */
333 public void setFunctions(Map<String, Object> funcs) {
334 functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
335 }
336
337 /**
338 * Retrieves the map of function namespaces.
339 *
340 * @return the map passed in setFunctions or the empty map if the
341 * original was null.
342 */
343 public Map<String, Object> getFunctions() {
344 return functions;
345 }
346
347 /**
348 * An overridable through covariant return Expression creator.
349 * @param text the script text
350 * @param tree the parse AST tree
351 * @return the script instance
352 */
353 protected Expression createExpression(ASTJexlScript tree, String text) {
354 return new ExpressionImpl(this, text, tree);
355 }
356
357 /**
358 * Creates an Expression from a String containing valid
359 * JEXL syntax. This method parses the expression which
360 * must contain either a reference or an expression.
361 * @param expression A String containing valid JEXL syntax
362 * @return An Expression object which can be evaluated with a JexlContext
363 * @throws JexlException An exception can be thrown if there is a problem
364 * parsing this expression, or if the expression is neither an
365 * expression nor a reference.
366 */
367 public Expression createExpression(String expression) {
368 return createExpression(expression, null);
369 }
370
371 /**
372 * Creates an Expression from a String containing valid
373 * JEXL syntax. This method parses the expression which
374 * must contain either a reference or an expression.
375 * @param expression A String containing valid JEXL syntax
376 * @return An Expression object which can be evaluated with a JexlContext
377 * @param info An info structure to carry debugging information if needed
378 * @throws JexlException An exception can be thrown if there is a problem
379 * parsing this expression, or if the expression is neither an
380 * expression or a reference.
381 */
382 public Expression createExpression(String expression, JexlInfo info) {
383 // Parse the expression
384 ASTJexlScript tree = parse(expression, info);
385 if (tree.jjtGetNumChildren() > 1) {
386 logger.warn("The JEXL Expression created will be a reference"
387 + " to the first expression from the supplied script: \"" + expression + "\" ");
388 }
389 return createExpression(tree, expression);
390 }
391
392 /**
393 * Creates a Script from a String containing valid JEXL syntax.
394 * This method parses the script which validates the syntax.
395 *
396 * @param scriptText A String containing valid JEXL syntax
397 * @return A {@link Script} which can be executed using a {@link JexlContext}.
398 * @throws JexlException if there is a problem parsing the script.
399 */
400 public Script createScript(String scriptText) {
401 return createScript(scriptText, null);
402 }
403
404 /**
405 * Creates a Script from a String containing valid JEXL syntax.
406 * This method parses the script which validates the syntax.
407 *
408 * @param scriptText A String containing valid JEXL syntax
409 * @param info An info structure to carry debugging information if needed
410 * @return A {@link Script} which can be executed using a {@link JexlContext}.
411 * @throws JexlException if there is a problem parsing the script.
412 */
413 public Script createScript(String scriptText, JexlInfo info) {
414 if (scriptText == null) {
415 throw new NullPointerException("scriptText is null");
416 }
417 // Parse the expression
418 ASTJexlScript tree = parse(scriptText, info);
419 return createScript(tree, scriptText);
420 }
421
422 /**
423 * An overridable through covariant return Script creator.
424 * @param text the script text
425 * @param tree the parse AST tree
426 * @return the script instance
427 */
428 protected Script createScript(ASTJexlScript tree, String text) {
429 return new ExpressionImpl(this, text, tree);
430 }
431
432 /**
433 * Creates a Script from a {@link File} containing valid JEXL syntax.
434 * This method parses the script and validates the syntax.
435 *
436 * @param scriptFile A {@link File} containing valid JEXL syntax.
437 * Must not be null. Must be a readable file.
438 * @return A {@link Script} which can be executed with a
439 * {@link JexlContext}.
440 * @throws IOException if there is a problem reading the script.
441 * @throws JexlException if there is a problem parsing the script.
442 */
443 public Script createScript(File scriptFile) throws IOException {
444 if (scriptFile == null) {
445 throw new NullPointerException("scriptFile is null");
446 }
447 if (!scriptFile.canRead()) {
448 throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
449 }
450 BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
451 JexlInfo info = null;
452 if (debug) {
453 info = createInfo(scriptFile.getName(), 0, 0);
454 }
455 return createScript(readerToString(reader), info);
456 }
457
458 /**
459 * Creates a Script from a {@link URL} containing valid JEXL syntax.
460 * This method parses the script and validates the syntax.
461 *
462 * @param scriptUrl A {@link URL} containing valid JEXL syntax.
463 * Must not be null. Must be a readable file.
464 * @return A {@link Script} which can be executed with a
465 * {@link JexlContext}.
466 * @throws IOException if there is a problem reading the script.
467 * @throws JexlException if there is a problem parsing the script.
468 */
469 public Script createScript(URL scriptUrl) throws IOException {
470 if (scriptUrl == null) {
471 throw new NullPointerException("scriptUrl is null");
472 }
473 URLConnection connection = scriptUrl.openConnection();
474
475 BufferedReader reader = new BufferedReader(
476 new InputStreamReader(connection.getInputStream()));
477 JexlInfo info = null;
478 if (debug) {
479 info = createInfo(scriptUrl.toString(), 0, 0);
480 }
481 return createScript(readerToString(reader), info);
482 }
483
484 /**
485 * Accesses properties of a bean using an expression.
486 * <p>
487 * jexl.get(myobject, "foo.bar"); should equate to
488 * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
489 * </p>
490 * <p>
491 * If the JEXL engine is silent, errors will be logged through its logger as warning.
492 * </p>
493 * @param bean the bean to get properties from
494 * @param expr the property expression
495 * @return the value of the property
496 * @throws JexlException if there is an error parsing the expression or during evaluation
497 */
498 public Object getProperty(Object bean, String expr) {
499 return getProperty(null, bean, expr);
500 }
501
502 /**
503 * Accesses properties of a bean using an expression.
504 * <p>
505 * If the JEXL engine is silent, errors will be logged through its logger as warning.
506 * </p>
507 * @param context the evaluation context
508 * @param bean the bean to get properties from
509 * @param expr the property expression
510 * @return the value of the property
511 * @throws JexlException if there is an error parsing the expression or during evaluation
512 */
513 public Object getProperty(JexlContext context, Object bean, String expr) {
514 if (context == null) {
515 context = EMPTY_CONTEXT;
516 }
517 // synthetize expr using register
518 expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
519 try {
520 parser.ALLOW_REGISTERS = true;
521 JexlNode tree = parse(expr, null);
522 JexlNode node = tree.jjtGetChild(0);
523 Interpreter interpreter = createInterpreter(context);
524 // set register
525 interpreter.setRegisters(bean);
526 return node.jjtAccept(interpreter, null);
527 } catch (JexlException xjexl) {
528 if (silent) {
529 logger.warn(xjexl.getMessage(), xjexl.getCause());
530 return null;
531 }
532 throw xjexl;
533 } finally {
534 parser.ALLOW_REGISTERS = false;
535 }
536 }
537
538 /**
539 * Assign properties of a bean using an expression.
540 * <p>
541 * jexl.set(myobject, "foo.bar", 10); should equate to
542 * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
543 * </p>
544 * <p>
545 * If the JEXL engine is silent, errors will be logged through its logger as warning.
546 * </p>
547 * @param bean the bean to set properties in
548 * @param expr the property expression
549 * @param value the value of the property
550 * @throws JexlException if there is an error parsing the expression or during evaluation
551 */
552 public void setProperty(Object bean, String expr, Object value) {
553 setProperty(null, bean, expr, value);
554 }
555
556 /**
557 * Assign properties of a bean using an expression.
558 * <p>
559 * If the JEXL engine is silent, errors will be logged through its logger as warning.
560 * </p>
561 * @param context the evaluation context
562 * @param bean the bean to set properties in
563 * @param expr the property expression
564 * @param value the value of the property
565 * @throws JexlException if there is an error parsing the expression or during evaluation
566 */
567 public void setProperty(JexlContext context, Object bean, String expr, Object value) {
568 if (context == null) {
569 context = EMPTY_CONTEXT;
570 }
571 // synthetize expr using registers
572 expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + "#1" + ";";
573 try {
574 parser.ALLOW_REGISTERS = true;
575 JexlNode tree = parse(expr, null);
576 JexlNode node = tree.jjtGetChild(0);
577 Interpreter interpreter = createInterpreter(context);
578 // set the registers
579 interpreter.setRegisters(bean, value);
580 node.jjtAccept(interpreter, null);
581 } catch (JexlException xjexl) {
582 if (silent) {
583 logger.warn(xjexl.getMessage(), xjexl.getCause());
584 return;
585 }
586 throw xjexl;
587 } finally {
588 parser.ALLOW_REGISTERS = false;
589 }
590 }
591
592 /**
593 * Invokes an object's method by name and arguments.
594 * @param obj the method's invoker object
595 * @param meth the method's name
596 * @param args the method's arguments
597 * @return the method returned value or null if it failed and engine is silent
598 * @throws JexlException if method could not be found or failed and engine is not silent
599 */
600 public Object invokeMethod(Object obj, String meth, Object... args) {
601 JexlException xjexl = null;
602 Object result = null;
603 JexlInfo info = debugInfo();
604 try {
605 JexlMethod method = uberspect.getMethod(obj, meth, args, info);
606 if (method == null && arithmetic.narrowArguments(args)) {
607 method = uberspect.getMethod(obj, meth, args, info);
608 }
609 if (method != null) {
610 result = method.invoke(obj, args);
611 } else {
612 xjexl = new JexlException(info, "failed finding method " + meth);
613 }
614 } catch (Exception xany) {
615 xjexl = new JexlException(info, "failed executing method " + meth, xany);
616 } finally {
617 if (xjexl != null) {
618 if (silent) {
619 logger.warn(xjexl.getMessage(), xjexl.getCause());
620 return null;
621 }
622 throw xjexl;
623 }
624 }
625 return result;
626 }
627
628 /**
629 * Creates a new instance of an object using the most appropriate constructor
630 * based on the arguments.
631 * @param <T> the type of object
632 * @param clazz the class to instantiate
633 * @param args the constructor arguments
634 * @return the created object instance or null on failure when silent
635 */
636 public <T> T newInstance(Class<? extends T> clazz, Object...args) {
637 return clazz.cast(doCreateInstance(clazz, args));
638 }
639
640 /**
641 * Creates a new instance of an object using the most appropriate constructor
642 * based on the arguments.
643 * @param clazz the name of the class to instantiate resolved through this engine's class loader
644 * @param args the constructor arguments
645 * @return the created object instance or null on failure when silent
646 */
647 public Object newInstance(String clazz, Object...args) {
648 return doCreateInstance(clazz, args);
649 }
650
651 /**
652 * Creates a new instance of an object using the most appropriate constructor
653 * based on the arguments.
654 * @param clazz the class to instantiate
655 * @param args the constructor arguments
656 * @return the created object instance or null on failure when silent
657 */
658 protected Object doCreateInstance(Object clazz, Object...args) {
659 JexlException xjexl = null;
660 Object result = null;
661 JexlInfo info = debugInfo();
662 try {
663 Constructor<?> ctor = uberspect.getConstructor(clazz, args, info);
664 if (ctor == null && arithmetic.narrowArguments(args)) {
665 ctor = uberspect.getConstructor(clazz, args, info);
666 }
667 if (ctor != null) {
668 result = ctor.newInstance(args);
669 } else {
670 xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
671 }
672 } catch (Exception xany) {
673 xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
674 } finally {
675 if (xjexl != null) {
676 if (silent) {
677 logger.warn(xjexl.getMessage(), xjexl.getCause());
678 return null;
679 }
680 throw xjexl;
681 }
682 }
683 return result;
684 }
685
686 /**
687 * Creates an interpreter.
688 * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
689 * @return an Interpreter
690 */
691 protected Interpreter createInterpreter(JexlContext context) {
692 if (context == null) {
693 context = EMPTY_CONTEXT;
694 }
695 return new Interpreter(this, context);
696 }
697
698 /**
699 * A soft reference on cache.
700 * <p>The cache is held through a soft reference, allowing it to be GCed under
701 * memory pressure.</p>
702 * @param <K> the cache key entry type
703 * @param <V> the cache key value type
704 */
705 protected class SoftCache<K, V> {
706 /**
707 * The cache size.
708 */
709 private final int size;
710 /**
711 * The soft reference to the cache map.
712 */
713 private SoftReference<Map<K, V>> ref = null;
714
715 /**
716 * Creates a new instance of a soft cache.
717 * @param theSize the cache size
718 */
719 SoftCache(int theSize) {
720 size = theSize;
721 }
722
723 /**
724 * Returns the cache size.
725 * @return the cache size
726 */
727 int size() {
728 return size;
729 }
730
731 /**
732 * Produces the cache entry set.
733 * @return the cache entry set
734 */
735 Set<Entry<K, V>> entrySet() {
736 Map<K, V> map = ref != null ? ref.get() : null;
737 return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
738 }
739
740 /**
741 * Gets a value from cache.
742 * @param key the cache entry key
743 * @return the cache entry value
744 */
745 V get(K key) {
746 final Map<K, V> map = ref != null ? ref.get() : null;
747 return map != null ? map.get(key) : null;
748 }
749
750 /**
751 * Puts a value in cache.
752 * @param key the cache entry key
753 * @param script the cache entry value
754 */
755 void put(K key, V script) {
756 Map<K, V> map = ref != null ? ref.get() : null;
757 if (map == null) {
758 map = createCache(size);
759 ref = new SoftReference<Map<K, V>>(map);
760 }
761 map.put(key, script);
762 }
763 }
764
765 /**
766 * Creates a cache.
767 * @param <K> the key type
768 * @param <V> the value type
769 * @param cacheSize the cache size, must be > 0
770 * @return a Map usable as a cache bounded to the given size
771 */
772 protected <K, V> Map<K, V> createCache(final int cacheSize) {
773 return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
774 /** Serial version UID. */
775 private static final long serialVersionUID = 3801124242820219131L;
776
777 @Override
778 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
779 return size() > cacheSize;
780 }
781 };
782 }
783
784 /**
785 * Parses an expression.
786 * @param expression the expression to parse
787 * @param info debug information structure
788 * @return the parsed tree
789 * @throws JexlException if any error occured during parsing
790 */
791 protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
792 String expr = cleanExpression(expression);
793 ASTJexlScript tree = null;
794 synchronized (parser) {
795 if (cache != null) {
796 tree = cache.get(expr);
797 if (tree != null) {
798 return tree;
799 }
800 }
801 try {
802 Reader reader = new StringReader(expr);
803 // use first calling method of JexlEngine as debug info
804 if (info == null) {
805 info = debugInfo();
806 }
807 tree = parser.parse(reader, info);
808 if (cache != null) {
809 cache.put(expr, tree);
810 }
811 } catch (TokenMgrError xtme) {
812 throw new JexlException(info, "tokenization failed", xtme);
813 } catch (ParseException xparse) {
814 throw new JexlException(info, "parsing failed", xparse);
815 }
816 }
817 return tree;
818 }
819
820 /**
821 * Creates a JexlInfo instance.
822 * @param fn url/file name
823 * @param l line number
824 * @param c column number
825 * @return a JexlInfo instance
826 */
827 protected JexlInfo createInfo(String fn, int l, int c) {
828 return new DebugInfo(fn, l, c);
829 }
830
831 /**
832 * Creates and fills up debugging information.
833 * <p>This gathers the class, method and line number of the first calling method
834 * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
835 * @return an Info if debug is set, null otherwise
836 */
837 protected JexlInfo debugInfo() {
838 JexlInfo info = null;
839 if (debug) {
840 Throwable xinfo = new Throwable();
841 xinfo.fillInStackTrace();
842 StackTraceElement[] stack = xinfo.getStackTrace();
843 StackTraceElement se = null;
844 Class<?> clazz = getClass();
845 for (int s = 1; s < stack.length; ++s, se = null) {
846 se = stack[s];
847 String className = se.getClassName();
848 if (!className.equals(clazz.getName())) {
849 // go deeper if called from JexlEngine or UnifiedJEXL
850 if (className.equals(JexlEngine.class.getName())) {
851 clazz = JexlEngine.class;
852 } else if (className.equals(UnifiedJEXL.class.getName())) {
853 clazz = UnifiedJEXL.class;
854 } else {
855 break;
856 }
857 }
858 }
859 if (se != null) {
860 info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0);
861 }
862 }
863 return info;
864 }
865
866 /**
867 * Trims the expression from front & ending spaces.
868 * @param str expression to clean
869 * @return trimmed expression ending in a semi-colon
870 */
871 public static final String cleanExpression(CharSequence str) {
872 if (str != null) {
873 int start = 0;
874 int end = str.length();
875 if (end > 0) {
876 // trim front spaces
877 while (start < end && str.charAt(start) == ' ') {
878 ++start;
879 }
880 // trim ending spaces
881 while (end > 0 && str.charAt(end - 1) == ' ') {
882 --end;
883 }
884 return str.subSequence(start, end).toString();
885 }
886 return "";
887 }
888 return null;
889 }
890
891 /**
892 * Read from a reader into a local buffer and return a String with
893 * the contents of the reader.
894 * @param scriptReader to be read.
895 * @return the contents of the reader as a String.
896 * @throws IOException on any error reading the reader.
897 */
898 public static final String readerToString(Reader scriptReader) throws IOException {
899 StringBuilder buffer = new StringBuilder();
900 BufferedReader reader;
901 if (scriptReader instanceof BufferedReader) {
902 reader = (BufferedReader) scriptReader;
903 } else {
904 reader = new BufferedReader(scriptReader);
905 }
906 try {
907 String line;
908 while ((line = reader.readLine()) != null) {
909 buffer.append(line).append('\n');
910 }
911 return buffer.toString();
912 } finally {
913 try {
914 reader.close();
915 } catch(IOException xio) {
916 // ignore
917 }
918 }
919
920 }
921 }