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