View Javadoc

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   * &lt;tools&gt;
63   *   &lt;toolbox scope="request"&gt;
64   *     &lt;tool class="org.apache.velocity.tools.generic.RenderTool"&gt;
65   *       &lt;property name="parseDepth" type="number" value="10"/&gt;
66   *     &lt;/tool&gt;
67   *   &lt;/toolbox&gt;
68   * &lt;/tools&gt;
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 }