http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java b/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java new file mode 100644 index 0000000..ec20b9f --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java @@ -0,0 +1,121 @@ +/* + * 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 org.apache.freemarker.core.model.impl; + +import java.io.Serializable; +import java.util.Enumeration; +import java.util.Iterator; + +import org.apache.freemarker.core.model.AdapterTemplateModel; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.WrapperTemplateModel; +import org.apache.freemarker.core.model.WrappingTemplateModel; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Adapts an {@link Enumeration} to the corresponding {@link TemplateModel} interface(s), most importantly to + * {@link TemplateCollectionModel}. Putting aside that it wraps an {@link Enumeration} instead of an {@link Iterator}, + * this is identical to {@link DefaultIteratorAdapter}, so see further details there. + */ +// TODO JUnit +public class DefaultEnumerationAdapter extends WrappingTemplateModel implements TemplateCollectionModel, + AdapterTemplateModel, WrapperTemplateModel, Serializable { + + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="We hope it's Seralizable") + private final Enumeration enumeration; + private boolean enumerationOwnedBySomeone; + + /** + * Factory method for creating new adapter instances. + * + * @param iterator + * The enumeration to adapt; can't be {@code null}. + */ + public static DefaultEnumerationAdapter adapt(Enumeration iterator, ObjectWrapper wrapper) { + return new DefaultEnumerationAdapter(iterator, wrapper); + } + + private DefaultEnumerationAdapter(Enumeration enumeration, ObjectWrapper wrapper) { + super(wrapper); + this.enumeration = enumeration; + } + + @Override + public Object getWrappedObject() { + return enumeration; + } + + @Override + public Object getAdaptedObject(Class hint) { + return getWrappedObject(); + } + + @Override + public TemplateModelIterator iterator() throws TemplateModelException { + return new SimpleTemplateModelIterator(); + } + + /** + * Not thread-safe. + */ + private class SimpleTemplateModelIterator implements TemplateModelIterator { + + private boolean enumerationOwnedByMe; + + @Override + public TemplateModel next() throws TemplateModelException { + if (!enumerationOwnedByMe) { + checkNotOwner(); + enumerationOwnedBySomeone = true; + enumerationOwnedByMe = true; + } + + if (!enumeration.hasMoreElements()) { + throw new TemplateModelException("The collection has no more items."); + } + + Object value = enumeration.nextElement(); + return value instanceof TemplateModel ? (TemplateModel) value : wrap(value); + } + + @Override + public boolean hasNext() throws TemplateModelException { + // Calling hasNext may looks safe, but I have met sync. problems. + if (!enumerationOwnedByMe) { + checkNotOwner(); + } + + return enumeration.hasMoreElements(); + } + + private void checkNotOwner() throws TemplateModelException { + if (enumerationOwnedBySomeone) { + throw new TemplateModelException( + "This collection value wraps a java.util.Enumeration, thus it can be listed only once."); + } + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java index 0351edd..ebc5a3d 100644 --- a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java +++ b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java @@ -19,22 +19,52 @@ package org.apache.freemarker.core.model.impl; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; -import java.util.ArrayList; +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 org.apache.freemarker.core.Configuration; import org.apache.freemarker.core.Version; +import org.apache.freemarker.core._CoreAPI; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core._DelayedFTLTypeDescription; +import org.apache.freemarker.core._DelayedShortClassName; +import org.apache.freemarker.core._TemplateModelException; +import org.apache.freemarker.core.model.AdapterTemplateModel; import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; +import org.apache.freemarker.core.model.RichObjectWrapper; import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelAdapter; import org.apache.freemarker.core.model.TemplateModelException; -import org.apache.freemarker.core.model.impl.beans.BeansWrapper; -import org.apache.freemarker.core.model.impl.beans.BeansWrapperConfiguration; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.WrapperTemplateModel; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util.WriteProtectable; +import org.apache.freemarker.core.util._ClassUtil; import org.apache.freemarker.dom.NodeModel; +import org.slf4j.Logger; import org.w3c.dom.Node; /** @@ -54,46 +84,583 @@ import org.w3c.dom.Node; * 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. */ -public class DefaultObjectWrapper extends org.apache.freemarker.core.model.impl.beans.BeansWrapper { - +// TODO Get rid of unused stuff (including other clases in this package) coming from BeansWrapper. +public class DefaultObjectWrapper implements RichObjectWrapper, WriteProtectable { + + private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER; + + /** + * 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 <tt>false</tt> 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 DefaultObjectWrapper}-s! + * + * <p>To write this, always use {@link #replaceClassIntrospector(ClassIntrospectorBuilder)}. + * + * <p>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 DefaultObjectWrapper}. + * 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; + /** - * Use {@link DefaultObjectWrapperBuilder} instead if possible. Instances created with this constructor won't share - * the class introspection caches with other instances. See {@link BeansWrapper#BeansWrapper(Version)} (the - * superclass constructor) for more details. - * + * {@link String} class name to {@link EnumerationModel} cache. + * This object only belongs to a single {@link DefaultObjectWrapper}. + * 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 DefaultObjectWrapper}. + */ + 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 int defaultDateType; // initialized by PropertyAssignments.apply + private ObjectWrapper outerIdentity = this; + private boolean methodsShadowItems = true; + private boolean strict; // initialized by PropertyAssignments.apply + + private final Version incompatibleImprovements; + + /** + * Use {@link DefaultObjectWrapperBuilder} 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 - * It's the same as in {@link BeansWrapper#BeansWrapper(Version)}. - * + * 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. + * + * <p>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. + * + * <p>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. + * + * <p>The changes enabled by {@code incompatibleImprovements} are: + * <ul> + * <li> + * <p>2.3.0: No changes; this is the starting point, the version used in older projects. + * </li> + * <li> + * <p>2.3.21 (or higher): + * Several glitches were oms in <em>overloaded</em> 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 oms, 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. + * </li> + * <li> + * <p>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 ...>}.) + * </li> + * </ul> + * + * <p>Note that the version will be normalized to the lowest version where the same incompatible + * {@link DefaultObjectWrapper} improvements were already present, so {@link #getIncompatibleImprovements()} might returns + * a lower version than what you have specified. + * * @since 2.3.21 */ public DefaultObjectWrapper(Version incompatibleImprovements) { - this(new DefaultObjectWrapperConfiguration(incompatibleImprovements) { }, false); + this(new DefaultObjectWrapperConfiguration(incompatibleImprovements) {}, false); + // Attention! Don't don anything here, as the instance is possibly already visible to other threads through the + // model factory callbacks. } /** - * Use {@link #DefaultObjectWrapper(DefaultObjectWrapperConfiguration, boolean)} instead if possible; - * it does the same, except that it tolerates a non-{@link DefaultObjectWrapperConfiguration} configuration too. - * + * Same as {@link #DefaultObjectWrapper(DefaultObjectWrapperConfiguration, boolean, boolean)} with {@code true} + * {@code finalizeConstruction} argument. + * * @since 2.3.21 */ - protected DefaultObjectWrapper(BeansWrapperConfiguration bwCfg, boolean writeProtected) { - super(bwCfg, writeProtected, false); - DefaultObjectWrapperConfiguration dowDowCfg = bwCfg instanceof DefaultObjectWrapperConfiguration - ? (DefaultObjectWrapperConfiguration) bwCfg - : new DefaultObjectWrapperConfiguration(bwCfg.getIncompatibleImprovements()) { }; + protected DefaultObjectWrapper(DefaultObjectWrapperConfiguration bwConf, boolean writeProtected) { + this(bwConf, writeProtected, true); + } + + /** + * Initializes the instance based on the the {@link DefaultObjectWrapperConfiguration} 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 DefaultObjectWrapper(DefaultObjectWrapperConfiguration bwConf, boolean writeProtected, boolean finalizeConstruction) { + incompatibleImprovements = bwConf.getIncompatibleImprovements(); // normalized + + defaultDateType = bwConf.getDefaultDateType(); + outerIdentity = bwConf.getOuterIdentity() != null ? bwConf.getOuterIdentity() : this; + strict = bwConf.isStrict(); + + if (!writeProtected) { + // As this is not a read-only DefaultObjectWrapper, 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(bwConf.classIntrospectorFactory, sharedIntrospectionLock); + } else { + // As this is a read-only DefaultObjectWrapper, the classIntrospector is never replaced, and since it's shared by + // other DefaultObjectWrapper instances, we use the lock belonging to the shared ClassIntrospector. + classIntrospector = bwConf.classIntrospectorFactory.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); + setUseModelCache(bwConf.getUseModelCache()); + finalizeConstruction(writeProtected); } /** - * Calls {@link BeansWrapper#BeansWrapper(BeansWrapperConfiguration, boolean)} and sets up - * {@link DefaultObjectWrapper}-specific fields. - * + * Meant to be called after {@link DefaultObjectWrapper#DefaultObjectWrapper(DefaultObjectWrapperConfiguration, 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 DefaultObjectWrapper(DefaultObjectWrapperConfiguration dowCfg, boolean writeProtected) { - this((BeansWrapperConfiguration) dowCfg, writeProtected); + protected void finalizeConstruction(boolean writeProtected) { + if (writeProtected) { + writeProtect(); + } + + // Attention! At this point, the DefaultObjectWrapper must be fully initialized, as when the model factories are + // registered below, the DefaultObjectWrapper can immediately get concurrent callbacks. That those other threads will + // see consistent image of the DefaultObjectWrapper 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 DefaultObjectWrapper} 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. + * + * <p>Consider using {@link DefaultObjectWrapperBuilder} instead, which gives an instance that's already + * write protected and also uses some shared caches/pools. + * + * @since 2.3.21 + */ + @Override + public void writeProtect() { + writeProtected = true; + } + + /** + * @since 2.3.21 + */ + @Override + 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 " + 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}. + * + * <p>If this property is <tt>false</tt> (the default) then an attempt to read + * a missing bean property is the same as reading an existing bean property whose + * value is <tt>null</tt>. The template can't tell the difference, and thus always + * can use <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins + * to handle the situation. + * + * <p>If this property is <tt>true</tt> then an attempt to read a bean propertly in + * the template (like <tt>myBean.aProperty</tt>) that doesn't exist in the bean + * object (as opposed to just holding <tt>null</tt> value) will cause + * {@link InvalidPropertyException}, which can't be suppressed in the template + * (not even with <tt>myBean.noSuchProperty?default('something')</tt>). This way + * <tt>?default('something')</tt> and <tt>?exists</tt> and similar built-ins can be used to + * handle existing properties whose value is <tt>null</tt>, 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 DefaultObjectWrapper 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 DefaultObjectWrapper 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 <tt>this</tt>. + * @see #setOuterIdentity(ObjectWrapper) + */ + public ObjectWrapper getOuterIdentity() { + return outerIdentity; + } + + // I have commented this out, as it won't be in 2.3.20 yet. + /* + /** + * Tells which non-backward-compatible overloaded method selection fixes to apply; + * see {@link #setOverloadedMethodSelection(Version)}. + * / + public Version getOverloadedMethodSelection() { + return overloadedMethodSelection; + } + + /** + * Sets which non-backward-compatible overloaded method selection fixes to apply. + * This has similar logic as {@link Configuration#setIncompatibleImprovements(Version)}, + * but only applies to this aspect. + * + * Currently significant values: + * <ul> + * <li>2.3.21: Completetlly rewritten overloaded method selection, fixes several issues with the old one.</li> + * </ul> + * / + public void setOverloadedMethodSelection(Version version) { + overloadedMethodSelection = version; + } + */ + + /** + * Sets the method exposure level. By default, set to <code>EXPOSE_SAFE</code>. + * @param exposureLevel can be any of the <code>EXPOSE_xxx</code> + * constants. + */ + public void setExposureLevel(int exposureLevel) { + checkModifiable(); + + if (classIntrospector.getExposureLevel() != exposureLevel) { + ClassIntrospectorBuilder pa = classIntrospector.getPropertyAssignments(); + pa.setExposureLevel(exposureLevel); + replaceClassIntrospector(pa); + } + } + + /** + * @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 pa = classIntrospector.getPropertyAssignments(); + pa.setExposeFields(exposeFields); + replaceClassIntrospector(pa); + } + } + + /** + * 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. + */ + public boolean isExposeFields() { + return classIntrospector.getExposeFields(); + } + + 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 pa = classIntrospector.getPropertyAssignments(); + pa.setMethodAppearanceFineTuner(methodAppearanceFineTuner); + replaceClassIntrospector(pa); + } + } + + MethodSorter getMethodSorter() { + return classIntrospector.getMethodSorter(); + } + + void setMethodSorter(MethodSorter methodSorter) { + checkModifiable(); + + if (classIntrospector.getMethodSorter() != methodSorter) { + ClassIntrospectorBuilder pa = classIntrospector.getPropertyAssignments(); + pa.setMethodSorter(methodSorter); + replaceClassIntrospector(pa); + } + } + + /** + * Tells if this instance acts like if its class introspection cache is sharable with other {@link DefaultObjectWrapper}-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 DefaultObjectWrapperBuilder} + * (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 pa) { + checkModifiable(); + + final ClassIntrospector newCI = new ClassIntrospector(pa, 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), <code>${object.name}</code> 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 + * <code>object.get(name)</code>, 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 <tt>java.util.Date</tt> instead of <tt>java.sql.Date</tt> or + * <tt>java.sql.Time</tt> or <tt>java.sql.Timestamp</tt>. 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 setUseModelCache(boolean useCache) { + checkModifiable(); + modelCache.setUseCache(useCache); + } + + /** + * @since 2.3.21 + */ + public boolean getUseModelCache() { + return modelCache.getUseCache(); + } + + /** + * Returns the version given with {@link #DefaultObjectWrapper(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; + } + /** * Wraps the parameter object to {@link TemplateModel} interface(s). Simple types like numbers, strings, booleans * and dates will be wrapped into the corresponding {@code SimpleXxx} classes (like {@link SimpleNumber}). @@ -104,11 +671,15 @@ public class DefaultObjectWrapper extends org.apache.freemarker.core.model.impl. @Override public TemplateModel wrap(Object obj) throws TemplateModelException { if (obj == null) { - return super.wrap(null); + return null; } if (obj instanceof TemplateModel) { return (TemplateModel) obj; } + if (obj instanceof TemplateModelAdapter) { + return ((TemplateModelAdapter) obj).getTemplateModel(); + } + if (obj instanceof String) { return new SimpleScalar((String) obj); } @@ -148,68 +719,847 @@ public class DefaultObjectWrapper extends org.apache.freemarker.core.model.impl. if (obj instanceof Iterable) { return DefaultIterableAdapter.adapt((Iterable<?>) obj, this); } + if (obj instanceof Enumeration) { + return DefaultEnumerationAdapter.adapt((Enumeration<?>) obj, this); + } + + // [FM3] Via plugin mechanism, not by default anymore + if (obj instanceof Node) { + return handW3CNode((Node) obj); + } + return handleUnknownType(obj); } - + + protected TemplateModel handW3CNode(Node node) throws TemplateModelException { + return NodeModel.wrap(node); + } + /** - * Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type, - * or for a W3C DOM_WRAPPER node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s - * (allows DOM_WRAPPER tree traversal), others will be wrapped using {@link BeansWrapper#wrap(Object)}. - * + * Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type. + * In its default implementation, the object will be wrapped as a generic JavaBean. + * * <p> * When you override this method, you should first decide if you want to wrap the object in a custom way (and if so * then do it and return with the result), and if not, then you should call the super method (assuming the default * behavior is fine with you). */ protected TemplateModel handleUnknownType(Object obj) throws TemplateModelException { - if (obj instanceof Node) { - return wrapDomNode(obj); + return modelCache.getInstance(obj); + } + + /** + * 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 a 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 + */ + @Override + public TemplateHashModel wrapAsAPI(Object obj) throws TemplateModelException { + return new APIModel(obj, this); + } + + private final ModelFactory BOOLEAN_FACTORY = new ModelFactory() { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return ((Boolean) object).booleanValue() ? trueModel : falseModel; + } + }; + + private static final ModelFactory ITERATOR_FACTORY = new ModelFactory() { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new IteratorModel((Iterator<?>) object, (DefaultObjectWrapper) wrapper); + } + }; + + private static final ModelFactory ENUMERATION_FACTORY = new ModelFactory() { + @Override + public TemplateModel create(Object object, ObjectWrapper wrapper) { + return new EnumerationModel((Enumeration<?>) object, (DefaultObjectWrapper) wrapper); + } + }; + + protected ModelFactory getModelFactory(Class<?> clazz) { + if (Map.class.isAssignableFrom(clazz)) { + return SimpleMapModel.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. + */ + @Override + 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 + */ + @Override + 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 + * (as it's the most specific common superclass). Must be 0 when unwrapping parameter values for + * non-overloaded methods. + * @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<Object, Object> recursionStops) + throws TemplateModelException { + if (model == null) { + return null; + } + + if (targetClass.isPrimitive()) { + targetClass = _ClassUtil.primitiveClassToBoxingClass(targetClass); + } + + // This is for transparent interop with other wrappers (and ourselves) + // Passing the targetClass allows e.g. a Jython-aware method that declares a + // PyObject as its argument to receive a PyObject from a Jython-aware TemplateModel + // passed as an argument to TemplateMethodModelEx etc. + if (model instanceof AdapterTemplateModel) { + Object wrapped = ((AdapterTemplateModel) model).getAdaptedObject(targetClass); + if (targetClass == Object.class || targetClass.isInstance(wrapped)) { + return wrapped; + } + + // Attempt numeric conversion: + if (targetClass != Object.class && (wrapped instanceof Number && _ClassUtil.isNumerical(targetClass))) { + Number number = forceUnwrappedNumberToType((Number) wrapped, targetClass); + if (number != null) return number; + } + } + + if (model instanceof WrapperTemplateModel) { + Object wrapped = ((WrapperTemplateModel) model).getWrappedObject(); + if (targetClass == Object.class || targetClass.isInstance(wrapped)) { + return wrapped; + } + + // Attempt numeric conversion: + if (targetClass != Object.class && (wrapped instanceof Number && _ClassUtil.isNumerical(targetClass))) { + Number number = forceUnwrappedNumberToType((Number) wrapped, targetClass); + if (number != null) { + return number; + } + } + } + + // Translation of generic template models to POJOs. First give priority + // to various model interfaces based on the targetClass. This helps us + // select the appropriate interface in multi-interface models when we + // know what is expected as the return type. + if (targetClass != Object.class) { + + // [2.4][IcI]: Should also check for CharSequence at the end + if (String.class == targetClass) { + if (model instanceof TemplateScalarModel) { + return ((TemplateScalarModel) model).getAsString(); + } + // String is final, so no other conversion will work + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } + + // Primitive numeric types & Number.class and its subclasses + if (_ClassUtil.isNumerical(targetClass)) { + if (model instanceof TemplateNumberModel) { + Number number = forceUnwrappedNumberToType( + ((TemplateNumberModel) model).getAsNumber(), targetClass); + if (number != null) { + return number; + } + } + } + + if (boolean.class == targetClass || Boolean.class == targetClass) { + if (model instanceof TemplateBooleanModel) { + return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean()); + } + // Boolean is final, no other conversion will work + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } + + if (Map.class == targetClass) { + if (model instanceof TemplateHashModel) { + return new HashAdapter((TemplateHashModel) model, this); + } + } + + if (List.class == targetClass) { + if (model instanceof TemplateSequenceModel) { + return new SequenceAdapter((TemplateSequenceModel) model, this); + } + } + + if (Set.class == targetClass) { + if (model instanceof TemplateCollectionModel) { + return new SetAdapter((TemplateCollectionModel) model, this); + } + } + + if (Collection.class == targetClass || Iterable.class == targetClass) { + if (model instanceof TemplateCollectionModel) { + return new CollectionAdapter((TemplateCollectionModel) model, + this); + } + if (model instanceof TemplateSequenceModel) { + return new SequenceAdapter((TemplateSequenceModel) model, this); + } + } + + // TemplateSequenceModels can be converted to arrays + if (targetClass.isArray()) { + if (model instanceof TemplateSequenceModel) { + return unwrapSequenceToArray((TemplateSequenceModel) model, targetClass, true, recursionStops); + } + // array classes are final, no other conversion will work + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } + + // Allow one-char strings to be coerced to characters + if (char.class == targetClass || targetClass == Character.class) { + if (model instanceof TemplateScalarModel) { + String s = ((TemplateScalarModel) model).getAsString(); + if (s.length() == 1) { + return Character.valueOf(s.charAt(0)); + } + } + // Character is final, no other conversion will work + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } + + if (Date.class.isAssignableFrom(targetClass) && model instanceof TemplateDateModel) { + Date date = ((TemplateDateModel) model).getAsDate(); + if (targetClass.isInstance(date)) { + return date; + } + } + } // End: if (targetClass != Object.class) + + // Since the targetClass was of no help initially, now we use + // a quite arbitrary order in which we walk through the TemplateModel subinterfaces, and unwrapp them to + // their "natural" Java correspondent. We still try exclude unwrappings that won't fit the target parameter + // type(s). This is mostly important because of multi-typed FTL values that could be unwrapped on multiple ways. + int itf = typeFlags; // Iteration's Type Flags. Should be always 0 for non-overloaded and when !is2321Bugfixed. + // If itf != 0, we possibly execute the following loop body at twice: once with utilizing itf, and if it has not + // returned, once more with itf == 0. Otherwise we execute this once with itf == 0. + do { + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_NUMBER) != 0) + && model instanceof TemplateNumberModel) { + Number number = ((TemplateNumberModel) model).getAsNumber(); + if (itf != 0 || targetClass.isInstance(number)) { + return number; + } + } + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_DATE) != 0) + && model instanceof TemplateDateModel) { + Date date = ((TemplateDateModel) model).getAsDate(); + if (itf != 0 || targetClass.isInstance(date)) { + return date; + } + } + if ((itf == 0 || (itf & (TypeFlags.ACCEPTS_STRING | TypeFlags.CHARACTER)) != 0) + && model instanceof TemplateScalarModel + && (itf != 0 || targetClass.isAssignableFrom(String.class))) { + String strVal = ((TemplateScalarModel) model).getAsString(); + if (itf == 0 || (itf & TypeFlags.CHARACTER) == 0) { + return strVal; + } else { // TypeFlags.CHAR == 1 + if (strVal.length() == 1) { + if ((itf & TypeFlags.ACCEPTS_STRING) != 0) { + return new CharacterOrString(strVal); + } else { + return Character.valueOf(strVal.charAt(0)); + } + } else if ((itf & TypeFlags.ACCEPTS_STRING) != 0) { + return strVal; + } + // It had to be unwrapped to Character, but the string length wasn't 1 => Fall through + } + } + // Should be earlier than TemplateScalarModel, but we keep it here until FM 2.4 or such + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_BOOLEAN) != 0) + && model instanceof TemplateBooleanModel + && (itf != 0 || targetClass.isAssignableFrom(Boolean.class))) { + return Boolean.valueOf(((TemplateBooleanModel) model).getAsBoolean()); + } + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_MAP) != 0) + && model instanceof TemplateHashModel + && (itf != 0 || targetClass.isAssignableFrom(HashAdapter.class))) { + return new HashAdapter((TemplateHashModel) model, this); + } + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_LIST) != 0) + && model instanceof TemplateSequenceModel + && (itf != 0 || targetClass.isAssignableFrom(SequenceAdapter.class))) { + return new SequenceAdapter((TemplateSequenceModel) model, this); + } + if ((itf == 0 || (itf & TypeFlags.ACCEPTS_SET) != 0) + && model instanceof TemplateCollectionModel + && (itf != 0 || targetClass.isAssignableFrom(SetAdapter.class))) { + return new SetAdapter((TemplateCollectionModel) model, this); + } + + if ((itf & TypeFlags.ACCEPTS_ARRAY) != 0 + && model instanceof TemplateSequenceModel) { + return new SequenceAdapter((TemplateSequenceModel) model, this); + } + + if (itf == 0) { + break; + } + itf = 0; // start 2nd iteration + } while (true); + + // Last ditch effort - is maybe the model itself is an instance of the required type? + // Note that this will be always true for Object.class targetClass. + if (targetClass.isInstance(model)) { + return model; + } + + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } + + /** + * @param tryOnly + * If {@code true}, if the conversion of an item to the component type isn't possible, the method returns + * {@link ObjectWrapperAndUnwrapper#CANT_UNWRAP_TO_TARGET_CLASS} instead of throwing a + * {@link TemplateModelException}. + */ + Object unwrapSequenceToArray( + TemplateSequenceModel seq, Class<?> arrayClass, boolean tryOnly, Map<Object, Object> recursionStops) + throws TemplateModelException { + if (recursionStops != null) { + Object retval = recursionStops.get(seq); + if (retval != null) { + return retval; + } + } else { + recursionStops = new IdentityHashMap<>(); + } + Class<?> componentType = arrayClass.getComponentType(); + Object array = Array.newInstance(componentType, seq.size()); + recursionStops.put(seq, array); + try { + final int size = seq.size(); + for (int i = 0; i < size; i++) { + final TemplateModel seqItem = seq.get(i); + Object val = tryUnwrapTo(seqItem, componentType, 0, recursionStops); + if (val == ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS) { + if (tryOnly) { + return ObjectWrapperAndUnwrapper.CANT_UNWRAP_TO_TARGET_CLASS; + } else { + throw new _TemplateModelException( + "Failed to convert ", new _DelayedFTLTypeDescription(seq), + " object to ", new _DelayedShortClassName(array.getClass()), + ": Problematic sequence item at index ", Integer.valueOf(i) ," with value type: ", + new _DelayedFTLTypeDescription(seqItem)); + } + + } + Array.set(array, i, val); + } + } finally { + recursionStops.remove(seq); } - return super.wrap(obj); + return array; } - - public TemplateModel wrapDomNode(Object obj) { - return NodeModel.wrap((Node) obj); + + Object listToArray(List<?> list, Class<?> arrayClass, Map<Object, Object> recursionStops) + throws TemplateModelException { + if (list instanceof SequenceAdapter) { + return unwrapSequenceToArray( + ((SequenceAdapter) list).getTemplateSequenceModel(), + arrayClass, false, + recursionStops); + } + + if (recursionStops != null) { + Object retval = recursionStops.get(list); + if (retval != null) { + return retval; + } + } else { + recursionStops = new IdentityHashMap<>(); + } + Class<?> componentType = arrayClass.getComponentType(); + Object array = Array.newInstance(componentType, list.size()); + recursionStops.put(list, array); + try { + boolean isComponentTypeExamined = false; + boolean isComponentTypeNumerical = false; // will be filled on demand + boolean isComponentTypeList = false; // will be filled on demand + int i = 0; + for (Object listItem : list) { + if (listItem != null && !componentType.isInstance(listItem)) { + // Type conversion is needed. If we can't do it, we just let it fail at Array.set later. + if (!isComponentTypeExamined) { + isComponentTypeNumerical = _ClassUtil.isNumerical(componentType); + isComponentTypeList = List.class.isAssignableFrom(componentType); + isComponentTypeExamined = true; + } + if (isComponentTypeNumerical && listItem instanceof Number) { + listItem = forceUnwrappedNumberToType((Number) listItem, componentType); + } else if (componentType == String.class && listItem instanceof Character) { + listItem = String.valueOf(((Character) listItem).charValue()); + } else if ((componentType == Character.class || componentType == char.class) + && listItem instanceof String) { + String listItemStr = (String) listItem; + if (listItemStr.length() == 1) { + listItem = Character.valueOf(listItemStr.charAt(0)); + } + } else if (componentType.isArray()) { + if (listItem instanceof List) { + listItem = listToArray((List<?>) listItem, componentType, recursionStops); + } else if (listItem instanceof TemplateSequenceModel) { + listItem = unwrapSequenceToArray((TemplateSequenceModel) listItem, componentType, false, recursionStops); + } + } else if (isComponentTypeList && listItem.getClass().isArray()) { + listItem = arrayToList(listItem); + } + } + try { + Array.set(array, i, listItem); + } catch (IllegalArgumentException e) { + throw new TemplateModelException( + "Failed to convert " + _ClassUtil.getShortClassNameOfObject(list) + + " object to " + _ClassUtil.getShortClassNameOfObject(array) + + ": Problematic List item at index " + i + " with value type: " + + _ClassUtil.getShortClassNameOfObject(listItem), e); + } + i++; + } + } finally { + recursionStops.remove(list); + } + return array; } /** - * Converts an array to a java.util.List. + * @param array Must be an array (of either a reference or primitive type) */ - protected Object convertArray(Object arr) { - // FM 2.4: Use Arrays.asList instead - final int size = Array.getLength(arr); - ArrayList list = new ArrayList(size); - for (int i = 0; i < size; i++) { - list.add(Array.get(arr, i)); + List<?> arrayToList(Object array) throws TemplateModelException { + if (array instanceof Object[]) { + // Array of any non-primitive type. + // Note that an array of non-primitive type is always instanceof Object[]. + Object[] objArray = (Object[]) array; + return objArray.length == 0 ? Collections.EMPTY_LIST : new NonPrimitiveArrayBackedReadOnlyList(objArray); + } else { + // Array of any primitive type + return Array.getLength(array) == 0 ? Collections.EMPTY_LIST : new PrimtiveArrayBackedReadOnlyList(array); + } + } + + /** + * Converts a number to the target type aggressively (possibly with overflow or significant loss of precision). + * @param n Non-{@code null} + * @return {@code null} if the conversion has failed. + */ + static Number forceUnwrappedNumberToType(final Number n, final Class<?> targetType) { + // We try to order the conditions by decreasing probability. + if (targetType == n.getClass()) { + return n; + } else if (targetType == int.class || targetType == Integer.class) { + return n instanceof Integer ? (Integer) n : Integer.valueOf(n.intValue()); + } else if (targetType == long.class || targetType == Long.class) { + return n instanceof Long ? (Long) n : Long.valueOf(n.longValue()); + } else if (targetType == double.class || targetType == Double.class) { + return n instanceof Double ? (Double) n : Double.valueOf(n.doubleValue()); + } else if (targetType == BigDecimal.class) { + if (n instanceof BigDecimal) { + return n; + } else if (n instanceof BigInteger) { + return new BigDecimal((BigInteger) n); + } else if (n instanceof Long) { + // Because we can't represent long accurately as double + return BigDecimal.valueOf(n.longValue()); + } else { + return new BigDecimal(n.doubleValue()); + } + } else if (targetType == float.class || targetType == Float.class) { + return n instanceof Float ? (Float) n : Float.valueOf(n.floatValue()); + } else if (targetType == byte.class || targetType == Byte.class) { + return n instanceof Byte ? (Byte) n : Byte.valueOf(n.byteValue()); + } else if (targetType == short.class || targetType == Short.class) { + return n instanceof Short ? (Short) n : Short.valueOf(n.shortValue()); + } else if (targetType == BigInteger.class) { + if (n instanceof BigInteger) { + return n; + } else { + if (n instanceof OverloadedNumberUtil.IntegerBigDecimal) { + return ((OverloadedNumberUtil.IntegerBigDecimal) n).bigIntegerValue(); + } else if (n instanceof BigDecimal) { + return ((BigDecimal) n).toBigInteger(); + } else { + return BigInteger.valueOf(n.longValue()); + } + } + } else { + final Number oriN = n instanceof OverloadedNumberUtil.NumberWithFallbackType + ? ((OverloadedNumberUtil.NumberWithFallbackType) n).getSourceNumber() : n; + if (targetType.isInstance(oriN)) { + // Handle nonstandard Number subclasses as well as directly java.lang.Number. + return oriN; + } else { + // Fails + return null; + } + } + } + + /** + * Invokes the specified method, wrapping the return value. The specialty + * of this method is that if the return value is null, and the return type + * of the invoked method is void, {@link TemplateModel#NOTHING} is returned. + * @param object the object to invoke the method on + * @param method the method to invoke + * @param args the arguments to the method + * @return the wrapped return value of the method. + * @throws InvocationTargetException if the invoked method threw an exception + * @throws IllegalAccessException if the method can't be invoked due to an + * access restriction. + * @throws TemplateModelException if the return value couldn't be wrapped + * (this can happen if the wrapper has an outer identity or is subclassed, + * and the outer identity or the subclass throws an exception. Plain + * DefaultObjectWrapper never throws TemplateModelException). + */ + TemplateModel invokeMethod(Object object, Method method, Object[] args) + throws InvocationTargetException, + IllegalAccessException, + TemplateModelException { + // [2.4]: Java's Method.invoke truncates numbers if the target type has not enough bits to hold the value. + // There should at least be an option to check this. + Object retval = method.invoke(object, args); + return + method.getReturnType() == void.class + ? TemplateModel.NOTHING + : getOuterIdentity().wrap(retval); + } + + /** + * Returns a hash model that represents the so-called class static models. + * Every class static model is itself a hash through which you can call + * static methods on the specified class. To obtain a static model for a + * class, get the element of this hash with the fully qualified class name. + * For example, if you place this hash model inside the root data model + * under name "statics", you can use i.e. <code>statics["java.lang. + * System"]. currentTimeMillis()</code> to call the {@link + * java.lang.System#currentTimeMillis()} method. + * @return a hash model whose keys are fully qualified class names, and + * that returns hash models whose elements are the static models of the + * classes. + */ + public TemplateHashModel getStaticModels() { + return staticModels; + } + + /** + * Returns a hash model that represents the so-called class enum models. + * Every class' enum model is itself a hash through which you can access + * enum value declared by the specified class, assuming that class is an + * enumeration. To obtain an enum model for a class, get the element of this + * hash with the fully qualified class name. For example, if you place this + * hash model inside the root data model under name "enums", you can use + * i.e. <code>statics["java.math.RoundingMode"].UP</code> to access the + * {@link java.math.RoundingMode#UP} value. + * @return a hash model whose keys are fully qualified class names, and + * that returns hash models whose elements are the enum models of the + * classes. + */ + public TemplateHashModel getEnumModels() { + return enumModels; + } + + /** For Unit tests only */ + ModelCache getModelCache() { + return modelCache; + } + + /** + * Creates a new instance of the specified class using the method call logic of this object wrapper for calling the + * constructor. Overloaded constructors and varargs are supported. Only public constructors will be called. + * + * @param clazz The class whose constructor we will call. + * @param arguments The list of {@link TemplateModel}-s to pass to the constructor after unwrapping them + * @return The instance created; it's not wrapped into {@link TemplateModel}. + */ + public Object newInstance(Class<?> clazz, List/*<? extends TemplateModel>*/ arguments) + throws TemplateModelException { + try { + Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY); + if (ctors == null) { + throw new TemplateModelException("Class " + clazz.getName() + + " has no public constructors."); + } + Constructor<?> ctor = null; + Object[] objargs; + if (ctors instanceof SimpleMethod) { + SimpleMethod sm = (SimpleMethod) ctors; + ctor = (Constructor<?>) sm.getMember(); + objargs = sm.unwrapArguments(arguments, this); + try { + return ctor.newInstance(objargs); + } catch (Exception e) { + if (e instanceof TemplateModelException) throw (TemplateModelException) e; + throw _MethodUtil.newInvocationTemplateModelException(null, ctor, e); + } + } else if (ctors instanceof OverloadedMethods) { + final MemberAndArguments mma = ((OverloadedMethods) ctors).getMemberAndArguments(arguments, this); + try { + return mma.invokeConstructor(this); + } catch (Exception e) { + if (e instanceof TemplateModelException) throw (TemplateModelException) e; + + throw _MethodUtil.newInvocationTemplateModelException(null, mma.getCallableMemberDescriptor(), e); + } + } else { + // Cannot happen + throw new BugException(); + } + } catch (TemplateModelException e) { + throw e; + } catch (Exception e) { + throw new TemplateModelException( + "Error while creating new instance of class " + clazz.getName() + "; see cause exception", e); + } + } + + /** + * Removes the introspection data for a class from the cache. + * Use this if you know that a class is not used anymore in templates. + * If the class will be still used, the cache entry will be silently + * re-created, so this isn't a dangerous operation. + * + * @since 2.3.20 + */ + public void removeFromClassIntrospectionCache(Class<?> clazz) { + classIntrospector.remove(clazz); + } + + /** + * Removes all class introspection data from the cache. + * + * <p>Use this if you want to free up memory on the expense of recreating + * the cache entries for the classes that will be used later in templates. + * + * @throws IllegalStateException if {@link #isClassIntrospectionCacheRestricted()} is {@code true}. + * + * @since 2.3.20 + */ + public void clearClassIntrospecitonCache() { + classIntrospector.clearCache(); + } + + ClassIntrospector getClassIntrospector() { + return classIntrospector; + } + + /** + * Converts any {@link BigDecimal}s in the passed array to the type of + * the corresponding formal argument of the method. + */ + // Unused? + public static void coerceBigDecimals(AccessibleObject callable, Object[] args) { + Class<?>[] formalTypes = null; + for (int i = 0; i < args.length; ++i) { + Object arg = args[i]; + if (arg instanceof BigDecimal) { + if (formalTypes == null) { + if (callable instanceof Method) { + formalTypes = ((Method) callable).getParameterTypes(); + } else if (callable instanceof Constructor) { + formalTypes = ((Constructor<?>) callable).getParameterTypes(); + } else { + throw new IllegalArgumentException("Expected method or " + + " constructor; callable is " + + callable.getClass().getName()); + } + } + args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]); + } + } + } + + /** + * Converts any {@link BigDecimal}-s in the passed array to the type of + * the corresponding formal argument of the method via {@link #coerceBigDecimal(BigDecimal, Class)}. + */ + public static void coerceBigDecimals(Class<?>[] formalTypes, Object[] args) { + int typeLen = formalTypes.length; + int argsLen = args.length; + int min = Math.min(typeLen, argsLen); + for (int i = 0; i < min; ++i) { + Object arg = args[i]; + if (arg instanceof BigDecimal) { + args[i] = coerceBigDecimal((BigDecimal) arg, formalTypes[i]); + } + } + if (argsLen > typeLen) { + Class<?> varArgType = formalTypes[typeLen - 1]; + for (int i = typeLen; i < argsLen; ++i) { + Object arg = args[i]; + if (arg instanceof BigDecimal) { + args[i] = coerceBigDecimal((BigDecimal) arg, varArgType); + } + } + } + } + + /** + * Converts {@link BigDecimal} to the class given in the {@code formalType} argument if that's a known numerical + * type, returns the {@link BigDecimal} as is otherwise. Overflow and precision loss are possible, similarly as + * with casting in Java. + */ + public static Object coerceBigDecimal(BigDecimal bd, Class<?> formalType) { + // int is expected in most situations, so we check it first + if (formalType == int.class || formalType == Integer.class) { + return Integer.valueOf(bd.intValue()); + } else if (formalType == double.class || formalType == Double.class) { + return Double.valueOf(bd.doubleValue()); + } else if (formalType == long.class || formalType == Long.class) { + return Long.valueOf(bd.longValue()); + } else if (formalType == float.class || formalType == Float.class) { + return Float.valueOf(bd.floatValue()); + } else if (formalType == short.class || formalType == Short.class) { + return Short.valueOf(bd.shortValue()); + } else if (formalType == byte.class || formalType == Byte.class) { + return Byte.valueOf(bd.byteValue()); + } else if (java.math.BigInteger.class.isAssignableFrom(formalType)) { + return bd.toBigInteger(); + } else { + return bd; } - return list; } /** * Returns the lowest version number that is equivalent with the parameter version. - * + * * @since 2.3.22 */ protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { - return BeansWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements); + _CoreAPI.checkVersionNotNullAndSupported(incompatibleImprovements); + return Configuration.VERSION_3_0_0; } + /** - * @since 2.3.22 + * Returns the name-value pairs that describe the configuration of this {@link DefaultObjectWrapper}; called from + * {@link #toString()}. The expected format is like {@code "foo=bar, baaz=wombat"}. When overriding this, you should + * call the super method, and then insert the content before it with a following {@code ", "}, or after it with a + * preceding {@code ", "}. */ - @Override protected String toPropertiesString() { - String bwProps = super.toPropertiesString(); - - // Remove simpleMapWrapper, as its irrelevant for this wrapper: - if (bwProps.startsWith("simpleMapWrapper")) { - int smwEnd = bwProps.indexOf(','); - if (smwEnd != -1) { - bwProps = bwProps.substring(smwEnd + 1).trim(); - } - } - - return ""; + // Start with "simpleMapWrapper", because the override in DefaultObjectWrapper expects it to be there! + return "exposureLevel=" + classIntrospector.getExposureLevel() + ", " + + "exposeFields=" + classIntrospector.getExposeFields() + ", " + + "sharedClassIntrospCache=" + + (classIntrospector.isShared() ? "@" + System.identityHashCode(classIntrospector) : "none"); } - + + /** + * Returns the exact class name and the identity hash, also the values of the most often used + * {@link DefaultObjectWrapper} configuration properties, also if which (if any) shared class introspection + * cache it uses. + */ + @Override + public String toString() { + final String propsStr = toPropertiesString(); + return _ClassUtil.getShortClassNameOfObject(this) + "@" + System.identityHashCode(this) + + "(" + incompatibleImprovements + ", " + + (propsStr.length() != 0 ? propsStr + ", ..." : "") + + ")"; + } + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/051a0822/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java index 40768f8..89af870 100644 --- a/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java +++ b/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperBuilder.java @@ -25,17 +25,85 @@ import java.util.Map; import java.util.WeakHashMap; import org.apache.freemarker.core.Version; -import org.apache.freemarker.core.model.impl.beans.BeansWrapperBuilder; -import org.apache.freemarker.core.model.impl.beans._BeansAPI; +import org.apache.freemarker.core.model.TemplateModel; /** - * Gets/creates a {@link DefaultObjectWrapper} singleton instance that's already configured as specified in the - * properties of this object; this is recommended over using the {@link DefaultObjectWrapper} constructors. The returned - * instance can't be further configured (it's write protected). - * - * <p>See {@link BeansWrapperBuilder} for more info, as that works identically. - * - * @since 2.3.21 + * Gets/creates a {@link DefaultObjectWrapper} singleton instance that's already configured as specified in the properties of + * this object; this is recommended over using the {@link DefaultObjectWrapper} constructors. The returned instance can't be + * further configured (it's write protected). + * + * <p>The builder meant to be used as a drop-away object (not stored in a field), like in this example: + * <pre> + * DefaultObjectWrapper dow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21).build(); + * </pre> + * + * <p>Or, a more complex example:</p> + * <pre> + * // Create the builder: + * DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21); + * // Set desired DefaultObjectWrapper configuration properties: + * builder.setUseModelCache(true); + * builder.setExposeFields(true); + * + * // Get the singleton: + * DefaultObjectWrapper dow = builder.build(); + * // You don't need the builder anymore. + * </pre> + * + * <p>Despite that builders aren't meant to be used as long-lived objects (singletons), the builder is thread-safe after + * you have stopped calling its setters and it was safely published (see JSR 133) to other threads. This can be useful + * if you have to put the builder into an IoC container, rather than the singleton it produces. + * + * <p>The main benefit of using a builder instead of a {@link DefaultObjectWrapper} constructor is that this way the + * internal object wrapping-related caches (most notably the class introspection cache) will come from a global, + * JVM-level (more precisely, {@code freemarker-core.jar}-class-loader-level) cache. Also the + * {@link DefaultObjectWrapper} singletons + * themselves are stored in this global cache. Some of the wrapping-related caches are expensive to build and can take + * significant amount of memory. Using builders, components that use FreeMarker will share {@link DefaultObjectWrapper} + * instances and said caches even if they use separate FreeMarker {@link org.apache.freemarker.core.Configuration}-s. (Many Java libraries use + * FreeMarker internally, so {@link org.apache.freemarker.core.Configuration} sharing is not an option.) + * + * <p>Note that the returned {@link DefaultObjectWrapper} instances are only weak-referenced from inside the builder mechanism, + * so singletons are garbage collected when they go out of usage, just like non-singletons. + * + * <p>About the object wrapping-related caches: + * <ul> + * <li><p>Class introspection cache: Stores information about classes that once had to be wrapped. The cache is + * stored in the static fields of certain FreeMarker classes. Thus, if you have two {@link DefaultObjectWrapper} + * instances, they might share the same class introspection cache. But if you have two + * {@code freemarker.jar}-s (typically, in two Web Application's {@code WEB-INF/lib} directories), those won't + * share their caches (as they don't share the same FreeMarker classes). + * Also, currently there's a separate cache for each permutation of the property values that influence class + * introspection: {@link DefaultObjectWrapperBuilder#setExposeFields(boolean) expose_fields} and + * {@link DefaultObjectWrapperBuilder#setExposureLevel(int) exposure_level}. So only {@link DefaultObjectWrapper} where those + * properties are the same may share class introspection caches among each other. + * </li> + * <li><p>Model caches: These are local to a {@link DefaultObjectWrapper}. {@link DefaultObjectWrapperBuilder} returns the same + * {@link DefaultObjectWrapper} instance for equivalent properties (unless the existing instance was garbage collected + * and thus a new one had to be created), hence these caches will be re-used too. {@link DefaultObjectWrapper} instances + * are cached in the static fields of FreeMarker too, but there's a separate cache for each + * Thread Context Class Loader, which in a servlet container practically means a separate cache for each Web + * Application (each servlet context). (This is like so because for resolving class names to classes FreeMarker + * uses the Thread Context Class Loader, so the result of the resolution can be different for different + * Thread Context Class Loaders.) The model caches are: + * <ul> + * <li><p> + * Static model caches: These are used by the hash returned by {@link DefaultObjectWrapper#getEnumModels()} and + * {@link DefaultObjectWrapper#getStaticModels()}, for caching {@link TemplateModel}-s for the static methods/fields + * and Java enums that were accessed through them. To use said hashes, you have to put them + * explicitly into the data-model or expose them to the template explicitly otherwise, so in most applications + * these caches aren't unused. + * </li> + * <li><p> + * Instance model cache: By default off (see {@link DefaultObjectWrapper#setUseModelCache(boolean)}). Caches the + * {@link TemplateModel}-s for all Java objects that were accessed from templates. + * </li> + * </ul> + * </li> + * </ul> + * + * <p>Note that what this method documentation says about {@link DefaultObjectWrapper} also applies to + * {@link DefaultObjectWrapperBuilder}. */ public class DefaultObjectWrapperBuilder extends DefaultObjectWrapperConfiguration { @@ -65,12 +133,20 @@ public class DefaultObjectWrapperBuilder extends DefaultObjectWrapperConfigurati * a singleton that is also in use elsewhere. */ public DefaultObjectWrapper build() { - return _BeansAPI.getBeansWrapperSubclassSingleton( + return _ModelAPI.getDefaultObjectWrapperSubclassSingleton( this, INSTANCE_CACHE, INSTANCE_CACHE_REF_QUEUE, DefaultObjectWrapperFactory.INSTANCE); } - + + /** + * For unit testing only + */ + static Map<ClassLoader, Map<DefaultObjectWrapperConfiguration, WeakReference<DefaultObjectWrapper>>> + getInstanceCache() { + return INSTANCE_CACHE; + } + private static class DefaultObjectWrapperFactory - implements _BeansAPI._BeansWrapperSubclassFactory<DefaultObjectWrapper, DefaultObjectWrapperConfiguration> { + implements _ModelAPI._DefaultObjectWrapperSubclassFactory<DefaultObjectWrapper, DefaultObjectWrapperConfiguration> { private static final DefaultObjectWrapperFactory INSTANCE = new DefaultObjectWrapperFactory();
