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 * <!-- insert a tile -->
48 * $tiles.myTileDefinition
49 *
50 * <!-- get named attribute value from the current tiles-context -->
51 * $tiles.getAttribute("myTileAttribute")
52 *
53 * <!-- import all attributes of the current tiles-context into the velocity-context. -->
54 * $tiles.importAttributes()
55 *
56 * Toolbox configuration:
57 * <tools>
58 * <toolbox scope="request">
59 * <tool class="org.apache.velocity.tools.struts.TilesTool"/>
60 * </toolbox>
61 * </tools>
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><tiles:insert attribute="foo" /></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><tiles:getAsString name="foo" /></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><tiles:importAttribute name="foo" /></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><tiles:importAttribute name="foo" scope="scopeValue" /></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><tiles:importAttribute /></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><tiles:importAttribute scope="scopeValue" /></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 }