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