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.util.ArrayList; 020 import org.apache.commons.jexl2.parser.JexlNode; 021 import org.apache.commons.jexl2.parser.StringParser; 022 023 /** 024 * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL. 025 * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs 026 * and facilitate the implementation of expression evaluation. 027 * <p> 028 * An expression can mix immediate, deferred and nested sub-expressions as well as string constants; 029 * <ul> 030 * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li> 031 * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li> 032 * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li> 033 * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li> 034 * </ul> 035 * </p> 036 * <p> 037 * Deferred & immediate expression carry different intentions: 038 * <ul> 039 * <li>An immediate expression indicate that evaluation is intended to be performed close to 040 * the definition/parsing point.</li> 041 * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li> 042 * </ul> 043 * </p> 044 * <p> 045 * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one 046 * of its subexpressions is deferred. Furthermore, this (composite) expression intent is 047 * to perform two evaluations; one close to its definition and another one in a later 048 * phase. 049 * </p> 050 * <p> 051 * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method 052 * will evaluate the immediate subexpression and return an expression that contains only 053 * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression 054 * is suitable for a later phase evaluation that may occur with a different JexlContext. 055 * Note that it is valid to call evaluate without prepare in which case the same JexlContext 056 * is used for the 2 evaluation phases. 057 * </p> 058 * <p> 059 * In the most common use-case where deferred expressions are to be kept around as properties of objects, 060 * one should parse & prepare an expression before storing it and evaluate it each time 061 * the property storing it is accessed. 062 * </p> 063 * <p> 064 * Note that nested expression use the JEXL syntax as in: 065 * <code>"#{${bar}+'.charAt(2)'}"</code> 066 * The most common mistake leading to an invalid expression being the following: 067 * <code>"#{${bar}charAt(2)}"</code> 068 * </p> 069 * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> ecxeptions; 070 * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode 071 * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate. 072 * </p> 073 * @since 2.0 074 */ 075 public final class UnifiedJEXL { 076 /** The JEXL engine instance. */ 077 private final JexlEngine jexl; 078 /** The expression cache. */ 079 private final JexlEngine.SoftCache<String,Expression> cache; 080 /** The default cache size. */ 081 private static final int CACHE_SIZE = 256; 082 /** 083 * Creates a new instance of UnifiedJEXL with a default size cache. 084 * @param aJexl the JexlEngine to use. 085 */ 086 public UnifiedJEXL(JexlEngine aJexl) { 087 this(aJexl, CACHE_SIZE); 088 } 089 090 /** 091 * Creates a new instance of UnifiedJEXL creating a local cache. 092 * @param aJexl the JexlEngine to use. 093 * @param cacheSize the number of expressions in this cache 094 */ 095 public UnifiedJEXL(JexlEngine aJexl, int cacheSize) { 096 this.jexl = aJexl; 097 this.cache = aJexl.new SoftCache<String,Expression>(cacheSize); 098 } 099 100 /** 101 * Types of expressions. 102 * Each instance carries a counter index per (composite sub-) expression type. 103 * @see ExpressionBuilder 104 */ 105 private static enum ExpressionType { 106 /** Constant expression, count index 0. */ 107 CONSTANT(0), 108 /** Immediate expression, count index 1. */ 109 IMMEDIATE(1), 110 /** Deferred expression, count index 2. */ 111 DEFERRED(2), 112 /** Nested (which are deferred) expressions, count index 2. */ 113 NESTED(2), 114 /** Composite expressions are not counted, index -1. */ 115 COMPOSITE(-1); 116 /** The index in arrays of expression counters for composite expressions. */ 117 private final int index; 118 /** 119 * Creates an ExpressionType. 120 * @param idx the index for this type in counters arrays. 121 */ 122 ExpressionType(int idx) { 123 this.index = idx; 124 } 125 } 126 127 /** 128 * A helper class to build expressions. 129 * Keeps count of sub-expressions by type. 130 */ 131 private static class ExpressionBuilder { 132 /** Per expression type counters. */ 133 private final int[] counts; 134 /** The list of expressions. */ 135 private final ArrayList<Expression> expressions; 136 137 /** 138 * Creates a builder. 139 * @param size the initial expression array size 140 */ 141 ExpressionBuilder(int size) { 142 counts = new int[]{0, 0, 0}; 143 expressions = new ArrayList<Expression>(size <= 0 ? 3 : size); 144 } 145 146 /** 147 * Adds an expression to the list of expressions, maintain per-type counts. 148 * @param expr the expression to add 149 */ 150 void add(Expression expr) { 151 counts[expr.getType().index] += 1; 152 expressions.add(expr); 153 } 154 155 /** 156 * Builds an expression from a source, performs checks. 157 * @param el the unified el instance 158 * @param source the source expression 159 * @return an expression 160 */ 161 Expression build(UnifiedJEXL el, Expression source) { 162 int sum = 0; 163 for (int count : counts) { 164 sum += count; 165 } 166 if (expressions.size() != sum) { 167 StringBuilder error = new StringBuilder("parsing algorithm error, exprs: "); 168 error.append(expressions.size()); 169 error.append(", constant:"); 170 error.append(counts[ExpressionType.CONSTANT.index]); 171 error.append(", immediate:"); 172 error.append(counts[ExpressionType.IMMEDIATE.index]); 173 error.append(", deferred:"); 174 error.append(counts[ExpressionType.DEFERRED.index]); 175 throw new IllegalStateException(error.toString()); 176 } 177 // if only one sub-expr, no need to create a composite 178 if (expressions.size() == 1) { 179 return expressions.get(0); 180 } else { 181 return el.new CompositeExpression(counts, expressions, source); 182 } 183 } 184 } 185 186 /** 187 * Gets the JexlEngine underlying the UnifiedJEXL. 188 * @return the JexlEngine 189 */ 190 public JexlEngine getEngine() { 191 return jexl; 192 } 193 194 /** 195 * The sole type of (runtime) exception the UnifiedJEXL can throw. 196 */ 197 public static class Exception extends RuntimeException { 198 /** Serial version UID. */ 199 private static final long serialVersionUID = -8201402995815975726L; 200 /** 201 * Creates a UnifiedJEXL.Exception. 202 * @param msg the exception message 203 * @param cause the exception cause 204 */ 205 public Exception(String msg, Throwable cause) { 206 super(msg, cause); 207 } 208 } 209 210 /** 211 * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'. 212 */ 213 public abstract class Expression { 214 /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */ 215 protected final Expression source; 216 /** 217 * Creates an expression. 218 * @param src the source expression if any 219 */ 220 Expression(Expression src) { 221 this.source = src != null ? src : this; 222 } 223 224 /** 225 * Formats this expression, adding its source string representation in 226 * comments if available: 'expression /*= source *\/'' . 227 * @return the formatted expression string 228 */ 229 @Override 230 public String toString() { 231 StringBuilder strb = new StringBuilder(); 232 if (source != this) { 233 strb.append(source.toString()); 234 strb.append(" /*= "); 235 } 236 asString(strb); 237 if (source != this) { 238 strb.append(" */"); 239 } 240 return strb.toString(); 241 } 242 243 /** 244 * Generates this expression's string representation. 245 * @return the string representation 246 */ 247 public String asString() { 248 StringBuilder strb = new StringBuilder(); 249 asString(strb); 250 return strb.toString(); 251 } 252 253 /** 254 * Adds this expression's string representation to a StringBuilder. 255 * @param strb the builder to fill 256 */ 257 abstract void asString(StringBuilder strb); 258 259 /** 260 * When the expression is dependant upon immediate and deferred sub-expressions, 261 * evaluates the immediate sub-expressions with the context passed as parameter 262 * and returns this expression deferred form. 263 * <p> 264 * In effect, this binds the result of the immediate sub-expressions evaluation in the 265 * context, allowing to differ evaluation of the remaining (deferred) expression within another context. 266 * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions. 267 * </p> 268 * <p> 269 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning. 270 * </p> 271 * @param context the context to use for immediate expression evaluations 272 * @return an expression or null if an error occurs and the {@link JexlEngine} is silent 273 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 274 */ 275 public abstract Expression prepare(JexlContext context); 276 277 /** 278 * Evaluates this expression. 279 * <p> 280 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning. 281 * </p> 282 * @param context the variable context 283 * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is 284 * silent 285 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 286 */ 287 public abstract Object evaluate(JexlContext context); 288 289 /** 290 * Checks whether this expression is immediate. 291 * @return true if immediate, false otherwise 292 */ 293 public boolean isImmediate() { 294 return true; 295 } 296 297 /** 298 * Checks whether this expression is deferred. 299 * @return true if deferred, false otherwise 300 */ 301 public final boolean isDeferred() { 302 return !isImmediate(); 303 } 304 305 /** 306 * Retrieves this expression's source expression. 307 * If this expression was prepared, this allows to retrieve the 308 * original expression that lead to it. 309 * Other expressions return themselves. 310 * @return the source expression 311 */ 312 public final Expression getSource() { 313 return source; 314 } 315 316 /** 317 * Gets this expression type. 318 * @return its type 319 */ 320 abstract ExpressionType getType(); 321 322 /** 323 * Prepares a sub-expression for interpretation. 324 * @param interpreter a JEXL interpreter 325 * @return a prepared expression 326 * @throws JexlException (only for nested & composite) 327 */ 328 abstract Expression prepare(Interpreter interpreter); 329 330 /** 331 * Intreprets a sub-expression. 332 * @param interpreter a JEXL interpreter 333 * @return the result of interpretation 334 * @throws JexlException (only for nested & composite) 335 */ 336 abstract Object evaluate(Interpreter interpreter); 337 } 338 339 340 /** A constant expression. */ 341 private class ConstantExpression extends Expression { 342 /** The constant held by this expression. */ 343 private final Object value; 344 /** 345 * Creates a constant expression. 346 * <p> 347 * If the wrapped constant is a string, it is treated 348 * as a JEXL strings with respect to escaping. 349 * </p> 350 * @param val the constant value 351 * @param source the source expression if any 352 */ 353 ConstantExpression(Object val, Expression source) { 354 super(source); 355 if (val == null) { 356 throw new NullPointerException("constant can not be null"); 357 } 358 if (val instanceof String) { 359 val = StringParser.buildString((String) val, false); 360 } 361 this.value = val; 362 } 363 364 /** {@inheritDoc} */ 365 @Override 366 public String asString() { 367 StringBuilder strb = new StringBuilder(); 368 strb.append('"'); 369 asString(strb); 370 strb.append('"'); 371 return strb.toString(); 372 } 373 374 /** {@inheritDoc} */ 375 @Override 376 ExpressionType getType() { 377 return ExpressionType.CONSTANT; 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 void asString(StringBuilder strb) { 383 String str = value.toString(); 384 if (value instanceof String || value instanceof CharSequence) { 385 for (int i = 0, size = str.length(); i < size; ++i) { 386 char c = str.charAt(i); 387 if (c == '"' || c == '\\') { 388 strb.append('\\'); 389 } 390 strb.append(c); 391 } 392 } else { 393 strb.append(str); 394 } 395 } 396 397 /** {@inheritDoc} */ 398 @Override 399 public Expression prepare(JexlContext context) { 400 return this; 401 } 402 403 /** {@inheritDoc} */ 404 @Override 405 Expression prepare(Interpreter interpreter) { 406 return this; 407 } 408 409 /** {@inheritDoc} */ 410 @Override 411 public Object evaluate(JexlContext context) { 412 return value; 413 } 414 415 /** {@inheritDoc} */ 416 @Override 417 Object evaluate(Interpreter interpreter) { 418 return value; 419 } 420 } 421 422 423 /** The base for Jexl based expressions. */ 424 private abstract class JexlBasedExpression extends Expression { 425 /** The JEXL string for this expression. */ 426 protected final CharSequence expr; 427 /** The JEXL node for this expression. */ 428 protected final JexlNode node; 429 /** 430 * Creates a JEXL interpretable expression. 431 * @param theExpr the expression as a string 432 * @param theNode the expression as an AST 433 * @param theSource the source expression if any 434 */ 435 protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) { 436 super(theSource); 437 this.expr = theExpr; 438 this.node = theNode; 439 } 440 441 /** {@inheritDoc} */ 442 @Override 443 public String toString() { 444 StringBuilder strb = new StringBuilder(expr.length() + 3); 445 if (source != this) { 446 strb.append(source.toString()); 447 strb.append(" /*= "); 448 } 449 strb.append(isImmediate() ? '$' : '#'); 450 strb.append("{"); 451 strb.append(expr); 452 strb.append("}"); 453 if (source != this) { 454 strb.append(" */"); 455 } 456 return strb.toString(); 457 } 458 459 /** {@inheritDoc} */ 460 @Override 461 public void asString(StringBuilder strb) { 462 strb.append(isImmediate() ? '$' : '#'); 463 strb.append("{"); 464 strb.append(expr); 465 strb.append("}"); 466 } 467 468 /** {@inheritDoc} */ 469 @Override 470 public Expression prepare(JexlContext context) { 471 return this; 472 } 473 474 /** {@inheritDoc} */ 475 @Override 476 Expression prepare(Interpreter interpreter) { 477 return this; 478 } 479 480 /** {@inheritDoc} */ 481 @Override 482 public Object evaluate(JexlContext context) { 483 return UnifiedJEXL.this.evaluate(context, this); 484 } 485 486 /** {@inheritDoc} */ 487 @Override 488 Object evaluate(Interpreter interpreter) { 489 return interpreter.interpret(node); 490 } 491 } 492 493 494 /** An immediate expression: ${jexl}. */ 495 private class ImmediateExpression extends JexlBasedExpression { 496 /** 497 * Creates an immediate expression. 498 * @param expr the expression as a string 499 * @param node the expression as an AST 500 * @param source the source expression if any 501 */ 502 ImmediateExpression(CharSequence expr, JexlNode node, Expression source) { 503 super(expr, node, source); 504 } 505 506 /** {@inheritDoc} */ 507 @Override 508 ExpressionType getType() { 509 return ExpressionType.IMMEDIATE; 510 } 511 512 /** {@inheritDoc} */ 513 @Override 514 public boolean isImmediate() { 515 return true; 516 } 517 } 518 519 /** An immediate expression: ${jexl}. */ 520 private class DeferredExpression extends JexlBasedExpression { 521 /** 522 * Creates a deferred expression. 523 * @param expr the expression as a string 524 * @param node the expression as an AST 525 * @param source the source expression if any 526 */ 527 DeferredExpression(CharSequence expr, JexlNode node, Expression source) { 528 super(expr, node, source); 529 } 530 531 /** {@inheritDoc} */ 532 @Override 533 ExpressionType getType() { 534 return ExpressionType.DEFERRED; 535 } 536 537 /** {@inheritDoc} */ 538 @Override 539 public boolean isImmediate() { 540 return false; 541 } 542 } 543 544 /** 545 * A deferred expression that nests an immediate expression. 546 * #{...${jexl}...} 547 * Note that the deferred syntax is JEXL's, not UnifiedJEXL. 548 */ 549 private class NestedExpression extends DeferredExpression { 550 /** 551 * Creates a nested expression. 552 * @param expr the expression as a string 553 * @param node the expression as an AST 554 * @param source the source expression if any 555 */ 556 NestedExpression(CharSequence expr, JexlNode node, Expression source) { 557 super(expr, node, source); 558 if (this.source != this) { 559 throw new IllegalArgumentException("Nested expression can not have a source"); 560 } 561 } 562 563 /** {@inheritDoc} */ 564 @Override 565 ExpressionType getType() { 566 return ExpressionType.NESTED; 567 } 568 569 /** {@inheritDoc} */ 570 @Override 571 public String toString() { 572 return expr.toString(); 573 } 574 575 /** {@inheritDoc} */ 576 @Override 577 public Expression prepare(JexlContext context) { 578 return UnifiedJEXL.this.prepare(context, this); 579 } 580 581 /** {@inheritDoc} */ 582 @Override 583 public Expression prepare(Interpreter interpreter) { 584 String value = interpreter.interpret(node).toString(); 585 JexlNode dnode = toNode(value, jexl.isDebug()? node.getInfo() : null); 586 return new DeferredExpression(value, dnode, this); 587 } 588 589 /** {@inheritDoc} */ 590 @Override 591 public Object evaluate(Interpreter interpreter) { 592 return prepare(interpreter).evaluate(interpreter); 593 } 594 } 595 596 597 /** A composite expression: "... ${...} ... #{...} ...". */ 598 private class CompositeExpression extends Expression { 599 /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */ 600 private final int meta; 601 /** The list of sub-expression resulting from parsing. */ 602 private final Expression[] exprs; 603 /** 604 * Creates a composite expression. 605 * @param counters counters of expression per type 606 * @param list the sub-expressions 607 * @param src the source for this expresion if any 608 */ 609 CompositeExpression(int[] counters, ArrayList<Expression> list, Expression src) { 610 super(src); 611 this.exprs = list.toArray(new Expression[list.size()]); 612 this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0) 613 | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0); 614 } 615 616 /** {@inheritDoc} */ 617 @Override 618 ExpressionType getType() { 619 return ExpressionType.COMPOSITE; 620 } 621 622 /** {@inheritDoc} */ 623 @Override 624 public boolean isImmediate() { 625 // immediate if no deferred 626 return (meta & 2) == 0; 627 } 628 629 /** {@inheritDoc} */ 630 @Override 631 void asString(StringBuilder strb) { 632 for (Expression e : exprs) { 633 e.asString(strb); 634 } 635 } 636 637 /** {@inheritDoc} */ 638 @Override 639 public Expression prepare(JexlContext context) { 640 return UnifiedJEXL.this.prepare(context, this); 641 } 642 643 /** {@inheritDoc} */ 644 @Override 645 Expression prepare(Interpreter interpreter) { 646 // if this composite is not its own source, it is already prepared 647 if (source != this) { 648 return this; 649 } 650 // we need to eval immediate expressions if there are some deferred/nested 651 // ie both immediate & deferred counts > 0, bits 1 & 0 set, (1 << 1) & 1 == 3 652 final boolean evalImmediate = meta == 3; 653 final int size = exprs.length; 654 final ExpressionBuilder builder = new ExpressionBuilder(size); 655 // tracking whether prepare will return a different expression 656 boolean eq = true; 657 for (int e = 0; e < size; ++e) { 658 Expression expr = exprs[e]; 659 Expression prepared = expr.prepare(interpreter); 660 if (evalImmediate && prepared instanceof ImmediateExpression) { 661 // evaluate immediate as constant 662 Object value = prepared.evaluate(interpreter); 663 prepared = value == null ? null : new ConstantExpression(value, prepared); 664 } 665 // add it if not null 666 if (prepared != null) { 667 builder.add(prepared); 668 } 669 // keep track of expression equivalence 670 eq &= expr == prepared; 671 } 672 Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this); 673 return ready; 674 } 675 676 /** {@inheritDoc} */ 677 @Override 678 public Object evaluate(JexlContext context) { 679 return UnifiedJEXL.this.evaluate(context, this); 680 } 681 682 /** {@inheritDoc} */ 683 @Override 684 Object evaluate(Interpreter interpreter) { 685 final int size = exprs.length; 686 Object value = null; 687 // common case: evaluate all expressions & concatenate them as a string 688 StringBuilder strb = new StringBuilder(); 689 for (int e = 0; e < size; ++e) { 690 value = exprs[e].evaluate(interpreter); 691 if (value != null) { 692 strb.append(value.toString()); 693 } 694 } 695 value = strb.toString(); 696 return value; 697 } 698 } 699 700 /** Creates a a {@link UnifiedJEXL.Expression} from an expression string. 701 * Uses & fills up the expression cache if any. 702 * <p> 703 * If the underlying JEXL engine is silent, errors will be logged through its logger as warnings. 704 * </p> 705 * @param expression the UnifiedJEXL string expression 706 * @return the UnifiedJEXL object expression, null if silent and an error occured 707 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 708 */ 709 public Expression parse(String expression) { 710 Exception xuel = null; 711 Expression stmt = null; 712 try { 713 if (cache == null) { 714 stmt = parseExpression(expression); 715 } else { 716 synchronized (cache) { 717 stmt = cache.get(expression); 718 if (stmt == null) { 719 stmt = parseExpression(expression); 720 cache.put(expression, stmt); 721 } 722 } 723 } 724 } catch (JexlException xjexl) { 725 xuel = new Exception("failed to parse '" + expression + "'", xjexl); 726 } catch (Exception xany) { 727 xuel = xany; 728 } finally { 729 if (xuel != null) { 730 if (jexl.isSilent()) { 731 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 732 return null; 733 } 734 throw xuel; 735 } 736 } 737 return stmt; 738 } 739 740 /** 741 * Prepares an expression (nested & composites), handles exception reporting. 742 * 743 * @param context the JEXL context to use 744 * @param expr the expression to prepare 745 * @return a prepared expression 746 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 747 */ 748 Expression prepare(JexlContext context, Expression expr) { 749 try { 750 Interpreter interpreter = jexl.createInterpreter(context); 751 interpreter.setSilent(false); 752 return expr.prepare(interpreter); 753 } catch (JexlException xjexl) { 754 Exception xuel = createException("prepare", expr, xjexl); 755 if (jexl.isSilent()) { 756 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 757 return null; 758 } 759 throw xuel; 760 } 761 } 762 763 /** 764 * Evaluates an expression (nested & composites), handles exception reporting. 765 * 766 * @param context the JEXL context to use 767 * @param expr the expression to prepare 768 * @return the result of the evaluation 769 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 770 */ 771 Object evaluate(JexlContext context, Expression expr) { 772 try { 773 Interpreter interpreter = jexl.createInterpreter(context); 774 interpreter.setSilent(false); 775 return expr.evaluate(interpreter); 776 } catch (JexlException xjexl) { 777 Exception xuel = createException("evaluate", expr, xjexl); 778 if (jexl.isSilent()) { 779 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 780 return null; 781 } 782 throw xuel; 783 } 784 } 785 786 /** 787 * Use the JEXL parser to create the AST for an expression. 788 * @param expression the expression to parse 789 * @return the AST 790 * @throws JexlException if an error occur during parsing 791 */ 792 private JexlNode toNode(CharSequence expression) { 793 return jexl.parse(expression, null); 794 } 795 796 /** 797 * Use the JEXL parser to create the AST for an expression. 798 * @param expression the expression to parse 799 * @param info debug information 800 * @return the AST 801 * @throws JexlException if an error occur during parsing 802 */ 803 private JexlNode toNode(CharSequence expression, JexlInfo info) { 804 return jexl.parse(expression, info); 805 } 806 807 /** 808 * Creates a UnifiedJEXL.Exception from a JexlException. 809 * @param action parse, prepare, evaluate 810 * @param expr the expression 811 * @param xany the exception 812 * @return an exception containing an explicit error message 813 */ 814 private Exception createException(String action, Expression expr, java.lang.Exception xany) { 815 StringBuilder strb = new StringBuilder("failed to "); 816 strb.append(action); 817 strb.append(" '"); 818 strb.append(expr.toString()); 819 strb.append("'"); 820 Throwable cause = xany.getCause(); 821 if (cause != null) { 822 String causeMsg = cause.getMessage(); 823 if (causeMsg != null) { 824 strb.append(", "); 825 strb.append(causeMsg); 826 } 827 } 828 return new Exception(strb.toString(), xany); 829 } 830 831 832 /** The different parsing states. */ 833 private static enum ParseState { 834 /** Parsing a constant. */ 835 CONST, 836 /** Parsing after $ .*/ 837 IMMEDIATE0, 838 /** Parsing after # .*/ 839 DEFERRED0, 840 /** Parsing after ${ .*/ 841 IMMEDIATE1, 842 /** Parsing after #{ .*/ 843 DEFERRED1, 844 /** Parsing after \ .*/ 845 ESCAPE 846 } 847 848 /** 849 * Parses a unified expression. 850 * @param expr the string expression 851 * @return the expression instance 852 * @throws JexlException if an error occur during parsing 853 */ 854 private Expression parseExpression(String expr) { 855 final int size = expr.length(); 856 ExpressionBuilder builder = new ExpressionBuilder(0); 857 StringBuilder strb = new StringBuilder(size); 858 ParseState state = ParseState.CONST; 859 int inner = 0; 860 boolean nested = false; 861 int inested = -1; 862 for (int i = 0; i < size; ++i) { 863 char c = expr.charAt(i); 864 switch (state) { 865 default: // in case we ever add new expression type 866 throw new UnsupportedOperationException("unexpected expression type"); 867 case CONST: 868 if (c == '$') { 869 state = ParseState.IMMEDIATE0; 870 } else if (c == '#') { 871 inested = i; 872 state = ParseState.DEFERRED0; 873 } else if (c == '\\') { 874 state = ParseState.ESCAPE; 875 } else { 876 // do buildup expr 877 strb.append(c); 878 } 879 break; 880 case IMMEDIATE0: // $ 881 if (c == '{') { 882 state = ParseState.IMMEDIATE1; 883 // if chars in buffer, create constant 884 if (strb.length() > 0) { 885 Expression cexpr = new ConstantExpression(strb.toString(), null); 886 builder.add(cexpr); 887 strb.delete(0, Integer.MAX_VALUE); 888 } 889 } else { 890 // revert to CONST 891 strb.append('$'); 892 strb.append(c); 893 state = ParseState.CONST; 894 } 895 break; 896 case DEFERRED0: // # 897 if (c == '{') { 898 state = ParseState.DEFERRED1; 899 // if chars in buffer, create constant 900 if (strb.length() > 0) { 901 Expression cexpr = new ConstantExpression(strb.toString(), null); 902 builder.add(cexpr); 903 strb.delete(0, Integer.MAX_VALUE); 904 } 905 } else { 906 // revert to CONST 907 strb.append('#'); 908 strb.append(c); 909 state = ParseState.CONST; 910 } 911 break; 912 case IMMEDIATE1: // ${... 913 if (c == '}') { 914 // materialize the immediate expr 915 Expression iexpr = new ImmediateExpression(strb.toString(), toNode(strb), null); 916 builder.add(iexpr); 917 strb.delete(0, Integer.MAX_VALUE); 918 state = ParseState.CONST; 919 } else { 920 // do buildup expr 921 strb.append(c); 922 } 923 break; 924 case DEFERRED1: // #{... 925 // skip inner strings (for '}') 926 if (c == '"' || c == '\'') { 927 strb.append(c); 928 i = StringParser.readString(strb, expr, i + 1, c); 929 continue; 930 } 931 // nested immediate in deferred; need to balance count of '{' & '}' 932 if (c == '{') { 933 if (expr.charAt(i - 1) == '$') { 934 inner += 1; 935 strb.deleteCharAt(strb.length() - 1); 936 nested = true; 937 } 938 continue; 939 } 940 // closing '}' 941 if (c == '}') { 942 // balance nested immediate 943 if (inner > 0) { 944 inner -= 1; 945 } else { 946 // materialize the nested/deferred expr 947 Expression dexpr = null; 948 if (nested) { 949 dexpr = new NestedExpression(expr.substring(inested, i + 1), toNode(strb), null); 950 } else { 951 dexpr = new DeferredExpression(strb.toString(), toNode(strb), null); 952 } 953 builder.add(dexpr); 954 strb.delete(0, Integer.MAX_VALUE); 955 nested = false; 956 state = ParseState.CONST; 957 } 958 } else { 959 // do buildup expr 960 strb.append(c); 961 } 962 break; 963 case ESCAPE: 964 if (c == '#') { 965 strb.append('#'); 966 } else if (c == '$') { 967 strb.append('$'); 968 } else { 969 strb.append('\\'); 970 strb.append(c); 971 } 972 state = ParseState.CONST; 973 } 974 } 975 // we should be in that state 976 if (state != ParseState.CONST) { 977 throw new Exception("malformed expression: " + expr, null); 978 } 979 // if any chars were buffered, add them as a constant 980 if (strb.length() > 0) { 981 Expression cexpr = new ConstantExpression(strb.toString(), null); 982 builder.add(cexpr); 983 } 984 return builder.build(this, null); 985 } 986 }