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.IndexedPropertyDescriptor; 56 import java.beans.PropertyDescriptor; 57 import java.lang.reflect.Field; 58 import java.lang.reflect.InvocationTargetException; 59 import java.lang.reflect.Method; 60 import java.util.ArrayList; 61 import java.util.Collection; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Set; 66 67 import freemarker.core.CollectionAndSequence; 68 import freemarker.ext.util.ModelFactory; 69 import freemarker.ext.util.WrapperTemplateModel; 70 import freemarker.log.Logger; 71 import freemarker.template.AdapterTemplateModel; 72 import freemarker.template.ObjectWrapper; 73 import freemarker.template.SimpleScalar; 74 import freemarker.template.SimpleSequence; 75 import freemarker.template.TemplateCollectionModel; 76 import freemarker.template.TemplateHashModelEx; 77 import freemarker.template.TemplateModel; 78 import freemarker.template.TemplateModelException; 79 import freemarker.template.TemplateModelIterator; 80 import freemarker.template.TemplateScalarModel; 81 82 /** 83 * A class that will wrap an arbitrary object into {@link freemarker.template.TemplateHashModel} 84 * interface allowing calls to arbitrary property getters and invocation of 85 * accessible methods on the object from a template using the 86 * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to 87 * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to 88 * access indexed properties. It uses Beans {@link java.beans.Introspector} 89 * to dynamically discover the properties and methods. 90 * @author Attila Szegedi 91 * @version $Id: BeanModel.java,v 1.49.2.4 2006/11/12 10:20:37 szegedia Exp $ 92 */ 93 94 public class BeanModel 95 implements 96 TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel 97 { 98 private static final Logger logger = Logger.getLogger("freemarker.beans"); 99 protected final Object object; 100 protected final BeansWrapper wrapper; 101 102 // We use this to represent an unknown value as opposed to known value of null (JR) 103 private static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN"); 104 105 static final ModelFactory FACTORY = 106 new ModelFactory() 107 { 108 public TemplateModel create(Object object, ObjectWrapper wrapper) 109 { 110 return new BeanModel(object, (BeansWrapper)wrapper); 111 } 112 }; 113 114 // Cached template models that implement member properties and methods for this 115 // instance. Keys are FeatureDescriptor instances (from classCache values), 116 // values are either ReflectionMethodModels/ReflectionScalarModels 117 private HashMap memberMap; 118 119 /** 120 * Creates a new model that wraps the specified object. Note that there are 121 * specialized subclasses of this class for wrapping arrays, collections, 122 * enumeration, iterators, and maps. Note also that the superclass can be 123 * used to wrap String objects if only scalar functionality is needed. You 124 * can also choose to delegate the choice over which model class is used for 125 * wrapping to {@link BeansWrapper#wrap(Object)}. 126 * @param object the object to wrap into a model. 127 * @param wrapper the {@link BeansWrapper} associated with this model. 128 * Every model has to have an associated {@link BeansWrapper} instance. The 129 * model gains many attributes from its wrapper, including the caching 130 * behavior, method exposure level, method-over-item shadowing policy etc. 131 */ 132 public BeanModel(Object object, BeansWrapper wrapper) 133 { 134 this.object = object; 135 this.wrapper = wrapper; 136 if (object == null) { 137 return; 138 } 139 wrapper.introspectClass(object.getClass()); 140 } 141 142 /** 143 * Uses Beans introspection to locate a property or method with name 144 * matching the key name. If a method or property is found, it is wrapped 145 * into {@link freemarker.template.TemplateMethodModelEx} (for a method or 146 * indexed property), or evaluated on-the-fly and the return value wrapped 147 * into appropriate model (for a simple property) Models for various 148 * properties and methods are cached on a per-class basis, so the costly 149 * introspection is performed only once per property or method of a class. 150 * (Side-note: this also implies that any class whose method has been called 151 * will be strongly referred to by the framework and will not become 152 * unloadable until this class has been unloaded first. Normally this is not 153 * an issue, but can be in a rare scenario where you create many classes on- 154 * the-fly. Also, as the cache grows with new classes and methods introduced 155 * to the framework, it may appear as if it were leaking memory. The 156 * framework does, however detect class reloads (if you happen to be in an 157 * environment that does this kind of things--servlet containers do it when 158 * they reload a web application) and flushes the cache. If no method or 159 * property matching the key is found, the framework will try to invoke 160 * methods with signature 161 * <tt>non-void-return-type get(java.lang.String)</tt>, 162 * then <tt>non-void-return-type get(java.lang.Object)</tt>, or 163 * alternatively (if the wrapped object is a resource bundle) 164 * <tt>Object getObject(java.lang.String)</tt>. 165 * @throws TemplateModelException if there was no property nor method nor 166 * a generic <tt>get</tt> method to invoke. 167 */ 168 public TemplateModel get(String key) 169 throws 170 TemplateModelException 171 { 172 Class clazz = object.getClass(); 173 Map classInfo = wrapper.getClassKeyMap(clazz); 174 TemplateModel retval = null; 175 176 try 177 { 178 if(wrapper.isMethodsShadowItems()) 179 { 180 Object fd = classInfo.get(key); 181 if(fd != null) 182 { 183 retval = invokeThroughDescriptor(fd, classInfo); 184 } else { 185 retval = invokeGenericGet(classInfo, clazz, key); 186 } 187 } 188 else 189 { 190 TemplateModel model = invokeGenericGet(classInfo, clazz, key); 191 final TemplateModel nullModel = wrapper.wrap(null); 192 if(model != nullModel && model != UNKNOWN) 193 { 194 return model; 195 } 196 Object fd = classInfo.get(key); 197 if(fd != null) { 198 retval = invokeThroughDescriptor(fd, classInfo); 199 if (retval == UNKNOWN && model == nullModel) { 200 // This is the (somewhat subtle) case where the generic get() returns null 201 // and we have no bean info, so we respect the fact that 202 // the generic get() returns null and return null. (JR) 203 retval = nullModel; 204 } 205 } 206 } 207 if (retval == UNKNOWN) { 208 if (wrapper.isStrict()) { 209 throw new InvalidPropertyException("No such bean property: " + key); 210 } else if (logger.isDebugEnabled()) { 211 logNoSuchKey(key, classInfo); 212 } 213 retval = wrapper.wrap(null); 214 } 215 return retval; 216 } 217 catch(TemplateModelException e) 218 { 219 throw e; 220 } 221 catch(Exception e) 222 { 223 throw new TemplateModelException("get(" + key + ") failed on " + 224 "instance of " + object.getClass().getName(), e); 225 } 226 } 227 228 private void logNoSuchKey(String key, Map keyMap) 229 { 230 logger.debug("Key '" + key + "' was not found on instance of " + 231 object.getClass().getName() + ". Introspection information for " + 232 "the class is: " + keyMap); 233 } 234 235 /** 236 * Whether the model has a plain get(String) or get(Object) method 237 */ 238 239 protected boolean hasPlainGetMethod() { 240 return wrapper.getClassKeyMap(object.getClass()).get(BeansWrapper.GENERIC_GET_KEY) != null; 241 } 242 243 private TemplateModel invokeThroughDescriptor(Object desc, Map classInfo) 244 throws 245 IllegalAccessException, 246 InvocationTargetException, 247 TemplateModelException 248 { 249 // See if this particular instance has a cached implementation 250 // for the requested feature descriptor 251 TemplateModel member; 252 synchronized(this) { 253 if(memberMap != null) { 254 member = (TemplateModel)memberMap.get(desc); 255 } 256 else { 257 member = null; 258 } 259 } 260 261 if(member != null) 262 return member; 263 264 TemplateModel retval = UNKNOWN; 265 if(desc instanceof IndexedPropertyDescriptor) 266 { 267 Method readMethod = 268 ((IndexedPropertyDescriptor)desc).getIndexedReadMethod(); 269 retval = member = 270 new SimpleMethodModel(object, readMethod, 271 BeansWrapper.getArgTypes(classInfo, readMethod), wrapper); 272 } 273 else if(desc instanceof PropertyDescriptor) 274 { 275 PropertyDescriptor pd = (PropertyDescriptor)desc; 276 retval = wrapper.invokeMethod(object, pd.getReadMethod(), null); 277 // (member == null) condition remains, as we don't cache these 278 } 279 else if(desc instanceof Field) 280 { 281 retval = wrapper.wrap(((Field)desc).get(object)); 282 // (member == null) condition remains, as we don't cache these 283 } 284 else if(desc instanceof Method) 285 { 286 Method method = (Method)desc; 287 retval = member = new SimpleMethodModel(object, method, 288 BeansWrapper.getArgTypes(classInfo, method), wrapper); 289 } 290 else if(desc instanceof MethodMap) 291 { 292 retval = member = 293 new OverloadedMethodModel(object, (MethodMap)desc); 294 } 295 296 // If new cacheable member was created, cache it 297 if(member != null) { 298 synchronized(this) { 299 if(memberMap == null) { 300 memberMap = new HashMap(); 301 } 302 memberMap.put(desc, member); 303 } 304 } 305 return retval; 306 } 307 308 protected TemplateModel invokeGenericGet(Map keyMap, Class clazz, String key) 309 throws 310 IllegalAccessException, 311 InvocationTargetException, 312 TemplateModelException 313 { 314 Method genericGet = (Method)keyMap.get(BeansWrapper.GENERIC_GET_KEY); 315 if(genericGet == null) 316 return UNKNOWN; 317 318 return wrapper.invokeMethod(object, genericGet, new Object[] { key }); 319 } 320 321 protected TemplateModel wrap(Object obj) 322 throws TemplateModelException 323 { 324 return wrapper.getOuterIdentity().wrap(obj); 325 } 326 327 protected Object unwrap(TemplateModel model) 328 throws 329 TemplateModelException 330 { 331 return wrapper.unwrap(model); 332 } 333 334 /** 335 * Tells whether the model is empty. It is empty if either the wrapped 336 * object is null, or it is a Boolean with false value. 337 */ 338 public boolean isEmpty() 339 { 340 if (object instanceof String) { 341 return ((String) object).length() == 0; 342 } 343 if (object instanceof Collection) { 344 return ((Collection) object).isEmpty(); 345 } 346 if (object instanceof Map) { 347 return ((Map) object).isEmpty(); 348 } 349 return object == null || Boolean.FALSE.equals(object); 350 } 351 352 public Object getAdaptedObject(Class hint) { 353 return object; 354 } 355 356 public Object getWrappedObject() { 357 return object; 358 } 359 360 public int size() 361 { 362 return wrapper.keyCount(object.getClass()); 363 } 364 365 public TemplateCollectionModel keys() 366 { 367 return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper)); 368 } 369 370 public TemplateCollectionModel values() throws TemplateModelException 371 { 372 List values = new ArrayList(size()); 373 TemplateModelIterator it = keys().iterator(); 374 while (it.hasNext()) { 375 String key = ((TemplateScalarModel)it.next()).getAsString(); 376 values.add(get(key)); 377 } 378 return new CollectionAndSequence(new SimpleSequence(values, wrapper)); 379 } 380 381 public String toString() { 382 return object.toString(); 383 } 384 385 /** 386 * Helper method to support TemplateHashModelEx. Returns the Set of 387 * Strings which are available via the TemplateHashModel 388 * interface. Subclasses that override <tt>invokeGenericGet</tt> to 389 * provide additional hash keys should also override this method. 390 */ 391 protected Set keySet() 392 { 393 return wrapper.keySet(object.getClass()); 394 } 395 }