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 }