/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package freemarker.ext.beans;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import freemarker.core.CollectionAndSequence;
import freemarker.core._DelayedFTLTypeDescription;
import freemarker.core._DelayedJQuote;
import freemarker.core._TemplateModelException;
import freemarker.ext.util.ModelFactory;
import freemarker.ext.util.WrapperTemplateModel;
import freemarker.log.Logger;
import freemarker.template.AdapterTemplateModel;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateModelWithAPISupport;
import freemarker.template.TemplateScalarModel;
import freemarker.template.utility.StringUtil;

A class that will wrap an arbitrary object into TemplateHashModel interface allowing calls to arbitrary property getters and invocation of accessible methods on the object from a template using the object.foo to access properties and object.bar(arg1, arg2) to invoke methods on it. You can also use the object.foo[index] syntax to access indexed properties. It uses Beans Introspector to dynamically discover the properties and methods.
/** * A class that will wrap an arbitrary object into {@link freemarker.template.TemplateHashModel} * interface allowing calls to arbitrary property getters and invocation of * accessible methods on the object from a template using the * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to * access indexed properties. It uses Beans {@link java.beans.Introspector} * to dynamically discover the properties and methods. */
public class BeanModel implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport { private static final Logger LOG = Logger.getLogger("freemarker.beans"); protected final Object object; protected final BeansWrapper wrapper; // We use this to represent an unknown value as opposed to known value of null (JR) static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN"); static final ModelFactory FACTORY = new ModelFactory() { public TemplateModel create(Object object, ObjectWrapper wrapper) { return new BeanModel(object, (BeansWrapper) wrapper); } }; // I've tried to use a volatile ConcurrentHashMap field instead of HashMap + synchronized(this), but oddly it was // a bit slower, at least on Java 8 u66. private HashMap<Object, TemplateModel> memberCache;
Creates a new model that wraps the specified object. Note that there are specialized subclasses of this class for wrapping arrays, collections, enumeration, iterators, and maps. Note also that the superclass can be used to wrap String objects if only scalar functionality is needed. You can also choose to delegate the choice over which model class is used for wrapping to BeansWrapper.wrap(Object).
Params:
  • object – the object to wrap into a model.
  • wrapper – the BeansWrapper associated with this model. Every model has to have an associated BeansWrapper instance. The model gains many attributes from its wrapper, including the caching behavior, method exposure level, method-over-item shadowing policy etc.
/** * Creates a new model that wraps the specified object. Note that there are * specialized subclasses of this class for wrapping arrays, collections, * enumeration, iterators, and maps. Note also that the superclass can be * used to wrap String objects if only scalar functionality is needed. You * can also choose to delegate the choice over which model class is used for * wrapping to {@link BeansWrapper#wrap(Object)}. * @param object the object to wrap into a model. * @param wrapper the {@link BeansWrapper} associated with this model. * Every model has to have an associated {@link BeansWrapper} instance. The * model gains many attributes from its wrapper, including the caching * behavior, method exposure level, method-over-item shadowing policy etc. */
public BeanModel(Object object, BeansWrapper wrapper) { // [2.4]: All models were introspected here, then the results was discareded, and get() will just do the // introspection again. So is this necessary? (The inrospectNow parameter was added in 2.3.21 to allow // lazy-introspecting BeansWrapper.trueModel|falseModel.) this(object, wrapper, true); }
Since:2.3.21
/** @since 2.3.21 */
BeanModel(Object object, BeansWrapper wrapper, boolean inrospectNow) { this.object = object; this.wrapper = wrapper; if (inrospectNow && object != null) { // [2.4]: Could this be removed? wrapper.getClassIntrospector().get(object.getClass()); } }
Uses Beans introspection to locate a property or method with name matching the key name. If a method or property is found, it's wrapped into TemplateMethodModelEx (for a method or indexed property), or evaluated on-the-fly and the return value wrapped into appropriate model (for a non-indexed property) Models for various properties and methods are cached on a per-class basis, so the costly introspection is performed only once per property or method of a class. (Side-note: this also implies that any class whose method has been called will be strongly referred to by the framework and will not become unloadable until this class has been unloaded first. Normally this is not an issue, but can be in a rare scenario where you create many classes on- the-fly. Also, as the cache grows with new classes and methods introduced to the framework, it may appear as if it were leaking memory. The framework does, however detect class reloads (if you happen to be in an environment that does this kind of things--servlet containers do it when they reload a web application) and flushes the cache. If no method or property matching the key is found, the framework will try to invoke methods with signature non-void-return-type get(java.lang.String), then non-void-return-type get(java.lang.Object), or alternatively (if the wrapped object is a resource bundle) Object getObject(java.lang.String).
Throws:
/** * Uses Beans introspection to locate a property or method with name * matching the key name. If a method or property is found, it's wrapped * into {@link freemarker.template.TemplateMethodModelEx} (for a method or * indexed property), or evaluated on-the-fly and the return value wrapped * into appropriate model (for a non-indexed property) Models for various * properties and methods are cached on a per-class basis, so the costly * introspection is performed only once per property or method of a class. * (Side-note: this also implies that any class whose method has been called * will be strongly referred to by the framework and will not become * unloadable until this class has been unloaded first. Normally this is not * an issue, but can be in a rare scenario where you create many classes on- * the-fly. Also, as the cache grows with new classes and methods introduced * to the framework, it may appear as if it were leaking memory. The * framework does, however detect class reloads (if you happen to be in an * environment that does this kind of things--servlet containers do it when * they reload a web application) and flushes the cache. If no method or * property matching the key is found, the framework will try to invoke * methods with signature * <tt>non-void-return-type get(java.lang.String)</tt>, * then <tt>non-void-return-type get(java.lang.Object)</tt>, or * alternatively (if the wrapped object is a resource bundle) * <tt>Object getObject(java.lang.String)</tt>. * @throws TemplateModelException if there was no property nor method nor * a generic <tt>get</tt> method to invoke. */
public TemplateModel get(String key) throws TemplateModelException { Class<?> clazz = object.getClass(); Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz); TemplateModel retval = null; try { if (wrapper.isMethodsShadowItems()) { Object fd = classInfo.get(key); if (fd != null) { retval = invokeThroughDescriptor(fd, classInfo); } else { retval = invokeGenericGet(classInfo, clazz, key); } } else { TemplateModel model = invokeGenericGet(classInfo, clazz, key); final TemplateModel nullModel = wrapper.wrap(null); if (model != nullModel && model != UNKNOWN) { return model; } Object fd = classInfo.get(key); if (fd != null) { retval = invokeThroughDescriptor(fd, classInfo); if (retval == UNKNOWN && model == nullModel) { // This is the (somewhat subtle) case where the generic get() returns null // and we have no bean info, so we respect the fact that // the generic get() returns null and return null. (JR) retval = nullModel; } } } if (retval == UNKNOWN) { if (wrapper.isStrict()) { throw new InvalidPropertyException("No such bean property: " + key); } else if (LOG.isDebugEnabled()) { logNoSuchKey(key, classInfo); } retval = wrapper.wrap(null); } return retval; } catch (TemplateModelException e) { throw e; } catch (Exception e) { throw new _TemplateModelException(e, "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key), "; see cause exception! The type of the containing value was: ", new _DelayedFTLTypeDescription(this) ); } } private void logNoSuchKey(String key, Map<?, ?> keyMap) { LOG.debug("Key " + StringUtil.jQuoteNoXSS(key) + " was not found on instance of " + object.getClass().getName() + ". Introspection information for " + "the class is: " + keyMap); }
Whether the model has a plain get(String) or get(Object) method
/** * Whether the model has a plain get(String) or get(Object) method */
protected boolean hasPlainGetMethod() { return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null; } private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo) throws IllegalAccessException, InvocationTargetException, TemplateModelException { // See if this particular instance has a cached implementation for the requested feature descriptor TemplateModel cachedModel; synchronized (this) { cachedModel = memberCache != null ? memberCache.get(desc) : null; } if (cachedModel != null) { return cachedModel; } TemplateModel resultModel = UNKNOWN; if (desc instanceof FastPropertyDescriptor) { FastPropertyDescriptor pd = (FastPropertyDescriptor) desc; Method indexedReadMethod = pd.getIndexedReadMethod(); if (indexedReadMethod != null) { if (!wrapper.getPreferIndexedReadMethod() && (pd.getReadMethod()) != null) { resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); // cachedModel remains null, as we don't cache these } else { resultModel = cachedModel = new SimpleMethodModel(object, indexedReadMethod, ClassIntrospector.getArgTypes(classInfo, indexedReadMethod), wrapper); } } else { resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), null); // cachedModel remains null, as we don't cache these } } else if (desc instanceof Field) { resultModel = wrapper.wrap(((Field) desc).get(object)); // cachedModel remains null, as we don't cache these } else if (desc instanceof Method) { Method method = (Method) desc; resultModel = cachedModel = new SimpleMethodModel( object, method, ClassIntrospector.getArgTypes(classInfo, method), wrapper); } else if (desc instanceof OverloadedMethods) { resultModel = cachedModel = new OverloadedMethodsModel( object, (OverloadedMethods) desc, wrapper); } // If new cachedModel was created, cache it if (cachedModel != null) { synchronized (this) { if (memberCache == null) { memberCache = new HashMap<Object, TemplateModel>(); } memberCache.put(desc, cachedModel); } } return resultModel; } void clearMemberCache() { synchronized (this) { memberCache = null; } } protected TemplateModel invokeGenericGet(Map/*<Object, Object>*/ classInfo, Class<?> clazz, String key) throws IllegalAccessException, InvocationTargetException, TemplateModelException { Method genericGet = (Method) classInfo.get(ClassIntrospector.GENERIC_GET_KEY); if (genericGet == null) { return UNKNOWN; } return wrapper.invokeMethod(object, genericGet, new Object[] { key }); } protected TemplateModel wrap(Object obj) throws TemplateModelException { return wrapper.getOuterIdentity().wrap(obj); } protected Object unwrap(TemplateModel model) throws TemplateModelException { return wrapper.unwrap(model); }
Tells whether the model is empty. It is empty if either the wrapped object is null, or it's a Boolean with false value.
/** * Tells whether the model is empty. It is empty if either the wrapped * object is null, or it's a Boolean with false value. */
public boolean isEmpty() { if (object instanceof String) { return ((String) object).length() == 0; } if (object instanceof Collection) { return ((Collection<?>) object).isEmpty(); } if (object instanceof Iterator && wrapper.is2324Bugfixed()) { return !((Iterator<?>) object).hasNext(); } if (object instanceof Map) { return ((Map<?,?>) object).isEmpty(); } return object == null || Boolean.FALSE.equals(object); }
Returns the same as getWrappedObject(); to ensure that, this method will be final starting from 2.4. This behavior of BeanModel is assumed by some FreeMarker code.
/** * Returns the same as {@link #getWrappedObject()}; to ensure that, this method will be final starting from 2.4. * This behavior of {@link BeanModel} is assumed by some FreeMarker code. */
public Object getAdaptedObject(Class<?> hint) { return object; // return getWrappedObject(); starting from 2.4 } public Object getWrappedObject() { return object; } public int size() { return wrapper.getClassIntrospector().keyCount(object.getClass()); } public TemplateCollectionModel keys() { return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper)); } public TemplateCollectionModel values() throws TemplateModelException { List<Object> values = new ArrayList<Object>(size()); TemplateModelIterator it = keys().iterator(); while (it.hasNext()) { String key = ((TemplateScalarModel) it.next()).getAsString(); values.add(get(key)); } return new CollectionAndSequence(new SimpleSequence(values, wrapper)); }
Used for classic_compatbile mode; don't use it for anything else. In FreeMarker 1.7 (and also at least in 2.1) BeanModel was a TemplateScalarModel. Some internal FreeMarker code tries to emulate FreeMarker classic by calling this method when a TemplateScalarModel is expected.
Returns:Never null
/** * Used for {@code classic_compatbile} mode; don't use it for anything else. * In FreeMarker 1.7 (and also at least in 2.1) {@link BeanModel} was a {@link TemplateScalarModel}. Some internal * FreeMarker code tries to emulate FreeMarker classic by calling this method when a {@link TemplateScalarModel} is * expected. * * @return Never {@code null} */
String getAsClassicCompatibleString() { if (object == null) { return "null"; } String s = object.toString(); return s != null ? s : "null"; } @Override public String toString() { return object.toString(); }
Helper method to support TemplateHashModelEx. Returns the Set of Strings which are available via the TemplateHashModel interface. Subclasses that override invokeGenericGet to provide additional hash keys should also override this method.
/** * Helper method to support TemplateHashModelEx. Returns the Set of * Strings which are available via the TemplateHashModel * interface. Subclasses that override <tt>invokeGenericGet</tt> to * provide additional hash keys should also override this method. */
protected Set/*<Object>*/ keySet() { return wrapper.getClassIntrospector().keySet(object.getClass()); } public TemplateModel getAPI() throws TemplateModelException { return wrapper.wrapAsAPI(object); } }