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