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