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 }