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