freemarker.ext.beans.BeansWrapper Maven / Gradle / Ivy
Show all versions of freemarker-gae Show documentation
/*
* 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.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import freemarker.core.BugException;
import freemarker.core._DelayedFTLTypeDescription;
import freemarker.core._DelayedShortClassName;
import freemarker.core._TemplateModelException;
import freemarker.ext.util.ModelCache;
import freemarker.ext.util.ModelFactory;
import freemarker.ext.util.WrapperTemplateModel;
import freemarker.log.Logger;
import freemarker.template.AdapterTemplateModel;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.ObjectWrapper;
import freemarker.template.ObjectWrapperAndUnwrapper;
import freemarker.template.SimpleObjectWrapper;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelAdapter;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template.Version;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.RichObjectWrapper;
import freemarker.template.utility.WriteProtectable;
/**
* {@link ObjectWrapper} that is able to expose the Java API of arbitrary Java objects. This is also the superclass of
* {@link DefaultObjectWrapper}. Note that instances of this class generally should be created with a
* {@link BeansWrapperBuilder}, not with its public constructors.
*
*
* As of 2.3.22, using {@link BeansWrapper} unextended is not recommended. Instead, {@link DefaultObjectWrapper} with
* its {@code incompatibleImprovements} property set to 2.3.22 (or higher) is the recommended {@link ObjectWrapper}.
*
*
* This class is only thread-safe after you have finished calling its setter methods, and then safely published it (see
* JSR 133 and related literature). When used as part of {@link Configuration}, of course it's enough if that was safely
* published and then left unmodified. Using {@link BeansWrapperBuilder} also guarantees thread safety.
*/
public class BeansWrapper implements RichObjectWrapper, WriteProtectable {
private static final Logger LOG = Logger.getLogger("freemarker.beans");
/**
* @deprecated Use {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} instead. It's not a public field
* anyway.
*/
@Deprecated
static final Object CAN_NOT_UNWRAP = ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS;
/**
* At this level of exposure, all methods and properties of the
* wrapped objects are exposed to the template.
*/
public static final int EXPOSE_ALL = 0;
/**
* At this level of exposure, all methods and properties of the wrapped
* objects are exposed to the template except methods that are deemed
* not safe. The not safe methods are java.lang.Object methods wait() and
* notify(), java.lang.Class methods getClassLoader() and newInstance(),
* java.lang.reflect.Method and java.lang.reflect.Constructor invoke() and
* newInstance() methods, all java.lang.reflect.Field set methods, all
* java.lang.Thread and java.lang.ThreadGroup methods that can change its
* state, as well as the usual suspects in java.lang.System and
* java.lang.Runtime.
*/
public static final int EXPOSE_SAFE = 1;
/**
* At this level of exposure, only property getters are exposed.
* Additionally, property getters that map to unsafe methods are not
* exposed (i.e. Class.classLoader and Thread.contextClassLoader).
*/
public static final int EXPOSE_PROPERTIES_ONLY = 2;
/**
* At this level of exposure, no bean properties and methods are exposed.
* Only map items, resource bundle items, and objects retrieved through
* the generic get method (on objects of classes that have a generic get
* method) can be retrieved through the hash interface. You might want to
* call {@link #setMethodsShadowItems(boolean)} with false value to
* speed up map item retrieval.
*/
public static final int EXPOSE_NOTHING = 3;
// -----------------------------------------------------------------------------------------------------------------
// Introspection cache:
private final Object sharedIntrospectionLock;
/**
* {@link Class} to class info cache.
* This object is possibly shared with other {@link BeansWrapper}-s!
*
*
To write this, always use {@link #replaceClassIntrospector(ClassIntrospectorBuilder)}.
*
*
When reading this, it's good idea to synchronize on sharedInrospectionLock when it doesn't hurt overall
* performance. In theory that's not needed, but apps might fail to keep the rules.
*/
private ClassIntrospector classIntrospector;
/**
* {@link String} class name to {@link StaticModel} cache.
* This object only belongs to a single {@link BeansWrapper}.
* This has to be final as {@link #getStaticModels()} might returns it any time and then it has to remain a good
* reference.
*/
private final StaticModels staticModels;
/**
* {@link String} class name to {@link EnumerationModel} cache.
* This object only belongs to a single {@link BeansWrapper}.
* This has to be final as {@link #getStaticModels()} might returns it any time and then it has to remain a good
* reference.
*/
private final ClassBasedModelFactory enumModels;
/**
* Object to wrapped object cache; not used by default.
* This object only belongs to a single {@link BeansWrapper}.
*/
private final ModelCache modelCache;
private final BooleanModel falseModel;
private final BooleanModel trueModel;
// -----------------------------------------------------------------------------------------------------------------
// Why volatile: In principle it need not be volatile, but we want to catch modification attempts even if the
// object was published improperly to other threads. After all, the main goal of WriteProtectable is protecting
// things from buggy user code.
private volatile boolean writeProtected;
private TemplateModel nullModel = null;
private int defaultDateType; // initialized from the BeansWrapperConfiguration
private ObjectWrapper outerIdentity = this;
private boolean methodsShadowItems = true;
private boolean simpleMapWrapper; // initialized from the BeansWrapperConfiguration
private boolean strict; // initialized from the BeansWrapperConfiguration
private boolean preferIndexedReadMethod; // initialized from the BeansWrapperConfiguration
private final Version incompatibleImprovements;
/**
* Creates a new instance with the incompatible-improvements-version specified in
* {@link Configuration#DEFAULT_INCOMPATIBLE_IMPROVEMENTS}.
*
* @deprecated Use {@link BeansWrapperBuilder} or, in rare cases, {@link #BeansWrapper(Version)} instead.
*/
@Deprecated
public BeansWrapper() {
this(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
// Attention! Don't change fields here, as the instance is possibly already visible to other threads.
}
/**
* Use {@link BeansWrapperBuilder} instead of the public constructors if possible.
* The main disadvantage of using the public constructors is that the instances won't share caches. So unless having
* a private cache is your goal, don't use them. See
*
* @param incompatibleImprovements
* Sets which of the non-backward-compatible improvements should be enabled. Not {@code null}. This version number
* is the same as the FreeMarker version number with which the improvements were implemented.
*
*
For new projects, it's recommended to set this to the FreeMarker version that's used during the development.
* For released products that are still actively developed it's a low risk change to increase the 3rd
* version number further as FreeMarker is updated, but of course you should always check the list of effects
* below. Increasing the 2nd or 1st version number possibly mean substantial changes with higher risk of breaking
* the application, but again, see the list of effects below.
*
*
The reason it's separate from {@link Configuration#setIncompatibleImprovements(Version)} is that
* {@link ObjectWrapper} objects are often shared among multiple {@link Configuration}-s, so the two version
* numbers are technically independent. But it's recommended to keep those two version numbers the same.
*
*
The changes enabled by {@code incompatibleImprovements} are:
*
* -
*
2.3.0: No changes; this is the starting point, the version used in older projects.
*
* -
*
2.3.21 (or higher):
* Several glitches were fixed in overloaded method selection. This usually just gets
* rid of errors (like ambiguity exceptions and numerical precision loses due to bad overloaded method
* choices), still, as in some cases the method chosen can be a different one now (that was the point of
* the reworking after all), it can mean a change in the behavior of the application. The most important
* change is that the treatment of {@code null} arguments were fixed, as earlier they were only seen
* applicable to parameters of type {@code Object}. Now {@code null}-s are seen to be applicable to any
* non-primitive parameters, and among those the one with the most specific type will be preferred (just
* like in Java), which is hence never the one with the {@code Object} parameter type. For more details
* about overloaded method selection changes see the version history in the FreeMarker Manual.
*
* -
*
2.3.24 (or higher):
* {@link Iterator}-s were always said to be non-empty when using {@code ?has_content} and such (i.e.,
* operators that check emptiness without reading any elements). Now an {@link Iterator} counts as
* empty exactly if it has no elements left. (Note that this bug has never affected basic functionality, like
* {@code <#list ...>}.)
*
* -
*
2.3.26 (or higher):
* The default of {@link BeansWrapper#getTreatDefaultMethodsAsBeanMembers()} changes from {@code false} to
* {@code true}. Thus, Java 8 default methods (and the bean properties they define) are exposed, despite that
* {@link java.beans.Introspector} (the official JavaBeans introspector) ignores them, at least as of Java 8.
*
* -
*
2.3.27 (or higher):
* The default of the {@link #setPreferIndexedReadMethod(boolean) preferIndexedReadMethod} setting changes
* from {@code true} to {@code false}.
*
*
*
* Note that the version will be normalized to the lowest version where the same incompatible
* {@link BeansWrapper} improvements were already present, so {@link #getIncompatibleImprovements()} might returns
* a lower version than what you have specified.
*
* @since 2.3.21
*/
public BeansWrapper(Version incompatibleImprovements) {
this(new BeansWrapperConfiguration(incompatibleImprovements) {}, false);
// Attention! Don't don anything here, as the instance is possibly already visible to other threads through the
// model factory callbacks.
}
private static volatile boolean ftmaDeprecationWarnLogged;
/**
* Same as {@link #BeansWrapper(BeansWrapperConfiguration, boolean, boolean)} with {@code true}
* {@code finalizeConstruction} argument.
*
* @since 2.3.21
*/
protected BeansWrapper(BeansWrapperConfiguration bwConf, boolean writeProtected) {
this(bwConf, writeProtected, true);
}
/**
* Initializes the instance based on the the {@link BeansWrapperConfiguration} specified.
*
* @param writeProtected Makes the instance's configuration settings read-only via
* {@link WriteProtectable#writeProtect()}; this way it can use the shared class introspection cache.
*
* @param finalizeConstruction Decides if the construction is finalized now, or the caller will do some more
* adjustments on the instance and then call {@link #finalizeConstruction(boolean)} itself.
*
* @since 2.3.22
*/
protected BeansWrapper(BeansWrapperConfiguration bwConf, boolean writeProtected, boolean finalizeConstruction) {
// Backward-compatibility hack for "finetuneMethodAppearance" overrides to work:
if (bwConf.getMethodAppearanceFineTuner() == null) {
Class> thisClass = this.getClass();
boolean overridden = false;
boolean testFailed = false;
try {
while (!overridden
&& thisClass != DefaultObjectWrapper.class
&& thisClass != BeansWrapper.class
&& thisClass != SimpleObjectWrapper.class) {
try {
thisClass.getDeclaredMethod("finetuneMethodAppearance",
new Class>[] { Class.class, Method.class, MethodAppearanceDecision.class });
overridden = true;
} catch (NoSuchMethodException e) {
thisClass = thisClass.getSuperclass();
}
}
} catch (Throwable e) {
// The security manager sometimes doesn't allow this
LOG.info("Failed to check if finetuneMethodAppearance is overidden in " + thisClass.getName()
+ "; acting like if it was, but this way it won't utilize the shared class introspection "
+ "cache.",
e);
overridden = true;
testFailed = true;
}
if (overridden) {
if (!testFailed && !ftmaDeprecationWarnLogged) {
LOG.warn("Overriding " + BeansWrapper.class.getName() + ".finetuneMethodAppearance is deprecated "
+ "and will be banned sometimes in the future. Use setMethodAppearanceFineTuner instead.");
ftmaDeprecationWarnLogged = true;
}
bwConf = (BeansWrapperConfiguration) bwConf.clone(false);
bwConf.setMethodAppearanceFineTuner(new MethodAppearanceFineTuner() {
public void process(
MethodAppearanceDecisionInput in, MethodAppearanceDecision out) {
BeansWrapper.this.finetuneMethodAppearance(in.getContainingClass(), in.getMethod(), out);
}
});
}
}
this.incompatibleImprovements = bwConf.getIncompatibleImprovements(); // normalized
simpleMapWrapper = bwConf.isSimpleMapWrapper();
preferIndexedReadMethod = bwConf.getPreferIndexedReadMethod();
defaultDateType = bwConf.getDefaultDateType();
outerIdentity = bwConf.getOuterIdentity() != null ? bwConf.getOuterIdentity() : this;
strict = bwConf.isStrict();
if (!writeProtected) {
// As this is not a read-only BeansWrapper, the classIntrospector will be possibly replaced for a few times,
// but we need to use the same sharedInrospectionLock forever, because that's what the model factories
// synchronize on, even during the classIntrospector is being replaced.
sharedIntrospectionLock = new Object();
classIntrospector = new ClassIntrospector(
_BeansAPI.getClassIntrospectorBuilder(bwConf), sharedIntrospectionLock);
} else {
// As this is a read-only BeansWrapper, the classIntrospector is never replaced, and since it's shared by
// other BeansWrapper instances, we use the lock belonging to the shared ClassIntrospector.
classIntrospector = _BeansAPI.getClassIntrospectorBuilder(bwConf).build();
sharedIntrospectionLock = classIntrospector.getSharedLock();
}
falseModel = new BooleanModel(Boolean.FALSE, this);
trueModel = new BooleanModel(Boolean.TRUE, this);
staticModels = new StaticModels(this);
enumModels = new _EnumModels(this);
modelCache = new BeansModelCache(this);
setUseCache(bwConf.getUseModelCache());
finalizeConstruction(writeProtected);
}
/**
* Meant to be called after {@link BeansWrapper#BeansWrapper(BeansWrapperConfiguration, boolean, boolean)} when
* its last argument was {@code false}; makes the instance read-only if necessary, then registers the model
* factories in the class introspector. No further changes should be done after calling this, if
* {@code writeProtected} was {@code true}.
*
* @since 2.3.22
*/
protected void finalizeConstruction(boolean writeProtected) {
if (writeProtected) {
writeProtect();
}
// Attention! At this point, the BeansWrapper must be fully initialized, as when the model factories are
// registered below, the BeansWrapper can immediately get concurrent callbacks. That those other threads will
// see consistent image of the BeansWrapper is ensured that callbacks are always sync-ed on
// classIntrospector.sharedLock, and so is classIntrospector.registerModelFactory(...).
registerModelFactories();
}
/**
* Makes the configuration properties (settings) of this {@link BeansWrapper} object read-only. As changing them
* after the object has become visible to multiple threads leads to undefined behavior, it's recommended to call
* this when you have finished configuring the object.
*
*
Consider using {@link BeansWrapperBuilder} instead, which gives an instance that's already
* write protected and also uses some shared caches/pools.
*
* @since 2.3.21
*/
public void writeProtect() {
writeProtected = true;
}
/**
* @since 2.3.21
*/
public boolean isWriteProtected() {
return writeProtected;
}
Object getSharedIntrospectionLock() {
return sharedIntrospectionLock;
}
/**
* If this object is already read-only according to {@link WriteProtectable}, throws {@link IllegalStateException},
* otherwise does nothing.
*
* @since 2.3.21
*/
protected void checkModifiable() {
if (writeProtected) throw new IllegalStateException(
"Can't modify the " + this.getClass().getName() + " object, as it was write protected.");
}
/**
* @see #setStrict(boolean)
*/
public boolean isStrict() {
return strict;
}
/**
* Specifies if an attempt to read a bean property that doesn't exist in the
* wrapped object should throw an {@link InvalidPropertyException}.
*
*
If this property is false (the default) then an attempt to read
* a missing bean property is the same as reading an existing bean property whose
* value is null. The template can't tell the difference, and thus always
* can use ?default('something') and ?exists and similar built-ins
* to handle the situation.
*
*
If this property is true then an attempt to read a bean propertly in
* the template (like myBean.aProperty) that doesn't exist in the bean
* object (as opposed to just holding null value) will cause
* {@link InvalidPropertyException}, which can't be suppressed in the template
* (not even with myBean.noSuchProperty?default('something')). This way
* ?default('something') and ?exists and similar built-ins can be used to
* handle existing properties whose value is null, without the risk of
* hiding typos in the property names. Typos will always cause error. But mind you, it
* goes against the basic approach of FreeMarker, so use this feature only if you really
* know what you are doing.
*/
public void setStrict(boolean strict) {
checkModifiable();
this.strict = strict;
}
/**
* When wrapping an object, the BeansWrapper commonly needs to wrap
* "sub-objects", for example each element in a wrapped collection.
* Normally it wraps these objects using itself. However, this makes
* it difficult to delegate to a BeansWrapper as part of a custom
* aggregate ObjectWrapper. This method lets you set the ObjectWrapper
* which will be used to wrap the sub-objects.
* @param outerIdentity the aggregate ObjectWrapper
*/
public void setOuterIdentity(ObjectWrapper outerIdentity) {
checkModifiable();
this.outerIdentity = outerIdentity;
}
/**
* By default returns this.
* @see #setOuterIdentity(ObjectWrapper)
*/
public ObjectWrapper getOuterIdentity() {
return outerIdentity;
}
/**
* When set to {@code true}, the keys in {@link Map}-s won't mix with the method names when looking at them
* from templates. The default is {@code false} for backward-compatibility, but is not recommended.
*
*
When this is {@code false}, {@code myMap.foo} or {@code myMap['foo']} either returns the method {@code foo},
* or calls {@code Map.get("foo")}. If both exists (the method and the {@link Map} key), one will hide the other,
* depending on the {@link #isMethodsShadowItems()}, which default to {@code true} (the method
* wins). Some frameworks use this so that you can call {@code myMap.get(nonStringKey)} from templates [*], but it
* comes on the cost of polluting the key-set with the method names, and risking methods accidentally hiding
* {@link Map} entries (or the other way around). Thus, this setup is not recommended.
* (Technical note: {@link Map}-s will be wrapped into {@link MapModel} in this case.)
*
*
When this is {@code true}, {@code myMap.foo} or {@code myMap['foo']} always calls {@code Map.get("foo")}.
* The methods of the {@link Map} object aren't visible from templates in this case. This, however, spoils the
* {@code myMap.get(nonStringKey)} workaround. But now you can use {@code myMap(nonStringKey)} instead, that is, you
* can use the map itself as the {@code get} method.
* (Technical note: {@link Map}-s will be wrapped into {@link SimpleMapModel} in this case.)
*
*
*: For historical reasons, FreeMarker 2.3.X doesn't support non-string keys with the {@code []} operator,
* hence the workarounds. This will be likely fixed in FreeMarker 2.4.0. Also note that the method- and
* the "field"-namespaces aren't separate in FreeMarker, hence {@code myMap.get} can return the {@code get}
* method.
*/
public void setSimpleMapWrapper(boolean simpleMapWrapper) {
checkModifiable();
this.simpleMapWrapper = simpleMapWrapper;
}
/**
* Tells whether Maps are exposed as simple maps, without access to their
* method. See {@link #setSimpleMapWrapper(boolean)} for details.
* @return true if Maps are exposed as simple hashes, false if they're
* exposed as full JavaBeans.
*/
public boolean isSimpleMapWrapper() {
return simpleMapWrapper;
}
/**
* Getter pair of {@link #setPreferIndexedReadMethod(boolean)}
*
* @since 2.3.27
*/
public boolean getPreferIndexedReadMethod() {
return preferIndexedReadMethod;
}
/**
* Sets if when a JavaBean property has both a normal read method (like {@code String[] getFoos()}) and an indexed
* read method (like {@code String getFoos(int index)}), and the Java {@link Introspector} exposes both (which only
* happens since Java 8, apparently), which read method will be used when the property is accessed with the
* shorthand syntax (like {@code myObj.foos}). Before {@link #getIncompatibleImprovements() incompatibleImprovements}
* 2.3.27 it defaults to {@code true} for backward compatibility (although it's actually less backward compatible if
* you are just switching to Java 8; see later), but the recommended value and the default starting with
* {@link #getIncompatibleImprovements() incompatibleImprovements} 2.3.27 is {@code false}. This setting has no
* effect on properties that only has normal read method, or only has indexed read method. In case a property has
* both, using the indexed reader method is disadvantageous, as then FreeMarker can't tell what the highest allowed
* index is, and so the property will be unlistable ({@code <#list foo as myObj.foos>} will fail).
*
*
* Apparently, this setting only matters since Java 8, as before that {@link Introspector} did not expose the
* indexed reader method if there was also a normal reader method. As with Java 8 the behavior of
* {@link Introspector} has changed, some old templates started to break, as the property has suddenly become
* unlistable (see earlier why). So setting this to {@code false} can be seen as a Java 8 compatibility fix.
*
* @since 2.3.27
*/
public void setPreferIndexedReadMethod(boolean preferIndexedReadMethod) {
checkModifiable();
this.preferIndexedReadMethod = preferIndexedReadMethod;
}
/**
* Sets the method exposure level. By default, set to EXPOSE_SAFE
.
* @param exposureLevel can be any of the EXPOSE_xxx
* constants.
*/
public void setExposureLevel(int exposureLevel) {
checkModifiable();
if (classIntrospector.getExposureLevel() != exposureLevel) {
ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
builder.setExposureLevel(exposureLevel);
replaceClassIntrospector(builder);
}
}
/**
* @since 2.3.21
*/
public int getExposureLevel() {
return classIntrospector.getExposureLevel();
}
/**
* Controls whether public instance fields of classes are exposed to
* templates.
* @param exposeFields if set to true, public instance fields of classes
* that do not have a property getter defined can be accessed directly by
* their name. If there is a property getter for a property of the same
* name as the field (i.e. getter "getFoo()" and field "foo"), then
* referring to "foo" in template invokes the getter. If set to false, no
* access to public instance fields of classes is given. Default is false.
*/
public void setExposeFields(boolean exposeFields) {
checkModifiable();
if (classIntrospector.getExposeFields() != exposeFields) {
ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
builder.setExposeFields(exposeFields);
replaceClassIntrospector(builder);
}
}
/**
* Controls whether Java 8 default methods that weren't overridden in a class will be recognized as bean property
* accessors and/or bean actions, and thus will be visible from templates. (We expose bean properties and bean
* actions, not methods in general.) Before {@link #getIncompatibleImprovements incompatibleImprovements} 2.3.26
* this defaults to {@code false} for backward compatibility. Starting with {@link #getIncompatibleImprovements
* incompatibleImprovements} 2.3.26 it defaults to {@code true}.
*
* Some explanation: FreeMarker uses {@link java.beans.Introspector} to discover the bean properties and actions of
* classes, for maximum conformance to the JavaBeans specification. But for some reason (perhaps just a bug in the
* Oracle/OpenJDK Java 8 implementation) that ignores the Java 8 default methods coming from the interfaces. When
* this setting is {@code true}, we search for non-overridden default methods ourselves, and add them to the set of
* discovered bean members.
*
* @since 2.3.26
*/
public void setTreatDefaultMethodsAsBeanMembers(boolean treatDefaultMethodsAsBeanMembers) {
checkModifiable();
if (classIntrospector.getTreatDefaultMethodsAsBeanMembers() != treatDefaultMethodsAsBeanMembers) {
ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
builder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
replaceClassIntrospector(builder);
}
}
/**
* Returns whether exposure of public instance fields of classes is
* enabled. See {@link #setExposeFields(boolean)} for details.
* @return true if public instance fields are exposed, false otherwise.
*
* @since 2.3.26
*/
public boolean isExposeFields() {
return classIntrospector.getExposeFields();
}
/**
* See {@link #setTreatDefaultMethodsAsBeanMembers(boolean)}.
*/
public boolean getTreatDefaultMethodsAsBeanMembers() {
return classIntrospector.getTreatDefaultMethodsAsBeanMembers();
}
public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
return classIntrospector.getMethodAppearanceFineTuner();
}
/**
* Used to tweak certain aspects of how methods appear in the data-model;
* see {@link MethodAppearanceFineTuner} for more.
*/
public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
checkModifiable();
if (classIntrospector.getMethodAppearanceFineTuner() != methodAppearanceFineTuner) {
ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
builder.setMethodAppearanceFineTuner(methodAppearanceFineTuner);
replaceClassIntrospector(builder);
}
}
MethodSorter getMethodSorter() {
return classIntrospector.getMethodSorter();
}
void setMethodSorter(MethodSorter methodSorter) {
checkModifiable();
if (classIntrospector.getMethodSorter() != methodSorter) {
ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
builder.setMethodSorter(methodSorter);
replaceClassIntrospector(builder);
}
}
/**
* Tells if this instance acts like if its class introspection cache is sharable with other {@link BeansWrapper}-s.
* A restricted cache denies certain too "antisocial" operations, like {@link #clearClassIntrospecitonCache()}.
* The value depends on how the instance
* was created; with a public constructor (then this is {@code false}), or with {@link BeansWrapperBuilder}
* (then it's {@code true}). Note that in the last case it's possible that the introspection cache
* will not be actually shared because there's no one to share with, but this will {@code true} even then.
*
* @since 2.3.21
*/
public boolean isClassIntrospectionCacheRestricted() {
return classIntrospector.getHasSharedInstanceRestrictons();
}
/**
* Replaces the value of {@link #classIntrospector}, but first it unregisters
* the model factories in the old {@link #classIntrospector}.
*/
private void replaceClassIntrospector(ClassIntrospectorBuilder builder) {
checkModifiable();
final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock);
final ClassIntrospector oldCI;
// In principle this need not be synchronized, but as apps might publish the configuration improperly, or
// even modify the wrapper after publishing. This doesn't give 100% protection from those violations,
// as classIntrospector reading aren't everywhere synchronized for performance reasons. It still decreases the
// chance of accidents, because some ops on classIntrospector are synchronized, and because it will at least
// push the new value into the common shared memory.
synchronized (sharedIntrospectionLock) {
oldCI = classIntrospector;
if (oldCI != null) {
// Note that after unregistering the model factory might still gets some callback from the old
// classIntrospector
if (staticModels != null) {
oldCI.unregisterModelFactory(staticModels);
staticModels.clearCache();
}
if (enumModels != null) {
oldCI.unregisterModelFactory(enumModels);
enumModels.clearCache();
}
if (modelCache != null) {
oldCI.unregisterModelFactory(modelCache);
modelCache.clearCache();
}
if (trueModel != null) {
trueModel.clearMemberCache();
}
if (falseModel != null) {
falseModel.clearMemberCache();
}
}
classIntrospector = newCI;
registerModelFactories();
}
}
private void registerModelFactories() {
if (staticModels != null) {
classIntrospector.registerModelFactory(staticModels);
}
if (enumModels != null) {
classIntrospector.registerModelFactory(enumModels);
}
if (modelCache != null) {
classIntrospector.registerModelFactory(modelCache);
}
}
/**
* Sets whether methods shadow items in beans. When true (this is the
* default value), ${object.name}
will first try to locate
* a bean method or property with the specified name on the object, and
* only if it doesn't find it will it try to call
* object.get(name)
, the so-called "generic get method" that
* is usually used to access items of a container (i.e. elements of a map).
* When set to false, the lookup order is reversed and generic get method
* is called first, and only if it returns null is method lookup attempted.
*/
public void setMethodsShadowItems(boolean methodsShadowItems) {
// This sync is here as this method was originally synchronized, but was never truly thread-safe, so I don't
// want to advertise it in the javadoc, nor I wanted to break any apps that work because of this accidentally.
synchronized (this) {
checkModifiable();
this.methodsShadowItems = methodsShadowItems;
}
}
boolean isMethodsShadowItems() {
return methodsShadowItems;
}
/**
* Sets the default date type to use for date models that result from
* a plain java.util.Date instead of java.sql.Date or
* java.sql.Time or java.sql.Timestamp. Default value is
* {@link TemplateDateModel#UNKNOWN}.
* @param defaultDateType the new default date type.
*/
public void setDefaultDateType(int defaultDateType) {
// This sync is here as this method was originally synchronized, but was never truly thread-safe, so I don't
// want to advertise it in the javadoc, nor I wanted to break any apps that work because of this accidentally.
synchronized (this) {
checkModifiable();
this.defaultDateType = defaultDateType;
}
}
/**
* Returns the default date type. See {@link #setDefaultDateType(int)} for
* details.
* @return the default date type
*/
public int getDefaultDateType() {
return defaultDateType;
}
/**
* Sets whether this wrapper caches the {@link TemplateModel}-s created for the Java objects that has wrapped with
* this object wrapper. Default is {@code false}.
* When set to {@code true}, calling {@link #wrap(Object)} multiple times for
* the same object will likely return the same model (although there is
* no guarantee as the cache items can be cleared any time).
*/
public void setUseCache(boolean useCache) {
checkModifiable();
modelCache.setUseCache(useCache);
}
/**
* @since 2.3.21
*/
public boolean getUseCache() {
return modelCache.getUseCache();
}
/**
* Sets the null model. This model is returned from the {@link #wrap(Object)} method whenever the wrapped object is
* {@code null}. It defaults to {@code null}, which is dealt with quite strictly on engine level, however you can
* substitute an arbitrary (perhaps more lenient) model, like an empty string. For proper working, the
* {@code nullModel} should be an {@link AdapterTemplateModel} that returns {@code null} for
* {@link AdapterTemplateModel#getAdaptedObject(Class)}.
*
* @deprecated Changing the {@code null} model can cause a lot of confusion; don't do it.
*/
@Deprecated
public void setNullModel(TemplateModel nullModel) {
checkModifiable();
this.nullModel = nullModel;
}
/**
* Returns the version given with {@link #BeansWrapper(Version)}, normalized to the lowest version where a change
* has occurred. Thus, this is not necessarily the same version than that was given to the constructor.
*
* @since 2.3.21
*/
public Version getIncompatibleImprovements() {
return incompatibleImprovements;
}
boolean is2321Bugfixed() {
return is2321Bugfixed(getIncompatibleImprovements());
}
static boolean is2321Bugfixed(Version version) {
return version.intValue() >= _TemplateAPI.VERSION_INT_2_3_21;
}
boolean is2324Bugfixed() {
return is2324Bugfixed(getIncompatibleImprovements());
}
static boolean is2324Bugfixed(Version version) {
return version.intValue() >= _TemplateAPI.VERSION_INT_2_3_24;
}
/**
* Returns the lowest version number that is equivalent with the parameter version.
* @since 2.3.21
*/
protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
_TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
if (incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_0) {
throw new IllegalArgumentException("Version must be at least 2.3.0.");
}
return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27
: incompatibleImprovements.intValue() == _TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26
: is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24
: is2321Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_21
: Configuration.VERSION_2_3_0;
}
/**
* Returns the default instance of the wrapper. This instance is used
* when you construct various bean models without explicitly specifying
* a wrapper. It is also returned by
* {@link freemarker.template.ObjectWrapper#BEANS_WRAPPER}
* and this is the sole instance that is used by the JSP adapter.
* You can modify the properties of the default instance (caching,
* exposure level, null model) to affect its operation. By default, the
* default instance is not caching, uses the EXPOSE_SAFE
* exposure level, and uses null reference as the null model.
*
* @deprecated Use {@link BeansWrapperBuilder} instead. The instance returned here is not read-only, so it's
* dangerous to use.
*/
@Deprecated
public static final BeansWrapper getDefaultInstance() {
return BeansWrapperSingletonHolder.INSTANCE;
}
/**
* Wraps the object with a template model that is most specific for the object's
* class. Specifically:
*
* - if the object is null, returns the {@link #setNullModel(TemplateModel) null model},
* - if the object is a Number returns a {@link NumberModel} for it,
* - if the object is a Date returns a {@link DateModel} for it,
* - if the object is a Boolean returns
* {@link freemarker.template.TemplateBooleanModel#TRUE} or
* {@link freemarker.template.TemplateBooleanModel#FALSE}
* - if the object is already a TemplateModel, returns it unchanged,
* - if the object is an array, returns a {@link ArrayModel} for it
*
- if the object is a Map, returns a {@link MapModel} for it
*
- if the object is a Collection, returns a {@link CollectionModel} for it
*
- if the object is an Iterator, returns a {@link IteratorModel} for it
*
- if the object is an Enumeration, returns a {@link EnumerationModel} for it
*
- if the object is a String, returns a {@link StringModel} for it
*
- otherwise, returns a generic {@link StringModel} for it.
*
*/
public TemplateModel wrap(Object object) throws TemplateModelException {
if (object == null) return nullModel;
return modelCache.getInstance(object);
}
/**
* Wraps a Java method so that it can be called from templates, without wrapping its parent ("this") object. The
* result is almost the same as that you would get by wrapping the parent object then getting the method from the
* resulting {@link TemplateHashModel} by name. Except, if the wrapped method is overloaded, with this method you
* explicitly select an overload, while otherwise you would get a {@link TemplateMethodModelEx} that selects an
* overload each time it's called based on the argument values.
*
* @param object The object whose method will be called, or {@code null} if {@code method} is a static method.
* This object will be used "as is", like without unwrapping it if it's a {@link TemplateModelAdapter}.
* @param method The method to call, which must be an (inherited) member of the class of {@code object}, as
* described by {@link Method#invoke(Object, Object...)}
*
* @since 2.3.22
*/
public TemplateMethodModelEx wrap(Object object, Method method) {
return new SimpleMethodModel(object, method, method.getParameterTypes(), this);
}
/**
* @since 2.3.22
*/
public TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException {
return new APIModel(obj, this);
}
/**
* @deprecated override {@link #getModelFactory(Class)} instead. Using this
* method will now bypass wrapper caching (if it's enabled) and always
* result in creation of a new wrapper. This method will be removed in 2.4
* @param object The object to wrap
* @param factory The factory that wraps the object
*/
@Deprecated
protected TemplateModel getInstance(Object object, ModelFactory factory) {
return factory.create(object, this);
}
private final ModelFactory BOOLEAN_FACTORY = new ModelFactory() {
public TemplateModel create(Object object, ObjectWrapper wrapper) {
return ((Boolean) object).booleanValue() ? trueModel : falseModel;
}
};
private static final ModelFactory ITERATOR_FACTORY = new ModelFactory() {
public TemplateModel create(Object object, ObjectWrapper wrapper) {
return new IteratorModel((Iterator>) object, (BeansWrapper) wrapper);
}
};
private static final ModelFactory ENUMERATION_FACTORY = new ModelFactory() {
public TemplateModel create(Object object, ObjectWrapper wrapper) {
return new EnumerationModel((Enumeration>) object, (BeansWrapper) wrapper);
}
};
protected ModelFactory getModelFactory(Class> clazz) {
if (Map.class.isAssignableFrom(clazz)) {
return simpleMapWrapper ? SimpleMapModel.FACTORY : MapModel.FACTORY;
}
if (Collection.class.isAssignableFrom(clazz)) {
return CollectionModel.FACTORY;
}
if (Number.class.isAssignableFrom(clazz)) {
return NumberModel.FACTORY;
}
if (Date.class.isAssignableFrom(clazz)) {
return DateModel.FACTORY;
}
if (Boolean.class == clazz) { // Boolean is final
return BOOLEAN_FACTORY;
}
if (ResourceBundle.class.isAssignableFrom(clazz)) {
return ResourceBundleModel.FACTORY;
}
if (Iterator.class.isAssignableFrom(clazz)) {
return ITERATOR_FACTORY;
}
if (Enumeration.class.isAssignableFrom(clazz)) {
return ENUMERATION_FACTORY;
}
if (clazz.isArray()) {
return ArrayModel.FACTORY;
}
return StringModel.FACTORY;
}
/**
* Attempts to unwrap a model into underlying object. Generally, this
* method is the inverse of the {@link #wrap(Object)} method. In addition
* it will unwrap arbitrary {@link TemplateNumberModel} instances into
* a number, arbitrary {@link TemplateDateModel} instances into a date,
* {@link TemplateScalarModel} instances into a String, arbitrary
* {@link TemplateBooleanModel} instances into a Boolean, arbitrary
* {@link TemplateHashModel} instances into a Map, arbitrary
* {@link TemplateSequenceModel} into a List, and arbitrary
* {@link TemplateCollectionModel} into a Set. All other objects are
* returned unchanged.
* @throws TemplateModelException if an attempted unwrapping fails.
*/
public Object unwrap(TemplateModel model) throws TemplateModelException {
return unwrap(model, Object.class);
}
/**
* Attempts to unwrap a model into an object of the desired class.
* Generally, this method is the inverse of the {@link #wrap(Object)}
* method. It recognizes a wide range of target classes - all Java built-in
* primitives, primitive wrappers, numbers, dates, sets, lists, maps, and
* native arrays.
* @param model the model to unwrap
* @param targetClass the class of the unwrapped result; {@code Object.class} of we don't know what the expected type is.
* @return the unwrapped result of the desired class
* @throws TemplateModelException if an attempted unwrapping fails.
*
* @see #tryUnwrapTo(TemplateModel, Class)
*/
public Object unwrap(TemplateModel model, Class> targetClass)
throws TemplateModelException {
final Object obj = tryUnwrapTo(model, targetClass);
if (obj == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) {
throw new TemplateModelException("Can not unwrap model of type " +
model.getClass().getName() + " to type " + targetClass.getName());
}
return obj;
}
/**
* @since 2.3.22
*/
public Object tryUnwrapTo(TemplateModel model, Class> targetClass) throws TemplateModelException {
return tryUnwrapTo(model, targetClass, 0);
}
/**
* @param typeFlags
* Used when unwrapping for overloaded methods and so the {@code targetClass} is possibly too generic.
* Must be 0 when unwrapping parameter values for non-overloaded methods, also if
* {@link #is2321Bugfixed()} is {@code false}.
* @return {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} or the unwrapped object.
*/
Object tryUnwrapTo(TemplateModel model, Class> targetClass, int typeFlags)
throws TemplateModelException {
Object res = tryUnwrapTo(model, targetClass, typeFlags, null);
if ((typeFlags & TypeFlags.WIDENED_NUMERICAL_UNWRAPPING_HINT) != 0
&& res instanceof Number) {
return OverloadedNumberUtil.addFallbackType((Number) res, typeFlags);
} else {
return res;
}
}
/**
* See {@link #tryUnwrapTo(TemplateModel, Class, int)}.
*/
private Object tryUnwrapTo(final TemplateModel model, Class> targetClass, final int typeFlags,
final Map