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.template;
18
19import com.google.clearsilver.jsilver.autoescape.AutoEscapeContext;
20import com.google.clearsilver.jsilver.autoescape.AutoEscapeOptions;
21import com.google.clearsilver.jsilver.autoescape.EscapeMode;
22import com.google.clearsilver.jsilver.data.DataContext;
23import com.google.clearsilver.jsilver.data.UniqueStack;
24import com.google.clearsilver.jsilver.exceptions.JSilverAutoEscapingException;
25import com.google.clearsilver.jsilver.exceptions.JSilverIOException;
26import com.google.clearsilver.jsilver.exceptions.JSilverInterpreterException;
27import com.google.clearsilver.jsilver.functions.FunctionExecutor;
28import com.google.clearsilver.jsilver.resourceloader.ResourceLoader;
29import com.google.clearsilver.jsilver.values.Value;
30
31import java.io.IOException;
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.List;
35import java.util.Map;
36import java.util.logging.Logger;
37
38/**
39 * Default implementation of RenderingContext.
40 */
41public class DefaultRenderingContext implements RenderingContext, FunctionExecutor {
42
43  public static final Logger logger = Logger.getLogger(DefaultRenderingContext.class.getName());
44  private final DataContext dataContext;
45  private final ResourceLoader resourceLoader;
46  private final Appendable out;
47  private final FunctionExecutor globalFunctionExecutor;
48  private final AutoEscapeOptions autoEscapeOptions;
49  private final UniqueStack<String> includeStack;
50
51  private List<String> escaperStack = new ArrayList<String>(8); // seems like a reasonable initial
52                                                                // capacity.
53  private String currentEscaper; // optimization to reduce List lookup.
54
55  private List<Template> executionStack = new ArrayList<Template>(8);
56
57  private Map<String, Macro> macros = new HashMap<String, Macro>();
58  private List<EscapeMode> autoEscapeStack = new ArrayList<EscapeMode>();
59  private EscapeMode autoEscapeMode;
60  private AutoEscapeContext autoEscapeContext;
61  private int line;
62  private int column;
63  private AutoEscapeContext.AutoEscapeState startingAutoEscapeState;
64
65  public DefaultRenderingContext(DataContext dataContext, ResourceLoader resourceLoader,
66      Appendable out, FunctionExecutor globalFunctionExecutor, AutoEscapeOptions autoEscapeOptions) {
67    this.dataContext = dataContext;
68    this.resourceLoader = resourceLoader;
69    this.out = out;
70    this.globalFunctionExecutor = globalFunctionExecutor;
71    this.autoEscapeOptions = autoEscapeOptions;
72    this.autoEscapeMode = EscapeMode.ESCAPE_NONE;
73    this.autoEscapeContext = null;
74    this.includeStack = new UniqueStack<String>();
75  }
76
77  /**
78   * Lookup a function by name, execute it and return the results.
79   */
80  @Override
81  public Value executeFunction(String name, Value... args) {
82    return globalFunctionExecutor.executeFunction(name, args);
83  }
84
85  @Override
86  public void escape(String name, String input, Appendable output) throws IOException {
87    globalFunctionExecutor.escape(name, input, output);
88  }
89
90  @Override
91  public boolean isEscapingFunction(String name) {
92    return globalFunctionExecutor.isEscapingFunction(name);
93  }
94
95  @Override
96  public void pushEscapingFunction(String name) {
97    escaperStack.add(currentEscaper);
98    if (name == null || name.equals("")) {
99      currentEscaper = null;
100    } else {
101      currentEscaper = name;
102    }
103  }
104
105  @Override
106  public void popEscapingFunction() {
107    int len = escaperStack.size();
108    if (len == 0) {
109      throw new IllegalStateException("No more escaping functions to pop.");
110    }
111    currentEscaper = escaperStack.remove(len - 1);
112  }
113
114  @Override
115  public void writeEscaped(String text) {
116    // If runtime auto escaping is enabled, only apply it if
117    // we are not going to do any other default escaping on the variable.
118    boolean applyAutoEscape = isRuntimeAutoEscaping() && (currentEscaper == null);
119    if (applyAutoEscape) {
120      autoEscapeContext.setCurrentPosition(line, column);
121      pushEscapingFunction(autoEscapeContext.getEscapingFunctionForCurrentState());
122    }
123    try {
124      if (shouldLogEscapedVariables()) {
125        StringBuilder tmp = new StringBuilder();
126        globalFunctionExecutor.escape(currentEscaper, text, tmp);
127        if (!tmp.toString().equals(text)) {
128          logger.warning(new StringBuilder(getLoggingPrefix()).append(" Auto-escape changed [")
129              .append(text).append("] to [").append(tmp.toString()).append("]").toString());
130        }
131        out.append(tmp);
132      } else {
133        globalFunctionExecutor.escape(currentEscaper, text, out);
134      }
135    } catch (IOException e) {
136      throw new JSilverIOException(e);
137    } finally {
138      if (applyAutoEscape) {
139        autoEscapeContext.insertText();
140        popEscapingFunction();
141      }
142    }
143  }
144
145  private String getLoggingPrefix() {
146    return "[" + getCurrentResourceName() + ":" + line + ":" + column + "]";
147  }
148
149  private boolean shouldLogEscapedVariables() {
150    return (autoEscapeOptions != null && autoEscapeOptions.getLogEscapedVariables());
151  }
152
153  @Override
154  public void writeUnescaped(CharSequence text) {
155    if (isRuntimeAutoEscaping() && (currentEscaper == null)) {
156      autoEscapeContext.setCurrentPosition(line, column);
157      autoEscapeContext.parseData(text.toString());
158    }
159    try {
160      out.append(text);
161    } catch (IOException e) {
162      throw new JSilverIOException(e);
163    }
164  }
165
166  @Override
167  public void pushExecutionContext(Template template) {
168    executionStack.add(template);
169  }
170
171  @Override
172  public void popExecutionContext() {
173    executionStack.remove(executionStack.size() - 1);
174  }
175
176  @Override
177  public void setCurrentPosition(int line, int column) {
178    // TODO: Should these be saved in executionStack as part
179    // of pushExecutionContext?
180    this.line = line;
181    this.column = column;
182  }
183
184  @Override
185  public void registerMacro(String name, Macro macro) {
186    macros.put(name, macro);
187  }
188
189  @Override
190  public Macro findMacro(String name) {
191    Macro macro = macros.get(name);
192    if (macro == null) {
193      throw new JSilverInterpreterException("No such macro: " + name);
194    }
195    return macro;
196  }
197
198  @Override
199  public DataContext getDataContext() {
200    return dataContext;
201  }
202
203  @Override
204  public ResourceLoader getResourceLoader() {
205    return resourceLoader;
206  }
207
208  @Override
209  public AutoEscapeOptions getAutoEscapeOptions() {
210    return autoEscapeOptions;
211  }
212
213  @Override
214  public EscapeMode getAutoEscapeMode() {
215    if (isRuntimeAutoEscaping() || (currentEscaper != null)) {
216      return EscapeMode.ESCAPE_NONE;
217    } else {
218      return autoEscapeMode;
219    }
220  }
221
222  @Override
223  public void pushAutoEscapeMode(EscapeMode mode) {
224    if (isRuntimeAutoEscaping()) {
225      throw new JSilverInterpreterException(
226          "cannot call pushAutoEscapeMode while runtime auto escaping is in progress");
227    }
228    autoEscapeStack.add(autoEscapeMode);
229    autoEscapeMode = mode;
230  }
231
232  @Override
233  public void popAutoEscapeMode() {
234    int len = autoEscapeStack.size();
235    if (len == 0) {
236      throw new IllegalStateException("No more auto escaping modes to pop.");
237    }
238    autoEscapeMode = autoEscapeStack.remove(autoEscapeStack.size() - 1);
239  }
240
241  @Override
242  public boolean isRuntimeAutoEscaping() {
243    return autoEscapeContext != null;
244  }
245
246  /**
247   * {@inheritDoc}
248   *
249   * @throws JSilverInterpreterException if startRuntimeAutoEscaping is called while runtime
250   *         autoescaping is already in progress.
251   */
252  @Override
253  public void startRuntimeAutoEscaping() {
254    if (isRuntimeAutoEscaping()) {
255      throw new JSilverInterpreterException("startRuntimeAutoEscaping() is not re-entrant at "
256          + getCurrentResourceName());
257    }
258    if (!autoEscapeMode.equals(EscapeMode.ESCAPE_NONE)) {
259      // TODO: Get the resourceName as a parameter to this function
260      autoEscapeContext = new AutoEscapeContext(autoEscapeMode, getCurrentResourceName());
261      startingAutoEscapeState = autoEscapeContext.getCurrentState();
262    } else {
263      autoEscapeContext = null;
264    }
265  }
266
267  private String getCurrentResourceName() {
268    if (executionStack.size() == 0) {
269      return "";
270    } else {
271      return executionStack.get(executionStack.size() - 1).getDisplayName();
272    }
273  }
274
275  @Override
276  public void stopRuntimeAutoEscaping() {
277    if (autoEscapeContext != null) {
278      if (!startingAutoEscapeState.equals(autoEscapeContext.getCurrentState())) {
279        // We do not allow a macro call to change context of the rest of the template.
280        // Since the rest of the template has already been auto-escaped at parse time
281        // with the assumption that the macro call will not modify the context.
282        throw new JSilverAutoEscapingException("Macro starts in context " + startingAutoEscapeState
283            + " but ends in different context " + autoEscapeContext.getCurrentState(),
284            autoEscapeContext.getResourceName());
285      }
286    }
287    autoEscapeContext = null;
288  }
289
290  @Override
291  public boolean pushIncludeStackEntry(String templateName) {
292    return includeStack.push(templateName);
293  }
294
295  @Override
296  public boolean popIncludeStackEntry(String templateName) {
297    return templateName.equals(includeStack.pop());
298  }
299
300  @Override
301  public Iterable<String> getIncludedTemplateNames() {
302    return includeStack;
303  }
304}
305