/* * Copyright (C) 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.clearsilver.jsilver.syntax; import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter; import com.google.clearsilver.jsilver.syntax.node.AAddExpression; import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand; import com.google.clearsilver.jsilver.syntax.node.AFunctionExpression; import com.google.clearsilver.jsilver.syntax.node.AMultipleCommand; import com.google.clearsilver.jsilver.syntax.node.ANameVariable; import com.google.clearsilver.jsilver.syntax.node.AStringExpression; import com.google.clearsilver.jsilver.syntax.node.AVarCommand; import com.google.clearsilver.jsilver.syntax.node.Node; import com.google.clearsilver.jsilver.syntax.node.PCommand; import com.google.clearsilver.jsilver.syntax.node.PExpression; import com.google.clearsilver.jsilver.syntax.node.PPosition; import com.google.clearsilver.jsilver.syntax.node.PVariable; import com.google.clearsilver.jsilver.syntax.node.TString; import java.util.Collection; import java.util.LinkedList; /** * Recursively optimizes the syntax tree with a set of simple operations. This class currently * optimizes: * *

* String add expressions in var commands are optimized by replacing something like: * *

 * <cs? var:a + b ?>
 * 
* with: * *
 * <cs? var:a ?><cs? var:b ?>
 * 
* * This avoids having to construct the intermediate result {@code a + b} at runtime and reduces * runtime heap allocations. *

* Functions call to escaping functions are optimized by replacing them with the equivalent escaping * construct. This is faster because escapers are called with the strings themselves whereas general * function calls require value objects to be created. *

* Expressions such as: * *

 * <cs? var:html_escape(foo) ?>
 * 
* are turned into: * *
 * <cs? escape:"html" ?>
 * <cs? var:foo ?>
 * <?cs /escape ?>
 * 
* * It also optimizes sequences of escaped expressions into a single escaped sequence. *

* It is important to note that these optimizations cannot be done in isolation if we want to * optimize compound expressions such as: * *

 * <cs? html_escape(foo + bar) + baz ?>
 * 
* which is turned into: * *
 * <cs? escape:"html" ?>
 * <cs? var:foo ?>
 * <cs? var:bar ?>
 * <?cs /escape ?>
 * <?cs var:baz ?>
 * 
* * WARNING: This class isn't strictly just an optimization and its modification of the syntax tree * actually improves JSilver's behavior, bringing it more in line with ClearSilver. Consider the * sequence: * *
 * <cs? escape:"html" ?>
 * <cs? var:url_escape(foo) ?>
 * <?cs /escape ?>
 * 
* * In JSilver (without this optimizer being run) this would result in {@code foo} being escaped by * both the html escaper and the url escaping function. However ClearSilver treats top-level escaper * functions specially and {@code foo} is only escaped once by the url escaping function. * * The good news is that this optimization rewrites the above example to: * *
 * <cs? escape:"html" ?>
 * <cs? escape:"url" ?>
 * <cs? var:foo ?>
 * <?cs /escape ?>
 * <?cs /escape ?>
 * 
* which fixes the problem because the new url escaper replaces the existing html escaper (rather * than combining with it). * * The only fly in the ointment here is the {@code url_validate} function which is treated like an * escaper by ClearSilver but which does not (currently) have an escaper associated with it. This * means that: * *
 * <cs? escape:"html" ?>
 * <cs? var:url_validate(foo) ?>
 * <?cs /escape ?>
 * 
* will not be rewritten by this class and will result in {@code foo} being escaped twice. * */ public class VarOptimizer extends DepthFirstAdapter { /** * A list of escaper names that are also exposed as escaping functions (eg, if the "foo" escaper * is also exposed as "foo_escape" function then this collection should contain the string "foo"). */ private final Collection escaperNames; public VarOptimizer(Collection escaperNames) { this.escaperNames = escaperNames; } @Override public void caseAMultipleCommand(AMultipleCommand multiCommand) { super.caseAMultipleCommand(multiCommand); multiCommand.replaceBy(optimizeEscapeSequences(multiCommand)); } @Override public void caseAVarCommand(AVarCommand varCommand) { super.caseAVarCommand(varCommand); varCommand.replaceBy(optimizeVarCommands(varCommand)); } /** * Optimizes a complex var command by recursively expanding its expression into a sequence of * simpler var commands. Currently two expressions are targetted for expansion: string * concatenation and escaping functions. */ private PCommand optimizeVarCommands(AVarCommand varCommand) { PExpression expression = varCommand.getExpression(); PPosition position = varCommand.getPosition(); // This test relies on the type optimizer having replaced add commands // with numeric add commands. if (expression instanceof AAddExpression) { // Replace: // with: AAddExpression addExpression = (AAddExpression) expression; AMultipleCommand multiCommand = new AMultipleCommand(); addToContents(multiCommand, optimizedVarCommandOf(position, addExpression.getLeft())); addToContents(multiCommand, optimizedVarCommandOf(position, addExpression.getRight())); return optimizeEscapeSequences(multiCommand); } // This test relies on the sequence optimizer removing single element // sequence commands. if (expression instanceof AFunctionExpression) { // Replace: // with: AFunctionExpression functionExpression = (AFunctionExpression) expression; String name = escapeNameOf(functionExpression); if (escaperNames.contains(name)) { LinkedList args = functionExpression.getArgs(); if (args.size() == 1) { return new AEscapeCommand(position, quotedStringExpressionOf(name), optimizedVarCommandOf(position, args.getFirst())); } } } return varCommand; } /** * Create a var command from the given expression and recursively optimize it, returning the * result. */ private PCommand optimizedVarCommandOf(PPosition position, PExpression expression) { return optimizeVarCommands(new AVarCommand(cloneOf(position), cloneOf(expression))); } /** Simple helper to clone nodes in a typesafe way */ @SuppressWarnings("unchecked") private static T cloneOf(T t) { return (T) t.clone(); } /** * Helper to efficiently add commands to a multiple command (if the command to be added is a * multiple command, we add its contents). This is used to implement a tail recursion optimization * to flatten multiple commands. */ private static void addToContents(AMultipleCommand multi, PCommand command) { if (command instanceof AMultipleCommand) { multi.getCommand().addAll(((AMultipleCommand) command).getCommand()); } else { multi.getCommand().add(command); } } /** When used as functions, escapers have the name 'foo_escape' */ private static final String ESCAPE_SUFFIX = "_escape"; /** * Returns the name of the escaper which could replace this function (or null if this function * cannot be replaced). */ private static String escapeNameOf(AFunctionExpression function) { PVariable nvar = function.getName(); if (!(nvar instanceof ANameVariable)) { // We are not interested in dynamic function calls (such as "a.b(x)") return null; } String name = ((ANameVariable) nvar).getWord().getText(); if (!name.endsWith(ESCAPE_SUFFIX)) { return null; } return name.substring(0, name.length() - ESCAPE_SUFFIX.length()); } /** * Returns a quoted string expression of the given text. *

* This is used because when an escaper is called as a function we need to replace: * *

   * <cs? var:foo_escape(bar) ?>
   * 
* with: * *
   * <cs? escape:"foo" ?><cs? var:bar ?><?cs /escape ?>
   * 
* Using the quoted escaper name. */ private static AStringExpression quotedStringExpressionOf(String text) { assert text.indexOf('"') == -1; return new AStringExpression(new TString('"' + text + '"')); } /** * Returns a new command containing the contents of the given multiple command but with with * multiple successive (matching) escape commands folded into one. */ private static PCommand optimizeEscapeSequences(AMultipleCommand multiCommand) { AEscapeCommand lastEscapeCommand = null; LinkedList commands = new LinkedList(); for (PCommand command : multiCommand.getCommand()) { AEscapeCommand escapeCommand = asSimpleEscapeCommand(command); if (isSameEscaper(escapeCommand, lastEscapeCommand)) { addToContents(contentsOf(lastEscapeCommand), escapeCommand.getCommand()); } else { // Add the original command and set the escaper (possibly null) commands.add(command); lastEscapeCommand = escapeCommand; } } assert !commands.isEmpty(); return (commands.size() > 1) ? new AMultipleCommand(commands) : commands.getFirst(); } /** * Returns the escaped command associated with the given escape function as a multiple command. If * the command was already a multiple command, it is returned, otherwise a new multiple command is * created to wrap the original escaped command. This helper facilitates merging multiple * sequences of escapers. */ private static AMultipleCommand contentsOf(AEscapeCommand escapeCommand) { PCommand escapedCommand = escapeCommand.getCommand(); if (escapedCommand instanceof AMultipleCommand) { return (AMultipleCommand) escapedCommand; } AMultipleCommand multiCommand = new AMultipleCommand(); multiCommand.getCommand().add(escapedCommand); escapeCommand.setCommand(multiCommand); return multiCommand; } /** * Returns the given command only if it is an escape command with a simple, string literal, name; * otherwise returns {@code null}. */ private static AEscapeCommand asSimpleEscapeCommand(PCommand command) { if (!(command instanceof AEscapeCommand)) { return null; } AEscapeCommand escapeCommand = (AEscapeCommand) command; if (!(escapeCommand.getExpression() instanceof AStringExpression)) { return null; } return escapeCommand; } /** * Compares two simple escape commands and returns true if they perform the same escaping * function. */ private static boolean isSameEscaper(AEscapeCommand newCommand, AEscapeCommand oldCommand) { if (newCommand == null || oldCommand == null) { return false; } return simpleNameOf(newCommand).equals(simpleNameOf(oldCommand)); } /** * Returns the name of the given simple escape command (as returned by * {@link #asSimpleEscapeCommand(PCommand)}). */ private static String simpleNameOf(AEscapeCommand escapeCommand) { return ((AStringExpression) escapeCommand.getExpression()).getValue().getText(); } }