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