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.Reader;
20  import java.io.Writer;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Objects;
24  import java.util.Set;
25  
26  import org.apache.commons.jexl3.JexlContext;
27  import org.apache.commons.jexl3.JexlException;
28  import org.apache.commons.jexl3.JexlInfo;
29  import org.apache.commons.jexl3.JexlOptions;
30  import org.apache.commons.jexl3.JxltEngine;
31  import org.apache.commons.jexl3.internal.TemplateEngine.Block;
32  import org.apache.commons.jexl3.internal.TemplateEngine.BlockType;
33  import org.apache.commons.jexl3.internal.TemplateEngine.TemplateExpression;
34  import org.apache.commons.jexl3.parser.ASTArguments;
35  import org.apache.commons.jexl3.parser.ASTFunctionNode;
36  import org.apache.commons.jexl3.parser.ASTIdentifier;
37  import org.apache.commons.jexl3.parser.ASTJexlScript;
38  import org.apache.commons.jexl3.parser.ASTNumberLiteral;
39  import org.apache.commons.jexl3.parser.JexlNode;
40  
41  /**
42   * A Template instance.
43   */
44  public final class TemplateScript implements JxltEngine.Template {
45  
46      /**
47       * Collects the call-site surrounding a call to jexl:print(i).
48       * <p>This allows parsing the blocks with the known symbols
49       * in the frame visible to the parser.</p>
50       *
51       * @param node the visited node
52       * @param callSites the map of printed expression number to node info
53       */
54      private static void collectPrintScope(final JexlNode node, final JexlNode.Info[] callSites) {
55          final int nc = node.jjtGetNumChildren();
56          if (node instanceof ASTFunctionNode && nc == 2) {
57              // is child[0] jexl:print()?
58              final ASTIdentifier nameNode = (ASTIdentifier) node.jjtGetChild(0);
59              if ("print".equals(nameNode.getName()) && "jexl".equals(nameNode.getNamespace())) {
60                  // is there one argument?
61                  final ASTArguments argNode = (ASTArguments) node.jjtGetChild(1);
62                  if (argNode.jjtGetNumChildren() == 1) {
63                      // seek the expression number
64                      final JexlNode arg0 = argNode.jjtGetChild(0);
65                      if (arg0 instanceof ASTNumberLiteral) {
66                          final int exprNumber = ((ASTNumberLiteral) arg0).getLiteral().intValue();
67                          callSites[exprNumber] = new JexlNode.Info(nameNode);
68                          return;
69                      }
70                  }
71              }
72          }
73          for (int c = 0; c < nc; ++c) {
74              collectPrintScope(node.jjtGetChild(c), callSites);
75          }
76      }
77  
78      /**
79       * Gets the scope from a node info.
80       *
81       * @param info the node info
82       * @param scope the outer scope
83       * @return the scope
84       */
85      private static Scope scopeOf(final JexlNode.Info info, final Scope scope) {
86          Scope found = null;
87          JexlNode walk = info.getNode();
88          while (walk != null) {
89              if (walk instanceof ASTJexlScript) {
90                  found = ((ASTJexlScript) walk).getScope();
91                  break;
92              }
93              walk = walk.jjtGetParent();
94          }
95          return found != null ? found : scope;
96      }
97  
98      /**
99       * Creates the expression array from the list of blocks.
100      *
101      * @param scope the outer scope
102      * @param blocks the list of blocks
103      * @return the array of expressions
104      */
105     private TemplateExpression[] calleeScripts(final Scope scope, final Block[] blocks, final JexlNode.Info[] callSites) {
106         final TemplateExpression[] expressions = new TemplateExpression[callSites.length];
107         // jexl:print(...) expression counter
108         int jpe = 0;
109         // create the expressions using the intended scopes
110         for (final Block block : blocks) {
111             if (block.getType() == BlockType.VERBATIM) {
112                 final JexlNode.Info ji = callSites[jpe];
113                 // no node info means this verbatim is surrounded by comments markers;
114                 // expr at this index is never called
115                 final TemplateExpression te = ji != null
116                     ? jxlt.parseExpression(ji, block.getBody(), scopeOf(ji, scope))
117                     : jxlt.new ConstantExpression(block.getBody(), null);
118                 expressions[jpe++] = te;
119             }
120         }
121         return expressions;
122     }
123 
124     /**
125      * Creates the script calling the list of blocks.
126      * <p>This is used to create a script from a list of blocks
127      * that were parsed from a template.</p>
128      *
129      * @param blocks the list of blocks
130      * @return the script source
131      */
132     private static String callerScript(final Block[] blocks) {
133         final StringBuilder strb = new StringBuilder();
134         int nuexpr = 0;
135         int line = 1;
136         for (final Block block : blocks) {
137             final int bl = block.getLine();
138             while (line < bl) {
139                 strb.append("//\n");
140                 line += 1;
141             }
142             if (block.getType() == BlockType.VERBATIM) {
143                 strb.append("jexl:print(");
144                 strb.append(nuexpr++);
145                 strb.append(");\n");
146                 line += 1;
147             } else {
148                 final String body = block.getBody();
149                 strb.append(body);
150                 // keep track of the line number
151                 for (int c = 0; c < body.length(); ++c) {
152                     if (body.charAt(c) == '\n') {
153                         line += 1;
154                     }
155                 }
156             }
157         }
158         return strb.toString();
159     }
160 
161     /** The prefix marker. */
162     private final String prefix;
163 
164     /** The array of source blocks. */
165     private final Block[] source;
166 
167     /** The resulting script. */
168     private final ASTJexlScript script;
169 
170     /** The TemplateEngine expressions called by the script. */
171     private final TemplateExpression[] exprs;
172 
173     /** The engine. */
174     private final TemplateEngine jxlt;
175 
176     /**
177      * Creates a new template from an character input.
178      *
179      * @param engine the template engine
180      * @param jexlInfo the source info
181      * @param directive the prefix for lines of code; cannot be "$", "${", "#" or "#{"
182                   since this would preclude being able to differentiate directives and jxlt expressions
183      * @param reader    the input reader
184      * @param parms     the parameter names
185      * @throws NullPointerException     if either the directive prefix or input is null
186      * @throws IllegalArgumentException if the directive prefix is invalid
187      */
188     public TemplateScript(final TemplateEngine engine,
189                           final JexlInfo jexlInfo,
190                           final String directive,
191                           final Reader reader,
192                           final String... parms) {
193         Objects.requireNonNull(directive, "directive");
194         final String engineImmediateCharString = Character.toString(engine.getImmediateChar());
195         final String engineDeferredCharString = Character.toString(engine.getDeferredChar());
196 
197         if (engineImmediateCharString.equals(directive)
198                 || engineDeferredCharString.equals(directive)
199                 || (engineImmediateCharString + "{").equals(directive)
200                 || (engineDeferredCharString + "{").equals(directive)) {
201             throw new IllegalArgumentException(directive + ": is not a valid directive pattern");
202         }
203         Objects.requireNonNull(reader, "reader");
204         this.jxlt = engine;
205         this.prefix = directive;
206         final Engine jexl = jxlt.getEngine();
207         // create the caller script
208         final Block[] blocks = jxlt.readTemplate(prefix, reader).toArray(new Block[0]);
209         int verbatims = 0;
210         for(final Block b : blocks) {
211             if (BlockType.VERBATIM == b.getType()) {
212                 verbatims += 1;
213             }
214         }
215         final String scriptSource = callerScript(blocks);
216         // allow lambda defining params
217         final JexlInfo info = jexlInfo == null ? jexl.createInfo() : jexlInfo;
218         final Scope scope = parms == null ? null : new Scope(null, parms);
219         final ASTJexlScript callerScript = jexl.jxltParse(info.at(1, 1), false, scriptSource, scope).script();
220         // seek the map of expression number to scope so we can parse Unified
221         // expression blocks with the appropriate symbols
222         final JexlNode.Info[] callSites = new JexlNode.Info[verbatims];
223         collectPrintScope(callerScript.script(), callSites);
224         // create the expressions from the blocks
225         this.exprs = calleeScripts(scope, blocks, callSites);
226         this.script = callerScript;
227         this.source = blocks;
228     }
229 
230     /**
231      * Private ctor used to expand deferred expressions during prepare.
232      *
233      * @param engine    the template engine
234      * @param thePrefix the directive prefix
235      * @param theSource the source
236      * @param theScript the script
237      * @param theExprs  the expressions
238      */
239     TemplateScript(final TemplateEngine engine,
240                    final String thePrefix,
241                    final Block[] theSource,
242                    final ASTJexlScript theScript,
243                    final TemplateExpression[] theExprs) {
244         jxlt = engine;
245         prefix = thePrefix;
246         source = theSource;
247         script = theScript;
248         exprs = theExprs;
249     }
250 
251     @Override
252     public String asString() {
253         final StringBuilder strb = new StringBuilder();
254         int e = 0;
255         for (final Block block : source) {
256             if (block.getType() == BlockType.DIRECTIVE) {
257                 strb.append(prefix);
258                 strb.append(block.getBody());
259             } else {
260                 exprs[e++].asString(strb);
261             }
262         }
263         return strb.toString();
264     }
265 
266     @Override
267     public void evaluate(final JexlContext context, final Writer writer) {
268         evaluate(context, writer, (Object[]) null);
269     }
270 
271     @Override
272     public void evaluate(final JexlContext context, final Writer writer, final Object... args) {
273         final Engine jexl = jxlt.getEngine();
274         final JexlOptions options = jexl.evalOptions(script, context);
275         final Frame frame = script.createFrame(args);
276         final TemplateInterpreter.Arguments targs = new TemplateInterpreter
277                 .Arguments(jexl)
278                 .context(context)
279                 .options(options)
280                 .frame(frame)
281                 .expressions(exprs)
282                 .writer(writer);
283         final Interpreter interpreter = jexl.createTemplateInterpreter(targs);
284         interpreter.interpret(script);
285     }
286 
287     /**
288      * @return exprs
289      */
290     TemplateExpression[] getExpressions() {
291         return exprs;
292     }
293 
294     @Override
295     public String[] getParameters() {
296         return script.getParameters();
297     }
298 
299     @Override
300     public Map<String, Object> getPragmas() {
301         return script.getPragmas();
302     }
303 
304     /**
305      * @return script
306      */
307     ASTJexlScript getScript() {
308         return script;
309     }
310 
311     @Override
312     public Set<List<String>> getVariables() {
313         final Engine.VarCollector collector = jxlt.getEngine().varCollector();
314         for (final TemplateExpression expr : exprs) {
315             expr.getVariables(collector);
316         }
317         return collector.collected();
318     }
319 
320     @Override
321     public TemplateScript prepare(final JexlContext context) {
322         return prepare(context, (Object[]) null);
323     }
324 
325     @Override
326     public TemplateScript prepare(final JexlContext context, final Object... args) {
327         final Engine jexl = jxlt.getEngine();
328         final JexlOptions options = jexl.evalOptions(script, context);
329         final Frame frame = script.createFrame(args);
330         final TemplateInterpreter.Arguments targs = new TemplateInterpreter
331                 .Arguments(jxlt.getEngine())
332                 .context(context)
333                 .options(options)
334                 .frame(frame);
335         final Interpreter interpreter = jexl.createTemplateInterpreter(targs);
336         final TemplateExpression[] prepared = new TemplateExpression[exprs.length];
337         for (int e = 0; e < exprs.length; ++e) {
338             try {
339                 prepared[e] = exprs[e].prepare(interpreter);
340             } catch (final JexlException xjexl) {
341                 final JexlException xuel = TemplateEngine.createException(xjexl.getInfo(), "prepare", exprs[e], xjexl);
342                 if (jexl.isSilent()) {
343                     if (jexl.logger.isWarnEnabled()) {
344                         jexl.logger.warn(xuel.getMessage(), xuel.getCause());
345                     }
346                     return null;
347                 }
348                 throw xuel;
349             }
350         }
351         return new TemplateScript(jxlt, prefix, source, script, prepared);
352     }
353 
354     @Override
355     public String toString() {
356         final StringBuilder strb = new StringBuilder();
357         for (final Block block : source) {
358             block.toString(strb, prefix);
359         }
360         return strb.toString();
361     }
362 }