| package org.apache.velocity.tools.generic; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| import java.io.StringWriter; |
| import org.apache.velocity.VelocityContext; |
| import org.apache.velocity.app.Velocity; |
| import org.apache.velocity.app.VelocityEngine; |
| import org.apache.velocity.context.Context; |
| import org.apache.velocity.tools.Scope; |
| import org.apache.velocity.tools.config.DefaultKey; |
| import org.apache.velocity.tools.config.InvalidScope; |
| |
| /** |
| * This tool exposes methods to evaluate the given |
| * strings as VTL (Velocity Template Language) |
| * using either a pre-configured context or one you |
| * provide directly. |
| * <pre> |
| * Example of eval(): |
| * Input |
| * ----- |
| * #set( $list = [1,2,3] ) |
| * #set( $object = '$list' ) |
| * #set( $method = 'size()' ) |
| * $render.eval("${object}.$method") |
| * |
| * Output |
| * ------ |
| * 3 |
| * |
| * Example of recurse(): |
| * Input |
| * ----- |
| * #macro( say_hi )hello world!#end |
| * #set( $foo = '#say_hi()' ) |
| * #set( $bar = '$foo' ) |
| * $render.recurse($bar) |
| * |
| * Output |
| * ------ |
| * hello world! |
| * |
| * |
| * Toolbox configuration: |
| * <tools> |
| * <toolbox scope="request"> |
| * <tool class="org.apache.velocity.tools.generic.RenderTool"> |
| * <property name="parseDepth" type="number" value="10"/> |
| * </tool> |
| * </toolbox> |
| * </tools> |
| * </pre> |
| * |
| * <p>Ok, so these examples are really lame. But, it seems like |
| * someone out there is always asking how to do stuff like this |
| * and we always tell them to write a tool. Now we can just tell |
| * them to use this tool.</p> |
| * |
| * <p>This tool may be used in any scope, however, the context provided |
| * for the {@link #eval(String)} and {@link #recurse(String)} methods |
| * will only be current if the tool is request scoped. If application or |
| * session scoped, then the context will be the same one set at the time |
| * of the tool's first use. In such a case, each call to eval(String) or |
| * recurse(String) will by default create a new Context that wraps the |
| * configured one to prevent modifications to the configured Context |
| * (concurrent or otherwise). If you wish to risk it and accrete changes |
| * then you can relax the thread-safety by setting the 'forceThreadSafe' |
| * property to 'false'. </p> |
| * |
| * <p>Of course none of the previous paragraph likely applies if you are |
| * not using the core tool management facilities or if you stick to the |
| * {@link #eval(Context,String)} and {@link #recurse(Context,String)} |
| * methods. :)</p> |
| * |
| * <p>This tool by default will catch |
| * and log any exceptions thrown during rendering and |
| * instead return null in such cases. It also limits recursion, by default, |
| * to 20 cycles, to prevent infinite loops. Both settings may be configured |
| * to behave otherwise.</p> |
| * |
| * @author Nathan Bubna |
| * @version $Revision$ $Date$ |
| */ |
| |
| @DefaultKey("render") |
| @InvalidScope(Scope.SESSION) |
| public class RenderTool extends SafeConfig |
| { |
| /** |
| * The maximum number of loops allowed when recursing. |
| * @since VelocityTools 1.2 |
| */ |
| public static final int DEFAULT_PARSE_DEPTH = 20; |
| @Deprecated |
| public static final String KEY_PARSE_DEPTH = "parse.depth"; |
| @Deprecated |
| public static final String KEY_CATCH_EXCEPTIONS = "catch.exceptions"; |
| |
| public static final String KEY_FORCE_THREAD_SAFE = "forceThreadSafe"; |
| |
| private VelocityEngine engine = null; |
| private Context context; |
| private int parseDepth = DEFAULT_PARSE_DEPTH; |
| private boolean catchExceptions = true; |
| private boolean forceThreadSafe = true; |
| |
| /** |
| * Looks for deprecated parse depth and catch.exceptions properties, |
| * as well as any 'forceThreadSafe' setting. |
| */ |
| protected void configure(ValueParser parser) |
| { |
| // look for deprecated parse.depth key |
| Integer depth = parser.getInteger(KEY_PARSE_DEPTH); |
| if (depth != null) |
| { |
| setParseDepth(depth); |
| } |
| |
| // look for deprecated catch.exceptions key |
| Boolean catchEm = parser.getBoolean(KEY_CATCH_EXCEPTIONS); |
| if (catchEm != null) |
| { |
| setCatchExceptions(catchEm); |
| } |
| |
| // check if they want thread-safety manually turned off |
| this.forceThreadSafe = |
| parser.getBoolean(KEY_FORCE_THREAD_SAFE, forceThreadSafe); |
| // if we're request-scoped, then there's no point in forcing the issue |
| if (Scope.REQUEST.equals(parser.getString("scope"))) |
| { |
| this.forceThreadSafe = false; |
| } |
| } |
| |
| /** |
| * Allow user to specify a VelocityEngine to be used |
| * in place of the Velocity singleton. |
| * @param ve VelocityEngine instance |
| */ |
| public void setVelocityEngine(VelocityEngine ve) |
| { |
| this.engine = ve; |
| } |
| |
| /** |
| * Set the maximum number of loops allowed when recursing. |
| * @param depth parse depth |
| * @since VelocityTools 1.2 |
| */ |
| public void setParseDepth(int depth) |
| { |
| if (!isConfigLocked()) |
| { |
| this.parseDepth = depth; |
| } |
| else if (this.parseDepth != depth) |
| { |
| getLog().error("Attempt was made to alter parse depth while config was locked."); |
| } |
| } |
| |
| /** |
| * Sets the {@link Context} to be used by the {@link #eval(String)} |
| * and {@link #recurse(String)} methods. |
| * @param context Velocity context |
| */ |
| public void setVelocityContext(Context context) |
| { |
| if (!isConfigLocked()) |
| { |
| if (context == null) |
| { |
| throw new NullPointerException("context must not be null"); |
| } |
| this.context = context; |
| } |
| else if (this.context != context) |
| { |
| getLog().error("Attempt was made to set a new context while config was locked."); |
| } |
| } |
| |
| /** |
| * Get the maximum number of loops allowed when recursing. |
| * @return parse depth |
| * @since VelocityTools 1.2 |
| */ |
| public int getParseDepth() |
| { |
| return this.parseDepth; |
| } |
| |
| /** |
| * Sets whether or not the render() and eval() methods should catch |
| * exceptions during their execution or not. |
| * @param catchExceptions whether to catch exceptions |
| * @since VelocityTools 1.3 |
| */ |
| public void setCatchExceptions(boolean catchExceptions) |
| { |
| if (!isConfigLocked()) |
| { |
| this.catchExceptions = catchExceptions; |
| } |
| else if (this.catchExceptions != catchExceptions) |
| { |
| getLog().error("Attempt was made to alter catchE while config was locked."); |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if this render() and eval() methods will |
| * catch exceptions thrown during rendering. |
| * @return whether to catch exceptions |
| * @since VelocityTools 1.3 |
| */ |
| public boolean getCatchExceptions() |
| { |
| return this.catchExceptions; |
| } |
| |
| /** |
| * <p>Evaluates a String containing VTL using the context passed |
| * to the {@link #setVelocityContext} method. If this tool is request |
| * scoped, then this will be the current context and open to modification |
| * by the rendered VTL. If application or session scoped, the context |
| * will be a new wrapper around the configured context to protect it |
| * from modification. |
| * The results of the rendering are returned as a String. By default, |
| * <code>null</code> will be returned when this throws an exception. |
| * This evaluation is not recursive.</p> |
| * |
| * @param vtl the code to be evaluated |
| * @return the evaluated code as a String |
| * @throws Exception if womething went wrong |
| */ |
| public String eval(String vtl) throws Exception |
| { |
| Context ctx = forceThreadSafe ? new VelocityContext(context) : context; |
| return eval(ctx, vtl); |
| } |
| |
| |
| /** |
| * <p>Recursively evaluates a String containing VTL using the |
| * current context, and returns the result as a String. It |
| * will continue to re-evaluate the output of the last |
| * evaluation until an evaluation returns the same code |
| * that was fed into it.</p> |
| * |
| * @see #eval(String) |
| * @param vtl the code to be evaluated |
| * @return the evaluated code as a String |
| * @throws Exception if womething went wrong |
| */ |
| public String recurse(String vtl) throws Exception |
| { |
| Context ctx = forceThreadSafe ? new VelocityContext(context) : context; |
| return recurse(ctx, vtl); |
| } |
| |
| /** |
| * <p>Evaluates a String containing VTL using the current context, |
| * and returns the result as a String. By default if this fails, then |
| * <code>null</code> will be returned, though this tool can be configured |
| * to let Exceptions pass through. This evaluation is not recursive.</p> |
| * |
| * @param ctx the current Context |
| * @param vtl the code to be evaluated |
| * @return the evaluated code as a String |
| * @throws Exception if womething went wrong |
| */ |
| public String eval(Context ctx, String vtl) throws Exception |
| { |
| if (this.catchExceptions) |
| { |
| try |
| { |
| return internalEval(ctx, vtl); |
| } |
| catch (Exception e) |
| { |
| getLog().error("evaluation failed:", e); |
| return null; |
| } |
| } |
| else |
| { |
| return internalEval(ctx, vtl); |
| } |
| } |
| |
| |
| /* Internal implementation of the eval() method function. */ |
| protected String internalEval(Context ctx, String vtl) throws Exception |
| { |
| if (vtl == null) |
| { |
| return null; |
| } |
| StringWriter sw = new StringWriter(); |
| boolean success; |
| if (engine == null) |
| { |
| success = Velocity.evaluate(ctx, sw, "RenderTool.eval()", vtl); |
| } |
| else |
| { |
| success = engine.evaluate(ctx, sw, "RenderTool.eval()", vtl); |
| } |
| if (success) |
| { |
| return sw.toString(); |
| } |
| /* or would it be preferable to return the original? */ |
| return null; |
| } |
| |
| /** |
| * <p>Recursively evaluates a String containing VTL using the |
| * current context, and returns the result as a String. It |
| * will continue to re-evaluate the output of the last |
| * evaluation until an evaluation returns the same code |
| * that was fed into it or the number of recursive loops |
| * exceeds the set parse depth.</p> |
| * |
| * @param ctx the current Context |
| * @param vtl the code to be evaluated |
| * @return the evaluated code as a String |
| * @throws Exception if womething went wrong |
| */ |
| public String recurse(Context ctx, String vtl) throws Exception |
| { |
| return internalRecurse(ctx, vtl, 0); |
| } |
| |
| protected String internalRecurse(Context ctx, String vtl, int count) throws Exception |
| { |
| String result = eval(ctx, vtl); |
| if (result == null || result.equals(vtl)) |
| { |
| return result; |
| } |
| else |
| { |
| // if we haven't reached our parse depth... |
| if (count < parseDepth) |
| { |
| // continue recursing |
| return internalRecurse(ctx, result, count + 1); |
| } |
| else |
| { |
| // abort, log and return what we have so far |
| getLog().error("recursion exceeded the maximum parse depth" + |
| " of {} on the following template: {}", |
| parseDepth, vtl); |
| return result; |
| } |
| } |
| } |
| } |