Save This Page
Home » freemarker-2.3.13 » freemarker.ext.beans » [javadoc | source]
    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;
   56   import java.beans.IndexedPropertyDescriptor;
   57   import java.beans.IntrospectionException;
   58   import java.beans.Introspector;
   59   import java.beans.MethodDescriptor;
   60   import java.beans.PropertyDescriptor;
   61   import java.io.InputStream;
   62   import java.lang.reflect.AccessibleObject;
   63   import java.lang.reflect.Array;
   64   import java.lang.reflect.Constructor;
   65   import java.lang.reflect.Field;
   66   import java.lang.reflect.InvocationTargetException;
   67   import java.lang.reflect.Method;
   68   import java.lang.reflect.Modifier;
   69   import java.math.BigDecimal;
   70   import java.math.BigInteger;
   71   import java.util.Arrays;
   72   import java.util.Collection;
   73   import java.util.Collections;
   74   import java.util.Date;
   75   import java.util.Enumeration;
   76   import java.util.HashMap;
   77   import java.util.HashSet;
   78   import java.util.Iterator;
   79   import java.util.List;
   80   import java.util.Map;
   81   import java.util.Properties;
   82   import java.util.ResourceBundle;
   83   import java.util.Set;
   84   import java.util.StringTokenizer;
   85   
   86   import freemarker.ext.util.IdentityHashMap;
   87   import freemarker.ext.util.ModelCache;
   88   import freemarker.ext.util.ModelFactory;
   89   import freemarker.ext.util.WrapperTemplateModel;
   90   import freemarker.log.Logger;
   91   import freemarker.template.AdapterTemplateModel;
   92   import freemarker.template.ObjectWrapper;
   93   import freemarker.template.TemplateBooleanModel;
   94   import freemarker.template.TemplateCollectionModel;
   95   import freemarker.template.TemplateDateModel;
   96   import freemarker.template.TemplateHashModel;
   97   import freemarker.template.TemplateModel;
   98   import freemarker.template.TemplateModelException;
   99   import freemarker.template.TemplateNumberModel;
  100   import freemarker.template.TemplateScalarModel;
  101   import freemarker.template.TemplateSequenceModel;
  102   import freemarker.template.utility.ClassUtil;
  103   import freemarker.template.utility.Collections12;
  104   import freemarker.template.utility.SecurityUtilities;
  105   import freemarker.template.utility.UndeclaredThrowableException;
  106   
  107   /**
  108    * Utility class that provides generic services to reflection classes.
  109    * It handles all polymorphism issues in the {@link #wrap(Object)} and {@link #unwrap(TemplateModel)} methods.
  110    * @author Attila Szegedi
  111    * @version $Id: BeansWrapper.java,v 1.91.2.13 2007/04/02 13:08:59 szegedia Exp $
  112    */
  113   public class BeansWrapper implements ObjectWrapper
  114   {
  115       public static final Object CAN_NOT_UNWRAP = new Object();
  116       private static final Class BIGINTEGER_CLASS = java.math.BigInteger.class;
  117       private static final Class BOOLEAN_CLASS = Boolean.class;
  118       private static final Class CHARACTER_CLASS = Character.class;
  119       private static final Class COLLECTION_CLASS = Collection.class;
  120       private static final Class DATE_CLASS = Date.class;
  121       private static final Class HASHADAPTER_CLASS = HashAdapter.class;
  122       private static final Class ITERABLE_CLASS;
  123       private static final Class LIST_CLASS = List.class;
  124       private static final Class MAP_CLASS = Map.class;
  125       private static final Class NUMBER_CLASS = Number.class;
  126       private static final Class OBJECT_CLASS = Object.class;
  127       private static final Class SEQUENCEADAPTER_CLASS = SequenceAdapter.class;
  128       private static final Class SET_CLASS = Set.class;
  129       private static final Class SETADAPTER_CLASS = SetAdapter.class;
  130       private static final Class STRING_CLASS = String.class;
  131       static {
  132           Class iterable;
  133           try {
  134               iterable = Class.forName("java.lang.Iterable");
  135           }
  136           catch(ClassNotFoundException e) {
  137               // We're running on a pre-1.5 JRE
  138               iterable = null;
  139           }
  140           ITERABLE_CLASS = iterable;
  141       }
  142       
  143       // When this property is true, some things are stricter. This is mostly to
  144       // catch anomalous things in development that can otherwise be valid situations
  145       // for our users.
  146       private static final boolean DEVELOPMENT = "true".equals(SecurityUtilities.getSystemProperty("freemarker.development"));
  147       
  148       private static final Constructor ENUMS_MODEL_CTOR = enumsModelCtor();
  149   
  150       private static final Logger logger = Logger.getLogger("freemarker.beans");
  151       
  152       private static final Set UNSAFE_METHODS = createUnsafeMethodsSet();
  153       
  154       static final Object GENERIC_GET_KEY = new Object();
  155       private static final Object CONSTRUCTORS = new Object();
  156       private static final Object ARGTYPES = new Object();
  157       
  158       /**
  159        * The default instance of BeansWrapper
  160        */
  161       private static final BeansWrapper INSTANCE = new BeansWrapper();
  162   
  163       // Cache of hash maps that contain already discovered properties and methods
  164       // for a specified class. Each key is a Class, each value is a hash map. In
  165       // that hash map, each key is a property/method name, each value is a
  166       // MethodDescriptor or a PropertyDescriptor assigned to that property/method.
  167       private final Map classCache = new HashMap();
  168       private Set cachedClassNames = new HashSet();
  169   
  170       private final StaticModels staticModels = new StaticModels(this);
  171       private final ClassBasedModelFactory enumModels = createEnumModels(this);
  172   
  173       private final ModelCache modelCache = new BeansModelCache(this);
  174       
  175       private final BooleanModel FALSE = new BooleanModel(Boolean.FALSE, this);
  176       private final BooleanModel TRUE = new BooleanModel(Boolean.TRUE, this);
  177   
  178       /**
  179        * At this level of exposure, all methods and properties of the
  180        * wrapped objects are exposed to the template.
  181        */
  182       public static final int EXPOSE_ALL = 0;
  183       
  184       /**
  185        * At this level of exposure, all methods and properties of the wrapped
  186        * objects are exposed to the template except methods that are deemed
  187        * not safe. The not safe methods are java.lang.Object methods wait() and
  188        * notify(), java.lang.Class methods getClassLoader() and newInstance(),
  189        * java.lang.reflect.Method and java.lang.reflect.Constructor invoke() and
  190        * newInstance() methods, all java.lang.reflect.Field set methods, all 
  191        * java.lang.Thread and java.lang.ThreadGroup methods that can change its 
  192        * state, as well as the usual suspects in java.lang.System and
  193        * java.lang.Runtime.
  194        */
  195       public static final int EXPOSE_SAFE = 1;
  196       
  197       /**
  198        * At this level of exposure, only property getters are exposed.
  199        * Additionally, property getters that map to unsafe methods are not
  200        * exposed (i.e. Class.classLoader and Thread.contextClassLoader).
  201        */
  202       public static final int EXPOSE_PROPERTIES_ONLY = 2;
  203   
  204       /**
  205        * At this level of exposure, no bean properties and methods are exposed.
  206        * Only map items, resource bundle items, and objects retrieved through
  207        * the generic get method (on objects of classes that have a generic get
  208        * method) can be retrieved through the hash interface. You might want to 
  209        * call {@link #setMethodsShadowItems(boolean)} with <tt>false</tt> value to
  210        * speed up map item retrieval.
  211        */
  212       public static final int EXPOSE_NOTHING = 3;
  213   
  214       private int exposureLevel = EXPOSE_SAFE;
  215       private TemplateModel nullModel = null;
  216       private boolean methodsShadowItems = true;
  217       private boolean exposeFields = false;
  218       private int defaultDateType = TemplateDateModel.UNKNOWN;
  219   
  220       private ObjectWrapper outerIdentity = this;
  221       private boolean simpleMapWrapper;
  222       private boolean strict = false; 
  223       
  224       /**
  225        * Creates a new instance of BeansWrapper. The newly created instance
  226        * will use the null reference as its null object, it will use
  227        * {@link #EXPOSE_SAFE} method exposure level, and will not cache
  228        * model instances.
  229        */
  230       public BeansWrapper()
  231       {
  232       }
  233       
  234       /**
  235        * @see #setStrict(boolean)
  236        */
  237       public boolean isStrict() {
  238       	return strict;
  239       }
  240       
  241       /**
  242        * Specifies if an attempt to read a bean property that doesn't exist in the
  243        * wrapped object should throw an {@link InvalidPropertyException}.
  244        * 
  245        * <p>If this property is <tt>false</tt> (the default) then an attempt to read
  246        * a missing bean property is the same as reading an existing bean property whose
  247        * value is <tt>null</tt>. The template can't tell the difference, and thus always
  248        * can use <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins
  249        * to handle the situation.
  250        *
  251        * <p>If this property is <tt>true</tt> then an attempt to read a bean propertly in
  252        * the template (like <tt>myBean.aProperty</tt>) that doesn't exist in the bean
  253        * object (as opposed to just holding <tt>null</tt> value) will cause
  254        * {@link InvalidPropertyException}, which can't be suppressed in the template
  255        * (not even with <tt>myBean.noSuchProperty?default('something')</tt>). This way
  256        * <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins can be used to
  257        * handle existing properties whose value is <tt>null</tt>, without the risk of
  258        * hiding typos in the property names. Typos will always cause error. But mind you, it
  259        * goes against the basic approach of FreeMarker, so use this feature only if you really
  260        * know what are you doing.
  261        */
  262       public void setStrict(boolean strict) {
  263       	this.strict = strict;
  264       }
  265   
  266       /**
  267        * When wrapping an object, the BeansWrapper commonly needs to wrap
  268        * "sub-objects", for example each element in a wrapped collection.
  269        * Normally it wraps these objects using itself. However, this makes
  270        * it difficult to delegate to a BeansWrapper as part of a custom
  271        * aggregate ObjectWrapper. This method lets you set the ObjectWrapper
  272        * which will be used to wrap the sub-objects.
  273        * @param outerIdentity the aggregate ObjectWrapper
  274        */
  275       public void setOuterIdentity(ObjectWrapper outerIdentity)
  276       {
  277           this.outerIdentity = outerIdentity;
  278       }
  279   
  280       /**
  281        * By default returns <tt>this</tt>.
  282        * @see #setOuterIdentity(ObjectWrapper)
  283        */
  284       public ObjectWrapper getOuterIdentity()
  285       {
  286           return outerIdentity;
  287       }
  288   
  289       /**
  290        * By default the BeansWrapper wraps classes implementing
  291        * java.util.Map using {@link MapModel}. Setting this flag will
  292        * cause it to use a {@link SimpleMapModel} instead. The biggest
  293        * difference is that when using a {@link SimpleMapModel}, the
  294        * map will be visible as <code>TemplateHashModelEx</code>,
  295        * and the subvariables will be the content of the map,
  296        * without the other methods and properties of the map object.
  297        * @param simpleMapWrapper enable simple map wrapping
  298        */
  299       public void setSimpleMapWrapper(boolean simpleMapWrapper)
  300       {
  301           this.simpleMapWrapper = simpleMapWrapper;
  302       }
  303   
  304       /**
  305        * Tells whether Maps are exposed as simple maps, without access to their
  306        * method. See {@link #setSimpleMapWrapper(boolean)} for details.
  307        * @return true if Maps are exposed as simple hashes, false if they're
  308        * exposed as full JavaBeans.
  309        */
  310       public boolean isSimpleMapWrapper()
  311       {
  312           return simpleMapWrapper;
  313       }
  314   
  315       /**
  316        * Sets the method exposure level. By default, set to <code>EXPOSE_SAFE</code>.
  317        * @param exposureLevel can be any of the <code>EXPOSE_xxx</code>
  318        * constants.
  319        */
  320       public void setExposureLevel(int exposureLevel)
  321       {
  322           if(exposureLevel < EXPOSE_ALL || exposureLevel > EXPOSE_NOTHING)
  323           {
  324               throw new IllegalArgumentException("Illegal exposure level " + exposureLevel);
  325           }
  326           this.exposureLevel = exposureLevel;
  327       }
  328       
  329       int getExposureLevel()
  330       {
  331           return exposureLevel;
  332       }
  333       
  334       /**
  335        * Controls whether public instance fields of classes are exposed to 
  336        * templates.
  337        * @param exposeFields if set to true, public instance fields of classes 
  338        * that do not have a property getter defined can be accessed directly by
  339        * their name. If there is a property getter for a property of the same 
  340        * name as the field (i.e. getter "getFoo()" and field "foo"), then 
  341        * referring to "foo" in template invokes the getter. If set to false, no
  342        * access to public instance fields of classes is given. Default is false.
  343        */
  344       public void setExposeFields(boolean exposeFields)
  345       {
  346           this.exposeFields = exposeFields;
  347       }
  348       
  349       /**
  350        * Returns whether exposure of public instance fields of classes is 
  351        * enabled. See {@link #setExposeFields(boolean)} for details.
  352        * @return true if public instance fields are exposed, false otherwise.
  353        */
  354       public boolean isExposeFields()
  355       {
  356           return exposeFields;
  357       }
  358       
  359       /**
  360        * Sets whether methods shadow items in beans. When true (this is the
  361        * default value), <code>${object.name}</code> will first try to locate
  362        * a bean method or property with the specified name on the object, and
  363        * only if it doesn't find it will it try to call
  364        * <code>object.get(name)</code>, the so-called "generic get method" that
  365        * is usually used to access items of a container (i.e. elements of a map).
  366        * When set to false, the lookup order is reversed and generic get method
  367        * is called first, and only if it returns null is method lookup attempted.
  368        */
  369       public synchronized void setMethodsShadowItems(boolean methodsShadowItems)
  370       {
  371           this.methodsShadowItems = methodsShadowItems;
  372       }
  373       
  374       boolean isMethodsShadowItems()
  375       {
  376           return methodsShadowItems;
  377       }
  378       
  379       /**
  380        * Sets the default date type to use for date models that result from
  381        * a plain <tt>java.util.Date</tt> instead of <tt>java.sql.Date</tt> or
  382        * <tt>java.sql.Time</tt> or <tt>java.sql.Timestamp</tt>. Default value is 
  383        * {@link TemplateDateModel#UNKNOWN}.
  384        * @param defaultDateType the new default date type.
  385        */
  386       public synchronized void setDefaultDateType(int defaultDateType) {
  387           this.defaultDateType = defaultDateType;
  388       }
  389   
  390       /**
  391        * Returns the default date type. See {@link #setDefaultDateType(int)} for
  392        * details.
  393        * @return the default date type
  394        */
  395       protected int getDefaultDateType() {
  396           return defaultDateType;
  397       }
  398       
  399       /**
  400        * Sets whether this wrapper caches model instances. Default is false.
  401        * When set to true, calling {@link #wrap(Object)} multiple times for
  402        * the same object will likely return the same model (although there is
  403        * no guarantee as the cache items can be cleared anytime).
  404        */
  405       public void setUseCache(boolean useCache)
  406       {
  407           modelCache.setUseCache(useCache);
  408       }
  409       
  410       /**
  411        * Sets the null model. This model is returned from the
  412        * {@link #wrap(Object)} method whenever the underlying object 
  413        * reference is null. It defaults to null reference, which is dealt 
  414        * with quite strictly on engine level, however you can substitute an 
  415        * arbitrary (perhaps more lenient) model, such as 
  416        * {@link freemarker.template.TemplateScalarModel#EMPTY_STRING}.
  417        */
  418       public void setNullModel(TemplateModel nullModel)
  419       {
  420           this.nullModel = nullModel;
  421       }
  422       
  423       /**
  424        * Returns the default instance of the wrapper. This instance is used
  425        * when you construct various bean models without explicitly specifying
  426        * a wrapper. It is also returned by 
  427        * {@link freemarker.template.ObjectWrapper#BEANS_WRAPPER}
  428        * and this is the sole instance that is used by the JSP adapter.
  429        * You can modify the properties of the default instance (caching,
  430        * exposure level, null model) to affect its operation. By default, the
  431        * default instance is not caching, uses the <code>EXPOSE_SAFE</code>
  432        * exposure level, and uses null reference as the null model.
  433        */
  434       public static final BeansWrapper getDefaultInstance()
  435       {
  436           return INSTANCE;
  437       }
  438   
  439       /**
  440        * Wraps the object with a template model that is most specific for the object's
  441        * class. Specifically:
  442        * <ul>
  443        * <li>if the object is null, returns the {@link #setNullModel(TemplateModel) null model},</li>
  444        * <li>if the object is a Number returns a {@link NumberModel} for it,</li>
  445        * <li>if the object is a Date returns a {@link DateModel} for it,</li>
  446        * <li>if the object is a Boolean returns 
  447        * {@link freemarker.template.TemplateBooleanModel#TRUE} or 
  448        * {@link freemarker.template.TemplateBooleanModel#FALSE}</li>
  449        * <li>if the object is already a TemplateModel, returns it unchanged,</li>
  450        * <li>if the object is an array, returns a {@link ArrayModel} for it
  451        * <li>if the object is a Map, returns a {@link MapModel} for it
  452        * <li>if the object is a Collection, returns a {@link CollectionModel} for it
  453        * <li>if the object is an Iterator, returns a {@link IteratorModel} for it
  454        * <li>if the object is an Enumeration, returns a {@link EnumerationModel} for it
  455        * <li>if the object is a String, returns a {@link StringModel} for it
  456        * <li>otherwise, returns a generic {@link BeanModel} for it.
  457        * </ul>
  458        */
  459       public TemplateModel wrap(Object object) throws TemplateModelException
  460       {
  461           if(object == null)
  462               return nullModel;
  463           return modelCache.getInstance(object);
  464       }
  465   
  466       /**
  467        * @deprecated override {@link #getModelFactory(Class)} instead. Using this
  468        * method will now bypass wrapper caching (if it is enabled) and always 
  469        * result in creation of a new wrapper. This method will be removed in 2.4
  470        * @param object
  471        * @param factory
  472        */
  473       protected TemplateModel getInstance(Object object, ModelFactory factory)
  474       {
  475           return factory.create(object, this);
  476       }
  477   
  478       private final ModelFactory BOOLEAN_FACTORY = new ModelFactory() {
  479           public TemplateModel create(Object object, ObjectWrapper wrapper) {
  480               return ((Boolean)object).booleanValue() ? TRUE : FALSE; 
  481           }
  482       };
  483   
  484       private static final ModelFactory ITERATOR_FACTORY = new ModelFactory() {
  485           public TemplateModel create(Object object, ObjectWrapper wrapper) {
  486               return new IteratorModel((Iterator)object, (BeansWrapper)wrapper); 
  487           }
  488       };
  489   
  490       private static final ModelFactory ENUMERATION_FACTORY = new ModelFactory() {
  491           public TemplateModel create(Object object, ObjectWrapper wrapper) {
  492               return new EnumerationModel((Enumeration)object, (BeansWrapper)wrapper); 
  493           }
  494       };
  495   
  496       protected ModelFactory getModelFactory(Class clazz) {
  497           if(Map.class.isAssignableFrom(clazz)) {
  498               return simpleMapWrapper ? SimpleMapModel.FACTORY : MapModel.FACTORY;
  499           }
  500           if(Collection.class.isAssignableFrom(clazz)) {
  501               return CollectionModel.FACTORY;
  502           }
  503           if(Number.class.isAssignableFrom(clazz)) {
  504               return NumberModel.FACTORY;
  505           }
  506           if(Date.class.isAssignableFrom(clazz)) {
  507               return DateModel.FACTORY;
  508           }
  509           if(Boolean.class == clazz) { // Boolean is final 
  510               return BOOLEAN_FACTORY;
  511           }
  512           if(ResourceBundle.class.isAssignableFrom(clazz)) {
  513               return ResourceBundleModel.FACTORY;
  514           }
  515           if(Iterator.class.isAssignableFrom(clazz)) {
  516               return ITERATOR_FACTORY;
  517           }
  518           if(Enumeration.class.isAssignableFrom(clazz)) {
  519               return ENUMERATION_FACTORY;
  520           }
  521           if(clazz.isArray()) {
  522               return ArrayModel.FACTORY;
  523           }
  524           return StringModel.FACTORY;
  525       }
  526   
  527       /**
  528        * Attempts to unwrap a model into underlying object. Generally, this
  529        * method is the inverse of the {@link #wrap(Object)} method. In addition
  530        * it will unwrap arbitrary {@link TemplateNumberModel} instances into
  531        * a number, arbitrary {@link TemplateDateModel} instances into a date,
  532        * {@link TemplateScalarModel} instances into a String, and
  533        * {@link TemplateBooleanModel} instances into a Boolean.
  534        * All other objects are returned unchanged.
  535        */
  536       public Object unwrap(TemplateModel model) throws TemplateModelException
  537       {
  538           return unwrap(model, OBJECT_CLASS);
  539       }
  540       
  541       public Object unwrap(TemplateModel model, Class hint) 
  542       throws TemplateModelException
  543       {
  544           return unwrap(model, hint, null);
  545       }
  546       
  547       private Object unwrap(TemplateModel model, Class hint, Map recursionStops) 
  548       throws TemplateModelException
  549       {
  550           if(model == nullModel) {
  551               return null;
  552           }
  553           
  554           boolean isBoolean = Boolean.TYPE == hint;
  555           boolean isChar = Character.TYPE == hint;
  556           
  557           // This is for transparent interop with other wrappers (and ourselves)
  558           // Passing the hint allows i.e. a Jython-aware method that declares a
  559           // PyObject as its argument to receive a PyObject from a JythonModel
  560           // passed as an argument to TemplateMethodModelEx etc.
  561           if(model instanceof AdapterTemplateModel) {
  562               Object adapted = ((AdapterTemplateModel)model).getAdaptedObject(
  563                       hint);
  564               if(hint.isInstance(adapted)) {
  565                   return adapted;
  566               }
  567               // Attempt numeric conversion 
  568               if(adapted instanceof Number && ((hint.isPrimitive() && !isChar && 
  569                       !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
  570                   Number number = convertUnwrappedNumber(hint,
  571                           (Number)adapted);
  572                   if(number != null) {
  573                       return number;
  574                   }
  575               }
  576           }
  577           
  578           if(model instanceof WrapperTemplateModel) {
  579               Object wrapped = ((WrapperTemplateModel)model).getWrappedObject();
  580               if(hint.isInstance(wrapped)) {
  581                   return wrapped;
  582               }
  583               // Attempt numeric conversion 
  584               if(wrapped instanceof Number && ((hint.isPrimitive() && !isChar && 
  585                       !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
  586                   Number number = convertUnwrappedNumber(hint,
  587                           (Number)wrapped);
  588                   if(number != null) {
  589                       return number;
  590                   }
  591               }
  592           }
  593           
  594           // Translation of generic template models to POJOs. First give priority
  595           // to various model interfaces based on the hint class. This helps us
  596           // select the appropriate interface in multi-interface models when we
  597           // know what is expected as the return type.
  598   
  599           if(STRING_CLASS == hint) {
  600               if(model instanceof TemplateScalarModel) {
  601                   return ((TemplateScalarModel)model).getAsString();
  602               }
  603               // String is final, so no other conversion will work
  604               return CAN_NOT_UNWRAP;
  605           }
  606   
  607           // Primitive numeric types & Number.class and its subclasses
  608           if((hint.isPrimitive() && !isChar && !isBoolean) 
  609                   || NUMBER_CLASS.isAssignableFrom(hint)) {
  610               if(model instanceof TemplateNumberModel) {
  611                   Number number = convertUnwrappedNumber(hint, 
  612                           ((TemplateNumberModel)model).getAsNumber());
  613                   if(number != null) {
  614                       return number;
  615                   }
  616               }
  617           }
  618           
  619           if(isBoolean || BOOLEAN_CLASS == hint) {
  620               if(model instanceof TemplateBooleanModel) {
  621                   return ((TemplateBooleanModel)model).getAsBoolean() 
  622                   ? Boolean.TRUE : Boolean.FALSE;
  623               }
  624               // Boolean is final, no other conversion will work
  625               return CAN_NOT_UNWRAP;
  626           }
  627   
  628           if(MAP_CLASS == hint) {
  629               if(model instanceof TemplateHashModel) {
  630                   return new HashAdapter((TemplateHashModel)model, this);
  631               }
  632           }
  633           
  634           if(LIST_CLASS == hint) {
  635               if(model instanceof TemplateSequenceModel) {
  636                   return new SequenceAdapter((TemplateSequenceModel)model, this);
  637               }
  638           }
  639           
  640           if(SET_CLASS == hint) {
  641               if(model instanceof TemplateCollectionModel) {
  642                   return new SetAdapter((TemplateCollectionModel)model, this);
  643               }
  644           }
  645           
  646           if(COLLECTION_CLASS == hint 
  647                   || ITERABLE_CLASS == hint) {
  648               if(model instanceof TemplateCollectionModel) {
  649                   return new CollectionAdapter((TemplateCollectionModel)model, 
  650                           this);
  651               }
  652               if(model instanceof TemplateSequenceModel) {
  653                   return new SequenceAdapter((TemplateSequenceModel)model, this);
  654               }
  655           }
  656           
  657           // TemplateSequenceModels can be converted to arrays
  658           if(hint.isArray()) {
  659               if(model instanceof TemplateSequenceModel) {
  660                   if(recursionStops != null) {
  661                       Object retval = recursionStops.get(model);
  662                       if(retval != null) {
  663                           return retval;
  664                       }
  665                   } else {
  666                       recursionStops = 
  667                           new IdentityHashMap();
  668                   }
  669                   TemplateSequenceModel seq = (TemplateSequenceModel)model;
  670                   Class componentType = hint.getComponentType();
  671                   Object array = Array.newInstance(componentType, seq.size());
  672                   recursionStops.put(model, array);
  673                   try {
  674                       int size = seq.size();
  675                       for (int i = 0; i < size; i++) {
  676                           Object val = unwrap(model, componentType, 
  677                                   recursionStops);
  678                           if(val == CAN_NOT_UNWRAP) {
  679                               return CAN_NOT_UNWRAP;
  680                           }
  681                           Array.set(array, i, val);
  682                       }
  683                   } finally {
  684                       recursionStops.remove(model);
  685                   }
  686                   return array;
  687               }
  688               // array classes are final, no other conversion will work
  689               return CAN_NOT_UNWRAP;
  690           }
  691           
  692           // Allow one-char strings to be coerced to characters
  693           if(isChar || hint == CHARACTER_CLASS) {
  694               if(model instanceof TemplateScalarModel) {
  695                   String s = ((TemplateScalarModel)model).getAsString();
  696                   if(s.length() == 1) {
  697                       return new Character(s.charAt(0));
  698                   }
  699               }
  700               // Character is final, no other conversion will work
  701               return CAN_NOT_UNWRAP;
  702           }
  703   
  704           if(DATE_CLASS.isAssignableFrom(hint)) {
  705               if(model instanceof TemplateDateModel) {
  706                   Date date = ((TemplateDateModel)model).getAsDate();
  707                   if(hint.isInstance(date)) {
  708                       return date;
  709                   }
  710               }
  711           }
  712           
  713           // Translation of generic template models to POJOs. Since hint was of
  714           // no help initially, now use an admittedly arbitrary order of 
  715           // interfaces. Note we still test for isInstance and isAssignableFrom
  716           // to guarantee we return a compatible value. 
  717           if(model instanceof TemplateNumberModel) {
  718               Number number = ((TemplateNumberModel)model).getAsNumber();
  719               if(hint.isInstance(number)) {
  720                   return number;
  721               }
  722           }
  723           if(model instanceof TemplateDateModel) {
  724               Date date = ((TemplateDateModel)model).getAsDate();
  725               if(hint.isInstance(date)) {
  726                   return date;
  727               }
  728           }
  729           if(model instanceof TemplateScalarModel && 
  730                   hint.isAssignableFrom(STRING_CLASS)) {
  731               return ((TemplateScalarModel)model).getAsString();
  732           }
  733           if(model instanceof TemplateBooleanModel && 
  734                   hint.isAssignableFrom(BOOLEAN_CLASS)) {
  735               return ((TemplateBooleanModel)model).getAsBoolean() 
  736               ? Boolean.TRUE : Boolean.FALSE;
  737           }
  738           if(model instanceof TemplateHashModel && hint.isAssignableFrom(
  739                   HASHADAPTER_CLASS)) {
  740               return new HashAdapter((TemplateHashModel)model, this);
  741           }
  742           if(model instanceof TemplateSequenceModel 
  743                   && hint.isAssignableFrom(SEQUENCEADAPTER_CLASS)) {
  744               return new SequenceAdapter((TemplateSequenceModel)model, this);
  745           }
  746           if(model instanceof TemplateCollectionModel && 
  747                   hint.isAssignableFrom(SETADAPTER_CLASS)) {
  748               return new SetAdapter((TemplateCollectionModel)model, this);
  749           }
  750   
  751           // Last ditch effort - is maybe the model itself instance of the 
  752           // required type?
  753           if(hint.isInstance(model)) {
  754               return model;
  755           }
  756           
  757           return CAN_NOT_UNWRAP;
  758       }
  759   
  760       private static Number convertUnwrappedNumber(Class hint, Number number)
  761       {
  762           if(hint == Integer.TYPE || hint == Integer.class) {
  763               return number instanceof Integer ? (Integer)number : 
  764                   new Integer(number.intValue());
  765           }
  766           if(hint == Long.TYPE || hint == Long.class) {
  767               return number instanceof Long ? (Long)number : 
  768                   new Long(number.longValue());
  769           }
  770           if(hint == Float.TYPE || hint == Float.class) {
  771               return number instanceof Float ? (Float)number : 
  772                   new Float(number.longValue());
  773           }
  774           if(hint == Double.TYPE 
  775                   || hint == Double.class) {
  776               return number instanceof Double ? (Double)number : 
  777                   new Double(number.longValue());
  778           }
  779           if(hint == Byte.TYPE || hint == Byte.class) {
  780               return number instanceof Byte ? (Byte)number : 
  781                   new Byte(number.byteValue());
  782           }
  783           if(hint == Short.TYPE || hint == Short.class) {
  784               return number instanceof Short ? (Short)number : 
  785                   new Short(number.shortValue());
  786           }
  787           if(hint == BigInteger.class) {
  788               return number instanceof BigInteger ? number : 
  789                   new BigInteger(number.toString());
  790           }
  791           if(hint == BigDecimal.class) {
  792               if(number instanceof BigDecimal) {
  793                   return number;
  794               }
  795               if(number instanceof BigInteger) {
  796                   return new BigDecimal((BigInteger)number);
  797               }
  798               if(number instanceof Long) {
  799                   // Because we can't represent long accurately as a 
  800                   // double
  801                   return new BigDecimal(number.toString());
  802               }
  803               return new BigDecimal(number.doubleValue());
  804           }
  805           // Handle nonstandard Number subclasses as well as directly 
  806           // java.lang.Number too
  807           if(hint.isInstance(number)) {
  808               return number;
  809           }
  810           return null;
  811       }
  812       
  813       /**
  814        * Invokes the specified method, wrapping the return value. The specialty
  815        * of this method is that if the return value is null, and the return type
  816        * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
  817        * @param object the object to invoke the method on
  818        * @param method the method to invoke 
  819        * @param args the arguments to the method
  820        * @return the wrapped return value of the method.
  821        * @throws InvocationTargetException if the invoked method threw an exception
  822        * @throws IllegalAccessException if the method can't be invoked due to an
  823        * access restriction. 
  824        * @throws TemplateModelException if the return value couldn't be wrapped
  825        * (this can happen if the wrapper has an outer identity or is subclassed,
  826        * and the outer identity or the subclass throws an exception. Plain
  827        * BeansWrapper never throws TemplateModelException).
  828        */
  829       TemplateModel invokeMethod(Object object, Method method, Object[] args)
  830       throws
  831           InvocationTargetException,
  832           IllegalAccessException,
  833           TemplateModelException
  834       {
  835           Object retval = method.invoke(object, args);
  836           return 
  837               method.getReturnType() == Void.TYPE 
  838               ? TemplateModel.NOTHING
  839               : getOuterIdentity().wrap(retval); 
  840       }
  841   
  842      /**
  843        * Returns a hash model that represents the so-called class static models.
  844        * Every class static model is itself a hash through which you can call
  845        * static methods on the specified class. To obtain a static model for a
  846        * class, get the element of this hash with the fully qualified class name.
  847        * For example, if you place this hash model inside the root data model
  848        * under name "statics", you can use i.e. <code>statics["java.lang.
  849        * System"]. currentTimeMillis()</code> to call the {@link 
  850        * java.lang.System#currentTimeMillis()} method.
  851        * @return a hash model whose keys are fully qualified class names, and
  852        * that returns hash models whose elements are the static models of the
  853        * classes.
  854        */
  855       public TemplateHashModel getStaticModels()
  856       {
  857           return staticModels;
  858       }
  859       
  860       
  861       /**
  862        * Returns a hash model that represents the so-called class enum models.
  863        * Every class' enum model is itself a hash through which you can access
  864        * enum value declared by the specified class, assuming that class is an
  865        * enumeration. To obtain an enum model for a class, get the element of this
  866        * hash with the fully qualified class name. For example, if you place this 
  867        * hash model inside the root data model under name "enums", you can use 
  868        * i.e. <code>statics["java.math.RoundingMode"].UP</code> to access the 
  869        * {@link java.math.RoundingMode#UP} value.
  870        * @return a hash model whose keys are fully qualified class names, and
  871        * that returns hash models whose elements are the enum models of the
  872        * classes.
  873        * @throws UnsupportedOperationException if this method is invoked on a 
  874        * pre-1.5 JRE, as Java enums aren't supported there.
  875        */
  876       public TemplateHashModel getEnumModels() {
  877           if(enumModels == null) {
  878               throw new UnsupportedOperationException(
  879                       "Enums not supported on pre-1.5 JRE");
  880           }
  881           return enumModels;
  882       }
  883   
  884       public Object newInstance(Class clazz, List arguments)
  885       throws
  886           TemplateModelException
  887       {
  888           try
  889           {
  890               introspectClass(clazz);
  891               Map classInfo = (Map)classCache.get(clazz);
  892               Object ctors = classInfo.get(CONSTRUCTORS);
  893               if(ctors == null)
  894               {
  895                   throw new TemplateModelException("Class " + clazz.getName() + 
  896                           " has no public constructors.");
  897               }
  898               Constructor ctor = null;
  899               Object[] objargs;
  900               if(ctors instanceof SimpleMemberModel)
  901               {
  902                   SimpleMemberModel smm = (SimpleMemberModel)ctors;
  903                   ctor = (Constructor)smm.getMember();
  904                   objargs = smm.unwrapArguments(arguments, this);
  905               }
  906               else if(ctors instanceof MethodMap)
  907               {
  908                   MethodMap methodMap = (MethodMap)ctors; 
  909                   MemberAndArguments maa = 
  910                       methodMap.getMemberAndArguments(arguments);
  911                   objargs = maa.getArgs();
  912                   ctor = (Constructor)maa.getMember();
  913               }
  914               else
  915               {
  916                   // Cannot happen
  917                   throw new Error();
  918               }
  919               return ctor.newInstance(objargs);
  920           }
  921           catch (TemplateModelException e)
  922           {
  923               throw e;
  924           }
  925           catch (Exception e)
  926           {
  927               throw new TemplateModelException(
  928                       "Could not create instance of class " + clazz.getName(), e);
  929           }
  930       }
  931       
  932       void introspectClass(Class clazz)
  933       {
  934           synchronized(classCache)
  935           {
  936               if(!classCache.containsKey(clazz))
  937               {
  938                   introspectClassInternal(clazz);
  939               }
  940           }
  941       }
  942   
  943       private void introspectClassInternal(Class clazz)
  944       {
  945           String className = clazz.getName();
  946           if(cachedClassNames.contains(className))
  947           {
  948               if(logger.isInfoEnabled())
  949               {
  950                   logger.info("Detected a reloaded class [" + className + 
  951                           "]. Clearing BeansWrapper caches.");
  952               }
  953               // Class reload detected, throw away caches
  954               classCache.clear();
  955               cachedClassNames = new HashSet();
  956               synchronized(this)
  957               {
  958                   modelCache.clearCache();
  959               }
  960               staticModels.clearCache();
  961               if(enumModels != null) {
  962                   enumModels.clearCache();
  963               }
  964           }
  965           classCache.put(clazz, populateClassMap(clazz));
  966           cachedClassNames.add(className);
  967       }
  968   
  969       Map getClassKeyMap(Class clazz)
  970       {
  971           Map map;
  972           synchronized(classCache)
  973           {
  974               map = (Map)classCache.get(clazz);
  975               if(map == null)
  976               {
  977                   introspectClassInternal(clazz);
  978                   map = (Map)classCache.get(clazz);
  979               }
  980           }
  981           return map;
  982       }
  983   
  984       /**
  985        * Returns the number of introspected methods/properties that should
  986        * be available via the TemplateHashModel interface. Affected by the
  987        * {@link #setMethodsShadowItems(boolean)} and {@link
  988        * #setExposureLevel(int)} settings.
  989        */
  990       int keyCount(Class clazz)
  991       {
  992           Map map = getClassKeyMap(clazz);
  993           int count = map.size();
  994           if (map.containsKey(CONSTRUCTORS))
  995               count--;
  996           if (map.containsKey(GENERIC_GET_KEY))
  997               count--;
  998           if (map.containsKey(ARGTYPES))
  999               count--;
 1000           return count;
 1001       }
 1002   
 1003       /**
 1004        * Returns the Set of names of introspected methods/properties that
 1005        * should be available via the TemplateHashModel interface. Affected
 1006        * by the {@link #setMethodsShadowItems(boolean)} and {@link
 1007        * #setExposureLevel(int)} settings.
 1008        */
 1009       Set keySet(Class clazz)
 1010       {
 1011           Set set = new HashSet(getClassKeyMap(clazz).keySet());
 1012           set.remove(CONSTRUCTORS);
 1013           set.remove(GENERIC_GET_KEY);
 1014           set.remove(ARGTYPES);
 1015           return set;
 1016       }
 1017       
 1018       /**
 1019        * Populates a map with property and method descriptors for a specified
 1020        * class. If any property or method descriptors specifies a read method
 1021        * that is not accessible, replaces it with appropriate accessible method
 1022        * from a superclass or interface.
 1023        */
 1024       private Map populateClassMap(Class clazz)
 1025       {
 1026           // Populate first from bean info
 1027           Map map = populateClassMapWithBeanInfo(clazz);
 1028           // Next add constructors
 1029           try
 1030           {
 1031               Constructor[] ctors = clazz.getConstructors();
 1032               if(ctors.length == 1)
 1033               {
 1034                   Constructor ctor = ctors[0];
 1035                   map.put(CONSTRUCTORS, new SimpleMemberModel(ctor, ctor.getParameterTypes()));
 1036               }
 1037               else if(ctors.length > 1)
 1038               {
 1039                   MethodMap ctorMap = new MethodMap("<init>", this);
 1040                   for (int i = 0; i < ctors.length; i++)
 1041                   {
 1042                       ctorMap.addMember(ctors[i]);
 1043                   }
 1044                   map.put(CONSTRUCTORS, ctorMap);
 1045               }
 1046           }
 1047           catch(SecurityException e)
 1048           {
 1049               logger.warn("Canont discover constructors for class " + 
 1050                       clazz.getName(), e);
 1051           }
 1052           switch(map.size())
 1053           {
 1054               case 0:
 1055               {
 1056                   map = Collections12.EMPTY_MAP;
 1057                   break; 
 1058               }
 1059               case 1:
 1060               {
 1061                   Map.Entry e = (Map.Entry)map.entrySet().iterator().next();
 1062                   map = Collections12.singletonMap(e.getKey(), e.getValue());
 1063                   break;
 1064               }
 1065           }
 1066           return map;
 1067       }
 1068   
 1069       private Map populateClassMapWithBeanInfo(Class clazz)
 1070       {
 1071           Map classMap = new HashMap();
 1072           if(exposeFields)
 1073           {
 1074               Field[] fields = clazz.getFields();
 1075               for (int i = 0; i < fields.length; i++)
 1076               {
 1077                   Field field = fields[i];
 1078                   if((field.getModifiers() & Modifier.STATIC) == 0)
 1079                   {
 1080                       classMap.put(field.getName(), field);
 1081                   }
 1082               }
 1083           }
 1084           Map accessibleMethods = discoverAccessibleMethods(clazz);
 1085           Method genericGet = (Method)accessibleMethods.get(MethodSignature.GET_STRING_SIGNATURE);
 1086           if(genericGet == null)
 1087           {
 1088               genericGet = (Method)accessibleMethods.get(MethodSignature.GET_OBJECT_SIGNATURE);
 1089           }
 1090           if(genericGet != null)
 1091           {
 1092               classMap.put(GENERIC_GET_KEY, genericGet);
 1093           }
 1094           if(exposureLevel == EXPOSE_NOTHING)
 1095           {
 1096               return classMap;
 1097           }
 1098           
 1099           try
 1100           {
 1101               BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
 1102               PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors();
 1103               MethodDescriptor[] mda = beanInfo.getMethodDescriptors();
 1104   
 1105               for(int i = pda.length - 1; i >= 0; --i) {
 1106                   PropertyDescriptor pd = pda[i];
 1107                   if(pd instanceof IndexedPropertyDescriptor) {
 1108                       IndexedPropertyDescriptor ipd = 
 1109                           (IndexedPropertyDescriptor)pd;
 1110                       Method readMethod = ipd.getIndexedReadMethod();
 1111                       Method publicReadMethod = getAccessibleMethod(readMethod, 
 1112                               accessibleMethods);
 1113                       if(publicReadMethod != null && isSafeMethod(publicReadMethod)) {
 1114                           try {
 1115                               if(readMethod != publicReadMethod) {
 1116                                   ipd = new IndexedPropertyDescriptor(
 1117                                           ipd.getName(), ipd.getReadMethod(), 
 1118                                           ipd.getWriteMethod(), publicReadMethod, 
 1119                                           ipd.getIndexedWriteMethod());
 1120                               }
 1121                               classMap.put(ipd.getName(), ipd);
 1122                               getArgTypes(classMap).put(publicReadMethod, 
 1123                                       publicReadMethod.getParameterTypes());
 1124                           }
 1125                           catch(IntrospectionException e) {
 1126                               logger.warn("Couldn't properly perform introspection", e);
 1127                           }
 1128                       }
 1129                   }
 1130                   else {
 1131                       Method readMethod = pd.getReadMethod();
 1132                       Method publicReadMethod = getAccessibleMethod(readMethod, accessibleMethods);
 1133                       if(publicReadMethod != null && isSafeMethod(publicReadMethod)) {
 1134                           try {
 1135                               if(readMethod != publicReadMethod) {
 1136                                   pd = new PropertyDescriptor(pd.getName(), 
 1137                                           publicReadMethod, pd.getWriteMethod());
 1138                                   pd.setReadMethod(publicReadMethod);
 1139                               }
 1140                               classMap.put(pd.getName(), pd);
 1141                           }
 1142                           catch(IntrospectionException e)
 1143                           {
 1144                               logger.warn("Couldn't properly perform introspection", e);
 1145                           }
 1146                       }
 1147                   }
 1148               }
 1149               if(exposureLevel < EXPOSE_PROPERTIES_ONLY)
 1150               {
 1151                   for(int i = mda.length - 1; i >= 0; --i)
 1152                   {
 1153                       MethodDescriptor md = mda[i];
 1154                       Method method = md.getMethod();
 1155                       Method publicMethod = getAccessibleMethod(method, accessibleMethods);
 1156                       if(publicMethod != null && isSafeMethod(publicMethod))
 1157                       {
 1158                           String name = md.getName();
 1159                           Object previous = classMap.get(name);
 1160                           if(previous instanceof Method)
 1161                           {
 1162                               // Overloaded method - replace method with a method map
 1163                               MethodMap methodMap = new MethodMap(name, this);
 1164                               methodMap.addMember((Method)previous);
 1165                               methodMap.addMember(publicMethod);
 1166                               classMap.put(name, methodMap);
 1167                               // remove parameter type information
 1168                               getArgTypes(classMap).remove(previous);
 1169                           }
 1170                           else if(previous instanceof MethodMap)
 1171                           {
 1172                               // Already overloaded method - add new overload
 1173                               ((MethodMap)previous).addMember(publicMethod);
 1174                           }
 1175                           else
 1176                           {
 1177                               // Simple method (this far)
 1178                               classMap.put(name, publicMethod);
 1179                               getArgTypes(classMap).put(publicMethod, 
 1180                                       publicMethod.getParameterTypes());
 1181                           }
 1182                       }
 1183                   }
 1184               }
 1185               return classMap;
 1186           }
 1187           catch(IntrospectionException e)
 1188           {
 1189               logger.warn("Couldn't properly perform introspection", e);
 1190               return new HashMap();
 1191           }
 1192       }
 1193   
 1194       private static Map getArgTypes(Map classMap) {
 1195           Map argTypes = (Map)classMap.get(ARGTYPES);
 1196           if(argTypes == null) {
 1197               argTypes = new HashMap();
 1198               classMap.put(ARGTYPES, argTypes);
 1199           }
 1200           return argTypes;
 1201       }
 1202       
 1203       static Class[] getArgTypes(Map classMap, AccessibleObject methodOrCtor) {
 1204           return (Class[])((Map)classMap.get(ARGTYPES)).get(methodOrCtor);
 1205       }
 1206   
 1207       private static Method getAccessibleMethod(Method m, Map accessibles)
 1208       {
 1209           return m == null ? null : (Method)accessibles.get(new MethodSignature(m));
 1210       }
 1211       
 1212       boolean isSafeMethod(Method method)
 1213       {
 1214           return exposureLevel < EXPOSE_SAFE || !UNSAFE_METHODS.contains(method);
 1215       }
 1216       
 1217       /**
 1218        * Retrieves mapping of methods to accessible methods for a class.
 1219        * In case the class is not public, retrieves methods with same 
 1220        * signature as its public methods from public superclasses and 
 1221        * interfaces (if they exist). Basically upcasts every method to the 
 1222        * nearest accessible method.
 1223        */
 1224       private static Map discoverAccessibleMethods(Class clazz)
 1225       {
 1226           Map map = new HashMap();
 1227           discoverAccessibleMethods(clazz, map);
 1228           return map;
 1229       }
 1230       
 1231       private static void discoverAccessibleMethods(Class clazz, Map map)
 1232       {
 1233           if(Modifier.isPublic(clazz.getModifiers()))
 1234           {
 1235               try
 1236               {
 1237                   Method[] methods = clazz.getMethods();
 1238                   for(int i = 0; i < methods.length; i++)
 1239                   {
 1240                       Method method = methods[i];
 1241                       MethodSignature sig = new MethodSignature(method);
 1242                       map.put(sig, method);
 1243                   }
 1244                   return;
 1245               }
 1246               catch(SecurityException e)
 1247               {
 1248                   logger.warn("Could not discover accessible methods of class " + 
 1249                           clazz.getName() + 
 1250                           ", attemping superclasses/interfaces.", e);
 1251                   // Fall through and attempt to discover superclass/interface 
 1252                   // methods
 1253               }
 1254           }
 1255   
 1256           Class[] interfaces = clazz.getInterfaces();
 1257           for(int i = 0; i < interfaces.length; i++)
 1258           {
 1259               discoverAccessibleMethods(interfaces[i], map);
 1260           }
 1261           Class superclass = clazz.getSuperclass();
 1262           if(superclass != null)
 1263           {
 1264               discoverAccessibleMethods(superclass, map);
 1265           }
 1266       }
 1267   
 1268       private static final class MethodSignature
 1269       {
 1270           private static final MethodSignature GET_STRING_SIGNATURE = 
 1271               new MethodSignature("get", new Class[] { STRING_CLASS });
 1272           private static final MethodSignature GET_OBJECT_SIGNATURE = 
 1273               new MethodSignature("get", new Class[] { OBJECT_CLASS });
 1274   
 1275           private final String name;
 1276           private final Class[] args;
 1277           
 1278           private MethodSignature(String name, Class[] args)
 1279           {
 1280               this.name = name;
 1281               this.args = args;
 1282           }
 1283           
 1284           MethodSignature(Method method)
 1285           {
 1286               this(method.getName(), method.getParameterTypes());
 1287           }
 1288           
 1289           public boolean equals(Object o)
 1290           {
 1291               if(o instanceof MethodSignature)
 1292               {
 1293                   MethodSignature ms = (MethodSignature)o;
 1294                   return ms.name.equals(name) && Arrays.equals(args, ms.args);
 1295               }
 1296               return false;
 1297           }
 1298           
 1299           public int hashCode()
 1300           {
 1301               return name.hashCode() ^ args.length;
 1302           }
 1303       }
 1304       
 1305       private static final Set createUnsafeMethodsSet()
 1306       {
 1307           Properties props = new Properties();
 1308           InputStream in = BeansWrapper.class.getResourceAsStream("unsafeMethods.txt");
 1309           if(in != null)
 1310           {
 1311               String methodSpec = null;
 1312               try
 1313               {
 1314                   try
 1315                   {
 1316                       props.load(in);
 1317                   }
 1318                   finally
 1319                   {
 1320                       in.close();
 1321                   }
 1322                   Set set = new HashSet(props.size() * 4/3, .75f);
 1323                   Map primClasses = createPrimitiveClassesMap();
 1324                   for (Iterator iterator = props.keySet().iterator(); iterator.hasNext();)
 1325                   {
 1326                       methodSpec = (String) iterator.next();
 1327                       try {
 1328                           set.add(parseMethodSpec(methodSpec, primClasses));
 1329                       }
 1330                       catch(ClassNotFoundException e) {
 1331                           if(DEVELOPMENT) {
 1332                               throw e;
 1333                           }
 1334                       }
 1335                       catch(NoSuchMethodException e) {
 1336                           if(DEVELOPMENT) {
 1337                               throw e;
 1338                           }
 1339                       }
 1340                   }
 1341                   return set;
 1342               }
 1343               catch(Exception e)
 1344               {
 1345                   throw new RuntimeException("Could not load unsafe method " + methodSpec + " " + e.getClass().getName() + " " + e.getMessage());
 1346               }
 1347           }
 1348           return Collections.EMPTY_SET;
 1349       }
 1350                                                                              
 1351       private static Method parseMethodSpec(String methodSpec, Map primClasses)
 1352       throws
 1353           ClassNotFoundException,
 1354           NoSuchMethodException
 1355       {
 1356           int brace = methodSpec.indexOf('(');
 1357           int dot = methodSpec.lastIndexOf('.', brace);
 1358           Class clazz = ClassUtil.forName(methodSpec.substring(0, dot));
 1359           String methodName = methodSpec.substring(dot + 1, brace);
 1360           String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
 1361           StringTokenizer tok = new StringTokenizer(argSpec, ",");
 1362           int argcount = tok.countTokens();
 1363           Class[] argTypes = new Class[argcount];
 1364           for (int i = 0; i < argcount; i++)
 1365           {
 1366               String argClassName = tok.nextToken();
 1367               argTypes[i] = (Class)primClasses.get(argClassName);
 1368               if(argTypes[i] == null)
 1369               {
 1370                   argTypes[i] = ClassUtil.forName(argClassName);
 1371               }
 1372           }
 1373           return clazz.getMethod(methodName, argTypes);
 1374       }
 1375   
 1376       private static Map createPrimitiveClassesMap()
 1377       {
 1378           Map map = new HashMap();
 1379           map.put("boolean", Boolean.TYPE);
 1380           map.put("byte", Byte.TYPE);
 1381           map.put("char", Character.TYPE);
 1382           map.put("short", Short.TYPE);
 1383           map.put("int", Integer.TYPE);
 1384           map.put("long", Long.TYPE);
 1385           map.put("float", Float.TYPE);
 1386           map.put("double", Double.TYPE);
 1387           return map;
 1388       }
 1389   
 1390   
 1391       /**
 1392        * Converts any {@link BigDecimal}s in the passed array to the type of
 1393        * the corresponding formal argument of the method.
 1394        */
 1395       public static void coerceBigDecimals(AccessibleObject callable, Object[] args)
 1396       {
 1397           Class[] formalTypes = null;
 1398           for(int i = 0; i < args.length; ++i) {
 1399               Object arg = args[i];
 1400               if(arg instanceof BigDecimal) {
 1401                   if(formalTypes == null) {
 1402                       if(callable instanceof Method) {
 1403                           formalTypes = ((Method)callable).getParameterTypes();
 1404                       }
 1405                       else if(callable instanceof Constructor) {
 1406                           formalTypes = ((Constructor)callable).getParameterTypes();
 1407                       }
 1408                       else {
 1409                           throw new IllegalArgumentException("Expected method or "
 1410                                   + " constructor; callable is " + 
 1411                                   callable.getClass().getName());
 1412                       }
 1413                   }
 1414                   args[i] = coerceBigDecimal((BigDecimal)arg, formalTypes[i]);
 1415               }
 1416           }
 1417       }
 1418       
 1419       /**
 1420        * Converts any {@link BigDecimal}s in the passed array to the type of
 1421        * the corresponding formal argument of the method.
 1422        */
 1423       public static void coerceBigDecimals(Class[] formalTypes, Object[] args)
 1424       {
 1425           int typeLen = formalTypes.length;
 1426           int argsLen = args.length;
 1427           int min = Math.min(typeLen, argsLen);
 1428           for(int i = 0; i < min; ++i) {
 1429               Object arg = args[i];
 1430               if(arg instanceof BigDecimal) {
 1431                   args[i] = coerceBigDecimal((BigDecimal)arg, formalTypes[i]);
 1432               }
 1433           }
 1434           if(argsLen > typeLen) {
 1435               Class varArgType = formalTypes[typeLen - 1];
 1436               for(int i = typeLen; i < argsLen; ++i) {
 1437                   Object arg = args[i];
 1438                   if(arg instanceof BigDecimal) {
 1439                       args[i] = coerceBigDecimal((BigDecimal)arg, varArgType);
 1440                   }
 1441               }
 1442           }
 1443       }
 1444       
 1445       public static Object coerceBigDecimal(BigDecimal bd, Class formalType) {
 1446           // int is expected in most situations, so we check it first
 1447           if(formalType == Integer.TYPE || formalType == Integer.class) {
 1448               return new Integer(bd.intValue());
 1449           }
 1450           else if(formalType == Double.TYPE || formalType == Double.class) {
 1451               return new Double(bd.doubleValue());
 1452           }
 1453           else if(formalType == Long.TYPE || formalType == Long.class) {
 1454               return new Long(bd.longValue());
 1455           }
 1456           else if(formalType == Float.TYPE || formalType == Float.class) {
 1457               return new Float(bd.floatValue());
 1458           }
 1459           else if(formalType == Short.TYPE || formalType == Short.class) {
 1460               return new Short(bd.shortValue());
 1461           }
 1462           else if(formalType == Byte.TYPE || formalType == Byte.class) {
 1463               return new Byte(bd.byteValue());
 1464           }
 1465           else if(BIGINTEGER_CLASS.isAssignableFrom(formalType)) {
 1466               return bd.toBigInteger();
 1467           }
 1468           return bd;
 1469       }
 1470   
 1471       private static ClassBasedModelFactory createEnumModels(BeansWrapper wrapper) {
 1472           if(ENUMS_MODEL_CTOR != null) {
 1473               try {
 1474                   return (ClassBasedModelFactory)ENUMS_MODEL_CTOR.newInstance(
 1475                           new Object[] { wrapper });
 1476               } catch(Exception e) {
 1477                   throw new UndeclaredThrowableException(e);
 1478               }
 1479           } else {
 1480               return null;
 1481           }
 1482       }
 1483       
 1484       private static Constructor enumsModelCtor() {
 1485           try {
 1486               // Check if Enums are available on this platform
 1487               Class.forName("java.lang.Enum");
 1488               // If they are, return the appropriate constructor for enum models
 1489               return Class.forName(
 1490                   "freemarker.ext.beans.EnumModels").getDeclaredConstructor(
 1491                           new Class[] { BeansWrapper.class });
 1492           }
 1493           catch(Exception e) {
 1494               // Otherwise, return null
 1495               return null;
 1496           }
 1497       }
 1498   }

Save This Page
Home » freemarker-2.3.13 » freemarker.ext.beans » [javadoc | source]