View Javadoc

1   package org.apache.velocity.tools;
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.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.HashMap;
25  import java.util.Map;
26  import org.apache.commons.beanutils.PropertyUtils;
27  import org.apache.velocity.tools.ClassUtils;
28  import org.apache.velocity.tools.config.SkipSetters;
29  
30  /**
31   * Manages data needed to create instances of a tool. New instances
32   * are returned for every call to create(obj).
33   *
34   * @author Nathan Bubna
35   * @author <a href="mailto:henning@schmiedehausen.org">Henning P. Schmiedehausen</a>
36   * @version $Id: ToolInfo.java 511959 2007-02-26 19:24:39Z nbubna $
37   */
38  public class ToolInfo implements java.io.Serializable
39  {
40      private static final long serialVersionUID = -8145087882015742757L;
41      public static final String CONFIGURE_METHOD_NAME = "configure";
42  
43      private String key;
44      private Class clazz;
45      private boolean restrictToIsExact;
46      private String restrictTo;
47      private Map<String,Object> properties;
48      private Boolean skipSetters;
49      private transient Method configure = null;
50  
51      /**
52       * Creates a new instance using the minimum required info
53       * necessary for a tool.
54       */
55      public ToolInfo(String key, Class clazz)
56      {
57          setKey(key);
58          setClass(clazz);
59      }
60  
61  
62      /***********************  Mutators *************************/
63  
64      public void setKey(String key)
65      {
66          this.key = key;
67          if (this.key == null)
68          {
69              throw new NullPointerException("Key cannot be null");
70          }
71      }
72  
73      /**
74       * Tries to create an instance of the specified Class, then looks for a
75       * configure(Map<String,Object>) method.
76       *
77       * @param clazz the java.lang.Class of the tool
78       */
79      public void setClass(Class clazz)
80      {
81          if (clazz == null)
82          {
83              throw new NullPointerException("Tool class must not be null");
84          }
85          this.clazz = clazz;
86  
87          //NOTE: we used to check here that we could get an instance of
88          //      the tool class, but that's been moved to ToolConfiguration
89          //      in order to fail as earlier as possible.  most people won't
90          //      manually create ToolInfo.  if they do and we can't get an
91          //      instance, they should be capable of figuring out what happened
92      }
93  
94      /**
95       * @param path the full or partial request path restriction of the tool
96       */
97      public void restrictTo(String path)
98      {
99          if (path != null && !path.startsWith("/"))
100         {
101             path = "/" + path;
102         }
103 
104         if (path == null || path.equals("*"))
105         {
106             // match all paths
107             restrictToIsExact = false;
108             this.restrictTo = null;
109         }
110         else if(path.endsWith("*"))
111         {
112             // match some paths
113             restrictToIsExact = false;
114             this.restrictTo = path.substring(0, path.length() - 1);
115         }
116         else
117         {
118             // match one path
119             restrictToIsExact = true;
120             this.restrictTo = path;
121         }
122     }
123 
124     public void setSkipSetters(boolean cfgOnly)
125     {
126         this.skipSetters = cfgOnly;
127     }
128 
129     /**
130      * Adds a map of properties from a parent scope to the properties
131      * for this tool.  Only new properties will be added; any that
132      * are already set for this tool will be ignored.
133      */
134     public void addProperties(Map<String,Object> parentProps)
135     {
136         // only add those new properties for which we
137         // do not already have a value. first prop set wins.
138         Map<String,Object> properties = getProps();
139         for (Map.Entry<String,Object> prop : parentProps.entrySet())
140         {
141             if (!properties.containsKey(prop.getKey()))
142             {
143                 properties.put(prop.getKey(), prop.getValue());
144             }
145         }
146     }
147 
148     /**
149      * Puts a new property for this tool.
150      */
151     public Object putProperty(String name, Object value)
152     {
153         return getProps().put(name, value);
154     }
155 
156     protected synchronized Map<String,Object> getProps()
157     {
158         if (properties == null)
159         {
160             properties = new HashMap<String,Object>();
161         }
162         return properties;
163     }
164 
165 
166     /***********************  Accessors *************************/
167 
168     public String getKey()
169     {
170         return key;
171     }
172 
173     public String getClassname()
174     {
175         return clazz.getName();
176     }
177 
178     public Class getToolClass()
179     {
180         return clazz;
181     }
182 
183     public Map<String,Object> getProperties()
184     {
185         return getProps();
186     }
187 
188     public boolean hasConfigure()
189     {
190         return (getConfigure() != null);
191     }
192 
193     public boolean isSkipSetters()
194     {
195         if (skipSetters == null)
196         {
197             skipSetters = (clazz.getAnnotation(SkipSetters.class) != null);
198         }
199         return skipSetters;
200     }
201 
202     /**
203      * @param path the path of a template requesting this tool
204      * @return <code>true</code> if the specified
205      *         request path matches the restrictions of this tool.
206      *         If there is no request path restriction for this tool,
207      *         it will always return <code>true</code>.
208      */
209     public boolean hasPermission(String path)
210     {
211         if (this.restrictTo == null)
212         {
213             return true;
214         }
215         else if (restrictToIsExact)
216         {
217             return this.restrictTo.equals(path);
218         }
219         else if (path != null)
220         {
221             return path.startsWith(this.restrictTo);
222         }
223         return false;
224     }
225 
226 
227     /***********************  create() *************************/
228 
229     /**
230      * Returns a new instance of the tool. If the tool
231      * has an configure(Map) method, the new instance
232      * will be initialized using the given properties combined with
233      * whatever "constant" properties have been put into this
234      * ToolInfo.
235      */
236     public Object create(Map<String,Object> dynamicProperties)
237     {
238         /* Get the tool instance */
239         Object tool = newInstance();
240 
241         /* put configured props into the combo last, since
242            dynamic properties will almost always be conventions
243            and we need to let configuration win out */
244         Map<String,Object> props;
245         if (properties == null)
246         {
247             props = dynamicProperties;
248         }
249         else
250         {
251             props = combine(dynamicProperties, properties);
252         }
253 
254         // perform the actual configuration of the new tool
255         configure(tool, props);
256         return tool;
257     }
258 
259 
260     /***********************  protected methods *************************/
261 
262     /**
263      * Actually performs configuration of the newly instantiated tool
264      * using the combined final set of configuration properties. First,
265      * if the class lacks the {@link SkipSetters} annotation, then any
266      * specific setters matching the configuration keys are called, then
267      * the general configure(Map) method (if any) is called.
268      */
269     protected void configure(Object tool, Map<String,Object> configuration)
270     {
271         if (!isSkipSetters() && configuration != null)
272         {
273             try
274             {
275                 // look for specific setters
276                 for (Map.Entry<String,Object> conf : configuration.entrySet())
277                 {
278                     setProperty(tool, conf.getKey(), conf.getValue());
279                 }
280             }
281             catch (RuntimeException re)
282             {
283                 throw re;
284             }
285             catch (Exception e)
286             {
287                 // convert to a runtime exception, and re-throw
288                 throw new RuntimeException(e);
289             }
290         }
291 
292         if (hasConfigure())
293         {
294             invoke(getConfigure(), tool, configuration);
295         }
296     }
297 
298     protected Method getConfigure()
299     {
300         if (this.configure == null)
301         {
302             // search for a configure(Map params) method in the class
303             try
304             {
305                 this.configure = ClassUtils.findMethod(clazz, CONFIGURE_METHOD_NAME,
306                                               new Class[]{ Map.class });
307             }
308             catch (SecurityException se)
309             {
310                 // fail early, rather than wait until
311                 String msg = "Unable to gain access to '" +
312                              CONFIGURE_METHOD_NAME + "(Map)'" +
313                              " method for '" + clazz.getName() +
314                              "' under the current security manager."+
315                              "  This tool cannot be properly configured for use.";
316                 throw new IllegalStateException(msg, se);
317             }
318         }
319         return this.configure;
320     }
321 
322     /* TODO? if we have performance issues with copyProperties,
323              look at possibly finding and caching these common setters
324                 setContext(VelocityContext)
325                 setVelocityEngine(VelocityEngine)
326                 setLog(Log)
327                 setLocale(Locale)
328              these four are tricky since we may not want servlet deps here
329                 setRequest(ServletRequest)
330                 setSession(HttpSession)
331                 setResponse(ServletResponse)
332                 setServletContext(ServletContext)    */
333 
334     protected Object newInstance()
335     {
336         try
337         {
338             return clazz.newInstance();
339         }
340         /* we shouldn't get either of these exceptions here because
341          * we already got an instance of this class during setClass().
342          * but to be safe, let's catch them and re-throw as RuntimeExceptions */
343         catch (IllegalAccessException iae)
344         {
345             String message = "Unable to instantiate instance of \"" +
346                   getClassname() + "\"";
347             throw new IllegalStateException(message, iae);
348         }
349         catch (InstantiationException ie)
350         {
351             String message = "Exception while instantiating instance of \"" +
352                   getClassname() + "\"";
353             throw new IllegalStateException(message, ie);
354         }
355     }
356 
357 
358     protected void invoke(Method method, Object tool, Object param)
359     {
360         try
361         {
362             // call the setup method on the instance
363             method.invoke(tool, new Object[]{ param });
364         }
365         catch (IllegalAccessException iae)
366         {
367             String msg = "Unable to invoke " + method + " on " + tool;
368             // restricting access to this method by this class ist verboten
369             throw new IllegalStateException(msg, iae);
370         }
371         catch (InvocationTargetException ite)
372         {
373             String msg = "Exception when invoking " + method + " on " + tool;
374             // convert to a runtime exception, and re-throw
375             throw new RuntimeException(msg, ite.getCause());
376         }
377     }
378 
379 
380     protected void setProperty(Object tool, String name, Object value) throws Exception
381     {
382         if (PropertyUtils.isWriteable(tool, name))
383         {
384             //TODO? support property conversion here?
385             //      heavy-handed way is BeanUtils.copyProperty(...)
386             PropertyUtils.setProperty(tool, name, value);
387         }
388     }
389 
390     //TODO? move to Utils?
391     protected Map<String,Object> combine(Map<String,Object>... maps)
392     {
393         Map<String,Object> combined = new HashMap<String,Object>();
394         for (Map<String,Object> map : maps)
395         {
396             if (map != null)
397             {
398                 combined.putAll(map);
399             }
400         }
401         return combined;
402     }
403 
404 }