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