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 17package com.google.clearsilver.jsilver.compiler; 18 19import com.google.clearsilver.jsilver.autoescape.EscapeMode; 20import static com.google.clearsilver.jsilver.compiler.JavaExpression.BooleanLiteralExpression; 21import static com.google.clearsilver.jsilver.compiler.JavaExpression.Type; 22import static com.google.clearsilver.jsilver.compiler.JavaExpression.call; 23import static com.google.clearsilver.jsilver.compiler.JavaExpression.callFindVariable; 24import static com.google.clearsilver.jsilver.compiler.JavaExpression.callOn; 25import static com.google.clearsilver.jsilver.compiler.JavaExpression.declare; 26import static com.google.clearsilver.jsilver.compiler.JavaExpression.increment; 27import static com.google.clearsilver.jsilver.compiler.JavaExpression.infix; 28import static com.google.clearsilver.jsilver.compiler.JavaExpression.inlineIf; 29import static com.google.clearsilver.jsilver.compiler.JavaExpression.integer; 30import static com.google.clearsilver.jsilver.compiler.JavaExpression.literal; 31import static com.google.clearsilver.jsilver.compiler.JavaExpression.macro; 32import static com.google.clearsilver.jsilver.compiler.JavaExpression.string; 33import static com.google.clearsilver.jsilver.compiler.JavaExpression.symbol; 34import com.google.clearsilver.jsilver.data.Data; 35import com.google.clearsilver.jsilver.data.DataContext; 36import com.google.clearsilver.jsilver.functions.Function; 37import com.google.clearsilver.jsilver.functions.FunctionExecutor; 38import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter; 39import com.google.clearsilver.jsilver.syntax.node.AAltCommand; 40import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand; 41import com.google.clearsilver.jsilver.syntax.node.ACallCommand; 42import com.google.clearsilver.jsilver.syntax.node.ADataCommand; 43import com.google.clearsilver.jsilver.syntax.node.ADefCommand; 44import com.google.clearsilver.jsilver.syntax.node.AEachCommand; 45import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand; 46import com.google.clearsilver.jsilver.syntax.node.AEvarCommand; 47import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand; 48import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand; 49import com.google.clearsilver.jsilver.syntax.node.AIfCommand; 50import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand; 51import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand; 52import com.google.clearsilver.jsilver.syntax.node.ALoopCommand; 53import com.google.clearsilver.jsilver.syntax.node.ALoopIncCommand; 54import com.google.clearsilver.jsilver.syntax.node.ALoopToCommand; 55import com.google.clearsilver.jsilver.syntax.node.ALvarCommand; 56import com.google.clearsilver.jsilver.syntax.node.ANameCommand; 57import com.google.clearsilver.jsilver.syntax.node.ANoopCommand; 58import com.google.clearsilver.jsilver.syntax.node.ASetCommand; 59import com.google.clearsilver.jsilver.syntax.node.AUvarCommand; 60import com.google.clearsilver.jsilver.syntax.node.AVarCommand; 61import com.google.clearsilver.jsilver.syntax.node.AWithCommand; 62import com.google.clearsilver.jsilver.syntax.node.PCommand; 63import com.google.clearsilver.jsilver.syntax.node.PExpression; 64import com.google.clearsilver.jsilver.syntax.node.PPosition; 65import com.google.clearsilver.jsilver.syntax.node.PVariable; 66import com.google.clearsilver.jsilver.syntax.node.Start; 67import com.google.clearsilver.jsilver.syntax.node.TCsOpen; 68import com.google.clearsilver.jsilver.syntax.node.TWord; 69import com.google.clearsilver.jsilver.template.Macro; 70import com.google.clearsilver.jsilver.template.RenderingContext; 71import com.google.clearsilver.jsilver.template.Template; 72import com.google.clearsilver.jsilver.values.Value; 73 74import java.io.IOException; 75import java.io.Writer; 76import java.lang.reflect.Method; 77import java.util.HashMap; 78import java.util.LinkedList; 79import java.util.Map; 80import 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 */ 88public 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 */ 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 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 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 224 public void caseADataCommand(ADataCommand node) { 225 String content = node.getData().getText(); 226 java.writeStatement(callOn(CONTEXT, "writeUnescaped", string(content))); 227 } 228 229 /** 230 * <?cs var:blah > expression. Evaluate as string and write output, using default escaping. 231 */ 232 @Override 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 * <?cs uvar:blah > expression. Evaluate as string and write output, but don't escape. 248 */ 249 @Override 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 * <?cs set:x='y' > command. 258 */ 259 @Override 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 * <?cs name:blah > command. Writes out the name of the original variable referred to by a 281 * given node. 282 */ 283 @Override 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 * <?cs if:blah > ... <?cs else > ... <?cs /if > command. 293 */ 294 @Override 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 * <?cs each:x=Stuff > ... <?cs /each > command. Loops over child items of a data 309 * node. 310 */ 311 @Override 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 * <?cs with:x=Something > ... <?cs /with > command. Aliases a value within a specific 321 * scope. 322 */ 323 @Override 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 * <?cs loop:10 > ... <?cs /loop > command. Loops over a range of numbers, starting at 370 * zero. 371 */ 372 @Override 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 * <?cs loop:0,10 > ... <?cs /loop > command. Loops over a range of numbers. 384 */ 385 @Override 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 * <?cs loop:0,10,2 > ... <?cs /loop > command. Loops over a range of numbers, with a 397 * specific increment. 398 */ 399 @Override 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 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 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 * <?cs alt:someValue > ... <?cs /alt > command. If value exists, write it, otherwise 478 * write the body of the command. 479 */ 480 @Override 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 */ 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 * <?cs escape:'html' > command. Changes default escaping function. 529 */ 530 @Override 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 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 * <?cs linclude:'somefile.cs' > command. Lazily includes another template (at render time). 559 * Throw an error if file does not exist. 560 */ 561 @Override 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 * <?cs linclude:'somefile.cs' > command. Lazily includes another template (at render time). 570 * Silently ignore if the included file does not exist. 571 */ 572 @Override 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 * <?cs include!'somefile.cs' > command. Throw an error if file does not exist. 581 */ 582 @Override 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 * <?cs include:'somefile.cs' > command. Silently ignore if the included file does not 591 * exist. 592 */ 593 @Override 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 * <?cs lvar:blah > command. Evaluate expression and execute commands within. 602 */ 603 @Override 604 public void caseALvarCommand(ALvarCommand node) { 605 capturePosition(node.getPosition()); 606 evaluateVariable(node.getExpression(), "[lvar expression]"); 607 } 608 609 /** 610 * <?cs evar:blah > command. Evaluate expression and execute commands within. 611 */ 612 @Override 613 public void caseAEvarCommand(AEvarCommand node) { 614 capturePosition(node.getPosition()); 615 evaluateVariable(node.getExpression(), "[evar expression]"); 616 } 617 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 * <?cs def:someMacro(x,y) > ... <?cs /def > command. Define a macro (available for 626 * the remainder of the context). 627 */ 628 @Override 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 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 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 * <?cs call:someMacro(x,y) command. Call a macro. 715 */ 716 @Override 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 */ 809 private void capturePosition(PPosition position) { 810 position.apply(this); 811 } 812 813 /** 814 * Every time a <cs token is found, grab the line and column and call 815 * context.setCurrentPosition() so this is captured for stack traces. 816 */ 817 @Override 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 825 private String generateTempVariable(String prefix) { 826 return prefix + tempVariable++; 827 } 828} 829