1 package org.apache.velocity.tools.generic; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import java.io.StringWriter; 23 import org.apache.velocity.VelocityContext; 24 import org.apache.velocity.app.Velocity; 25 import org.apache.velocity.app.VelocityEngine; 26 import org.apache.velocity.context.Context; 27 import org.apache.velocity.tools.Scope; 28 import org.apache.velocity.tools.config.DefaultKey; 29 30 /** 31 * This tool exposes methods to evaluate the given 32 * strings as VTL (Velocity Template Language) 33 * using either a pre-configured context or one you 34 * provide directly. 35 * <pre> 36 * Example of eval(): 37 * Input 38 * ----- 39 * #set( $list = [1,2,3] ) 40 * #set( $object = '$list' ) 41 * #set( $method = 'size()' ) 42 * $render.eval("${object}.$method") 43 * 44 * Output 45 * ------ 46 * 3 47 * 48 * Example of recurse(): 49 * Input 50 * ----- 51 * #macro( say_hi )hello world!#end 52 * #set( $foo = '#say_hi()' ) 53 * #set( $bar = '$foo' ) 54 * $render.recurse($bar) 55 * 56 * Output 57 * ------ 58 * hello world! 59 * 60 * 61 * Toolbox configuration: 62 * <tools> 63 * <toolbox scope="request"> 64 * <tool class="org.apache.velocity.tools.generic.RenderTool"> 65 * <property name="parseDepth" type="number" value="10"/> 66 * </tool> 67 * </toolbox> 68 * </tools> 69 * </pre> 70 * 71 * <p>Ok, so these examples are really lame. But, it seems like 72 * someone out there is always asking how to do stuff like this 73 * and we always tell them to write a tool. Now we can just tell 74 * them to use this tool.</p> 75 * 76 * <p>This tool may be used in any scope, however, the context provided 77 * for the {@link #eval(String)} and {@link #recurse(String)} methods 78 * will only be current if the tool is request scoped. If application or 79 * session scoped, then the context will be the same one set at the time 80 * of the tool's first use. In such a case, each call to eval(String) or 81 * recurse(String) will by default create a new Context that wraps the 82 * configured one to prevent modifications to the configured Context 83 * (concurrent or otherwise). If you wish to risk it and accrete changes 84 * then you can relax the thread-safety by setting the 'forceThreadSafe' 85 * property to 'false'. </p> 86 * 87 * <p>Of course none of the previous paragraph likely applies if you are 88 * not using the core tool management facilities or if you stick to the 89 * {@link #eval(Context,String)} and {@link #recurse(Context,String)} 90 * methods. :)</p> 91 * 92 * <p>This tool by default will catch 93 * and log any exceptions thrown during rendering and 94 * instead return null in such cases. It also limits recursion, by default, 95 * to 20 cycles, to prevent infinite loops. Both settings may be configured 96 * to behave otherwise.</p> 97 * 98 * @author Nathan Bubna 99 * @version $Revision: 671010 $ $Date: 2008-06-23 20:40:41 -0700 (Mon, 23 Jun 2008) $ 100 */ 101 @DefaultKey("render") 102 public class RenderTool extends SafeConfig 103 { 104 /** 105 * The maximum number of loops allowed when recursing. 106 * @since VelocityTools 1.2 107 */ 108 public static final int DEFAULT_PARSE_DEPTH = 20; 109 @Deprecated 110 public static final String KEY_PARSE_DEPTH = "parse.depth"; 111 @Deprecated 112 public static final String KEY_CATCH_EXCEPTIONS = "catch.exceptions"; 113 114 public static final String KEY_FORCE_THREAD_SAFE = "forceThreadSafe"; 115 116 private static final String LOG_TAG = "RenderTool.eval()"; 117 118 private VelocityEngine engine = null; 119 private Context context; 120 private int parseDepth = DEFAULT_PARSE_DEPTH; 121 private boolean catchExceptions = true; 122 private boolean forceThreadSafe = true; 123 124 /** 125 * Looks for deprecated parse depth and catch.exceptions properties, 126 * as well as any 'forceThreadSafe' setting. 127 */ 128 protected void configure(ValueParser parser) 129 { 130 // look for deprecated parse.depth key 131 Integer depth = parser.getInteger(KEY_PARSE_DEPTH); 132 if (depth != null) 133 { 134 setParseDepth(depth); 135 } 136 137 // look for deprecated catch.exceptions key 138 Boolean catchEm = parser.getBoolean(KEY_CATCH_EXCEPTIONS); 139 if (catchEm != null) 140 { 141 setCatchExceptions(catchEm); 142 } 143 144 // check if they want thread-safety manually turned off 145 this.forceThreadSafe = 146 parser.getBoolean(KEY_FORCE_THREAD_SAFE, forceThreadSafe); 147 // if we're request-scoped, then there's no point in forcing the issue 148 if (Scope.REQUEST.equals(parser.getString("scope"))) 149 { 150 this.forceThreadSafe = false; 151 } 152 } 153 154 /** 155 * Allow user to specify a VelocityEngine to be used 156 * in place of the Velocity singleton. 157 */ 158 public void setVelocityEngine(VelocityEngine ve) 159 { 160 this.engine = ve; 161 } 162 163 /** 164 * Set the maximum number of loops allowed when recursing. 165 * 166 * @since VelocityTools 1.2 167 */ 168 public void setParseDepth(int depth) 169 { 170 if (!isConfigLocked()) 171 { 172 this.parseDepth = depth; 173 } 174 else if (this.parseDepth != depth) 175 { 176 debug("RenderTool: Attempt was made to alter parse depth while config was locked."); 177 } 178 } 179 180 /** 181 * Sets the {@link Context} to be used by the {@link #eval(String)} 182 * and {@link #recurse(String)} methods. 183 */ 184 public void setVelocityContext(Context context) 185 { 186 if (!isConfigLocked()) 187 { 188 if (context == null) 189 { 190 throw new NullPointerException("context must not be null"); 191 } 192 this.context = context; 193 } 194 else if (this.context != context) 195 { 196 debug("RenderTool: Attempt was made to set a new context while config was locked."); 197 } 198 } 199 200 /** 201 * Get the maximum number of loops allowed when recursing. 202 * 203 * @since VelocityTools 1.2 204 */ 205 public int getParseDepth() 206 { 207 return this.parseDepth; 208 } 209 210 /** 211 * Sets whether or not the render() and eval() methods should catch 212 * exceptions during their execution or not. 213 * @since VelocityTools 1.3 214 */ 215 public void setCatchExceptions(boolean catchExceptions) 216 { 217 if (!isConfigLocked()) 218 { 219 this.catchExceptions = catchExceptions; 220 } 221 else if (this.catchExceptions != catchExceptions) 222 { 223 debug("RenderTool: Attempt was made to alter catchE while config was locked."); 224 } 225 } 226 227 /** 228 * Returns <code>true</code> if this render() and eval() methods will 229 * catch exceptions thrown during rendering. 230 * @since VelocityTools 1.3 231 */ 232 public boolean getCatchExceptions() 233 { 234 return this.catchExceptions; 235 } 236 237 /** 238 * <p>Evaluates a String containing VTL using the context passed 239 * to the {@link #setVelocityContext} method. If this tool is request 240 * scoped, then this will be the current context and open to modification 241 * by the rendered VTL. If application or session scoped, the context 242 * will be a new wrapper around the configured context to protect it 243 * from modification. 244 * The results of the rendering are returned as a String. By default, 245 * <code>null</code> will be returned when this throws an exception. 246 * This evaluation is not recursive.</p> 247 * 248 * @param vtl the code to be evaluated 249 * @return the evaluated code as a String 250 */ 251 public String eval(String vtl) throws Exception 252 { 253 Context ctx = forceThreadSafe ? new VelocityContext(context) : context; 254 return eval(ctx, vtl); 255 } 256 257 258 /** 259 * <p>Recursively evaluates a String containing VTL using the 260 * current context, and returns the result as a String. It 261 * will continue to re-evaluate the output of the last 262 * evaluation until an evaluation returns the same code 263 * that was fed into it.</p> 264 * 265 * @see #eval(String) 266 * @param vtl the code to be evaluated 267 * @return the evaluated code as a String 268 */ 269 public String recurse(String vtl) throws Exception 270 { 271 Context ctx = forceThreadSafe ? new VelocityContext(context) : context; 272 return recurse(ctx, vtl); 273 } 274 275 /** 276 * <p>Evaluates a String containing VTL using the current context, 277 * and returns the result as a String. By default if this fails, then 278 * <code>null</code> will be returned, though this tool can be configured 279 * to let Exceptions pass through. This evaluation is not recursive.</p> 280 * 281 * @param ctx the current Context 282 * @param vtl the code to be evaluated 283 * @return the evaluated code as a String 284 */ 285 public String eval(Context ctx, String vtl) throws Exception 286 { 287 if (this.catchExceptions) 288 { 289 try 290 { 291 return internalEval(ctx, vtl); 292 } 293 catch (Exception e) 294 { 295 debug(LOG_TAG+" failed due to "+e, e); 296 return null; 297 } 298 } 299 else 300 { 301 return internalEval(ctx, vtl); 302 } 303 } 304 305 306 /* Internal implementation of the eval() method function. */ 307 protected String internalEval(Context ctx, String vtl) throws Exception 308 { 309 if (vtl == null) 310 { 311 return null; 312 } 313 StringWriter sw = new StringWriter(); 314 boolean success; 315 if (engine == null) 316 { 317 success = Velocity.evaluate(ctx, sw, LOG_TAG, vtl); 318 } 319 else 320 { 321 success = engine.evaluate(ctx, sw, LOG_TAG, vtl); 322 } 323 if (success) 324 { 325 return sw.toString(); 326 } 327 /* or would it be preferable to return the original? */ 328 return null; 329 } 330 331 /** 332 * <p>Recursively evaluates a String containing VTL using the 333 * current context, and returns the result as a String. It 334 * will continue to re-evaluate the output of the last 335 * evaluation until an evaluation returns the same code 336 * that was fed into it or the number of recursive loops 337 * exceeds the set parse depth.</p> 338 * 339 * @param ctx the current Context 340 * @param vtl the code to be evaluated 341 * @return the evaluated code as a String 342 */ 343 public String recurse(Context ctx, String vtl) throws Exception 344 { 345 return internalRecurse(ctx, vtl, 0); 346 } 347 348 protected String internalRecurse(Context ctx, String vtl, int count) throws Exception 349 { 350 String result = eval(ctx, vtl); 351 if (result == null || result.equals(vtl)) 352 { 353 return result; 354 } 355 else 356 { 357 // if we haven't reached our parse depth... 358 if (count < parseDepth) 359 { 360 // continue recursing 361 return internalRecurse(ctx, result, count + 1); 362 } 363 else 364 { 365 // abort, log and return what we have so far 366 debug("RenderTool.recurse() exceeded the maximum parse depth of " 367 + parseDepth + "on the following template: "+vtl); 368 return result; 369 } 370 } 371 } 372 373 // internal convenience methods 374 private void debug(String message) 375 { 376 if (engine == null) 377 { 378 Velocity.getLog().debug(message); 379 } 380 else 381 { 382 engine.getLog().debug(message); 383 } 384 } 385 386 private void debug(String message, Throwable t) 387 { 388 if (engine == null) 389 { 390 Velocity.getLog().debug(message, t); 391 } 392 else 393 { 394 engine.getLog().debug(message, t); 395 } 396 } 397 398 }