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