View Javadoc

1   /*
2    * $Id: VelocityManager.java 651946 2008-04-27 13:41:38Z apetrelli $
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  package org.apache.struts2.views.velocity;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.StringTokenizer;
36  
37  import javax.servlet.ServletContext;
38  import javax.servlet.http.HttpServletRequest;
39  import javax.servlet.http.HttpServletResponse;
40  
41  import org.apache.struts2.ServletActionContext;
42  import org.apache.struts2.StrutsConstants;
43  import org.apache.struts2.StrutsException;
44  import org.apache.struts2.util.VelocityStrutsUtil;
45  import org.apache.struts2.views.TagLibrary;
46  import org.apache.struts2.views.jsp.ui.OgnlTool;
47  import org.apache.struts2.views.util.ContextUtil;
48  import org.apache.velocity.VelocityContext;
49  import org.apache.velocity.app.Velocity;
50  import org.apache.velocity.app.VelocityEngine;
51  import org.apache.velocity.context.Context;
52  import org.apache.velocity.tools.view.ToolboxManager;
53  import org.apache.velocity.tools.view.context.ChainedContext;
54  import org.apache.velocity.tools.view.servlet.ServletToolboxManager;
55  
56  import com.opensymphony.xwork2.ObjectFactory;
57  import com.opensymphony.xwork2.inject.Container;
58  import com.opensymphony.xwork2.inject.Inject;
59  import com.opensymphony.xwork2.util.ValueStack;
60  import com.opensymphony.xwork2.util.logging.Logger;
61  import com.opensymphony.xwork2.util.logging.LoggerFactory;
62  
63  
64  /***
65   * Manages the environment for Velocity result types
66   *
67   */
68  public class VelocityManager {
69      private static final Logger LOG = LoggerFactory.getLogger(VelocityManager.class);
70      public static final String STRUTS = "struts";
71      private ObjectFactory objectFactory;
72  
73      /***
74       * the parent JSP tag
75       */
76      public static final String PARENT = "parent";
77  
78      /***
79       * the current JSP tag
80       */
81      public static final String TAG = "tag";
82  
83      private VelocityEngine velocityEngine;
84  
85      /***
86       * A reference to the toolbox manager.
87       */
88      protected ToolboxManager toolboxManager = null;
89      private String toolBoxLocation;
90  
91  
92      /***
93       * Names of contexts that will be chained on every request
94       */
95      private String[] chainedContextNames;
96  
97      private Properties velocityProperties;
98  
99      private String customConfigFile;
100 
101     private List<TagLibrary> tagLibraries;
102 
103     public VelocityManager() {
104     }
105 
106     @Inject
107     public void setObjectFactory(ObjectFactory fac) {
108         this.objectFactory = fac;
109     }
110 
111     @Inject
112     public void setContainer(Container container) {
113         List<TagLibrary> list = new ArrayList<TagLibrary>();
114         Set<String> prefixes = container.getInstanceNames(TagLibrary.class);
115         for (String prefix : prefixes) {
116             list.add(container.getInstance(TagLibrary.class, prefix));
117         }
118         this.tagLibraries = Collections.unmodifiableList(list);
119     }
120 
121     /***
122      * retrieve an instance to the current VelocityManager
123      */
124     /*public synchronized static VelocityManager getInstance() {
125         if (instance == null) {
126             String classname = VelocityManager.class.getName();
127 
128             if (Settings.isSet(StrutsConstants.STRUTS_VELOCITY_MANAGER_CLASSNAME)) {
129                 classname = Settings.get(StrutsConstants.STRUTS_VELOCITY_MANAGER_CLASSNAME).trim();
130             }
131 
132             if (!classname.equals(VelocityManager.class.getName())) {
133                 try {
134                     log.info("Instantiating VelocityManager!, " + classname);
135                     // singleton instances shouldn't be built accessing request or session-specific context data
136                     instance = (VelocityManager) ObjectFactory.getObjectFactory().buildBean(classname, null);
137                 } catch (Exception e) {
138                     log.fatal("Fatal exception occurred while trying to instantiate a VelocityManager instance, " + classname, e);
139                     instance = new VelocityManager();
140                 }
141             } else {
142                 instance = new VelocityManager();
143             }
144         }
145 
146         return instance;
147     }
148     */
149 
150     /***
151      * @return a reference to the VelocityEngine used by <b>all</b> struts velocity thingies with the exception of
152      *         directly accessed *.vm pages
153      */
154     public VelocityEngine getVelocityEngine() {
155         return velocityEngine;
156     }
157 
158     /***
159      * This method is responsible for creating the standard VelocityContext used by all WW2 velocity views.  The
160      * following context parameters are defined:
161      * <p/>
162      * <ul>
163      * <li><strong>request</strong> - the current HttpServletRequest</li>
164      * <li><strong>response</strong> - the current HttpServletResponse</li>
165      * <li><strong>stack</strong> - the current {@link ValueStack}</li>
166      * <li><strong>ognl</strong> - an {@link OgnlTool}</li>
167      * <li><strong>struts</strong> - an instance of {@link org.apache.struts2.util.StrutsUtil}</li>
168      * <li><strong>action</strong> - the current Struts action</li>
169      * </ul>
170      *
171      * @return a new StrutsVelocityContext
172      */
173     public Context createContext(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
174         VelocityContext[] chainedContexts = prepareChainedContexts(req, res, stack.getContext());
175         StrutsVelocityContext context = new StrutsVelocityContext(chainedContexts, stack);
176         Map standardMap = ContextUtil.getStandardContext(stack, req, res);
177         for (Iterator iterator = standardMap.entrySet().iterator(); iterator.hasNext();) {
178             Map.Entry entry = (Map.Entry) iterator.next();
179             context.put((String) entry.getKey(), entry.getValue());
180         }
181         context.put(STRUTS, new VelocityStrutsUtil(velocityEngine, context, stack, req, res));
182 
183 
184         ServletContext ctx = null;
185         try {
186             ctx = ServletActionContext.getServletContext();
187         } catch (NullPointerException npe) {
188             // in case this was used outside the lifecycle of struts servlet
189             LOG.debug("internal toolbox context ignored");
190         }
191 
192         if (toolboxManager != null && ctx != null) {
193             ChainedContext chained = new ChainedContext(context, req, res, ctx);
194             chained.setToolbox(toolboxManager.getToolbox(chained));
195             return chained;
196         } else {
197             return context;
198         }
199 
200     }
201 
202     /***
203      * constructs contexts for chaining on this request.  This method does not
204      * perform any initialization of the contexts.  All that must be done in the
205      * context itself.
206      *
207      * @param servletRequest
208      * @param servletResponse
209      * @param extraContext
210      * @return an VelocityContext[] of contexts to chain
211      */
212     protected VelocityContext[] prepareChainedContexts(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Map extraContext) {
213         if (this.chainedContextNames == null) {
214             return null;
215         }
216         List contextList = new ArrayList();
217         for (int i = 0; i < chainedContextNames.length; i++) {
218             String className = chainedContextNames[i];
219             try {
220                 VelocityContext velocityContext = (VelocityContext) objectFactory.buildBean(className, null);
221                 contextList.add(velocityContext);
222             } catch (Exception e) {
223                 LOG.warn("Warning.  " + e.getClass().getName() + " caught while attempting to instantiate a chained VelocityContext, " + className + " -- skipping");
224             }
225         }
226         if (contextList.size() > 0) {
227             VelocityContext[] extraContexts = new VelocityContext[contextList.size()];
228             contextList.toArray(extraContexts);
229             return extraContexts;
230         } else {
231             return null;
232         }
233     }
234 
235     /***
236      * initializes the VelocityManager.  this should be called during the initialization process, say by
237      * ServletDispatcher.  this may be called multiple times safely although calls beyond the first won't do anything
238      *
239      * @param context the current servlet context
240      */
241     public synchronized void init(ServletContext context) {
242         if (velocityEngine == null) {
243             velocityEngine = newVelocityEngine(context);
244         }
245         this.initToolbox(context);
246     }
247 
248     /***
249      * load optional velocity properties using the following loading strategy
250      * <ul>
251      * <li>relative to the servlet context path</li>
252      * <li>relative to the WEB-INF directory</li>
253      * <li>on the classpath</li>
254      * </ul>
255      *
256      * @param context the current ServletContext.  may <b>not</b> be null
257      * @return the optional properties if struts.velocity.configfile was specified, an empty Properties file otherwise
258      */
259     public Properties loadConfiguration(ServletContext context) {
260         if (context == null) {
261             String gripe = "Error attempting to create a loadConfiguration from a null ServletContext!";
262             LOG.error(gripe);
263             throw new IllegalArgumentException(gripe);
264         }
265 
266         Properties properties = new Properties();
267 
268         // now apply our systemic defaults, then allow user to override
269         applyDefaultConfiguration(context, properties);
270 
271 
272         String defaultUserDirective = properties.getProperty("userdirective");
273 
274         /***
275          * if the user has specified an external velocity configuration file, we'll want to search for it in the
276          * following order
277          *
278          * 1. relative to the context path
279          * 2. relative to /WEB-INF
280          * 3. in the class path
281          */
282         String configfile;
283 
284         if (customConfigFile != null) {
285             configfile = customConfigFile;
286         } else {
287             configfile = "velocity.properties";
288         }
289 
290         configfile = configfile.trim();
291 
292         InputStream in = null;
293         String resourceLocation = null;
294 
295         try {
296             if (context.getRealPath(configfile) != null) {
297                 // 1. relative to context path, i.e. /velocity.properties
298                 String filename = context.getRealPath(configfile);
299 
300                 if (filename != null) {
301                     File file = new File(filename);
302 
303                     if (file.isFile()) {
304                         resourceLocation = file.getCanonicalPath() + " from file system";
305                         in = new FileInputStream(file);
306                     }
307 
308                     // 2. if nothing was found relative to the context path, search relative to the WEB-INF directory
309                     if (in == null) {
310                         file = new File(context.getRealPath("/WEB-INF/" + configfile));
311 
312                         if (file.isFile()) {
313                             resourceLocation = file.getCanonicalPath() + " from file system";
314                             in = new FileInputStream(file);
315                         }
316                     }
317                 }
318             }
319 
320             // 3. finally, if there's no physical file, how about something in our classpath
321             if (in == null) {
322                 in = VelocityManager.class.getClassLoader().getResourceAsStream(configfile);
323                 if (in != null) {
324                     resourceLocation = configfile + " from classloader";
325                 }
326             }
327 
328             // if we've got something, load 'er up
329             if (in != null) {
330                 LOG.info("Initializing velocity using " + resourceLocation);
331                 properties.load(in);
332             }
333         } catch (IOException e) {
334             LOG.warn("Unable to load velocity configuration " + resourceLocation, e);
335         } finally {
336             if (in != null) {
337                 try {
338                     in.close();
339                 } catch (IOException e) {
340                 }
341             }
342         }
343 
344         // overide with programmatically set properties
345         if (this.velocityProperties != null) {
346             Iterator keys = this.velocityProperties.keySet().iterator();
347             while (keys.hasNext()) {
348                 String key = (String) keys.next();
349                 properties.setProperty(key, this.velocityProperties.getProperty(key));
350             }
351         }
352 
353         String userdirective = properties.getProperty("userdirective");
354 
355         if ((userdirective == null) || userdirective.trim().equals("")) {
356             userdirective = defaultUserDirective;
357         } else {
358             userdirective = userdirective.trim() + "," + defaultUserDirective;
359         }
360 
361         properties.setProperty("userdirective", userdirective);
362 
363 
364         // for debugging purposes, allows users to dump out the properties that have been configured
365         if (LOG.isDebugEnabled()) {
366             LOG.debug("Initializing Velocity with the following properties ...");
367 
368             for (Iterator iter = properties.keySet().iterator();
369                  iter.hasNext();) {
370                 String key = (String) iter.next();
371                 String value = properties.getProperty(key);
372 
373                 if (LOG.isDebugEnabled()) {
374                     LOG.debug("    '" + key + "' = '" + value + "'");
375                 }
376             }
377         }
378 
379         return properties;
380     }
381 
382     @Inject(StrutsConstants.STRUTS_VELOCITY_CONFIGFILE)
383     public void setCustomConfigFile(String val) {
384         this.customConfigFile = val;
385     }
386 
387     @Inject(StrutsConstants.STRUTS_VELOCITY_TOOLBOXLOCATION)
388     public void setToolBoxLocation(String toolboxLocation) {
389         this.toolBoxLocation = toolboxLocation;
390     }
391 
392     /***
393      * allow users to specify via the struts.properties file a set of additional VelocityContexts to chain to the
394      * the StrutsVelocityContext.  The intent is to allow these contexts to store helper objects that the ui
395      * developer may want access to.  Examples of reasonable VelocityContexts would be an IoCVelocityContext, a
396      * SpringReferenceVelocityContext, and a ToolboxVelocityContext
397      */
398     @Inject(StrutsConstants.STRUTS_VELOCITY_CONTEXTS)
399     public void setChainedContexts(String contexts) {
400         // we expect contexts to be a comma separated list of classnames
401         StringTokenizer st = new StringTokenizer(contexts, ",");
402         List contextList = new ArrayList();
403 
404         while (st.hasMoreTokens()) {
405             String classname = st.nextToken();
406             contextList.add(classname);
407         }
408         if (contextList.size() > 0) {
409             String[] chainedContexts = new String[contextList.size()];
410             contextList.toArray(chainedContexts);
411             this.chainedContextNames = chainedContexts;
412         }
413     }
414 
415     /***
416      * Initializes the ServletToolboxManager for this servlet's
417      * toolbox (if any).
418      */
419     protected void initToolbox(ServletContext context) {
420         /* if we have a toolbox, get a manager for it */
421         if (toolBoxLocation != null) {
422             toolboxManager = ServletToolboxManager.getInstance(context, toolBoxLocation);
423         } else {
424             Velocity.info("VelocityViewServlet: No toolbox entry in configuration.");
425         }
426     }
427 
428 
429 
430 
431     /***
432      * <p/>
433      * Instantiates a new VelocityEngine.
434      * </p>
435      * <p/>
436      * The following is the default Velocity configuration
437      * </p>
438      * <pre>
439      *  resource.loader = file, class
440      *  file.resource.loader.path = real path of webapp
441      *  class.resource.loader.description = Velocity Classpath Resource Loader
442      *  class.resource.loader.class = org.apache.struts2.views.velocity.StrutsResourceLoader
443      * </pre>
444      * <p/>
445      * this default configuration can be overridden by specifying a struts.velocity.configfile property in the
446      * struts.properties file.  the specified config file will be searched for in the following order:
447      * </p>
448      * <ul>
449      * <li>relative to the servlet context path</li>
450      * <li>relative to the WEB-INF directory</li>
451      * <li>on the classpath</li>
452      * </ul>
453      *
454      * @param context the current ServletContext.  may <b>not</b> be null
455      */
456     protected VelocityEngine newVelocityEngine(ServletContext context) {
457         if (context == null) {
458             String gripe = "Error attempting to create a new VelocityEngine from a null ServletContext!";
459             LOG.error(gripe);
460             throw new IllegalArgumentException(gripe);
461         }
462 
463         Properties p = loadConfiguration(context);
464 
465         VelocityEngine velocityEngine = new VelocityEngine();
466 
467         //  Set the velocity attribute for the servlet context
468         //  if this is not set the webapp loader WILL NOT WORK
469         velocityEngine.setApplicationAttribute(ServletContext.class.getName(),
470                 context);
471 
472         try {
473             velocityEngine.init(p);
474         } catch (Exception e) {
475             String gripe = "Unable to instantiate VelocityEngine!";
476             throw new StrutsException(gripe, e);
477         }
478 
479         return velocityEngine;
480     }
481 
482     /***
483      * once we've loaded up the user defined configurations, we will want to apply Struts specification configurations.
484      * <ul>
485      * <li>if Velocity.RESOURCE_LOADER has not been defined, then we will use the defaults which is a joined file,
486      * class loader for unpackaed wars and a straight class loader otherwise</li>
487      * <li>we need to define the various Struts custom user directives such as #param, #tag, and #bodytag</li>
488      * </ul>
489      *
490      * @param context
491      * @param p
492      */
493     private void applyDefaultConfiguration(ServletContext context, Properties p) {
494         // ensure that caching isn't overly aggressive
495 
496         /***
497          * Load a default resource loader definition if there isn't one present.
498          * Ben Hall (22/08/2003)
499          */
500         if (p.getProperty(Velocity.RESOURCE_LOADER) == null) {
501             p.setProperty(Velocity.RESOURCE_LOADER, "strutsfile, strutsclass");
502         }
503 
504         /***
505          * If there's a "real" path add it for the strutsfile resource loader.
506          * If there's no real path and they haven't configured a loader then we change
507          * resource loader property to just use the strutsclass loader
508          * Ben Hall (22/08/2003)
509          */
510         if (context.getRealPath("") != null) {
511             p.setProperty("strutsfile.resource.loader.description", "Velocity File Resource Loader");
512             p.setProperty("strutsfile.resource.loader.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
513             p.setProperty("strutsfile.resource.loader.path", context.getRealPath(""));
514             p.setProperty("strutsfile.resource.loader.modificationCheckInterval", "2");
515             p.setProperty("strutsfile.resource.loader.cache", "true");
516         } else {
517             // remove strutsfile from resource loader property
518             String prop = p.getProperty(Velocity.RESOURCE_LOADER);
519             if (prop.indexOf("strutsfile,") != -1) {
520                 prop = replace(prop, "strutsfile,", "");
521             } else if (prop.indexOf(", strutsfile") != -1) {
522                 prop = replace(prop, ", strutsfile", "");
523             } else if (prop.indexOf("strutsfile") != -1) {
524                 prop = replace(prop, "strutsfile", "");
525             }
526 
527             p.setProperty(Velocity.RESOURCE_LOADER, prop);
528         }
529 
530         /***
531          * Refactored the Velocity templates for the Struts taglib into the classpath from the web path.  This will
532          * enable Struts projects to have access to the templates by simply including the Struts jar file.
533          * Unfortunately, there does not appear to be a macro for the class loader keywords
534          * Matt Ho - Mon Mar 17 00:21:46 PST 2003
535          */
536         p.setProperty("strutsclass.resource.loader.description", "Velocity Classpath Resource Loader");
537         p.setProperty("strutsclass.resource.loader.class", "org.apache.struts2.views.velocity.StrutsResourceLoader");
538         p.setProperty("strutsclass.resource.loader.modificationCheckInterval", "2");
539         p.setProperty("strutsclass.resource.loader.cache", "true");
540 
541         // components
542         StringBuffer sb = new StringBuffer();
543 
544         for (TagLibrary tagLibrary : tagLibraries) {
545             List<Class> directives = tagLibrary.getVelocityDirectiveClasses();
546             for (Class directive : directives) {
547                 addDirective(sb, directive);
548             }
549         }
550 
551         String directives = sb.toString();
552 
553         String userdirective = p.getProperty("userdirective");
554         if ((userdirective == null) || userdirective.trim().equals("")) {
555             userdirective = directives;
556         } else {
557             userdirective = userdirective.trim() + "," + directives;
558         }
559 
560         p.setProperty("userdirective", userdirective);
561     }
562 
563     private void addDirective(StringBuffer sb, Class clazz) {
564         sb.append(clazz.getName()).append(",");
565     }
566 
567     private static final String replace(String string, String oldString, String newString) {
568         if (string == null) {
569             return null;
570         }
571         // If the newString is null, just return the string since there's nothing to replace.
572         if (newString == null) {
573             return string;
574         }
575         int i = 0;
576         // Make sure that oldString appears at least once before doing any processing.
577         if ((i = string.indexOf(oldString, i)) >= 0) {
578             // Use char []'s, as they are more efficient to deal with.
579             char[] string2 = string.toCharArray();
580             char[] newString2 = newString.toCharArray();
581             int oLength = oldString.length();
582             StringBuffer buf = new StringBuffer(string2.length);
583             buf.append(string2, 0, i).append(newString2);
584             i += oLength;
585             int j = i;
586             // Replace all remaining instances of oldString with newString.
587             while ((i = string.indexOf(oldString, i)) > 0) {
588                 buf.append(string2, j, i - j).append(newString2);
589                 i += oLength;
590                 j = i;
591             }
592             buf.append(string2, j, string2.length - j);
593             return buf.toString();
594         }
595         return string;
596     }
597 
598     /***
599      * @return the velocityProperties
600      */
601     public Properties getVelocityProperties() {
602         return velocityProperties;
603     }
604 
605     /***
606      * @param velocityProperties the velocityProperties to set
607      */
608     public void setVelocityProperties(Properties velocityProperties) {
609         this.velocityProperties = velocityProperties;
610     }
611 }