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