| /* |
| * 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.template; |
| |
| import com.google.clearsilver.jsilver.autoescape.AutoEscapeContext; |
| import com.google.clearsilver.jsilver.autoescape.AutoEscapeOptions; |
| import com.google.clearsilver.jsilver.autoescape.EscapeMode; |
| import com.google.clearsilver.jsilver.data.DataContext; |
| import com.google.clearsilver.jsilver.data.UniqueStack; |
| import com.google.clearsilver.jsilver.exceptions.JSilverAutoEscapingException; |
| import com.google.clearsilver.jsilver.exceptions.JSilverIOException; |
| import com.google.clearsilver.jsilver.exceptions.JSilverInterpreterException; |
| import com.google.clearsilver.jsilver.functions.FunctionExecutor; |
| import com.google.clearsilver.jsilver.resourceloader.ResourceLoader; |
| import com.google.clearsilver.jsilver.values.Value; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.logging.Logger; |
| |
| /** |
| * Default implementation of RenderingContext. |
| */ |
| public class DefaultRenderingContext implements RenderingContext, FunctionExecutor { |
| |
| public static final Logger logger = Logger.getLogger(DefaultRenderingContext.class.getName()); |
| private final DataContext dataContext; |
| private final ResourceLoader resourceLoader; |
| private final Appendable out; |
| private final FunctionExecutor globalFunctionExecutor; |
| private final AutoEscapeOptions autoEscapeOptions; |
| private final UniqueStack<String> includeStack; |
| |
| private List<String> escaperStack = new ArrayList<String>(8); // seems like a reasonable initial |
| // capacity. |
| private String currentEscaper; // optimization to reduce List lookup. |
| |
| private List<Template> executionStack = new ArrayList<Template>(8); |
| |
| private Map<String, Macro> macros = new HashMap<String, Macro>(); |
| private List<EscapeMode> autoEscapeStack = new ArrayList<EscapeMode>(); |
| private EscapeMode autoEscapeMode; |
| private AutoEscapeContext autoEscapeContext; |
| private int line; |
| private int column; |
| private AutoEscapeContext.AutoEscapeState startingAutoEscapeState; |
| |
| public DefaultRenderingContext(DataContext dataContext, ResourceLoader resourceLoader, |
| Appendable out, FunctionExecutor globalFunctionExecutor, AutoEscapeOptions autoEscapeOptions) { |
| this.dataContext = dataContext; |
| this.resourceLoader = resourceLoader; |
| this.out = out; |
| this.globalFunctionExecutor = globalFunctionExecutor; |
| this.autoEscapeOptions = autoEscapeOptions; |
| this.autoEscapeMode = EscapeMode.ESCAPE_NONE; |
| this.autoEscapeContext = null; |
| this.includeStack = new UniqueStack<String>(); |
| } |
| |
| /** |
| * Lookup a function by name, execute it and return the results. |
| */ |
| @Override |
| public Value executeFunction(String name, Value... args) { |
| return globalFunctionExecutor.executeFunction(name, args); |
| } |
| |
| @Override |
| public void escape(String name, String input, Appendable output) throws IOException { |
| globalFunctionExecutor.escape(name, input, output); |
| } |
| |
| @Override |
| public boolean isEscapingFunction(String name) { |
| return globalFunctionExecutor.isEscapingFunction(name); |
| } |
| |
| @Override |
| public void pushEscapingFunction(String name) { |
| escaperStack.add(currentEscaper); |
| if (name == null || name.equals("")) { |
| currentEscaper = null; |
| } else { |
| currentEscaper = name; |
| } |
| } |
| |
| @Override |
| public void popEscapingFunction() { |
| int len = escaperStack.size(); |
| if (len == 0) { |
| throw new IllegalStateException("No more escaping functions to pop."); |
| } |
| currentEscaper = escaperStack.remove(len - 1); |
| } |
| |
| @Override |
| public void writeEscaped(String text) { |
| // If runtime auto escaping is enabled, only apply it if |
| // we are not going to do any other default escaping on the variable. |
| boolean applyAutoEscape = isRuntimeAutoEscaping() && (currentEscaper == null); |
| if (applyAutoEscape) { |
| autoEscapeContext.setCurrentPosition(line, column); |
| pushEscapingFunction(autoEscapeContext.getEscapingFunctionForCurrentState()); |
| } |
| try { |
| if (shouldLogEscapedVariables()) { |
| StringBuilder tmp = new StringBuilder(); |
| globalFunctionExecutor.escape(currentEscaper, text, tmp); |
| if (!tmp.toString().equals(text)) { |
| logger.warning(new StringBuilder(getLoggingPrefix()).append(" Auto-escape changed [") |
| .append(text).append("] to [").append(tmp.toString()).append("]").toString()); |
| } |
| out.append(tmp); |
| } else { |
| globalFunctionExecutor.escape(currentEscaper, text, out); |
| } |
| } catch (IOException e) { |
| throw new JSilverIOException(e); |
| } finally { |
| if (applyAutoEscape) { |
| autoEscapeContext.insertText(); |
| popEscapingFunction(); |
| } |
| } |
| } |
| |
| private String getLoggingPrefix() { |
| return "[" + getCurrentResourceName() + ":" + line + ":" + column + "]"; |
| } |
| |
| private boolean shouldLogEscapedVariables() { |
| return (autoEscapeOptions != null && autoEscapeOptions.getLogEscapedVariables()); |
| } |
| |
| @Override |
| public void writeUnescaped(CharSequence text) { |
| if (isRuntimeAutoEscaping() && (currentEscaper == null)) { |
| autoEscapeContext.setCurrentPosition(line, column); |
| autoEscapeContext.parseData(text.toString()); |
| } |
| try { |
| out.append(text); |
| } catch (IOException e) { |
| throw new JSilverIOException(e); |
| } |
| } |
| |
| @Override |
| public void pushExecutionContext(Template template) { |
| executionStack.add(template); |
| } |
| |
| @Override |
| public void popExecutionContext() { |
| executionStack.remove(executionStack.size() - 1); |
| } |
| |
| @Override |
| public void setCurrentPosition(int line, int column) { |
| // TODO: Should these be saved in executionStack as part |
| // of pushExecutionContext? |
| this.line = line; |
| this.column = column; |
| } |
| |
| @Override |
| public void registerMacro(String name, Macro macro) { |
| macros.put(name, macro); |
| } |
| |
| @Override |
| public Macro findMacro(String name) { |
| Macro macro = macros.get(name); |
| if (macro == null) { |
| throw new JSilverInterpreterException("No such macro: " + name); |
| } |
| return macro; |
| } |
| |
| @Override |
| public DataContext getDataContext() { |
| return dataContext; |
| } |
| |
| @Override |
| public ResourceLoader getResourceLoader() { |
| return resourceLoader; |
| } |
| |
| @Override |
| public AutoEscapeOptions getAutoEscapeOptions() { |
| return autoEscapeOptions; |
| } |
| |
| @Override |
| public EscapeMode getAutoEscapeMode() { |
| if (isRuntimeAutoEscaping() || (currentEscaper != null)) { |
| return EscapeMode.ESCAPE_NONE; |
| } else { |
| return autoEscapeMode; |
| } |
| } |
| |
| @Override |
| public void pushAutoEscapeMode(EscapeMode mode) { |
| if (isRuntimeAutoEscaping()) { |
| throw new JSilverInterpreterException( |
| "cannot call pushAutoEscapeMode while runtime auto escaping is in progress"); |
| } |
| autoEscapeStack.add(autoEscapeMode); |
| autoEscapeMode = mode; |
| } |
| |
| @Override |
| public void popAutoEscapeMode() { |
| int len = autoEscapeStack.size(); |
| if (len == 0) { |
| throw new IllegalStateException("No more auto escaping modes to pop."); |
| } |
| autoEscapeMode = autoEscapeStack.remove(autoEscapeStack.size() - 1); |
| } |
| |
| @Override |
| public boolean isRuntimeAutoEscaping() { |
| return autoEscapeContext != null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @throws JSilverInterpreterException if startRuntimeAutoEscaping is called while runtime |
| * autoescaping is already in progress. |
| */ |
| @Override |
| public void startRuntimeAutoEscaping() { |
| if (isRuntimeAutoEscaping()) { |
| throw new JSilverInterpreterException("startRuntimeAutoEscaping() is not re-entrant at " |
| + getCurrentResourceName()); |
| } |
| if (!autoEscapeMode.equals(EscapeMode.ESCAPE_NONE)) { |
| // TODO: Get the resourceName as a parameter to this function |
| autoEscapeContext = new AutoEscapeContext(autoEscapeMode, getCurrentResourceName()); |
| startingAutoEscapeState = autoEscapeContext.getCurrentState(); |
| } else { |
| autoEscapeContext = null; |
| } |
| } |
| |
| private String getCurrentResourceName() { |
| if (executionStack.size() == 0) { |
| return ""; |
| } else { |
| return executionStack.get(executionStack.size() - 1).getDisplayName(); |
| } |
| } |
| |
| @Override |
| public void stopRuntimeAutoEscaping() { |
| if (autoEscapeContext != null) { |
| if (!startingAutoEscapeState.equals(autoEscapeContext.getCurrentState())) { |
| // We do not allow a macro call to change context of the rest of the template. |
| // Since the rest of the template has already been auto-escaped at parse time |
| // with the assumption that the macro call will not modify the context. |
| throw new JSilverAutoEscapingException("Macro starts in context " + startingAutoEscapeState |
| + " but ends in different context " + autoEscapeContext.getCurrentState(), |
| autoEscapeContext.getResourceName()); |
| } |
| } |
| autoEscapeContext = null; |
| } |
| |
| @Override |
| public boolean pushIncludeStackEntry(String templateName) { |
| return includeStack.push(templateName); |
| } |
| |
| @Override |
| public boolean popIncludeStackEntry(String templateName) { |
| return templateName.equals(includeStack.pop()); |
| } |
| |
| @Override |
| public Iterable<String> getIncludedTemplateNames() { |
| return includeStack; |
| } |
| } |