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.IOException; 021 import java.io.Reader; 022 import java.io.StringReader; 023 import java.io.Writer; 024 import java.util.ArrayList; 025 import java.util.Collections; 026 import java.util.LinkedHashSet; 027 import java.util.List; 028 import java.util.Set; 029 import org.apache.commons.jexl2.introspection.JexlMethod; 030 import org.apache.commons.jexl2.introspection.Uberspect; 031 import org.apache.commons.jexl2.parser.ASTJexlScript; 032 import org.apache.commons.jexl2.parser.JexlNode; 033 import org.apache.commons.jexl2.parser.StringParser; 034 035 /** 036 * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL. 037 * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs 038 * and facilitate the implementation of expression evaluation. 039 * <p> 040 * An expression can mix immediate, deferred and nested sub-expressions as well as string constants; 041 * <ul> 042 * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li> 043 * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li> 044 * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li> 045 * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li> 046 * </ul> 047 * </p> 048 * <p> 049 * Deferred & immediate expression carry different intentions: 050 * <ul> 051 * <li>An immediate expression indicate that evaluation is intended to be performed close to 052 * the definition/parsing point.</li> 053 * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li> 054 * </ul> 055 * </p> 056 * <p> 057 * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one 058 * of its subexpressions is deferred. Furthermore, this (composite) expression intent is 059 * to perform two evaluations; one close to its definition and another one in a later 060 * phase. 061 * </p> 062 * <p> 063 * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method 064 * will evaluate the immediate subexpression and return an expression that contains only 065 * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression 066 * is suitable for a later phase evaluation that may occur with a different JexlContext. 067 * Note that it is valid to call evaluate without prepare in which case the same JexlContext 068 * is used for the 2 evaluation phases. 069 * </p> 070 * <p> 071 * In the most common use-case where deferred expressions are to be kept around as properties of objects, 072 * one should parse & prepare an expression before storing it and evaluate it each time 073 * the property storing it is accessed. 074 * </p> 075 * <p> 076 * Note that nested expression use the JEXL syntax as in: 077 * <code>"#{${bar}+'.charAt(2)'}"</code> 078 * The most common mistake leading to an invalid expression being the following: 079 * <code>"#{${bar}charAt(2)}"</code> 080 * </p> 081 * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> exceptions; 082 * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode 083 * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate. 084 * </p> 085 * @since 2.0 086 */ 087 public final class UnifiedJEXL { 088 /** The JEXL engine instance. */ 089 private final JexlEngine jexl; 090 /** The expression cache. */ 091 private final JexlEngine.SoftCache<String, Expression> cache; 092 /** The default cache size. */ 093 private static final int CACHE_SIZE = 256; 094 /** The first character for immediate expressions. */ 095 private static final char IMM_CHAR = '$'; 096 /** The first character for deferred expressions. */ 097 private static final char DEF_CHAR = '#'; 098 099 /** 100 * Creates a new instance of UnifiedJEXL with a default size cache. 101 * @param aJexl the JexlEngine to use. 102 */ 103 public UnifiedJEXL(JexlEngine aJexl) { 104 this(aJexl, CACHE_SIZE); 105 } 106 107 /** 108 * Creates a new instance of UnifiedJEXL creating a local cache. 109 * @param aJexl the JexlEngine to use. 110 * @param cacheSize the number of expressions in this cache 111 */ 112 public UnifiedJEXL(JexlEngine aJexl, int cacheSize) { 113 this.jexl = aJexl; 114 this.cache = aJexl.new SoftCache<String, Expression>(cacheSize); 115 } 116 117 /** 118 * Types of expressions. 119 * Each instance carries a counter index per (composite sub-) expression type. 120 * @see ExpressionBuilder 121 */ 122 private static enum ExpressionType { 123 /** Constant expression, count index 0. */ 124 CONSTANT(0), 125 /** Immediate expression, count index 1. */ 126 IMMEDIATE(1), 127 /** Deferred expression, count index 2. */ 128 DEFERRED(2), 129 /** Nested (which are deferred) expressions, count index 2. */ 130 NESTED(2), 131 /** Composite expressions are not counted, index -1. */ 132 COMPOSITE(-1); 133 /** The index in arrays of expression counters for composite expressions. */ 134 private final int index; 135 136 /** 137 * Creates an ExpressionType. 138 * @param idx the index for this type in counters arrays. 139 */ 140 ExpressionType(int idx) { 141 this.index = idx; 142 } 143 } 144 145 /** 146 * A helper class to build expressions. 147 * Keeps count of sub-expressions by type. 148 */ 149 private static class ExpressionBuilder { 150 /** Per expression type counters. */ 151 private final int[] counts; 152 /** The list of expressions. */ 153 private final ArrayList<Expression> expressions; 154 155 /** 156 * Creates a builder. 157 * @param size the initial expression array size 158 */ 159 ExpressionBuilder(int size) { 160 counts = new int[]{0, 0, 0}; 161 expressions = new ArrayList<Expression>(size <= 0 ? 3 : size); 162 } 163 164 /** 165 * Adds an expression to the list of expressions, maintain per-type counts. 166 * @param expr the expression to add 167 */ 168 void add(Expression expr) { 169 counts[expr.getType().index] += 1; 170 expressions.add(expr); 171 } 172 173 /** 174 * Builds an expression from a source, performs checks. 175 * @param el the unified el instance 176 * @param source the source expression 177 * @return an expression 178 */ 179 Expression build(UnifiedJEXL el, Expression source) { 180 int sum = 0; 181 for (int count : counts) { 182 sum += count; 183 } 184 if (expressions.size() != sum) { 185 StringBuilder error = new StringBuilder("parsing algorithm error, exprs: "); 186 error.append(expressions.size()); 187 error.append(", constant:"); 188 error.append(counts[ExpressionType.CONSTANT.index]); 189 error.append(", immediate:"); 190 error.append(counts[ExpressionType.IMMEDIATE.index]); 191 error.append(", deferred:"); 192 error.append(counts[ExpressionType.DEFERRED.index]); 193 throw new IllegalStateException(error.toString()); 194 } 195 // if only one sub-expr, no need to create a composite 196 if (expressions.size() == 1) { 197 return expressions.get(0); 198 } else { 199 return el.new CompositeExpression(counts, expressions, source); 200 } 201 } 202 } 203 204 /** 205 * Gets the JexlEngine underlying the UnifiedJEXL. 206 * @return the JexlEngine 207 */ 208 public JexlEngine getEngine() { 209 return jexl; 210 } 211 212 /** 213 * Clears the cache. 214 * @since 2.1 215 */ 216 public void clearCache() { 217 synchronized (cache) { 218 cache.clear(); 219 } 220 } 221 222 /** 223 * The sole type of (runtime) exception the UnifiedJEXL can throw. 224 */ 225 public static class Exception extends RuntimeException { 226 /** Serial version UID. */ 227 private static final long serialVersionUID = -8201402995815975726L; 228 229 /** 230 * Creates a UnifiedJEXL.Exception. 231 * @param msg the exception message 232 * @param cause the exception cause 233 */ 234 public Exception(String msg, Throwable cause) { 235 super(msg, cause); 236 } 237 } 238 239 /** 240 * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'. 241 */ 242 public abstract class Expression { 243 /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */ 244 protected final Expression source; 245 246 /** 247 * Creates an expression. 248 * @param src the source expression if any 249 */ 250 Expression(Expression src) { 251 this.source = src != null ? src : this; 252 } 253 254 /** 255 * Checks whether this expression is immediate. 256 * @return true if immediate, false otherwise 257 */ 258 public boolean isImmediate() { 259 return true; 260 } 261 262 /** 263 * Checks whether this expression is deferred. 264 * @return true if deferred, false otherwise 265 */ 266 public final boolean isDeferred() { 267 return !isImmediate(); 268 } 269 270 /** 271 * Gets this expression type. 272 * @return its type 273 */ 274 abstract ExpressionType getType(); 275 276 /** 277 * Formats this expression, adding its source string representation in 278 * comments if available: 'expression /*= source *\/'' . 279 * <b>Note:</b> do not override; will be made final in a future release. 280 * @return the formatted expression string 281 */ 282 @Override 283 public String toString() { 284 StringBuilder strb = new StringBuilder(); 285 asString(strb); 286 if (source != this) { 287 strb.append(" /*= "); 288 strb.append(source.toString()); 289 strb.append(" */"); 290 } 291 return strb.toString(); 292 } 293 294 /** 295 * Generates this expression's string representation. 296 * @return the string representation 297 */ 298 public String asString() { 299 StringBuilder strb = new StringBuilder(); 300 asString(strb); 301 return strb.toString(); 302 } 303 304 /** 305 * Adds this expression's string representation to a StringBuilder. 306 * @param strb the builder to fill 307 * @return the builder argument 308 */ 309 public abstract StringBuilder asString(StringBuilder strb); 310 311 /** 312 * Gets the list of variables accessed by this expression. 313 * <p>This method will visit all nodes of the sub-expressions and extract all variables whether they 314 * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p> 315 * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string) 316 * or the empty set if no variables are used 317 * @since 2.1 318 */ 319 public Set<List<String>> getVariables() { 320 return Collections.emptySet(); 321 } 322 323 /** 324 * Fills up the list of variables accessed by this expression. 325 * @param refs the set of variable being filled 326 * @since 2.1 327 */ 328 protected void getVariables(Set<List<String>> refs) { 329 // nothing to do 330 } 331 332 /** 333 * Evaluates the immediate sub-expressions. 334 * <p> 335 * When the expression is dependant upon immediate and deferred sub-expressions, 336 * evaluates the immediate sub-expressions with the context passed as parameter 337 * and returns this expression deferred form. 338 * </p> 339 * <p> 340 * In effect, this binds the result of the immediate sub-expressions evaluation in the 341 * context, allowing to differ evaluation of the remaining (deferred) expression within another context. 342 * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions. 343 * </p> 344 * <p> 345 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning. 346 * </p> 347 * <b>Note:</b> do not override; will be made final in a future release. 348 * @param context the context to use for immediate expression evaluations 349 * @return an expression or null if an error occurs and the {@link JexlEngine} is running in silent mode 350 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not in silent mode 351 */ 352 public Expression prepare(JexlContext context) { 353 try { 354 Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent()); 355 if (context instanceof TemplateContext) { 356 interpreter.setFrame(((TemplateContext) context).getFrame()); 357 } 358 return prepare(interpreter); 359 } catch (JexlException xjexl) { 360 Exception xuel = createException("prepare", this, xjexl); 361 if (jexl.isSilent()) { 362 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 363 return null; 364 } 365 throw xuel; 366 } 367 } 368 369 /** 370 * Evaluates this expression. 371 * <p> 372 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning. 373 * </p> 374 * <b>Note:</b> do not override; will be made final in a future release. 375 * @param context the variable context 376 * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is 377 * running in silent mode 378 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 379 */ 380 public Object evaluate(JexlContext context) { 381 try { 382 Interpreter interpreter = new Interpreter(jexl, context, !jexl.isLenient(), jexl.isSilent()); 383 if (context instanceof TemplateContext) { 384 interpreter.setFrame(((TemplateContext) context).getFrame()); 385 } 386 return evaluate(interpreter); 387 } catch (JexlException xjexl) { 388 Exception xuel = createException("prepare", this, xjexl); 389 if (jexl.isSilent()) { 390 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 391 return null; 392 } 393 throw xuel; 394 } 395 } 396 397 /** 398 * Retrieves this expression's source expression. 399 * If this expression was prepared, this allows to retrieve the 400 * original expression that lead to it. 401 * Other expressions return themselves. 402 * @return the source expression 403 */ 404 public final Expression getSource() { 405 return source; 406 } 407 408 /** 409 * Prepares a sub-expression for interpretation. 410 * @param interpreter a JEXL interpreter 411 * @return a prepared expression 412 * @throws JexlException (only for nested & composite) 413 */ 414 protected Expression prepare(Interpreter interpreter) { 415 return this; 416 } 417 418 /** 419 * Intreprets a sub-expression. 420 * @param interpreter a JEXL interpreter 421 * @return the result of interpretation 422 * @throws JexlException (only for nested & composite) 423 */ 424 protected abstract Object evaluate(Interpreter interpreter); 425 } 426 427 /** A constant expression. */ 428 private class ConstantExpression extends Expression { 429 /** The constant held by this expression. */ 430 private final Object value; 431 432 /** 433 * Creates a constant expression. 434 * <p> 435 * If the wrapped constant is a string, it is treated 436 * as a JEXL strings with respect to escaping. 437 * </p> 438 * @param val the constant value 439 * @param source the source expression if any 440 */ 441 ConstantExpression(Object val, Expression source) { 442 super(source); 443 if (val == null) { 444 throw new NullPointerException("constant can not be null"); 445 } 446 if (val instanceof String) { 447 val = StringParser.buildString((String) val, false); 448 } 449 this.value = val; 450 } 451 452 /** {@inheritDoc} */ 453 @Override 454 ExpressionType getType() { 455 return ExpressionType.CONSTANT; 456 } 457 458 /** {@inheritDoc} */ 459 @Override 460 public StringBuilder asString(StringBuilder strb) { 461 if (value != null) { 462 strb.append(value.toString()); 463 } 464 return strb; 465 } 466 467 /** {@inheritDoc} */ 468 @Override 469 protected Object evaluate(Interpreter interpreter) { 470 return value; 471 } 472 } 473 474 /** The base for Jexl based expressions. */ 475 private abstract class JexlBasedExpression extends Expression { 476 /** The JEXL string for this expression. */ 477 protected final CharSequence expr; 478 /** The JEXL node for this expression. */ 479 protected final JexlNode node; 480 481 /** 482 * Creates a JEXL interpretable expression. 483 * @param theExpr the expression as a string 484 * @param theNode the expression as an AST 485 * @param theSource the source expression if any 486 */ 487 protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) { 488 super(theSource); 489 this.expr = theExpr; 490 this.node = theNode; 491 } 492 493 /** {@inheritDoc} */ 494 @Override 495 public StringBuilder asString(StringBuilder strb) { 496 strb.append(isImmediate() ? IMM_CHAR : DEF_CHAR); 497 strb.append("{"); 498 strb.append(expr); 499 strb.append("}"); 500 return strb; 501 } 502 503 /** {@inheritDoc} */ 504 @Override 505 protected Object evaluate(Interpreter interpreter) { 506 return interpreter.interpret(node); 507 } 508 509 /** {@inheritDoc} */ 510 @Override 511 public Set<List<String>> getVariables() { 512 Set<List<String>> refs = new LinkedHashSet<List<String>>(); 513 getVariables(refs); 514 return refs; 515 } 516 517 /** {@inheritDoc} */ 518 @Override 519 protected void getVariables(Set<List<String>> refs) { 520 jexl.getVariables(node, refs, null); 521 } 522 } 523 524 /** An immediate expression: ${jexl}. */ 525 private class ImmediateExpression extends JexlBasedExpression { 526 /** 527 * Creates an immediate expression. 528 * @param expr the expression as a string 529 * @param node the expression as an AST 530 * @param source the source expression if any 531 */ 532 ImmediateExpression(CharSequence expr, JexlNode node, Expression source) { 533 super(expr, node, source); 534 } 535 536 /** {@inheritDoc} */ 537 @Override 538 ExpressionType getType() { 539 return ExpressionType.IMMEDIATE; 540 } 541 542 /** {@inheritDoc} */ 543 @Override 544 protected Expression prepare(Interpreter interpreter) { 545 // evaluate immediate as constant 546 Object value = evaluate(interpreter); 547 return value != null ? new ConstantExpression(value, source) : null; 548 } 549 } 550 551 /** A deferred expression: #{jexl}. */ 552 private class DeferredExpression extends JexlBasedExpression { 553 /** 554 * Creates a deferred expression. 555 * @param expr the expression as a string 556 * @param node the expression as an AST 557 * @param source the source expression if any 558 */ 559 DeferredExpression(CharSequence expr, JexlNode node, Expression source) { 560 super(expr, node, source); 561 } 562 563 /** {@inheritDoc} */ 564 @Override 565 public boolean isImmediate() { 566 return false; 567 } 568 569 /** {@inheritDoc} */ 570 @Override 571 ExpressionType getType() { 572 return ExpressionType.DEFERRED; 573 } 574 575 /** {@inheritDoc} */ 576 @Override 577 protected Expression prepare(Interpreter interpreter) { 578 return new ImmediateExpression(expr, node, source); 579 } 580 581 /** {@inheritDoc} */ 582 @Override 583 protected void getVariables(Set<List<String>> refs) { 584 // noop 585 } 586 } 587 588 /** 589 * An immediate expression nested into a deferred expression. 590 * #{...${jexl}...} 591 * Note that the deferred syntax is JEXL's, not UnifiedJEXL. 592 */ 593 private class NestedExpression extends JexlBasedExpression { 594 /** 595 * Creates a nested expression. 596 * @param expr the expression as a string 597 * @param node the expression as an AST 598 * @param source the source expression if any 599 */ 600 NestedExpression(CharSequence expr, JexlNode node, Expression source) { 601 super(expr, node, source); 602 if (this.source != this) { 603 throw new IllegalArgumentException("Nested expression can not have a source"); 604 } 605 } 606 607 @Override 608 public StringBuilder asString(StringBuilder strb) { 609 strb.append(expr); 610 return strb; 611 } 612 613 /** {@inheritDoc} */ 614 @Override 615 public boolean isImmediate() { 616 return false; 617 } 618 619 /** {@inheritDoc} */ 620 @Override 621 ExpressionType getType() { 622 return ExpressionType.NESTED; 623 } 624 625 /** {@inheritDoc} */ 626 @Override 627 protected Expression prepare(Interpreter interpreter) { 628 String value = interpreter.interpret(node).toString(); 629 JexlNode dnode = jexl.parse(value, jexl.isDebug() ? node.debugInfo() : null, null); 630 return new ImmediateExpression(value, dnode, this); 631 } 632 633 /** {@inheritDoc} */ 634 @Override 635 protected Object evaluate(Interpreter interpreter) { 636 return prepare(interpreter).evaluate(interpreter); 637 } 638 } 639 640 /** A composite expression: "... ${...} ... #{...} ...". */ 641 private class CompositeExpression extends Expression { 642 /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */ 643 private final int meta; 644 /** The list of sub-expression resulting from parsing. */ 645 protected final Expression[] exprs; 646 647 /** 648 * Creates a composite expression. 649 * @param counters counters of expression per type 650 * @param list the sub-expressions 651 * @param src the source for this expresion if any 652 */ 653 CompositeExpression(int[] counters, ArrayList<Expression> list, Expression src) { 654 super(src); 655 this.exprs = list.toArray(new Expression[list.size()]); 656 this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0) 657 | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0); 658 } 659 660 /** {@inheritDoc} */ 661 @Override 662 public boolean isImmediate() { 663 // immediate if no deferred 664 return (meta & 2) == 0; 665 } 666 667 /** {@inheritDoc} */ 668 @Override 669 ExpressionType getType() { 670 return ExpressionType.COMPOSITE; 671 } 672 673 /** {@inheritDoc} */ 674 @Override 675 public StringBuilder asString(StringBuilder strb) { 676 for (Expression e : exprs) { 677 e.asString(strb); 678 } 679 return strb; 680 } 681 682 /** {@inheritDoc} */ 683 @Override 684 public Set<List<String>> getVariables() { 685 Set<List<String>> refs = new LinkedHashSet<List<String>>(); 686 for (Expression expr : exprs) { 687 expr.getVariables(refs); 688 } 689 return refs; 690 } 691 692 /** {@inheritDoc} */ 693 @Override 694 protected Expression prepare(Interpreter interpreter) { 695 // if this composite is not its own source, it is already prepared 696 if (source != this) { 697 return this; 698 } 699 // we need to prepare all sub-expressions 700 final int size = exprs.length; 701 final ExpressionBuilder builder = new ExpressionBuilder(size); 702 // tracking whether prepare will return a different expression 703 boolean eq = true; 704 for (int e = 0; e < size; ++e) { 705 Expression expr = exprs[e]; 706 Expression prepared = expr.prepare(interpreter); 707 // add it if not null 708 if (prepared != null) { 709 builder.add(prepared); 710 } 711 // keep track of expression equivalence 712 eq &= expr == prepared; 713 } 714 Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this); 715 return ready; 716 } 717 718 /** {@inheritDoc} */ 719 @Override 720 protected Object evaluate(Interpreter interpreter) { 721 final int size = exprs.length; 722 Object value = null; 723 // common case: evaluate all expressions & concatenate them as a string 724 StringBuilder strb = new StringBuilder(); 725 for (int e = 0; e < size; ++e) { 726 value = exprs[e].evaluate(interpreter); 727 if (value != null) { 728 strb.append(value.toString()); 729 } 730 } 731 value = strb.toString(); 732 return value; 733 } 734 } 735 736 /** Creates a a {@link UnifiedJEXL.Expression} from an expression string. 737 * Uses & fills up the expression cache if any. 738 * <p> 739 * If the underlying JEXL engine is silent, errors will be logged through its logger as warnings. 740 * </p> 741 * @param expression the UnifiedJEXL string expression 742 * @return the UnifiedJEXL object expression, null if silent and an error occured 743 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent 744 */ 745 public Expression parse(String expression) { 746 Exception xuel = null; 747 Expression stmt = null; 748 try { 749 if (cache == null) { 750 stmt = parseExpression(expression, null); 751 } else { 752 synchronized (cache) { 753 stmt = cache.get(expression); 754 if (stmt == null) { 755 stmt = parseExpression(expression, null); 756 cache.put(expression, stmt); 757 } 758 } 759 } 760 } catch (JexlException xjexl) { 761 xuel = new Exception("failed to parse '" + expression + "'", xjexl); 762 } catch (Exception xany) { 763 xuel = xany; 764 } finally { 765 if (xuel != null) { 766 if (jexl.isSilent()) { 767 jexl.logger.warn(xuel.getMessage(), xuel.getCause()); 768 return null; 769 } 770 throw xuel; 771 } 772 } 773 return stmt; 774 } 775 776 /** 777 * Creates a UnifiedJEXL.Exception from a JexlException. 778 * @param action parse, prepare, evaluate 779 * @param expr the expression 780 * @param xany the exception 781 * @return an exception containing an explicit error message 782 */ 783 private Exception createException(String action, Expression expr, java.lang.Exception xany) { 784 StringBuilder strb = new StringBuilder("failed to "); 785 strb.append(action); 786 if (expr != null) { 787 strb.append(" '"); 788 strb.append(expr.toString()); 789 strb.append("'"); 790 } 791 Throwable cause = xany.getCause(); 792 if (cause != null) { 793 String causeMsg = cause.getMessage(); 794 if (causeMsg != null) { 795 strb.append(", "); 796 strb.append(causeMsg); 797 } 798 } 799 return new Exception(strb.toString(), xany); 800 } 801 802 /** The different parsing states. */ 803 private static enum ParseState { 804 /** Parsing a constant. */ 805 CONST, 806 /** Parsing after $ .*/ 807 IMMEDIATE0, 808 /** Parsing after # .*/ 809 DEFERRED0, 810 /** Parsing after ${ .*/ 811 IMMEDIATE1, 812 /** Parsing after #{ .*/ 813 DEFERRED1, 814 /** Parsing after \ .*/ 815 ESCAPE 816 } 817 818 /** 819 * Parses a unified expression. 820 * @param expr the string expression 821 * @param scope the expression scope 822 * @return the expression instance 823 * @throws JexlException if an error occur during parsing 824 */ 825 private Expression parseExpression(String expr, JexlEngine.Scope scope) { 826 final int size = expr.length(); 827 ExpressionBuilder builder = new ExpressionBuilder(0); 828 StringBuilder strb = new StringBuilder(size); 829 ParseState state = ParseState.CONST; 830 int inner = 0; 831 boolean nested = false; 832 int inested = -1; 833 for (int i = 0; i < size; ++i) { 834 char c = expr.charAt(i); 835 switch (state) { 836 default: // in case we ever add new expression type 837 throw new UnsupportedOperationException("unexpected expression type"); 838 case CONST: 839 if (c == IMM_CHAR) { 840 state = ParseState.IMMEDIATE0; 841 } else if (c == DEF_CHAR) { 842 inested = i; 843 state = ParseState.DEFERRED0; 844 } else if (c == '\\') { 845 state = ParseState.ESCAPE; 846 } else { 847 // do buildup expr 848 strb.append(c); 849 } 850 break; 851 case IMMEDIATE0: // $ 852 if (c == '{') { 853 state = ParseState.IMMEDIATE1; 854 // if chars in buffer, create constant 855 if (strb.length() > 0) { 856 Expression cexpr = new ConstantExpression(strb.toString(), null); 857 builder.add(cexpr); 858 strb.delete(0, Integer.MAX_VALUE); 859 } 860 } else { 861 // revert to CONST 862 strb.append(IMM_CHAR); 863 strb.append(c); 864 state = ParseState.CONST; 865 } 866 break; 867 case DEFERRED0: // # 868 if (c == '{') { 869 state = ParseState.DEFERRED1; 870 // if chars in buffer, create constant 871 if (strb.length() > 0) { 872 Expression cexpr = new ConstantExpression(strb.toString(), null); 873 builder.add(cexpr); 874 strb.delete(0, Integer.MAX_VALUE); 875 } 876 } else { 877 // revert to CONST 878 strb.append(DEF_CHAR); 879 strb.append(c); 880 state = ParseState.CONST; 881 } 882 break; 883 case IMMEDIATE1: // ${... 884 if (c == '}') { 885 // materialize the immediate expr 886 Expression iexpr = new ImmediateExpression( 887 strb.toString(), 888 jexl.parse(strb, null, scope), 889 null); 890 builder.add(iexpr); 891 strb.delete(0, Integer.MAX_VALUE); 892 state = ParseState.CONST; 893 } else { 894 // do buildup expr 895 strb.append(c); 896 } 897 break; 898 case DEFERRED1: // #{... 899 // skip inner strings (for '}') 900 if (c == '"' || c == '\'') { 901 strb.append(c); 902 i = StringParser.readString(strb, expr, i + 1, c); 903 continue; 904 } 905 // nested immediate in deferred; need to balance count of '{' & '}' 906 if (c == '{') { 907 if (expr.charAt(i - 1) == IMM_CHAR) { 908 inner += 1; 909 strb.deleteCharAt(strb.length() - 1); 910 nested = true; 911 } 912 continue; 913 } 914 // closing '}' 915 if (c == '}') { 916 // balance nested immediate 917 if (inner > 0) { 918 inner -= 1; 919 } else { 920 // materialize the nested/deferred expr 921 Expression dexpr = null; 922 if (nested) { 923 dexpr = new NestedExpression( 924 expr.substring(inested, i + 1), 925 jexl.parse(strb, null, scope), 926 null); 927 } else { 928 dexpr = new DeferredExpression( 929 strb.toString(), 930 jexl.parse(strb, null, scope), 931 null); 932 } 933 builder.add(dexpr); 934 strb.delete(0, Integer.MAX_VALUE); 935 nested = false; 936 state = ParseState.CONST; 937 } 938 } else { 939 // do buildup expr 940 strb.append(c); 941 } 942 break; 943 case ESCAPE: 944 if (c == DEF_CHAR) { 945 strb.append(DEF_CHAR); 946 } else if (c == IMM_CHAR) { 947 strb.append(IMM_CHAR); 948 } else { 949 strb.append('\\'); 950 strb.append(c); 951 } 952 state = ParseState.CONST; 953 } 954 } 955 // we should be in that state 956 if (state != ParseState.CONST) { 957 throw new Exception("malformed expression: " + expr, null); 958 } 959 // if any chars were buffered, add them as a constant 960 if (strb.length() > 0) { 961 Expression cexpr = new ConstantExpression(strb.toString(), null); 962 builder.add(cexpr); 963 } 964 return builder.build(this, null); 965 } 966 967 /** 968 * The enum capturing the difference between verbatim and code source fragments. 969 */ 970 private static enum BlockType { 971 /** Block is to be output "as is". */ 972 VERBATIM, 973 /** Block is a directive, ie a fragment of code. */ 974 DIRECTIVE; 975 } 976 977 /** 978 * Abstract the source fragments, verbatim or immediate typed text blocks. 979 * @since 2.1 980 */ 981 private static final class TemplateBlock { 982 /** The type of block, verbatim or directive. */ 983 private final BlockType type; 984 /** The actual contexnt. */ 985 private final String body; 986 987 /** 988 * Creates a new block. 989 * @param theType the type 990 * @param theBlock the content 991 */ 992 TemplateBlock(BlockType theType, String theBlock) { 993 type = theType; 994 body = theBlock; 995 } 996 997 @Override 998 public String toString() { 999 return body; 1000 } 1001 } 1002 1003 /** 1004 * A Template is a script that evaluates by writing its content through a Writer. 1005 * This is a simplified replacement for Velocity that uses JEXL (instead of OGNL/VTL) as the scripting 1006 * language. 1007 * <p> 1008 * The source text is parsed considering each line beginning with '$$' (as default pattern) as JEXL script code 1009 * and all others as Unified JEXL expressions; those expressions will be invoked from the script during 1010 * evaluation and their output gathered through a writer. 1011 * It is thus possible to use looping or conditional construct "around" expressions generating output. 1012 * </p> 1013 * For instance: 1014 * <p><blockquote><pre> 1015 * $$ for(var x : [1, 3, 5, 42, 169]) { 1016 * $$ if (x == 42) { 1017 * Life, the universe, and everything 1018 * $$ } else if (x > 42) { 1019 * The value $(x} is over fourty-two 1020 * $$ } else { 1021 * The value ${x} is under fourty-two 1022 * $$ } 1023 * $$ } 1024 * </pre></blockquote> 1025 * Will evaluate as: 1026 * <p><blockquote><pre> 1027 * The value 1 is under fourty-two 1028 * The value 3 is under fourty-two 1029 * The value 5 is under fourty-two 1030 * Life, the universe, and everything 1031 * The value 169 is over fourty-two 1032 * </pre></blockquote> 1033 * <p> 1034 * During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case. 1035 * This allows writing directly through the writer without adding new-lines as in: 1036 * <p><blockquote><pre> 1037 * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') } 1038 * </pre></blockquote> 1039 * </p> 1040 * <p> 1041 * A template is expanded as one JEXL script and a list of UnifiedJEXL expressions; each UnifiedJEXL expression 1042 * being replace in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template). 1043 * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:) 1044 * and stores the expression array and the writer (java.io.Writer) that the 'jexl:print(...)' 1045 * delegates the output generation to. 1046 * </p> 1047 * @since 2.1 1048 */ 1049 public final class Template { 1050 /** The prefix marker. */ 1051 private final String prefix; 1052 /** The array of source blocks. */ 1053 private final TemplateBlock[] source; 1054 /** The resulting script. */ 1055 private final ASTJexlScript script; 1056 /** The UnifiedJEXL expressions called by the script. */ 1057 private final Expression[] exprs; 1058 1059 /** 1060 * Creates a new template from an input. 1061 * @param directive the prefix for lines of code; can not be "$", "${", "#" or "#{" 1062 * since this would preclude being able to differentiate directives and UnifiedJEXL expressions 1063 * @param reader the input reader 1064 * @param parms the parameter names 1065 * @throws NullPointerException if either the directive prefix or input is null 1066 * @throws IllegalArgumentException if the directive prefix is invalid 1067 */ 1068 public Template(String directive, Reader reader, String... parms) { 1069 if (directive == null) { 1070 throw new NullPointerException("null prefix"); 1071 } 1072 if ("$".equals(directive) 1073 || "${".equals(directive) 1074 || "#".equals(directive) 1075 || "#{".equals(directive)) { 1076 throw new IllegalArgumentException(directive + ": is not a valid directive pattern"); 1077 } 1078 if (reader == null) { 1079 throw new NullPointerException("null input"); 1080 } 1081 JexlEngine.Scope scope = new JexlEngine.Scope(parms); 1082 prefix = directive; 1083 List<TemplateBlock> blocks = readTemplate(prefix, reader); 1084 List<Expression> uexprs = new ArrayList<Expression>(); 1085 StringBuilder strb = new StringBuilder(); 1086 int nuexpr = 0; 1087 int codeStart = -1; 1088 for (int b = 0; b < blocks.size(); ++b) { 1089 TemplateBlock block = blocks.get(b); 1090 if (block.type == BlockType.VERBATIM) { 1091 strb.append("jexl:print("); 1092 strb.append(nuexpr++); 1093 strb.append(");"); 1094 } else { 1095 // keep track of first block of code, the frame creator 1096 if (codeStart < 0) { 1097 codeStart = b; 1098 } 1099 strb.append(block.body); 1100 } 1101 } 1102 // parse the script 1103 script = getEngine().parse(strb.toString(), null, scope); 1104 scope = script.getScope(); 1105 // parse the exprs using the code frame for those appearing after the first block of code 1106 for (int b = 0; b < blocks.size(); ++b) { 1107 TemplateBlock block = blocks.get(b); 1108 if (block.type == BlockType.VERBATIM) { 1109 uexprs.add(UnifiedJEXL.this.parseExpression(block.body, b > codeStart ? scope : null)); 1110 } 1111 } 1112 source = blocks.toArray(new TemplateBlock[blocks.size()]); 1113 exprs = uexprs.toArray(new Expression[uexprs.size()]); 1114 } 1115 1116 /** 1117 * Private ctor used to expand deferred expressions during prepare. 1118 * @param thePrefix the directive prefix 1119 * @param theSource the source 1120 * @param theScript the script 1121 * @param theExprs the expressions 1122 */ 1123 private Template(String thePrefix, TemplateBlock[] theSource, ASTJexlScript theScript, Expression[] theExprs) { 1124 prefix = thePrefix; 1125 source = theSource; 1126 script = theScript; 1127 exprs = theExprs; 1128 } 1129 1130 @Override 1131 public String toString() { 1132 StringBuilder strb = new StringBuilder(); 1133 for (TemplateBlock block : source) { 1134 if (block.type == BlockType.DIRECTIVE) { 1135 strb.append(prefix); 1136 } 1137 strb.append(block.toString()); 1138 strb.append('\n'); 1139 } 1140 return strb.toString(); 1141 } 1142 1143 /** 1144 * Recreate the template source from its inner components. 1145 * @return the template source rewritten 1146 */ 1147 public String asString() { 1148 StringBuilder strb = new StringBuilder(); 1149 int e = 0; 1150 for (int b = 0; b < source.length; ++b) { 1151 TemplateBlock block = source[b]; 1152 if (block.type == BlockType.DIRECTIVE) { 1153 strb.append(prefix); 1154 } else { 1155 exprs[e++].asString(strb); 1156 } 1157 } 1158 return strb.toString(); 1159 } 1160 1161 /** 1162 * Prepares this template by expanding any contained deferred expression. 1163 * @param context the context to prepare against 1164 * @return the prepared version of the template 1165 */ 1166 public Template prepare(JexlContext context) { 1167 JexlEngine.Frame frame = script.createFrame((Object[]) null); 1168 TemplateContext tcontext = new TemplateContext(context, frame, exprs, null); 1169 Expression[] immediates = new Expression[exprs.length]; 1170 for (int e = 0; e < exprs.length; ++e) { 1171 immediates[e] = exprs[e].prepare(tcontext); 1172 } 1173 return new Template(prefix, source, script, immediates); 1174 } 1175 1176 /** 1177 * Evaluates this template. 1178 * @param context the context to use during evaluation 1179 * @param writer the writer to use for output 1180 */ 1181 public void evaluate(JexlContext context, Writer writer) { 1182 evaluate(context, writer, (Object[]) null); 1183 } 1184 1185 /** 1186 * Evaluates this template. 1187 * @param context the context to use during evaluation 1188 * @param writer the writer to use for output 1189 * @param args the arguments 1190 */ 1191 public void evaluate(JexlContext context, Writer writer, Object... args) { 1192 JexlEngine.Frame frame = script.createFrame(args); 1193 TemplateContext tcontext = new TemplateContext(context, frame, exprs, writer); 1194 Interpreter interpreter = jexl.createInterpreter(tcontext, !jexl.isLenient(), false); 1195 interpreter.setFrame(frame); 1196 interpreter.interpret(script); 1197 } 1198 } 1199 1200 /** 1201 * The type of context to use during evaluation of templates. 1202 * <p>This context exposes its writer as '$jexl' to the scripts.</p> 1203 * <p>public for introspection purpose.</p> 1204 * @since 2.1 1205 */ 1206 public final class TemplateContext implements JexlContext, NamespaceResolver { 1207 /** The wrapped context. */ 1208 private final JexlContext wrap; 1209 /** The array of UnifiedJEXL expressions. */ 1210 private final Expression[] exprs; 1211 /** The writer used to output. */ 1212 private final Writer writer; 1213 /** The call frame. */ 1214 private final JexlEngine.Frame frame; 1215 1216 /** 1217 * Creates a template context instance. 1218 * @param jcontext the base context 1219 * @param jframe the calling frame 1220 * @param expressions the list of expression from the template to evaluate 1221 * @param out the output writer 1222 */ 1223 protected TemplateContext(JexlContext jcontext, JexlEngine.Frame jframe, Expression[] expressions, Writer out) { 1224 wrap = jcontext; 1225 frame = jframe; 1226 exprs = expressions; 1227 writer = out; 1228 } 1229 1230 /** 1231 * Gets this context calling frame. 1232 * @return the engine frame 1233 */ 1234 public JexlEngine.Frame getFrame() { 1235 return frame; 1236 } 1237 1238 /** {@inheritDoc} */ 1239 public Object get(String name) { 1240 if ("$jexl".equals(name)) { 1241 return writer; 1242 } else { 1243 return wrap.get(name); 1244 } 1245 } 1246 1247 /** {@inheritDoc} */ 1248 public void set(String name, Object value) { 1249 wrap.set(name, value); 1250 } 1251 1252 /** {@inheritDoc} */ 1253 public boolean has(String name) { 1254 return wrap.has(name); 1255 } 1256 1257 /** {@inheritDoc} */ 1258 public Object resolveNamespace(String ns) { 1259 if ("jexl".equals(ns)) { 1260 return this; 1261 } else if (wrap instanceof NamespaceResolver) { 1262 return ((NamespaceResolver) wrap).resolveNamespace(ns); 1263 } else { 1264 return null; 1265 } 1266 } 1267 1268 /** 1269 * Includes a call to another template. 1270 * <p>Evaluates a template using this template initial context and writer.</p> 1271 * @param template the template to evaluate 1272 * @param args the arguments 1273 */ 1274 public void include(Template template, Object... args) { 1275 template.evaluate(wrap, writer, args); 1276 } 1277 1278 /** 1279 * Prints an expression result. 1280 * @param e the expression number 1281 */ 1282 public void print(int e) { 1283 if (e < 0 || e >= exprs.length) { 1284 return; 1285 } 1286 Expression expr = exprs[e]; 1287 if (expr.isDeferred()) { 1288 expr = expr.prepare(wrap); 1289 } 1290 if (expr instanceof CompositeExpression) { 1291 printComposite((CompositeExpression) expr); 1292 } else { 1293 doPrint(expr.evaluate(this)); 1294 } 1295 } 1296 1297 /** 1298 * Prints a composite expression. 1299 * @param composite the composite expression 1300 */ 1301 protected void printComposite(CompositeExpression composite) { 1302 Expression[] cexprs = composite.exprs; 1303 final int size = cexprs.length; 1304 Object value = null; 1305 for (int e = 0; e < size; ++e) { 1306 value = cexprs[e].evaluate(this); 1307 doPrint(value); 1308 } 1309 } 1310 1311 /** 1312 * Prints to output. 1313 * <p>This will dynamically try to find the best suitable method in the writer through uberspection. 1314 * Subclassing Writer by adding 'print' methods should be the preferred way to specialize output. 1315 * </p> 1316 * @param arg the argument to print out 1317 */ 1318 private void doPrint(Object arg) { 1319 try { 1320 if (arg instanceof CharSequence) { 1321 writer.write(arg.toString()); 1322 } else if (arg != null) { 1323 Object[] value = {arg}; 1324 Uberspect uber = getEngine().getUberspect(); 1325 JexlMethod method = uber.getMethod(writer, "print", value, null); 1326 if (method != null) { 1327 method.invoke(writer, value); 1328 } else { 1329 writer.write(arg.toString()); 1330 } 1331 } 1332 } catch (java.io.IOException xio) { 1333 throw createException("call print", null, xio); 1334 } catch (java.lang.Exception xany) { 1335 throw createException("invoke print", null, xany); 1336 } 1337 } 1338 } 1339 1340 /** 1341 * Whether a sequence starts with a given set of characters (following spaces). 1342 * <p>Space characters at beginning of line before the pattern are discarded.</p> 1343 * @param sequence the sequence 1344 * @param pattern the pattern to match at start of sequence 1345 * @return the first position after end of pattern if it matches, -1 otherwise 1346 * @since 2.1 1347 */ 1348 protected int startsWith(CharSequence sequence, CharSequence pattern) { 1349 int s = 0; 1350 while (Character.isSpaceChar(sequence.charAt(s))) { 1351 s += 1; 1352 } 1353 sequence = sequence.subSequence(s, sequence.length()); 1354 if (pattern.length() <= sequence.length() 1355 && sequence.subSequence(0, pattern.length()).equals(pattern)) { 1356 return s + pattern.length(); 1357 } else { 1358 return -1; 1359 } 1360 } 1361 1362 /** 1363 * Reads lines of a template grouping them by typed blocks. 1364 * @param prefix the directive prefix 1365 * @param source the source reader 1366 * @return the list of blocks 1367 * @since 2.1 1368 */ 1369 protected List<TemplateBlock> readTemplate(final String prefix, Reader source) { 1370 try { 1371 int prefixLen = prefix.length(); 1372 List<TemplateBlock> blocks = new ArrayList<TemplateBlock>(); 1373 BufferedReader reader; 1374 if (source instanceof BufferedReader) { 1375 reader = (BufferedReader) source; 1376 } else { 1377 reader = new BufferedReader(source); 1378 } 1379 StringBuilder strb = new StringBuilder(); 1380 BlockType type = null; 1381 while (true) { 1382 CharSequence line = reader.readLine(); 1383 if (line == null) { 1384 // at end 1385 TemplateBlock block = new TemplateBlock(type, strb.toString()); 1386 blocks.add(block); 1387 break; 1388 } else if (type == null) { 1389 // determine starting type if not known yet 1390 prefixLen = startsWith(line, prefix); 1391 if (prefixLen >= 0) { 1392 type = BlockType.DIRECTIVE; 1393 strb.append(line.subSequence(prefixLen, line.length())); 1394 } else { 1395 type = BlockType.VERBATIM; 1396 strb.append(line.subSequence(0, line.length())); 1397 strb.append('\n'); 1398 } 1399 } else if (type == BlockType.DIRECTIVE) { 1400 // switch to verbatim if necessary 1401 prefixLen = startsWith(line, prefix); 1402 if (prefixLen < 0) { 1403 TemplateBlock code = new TemplateBlock(BlockType.DIRECTIVE, strb.toString()); 1404 strb.delete(0, Integer.MAX_VALUE); 1405 blocks.add(code); 1406 type = BlockType.VERBATIM; 1407 strb.append(line.subSequence(0, line.length())); 1408 } else { 1409 strb.append(line.subSequence(prefixLen, line.length())); 1410 } 1411 } else if (type == BlockType.VERBATIM) { 1412 // switch to code if necessary( 1413 prefixLen = startsWith(line, prefix); 1414 if (prefixLen >= 0) { 1415 strb.append('\n'); 1416 TemplateBlock verbatim = new TemplateBlock(BlockType.VERBATIM, strb.toString()); 1417 strb.delete(0, Integer.MAX_VALUE); 1418 blocks.add(verbatim); 1419 type = BlockType.DIRECTIVE; 1420 strb.append(line.subSequence(prefixLen, line.length())); 1421 } else { 1422 strb.append(line.subSequence(0, line.length())); 1423 } 1424 } 1425 } 1426 return blocks; 1427 } catch (IOException xio) { 1428 return null; 1429 } 1430 } 1431 1432 /** 1433 * Creates a new template. 1434 * @param prefix the directive prefix 1435 * @param source the source 1436 * @param parms the parameter names 1437 * @return the template 1438 * @since 2.1 1439 */ 1440 public Template createTemplate(String prefix, Reader source, String... parms) { 1441 return new Template(prefix, source, parms); 1442 } 1443 1444 /** 1445 * Creates a new template. 1446 * @param source the source 1447 * @param parms the parameter names 1448 * @return the template 1449 * @since 2.1 1450 */ 1451 public Template createTemplate(String source, String... parms) { 1452 return new Template("$$", new StringReader(source), parms); 1453 } 1454 1455 /** 1456 * Creates a new template. 1457 * @param source the source 1458 * @return the template 1459 * @since 2.1 1460 */ 1461 public Template createTemplate(String source) { 1462 return new Template("$$", new StringReader(source), (String[]) null); 1463 } 1464 }