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