KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > freemarker > ext > beans > BeansWrapper


1 /*
2  * Copyright (c) 2003 The Visigoth Software Society. All rights
3  * reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  * notice, this list of conditions and the following disclaimer in
14  * the documentation and/or other materials provided with the
15  * distribution.
16  *
17  * 3. The end-user documentation included with the redistribution, if
18  * any, must include the following acknowledgement:
19  * "This product includes software developed by the
20  * Visigoth Software Society (http://www.visigoths.org/)."
21  * Alternately, this acknowledgement may appear in the software itself,
22  * if and wherever such third-party acknowledgements normally appear.
23  *
24  * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
25  * project contributors may be used to endorse or promote products derived
26  * from this software without prior written permission. For written
27  * permission, please contact visigoths@visigoths.org.
28  *
29  * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
30  * nor may "FreeMarker" or "Visigoth" appear in their names
31  * without prior written permission of the Visigoth Software Society.
32  *
33  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
34  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36  * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
37  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
40  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
41  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
42  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
43  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
44  * SUCH DAMAGE.
45  * ====================================================================
46  *
47  * This software consists of voluntary contributions made by many
48  * individuals on behalf of the Visigoth Software Society. For more
49  * information on the Visigoth Software Society, please see
50  * http://www.visigoths.org/
51  */

52
53 package freemarker.ext.beans;
54
55 import java.beans.BeanInfo JavaDoc;
56 import java.beans.IndexedPropertyDescriptor JavaDoc;
57 import java.beans.IntrospectionException JavaDoc;
58 import java.beans.Introspector JavaDoc;
59 import java.beans.MethodDescriptor JavaDoc;
60 import java.beans.PropertyDescriptor JavaDoc;
61 import java.io.InputStream JavaDoc;
62 import java.lang.reflect.AccessibleObject JavaDoc;
63 import java.lang.reflect.Constructor JavaDoc;
64 import java.lang.reflect.InvocationTargetException JavaDoc;
65 import java.lang.reflect.Method JavaDoc;
66 import java.lang.reflect.Modifier JavaDoc;
67 import java.math.BigDecimal JavaDoc;
68 import java.util.Arrays JavaDoc;
69 import java.util.Collection JavaDoc;
70 import java.util.Collections JavaDoc;
71 import java.util.Date JavaDoc;
72 import java.util.Enumeration JavaDoc;
73 import java.util.HashMap JavaDoc;
74 import java.util.HashSet JavaDoc;
75 import java.util.Iterator JavaDoc;
76 import java.util.List JavaDoc;
77 import java.util.Map JavaDoc;
78 import java.util.Properties JavaDoc;
79 import java.util.ResourceBundle JavaDoc;
80 import java.util.Set JavaDoc;
81 import java.util.StringTokenizer JavaDoc;
82
83 import freemarker.ext.util.ModelCache;
84 import freemarker.ext.util.ModelFactory;
85 import freemarker.ext.util.WrapperTemplateModel;
86 import freemarker.log.Logger;
87 import freemarker.template.AdapterTemplateModel;
88 import freemarker.template.ObjectWrapper;
89 import freemarker.template.TemplateBooleanModel;
90 import freemarker.template.TemplateCollectionModel;
91 import freemarker.template.TemplateDateModel;
92 import freemarker.template.TemplateHashModel;
93 import freemarker.template.TemplateModel;
94 import freemarker.template.TemplateModelAdapter;
95 import freemarker.template.TemplateModelException;
96 import freemarker.template.TemplateNumberModel;
97 import freemarker.template.TemplateScalarModel;
98 import freemarker.template.TemplateSequenceModel;
99 import freemarker.template.utility.ClassUtil;
100 import freemarker.template.utility.Collections12;
101 import freemarker.template.utility.SecurityUtilities;
102
103 /**
104  * Utility class that provides generic services to reflection classes.
105  * It handles all polymorphism issues in the {@link #wrap(Object)} and {@link #unwrap(TemplateModel)} methods.
106  * @author Attila Szegedi
107  * @version $Id: BeansWrapper.java,v 1.91 2005/06/13 21:34:47 szegedia Exp $
108  */

109 public class BeansWrapper implements ObjectWrapper
110 {
111     private static final Class JavaDoc BIGINTEGER_CLASS = java.math.BigInteger JavaDoc.class;
112     private static final Class JavaDoc BOOLEAN_CLASS = Boolean JavaDoc.class;
113     private static final Class JavaDoc CHARACTER_CLASS = Character JavaDoc.class;
114     private static final Class JavaDoc COLLECTION_CLASS = Collection JavaDoc.class;
115     private static final Class JavaDoc DATE_CLASS = Date JavaDoc.class;
116     private static final Class JavaDoc LIST_CLASS = List JavaDoc.class;
117     private static final Class JavaDoc MAP_CLASS = Map JavaDoc.class;
118     private static final Class JavaDoc NUMBER_CLASS = Number JavaDoc.class;
119     private static final Class JavaDoc OBJECT_CLASS = Object JavaDoc.class;
120     private static final Class JavaDoc SET_CLASS = Set JavaDoc.class;
121     private static final Class JavaDoc STRING_CLASS = String JavaDoc.class;
122     private static final Class JavaDoc TEMPLATE_MODEL_CLASS = TemplateModel.class;
123     
124     // When this property is true, some things are stricter. This is mostly to
125
// catch anomalous things in development that can otherwise be valid situations
126
// for our users.
127
private static final boolean DEVELOPMENT = "true".equals(SecurityUtilities.getSystemProperty("freemarker.development"));
128     
129     private static final Logger logger = Logger.getLogger("freemarker.beans");
130     
131     private static final Set JavaDoc UNSAFE_METHODS = createUnsafeMethodsSet();
132     
133     static final Object JavaDoc GENERIC_GET_KEY = new Object JavaDoc();
134     private static final Object JavaDoc CONSTRUCTORS = new Object JavaDoc();
135     private static final Object JavaDoc ARGTYPES = new Object JavaDoc();
136     
137     /**
138      * The default instance of BeansWrapper
139      */

140     private static final BeansWrapper INSTANCE = new BeansWrapper();
141
142     // Cache of hash maps that contain already discovered properties and methods
143
// for a specified class. Each key is a Class, each value is a hash map. In
144
// that hash map, each key is a property/method name, each value is a
145
// MethodDescriptor or a PropertyDescriptor assigned to that property/method.
146
private final Map JavaDoc classCache = new HashMap JavaDoc();
147     private Set JavaDoc cachedClassNames = new HashSet JavaDoc();
148
149     private final StaticModels staticModels = new StaticModels(this);
150
151     private final ModelCache modelCache = new ModelCache(this);
152     
153     private final BooleanModel FALSE = new BooleanModel(Boolean.FALSE, this);
154     private final BooleanModel TRUE = new BooleanModel(Boolean.TRUE, this);
155
156     /**
157      * At this level of exposure, all methods and properties of the
158      * wrapped objects are exposed to the template.
159      */

160     public static final int EXPOSE_ALL = 0;
161     
162     /**
163      * At this level of exposure, all methods and properties of the wrapped
164      * objects are exposed to the template except methods that are deemed
165      * not safe. The not safe methods are java.lang.Object methods wait() and
166      * notify(), java.lang.Class methods getClassLoader() and newInstance(),
167      * java.lang.reflect.Method and java.lang.reflect.Constructor invoke() and
168      * newInstance() methods, all java.lang.reflect.Field set methods, all
169      * java.lang.Thread and java.lang.ThreadGroup methods that can change its
170      * state, as well as the usual suspects in java.lang.System and
171      * java.lang.Runtime.
172      */

173     public static final int EXPOSE_SAFE = 1;
174     
175     /**
176      * At this level of exposure, only property getters are exposed.
177      * Additionally, property getters that map to unsafe methods are not
178      * exposed (i.e. Class.classLoader and Thread.contextClassLoader).
179      */

180     public static final int EXPOSE_PROPERTIES_ONLY = 2;
181
182     /**
183      * At this level of exposure, no bean properties and methods are exposed.
184      * Only map items, resource bundle items, and objects retrieved through
185      * the generic get method (on objects of classes that have a generic get
186      * method) can be retrieved through the hash interface. You might want to
187      * call {@link #setMethodsShadowItems(boolean)} with <tt>false</tt> value to
188      * speed up map item retrieval.
189      */

190     public static final int EXPOSE_NOTHING = 3;
191
192     private int exposureLevel = EXPOSE_SAFE;
193     private TemplateModel nullModel = null;
194     private boolean methodsShadowItems = true;
195     private int defaultDateType = TemplateDateModel.UNKNOWN;
196
197     private ObjectWrapper outerIdentity = this;
198     private boolean simpleMapWrapper;
199     
200     /**
201      * Creates a new instance of BeansWrapper. The newly created instance
202      * will use the null reference as its null object, it will use
203      * {@link #EXPOSE_SAFE} method exposure level, and will not cache
204      * model instances.
205      */

206     public BeansWrapper()
207     {
208     }
209
210     /**
211      * When wrapping an object, the BeansWrapper commonly needs to wrap
212      * "sub-objects", for example each element in a wrapped collection.
213      * Normally it wraps these objects using itself. However, this makes
214      * it difficult to delegate to a BeansWrapper as part of a custom
215      * aggregate ObjectWrapper. This method lets you set the ObjectWrapper
216      * which will be used to wrap the sub-objects.
217      * @param outerIdentity the aggregate ObjectWrapper
218      */

219     public void setOuterIdentity(ObjectWrapper outerIdentity)
220     {
221         this.outerIdentity = outerIdentity;
222     }
223
224     /**
225      * By default returns <tt>this</tt>.
226      * @see #setOuterIdentity(ObjectWrapper)
227      */

228     public ObjectWrapper getOuterIdentity()
229     {
230         return outerIdentity;
231     }
232
233     /**
234      * By default the BeansWrapper wraps classes implementing
235      * java.util.Map using {@link MapModel}. Setting this flag will
236      * cause it to use a {@link SimpleMapModel} instead. The biggest
237      * difference is that when using a {@link SimpleMapModel}, the
238      * map will be visible as <code>TemplateHashModelEx</code>,
239      * and the subvariables will be the content of the map,
240      * without the other methods and properties of the map object.
241      * @param simpleMapWrapper enable simple map wrapping
242      */

243     public void setSimpleMapWrapper(boolean simpleMapWrapper)
244     {
245         this.simpleMapWrapper = simpleMapWrapper;
246     }
247
248     public boolean isSimpleMapWrapper()
249     {
250         return simpleMapWrapper;
251     }
252
253     /**
254      * Sets the method exposure level. By default, set to <code>EXPOSE_SAFE</code>.
255      * @param exposureLevel can be any of the <code>EXPOSE_xxx</code>
256      * constants.
257      */

258     public void setExposureLevel(int exposureLevel)
259     {
260         if(exposureLevel < EXPOSE_ALL || exposureLevel > EXPOSE_NOTHING)
261         {
262             throw new IllegalArgumentException JavaDoc("Illegal exposure level " + exposureLevel);
263         }
264         this.exposureLevel = exposureLevel;
265     }
266     
267     int getExposureLevel()
268     {
269         return exposureLevel;
270     }
271     
272     /**
273      * Sets whether methods shadow items in beans. When true (this is the
274      * default value), <code>${object.name}</code> will first try to locate
275      * a bean method or property with the specified name on the object, and
276      * only if it doesn't find it will it try to call
277      * <code>object.get(name)</code>, the so-called "generic get method" that
278      * is usually used to access items of a container (i.e. elements of a map).
279      * When set to false, the lookup order is reversed and generic get method
280      * is called first, and only if it returns null is method lookup attempted.
281      */

282     public synchronized void setMethodsShadowItems(boolean methodsShadowItems)
283     {
284         this.methodsShadowItems = methodsShadowItems;
285     }
286     
287     boolean isMethodsShadowItems()
288     {
289         return methodsShadowItems;
290     }
291     
292     /**
293      * Sets the default date type to use for date models that result from
294      * a plain <tt>java.util.Date</tt> instead of <tt>java.sql.Date</tt> or
295      * <tt>java.sql.Time</tt> or <tt>java.sql.Timestamp</tt>. Default value is
296      * {@link TemplateDateModel#UNKNOWN}.
297      * @param defaultDateType the new default date type.
298      */

299     public synchronized void setDefaultDateType(int defaultDateType) {
300         this.defaultDateType = defaultDateType;
301     }
302     
303     protected int getDefaultDateType() {
304         return defaultDateType;
305     }
306     
307     /**
308      * Sets whether this wrapper caches model instances. Default is false.
309      * When set to true, calling {@link #wrap(Object)} multiple times for
310      * the same object will likely return the same model (although there is
311      * no guarantee as the cache items can be cleared anytime).
312      */

313     public void setUseCache(boolean useCache)
314     {
315         modelCache.setUseCache(useCache);
316     }
317     
318     /**
319      * Sets the null model. This model is returned from the
320      * {@link #wrap(Object)} method whenever the underlying object
321      * reference is null. It defaults to null reference, which is dealt
322      * with quite strictly on engine level, however you can substitute an
323      * arbitrary (perhaps more lenient) model, such as
324      * {@link freemarker.template.TemplateScalarModel#EMPTY_STRING}.
325      */

326     public void setNullModel(TemplateModel nullModel)
327     {
328         this.nullModel = nullModel;
329     }
330     
331     /**
332      * Returns the default instance of the wrapper. This instance is used
333      * when you construct various bean models without explicitly specifying
334      * a wrapper. It is also returned by
335      * {@link freemarker.template.ObjectWrapper#BEANS_WRAPPER}
336      * and this is the sole instance that is used by the JSP adapter.
337      * You can modify the properties of the default instance (caching,
338      * exposure level, null model) to affect its operation. By default, the
339      * default instance is not caching, uses the <code>EXPOSE_SAFE</code>
340      * exposure level, and uses null reference as the null model.
341      */

342     public static final BeansWrapper getDefaultInstance()
343     {
344         return INSTANCE;
345     }
346
347     /**
348      * Wraps the object with a template model that is most specific for the object's
349      * class. Specifically:
350      * <ul>
351      * <li>if the object is null, returns the {@link #setNullModel(TemplateModel) null model},</li>
352      * <li>if the object is a Number returns a {@link NumberModel} for it,</li>
353      * <li>if the object is a Date returns a {@link DateModel} for it,</li>
354      * <li>if the object is a Boolean returns
355      * {@link freemarker.template.TemplateBooleanModel#TRUE} or
356      * {@link freemarker.template.TemplateBooleanModel#FALSE}</li>
357      * <li>if the object is already a TemplateModel, returns it unchanged,</li>
358      * <li>if the object is an array, returns a {@link ArrayModel} for it
359      * <li>if the object is a Map, returns a {@link MapModel} for it
360      * <li>if the object is a Collection, returns a {@link CollectionModel} for it
361      * <li>if the object is an Iterator, returns a {@link IteratorModel} for it
362      * <li>if the object is an Enumeration, returns a {@link EnumerationModel} for it
363      * <li>if the object is a String, returns a {@link StringModel} for it
364      * <li>otherwise, returns a generic {@link BeanModel} for it.
365      * </ul>
366      */

367     public TemplateModel wrap(Object JavaDoc object) throws TemplateModelException
368     {
369         if(object == null)
370             return nullModel;
371         if(object instanceof TemplateModel)
372             return (TemplateModel)object;
373         if(object instanceof TemplateModelAdapter)
374             return ((TemplateModelAdapter)object).getTemplateModel();
375         if(object instanceof Map JavaDoc)
376             return modelCache.getInstance(object, simpleMapWrapper ? SimpleMapModel.FACTORY : MapModel.FACTORY);
377         if(object instanceof Collection JavaDoc)
378             return modelCache.getInstance(object, CollectionModel.FACTORY);
379         if(object.getClass().isArray())
380             return modelCache.getInstance(object, ArrayModel.FACTORY);
381         if(object instanceof Number JavaDoc)
382             return modelCache.getInstance(object, NumberModel.FACTORY);
383         if(object instanceof Date JavaDoc)
384             return modelCache.getInstance(object, DateModel.FACTORY);
385         if(object instanceof Boolean JavaDoc)
386             return ((Boolean JavaDoc)object).booleanValue() ? TRUE : FALSE;
387         if(object instanceof ResourceBundle JavaDoc)
388             return modelCache.getInstance(object, ResourceBundleModel.FACTORY);
389         if(object instanceof Iterator JavaDoc)
390             return new IteratorModel((Iterator JavaDoc)object, this);
391         if(object instanceof Enumeration JavaDoc)
392             return new EnumerationModel((Enumeration JavaDoc)object, this);
393         return modelCache.getInstance(object, StringModel.FACTORY);
394     }
395
396     protected TemplateModel getInstance(Object JavaDoc object, ModelFactory factory)
397     {
398         return modelCache.getInstance(object, factory);
399     }
400     
401     protected TemplateModel create(Object JavaDoc object, Object JavaDoc factory)
402     {
403         return ((ModelFactory)factory).create(object, this);
404     }
405
406     /**
407      * Attempts to unwrap a model into underlying object. Generally, this
408      * method is the inverse of the {@link #wrap(Object)} method. In addition
409      * it will unwrap arbitrary {@link TemplateNumberModel} instances into
410      * a number, arbitrary {@link TemplateDateModel} instances into a date,
411      * {@link TemplateScalarModel} instances into a String, and
412      * {@link TemplateBooleanModel} instances into a Boolean.
413      * All other objects are returned unchanged.
414      */

415     public Object JavaDoc unwrap(TemplateModel model) throws TemplateModelException
416     {
417         return unwrap(model, OBJECT_CLASS);
418     }
419     
420     public Object JavaDoc unwrap(TemplateModel model, Class JavaDoc hint)
421     throws TemplateModelException
422     {
423         if(model == nullModel) {
424             return null;
425         }
426         
427         // This is to be able to pass TemplateModel objects to methods that
428
// explicitly declare them in their argument types
429
if(TEMPLATE_MODEL_CLASS.isAssignableFrom(hint)) {
430             return model;
431         }
432         
433         // This is for transparent interop with other wrappers (and ourselves)
434
// Passing the hint allows i.e. a Jython-aware method that declares a
435
// PyObject as its argument to receive a PyObject from a JythonModel
436
// passed as an argument to TemplateMethodModelEx etc.
437
if(model instanceof AdapterTemplateModel) {
438             return ((AdapterTemplateModel)model).getAdaptedObject(hint);
439         }
440         if(model instanceof WrapperTemplateModel) {
441             return ((WrapperTemplateModel)model).getWrappedObject();
442         }
443         
444         // Translation of generic template models to POJOs. First give priority
445
// to various model interfaces based on the hint class. This helps us
446
// select the appropriate interface in multi-interface models when we
447
// know what is expected as the return type.
448

449         if(STRING_CLASS == hint) {
450             if(model instanceof TemplateScalarModel) {
451                 return ((TemplateScalarModel)model).getAsString();
452             }
453         }
454         boolean isBoolean = Boolean.TYPE == hint;
455         if(isBoolean || BOOLEAN_CLASS == hint) {
456             if(model instanceof TemplateBooleanModel) {
457                 return ((TemplateBooleanModel)model).getAsBoolean() ? Boolean.TRUE : Boolean.FALSE;
458             }
459         }
460         if(MAP_CLASS == hint) {
461             if(model instanceof TemplateHashModel) {
462                 return new HashAdapter((TemplateHashModel)model, this);
463             }
464         }
465         if(LIST_CLASS == hint) {
466             if(model instanceof TemplateSequenceModel) {
467                 return new SequenceAdapter((TemplateSequenceModel)model, this);
468             }
469         }
470         if(SET_CLASS == hint || COLLECTION_CLASS == hint) {
471             if(model instanceof TemplateCollectionModel) {
472                 return new CollectionAdapter((TemplateCollectionModel)model, this);
473             }
474         }
475         
476         // Allow one-char strings to be coerced to characters
477
boolean isChar = hint == Character.TYPE;
478         if(isChar || hint == CHARACTER_CLASS) {
479             if(model instanceof TemplateScalarModel) {
480                 String JavaDoc s = ((TemplateScalarModel)model).getAsString();
481                 if(s.length() == 1) {
482                     return new Character JavaDoc(s.charAt(0));
483                 }
484             }
485         }
486
487         // Primitive numeric types & Number.class
488
if((hint.isPrimitive() && !isChar && !isBoolean)
489                 || NUMBER_CLASS.isAssignableFrom(hint)) {
490             if(model instanceof TemplateNumberModel) {
491                 return ((TemplateNumberModel)model).getAsNumber();
492             }
493         }
494         if(DATE_CLASS.isAssignableFrom(hint)) {
495             if(model instanceof TemplateDateModel) {
496                 return ((TemplateDateModel)model).getAsDate();
497             }
498         }
499         
500         // Translation of generic template models to POJOs. Since hint was of
501
// no help, now use an admittedly arbitrary order of interfaces.
502
if(model instanceof TemplateNumberModel)
503             return ((TemplateNumberModel)model).getAsNumber();
504         if(model instanceof TemplateDateModel)
505             return ((TemplateDateModel)model).getAsDate();
506         if(model instanceof TemplateScalarModel)
507             return ((TemplateScalarModel)model).getAsString();
508         if(model instanceof TemplateBooleanModel)
509             return ((TemplateBooleanModel)model).getAsBoolean() ? Boolean.TRUE : Boolean.FALSE;
510         if(model instanceof TemplateHashModel)
511             return new HashAdapter((TemplateHashModel)model, this);
512         if(model instanceof TemplateSequenceModel)
513             return new SequenceAdapter((TemplateSequenceModel)model, this);
514         if(model instanceof TemplateCollectionModel)
515             return new CollectionAdapter((TemplateCollectionModel)model, this);
516         
517         // If everything else fails, return the model as-is
518
// (TemplateTransformModel and TemplateMethodModel[Ex] will fall into
519
// this category.
520
return model;
521     }
522     
523     /**
524      * Auxiliary method that unwraps arguments for a method or constructor call.
525      * @param arguments the argument list of template models
526      * @param argTypes the preferred types of the arguments
527      * @return Object[] the unwrapped arguments. null if the passed list was
528      * null.
529      * @throws TemplateModelException if unwrapping any argument throws one
530      */

531     Object JavaDoc[] unwrapArguments(List JavaDoc arguments, Class JavaDoc[] argTypes) throws
532     TemplateModelException
533     {
534         Object JavaDoc[] args = null;
535         if(arguments != null) {
536             int size = arguments.size();
537             args = new Object JavaDoc[size];
538             Iterator JavaDoc it = arguments.iterator();
539             for(int i = 0; it.hasNext(); ++i) {
540                 args[i] = unwrap((TemplateModel)it.next(), argTypes[i]);
541             }
542         }
543         return args;
544     }
545     
546     Object JavaDoc[] unwrapArguments(List JavaDoc arguments) throws TemplateModelException
547     {
548         Object JavaDoc[] args = null;
549         if(arguments != null) {
550             int size = arguments.size();
551             args = new Object JavaDoc[size];
552             Iterator JavaDoc it = arguments.iterator();
553             int i = 0;
554             while(it.hasNext()) {
555                 args[i++] = unwrap((TemplateModel)it.next());
556             }
557         }
558         return args;
559     }
560
561     /**
562      * Invokes the specified method, wrapping the return value. The specialty
563      * of this method is that if the return value is null, and the return type
564      * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
565      * @param object the object to invoke the method on
566      * @param method the method to invoke
567      * @param args the arguments to the method
568      * @return the wrapped return value of the method.
569      * @throws InvocationTargetException if the invoked method threw an exception
570      * @throws IllegalAccessException if the method can't be invoked due to an
571      * access restriction.
572      * @throws TemplateModelException if the return value couldn't be wrapped
573      * (this can happen if the wrapper has an outer identity or is subclassed,
574      * and the outer identity or the subclass throws an exception. Plain
575      * BeansWrapper never throws TemplateModelException).
576      */

577     TemplateModel invokeMethod(Object JavaDoc object, Method JavaDoc method, Object JavaDoc[] args)
578     throws
579         InvocationTargetException JavaDoc,
580         IllegalAccessException JavaDoc,
581         TemplateModelException
582     {
583         Object JavaDoc retval = method.invoke(object, args);
584         return
585             method.getReturnType() == Void.TYPE
586             ? TemplateModel.NOTHING
587             : getOuterIdentity().wrap(retval);
588     }
589
590    /**
591      * Returns a hash model that represents the so-called class static models.
592      * Every class static model is itself a hash through which you can call
593      * static methods on the specified class. To obtain a static model for a
594      * class, get the element of this hash with the fully qualified class name.
595      * For example, if you place this hash model inside the root data model
596      * under name "statics", you can use i.e. <code>statics["java.lang.
597      * System"]. currentTimeMillis()</code> to call the {@link
598      * java.lang.System#currentTimeMillis()} method.
599      * @return a hash model whose keys are fully qualified class names, and
600      * that returns hash models whose elements are the static models of the
601      * classes.
602      */

603     public TemplateHashModel getStaticModels()
604     {
605         return staticModels;
606     }
607     
608     public Object JavaDoc newInstance(Class JavaDoc clazz, List JavaDoc arguments)
609     throws
610         TemplateModelException
611     {
612         try
613         {
614             introspectClass(clazz);
615             Map JavaDoc classInfo = (Map JavaDoc)classCache.get(clazz);
616             Object JavaDoc ctors = classInfo.get(CONSTRUCTORS);
617             if(ctors == null)
618             {
619                 throw new TemplateModelException("Class " + clazz.getName() +
620                         " has no public constructors.");
621             }
622             Constructor JavaDoc ctor = null;
623             Object JavaDoc[] objargs;
624             if(ctors instanceof Constructor JavaDoc)
625             {
626                 ctor = (Constructor JavaDoc)ctors;
627                 objargs = unwrapArguments(arguments, getArgTypes(classInfo, ctor));
628             }
629             else if(ctors instanceof MethodMap)
630             {
631                 MethodMap methodMap = (MethodMap)ctors;
632                 objargs = unwrapArguments(arguments, methodMap.getUnwrapTypes(arguments));
633                 ctor = (Constructor JavaDoc)methodMap.getMostSpecific(objargs);
634             }
635             else
636             {
637                 // Cannot happen
638
throw new Error JavaDoc();
639             }
640             if(objargs != null) {
641                 coerceBigDecimals(ctor, objargs);
642             }
643             return ctor.newInstance(objargs);
644         }
645         catch (TemplateModelException e)
646         {
647             throw e;
648         }
649         catch (Exception JavaDoc e)
650         {
651             throw new TemplateModelException(
652                     "Could not create instance of class " + clazz.getName(), e);
653         }
654     }
655     
656     void introspectClass(Class JavaDoc clazz)
657     {
658         synchronized(classCache)
659         {
660             if(!classCache.containsKey(clazz))
661             {
662                 String JavaDoc className = clazz.getName();
663                 if(cachedClassNames.contains(className))
664                 {
665                     if(logger.isInfoEnabled())
666                     {
667                         logger.info("Detected a reloaded class [" + className +
668                                 "]. Clearing BeansWrapper caches.");
669                     }
670                     // Class reload detected, throw away caches
671
classCache.clear();
672                     cachedClassNames = new HashSet JavaDoc();
673                     synchronized(this)
674                     {
675                         modelCache.clearCache();
676                     }
677                     staticModels.clearCache();
678                 }
679                 classCache.put(clazz, populateClassMap(clazz));
680                 cachedClassNames.add(className);
681             }
682         }
683     }
684
685     Map JavaDoc getClassKeyMap(Class JavaDoc clazz)
686     {
687         synchronized(classCache)
688         {
689             return (Map JavaDoc)classCache.get(clazz);
690         }
691     }
692
693     /**
694      * Returns the number of introspected methods/properties that should
695      * be available via the TemplateHashModel interface. Affected by the
696      * {@link #setMethodsShadowItems(boolean)} and {@link
697      * #setExposureLevel(int)} settings.
698      */

699     int keyCount(Class JavaDoc clazz)
700     {
701         Map JavaDoc map = getClassKeyMap(clazz);
702         int count = map.size();
703         if (map.containsKey(CONSTRUCTORS))
704             count--;
705         if (map.containsKey(GENERIC_GET_KEY))
706             count--;
707         if (map.containsKey(ARGTYPES))
708             count--;
709         return count;
710     }
711
712     /**
713      * Returns the Set of names of introspected methods/properties that
714      * should be available via the TemplateHashModel interface. Affected
715      * by the {@link #setMethodsShadowItems(boolean)} and {@link
716      * #setExposureLevel(int)} settings.
717      */

718     Set JavaDoc keySet(Class JavaDoc clazz)
719     {
720         Set JavaDoc set = new HashSet JavaDoc(getClassKeyMap(clazz).keySet());
721         set.remove(CONSTRUCTORS);
722         set.remove(GENERIC_GET_KEY);
723         set.remove(ARGTYPES);
724         return set;
725     }
726     
727     /**
728      * Populates a map with property and method descriptors for a specified
729      * class. If any property or method descriptors specifies a read method
730      * that is not accessible, replaces it with appropriate accessible method
731      * from a superclass or interface.
732      */

733     private Map JavaDoc populateClassMap(Class JavaDoc clazz)
734     {
735         // Populate first from bean info
736
Map JavaDoc map = populateClassMapWithBeanInfo(clazz);
737         // Next add constructors
738
try
739         {
740             Constructor JavaDoc[] ctors = clazz.getConstructors();
741             if(ctors.length == 1)
742             {
743                 Constructor JavaDoc ctor = ctors[0];
744                 map.put(CONSTRUCTORS, ctor);
745                 getArgTypes(map).put(ctor, ctor.getParameterTypes());
746             }
747             else if(ctors.length > 1)
748             {
749                 MethodMap ctorMap = new MethodMap("<init>");
750                 for (int i = 0; i < ctors.length; i++)
751                 {
752                     ctorMap.addConstructor(ctors[i]);
753                 }
754                 map.put(CONSTRUCTORS, ctorMap);
755             }
756         }
757         catch(SecurityException JavaDoc e)
758         {
759             logger.warn("Canont discover constructors for class " +
760                     clazz.getName(), e);
761         }
762         switch(map.size())
763         {
764             case 0:
765             {
766                 map = Collections12.EMPTY_MAP;
767                 break;
768             }
769             case 1:
770             {
771                 Map.Entry JavaDoc e = (Map.Entry JavaDoc)map.entrySet().iterator().next();
772                 map = Collections12.singletonMap(e.getKey(), e.getValue());
773                 break;
774             }
775         }
776         return map;
777     }
778
779     private Map JavaDoc populateClassMapWithBeanInfo(Class JavaDoc clazz)
780     {
781         Map JavaDoc classMap = new HashMap JavaDoc();
782         Map JavaDoc accessibleMethods = discoverAccessibleMethods(clazz);
783         Method JavaDoc genericGet = (Method JavaDoc)accessibleMethods.get(MethodSignature.GET_STRING_SIGNATURE);
784         if(genericGet == null)
785         {
786             genericGet = (Method JavaDoc)accessibleMethods.get(MethodSignature.GET_OBJECT_SIGNATURE);
787         }
788         if(genericGet != null)
789         {
790             classMap.put(GENERIC_GET_KEY, genericGet);
791         }
792         if(exposureLevel == EXPOSE_NOTHING)
793         {
794             return classMap;
795         }
796         
797         try
798         {
799             BeanInfo JavaDoc beanInfo = Introspector.getBeanInfo(clazz);
800             PropertyDescriptor JavaDoc[] pda = beanInfo.getPropertyDescriptors();
801             MethodDescriptor JavaDoc[] mda = beanInfo.getMethodDescriptors();
802
803             for(int i = pda.length - 1; i >= 0; --i) {
804                 PropertyDescriptor JavaDoc pd = pda[i];
805                 if(pd instanceof IndexedPropertyDescriptor JavaDoc) {
806                     IndexedPropertyDescriptor JavaDoc ipd =
807                         (IndexedPropertyDescriptor JavaDoc)pd;
808                     Method JavaDoc readMethod = ipd.getIndexedReadMethod();
809                     Method JavaDoc publicReadMethod = getAccessibleMethod(readMethod,
810                             accessibleMethods);
811                     if(publicReadMethod != null && isSafeMethod(publicReadMethod)) {
812                         try {
813                             if(readMethod != publicReadMethod) {
814                                 ipd = new IndexedPropertyDescriptor JavaDoc(
815                                         ipd.getName(), ipd.getReadMethod(),
816                                         ipd.getWriteMethod(), publicReadMethod,
817                                         ipd.getWriteMethod());
818                             }
819                             classMap.put(ipd.getName(), ipd);
820                             getArgTypes(classMap).put(publicReadMethod,
821                                     publicReadMethod.getParameterTypes());
822                         }
823                         catch(IntrospectionException JavaDoc e) {
824                             logger.warn("Couldn't properly perform introspection", e);
825                         }
826                     }
827                 }
828                 else {
829                     Method JavaDoc readMethod = pd.getReadMethod();
830                     Method JavaDoc publicReadMethod = getAccessibleMethod(readMethod, accessibleMethods);
831                     if(publicReadMethod != null && isSafeMethod(publicReadMethod)) {
832                         try {
833                             if(readMethod != publicReadMethod) {
834                                 pd = new PropertyDescriptor JavaDoc(pd.getName(),
835                                         publicReadMethod, pd.getWriteMethod());
836                                 pd.setReadMethod(publicReadMethod);
837                             }
838                             classMap.put(pd.getName(), pd);
839                         }
840                         catch(IntrospectionException JavaDoc e)
841                         {
842                             logger.warn("Couldn't properly perform introspection", e);
843                         }
844                     }
845                 }
846             }
847             if(exposureLevel < EXPOSE_PROPERTIES_ONLY)
848             {
849                 for(int i = mda.length - 1; i >= 0; --i)
850                 {
851                     MethodDescriptor JavaDoc md = mda[i];
852                     Method JavaDoc method = md.getMethod();
853                     Method JavaDoc publicMethod = getAccessibleMethod(method, accessibleMethods);
854                     if(publicMethod != null && isSafeMethod(publicMethod))
855                     {
856                         String JavaDoc name = md.getName();
857                         Object JavaDoc previous = classMap.get(name);
858                         if(previous instanceof Method JavaDoc)
859                         {
860                             // Overloaded method - replace method with a method map
861
MethodMap methodMap = new MethodMap(name);
862                             methodMap.addMethod((Method JavaDoc)previous);
863                             methodMap.addMethod(publicMethod);
864                             classMap.put(name, methodMap);
865                             // remove parameter type information
866
getArgTypes(classMap).remove(previous);
867                         }
868                         else if(previous instanceof MethodMap)
869                         {
870                             // Already overloaded method - add new overload
871
((MethodMap)previous).addMethod(publicMethod);
872                         }
873                         else
874                         {
875                             // Simple method (this far)
876
classMap.put(name, publicMethod);
877                             getArgTypes(classMap).put(publicMethod,
878                                     publicMethod.getParameterTypes());
879                         }
880                     }
881                 }
882             }
883             return classMap;
884         }
885         catch(IntrospectionException JavaDoc e)
886         {
887             logger.warn("Couldn't properly perform introspection", e);
888             return new HashMap JavaDoc();
889         }
890     }
891
892     private static Map JavaDoc getArgTypes(Map JavaDoc classMap) {
893         Map JavaDoc argTypes = (Map JavaDoc)classMap.get(ARGTYPES);
894         if(argTypes == null) {
895             argTypes = new HashMap JavaDoc();
896             classMap.put(ARGTYPES, argTypes);
897         }
898         return argTypes;
899     }
900     
901     static Class JavaDoc[] getArgTypes(Map JavaDoc classMap, AccessibleObject JavaDoc methodOrCtor) {
902         return (Class JavaDoc[])((Map JavaDoc)classMap.get(ARGTYPES)).get(methodOrCtor);
903     }
904
905     private static Method JavaDoc getAccessibleMethod(Method JavaDoc m, Map JavaDoc accessibles)
906     {
907         return m == null ? null : (Method JavaDoc)accessibles.get(new MethodSignature(m));
908     }
909     
910     boolean isSafeMethod(Method JavaDoc method)
911     {
912         return exposureLevel < EXPOSE_SAFE || !UNSAFE_METHODS.contains(method);
913     }
914     
915     /**
916      * Retrieves mapping of methods to accessible methods for a class.
917      * In case the class is not public, retrieves methods with same
918      * signature as its public methods from public superclasses and
919      * interfaces (if they exist). Basically upcasts every method to the
920      * nearest accessible method.
921      */

922     private static Map JavaDoc discoverAccessibleMethods(Class JavaDoc clazz)
923     {
924         Map JavaDoc map = new HashMap JavaDoc();
925         discoverAccessibleMethods(clazz, map);
926         return map;
927     }
928     
929     private static void discoverAccessibleMethods(Class JavaDoc clazz, Map JavaDoc map)
930     {
931         if(Modifier.isPublic(clazz.getModifiers()))
932         {
933             try
934             {
935                 Method JavaDoc[] methods = clazz.getMethods();
936                 for(int i = 0; i < methods.length; i++)
937                 {
938                     Method JavaDoc method = methods[i];
939                     MethodSignature sig = new MethodSignature(method);
940                     map.put(sig, method);
941                 }
942                 return;
943             }
944             catch(SecurityException JavaDoc e)
945             {
946                 logger.warn("Could not discover accessible methods of class " +
947                         clazz.getName() +
948                         ", attemping superclasses/interfaces.", e);
949                 // Fall through and attempt to discover superclass/interface
950
// methods
951
}
952         }
953
954         Class JavaDoc[] interfaces = clazz.getInterfaces();
955         for(int i = 0; i < interfaces.length; i++)
956         {
957             discoverAccessibleMethods(interfaces[i], map);
958         }
959         Class JavaDoc superclass = clazz.getSuperclass();
960         if(superclass != null)
961         {
962             discoverAccessibleMethods(superclass, map);
963         }
964     }
965
966     private static final class MethodSignature
967     {
968         private static final MethodSignature GET_STRING_SIGNATURE =
969             new MethodSignature("get", new Class JavaDoc[] { STRING_CLASS });
970         private static final MethodSignature GET_OBJECT_SIGNATURE =
971             new MethodSignature("get", new Class JavaDoc[] { OBJECT_CLASS });
972
973         private final String JavaDoc name;
974         private final Class JavaDoc[] args;
975         
976         private MethodSignature(String JavaDoc name, Class JavaDoc[] args)
977         {
978             this.name = name;
979             this.args = args;
980         }
981         
982         MethodSignature(Method JavaDoc method)
983         {
984             this(method.getName(), method.getParameterTypes());
985         }
986         
987         public boolean equals(Object JavaDoc o)
988         {
989             if(o instanceof MethodSignature)
990             {
991                 MethodSignature ms = (MethodSignature)o;
992                 return ms.name.equals(name) && Arrays.equals(args, ms.args);
993             }
994             return false;
995         }
996         
997         public int hashCode()
998         {
999             return name.hashCode() ^ args.length;
1000        }
1001    }
1002    
1003    private static final Set JavaDoc createUnsafeMethodsSet()
1004    {
1005        Properties JavaDoc props = new Properties JavaDoc();
1006        InputStream JavaDoc in = BeansWrapper.class.getResourceAsStream("unsafeMethods.txt");
1007        if(in != null)
1008        {
1009            String JavaDoc methodSpec = null;
1010            try
1011            {
1012                try
1013                {
1014                    props.load(in);
1015                }
1016                finally
1017                {
1018                    in.close();
1019                }
1020                Set JavaDoc set = new HashSet JavaDoc(props.size() * 4/3, .75f);
1021                Map JavaDoc primClasses = createPrimitiveClassesMap();
1022                for (Iterator JavaDoc iterator = props.keySet().iterator(); iterator.hasNext();)
1023                {
1024                    methodSpec = (String JavaDoc) iterator.next();
1025                    try {
1026                        set.add(parseMethodSpec(methodSpec, primClasses));
1027                    }
1028                    catch(ClassNotFoundException JavaDoc e) {
1029                        if(DEVELOPMENT) {
1030                            throw e;
1031                        }
1032                    }
1033                    catch(NoSuchMethodException JavaDoc e) {
1034                        if(DEVELOPMENT) {
1035                            throw e;
1036                        }
1037                    }
1038                }
1039            }
1040            catch(Exception JavaDoc e)
1041            {
1042                throw new RuntimeException JavaDoc("Could not load unsafe method " + methodSpec + " " + e.getClass().getName() + " " + e.getMessage());
1043            }
1044        }
1045        return Collections.EMPTY_SET;
1046    }
1047                                                                           
1048    private static Method JavaDoc parseMethodSpec(String JavaDoc methodSpec, Map JavaDoc primClasses)
1049    throws
1050        ClassNotFoundException JavaDoc,
1051        NoSuchMethodException JavaDoc
1052    {
1053        int brace = methodSpec.indexOf('(');
1054        int dot = methodSpec.lastIndexOf('.', brace);
1055        Class JavaDoc clazz = ClassUtil.forName(methodSpec.substring(0, dot));
1056        String JavaDoc methodName = methodSpec.substring(dot + 1, brace);
1057        String JavaDoc argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
1058        StringTokenizer JavaDoc tok = new StringTokenizer JavaDoc(argSpec, ",");
1059        int argcount = tok.countTokens();
1060        Class JavaDoc[] argTypes = new Class JavaDoc[argcount];
1061        for (int i = 0; i < argcount; i++)
1062        {
1063            String JavaDoc argClassName = tok.nextToken();
1064            argTypes[i] = (Class JavaDoc)primClasses.get(argClassName);
1065            if(argTypes[i] == null)
1066            {
1067                argTypes[i] = ClassUtil.forName(argClassName);
1068            }
1069        }
1070        return clazz.getMethod(methodName, argTypes);
1071    }
1072
1073    private static Map JavaDoc createPrimitiveClassesMap()
1074    {
1075        Map JavaDoc map = new HashMap JavaDoc();
1076        map.put("boolean", Boolean.TYPE);
1077        map.put("byte", Byte.TYPE);
1078        map.put("char", Character.TYPE);
1079        map.put("short", Short.TYPE);
1080        map.put("int", Integer.TYPE);
1081        map.put("long", Long.TYPE);
1082        map.put("float", Float.TYPE);
1083        map.put("double", Double.TYPE);
1084        return map;
1085    }
1086
1087
1088    /**
1089     * Converts any {@link BigDecimal}s in the passed array to the type of
1090     * the corresponding formal argument of the method.
1091     */

1092    public static void coerceBigDecimals(AccessibleObject JavaDoc callable, Object JavaDoc[] args)
1093    {
1094        Class JavaDoc[] formalTypes = null;
1095        for(int i = 0, l = args.length; i < l; ++i)
1096        {
1097            Object JavaDoc arg = args[i];
1098            if(arg instanceof BigDecimal JavaDoc)
1099            {
1100                BigDecimal JavaDoc bd = (BigDecimal JavaDoc)arg;
1101                if(formalTypes == null)
1102                {
1103                    if(callable instanceof Method JavaDoc) {
1104                        formalTypes = ((Method JavaDoc)callable).getParameterTypes();
1105                    }
1106                    else if(callable instanceof Constructor JavaDoc) {
1107                        formalTypes = ((Constructor JavaDoc)callable).getParameterTypes();
1108                    }
1109                    else {
1110                        // Cannot happen
1111
throw new Error JavaDoc();
1112                    }
1113                    if(formalTypes.length != l)
1114                    {
1115                        // This will die anyway due to incorrect number of
1116
// arguments, so there's no point in checking
1117
return;
1118                    }
1119                }
1120                Class JavaDoc formalType = formalTypes[i];
1121                // int is expected in most situations, so we check it first
1122
if(formalType == Integer.TYPE || formalType == Integer JavaDoc.class)
1123                {
1124                    args[i] = new Integer JavaDoc(bd.intValue());
1125                }
1126                else if(formalType == Double.TYPE || formalType == Double JavaDoc.class)
1127                {
1128                    args[i] = new Double JavaDoc(bd.doubleValue());
1129                }
1130                else if(formalType == Long.TYPE || formalType == Long JavaDoc.class)
1131                {
1132                    args[i] = new Long JavaDoc(bd.longValue());
1133                }
1134                else if(formalType == Float.TYPE || formalType == Float JavaDoc.class)
1135                {
1136                    args[i] = new Float JavaDoc(bd.floatValue());
1137                }
1138                else if(formalType == Short.TYPE || formalType == Short JavaDoc.class)
1139                {
1140                    args[i] = new Short JavaDoc(bd.shortValue());
1141                }
1142                else if(formalType == Byte.TYPE || formalType == Byte JavaDoc.class)
1143                {
1144                    args[i] = new Byte JavaDoc(bd.byteValue());
1145                }
1146                else if(BIGINTEGER_CLASS.isAssignableFrom(formalType))
1147                {
1148                    args[i] = bd.toBigInteger();
1149                }
1150            }
1151        }
1152    }
1153}
1154
Popular Tags