View Javadoc

1   package org.apache.velocity.tools.struts;
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.util.Iterator;
23  import java.util.Map;
24  import java.util.Stack;
25  import javax.servlet.http.HttpSession;
26  import org.apache.struts.tiles.AttributeDefinition;
27  import org.apache.struts.tiles.ComponentContext;
28  import org.apache.struts.tiles.ComponentDefinition;
29  import org.apache.struts.tiles.Controller;
30  import org.apache.struts.tiles.DefinitionAttribute;
31  import org.apache.struts.tiles.DefinitionNameAttribute;
32  import org.apache.struts.tiles.DefinitionsFactoryException;
33  import org.apache.struts.tiles.DirectStringAttribute;
34  import org.apache.struts.tiles.TilesUtil;
35  import org.apache.velocity.context.Context;
36  import org.apache.velocity.tools.Scope;
37  import org.apache.velocity.tools.config.DefaultKey;
38  import org.apache.velocity.tools.config.ValidScope;
39  import org.apache.velocity.tools.view.ImportSupport;
40  import org.apache.velocity.tools.view.ViewContext;
41  
42  /**
43   * <p>The TilesTool is used to interact with the Struts-Tiles framework that is part
44   *  of Struts 1.</p>
45   * <p><pre>
46   * Template example(s):
47   *  &lt;!-- insert a tile --&gt;
48   *  $tiles.myTileDefinition
49   *
50   *  &lt;!-- get named attribute value from the current tiles-context --&gt;
51   *  $tiles.getAttribute("myTileAttribute")
52   *
53   *  &lt;!-- import all attributes of the current tiles-context into the velocity-context. --&gt;
54   *  $tiles.importAttributes()
55   *
56   * Toolbox configuration:
57   * &lt;tools&gt;
58   *   &lt;toolbox scope="request"&gt;
59   *     &lt;tool class="org.apache.velocity.tools.struts.TilesTool"/&gt;
60   *   &lt;/toolbox&gt;
61   * &lt;/tools&gt;
62   * </pre></p>
63   *
64   * <p>This tool may only be used in the request scope.</p>
65   *
66   * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a>
67   * @since VelocityTools 1.1
68   * @version $Revision: 601976 $ $Date: 2007-12-07 04:50:51 +0100 (ven., 07 déc. 2007) $
69   */
70  @DefaultKey("tiles")
71  @ValidScope(Scope.REQUEST)
72  public class TilesTool extends ImportSupport
73  {
74      static final String PAGE_SCOPE = "page";
75      static final String REQUEST_SCOPE = "request";
76      static final String SESSION_SCOPE = "session";
77      static final String APPLICATION_SCOPE = "application";
78  
79      protected Context velocityContext;
80  
81      /**
82       * A stack to hold ComponentContexts while nested tile-definitions
83       * are rendered.
84       */
85      protected Stack contextStack;
86      
87      /**
88       * Indicates if there is a MethodExceptionEventHandler present
89       */
90      protected boolean catchExceptions = true;
91  
92      /******************************* Constructors ****************************/
93  
94      @Deprecated
95      public void init(Object obj)
96      {
97          if (obj instanceof ViewContext)
98          {
99              ViewContext ctx = (ViewContext)obj;
100             setVelocityContext(ctx.getVelocityContext());
101             setRequest(ctx.getRequest());
102             setResponse(ctx.getResponse());
103             setServletContext(ctx.getServletContext());
104             setLog(ctx.getVelocityEngine().getLog());
105         }
106     }
107 
108     /**
109      * Initializes this tool.
110      *
111      * @param context the current {@link Context}
112      * @throws IllegalArgumentException if the param is not a {@link Context}
113      */
114     public void setVelocityContext(Context context)
115     {
116         if (context == null)
117         {
118             throw new NullPointerException("velocity context should not be null");
119         }
120         this.velocityContext = context;
121     }
122 
123     public void setCatchExceptions(boolean catchExceptions)
124     {
125         this.catchExceptions = catchExceptions;
126     }
127 
128     /***************************** View Helpers ******************************/
129 
130     /**
131      * A generic tiles insert function.
132      *
133      * <p>This is functionally equivalent to
134      * <code>&lt;tiles:insert attribute="foo" /&gt;</code>.</p>
135      *
136      * @param obj Can be any of the following:
137      *        AttributeDefinition,
138      *        tile-definition name,
139      *        tile-attribute name,
140      *        regular uri.
141      *        (checked in that order)
142      * @return the rendered template or value as a String
143      * @throws Exception on failure
144      */
145     public String get(Object obj) throws Exception {
146         try
147         {
148             Object value = getCurrentContext().getAttribute(obj.toString());
149             if (value != null)
150             {
151                 return processObjectValue(value);
152             }
153             return processAsDefinitionOrURL(obj.toString());
154         }
155         catch (Exception e)
156         {
157             LOG.error("TilesTool : Exeption while rendering Tile " + obj, e);
158 
159             /* delegate exception handling to an EventHandler if present. */
160             if (!this.catchExceptions) {
161             	throw e;
162             }
163             
164             return null;
165         }
166     }
167 
168     /**
169      * Fetches a named attribute-value from the current tiles-context.
170      *
171      * <p>This is functionally equivalent to
172      * <code>&lt;tiles:getAsString name="foo" /&gt;</code>.</p>
173      *
174      * @param name the name of the tiles-attribute to fetch
175      * @return attribute value for the named attribute
176      */
177     public Object getAttribute(String name)
178     {
179         Object value = getCurrentContext().getAttribute(name);
180         if (value == null)
181         {
182             LOG.warn("TilesTool : Tile attribute '" + name + "' wasn't found in context.");
183         }
184         return value;
185     }
186 
187     /**
188      * Imports the named attribute-value from the current tiles-context into the
189      * current Velocity context.
190      *
191      * <p>This is functionally equivalent to
192      * <code>&lt;tiles:importAttribute name="foo" /&gt;</code>
193      *
194      * @param name the name of the tiles-attribute to import
195      */
196     public void importAttribute(String name)
197     {
198         this.importAttribute(name, PAGE_SCOPE);
199     }
200 
201     /**
202      * Imports the named attribute-value from the current tiles-context into the
203      * named context ("page", "request", "session", or "application").
204      *
205      * <p>This is functionally equivalent to
206      * <code>&lt;tiles:importAttribute name="foo" scope="scopeValue" /&gt;</code>
207      *
208      * @param name the name of the tiles-attribute to import
209      * @param scope the named context scope to put the attribute into.
210      */
211     public void importAttribute(String name, String scope)
212     {
213         Object value = getCurrentContext().getAttribute(name);
214         if (value == null)
215         {
216             LOG.warn("TilesTool : Tile attribute '" + name + "' wasn't found in context.");
217         }
218 
219         if (scope.equals(PAGE_SCOPE))
220         {
221             velocityContext.put(name, value);
222         }
223         else if (scope.equals(REQUEST_SCOPE))
224         {
225             request.setAttribute(name, value);
226         }
227         else if (scope.equals(SESSION_SCOPE))
228         {
229             request.getSession().setAttribute(name, value);
230         }
231         else if (scope.equals(APPLICATION_SCOPE))
232         {
233             application.setAttribute(name, value);
234         }
235     }
236 
237     /**
238      * Imports all attributes in the current tiles-context into the
239      * current velocity-context.
240      *
241      * <p>This is functionally equivalent to
242      * <code>&lt;tiles:importAttribute /&gt;</code>.</p>
243      */
244     public void importAttributes()
245     {
246         this.importAttributes(PAGE_SCOPE);
247     }
248 
249     /**
250      * Imports all attributes in the current tiles-context into the named
251      * context ("page", "request", "session", or "application").
252      *
253      * <p>This is functionally equivalent to
254      * <code>&lt;tiles:importAttribute scope="scopeValue" /&gt;</code>.</p>
255      *
256      * @param scope the named context scope to put the attributes into.
257      */
258     public void importAttributes(String scope)
259     {
260         ComponentContext context = getCurrentContext();
261         Iterator names = context.getAttributeNames();
262 
263         if (scope.equals(PAGE_SCOPE))
264         {
265             while (names.hasNext())
266             {
267                 String name = (String)names.next();
268                 velocityContext.put(name, context.getAttribute(name));
269             }
270         }
271         else if (scope.equals(REQUEST_SCOPE))
272         {
273             while (names.hasNext())
274             {
275                 String name = (String)names.next();
276                 request.setAttribute(name, context.getAttribute(name));
277             }
278         }
279         else if (scope.equals(SESSION_SCOPE))
280         {
281             HttpSession session = request.getSession();
282             while (names.hasNext())
283             {
284                 String name = (String)names.next();
285                 session.setAttribute(name, context.getAttribute(name));
286             }
287         }
288         else if (scope.equals(APPLICATION_SCOPE))
289         {
290             while (names.hasNext())
291             {
292                 String name = (String)names.next();
293                 application.setAttribute(name, context.getAttribute(name));
294             }
295         }
296     }
297 
298 
299     /************************** Protected Methods ****************************/
300 
301     /**
302      * Process an object retrieved as a bean or attribute.
303      *
304      * @param value - Object can be a typed attribute, a String, or anything
305      *        else. If typed attribute, use associated type. Otherwise, apply
306      *        toString() on object, and use returned string as a name.
307      * @throws Exception - Throws by underlying nested call to
308      *         processDefinitionName()
309      * @return the fully processed value as String
310      */
311     protected String processObjectValue(Object value) throws Exception
312     {
313         /* First, check if value is one of the Typed Attribute */
314         if (value instanceof AttributeDefinition)
315         {
316             /* We have a type => return appropriate IncludeType */
317             return processTypedAttribute((AttributeDefinition)value);
318         }
319         else if (value instanceof ComponentDefinition)
320         {
321             return processDefinition((ComponentDefinition)value);
322         }
323         /* Value must denote a valid String */
324         return processAsDefinitionOrURL(value.toString());
325     }
326 
327     /**
328      * Process typed attribute according to its type.
329      *
330      * @param value Typed attribute to process.
331      * @return the fully processed attribute value as String.
332      * @throws Exception - Throws by underlying nested call to processDefinitionName()
333      */
334     protected String processTypedAttribute(AttributeDefinition value)
335         throws Exception
336     {
337         if (value instanceof DirectStringAttribute)
338         {
339             return (String)value.getValue();
340         }
341         else if (value instanceof DefinitionAttribute)
342         {
343             return processDefinition((ComponentDefinition)value.getValue());
344         }
345         else if (value instanceof DefinitionNameAttribute)
346         {
347             return processAsDefinitionOrURL((String)value.getValue());
348         }
349         /* else if( value instanceof PathAttribute ) */
350         return doInsert((String)value.getValue(), null, null);
351     }
352 
353     /**
354      * Try to process name as a definition, or as an URL if not found.
355      *
356      * @param name Name to process.
357      * @return the fully processed definition or URL
358      * @throws Exception
359      */
360     protected String processAsDefinitionOrURL(String name) throws Exception
361     {
362         try
363         {
364             ComponentDefinition definition =
365                     TilesUtil.getDefinition(name, this.request, this.application);
366             if (definition != null)
367             {
368                 return processDefinition(definition);
369             }
370         }
371         catch (DefinitionsFactoryException ex)
372         {
373         /* silently failed, because we can choose to not define a factory. */
374         }
375         /* no definition found, try as url */
376         return processUrl(name);
377     }
378 
379     /**
380      * End of Process for definition.
381      *
382      * @param definition Definition to process.
383      * @return the fully processed definition.
384      * @throws Exception from InstantiationException Can't create requested controller
385      */
386     protected String processDefinition(ComponentDefinition definition) throws
387             Exception
388     {
389         Controller controller = null;
390         try
391         {
392             controller = definition.getOrCreateController();
393 
394             String role = definition.getRole();
395             String page = definition.getTemplate();
396 
397             return doInsert(definition.getAttributes(),
398                             page,
399                             role,
400                             controller);
401         }
402         catch (InstantiationException ex)
403         {
404             throw new Exception(ex.getMessage());
405         }
406     }
407 
408     /**
409      * Processes an url
410      *
411      * @param url the URI to process.
412      * @return the rendered template as String.
413      * @throws Exception
414      */
415     protected String processUrl(String url) throws Exception
416     {
417         return doInsert(url, null, null);
418     }
419 
420     /**
421      * Use this if there is no nested tile.
422      *
423      * @param page the page to process.
424      * @param role possible user-role
425      * @param controller possible tiles-controller
426      * @return the rendered template as String.
427      * @throws Exception
428      */
429     protected String doInsert(String page, String role, Controller controller) throws
430             Exception
431     {
432         if (role != null && !this.request.isUserInRole(role))
433         {
434             return null;
435         }
436 
437         ComponentContext subCompContext = new ComponentContext();
438         return doInsert(subCompContext, page, role, controller);
439     }
440 
441     /**
442      * Use this if there is a nested tile.
443      *
444      * @param attributes attributes for the sub-context
445      * @param page the page to process.
446      * @param role possible user-role
447      * @param controller possible tiles-controller
448      * @return the rendered template as String.
449      * @throws Exception
450      */
451     protected String doInsert(Map attributes,
452                               String page,
453                               String role,
454                               Controller controller) throws Exception
455     {
456         if (role != null && !this.request.isUserInRole(role))
457         {
458             return null;
459         }
460 
461         ComponentContext subCompContext = new ComponentContext(attributes);
462         return doInsert(subCompContext, page, role, controller);
463     }
464 
465     /**
466      * An extension of the other two doInsert functions
467      *
468      * @param subCompContext the sub-context to set in scope when the
469      *        template is rendered.
470      * @param page the page to process.
471      * @param role possible user-role
472      * @param controller possible tiles-controller
473      * @return the rendered template as String.
474      * @throws Exception
475      */
476     protected String doInsert(ComponentContext subCompContext,
477                               String page,
478                               String role,
479                               Controller controller) throws Exception
480     {
481         pushTilesContext();
482         try
483         {
484             ComponentContext.setContext(subCompContext, this.request);
485 
486             /* Call controller if any */
487             if (controller != null)
488             {
489                 controller.execute(subCompContext,
490                                    this.request,
491                                    this.response,
492                                    this.application);
493             }
494             /* pass things off to ImportSupport */
495             return this.acquireString(page);
496         }
497         finally
498         {
499             popTilesContext();
500         }
501     }
502 
503     /**
504      * Retrieve the current tiles component context.
505      * This is pretty much just a convenience method.
506      */
507     protected ComponentContext getCurrentContext()
508     {
509         return ComponentContext.getContext(this.request);
510     }
511 
512     /**
513      * <p>pushes the current tiles context onto the context-stack.
514      * preserving the context is necessary so that a sub-context can be
515      * put into request scope and lower level tiles can be rendered</p>
516      */
517     protected void pushTilesContext()
518     {
519         if (this.contextStack == null)
520         {
521             this.contextStack = new Stack();
522         }
523         contextStack.push(getCurrentContext());
524     }
525 
526     /**
527      * Pops the tiles sub-context off the context-stack after the lower level
528      * tiles have been rendered.
529      */
530     protected void popTilesContext()
531     {
532         ComponentContext context = (ComponentContext)this.contextStack.pop();
533         ComponentContext.setContext(context, this.request);
534     }
535 
536 }