001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.ext.beans;
054:
055: import java.beans.IndexedPropertyDescriptor;
056: import java.beans.PropertyDescriptor;
057: import java.lang.reflect.Field;
058: import java.lang.reflect.InvocationTargetException;
059: import java.lang.reflect.Method;
060: import java.util.ArrayList;
061: import java.util.Collection;
062: import java.util.HashMap;
063: import java.util.List;
064: import java.util.Map;
065: import java.util.Set;
066:
067: import freemarker.core.CollectionAndSequence;
068: import freemarker.ext.util.ModelFactory;
069: import freemarker.ext.util.WrapperTemplateModel;
070: import freemarker.log.Logger;
071: import freemarker.template.AdapterTemplateModel;
072: import freemarker.template.ObjectWrapper;
073: import freemarker.template.SimpleScalar;
074: import freemarker.template.SimpleSequence;
075: import freemarker.template.TemplateCollectionModel;
076: import freemarker.template.TemplateHashModelEx;
077: import freemarker.template.TemplateModel;
078: import freemarker.template.TemplateModelException;
079: import freemarker.template.TemplateModelIterator;
080: import freemarker.template.TemplateScalarModel;
081:
082: /**
083: * A class that will wrap an arbitrary object into {@link freemarker.template.TemplateHashModel}
084: * interface allowing calls to arbitrary property getters and invocation of
085: * accessible methods on the object from a template using the
086: * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to
087: * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to
088: * access indexed properties. It uses Beans {@link java.beans.Introspector}
089: * to dynamically discover the properties and methods.
090: * @author Attila Szegedi
091: * @version $Id: BeanModel.java,v 1.49.2.4 2006/11/12 10:20:37 szegedia Exp $
092: */
093:
094: public class BeanModel implements TemplateHashModelEx,
095: AdapterTemplateModel, WrapperTemplateModel {
096: private static final Logger logger = Logger
097: .getLogger("freemarker.beans");
098: protected final Object object;
099: protected final BeansWrapper wrapper;
100:
101: // We use this to represent an unknown value as opposed to known value of null (JR)
102: private static final TemplateModel UNKNOWN = new SimpleScalar(
103: "UNKNOWN");
104:
105: static final ModelFactory FACTORY = new ModelFactory() {
106: public TemplateModel create(Object object, ObjectWrapper wrapper) {
107: return new BeanModel(object, (BeansWrapper) wrapper);
108: }
109: };
110:
111: // Cached template models that implement member properties and methods for this
112: // instance. Keys are FeatureDescriptor instances (from classCache values),
113: // values are either ReflectionMethodModels/ReflectionScalarModels
114: private final HashMap memberMap = new HashMap();
115:
116: /**
117: * Creates a new model that wraps the specified object. Note that there are
118: * specialized subclasses of this class for wrapping arrays, collections,
119: * enumeration, iterators, and maps. Note also that the superclass can be
120: * used to wrap String objects if only scalar functionality is needed. You
121: * can also choose to delegate the choice over which model class is used for
122: * wrapping to {@link BeansWrapper#wrap(Object)}.
123: * @param object the object to wrap into a model.
124: * @param wrapper the {@link BeansWrapper} associated with this model.
125: * Every model has to have an associated {@link BeansWrapper} instance. The
126: * model gains many attributes from its wrapper, including the caching
127: * behavior, method exposure level, method-over-item shadowing policy etc.
128: */
129: public BeanModel(Object object, BeansWrapper wrapper) {
130: this .object = object;
131: this .wrapper = wrapper;
132: if (object == null) {
133: return;
134: }
135: wrapper.introspectClass(object.getClass());
136: }
137:
138: /**
139: * Uses Beans introspection to locate a property or method with name
140: * matching the key name. If a method or property is found, it is wrapped
141: * into {@link freemarker.template.TemplateMethodModelEx} (for a method or
142: * indexed property), or evaluated on-the-fly and the return value wrapped
143: * into appropriate model (for a simple property) Models for various
144: * properties and methods are cached on a per-class basis, so the costly
145: * introspection is performed only once per property or method of a class.
146: * (Side-note: this also implies that any class whose method has been called
147: * will be strongly referred to by the framework and will not become
148: * unloadable until this class has been unloaded first. Normally this is not
149: * an issue, but can be in a rare scenario where you create many classes on-
150: * the-fly. Also, as the cache grows with new classes and methods introduced
151: * to the framework, it may appear as if it were leaking memory. The
152: * framework does, however detect class reloads (if you happen to be in an
153: * environment that does this kind of things--servlet containers do it when
154: * they reload a web application) and flushes the cache. If no method or
155: * property matching the key is found, the framework will try to invoke
156: * methods with signature
157: * <tt>non-void-return-type get(java.lang.String)</tt>,
158: * then <tt>non-void-return-type get(java.lang.Object)</tt>, or
159: * alternatively (if the wrapped object is a resource bundle)
160: * <tt>Object getObject(java.lang.String)</tt>.
161: * @throws TemplateModelException if there was no property nor method nor
162: * a generic <tt>get</tt> method to invoke.
163: */
164: public TemplateModel get(String key) throws TemplateModelException {
165: Class clazz = object.getClass();
166: Map classInfo = wrapper.getClassKeyMap(clazz);
167: TemplateModel retval = null;
168:
169: try {
170: if (wrapper.isMethodsShadowItems()) {
171: Object fd = classInfo.get(key);
172: if (fd != null) {
173: retval = invokeThroughDescriptor(fd, classInfo);
174: } else {
175: retval = invokeGenericGet(classInfo, clazz, key);
176: }
177: } else {
178: TemplateModel model = invokeGenericGet(classInfo,
179: clazz, key);
180: final TemplateModel nullModel = wrapper.wrap(null);
181: if (model != nullModel && model != UNKNOWN) {
182: return model;
183: }
184: Object fd = classInfo.get(key);
185: if (fd != null) {
186: retval = invokeThroughDescriptor(fd, classInfo);
187: if (retval == UNKNOWN && model == nullModel) {
188: // This is the (somewhat subtle) case where the generic get() returns null
189: // and we have no bean info, so we respect the fact that
190: // the generic get() returns null and return null. (JR)
191: retval = nullModel;
192: }
193: }
194: }
195: if (retval == UNKNOWN) {
196: if (wrapper.isStrict()) {
197: throw new InvalidPropertyException(
198: "No such bean property: " + key);
199: } else if (logger.isDebugEnabled()) {
200: logNoSuchKey(key, classInfo);
201: }
202: retval = wrapper.wrap(null);
203: }
204: return retval;
205: } catch (TemplateModelException e) {
206: throw e;
207: } catch (Exception e) {
208: throw new TemplateModelException("get(" + key
209: + ") failed on " + "instance of "
210: + object.getClass().getName(), e);
211: }
212: }
213:
214: private void logNoSuchKey(String key, Map keyMap) {
215: logger.debug("Key '" + key + "' was not found on instance of "
216: + object.getClass().getName()
217: + ". Introspection information for " + "the class is: "
218: + keyMap);
219: }
220:
221: /**
222: * Whether the model has a plain get(String) or get(Object) method
223: */
224:
225: protected boolean hasPlainGetMethod() {
226: return wrapper.getClassKeyMap(object.getClass()).get(
227: BeansWrapper.GENERIC_GET_KEY) != null;
228: }
229:
230: private TemplateModel invokeThroughDescriptor(Object desc,
231: Map classInfo) throws IllegalAccessException,
232: InvocationTargetException, TemplateModelException {
233: // See if this particular instance has a cached implementation
234: // for the requested feature descriptor
235: TemplateModel member = null;
236: synchronized (memberMap) {
237: member = (TemplateModel) memberMap.get(desc);
238: }
239:
240: if (member != null)
241: return member;
242:
243: TemplateModel retval = UNKNOWN;
244: if (desc instanceof IndexedPropertyDescriptor) {
245: Method readMethod = ((IndexedPropertyDescriptor) desc)
246: .getIndexedReadMethod();
247: retval = member = new SimpleMethodModel(object, readMethod,
248: BeansWrapper.getArgTypes(classInfo, readMethod),
249: wrapper);
250: } else if (desc instanceof PropertyDescriptor) {
251: PropertyDescriptor pd = (PropertyDescriptor) desc;
252: retval = wrapper.invokeMethod(object, pd.getReadMethod(),
253: null);
254: // (member == null) condition remains, as we don't cache these
255: } else if (desc instanceof Field) {
256: retval = wrapper.wrap(((Field) desc).get(object));
257: // (member == null) condition remains, as we don't cache these
258: } else if (desc instanceof Method) {
259: Method method = (Method) desc;
260: retval = member = new SimpleMethodModel(object, method,
261: BeansWrapper.getArgTypes(classInfo, method),
262: wrapper);
263: } else if (desc instanceof MethodMap) {
264: retval = member = new OverloadedMethodModel(object,
265: (MethodMap) desc, wrapper);
266: }
267:
268: // If new cacheable member was created, cache it
269: if (member != null) {
270: synchronized (memberMap) {
271: memberMap.put(desc, member);
272: }
273: }
274: return retval;
275: }
276:
277: protected TemplateModel invokeGenericGet(Map keyMap, Class clazz,
278: String key) throws IllegalAccessException,
279: InvocationTargetException, TemplateModelException {
280: Method genericGet = (Method) keyMap
281: .get(BeansWrapper.GENERIC_GET_KEY);
282: if (genericGet == null)
283: return UNKNOWN;
284:
285: return wrapper.invokeMethod(object, genericGet,
286: new Object[] { key });
287: }
288:
289: protected TemplateModel wrap(Object obj)
290: throws TemplateModelException {
291: return wrapper.getOuterIdentity().wrap(obj);
292: }
293:
294: protected Object unwrap(TemplateModel model)
295: throws TemplateModelException {
296: return wrapper.unwrap(model);
297: }
298:
299: /**
300: * Tells whether the model is empty. It is empty if either the wrapped
301: * object is null, or it is a Boolean with false value.
302: */
303: public boolean isEmpty() {
304: if (object instanceof String) {
305: return ((String) object).length() == 0;
306: }
307: if (object instanceof Collection) {
308: return ((Collection) object).isEmpty();
309: }
310: if (object instanceof Map) {
311: return ((Map) object).isEmpty();
312: }
313: return object == null || Boolean.FALSE.equals(object);
314: }
315:
316: public Object getAdaptedObject(Class hint) {
317: return object;
318: }
319:
320: public Object getWrappedObject() {
321: return object;
322: }
323:
324: public int size() {
325: return wrapper.keyCount(object.getClass());
326: }
327:
328: public TemplateCollectionModel keys() {
329: return new CollectionAndSequence(new SimpleSequence(keySet(),
330: wrapper));
331: }
332:
333: public TemplateCollectionModel values()
334: throws TemplateModelException {
335: List values = new ArrayList(size());
336: TemplateModelIterator it = keys().iterator();
337: while (it.hasNext()) {
338: String key = ((TemplateScalarModel) it.next())
339: .getAsString();
340: values.add(get(key));
341: }
342: return new CollectionAndSequence(new SimpleSequence(values,
343: wrapper));
344: }
345:
346: public String toString() {
347: return object.toString();
348: }
349:
350: /**
351: * Helper method to support TemplateHashModelEx. Returns the Set of
352: * Strings which are available via the TemplateHashModel
353: * interface. Subclasses that override <tt>invokeGenericGet</tt> to
354: * provide additional hash keys should also override this method.
355: */
356: protected Set keySet() {
357: return wrapper.keySet(object.getClass());
358: }
359: }
|