View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.Reader;
22  import java.io.StringReader;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.Set;
29  
30  import org.apache.commons.jexl3.JexlCache;
31  import org.apache.commons.jexl3.JexlContext;
32  import org.apache.commons.jexl3.JexlException;
33  import org.apache.commons.jexl3.JexlFeatures;
34  import org.apache.commons.jexl3.JexlInfo;
35  import org.apache.commons.jexl3.JexlOptions;
36  import org.apache.commons.jexl3.JxltEngine;
37  import org.apache.commons.jexl3.parser.ASTJexlScript;
38  import org.apache.commons.jexl3.parser.JexlNode;
39  import org.apache.commons.jexl3.parser.StringParser;
40  import org.apache.commons.logging.Log;
41  
42  /**
43   * A JxltEngine implementation.
44   * @since 3.0
45   */
46  public final class TemplateEngine extends JxltEngine {
47      /**
48       * Abstract the source fragments, verbatim or immediate typed text blocks.
49       */
50      static final class Block {
51          /** The type of block: verbatim or directive. */
52          private final BlockType type;
53          /** The block start line info. */
54          private final int line;
55          /** The actual content. */
56          private final String body;
57  
58          /**
59           * Creates a new block.
60           * @param theType  the block type
61           * @param theLine  the line number
62           * @param theBlock the content
63           */
64          Block(final BlockType theType, final int theLine, final String theBlock) {
65              type = theType;
66              line = theLine;
67              body = theBlock;
68          }
69  
70          /**
71           * @return body
72           */
73          String getBody() {
74              return body;
75          }
76  
77          /**
78           * @return line
79           */
80          int getLine() {
81              return line;
82          }
83  
84          /**
85           * @return type
86           */
87          BlockType getType() {
88              return type;
89          }
90  
91          /**
92           * Appends this block string representation to a builder.
93           * @param strb   the string builder to append to
94           * @param prefix the line prefix (immediate or deferred)
95           */
96          void toString(final StringBuilder strb, final String prefix) {
97              if (BlockType.VERBATIM.equals(type)) {
98                  strb.append(body);
99              } else {
100                 final Iterator<CharSequence> lines = readLines(new StringReader(body));
101                 while (lines.hasNext()) {
102                     strb.append(prefix).append(lines.next());
103                 }
104             }
105         }
106     }
107     /**
108      * The enum capturing the difference between verbatim and code source fragments.
109      */
110     enum BlockType {
111         /** Block is to be output "as is" but may be a unified expression. */
112         VERBATIM,
113         /** Block is a directive, i.e., a fragment of JEXL code. */
114         DIRECTIVE
115     }
116 
117     /** A composite unified expression: "... ${...} ... #{...} ...". */
118     final class CompositeExpression extends TemplateExpression {
119         /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */
120         private final int meta;
121         /** The list of sub-expression resulting from parsing. */
122         final TemplateExpression[] exprs;
123 
124         /**
125          * Creates a composite expression.
126          * @param counters counters of expressions per type
127          * @param list     the sub-expressions
128          * @param src      the source for this expression if any
129          */
130         CompositeExpression(final int[] counters, final List<TemplateExpression> list, final TemplateExpression src) {
131             super(src);
132             this.exprs = list.toArray(new TemplateExpression[0]);
133             this.meta = (counters[ExpressionType.DEFERRED.getIndex()] > 0 ? 2 : 0)
134                     | (counters[ExpressionType.IMMEDIATE.getIndex()] > 0 ? 1 : 0);
135         }
136 
137         @Override
138         public StringBuilder asString(final StringBuilder strb) {
139             for (final TemplateExpression e : exprs) {
140                 e.asString(strb);
141             }
142             return strb;
143         }
144 
145         @Override
146         protected Object evaluate(final Interpreter interpreter) {
147             Object value;
148             // common case: evaluate all expressions & concatenate them as a string
149             final StringBuilder strb = new StringBuilder();
150             for (final TemplateExpression expr : exprs) {
151                 value = expr.evaluate(interpreter);
152                 if (value != null) {
153                     strb.append(value);
154                 }
155             }
156             return strb.toString();
157         }
158 
159         @Override
160         ExpressionType getType() {
161             return ExpressionType.COMPOSITE;
162         }
163 
164         @Override
165         public Set<List<String>> getVariables() {
166             final Engine.VarCollector collector = jexl.varCollector();
167             for (final TemplateExpression expr : exprs) {
168                 expr.getVariables(collector);
169             }
170             return collector.collected();
171         }
172 
173         /**
174          * Fills up the list of variables accessed by this unified expression.
175          * @param collector the variable collector
176          */
177         @Override
178         protected void getVariables(final Engine.VarCollector collector) {
179             for (final TemplateExpression expr : exprs) {
180                 expr.getVariables(collector);
181             }
182         }
183 
184         @Override
185         public boolean isImmediate() {
186             // immediate if no deferred
187             return (meta & 2) == 0;
188         }
189 
190         @Override
191         protected TemplateExpression prepare(final Interpreter interpreter) {
192             // if this composite is not its own source, it is already prepared
193             if (source != this) {
194                 return this;
195             }
196             // we need to prepare all sub-expressions
197             final int size = exprs.length;
198             final ExpressionBuilder builder = new ExpressionBuilder(size);
199             // tracking whether prepare will return a different expression
200             boolean eq = true;
201             for (final TemplateExpression expr : exprs) {
202                 final TemplateExpression prepared = expr.prepare(interpreter);
203                 // add it if not null
204                 if (prepared != null) {
205                     builder.add(prepared);
206                 }
207                 // keep track of TemplateExpression equivalence
208                 eq &= expr == prepared;
209             }
210             return eq ? this : builder.build(TemplateEngine.this, this);
211         }
212     }
213     /** A constant unified expression. */
214     final class ConstantExpression extends TemplateExpression {
215         /** The constant held by this unified expression. */
216         private final Object value;
217 
218         /**
219          * Creates a constant unified expression.
220          * <p>
221          * If the wrapped constant is a string, it is treated
222          * as a JEXL strings with respect to escaping.
223          * </p>
224          * @param val    the constant value
225          * @param source the source TemplateExpression if any
226          */
227         ConstantExpression(final Object val, final TemplateExpression source) {
228             super(source);
229             Objects.requireNonNull(val, "val");
230             this.value = val instanceof String
231                     ? StringParser.buildTemplate((String) val, false)
232                     : val;
233         }
234 
235         @Override
236         public StringBuilder asString(final StringBuilder strb) {
237             if (value != null) {
238                 strb.append(value);
239             }
240             return strb;
241         }
242 
243         @Override
244         protected Object evaluate(final Interpreter interpreter) {
245             return value;
246         }
247 
248         @Override
249         ExpressionType getType() {
250             return ExpressionType.CONSTANT;
251         }
252     }
253     /** A deferred unified expression: #{jexl}. */
254     final class DeferredExpression extends JexlBasedExpression {
255         /**
256          * Creates a deferred unified expression.
257          * @param expr   the unified expression as a string
258          * @param node   the unified expression as an AST
259          * @param source the source unified expression if any
260          */
261         DeferredExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) {
262             super(expr, node, source);
263         }
264 
265         @Override
266         ExpressionType getType() {
267             return ExpressionType.DEFERRED;
268         }
269 
270         @Override
271         protected void getVariables(final Engine.VarCollector collector) {
272             // noop
273         }
274 
275         @Override
276         public boolean isImmediate() {
277             return false;
278         }
279 
280         @Override
281         protected TemplateExpression prepare(final Interpreter interpreter) {
282             return new ImmediateExpression(expr, node, source);
283         }
284     }
285     /**
286      * A helper class to build expressions.
287      * Keeps count of sub-expressions by type.
288      */
289     static final class ExpressionBuilder {
290         /** Per TemplateExpression type counters. */
291         private final int[] counts;
292         /** The list of expressions. */
293         private final List<TemplateExpression> expressions;
294 
295         /**
296          * Creates a builder.
297          * @param size the initial TemplateExpression array size
298          */
299         ExpressionBuilder(final int size) {
300             counts = new int[]{0, 0, 0};
301             expressions = new ArrayList<>(size <= 0 ? 3 : size);
302         }
303 
304         /**
305          * Adds an TemplateExpression to the list of expressions, maintain per-type counts.
306          * @param expr the TemplateExpression to add
307          */
308         void add(final TemplateExpression expr) {
309             counts[expr.getType().getIndex()] += 1;
310             expressions.add(expr);
311         }
312 
313         /**
314          * Builds a TemplateExpression from a source, performs checks.
315          * @param el     the unified el instance
316          * @param source the source TemplateExpression
317          * @return an TemplateExpression
318          */
319         TemplateExpression build(final TemplateEngine el, final TemplateExpression source) {
320             int sum = 0;
321             for (final int count : counts) {
322                 sum += count;
323             }
324             if (expressions.size() != sum) {
325                 final StringBuilder error = new StringBuilder("parsing algorithm error: ");
326                 throw new IllegalStateException(toString(error).toString());
327             }
328             // if only one sub-expr, no need to create a composite
329             if (expressions.size() == 1) {
330                 return expressions.get(0);
331             }
332             return el.new CompositeExpression(counts, expressions, source);
333         }
334 
335         @Override
336         public String toString() {
337             return toString(new StringBuilder()).toString();
338         }
339 
340         /**
341          * Base for to-string.
342          * @param error the builder to fill
343          * @return the builder
344          */
345         StringBuilder toString(final StringBuilder error) {
346             error.append("exprs{");
347             error.append(expressions.size());
348             error.append(", constant:");
349             error.append(counts[ExpressionType.CONSTANT.getIndex()]);
350             error.append(", immediate:");
351             error.append(counts[ExpressionType.IMMEDIATE.getIndex()]);
352             error.append(", deferred:");
353             error.append(counts[ExpressionType.DEFERRED.getIndex()]);
354             error.append("}");
355             return error;
356         }
357     }
358 
359     /**
360      * Types of expressions.
361      * Each instance carries a counter index per (composite sub-) template expression type.
362      * @see ExpressionBuilder
363      */
364     enum ExpressionType {
365         /** Constant TemplateExpression, count index 0. */
366         CONSTANT(0),
367         /** Immediate TemplateExpression, count index 1. */
368         IMMEDIATE(1),
369         /** Deferred TemplateExpression, count index 2. */
370         DEFERRED(2),
371         /** Nested (which are deferred) expressions, count
372          * index 2. */
373         NESTED(2),
374         /** Composite expressions are not counted, index -1. */
375         COMPOSITE(-1);
376         /** The index in arrays of TemplateExpression counters for composite expressions. */
377         private final int index;
378 
379         /**
380          * Creates an ExpressionType.
381          * @param idx the index for this type in counters arrays.
382          */
383         ExpressionType(final int idx) {
384             this.index = idx;
385         }
386 
387         /**
388          * @return the index member
389          */
390         int getIndex() {
391             return index;
392         }
393     }
394 
395     /** An immediate unified expression: ${jexl}. */
396     final class ImmediateExpression extends JexlBasedExpression {
397         /**
398          * Creates an immediate unified expression.
399          * @param expr   the unified expression as a string
400          * @param node   the unified expression as an AST
401          * @param source the source unified expression if any
402          */
403         ImmediateExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) {
404             super(expr, node, source);
405         }
406 
407         @Override
408         ExpressionType getType() {
409             return ExpressionType.IMMEDIATE;
410         }
411 
412         @Override
413         protected TemplateExpression prepare(final Interpreter interpreter) {
414             // evaluate immediate as constant
415             final Object value = evaluate(interpreter);
416             return value != null ? new ConstantExpression(value, source) : null;
417         }
418     }
419 
420     /** The base for JEXL based unified expressions. */
421     abstract class JexlBasedExpression extends TemplateExpression {
422         /** The JEXL string for this unified expression. */
423         protected final CharSequence expr;
424         /** The JEXL node for this unified expression. */
425         protected final JexlNode node;
426 
427         /**
428          * Creates a JEXL interpretable unified expression.
429          * @param theExpr   the unified expression as a string
430          * @param theNode   the unified expression as an AST
431          * @param theSource the source unified expression if any
432          */
433         protected JexlBasedExpression(final CharSequence theExpr, final JexlNode theNode, final TemplateExpression theSource) {
434             super(theSource);
435             this.expr = theExpr;
436             this.node = theNode;
437         }
438 
439         @Override
440         public StringBuilder asString(final StringBuilder strb) {
441             strb.append(isImmediate() ? immediateChar : deferredChar);
442             strb.append("{");
443             strb.append(expr);
444             strb.append("}");
445             return strb;
446         }
447 
448         @Override
449         protected Object evaluate(final Interpreter interpreter) {
450             return interpreter.interpret(node);
451         }
452 
453         @Override
454         JexlInfo getInfo() {
455             return node.jexlInfo();
456         }
457 
458         @Override
459         public Set<List<String>> getVariables() {
460             final Engine.VarCollector collector = jexl.varCollector();
461             getVariables(collector);
462             return collector.collected();
463         }
464 
465         @Override
466         protected void getVariables(final Engine.VarCollector collector) {
467             jexl.getVariables(node instanceof ASTJexlScript? (ASTJexlScript) node : null, node, collector);
468         }
469 
470         @Override
471         protected JexlOptions options(final JexlContext context) {
472             return jexl.evalOptions(node instanceof ASTJexlScript? (ASTJexlScript) node : null, context);
473         }
474     }
475 
476     /**
477      * An immediate unified expression nested into a deferred unified expression.
478      * #{...${jexl}...}
479      * Note that the deferred syntax is JEXL's.
480      */
481     final class NestedExpression extends JexlBasedExpression {
482         /**
483          * Creates a nested unified expression.
484          * @param expr   the unified expression as a string
485          * @param node   the unified expression as an AST
486          * @param source the source unified expression if any
487          */
488         NestedExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) {
489             super(expr, node, source);
490             if (this.source != this) {
491                 throw new IllegalArgumentException("Nested TemplateExpression cannot have a source");
492             }
493         }
494 
495         @Override
496         public StringBuilder asString(final StringBuilder strb) {
497             strb.append(expr);
498             return strb;
499         }
500 
501         @Override
502         protected Object evaluate(final Interpreter interpreter) {
503             return prepare(interpreter).evaluate(interpreter);
504         }
505 
506         @Override
507         ExpressionType getType() {
508             return ExpressionType.NESTED;
509         }
510 
511         @Override
512         public boolean isImmediate() {
513             return false;
514         }
515 
516         @Override
517         protected TemplateExpression prepare(final Interpreter interpreter) {
518             final String value = interpreter.interpret(node).toString();
519             final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), noscript, value, null);
520             return new ImmediateExpression(value, dnode, this);
521         }
522     }
523 
524     /** The different parsing states. */
525     private enum ParseState {
526         /** Parsing a constant. */
527         CONST,
528         /** Parsing after <code>$</code> . */
529         IMMEDIATE0,
530         /** Parsing after <code>#</code> . */
531         DEFERRED0,
532         /** Parsing after <code>${</code> . */
533         IMMEDIATE1,
534         /** Parsing after <code>#{</code> . */
535         DEFERRED1,
536         /** Parsing after <code>\</code> . */
537         ESCAPE
538     }
539 
540     /**
541      * The abstract base class for all unified expressions, immediate '${...}' and deferred '#{...}'.
542      */
543     abstract class TemplateExpression implements Expression {
544         /** The source of this template expression(see {@link TemplateEngine.TemplateExpression#prepare}). */
545         protected final TemplateExpression source;
546 
547         /**
548          * Creates an TemplateExpression.
549          * @param src the source TemplateExpression if any
550          */
551         TemplateExpression(final TemplateExpression src) {
552             this.source = src != null ? src : this;
553         }
554 
555         @Override
556         public String asString() {
557             final StringBuilder strb = new StringBuilder();
558             asString(strb);
559             return strb.toString();
560         }
561 
562         /**
563          * Interprets a sub-expression.
564          * @param interpreter a JEXL interpreter
565          * @return the result of interpretation
566          * @throws JexlException (only for nested and composite)
567          */
568         protected abstract Object evaluate(Interpreter interpreter);
569 
570         @Override
571         public final Object evaluate(final JexlContext context) {
572             return evaluate(context, null, null);
573         }
574 
575         /**
576          * Evaluates this expression.
577          *
578          * @param frame the frame storing parameters and local variables
579          * @param context the context storing global variables
580          * @param options flags and properties that can alter the evaluation behavior.
581          * @return the expression value
582          * @throws JexlException if expression evaluation fails
583          */
584         protected final Object evaluate(final JexlContext context, final Frame frame, final JexlOptions options) {
585             try {
586                 final TemplateInterpreter.Arguments args = new TemplateInterpreter.Arguments(jexl).context(context)
587                         .options(options != null ? options : options(context)).frame(frame);
588                 final Interpreter interpreter = jexl.createTemplateInterpreter(args);
589                 return evaluate(interpreter);
590             } catch (final JexlException xjexl) {
591                 final JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl);
592                 if (jexl.isSilent()) {
593                     if (logger.isWarnEnabled()) {
594                         logger.warn(xuel.getMessage(), xuel.getCause());
595                     }
596                     return null;
597                 }
598                 throw xuel;
599             }
600         }
601 
602         /** @return the info */
603         JexlInfo getInfo() {
604             return null;
605         }
606 
607         @Override
608         public final TemplateExpression getSource() {
609             return source;
610         }
611 
612         /**
613          * Gets this TemplateExpression type.
614          * @return its type
615          */
616         abstract ExpressionType getType();
617 
618         @Override
619         public Set<List<String>> getVariables() {
620             return Collections.emptySet();
621         }
622 
623         /**
624          * Fills up the list of variables accessed by this unified expression.
625          * @param collector the variable collector
626          */
627         protected void getVariables(final Engine.VarCollector collector) {
628             // nothing to do
629         }
630 
631         @Override
632         public final boolean isDeferred() {
633             return !isImmediate();
634         }
635 
636         @Override
637         public boolean isImmediate() {
638             return true;
639         }
640 
641         /**
642          * The options to use during evaluation.
643          * @param context the context
644          * @return the options
645          */
646         protected JexlOptions options(final JexlContext context) {
647             return jexl.evalOptions(null, context);
648         }
649 
650         /**
651          * Prepares a sub-expression for interpretation.
652          * @param interpreter a JEXL interpreter
653          * @return a prepared unified expression
654          * @throws JexlException (only for nested and composite)
655          */
656         protected TemplateExpression prepare(final Interpreter interpreter) {
657             return this;
658         }
659 
660         @Override
661         public final TemplateExpression prepare(final JexlContext context) {
662                 return prepare(context, null, null);
663         }
664 
665         /**
666          * Prepares this expression.
667          *
668          * @param frame the frame storing parameters and local variables
669          * @param context the context storing global variables
670          * @param opts flags and properties that can alter the evaluation behavior.
671          * @return the expression value
672          * @throws JexlException if expression preparation fails
673          */
674         protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions opts) {
675             try {
676                 final JexlOptions interOptions = opts != null ? opts : jexl.evalOptions(context);
677                 final Interpreter interpreter = jexl.createInterpreter(context, frame, interOptions);
678                 return prepare(interpreter);
679             } catch (final JexlException xjexl) {
680                 final JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl);
681                 if (jexl.isSilent()) {
682                     if (logger.isWarnEnabled()) {
683                         logger.warn(xuel.getMessage(), xuel.getCause());
684                     }
685                     return null;
686                 }
687                 throw xuel;
688             }
689         }
690 
691         @Override
692         public final String toString() {
693             final StringBuilder strb = new StringBuilder();
694             asString(strb);
695             if (source != this) {
696                 strb.append(" /*= ");
697                 strb.append(source);
698                 strb.append(" */");
699             }
700             return strb.toString();
701         }
702     }
703 
704     /**
705      * Helper for expression dealing with embedded strings.
706      * @param strb the expression buffer to copy characters into
707      * @param expr the source
708      * @param position the offset into the source
709      * @param c the separator character
710      * @return the new position to read the source from
711      */
712     private static int append(final StringBuilder strb, final CharSequence expr, final int position, final char c) {
713         strb.append(c);
714         if (c != '"' && c != '\'') {
715             return position;
716         }
717         // read thru strings
718         final int end = expr.length();
719         boolean escape= false;
720         int index = position + 1;
721         for (; index < end; ++index) {
722             final char ec = expr.charAt(index);
723             strb.append(ec);
724             if (ec == '\\') {
725                 escape = !escape;
726             } else if (escape) {
727                 escape = false;
728             } else if (ec == c) {
729                 break;
730             }
731         }
732         return index;
733     }
734 
735     /**
736      * Creates a JxltEngine.Exception from a JexlException.
737      * @param info   the source info
738      * @param action createExpression, prepare, evaluate
739      * @param expr   the template expression
740      * @param xany   the exception
741      * @return an exception containing an explicit error message
742      */
743     static Exception createException(final JexlInfo info,
744                                      final String action,
745                                      final TemplateExpression expr,
746                                      final java.lang.Exception xany) {
747         final StringBuilder strb = new StringBuilder("failed to ");
748         strb.append(action);
749         if (expr != null) {
750             strb.append(" '");
751             strb.append(expr);
752             strb.append("'");
753         }
754         final Throwable cause = xany.getCause();
755         if (cause != null) {
756             final String causeMsg = cause.getMessage();
757             if (causeMsg != null) {
758                 strb.append(", ");
759                 strb.append(causeMsg);
760             }
761         }
762         return new Exception(info, strb.toString(), xany);
763     }
764 
765     /**
766      * Reads lines from a (buffered / mark-able) reader keeping all new-lines and line-feeds.
767      * @param reader the reader
768      * @return the line iterator
769      */
770     static Iterator<CharSequence> readLines(final Reader reader) {
771         if (!reader.markSupported()) {
772             throw new IllegalArgumentException("mark support in reader required");
773         }
774         return new Iterator<CharSequence>() {
775             private CharSequence next = doNext();
776 
777             private CharSequence doNext() {
778                 final StringBuilder strb = new StringBuilder(64); // CSOFF: MagicNumber
779                 int c;
780                 boolean eol = false;
781                 try {
782                     while ((c = reader.read()) >= 0) {
783                         if (eol) {// && (c != '\n' && c != '\r')) {
784                             reader.reset();
785                             break;
786                         }
787                         if (c == '\n') {
788                             eol = true;
789                         }
790                         strb.append((char) c);
791                         reader.mark(1);
792                     }
793                 } catch (final IOException xio) {
794                     return null;
795                 }
796                 return strb.length() > 0 ? strb : null;
797             }
798 
799             @Override
800             public boolean hasNext() {
801                 return next != null;
802             }
803 
804             @Override
805             public CharSequence next() {
806                 final CharSequence current = next;
807                 if (current != null) {
808                     next = doNext();
809                 }
810                 return current;
811             }
812         };
813     }
814 
815     /** The TemplateExpression cache. */
816     final JexlCache<Source, Object> cache;
817 
818     /** The JEXL engine instance. */
819     final Engine jexl;
820 
821     /** The logger. */
822     final Log logger;
823 
824     /** The first character for immediate expressions. */
825     final char immediateChar;
826 
827     /** The first character for deferred expressions. */
828     final char deferredChar;
829 
830     /** Whether expressions can use JEXL script or only expressions (i.e., no for, var, etc). */
831     final boolean noscript;
832 
833     /**
834      * Creates a new instance of {@link JxltEngine} creating a local cache.
835      * @param jexl     the JexlEngine to use.
836      * @param noScript  whether this engine only allows JEXL expressions or scripts
837      * @param cacheSize the number of expressions in this cache, default is 256
838      * @param immediate the immediate template expression character, default is '$'
839      * @param deferred  the deferred template expression character, default is '#'
840      */
841     public TemplateEngine(final Engine jexl,
842                           final boolean noScript,
843                           final int cacheSize,
844                           final char immediate,
845                           final char deferred) {
846         this.jexl = jexl;
847         this.logger = jexl.logger;
848         this.cache = jexl.createCache(cacheSize);
849         immediateChar = immediate;
850         deferredChar = deferred;
851         noscript = noScript;
852     }
853 
854     /**
855      * Clears the cache.
856      */
857     @Override
858     public void clearCache() {
859         synchronized (cache) {
860             cache.clear();
861         }
862     }
863 
864     @Override
865     public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression) {
866         return createExpression(jexlInfo, expression, null);
867     }
868 
869     public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression, final Scope scope) {
870         final JexlInfo info = jexlInfo == null ?  jexl.createInfo() : jexlInfo;
871         Exception xuel = null;
872         TemplateExpression stmt = null;
873         final JexlFeatures features = noscript ? jexl.expressionFeatures : jexl.scriptFeatures;
874         // do not cache interpolation expression, they are stored in AST node
875         final boolean cached = cache != null && expression.length() < jexl.cacheThreshold;
876         try {
877             if (!cached) {
878                 stmt = parseExpression(info, expression, scope);
879             } else {
880                 final Source source = new Source(features, Scope.getSymbolsMap(scope), expression);
881                 final Object c = cache.get(source);
882                 stmt = c instanceof TemplateExpression ? (TemplateExpression) c : null;
883                 if (stmt != null) {
884                     return stmt;
885                 }
886                 stmt = parseExpression(info, expression, scope);
887                 cache.put(source, stmt);
888             }
889         } catch (final JexlException xjexl) {
890             xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl);
891         }
892         if (xuel != null) {
893             if (!jexl.isSilent()) {
894                 throw xuel;
895             }
896             if (logger.isWarnEnabled()) {
897                 logger.warn(xuel.getMessage(), xuel.getCause());
898             }
899             stmt = null;
900         }
901         return stmt;
902     }
903 
904     @Override
905     public TemplateScript createTemplate(final JexlInfo info, final String prefix, final Reader source, final String... parms) {
906         return new TemplateScript(this, info, prefix, source,  parms);
907     }
908 
909     /**
910      * @return the deferred character
911      */
912     char getDeferredChar() {
913         return deferredChar;
914     }
915 
916     /**
917      * Gets the JexlEngine underlying this JxltEngine.
918      * @return the JexlEngine
919      */
920     @Override
921     public Engine getEngine() {
922         return jexl;
923     }
924 
925     /**
926      * @return the immediate character
927      */
928     char getImmediateChar() {
929         return immediateChar;
930     }
931 
932     /**
933      * Parses a unified expression.
934      * @param info  the source info
935      * @param expr  the string expression
936      * @param scope the template scope
937      * @return the unified expression instance
938      * @throws JexlException if an error occurs during parsing
939      */
940     TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope) {  // CSOFF: MethodLength
941         final int size = expr.length();
942         final ExpressionBuilder builder = new ExpressionBuilder(0);
943         final StringBuilder strb = new StringBuilder(size);
944         ParseState state = ParseState.CONST;
945         int immediate1 = 0;
946         int deferred1 = 0;
947         int inner1 = 0;
948         boolean nested = false;
949         int inested = -1;
950         int lineno = info.getLine();
951         for (int column = 0; column < size; ++column) {
952             final char c = expr.charAt(column);
953             switch (state) {
954             case CONST:
955                 if (c == immediateChar) {
956                     state = ParseState.IMMEDIATE0;
957                 } else if (c == deferredChar) {
958                     inested = column;
959                     state = ParseState.DEFERRED0;
960                 } else if (c == '\\') {
961                     state = ParseState.ESCAPE;
962                 } else {
963                     // do buildup expr
964                     strb.append(c);
965                 }
966                 break;
967             case IMMEDIATE0: // $
968                 if (c != '{') {
969                     // revert to CONST
970                     strb.append(immediateChar);
971                     state = ParseState.CONST;
972                     // 'unread' the current character
973                     column -= 1;
974                     continue;
975                 }
976                 state = ParseState.IMMEDIATE1;
977                 // if chars in buffer, create constant
978                 if (strb.length() > 0) {
979                     final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
980                     builder.add(cexpr);
981                     strb.delete(0, Integer.MAX_VALUE);
982                 }
983                 break;
984             case DEFERRED0: // #
985                 if (c != '{') {
986                     // revert to CONST
987                     strb.append(deferredChar);
988                     state = ParseState.CONST;
989                     // 'unread' the current character
990                     column -= 1;
991                     continue;
992                 }
993                 state = ParseState.DEFERRED1;
994                 // if chars in buffer, create constant
995                 if (strb.length() > 0) {
996                     final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
997                     builder.add(cexpr);
998                     strb.delete(0, Integer.MAX_VALUE);
999                 }
1000                 break;
1001             case IMMEDIATE1: // ${...
1002                 if (c == '}') {
1003                     if (immediate1 > 0) {
1004                         immediate1 -= 1;
1005                         strb.append(c);
1006                     } else {
1007                         // materialize the immediate expr
1008                         final String src = escapeString(strb);
1009                         final JexlInfo srcInfo = info.at(lineno, column);
1010                         final TemplateExpression iexpr = new ImmediateExpression(src, jexl.jxltParse(srcInfo, noscript, src, scope), null);
1011                         builder.add(iexpr);
1012                         strb.delete(0, Integer.MAX_VALUE);
1013                         state = ParseState.CONST;
1014                     }
1015                 } else {
1016                     if (c == '{') {
1017                         immediate1 += 1;
1018                     }
1019                     // do buildup expr
1020                     column = append(strb, expr, column, c);
1021                 }
1022                 break;
1023             case DEFERRED1: // #{...
1024                 // skip inner strings (for '}')
1025                 // nested immediate in deferred; need to balance count of '{' & '}'
1026                 // closing '}'
1027                 switch (c) {
1028                 case '"':
1029                 case '\'':
1030                     strb.append(c);
1031                     column = StringParser.readString(strb, expr, column + 1, c);
1032                     continue;
1033                 case '{':
1034                     if (expr.charAt(column - 1) == immediateChar) {
1035                         inner1 += 1;
1036                         strb.deleteCharAt(strb.length() - 1);
1037                         nested = true;
1038                     } else {
1039                         deferred1 += 1;
1040                         strb.append(c);
1041                     }
1042                     continue;
1043                 case '}':
1044                     // balance nested immediate
1045                     if (deferred1 > 0) {
1046                         deferred1 -= 1;
1047                         strb.append(c);
1048                     } else if (inner1 > 0) {
1049                         inner1 -= 1;
1050                     } else {
1051                         // materialize the nested/deferred expr
1052                         final String src = escapeString(strb);
1053                         final JexlInfo srcInfo = info.at(lineno, column);
1054                         TemplateExpression dexpr;
1055                         if (nested) {
1056                             dexpr = new NestedExpression(
1057                                         escapeString(expr.substring(inested, column + 1)),
1058                                         jexl.jxltParse(srcInfo, noscript, src, scope),
1059                                  null);
1060                         } else {
1061                             dexpr = new DeferredExpression(
1062                                     src,
1063                                     jexl.jxltParse(srcInfo, noscript, src, scope),
1064                              null);
1065                         }
1066                         builder.add(dexpr);
1067                         strb.delete(0, Integer.MAX_VALUE);
1068                         nested = false;
1069                         state = ParseState.CONST;
1070                     }
1071                     break;
1072                 default:
1073                     // do buildup expr
1074                     column = append(strb, expr, column, c);
1075                     break;
1076                 }
1077                 break;
1078             case ESCAPE:
1079                 if (c == deferredChar) {
1080                     strb.append(deferredChar);
1081                 } else if (c == immediateChar) {
1082                     strb.append(immediateChar);
1083                 } else {
1084                     strb.append('\\');
1085                     strb.append(c);
1086                 }
1087                 state = ParseState.CONST;
1088                 break;
1089             default: // in case we ever add new unified expression type
1090                 throw new UnsupportedOperationException("unexpected unified expression type");
1091             }
1092             if (c == '\n') {
1093                 lineno += 1;
1094             }
1095         }
1096         // we should be in that state
1097         if (state != ParseState.CONST) {
1098             // otherwise, we ended a line with a \, $ or #
1099             switch (state) {
1100                 case ESCAPE:
1101                     strb.append('\\');
1102                     strb.append('\\');
1103                     break;
1104                 case DEFERRED0:
1105                     strb.append(deferredChar);
1106                     break;
1107                 case IMMEDIATE0:
1108                     strb.append(immediateChar);
1109                     break;
1110                 default:
1111                     throw new Exception(info.at(lineno, 0), "malformed expression: " + expr, null);
1112             }
1113         }
1114         // if any chars were buffered, add them as a constant
1115         if (strb.length() > 0) {
1116             final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
1117             builder.add(cexpr);
1118         }
1119         return builder.build(this, null);
1120     }
1121 
1122     private String escapeString(final CharSequence str) {
1123         return StringParser.escapeString(str, (char) 0);
1124     }
1125 
1126     /**
1127      * Reads lines of a template grouping them by typed blocks.
1128      * @param prefix the directive prefix
1129      * @param source the source reader
1130      * @return the list of blocks
1131      */
1132      List<Block> readTemplate(final String prefix, final Reader source) {
1133         final ArrayList<Block> blocks = new ArrayList<>();
1134         final BufferedReader reader;
1135         if (source instanceof BufferedReader) {
1136             reader = (BufferedReader) source;
1137         } else {
1138             reader = new BufferedReader(source);
1139         }
1140         final StringBuilder strb = new StringBuilder();
1141         BlockType type = null;
1142         int prefixLen;
1143         final Iterator<CharSequence> lines = readLines(reader);
1144         int lineno = 1;
1145         int start = 0;
1146         while (lines.hasNext()) {
1147             final CharSequence line = lines.next();
1148             if (line == null) {
1149                 break;
1150             }
1151             if (type == null) {
1152                 // determine starting type if not known yet
1153                 prefixLen = startsWith(line, prefix);
1154                 if (prefixLen >= 0) {
1155                     type = BlockType.DIRECTIVE;
1156                     strb.append(line.subSequence(prefixLen, line.length()));
1157                 } else {
1158                     type = BlockType.VERBATIM;
1159                     strb.append(line.subSequence(0, line.length()));
1160                 }
1161                 start = lineno;
1162             } else if (type == BlockType.DIRECTIVE) {
1163                 // switch to verbatim if necessary
1164                 prefixLen = startsWith(line, prefix);
1165                 if (prefixLen < 0) {
1166                     final Block directive = new Block(BlockType.DIRECTIVE, start, strb.toString());
1167                     strb.delete(0, Integer.MAX_VALUE);
1168                     blocks.add(directive);
1169                     type = BlockType.VERBATIM;
1170                     strb.append(line.subSequence(0, line.length()));
1171                     start = lineno;
1172                 } else {
1173                     // still a directive
1174                     strb.append(line.subSequence(prefixLen, line.length()));
1175                 }
1176             } else { //if (type == BlockType.VERBATIM) {
1177                 // switch to directive if necessary
1178                 prefixLen = startsWith(line, prefix);
1179                 if (prefixLen >= 0) {
1180                     final Block verbatim = new Block(BlockType.VERBATIM, start, strb.toString());
1181                     strb.delete(0, Integer.MAX_VALUE);
1182                     blocks.add(verbatim);
1183                     type = BlockType.DIRECTIVE;
1184                     strb.append(line.subSequence(prefixLen, line.length()));
1185                     start = lineno;
1186                 } else {
1187                     strb.append(line.subSequence(0, line.length()));
1188                 }
1189             }
1190             lineno += 1;
1191         }
1192         // input may be null
1193         if (type != null && strb.length() > 0) {
1194             final Block block = new Block(type, start, strb.toString());
1195             blocks.add(block);
1196         }
1197         blocks.trimToSize();
1198         return blocks;
1199     }
1200 
1201     /**
1202      * Tests whether a sequence starts with a given set of characters (following spaces).
1203      * <p>Space characters at beginning of line before the pattern are discarded.</p>
1204      * @param sequence the sequence
1205      * @param pattern  the pattern to match at start of sequence
1206      * @return the first position after end of pattern if it matches, -1 otherwise
1207      */
1208     int startsWith(final CharSequence sequence, final CharSequence pattern) {
1209         final int length = sequence.length();
1210         int s = 0;
1211         while (s < length && Character.isSpaceChar(sequence.charAt(s))) {
1212             s += 1;
1213         }
1214         if (s < length && pattern.length() <= length - s) {
1215             final CharSequence subSequence = sequence.subSequence(s, length);
1216             if (subSequence.subSequence(0, pattern.length()).equals(pattern)) {
1217                 return s + pattern.length();
1218             }
1219         }
1220         return -1;
1221     }
1222 }