• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.clearsilver.jsilver.compiler;
18 
19 import com.google.clearsilver.jsilver.autoescape.EscapeMode;
20 import static com.google.clearsilver.jsilver.compiler.JavaExpression.BooleanLiteralExpression;
21 import static com.google.clearsilver.jsilver.compiler.JavaExpression.Type;
22 import static com.google.clearsilver.jsilver.compiler.JavaExpression.call;
23 import static com.google.clearsilver.jsilver.compiler.JavaExpression.callFindVariable;
24 import static com.google.clearsilver.jsilver.compiler.JavaExpression.callOn;
25 import static com.google.clearsilver.jsilver.compiler.JavaExpression.declare;
26 import static com.google.clearsilver.jsilver.compiler.JavaExpression.increment;
27 import static com.google.clearsilver.jsilver.compiler.JavaExpression.infix;
28 import static com.google.clearsilver.jsilver.compiler.JavaExpression.inlineIf;
29 import static com.google.clearsilver.jsilver.compiler.JavaExpression.integer;
30 import static com.google.clearsilver.jsilver.compiler.JavaExpression.literal;
31 import static com.google.clearsilver.jsilver.compiler.JavaExpression.macro;
32 import static com.google.clearsilver.jsilver.compiler.JavaExpression.string;
33 import static com.google.clearsilver.jsilver.compiler.JavaExpression.symbol;
34 import com.google.clearsilver.jsilver.data.Data;
35 import com.google.clearsilver.jsilver.data.DataContext;
36 import com.google.clearsilver.jsilver.functions.Function;
37 import com.google.clearsilver.jsilver.functions.FunctionExecutor;
38 import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter;
39 import com.google.clearsilver.jsilver.syntax.node.AAltCommand;
40 import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand;
41 import com.google.clearsilver.jsilver.syntax.node.ACallCommand;
42 import com.google.clearsilver.jsilver.syntax.node.ADataCommand;
43 import com.google.clearsilver.jsilver.syntax.node.ADefCommand;
44 import com.google.clearsilver.jsilver.syntax.node.AEachCommand;
45 import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand;
46 import com.google.clearsilver.jsilver.syntax.node.AEvarCommand;
47 import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand;
48 import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand;
49 import com.google.clearsilver.jsilver.syntax.node.AIfCommand;
50 import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand;
51 import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand;
52 import com.google.clearsilver.jsilver.syntax.node.ALoopCommand;
53 import com.google.clearsilver.jsilver.syntax.node.ALoopIncCommand;
54 import com.google.clearsilver.jsilver.syntax.node.ALoopToCommand;
55 import com.google.clearsilver.jsilver.syntax.node.ALvarCommand;
56 import com.google.clearsilver.jsilver.syntax.node.ANameCommand;
57 import com.google.clearsilver.jsilver.syntax.node.ANoopCommand;
58 import com.google.clearsilver.jsilver.syntax.node.ASetCommand;
59 import com.google.clearsilver.jsilver.syntax.node.AUvarCommand;
60 import com.google.clearsilver.jsilver.syntax.node.AVarCommand;
61 import com.google.clearsilver.jsilver.syntax.node.AWithCommand;
62 import com.google.clearsilver.jsilver.syntax.node.PCommand;
63 import com.google.clearsilver.jsilver.syntax.node.PExpression;
64 import com.google.clearsilver.jsilver.syntax.node.PPosition;
65 import com.google.clearsilver.jsilver.syntax.node.PVariable;
66 import com.google.clearsilver.jsilver.syntax.node.Start;
67 import com.google.clearsilver.jsilver.syntax.node.TCsOpen;
68 import com.google.clearsilver.jsilver.syntax.node.TWord;
69 import com.google.clearsilver.jsilver.template.Macro;
70 import com.google.clearsilver.jsilver.template.RenderingContext;
71 import com.google.clearsilver.jsilver.template.Template;
72 import com.google.clearsilver.jsilver.values.Value;
73 
74 import java.io.IOException;
75 import java.io.Writer;
76 import java.lang.reflect.Method;
77 import java.util.HashMap;
78 import java.util.LinkedList;
79 import java.util.Map;
80 import java.util.Queue;
81 
82 /**
83  * Translates a JSilver AST into compilable Java code. This executes much faster than the
84  * interpreter.
85  *
86  * @see TemplateCompiler
87  */
88 public class TemplateTranslator extends DepthFirstAdapter {
89 
90   // Root data
91   public static final JavaExpression DATA = symbol(Type.DATA, "data");
92   // RenderingContext
93   public static final JavaExpression CONTEXT = symbol("context");
94   // DataContext
95   public static final JavaExpression DATA_CONTEXT = symbol(Type.DATA_CONTEXT, "dataContext");
96   public static final JavaExpression NULL = symbol("null");
97   // Accessed from macros as well.
98   public static final JavaExpression RESOURCE_LOADER = callOn(CONTEXT, "getResourceLoader");
99   public static final JavaExpression TEMPLATE_LOADER = symbol("getTemplateLoader()");
100   public static final JavaExpression THIS_TEMPLATE = symbol("this");
101 
102   private final JavaSourceWriter java;
103 
104   private final String packageName;
105   private final String className;
106 
107   private final ExpressionTranslator expressionTranslator = new ExpressionTranslator();
108   private final VariableTranslator variableTranslator =
109       new VariableTranslator(expressionTranslator);
110   private final EscapingEvaluator escapingEvaluator = new EscapingEvaluator(variableTranslator);
111 
112   private static final Method RENDER_METHOD;
113 
114   private int tempVariable = 0;
115   /**
116    * Used to determine the escaping to apply before displaying a variable. If propagateEscapeStatus
117    * is enabled, string and numeric literals are not escaped, nor is the output of an escaping
118    * function. If not, any expression that contains an escaping function is not escaped. This
119    * maintains compatibility with the way ClearSilver works.
120    */
121   private boolean propagateEscapeStatus;
122 
123   /**
124    * Holds Macro information used while generating code.
125    */
126   private static class MacroInfo {
127     /**
128      * JavaExpression used for outputting the static Macro variable name.
129      */
130     JavaExpression symbol;
131 
132     /**
133      * Parser node for the definition. Stored for evaluation after main render method is output.
134      */
135     ADefCommand defNode;
136   }
137 
138   /**
139    * Map of macro names to definition nodes and java expressions used to refer to them.
140    */
141   private final Map<String, MacroInfo> macroMap = new HashMap<String, MacroInfo>();
142 
143   /**
144    * Used to iterate through list of macros. We can't rely on Map's iterator because we may be
145    * adding to the map as we iterate through the values() list and that would throw a
146    * ConcurrentModificationException.
147    */
148   private final Queue<MacroInfo> macroQueue = new LinkedList<MacroInfo>();
149 
150   /**
151    * Creates a MacroInfo object and adds it to the data structures. Also outputs statement to
152    * register the macro.
153    *
154    * @param name name of the macro as defined in the template.
155    * @param symbol static variable name of the macro definition.
156    * @param defNode parser node holding the macro definition to be evaluated later.
157    */
addMacro(String name, JavaExpression symbol, ADefCommand defNode)158   private void addMacro(String name, JavaExpression symbol, ADefCommand defNode) {
159     if (macroMap.get(name) != null) {
160       // TODO: This macro is already defined. Should throw an error.
161     }
162     MacroInfo info = new MacroInfo();
163     info.symbol = symbol;
164     info.defNode = defNode;
165     macroMap.put(name, info);
166     macroQueue.add(info);
167 
168     // Register the macro.
169     java.writeStatement(callOn(CONTEXT, "registerMacro", string(name), symbol));
170   }
171 
172   static {
173     try {
174       RENDER_METHOD = Template.class.getMethod("render", RenderingContext.class);
175     } catch (NoSuchMethodException e) {
176       throw new Error("Cannot find CompiledTemplate.render() method! " + "Has signature changed?",
177           e);
178     }
179   }
180 
TemplateTranslator(String packageName, String className, Writer output, boolean propagateEscapeStatus)181   public TemplateTranslator(String packageName, String className, Writer output,
182       boolean propagateEscapeStatus) {
183     this.packageName = packageName;
184     this.className = className;
185     java = new JavaSourceWriter(output);
186     this.propagateEscapeStatus = propagateEscapeStatus;
187   }
188 
189   @Override
caseStart(Start node)190   public void caseStart(Start node) {
191     java.writeComment("This class is autogenerated by JSilver. Do not edit.");
192     java.writePackage(packageName);
193     java.writeImports(BaseCompiledTemplate.class, Template.class, Macro.class,
194         RenderingContext.class, Data.class, DataContext.class, Function.class,
195         FunctionExecutor.class, Value.class, EscapeMode.class, IOException.class);
196     java.startClass(className, BaseCompiledTemplate.class.getSimpleName());
197 
198     // Implement render() method.
199     java.startMethod(RENDER_METHOD, "context");
200     java
201         .writeStatement(declare(Type.DATA_CONTEXT, "dataContext", callOn(CONTEXT, "getDataContext")));
202     java.writeStatement(callOn(CONTEXT, "pushExecutionContext", THIS_TEMPLATE));
203     super.caseStart(node); // Walk template AST.
204     java.writeStatement(callOn(CONTEXT, "popExecutionContext"));
205     java.endMethod();
206 
207     // The macros have to be defined outside of the render method.
208     // (Well actually they *could* be defined inline as anon classes, but it
209     // would make the generated code quite hard to understand).
210     MacroTransformer macroTransformer = new MacroTransformer();
211 
212     while (!macroQueue.isEmpty()) {
213       MacroInfo curr = macroQueue.remove();
214       macroTransformer.parseDefNode(curr.symbol, curr.defNode);
215     }
216 
217     java.endClass();
218   }
219 
220   /**
221    * Chunk of data (i.e. not a CS command).
222    */
223   @Override
caseADataCommand(ADataCommand node)224   public void caseADataCommand(ADataCommand node) {
225     String content = node.getData().getText();
226     java.writeStatement(callOn(CONTEXT, "writeUnescaped", string(content)));
227   }
228 
229   /**
230    * &lt;?cs var:blah &gt; expression. Evaluate as string and write output, using default escaping.
231    */
232   @Override
caseAVarCommand(AVarCommand node)233   public void caseAVarCommand(AVarCommand node) {
234     capturePosition(node.getPosition());
235 
236     String tempVariableName = generateTempVariable("result");
237     JavaExpression result = symbol(Type.STRING, tempVariableName);
238     java.writeStatement(declare(Type.STRING, tempVariableName, expressionTranslator
239         .translateToString(node.getExpression())));
240 
241     JavaExpression escaping =
242         escapingEvaluator.computeIfExemptFromEscaping(node.getExpression(), propagateEscapeStatus);
243     writeVariable(result, escaping);
244   }
245 
246   /**
247    * &lt;?cs uvar:blah &gt; expression. Evaluate as string and write output, but don't escape.
248    */
249   @Override
caseAUvarCommand(AUvarCommand node)250   public void caseAUvarCommand(AUvarCommand node) {
251     capturePosition(node.getPosition());
252     java.writeStatement(callOn(CONTEXT, "writeUnescaped", expressionTranslator
253         .translateToString(node.getExpression())));
254   }
255 
256   /**
257    * &lt;?cs set:x='y' &gt; command.
258    */
259   @Override
caseASetCommand(ASetCommand node)260   public void caseASetCommand(ASetCommand node) {
261     capturePosition(node.getPosition());
262     String tempVariableName = generateTempVariable("setNode");
263 
264     // Data setNode1 = dataContext.findVariable("x", true);
265     JavaExpression setNode = symbol(Type.DATA, tempVariableName);
266     java.writeStatement(declare(Type.DATA, tempVariableName, callFindVariable(variableTranslator
267         .translate(node.getVariable()), true)));
268     // setNode1.setValue("hello");
269     java.writeStatement(callOn(setNode, "setValue", expressionTranslator.translateToString(node
270         .getExpression())));
271 
272     if (propagateEscapeStatus) {
273       // setNode1.setEscapeMode(EscapeMode.ESCAPE_IS_CONSTANT);
274       java.writeStatement(callOn(setNode, "setEscapeMode", escapingEvaluator.computeEscaping(node
275           .getExpression(), propagateEscapeStatus)));
276     }
277   }
278 
279   /**
280    * &lt;?cs name:blah &gt; command. Writes out the name of the original variable referred to by a
281    * given node.
282    */
283   @Override
caseANameCommand(ANameCommand node)284   public void caseANameCommand(ANameCommand node) {
285     capturePosition(node.getPosition());
286     JavaExpression readNode =
287         callFindVariable(variableTranslator.translate(node.getVariable()), false);
288     java.writeStatement(callOn(CONTEXT, "writeEscaped", call("getNodeName", readNode)));
289   }
290 
291   /**
292    * &lt;?cs if:blah &gt; ... &lt;?cs else &gt; ... &lt;?cs /if &gt; command.
293    */
294   @Override
caseAIfCommand(AIfCommand node)295   public void caseAIfCommand(AIfCommand node) {
296     capturePosition(node.getPosition());
297 
298     java.startIfBlock(expressionTranslator.translateToBoolean(node.getExpression()));
299     node.getBlock().apply(this);
300     if (!(node.getOtherwise() instanceof ANoopCommand)) {
301       java.endIfStartElseBlock();
302       node.getOtherwise().apply(this);
303     }
304     java.endIfBlock();
305   }
306 
307   /**
308    * &lt;?cs each:x=Stuff &gt; ... &lt;?cs /each &gt; command. Loops over child items of a data
309    * node.
310    */
311   @Override
caseAEachCommand(AEachCommand node)312   public void caseAEachCommand(AEachCommand node) {
313     capturePosition(node.getPosition());
314 
315     JavaExpression parent = expressionTranslator.translateToData(node.getExpression());
316     writeEach(node.getVariable(), parent, node.getCommand());
317   }
318 
319   /**
320    * &lt;?cs with:x=Something &gt; ... &lt;?cs /with &gt; command. Aliases a value within a specific
321    * scope.
322    */
323   @Override
caseAWithCommand(AWithCommand node)324   public void caseAWithCommand(AWithCommand node) {
325     capturePosition(node.getPosition());
326 
327     java.startScopedBlock();
328     java.writeComment("with:");
329 
330     // Extract the value first in case the temp variable has the same name.
331     JavaExpression value = expressionTranslator.translateUntyped(node.getExpression());
332     String methodName = null;
333     if (value.getType() == Type.VAR_NAME) {
334       String withValueName = generateTempVariable("withValue");
335       java.writeStatement(declare(Type.STRING, withValueName, value));
336       value = symbol(Type.VAR_NAME, withValueName);
337       methodName = "createLocalVariableByPath";
338 
339       // We need to check if the variable exists. If not, we skip the with
340       // call.
341       java.startIfBlock(JavaExpression.infix(Type.BOOLEAN, "!=", value.cast(Type.DATA), literal(
342           Type.DATA, "null")));
343     } else {
344       // Cast to string so we support numeric or boolean values as well.
345       value = value.cast(Type.STRING);
346       methodName = "createLocalVariableByValue";
347     }
348 
349     JavaExpression itemKey = variableTranslator.translate(node.getVariable());
350 
351     // Push a new local variable scope for the with local variable
352     java.writeStatement(callOn(DATA_CONTEXT, "pushVariableScope"));
353 
354     java.writeStatement(callOn(DATA_CONTEXT, methodName, itemKey, value));
355     node.getCommand().apply(this);
356 
357     // Release the variable scope used by the with statement
358     java.writeStatement(callOn(DATA_CONTEXT, "popVariableScope"));
359 
360     if (value.getType() == Type.VAR_NAME) {
361       // End of if block that checks that the Data node exists.
362       java.endIfBlock();
363     }
364 
365     java.endScopedBlock();
366   }
367 
368   /**
369    * &lt;?cs loop:10 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers, starting at
370    * zero.
371    */
372   @Override
caseALoopToCommand(ALoopToCommand node)373   public void caseALoopToCommand(ALoopToCommand node) {
374     capturePosition(node.getPosition());
375 
376     JavaExpression start = integer(0);
377     JavaExpression end = expressionTranslator.translateToNumber(node.getExpression());
378     JavaExpression incr = integer(1);
379     writeLoop(node.getVariable(), start, end, incr, node.getCommand());
380   }
381 
382   /**
383    * &lt;?cs loop:0,10 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers.
384    */
385   @Override
caseALoopCommand(ALoopCommand node)386   public void caseALoopCommand(ALoopCommand node) {
387     capturePosition(node.getPosition());
388 
389     JavaExpression start = expressionTranslator.translateToNumber(node.getStart());
390     JavaExpression end = expressionTranslator.translateToNumber(node.getEnd());
391     JavaExpression incr = integer(1);
392     writeLoop(node.getVariable(), start, end, incr, node.getCommand());
393   }
394 
395   /**
396    * &lt;?cs loop:0,10,2 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers, with a
397    * specific increment.
398    */
399   @Override
caseALoopIncCommand(ALoopIncCommand node)400   public void caseALoopIncCommand(ALoopIncCommand node) {
401     capturePosition(node.getPosition());
402 
403     JavaExpression start = expressionTranslator.translateToNumber(node.getStart());
404     JavaExpression end = expressionTranslator.translateToNumber(node.getEnd());
405     JavaExpression incr = expressionTranslator.translateToNumber(node.getIncrement());
406     writeLoop(node.getVariable(), start, end, incr, node.getCommand());
407   }
408 
writeLoop(PVariable itemVariable, JavaExpression start, JavaExpression end, JavaExpression incr, PCommand command)409   private void writeLoop(PVariable itemVariable, JavaExpression start, JavaExpression end,
410       JavaExpression incr, PCommand command) {
411 
412     java.startScopedBlock();
413 
414     String startVarName = generateTempVariable("start");
415     java.writeStatement(declare(Type.INT, startVarName, start));
416     JavaExpression startVar = symbol(Type.INT, startVarName);
417 
418     String endVarName = generateTempVariable("end");
419     java.writeStatement(declare(Type.INT, endVarName, end));
420     JavaExpression endVar = symbol(Type.INT, endVarName);
421 
422     String incrVarName = generateTempVariable("incr");
423     java.writeStatement(declare(Type.INT, incrVarName, incr));
424     JavaExpression incrVar = symbol(Type.INT, incrVarName);
425 
426     // TODO: Test correctness of values.
427     java.startIfBlock(call(Type.BOOLEAN, "validateLoopArgs", startVar, endVar, incrVar));
428 
429     JavaExpression itemKey = variableTranslator.translate(itemVariable);
430 
431     // Push a new local variable scope for the loop local variable
432     java.writeStatement(callOn(DATA_CONTEXT, "pushVariableScope"));
433 
434     String loopVariable = generateTempVariable("loop");
435     JavaExpression loopVar = symbol(Type.INT, loopVariable);
436     JavaExpression ifStart = declare(Type.INT, loopVariable, startVar);
437     JavaExpression ifEnd =
438         inlineIf(Type.BOOLEAN, infix(Type.BOOLEAN, ">=", incrVar, integer(0)), infix(Type.BOOLEAN,
439             "<=", loopVar, endVar), infix(Type.BOOLEAN, ">=", loopVar, endVar));
440     java.startForLoop(ifStart, ifEnd, increment(Type.INT, loopVar, incrVar));
441 
442     java.writeStatement(callOn(DATA_CONTEXT, "createLocalVariableByValue", itemKey, symbol(
443         loopVariable).cast(Type.STRING), infix(Type.BOOLEAN, "==", symbol(loopVariable), startVar),
444         infix(Type.BOOLEAN, "==", symbol(loopVariable), endVar)));
445     command.apply(this);
446 
447     java.endLoop();
448 
449     // Release the variable scope used by the loop statement
450     java.writeStatement(callOn(DATA_CONTEXT, "popVariableScope"));
451 
452     java.endIfBlock();
453     java.endScopedBlock();
454   }
455 
writeEach(PVariable itemVariable, JavaExpression parentData, PCommand command)456   private void writeEach(PVariable itemVariable, JavaExpression parentData, PCommand command) {
457 
458     JavaExpression itemKey = variableTranslator.translate(itemVariable);
459 
460     // Push a new local variable scope for the each local variable
461     java.writeStatement(callOn(DATA_CONTEXT, "pushVariableScope"));
462 
463     String childDataVariable = generateTempVariable("child");
464     java.startIterableForLoop("Data", childDataVariable, call("getChildren", parentData));
465 
466     java.writeStatement(callOn(DATA_CONTEXT, "createLocalVariableByPath", itemKey, callOn(
467         Type.STRING, symbol(childDataVariable), "getFullPath")));
468     command.apply(this);
469 
470     java.endLoop();
471 
472     // Release the variable scope used by the each statement
473     java.writeStatement(callOn(DATA_CONTEXT, "popVariableScope"));
474   }
475 
476   /**
477    * &lt;?cs alt:someValue &gt; ... &lt;?cs /alt &gt; command. If value exists, write it, otherwise
478    * write the body of the command.
479    */
480   @Override
caseAAltCommand(AAltCommand node)481   public void caseAAltCommand(AAltCommand node) {
482     capturePosition(node.getPosition());
483     String tempVariableName = generateTempVariable("altVar");
484 
485     JavaExpression declaration =
486         expressionTranslator.declareAsVariable(tempVariableName, node.getExpression());
487     JavaExpression reference = symbol(declaration.getType(), tempVariableName);
488     java.writeStatement(declaration);
489     java.startIfBlock(reference.cast(Type.BOOLEAN));
490 
491     JavaExpression escaping =
492         escapingEvaluator.computeIfExemptFromEscaping(node.getExpression(), propagateEscapeStatus);
493     writeVariable(reference, escaping);
494     java.endIfStartElseBlock();
495     node.getCommand().apply(this);
496     java.endIfBlock();
497   }
498 
499   /*
500    * Generates a statement that will write out a variable expression, after determining whether the
501    * variable expression should be exempted from any global escaping that may currently be in
502    * effect. We try to make this determination during translation if possible, and if we cannot, we
503    * output an if/else statement to check the escaping status of the expression at run time.
504    *
505    * Currently, unless the expression contains a function call, we know at translation tmie that it
506    * does not need to be exempted.
507    */
writeVariable(JavaExpression result, JavaExpression escapingExpression)508   private void writeVariable(JavaExpression result, JavaExpression escapingExpression) {
509 
510     if (escapingExpression instanceof BooleanLiteralExpression) {
511       BooleanLiteralExpression expr = (BooleanLiteralExpression) escapingExpression;
512       if (expr.getValue()) {
513         java.writeStatement(callOn(CONTEXT, "writeUnescaped", result.cast(Type.STRING)));
514       } else {
515         java.writeStatement(callOn(CONTEXT, "writeEscaped", result.cast(Type.STRING)));
516       }
517 
518     } else {
519       java.startIfBlock(escapingExpression);
520       java.writeStatement(callOn(CONTEXT, "writeUnescaped", result.cast(Type.STRING)));
521       java.endIfStartElseBlock();
522       java.writeStatement(callOn(CONTEXT, "writeEscaped", result.cast(Type.STRING)));
523       java.endIfBlock();
524     }
525   }
526 
527   /**
528    * &lt;?cs escape:'html' &gt; command. Changes default escaping function.
529    */
530   @Override
caseAEscapeCommand(AEscapeCommand node)531   public void caseAEscapeCommand(AEscapeCommand node) {
532     capturePosition(node.getPosition());
533     java.writeStatement(callOn(CONTEXT, "pushEscapingFunction", expressionTranslator
534         .translateToString(node.getExpression())));
535     node.getCommand().apply(this);
536     java.writeStatement(callOn(CONTEXT, "popEscapingFunction"));
537   }
538 
539   /**
540    * A fake command injected by AutoEscaper.
541    *
542    * AutoEscaper determines the html context in which an include or lvar or evar command is called
543    * and stores this context in the AAutoescapeCommand node. This function loads the include or lvar
544    * template in this stored context.
545    */
546   @Override
caseAAutoescapeCommand(AAutoescapeCommand node)547   public void caseAAutoescapeCommand(AAutoescapeCommand node) {
548     capturePosition(node.getPosition());
549 
550     java.writeStatement(callOn(CONTEXT, "pushAutoEscapeMode", callOn(symbol("EscapeMode"),
551         "computeEscapeMode", expressionTranslator.translateToString(node.getExpression()))));
552     node.getCommand().apply(this);
553     java.writeStatement(callOn(CONTEXT, "popAutoEscapeMode"));
554 
555   }
556 
557   /**
558    * &lt;?cs linclude:'somefile.cs' &gt; command. Lazily includes another template (at render time).
559    * Throw an error if file does not exist.
560    */
561   @Override
caseAHardLincludeCommand(AHardLincludeCommand node)562   public void caseAHardLincludeCommand(AHardLincludeCommand node) {
563     capturePosition(node.getPosition());
564     java.writeStatement(call("include", expressionTranslator
565         .translateToString(node.getExpression()), JavaExpression.bool(false), CONTEXT));
566   }
567 
568   /**
569    * &lt;?cs linclude:'somefile.cs' &gt; command. Lazily includes another template (at render time).
570    * Silently ignore if the included file does not exist.
571    */
572   @Override
caseALincludeCommand(ALincludeCommand node)573   public void caseALincludeCommand(ALincludeCommand node) {
574     capturePosition(node.getPosition());
575     java.writeStatement(call("include", expressionTranslator
576         .translateToString(node.getExpression()), JavaExpression.bool(true), CONTEXT));
577   }
578 
579   /**
580    * &lt;?cs include!'somefile.cs' &gt; command. Throw an error if file does not exist.
581    */
582   @Override
caseAHardIncludeCommand(AHardIncludeCommand node)583   public void caseAHardIncludeCommand(AHardIncludeCommand node) {
584     capturePosition(node.getPosition());
585     java.writeStatement(call("include", expressionTranslator
586         .translateToString(node.getExpression()), JavaExpression.bool(false), CONTEXT));
587   }
588 
589   /**
590    * &lt;?cs include:'somefile.cs' &gt; command. Silently ignore if the included file does not
591    * exist.
592    */
593   @Override
caseAIncludeCommand(AIncludeCommand node)594   public void caseAIncludeCommand(AIncludeCommand node) {
595     capturePosition(node.getPosition());
596     java.writeStatement(call("include", expressionTranslator
597         .translateToString(node.getExpression()), JavaExpression.bool(true), CONTEXT));
598   }
599 
600   /**
601    * &lt;?cs lvar:blah &gt; command. Evaluate expression and execute commands within.
602    */
603   @Override
caseALvarCommand(ALvarCommand node)604   public void caseALvarCommand(ALvarCommand node) {
605     capturePosition(node.getPosition());
606     evaluateVariable(node.getExpression(), "[lvar expression]");
607   }
608 
609   /**
610    * &lt;?cs evar:blah &gt; command. Evaluate expression and execute commands within.
611    */
612   @Override
caseAEvarCommand(AEvarCommand node)613   public void caseAEvarCommand(AEvarCommand node) {
614     capturePosition(node.getPosition());
615     evaluateVariable(node.getExpression(), "[evar expression]");
616   }
617 
evaluateVariable(PExpression expression, String stackTraceDescription)618   private void evaluateVariable(PExpression expression, String stackTraceDescription) {
619     java.writeStatement(callOn(callOn(TEMPLATE_LOADER, "createTemp", string(stackTraceDescription),
620         expressionTranslator.translateToString(expression), callOn(CONTEXT, "getAutoEscapeMode")),
621         "render", CONTEXT));
622   }
623 
624   /**
625    * &lt;?cs def:someMacro(x,y) &gt; ... &lt;?cs /def &gt; command. Define a macro (available for
626    * the remainder of the context).
627    */
628   @Override
caseADefCommand(ADefCommand node)629   public void caseADefCommand(ADefCommand node) {
630     capturePosition(node.getPosition());
631 
632     // This doesn't actually define the macro body yet, it just calls:
633     // registerMacro("someMacroName", someReference);
634     // where someReference is defined as a field later on (with the body).
635     String name = makeWord(node.getMacro());
636     if (macroMap.containsKey(name)) {
637       // this is a duplicated definition.
638       // TODO: Handle duplicates correctly.
639     }
640     // Keep track of the macro so we can generate the body later.
641     // See MacroTransformer.
642     addMacro(name, macro("macro" + macroMap.size()), node);
643   }
644 
645   /**
646    * This is a special tree walker that's called after the render() method has been generated to
647    * create the macro definitions and their bodies.
648    *
649    * It basically generates fields that look like this:
650    *
651    * private final Macro macro1 = new CompiledMacro("myMacro", "arg1", "arg2"...) { public void
652    * render(Data data, RenderingContext context) { // macro body. } };
653    */
654   private class MacroTransformer {
655 
parseDefNode(JavaExpression macroName, ADefCommand node)656     public void parseDefNode(JavaExpression macroName, ADefCommand node) {
657       java.startField("Macro", macroName);
658 
659       // Parameters passed to constructor. First is name of macro, the rest
660       // are the name of the arguments.
661       // e.g. cs def:doStuff(person, cheese)
662       // -> new CompiledMacro("doStuff", "person", "cheese") { .. }.
663       int i = 0;
664       JavaExpression[] args = new JavaExpression[1 + node.getArguments().size()];
665       args[i++] = string(makeWord(node.getMacro()));
666       for (PVariable argName : node.getArguments()) {
667         args[i++] = variableTranslator.translate(argName);
668       }
669       java.startAnonymousClass("CompiledMacro", args);
670 
671       java.startMethod(RENDER_METHOD, "context");
672       java.writeStatement(declare(Type.DATA_CONTEXT, "dataContext", callOn(CONTEXT,
673           "getDataContext")));
674       java.writeStatement(callOn(CONTEXT, "pushExecutionContext", THIS_TEMPLATE));
675       // If !context.isRuntimeAutoEscaping(), enable runtime autoescaping for macro call.
676       String tempVariableName = generateTempVariable("doRuntimeAutoEscaping");
677       JavaExpression value =
678           JavaExpression.prefix(Type.BOOLEAN, "!", callOn(CONTEXT, "isRuntimeAutoEscaping"));
679       JavaExpression stmt = declare(Type.BOOLEAN, tempVariableName, value);
680       java.writeStatement(stmt);
681 
682       JavaExpression doRuntimeAutoEscaping = symbol(Type.BOOLEAN, tempVariableName);
683       java.startIfBlock(doRuntimeAutoEscaping.cast(Type.BOOLEAN));
684       java.writeStatement(callOn(CONTEXT, "startRuntimeAutoEscaping"));
685       java.endIfBlock();
686 
687       node.getCommand().apply(TemplateTranslator.this);
688 
689       java.startIfBlock(doRuntimeAutoEscaping.cast(Type.BOOLEAN));
690       java.writeStatement(callOn(CONTEXT, "stopRuntimeAutoEscaping"));
691       java.endIfBlock();
692       java.writeStatement(callOn(CONTEXT, "popExecutionContext"));
693       java.endMethod();
694       java.endAnonymousClass();
695       java.endField();
696     }
697   }
698 
makeWord(LinkedList<TWord> words)699   private String makeWord(LinkedList<TWord> words) {
700     if (words.size() == 1) {
701       return words.getFirst().getText();
702     }
703     StringBuilder result = new StringBuilder();
704     for (TWord word : words) {
705       if (result.length() > 0) {
706         result.append('.');
707       }
708       result.append(word.getText());
709     }
710     return result.toString();
711   }
712 
713   /**
714    * &lt;?cs call:someMacro(x,y) command. Call a macro.
715    */
716   @Override
caseACallCommand(ACallCommand node)717   public void caseACallCommand(ACallCommand node) {
718     capturePosition(node.getPosition());
719 
720     String name = makeWord(node.getMacro());
721 
722     java.startScopedBlock();
723     java.writeComment("call:" + name);
724 
725     // Lookup macro.
726     // The expression used for the macro will either be the name of the
727     // static Macro object representing the macro (if we can statically
728     // determine it), or will be a temporary Macro variable (named
729     // 'macroCall###') that gets the result of findMacro at evaluation time.
730     JavaExpression macroCalled;
731     MacroInfo macroInfo = macroMap.get(name);
732     if (macroInfo == null) {
733       // We never saw the definition of the macro. Assume it might come in an
734       // included file and look it up at render time.
735       String macroCall = generateTempVariable("macroCall");
736       java
737           .writeStatement(declare(Type.MACRO, macroCall, callOn(CONTEXT, "findMacro", string(name))));
738 
739       macroCalled = macro(macroCall);
740     } else {
741       macroCalled = macroInfo.symbol;
742     }
743 
744     int numArgs = node.getArguments().size();
745     if (numArgs > 0) {
746 
747       // TODO: Add check that number of arguments passed in equals the
748       // number expected by the macro. This should be done at translation
749       // time in a future CL.
750 
751       JavaExpression[] argValues = new JavaExpression[numArgs];
752       JavaExpression[] argStatus = new JavaExpression[numArgs];
753 
754       // Read the values first in case the new local variables shares the same
755       // name as a variable (or variable expansion) being passed in to the macro.
756       int i = 0;
757       for (PExpression argNode : node.getArguments()) {
758         JavaExpression value = expressionTranslator.translateUntyped(argNode);
759         if (value.getType() != Type.VAR_NAME) {
760           value = value.cast(Type.STRING);
761         }
762         String valueName = generateTempVariable("argValue");
763         java.writeStatement(declare(Type.STRING, valueName, value));
764         argValues[i] = JavaExpression.symbol(value.getType(), valueName);
765         if (propagateEscapeStatus) {
766           argStatus[i] = escapingEvaluator.computeEscaping(argNode, propagateEscapeStatus);
767         } else {
768           argStatus[i] = JavaExpression.symbol("EscapeMode.ESCAPE_NONE");
769         }
770 
771         i++;
772       }
773 
774       // Push a new local variable scope for this macro execution.
775       java.writeStatement(callOn(DATA_CONTEXT, "pushVariableScope"));
776 
777       // Create the local variables for each argument.
778       for (i = 0; i < argValues.length; i++) {
779         JavaExpression value = argValues[i];
780         JavaExpression tempVar = callOn(macroCalled, "getArgumentName", integer(i));
781         String methodName;
782         if (value.getType() == Type.VAR_NAME) {
783           methodName = "createLocalVariableByPath";
784           java.writeStatement(callOn(DATA_CONTEXT, methodName, tempVar, value));
785         } else {
786           // Must be String as we cast it above.
787           methodName = "createLocalVariableByValue";
788           java.writeStatement(callOn(DATA_CONTEXT, methodName, tempVar, value, argStatus[i]));
789         }
790       }
791     }
792 
793     // Render macro.
794     java.writeStatement(callOn(macroCalled, "render", CONTEXT));
795 
796     if (numArgs > 0) {
797       // Release the variable scope used by the macro call
798       java.writeStatement(callOn(DATA_CONTEXT, "popVariableScope"));
799     }
800 
801     java.endScopedBlock();
802   }
803 
804   /**
805    * Walks the PPosition tree, which calls {@link #caseTCsOpen(TCsOpen)} below. This is simply to
806    * capture the position of the node in the original template file, to help developers diagnose
807    * errors.
808    */
capturePosition(PPosition position)809   private void capturePosition(PPosition position) {
810     position.apply(this);
811   }
812 
813   /**
814    * Every time a &lt;cs token is found, grab the line and column and call
815    * context.setCurrentPosition() so this is captured for stack traces.
816    */
817   @Override
caseTCsOpen(TCsOpen node)818   public void caseTCsOpen(TCsOpen node) {
819     int line = node.getLine();
820     int column = node.getPos();
821     java.writeStatement(callOn(CONTEXT, "setCurrentPosition", JavaExpression.integer(line),
822         JavaExpression.integer(column)));
823   }
824 
generateTempVariable(String prefix)825   private String generateTempVariable(String prefix) {
826     return prefix + tempVariable++;
827   }
828 }
829