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