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 */ 017package org.apache.commons.jexl2; 018 019import java.io.BufferedReader; 020import java.io.IOException; 021import java.io.Reader; 022import java.io.StringReader; 023import java.io.Writer; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Set; 029import org.apache.commons.jexl2.introspection.JexlMethod; 030import org.apache.commons.jexl2.introspection.Uberspect; 031import org.apache.commons.jexl2.parser.ASTJexlScript; 032import org.apache.commons.jexl2.parser.JexlNode; 033import 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 * </p> 042 * <ul> 043 * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li> 044 * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li> 045 * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li> 046 * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li> 047 * </ul> 048 * <p> 049 * Deferred & immediate expression carry different intentions: 050 * </p> 051 * <ul> 052 * <li>An immediate expression indicate that evaluation is intended to be performed close to 053 * the definition/parsing point.</li> 054 * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li> 055 * </ul> 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 */ 087public 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 * <p>For instance: 1014 * </p> 1015 * <blockquote><pre> 1016 * $$ for(var x : [1, 3, 5, 42, 169]) { 1017 * $$ if (x == 42) { 1018 * Life, the universe, and everything 1019 * $$ } else if (x > 42) { 1020 * The value $(x} is over fourty-two 1021 * $$ } else { 1022 * The value ${x} is under fourty-two 1023 * $$ } 1024 * $$ } 1025 * </pre></blockquote> 1026 * <p> 1027 * Will evaluate as: 1028 * </p> 1029 * <blockquote><pre> 1030 * The value 1 is under fourty-two 1031 * The value 3 is under fourty-two 1032 * The value 5 is under fourty-two 1033 * Life, the universe, and everything 1034 * The value 169 is over fourty-two 1035 * </pre></blockquote> 1036 * <p> 1037 * During evaluation, the template context exposes its writer as '$jexl' which is safe to use in this case. 1038 * This allows writing directly through the writer without adding new-lines as in: 1039 * </p> 1040 * <blockquote><pre> 1041 * $$ for(var cell : cells) { $jexl.print(cell); $jexl.print(';') } 1042 * </pre></blockquote> 1043 * <p> 1044 * A template is expanded as one JEXL script and a list of UnifiedJEXL expressions; each UnifiedJEXL expression 1045 * being replace in the script by a call to jexl:print(expr) (the expr is in fact the expr number in the template). 1046 * This integration uses a specialized JexlContext (TemplateContext) that serves as a namespace (for jexl:) 1047 * and stores the expression array and the writer (java.io.Writer) that the 'jexl:print(...)' 1048 * delegates the output generation to. 1049 * </p> 1050 * @since 2.1 1051 */ 1052 public final class Template { 1053 /** The prefix marker. */ 1054 private final String prefix; 1055 /** The array of source blocks. */ 1056 private final TemplateBlock[] source; 1057 /** The resulting script. */ 1058 private final ASTJexlScript script; 1059 /** The UnifiedJEXL expressions called by the script. */ 1060 private final Expression[] exprs; 1061 1062 /** 1063 * Creates a new template from an input. 1064 * @param directive the prefix for lines of code; can not be "$", "${", "#" or "#{" 1065 * since this would preclude being able to differentiate directives and UnifiedJEXL expressions 1066 * @param reader the input reader 1067 * @param parms the parameter names 1068 * @throws NullPointerException if either the directive prefix or input is null 1069 * @throws IllegalArgumentException if the directive prefix is invalid 1070 */ 1071 public Template(String directive, Reader reader, String... parms) { 1072 if (directive == null) { 1073 throw new NullPointerException("null prefix"); 1074 } 1075 if ("$".equals(directive) 1076 || "${".equals(directive) 1077 || "#".equals(directive) 1078 || "#{".equals(directive)) { 1079 throw new IllegalArgumentException(directive + ": is not a valid directive pattern"); 1080 } 1081 if (reader == null) { 1082 throw new NullPointerException("null input"); 1083 } 1084 JexlEngine.Scope scope = new JexlEngine.Scope(parms); 1085 prefix = directive; 1086 List<TemplateBlock> blocks = readTemplate(prefix, reader); 1087 List<Expression> uexprs = new ArrayList<Expression>(); 1088 StringBuilder strb = new StringBuilder(); 1089 int nuexpr = 0; 1090 int codeStart = -1; 1091 for (int b = 0; b < blocks.size(); ++b) { 1092 TemplateBlock block = blocks.get(b); 1093 if (block.type == BlockType.VERBATIM) { 1094 strb.append("jexl:print("); 1095 strb.append(nuexpr++); 1096 strb.append(");"); 1097 } else { 1098 // keep track of first block of code, the frame creator 1099 if (codeStart < 0) { 1100 codeStart = b; 1101 } 1102 strb.append(block.body); 1103 } 1104 } 1105 // parse the script 1106 script = getEngine().parse(strb.toString(), null, scope); 1107 scope = script.getScope(); 1108 // parse the exprs using the code frame for those appearing after the first block of code 1109 for (int b = 0; b < blocks.size(); ++b) { 1110 TemplateBlock block = blocks.get(b); 1111 if (block.type == BlockType.VERBATIM) { 1112 uexprs.add(UnifiedJEXL.this.parseExpression(block.body, b > codeStart ? scope : null)); 1113 } 1114 } 1115 source = blocks.toArray(new TemplateBlock[blocks.size()]); 1116 exprs = uexprs.toArray(new Expression[uexprs.size()]); 1117 } 1118 1119 /** 1120 * Private ctor used to expand deferred expressions during prepare. 1121 * @param thePrefix the directive prefix 1122 * @param theSource the source 1123 * @param theScript the script 1124 * @param theExprs the expressions 1125 */ 1126 private Template(String thePrefix, TemplateBlock[] theSource, ASTJexlScript theScript, Expression[] theExprs) { 1127 prefix = thePrefix; 1128 source = theSource; 1129 script = theScript; 1130 exprs = theExprs; 1131 } 1132 1133 @Override 1134 public String toString() { 1135 StringBuilder strb = new StringBuilder(); 1136 for (TemplateBlock block : source) { 1137 if (block.type == BlockType.DIRECTIVE) { 1138 strb.append(prefix); 1139 } 1140 strb.append(block.toString()); 1141 strb.append('\n'); 1142 } 1143 return strb.toString(); 1144 } 1145 1146 /** 1147 * Recreate the template source from its inner components. 1148 * @return the template source rewritten 1149 */ 1150 public String asString() { 1151 StringBuilder strb = new StringBuilder(); 1152 int e = 0; 1153 for (int b = 0; b < source.length; ++b) { 1154 TemplateBlock block = source[b]; 1155 if (block.type == BlockType.DIRECTIVE) { 1156 strb.append(prefix); 1157 } else { 1158 exprs[e++].asString(strb); 1159 } 1160 } 1161 return strb.toString(); 1162 } 1163 1164 /** 1165 * Prepares this template by expanding any contained deferred expression. 1166 * @param context the context to prepare against 1167 * @return the prepared version of the template 1168 */ 1169 public Template prepare(JexlContext context) { 1170 JexlEngine.Frame frame = script.createFrame((Object[]) null); 1171 TemplateContext tcontext = new TemplateContext(context, frame, exprs, null); 1172 Expression[] immediates = new Expression[exprs.length]; 1173 for (int e = 0; e < exprs.length; ++e) { 1174 immediates[e] = exprs[e].prepare(tcontext); 1175 } 1176 return new Template(prefix, source, script, immediates); 1177 } 1178 1179 /** 1180 * Evaluates this template. 1181 * @param context the context to use during evaluation 1182 * @param writer the writer to use for output 1183 */ 1184 public void evaluate(JexlContext context, Writer writer) { 1185 evaluate(context, writer, (Object[]) null); 1186 } 1187 1188 /** 1189 * Evaluates this template. 1190 * @param context the context to use during evaluation 1191 * @param writer the writer to use for output 1192 * @param args the arguments 1193 */ 1194 public void evaluate(JexlContext context, Writer writer, Object... args) { 1195 JexlEngine.Frame frame = script.createFrame(args); 1196 TemplateContext tcontext = new TemplateContext(context, frame, exprs, writer); 1197 Interpreter interpreter = jexl.createInterpreter(tcontext, !jexl.isLenient(), false); 1198 interpreter.setFrame(frame); 1199 interpreter.interpret(script); 1200 } 1201 } 1202 1203 /** 1204 * The type of context to use during evaluation of templates. 1205 * <p>This context exposes its writer as '$jexl' to the scripts.</p> 1206 * <p>public for introspection purpose.</p> 1207 * @since 2.1 1208 */ 1209 public final class TemplateContext implements JexlContext, NamespaceResolver { 1210 /** The wrapped context. */ 1211 private final JexlContext wrap; 1212 /** The array of UnifiedJEXL expressions. */ 1213 private final Expression[] exprs; 1214 /** The writer used to output. */ 1215 private final Writer writer; 1216 /** The call frame. */ 1217 private final JexlEngine.Frame frame; 1218 1219 /** 1220 * Creates a template context instance. 1221 * @param jcontext the base context 1222 * @param jframe the calling frame 1223 * @param expressions the list of expression from the template to evaluate 1224 * @param out the output writer 1225 */ 1226 protected TemplateContext(JexlContext jcontext, JexlEngine.Frame jframe, Expression[] expressions, Writer out) { 1227 wrap = jcontext; 1228 frame = jframe; 1229 exprs = expressions; 1230 writer = out; 1231 } 1232 1233 /** 1234 * Gets this context calling frame. 1235 * @return the engine frame 1236 */ 1237 public JexlEngine.Frame getFrame() { 1238 return frame; 1239 } 1240 1241 /** {@inheritDoc} */ 1242 public Object get(String name) { 1243 if ("$jexl".equals(name)) { 1244 return writer; 1245 } else { 1246 return wrap.get(name); 1247 } 1248 } 1249 1250 /** {@inheritDoc} */ 1251 public void set(String name, Object value) { 1252 wrap.set(name, value); 1253 } 1254 1255 /** {@inheritDoc} */ 1256 public boolean has(String name) { 1257 return wrap.has(name); 1258 } 1259 1260 /** {@inheritDoc} */ 1261 public Object resolveNamespace(String ns) { 1262 if ("jexl".equals(ns)) { 1263 return this; 1264 } else if (wrap instanceof NamespaceResolver) { 1265 return ((NamespaceResolver) wrap).resolveNamespace(ns); 1266 } else { 1267 return null; 1268 } 1269 } 1270 1271 /** 1272 * Includes a call to another template. 1273 * <p>Evaluates a template using this template initial context and writer.</p> 1274 * @param template the template to evaluate 1275 * @param args the arguments 1276 */ 1277 public void include(Template template, Object... args) { 1278 template.evaluate(wrap, writer, args); 1279 } 1280 1281 /** 1282 * Prints an expression result. 1283 * @param e the expression number 1284 */ 1285 public void print(int e) { 1286 if (e < 0 || e >= exprs.length) { 1287 return; 1288 } 1289 Expression expr = exprs[e]; 1290 if (expr.isDeferred()) { 1291 expr = expr.prepare(wrap); 1292 } 1293 if (expr instanceof CompositeExpression) { 1294 printComposite((CompositeExpression) expr); 1295 } else { 1296 doPrint(expr.evaluate(this)); 1297 } 1298 } 1299 1300 /** 1301 * Prints a composite expression. 1302 * @param composite the composite expression 1303 */ 1304 protected void printComposite(CompositeExpression composite) { 1305 Expression[] cexprs = composite.exprs; 1306 final int size = cexprs.length; 1307 Object value = null; 1308 for (int e = 0; e < size; ++e) { 1309 value = cexprs[e].evaluate(this); 1310 doPrint(value); 1311 } 1312 } 1313 1314 /** 1315 * Prints to output. 1316 * <p>This will dynamically try to find the best suitable method in the writer through uberspection. 1317 * Subclassing Writer by adding 'print' methods should be the preferred way to specialize output. 1318 * </p> 1319 * @param arg the argument to print out 1320 */ 1321 private void doPrint(Object arg) { 1322 try { 1323 if (arg instanceof CharSequence) { 1324 writer.write(arg.toString()); 1325 } else if (arg != null) { 1326 Object[] value = {arg}; 1327 Uberspect uber = getEngine().getUberspect(); 1328 JexlMethod method = uber.getMethod(writer, "print", value, null); 1329 if (method != null) { 1330 method.invoke(writer, value); 1331 } else { 1332 writer.write(arg.toString()); 1333 } 1334 } 1335 } catch (java.io.IOException xio) { 1336 throw createException("call print", null, xio); 1337 } catch (java.lang.Exception xany) { 1338 throw createException("invoke print", null, xany); 1339 } 1340 } 1341 } 1342 1343 /** 1344 * Whether a sequence starts with a given set of characters (following spaces). 1345 * <p>Space characters at beginning of line before the pattern are discarded.</p> 1346 * @param sequence the sequence 1347 * @param pattern the pattern to match at start of sequence 1348 * @return the first position after end of pattern if it matches, -1 otherwise 1349 * @since 2.1 1350 */ 1351 protected int startsWith(CharSequence sequence, CharSequence pattern) { 1352 int s = 0; 1353 while (Character.isSpaceChar(sequence.charAt(s))) { 1354 s += 1; 1355 } 1356 sequence = sequence.subSequence(s, sequence.length()); 1357 if (pattern.length() <= sequence.length() 1358 && sequence.subSequence(0, pattern.length()).equals(pattern)) { 1359 return s + pattern.length(); 1360 } else { 1361 return -1; 1362 } 1363 } 1364 1365 /** 1366 * Reads lines of a template grouping them by typed blocks. 1367 * @param prefix the directive prefix 1368 * @param source the source reader 1369 * @return the list of blocks 1370 * @since 2.1 1371 */ 1372 protected List<TemplateBlock> readTemplate(final String prefix, Reader source) { 1373 try { 1374 int prefixLen = prefix.length(); 1375 List<TemplateBlock> blocks = new ArrayList<TemplateBlock>(); 1376 BufferedReader reader; 1377 if (source instanceof BufferedReader) { 1378 reader = (BufferedReader) source; 1379 } else { 1380 reader = new BufferedReader(source); 1381 } 1382 StringBuilder strb = new StringBuilder(); 1383 BlockType type = null; 1384 while (true) { 1385 CharSequence line = reader.readLine(); 1386 if (line == null) { 1387 // at end 1388 TemplateBlock block = new TemplateBlock(type, strb.toString()); 1389 blocks.add(block); 1390 break; 1391 } else if (type == null) { 1392 // determine starting type if not known yet 1393 prefixLen = startsWith(line, prefix); 1394 if (prefixLen >= 0) { 1395 type = BlockType.DIRECTIVE; 1396 strb.append(line.subSequence(prefixLen, line.length())); 1397 } else { 1398 type = BlockType.VERBATIM; 1399 strb.append(line.subSequence(0, line.length())); 1400 strb.append('\n'); 1401 } 1402 } else if (type == BlockType.DIRECTIVE) { 1403 // switch to verbatim if necessary 1404 prefixLen = startsWith(line, prefix); 1405 if (prefixLen < 0) { 1406 TemplateBlock code = new TemplateBlock(BlockType.DIRECTIVE, strb.toString()); 1407 strb.delete(0, Integer.MAX_VALUE); 1408 blocks.add(code); 1409 type = BlockType.VERBATIM; 1410 strb.append(line.subSequence(0, line.length())); 1411 } else { 1412 strb.append(line.subSequence(prefixLen, line.length())); 1413 } 1414 } else if (type == BlockType.VERBATIM) { 1415 // switch to code if necessary( 1416 prefixLen = startsWith(line, prefix); 1417 if (prefixLen >= 0) { 1418 strb.append('\n'); 1419 TemplateBlock verbatim = new TemplateBlock(BlockType.VERBATIM, strb.toString()); 1420 strb.delete(0, Integer.MAX_VALUE); 1421 blocks.add(verbatim); 1422 type = BlockType.DIRECTIVE; 1423 strb.append(line.subSequence(prefixLen, line.length())); 1424 } else { 1425 strb.append(line.subSequence(0, line.length())); 1426 } 1427 } 1428 } 1429 return blocks; 1430 } catch (IOException xio) { 1431 return null; 1432 } 1433 } 1434 1435 /** 1436 * Creates a new template. 1437 * @param prefix the directive prefix 1438 * @param source the source 1439 * @param parms the parameter names 1440 * @return the template 1441 * @since 2.1 1442 */ 1443 public Template createTemplate(String prefix, Reader source, String... parms) { 1444 return new Template(prefix, source, parms); 1445 } 1446 1447 /** 1448 * Creates a new template. 1449 * @param source the source 1450 * @param parms the parameter names 1451 * @return the template 1452 * @since 2.1 1453 */ 1454 public Template createTemplate(String source, String... parms) { 1455 return new Template("$$", new StringReader(source), parms); 1456 } 1457 1458 /** 1459 * Creates a new template. 1460 * @param source the source 1461 * @return the template 1462 * @since 2.1 1463 */ 1464 public Template createTemplate(String source) { 1465 return new Template("$$", new StringReader(source), (String[]) null); 1466 } 1467}