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 }