You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:53:07 UTC
[24/51] [partial] incubator-freemarker git commit: Migrated from Ant
to Gradle, and modularized the project. This is an incomplete migration;
there are some TODO-s in the build scripts, and release related tasks are
still missing. What works: Building th
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
new file mode 100644
index 0000000..2159f31
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -0,0 +1,1263 @@
+/*
+ * 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.beans.BeanInfo;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonBuilder;
+import org.apache.freemarker.core.util._JavaVersions;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+
+/**
+ * Returns information about a {@link Class} that's useful for FreeMarker. Encapsulates a cache for this. Thread-safe,
+ * doesn't even require "proper publishing" starting from 2.3.24 or Java 5. Immutable, with the exception of the
+ * internal caches.
+ *
+ * <p>
+ * Note that instances of this are cached on the level of FreeMarker's defining class loader. Hence, it must not do
+ * operations that depend on the Thread Context Class Loader, such as resolving class names.
+ */
+class ClassIntrospector {
+
+ // Attention: This class must be thread-safe (not just after proper publishing). This is important as some of
+ // these are shared by many object wrappers, and concurrency related glitches due to user errors must remain
+ // local to the object wrappers, not corrupting the shared ClassIntrospector.
+
+ private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER;
+
+ private static final String JREBEL_SDK_CLASS_NAME = "org.zeroturnaround.javarebel.ClassEventListener";
+ private static final String JREBEL_INTEGRATION_ERROR_MSG
+ = "Error initializing JRebel integration. JRebel integration disabled.";
+
+ private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER;
+ static {
+ boolean jRebelAvailable;
+ try {
+ Class.forName(JREBEL_SDK_CLASS_NAME);
+ jRebelAvailable = true;
+ } catch (Throwable e) {
+ jRebelAvailable = false;
+ try {
+ if (!(e instanceof ClassNotFoundException)) {
+ LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+ }
+ } catch (Throwable loggingE) {
+ // ignore
+ }
+ }
+
+ ClassChangeNotifier classChangeNotifier;
+ if (jRebelAvailable) {
+ try {
+ classChangeNotifier = (ClassChangeNotifier)
+ Class.forName("org.apache.freemarker.core.model.impl.JRebelClassChangeNotifier").newInstance();
+ } catch (Throwable e) {
+ classChangeNotifier = null;
+ try {
+ LOG.error(JREBEL_INTEGRATION_ERROR_MSG, e);
+ } catch (Throwable loggingE) {
+ // ignore
+ }
+ }
+ } else {
+ classChangeNotifier = null;
+ }
+
+ CLASS_CHANGE_NOTIFIER = classChangeNotifier;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Introspection info Map keys:
+
+ /** Key in the class info Map to the Map that maps method to argument type arrays */
+ private static final Object ARG_TYPES_BY_METHOD_KEY = new Object();
+ /** Key in the class info Map to the object that represents the constructors (one or multiple due to overloading) */
+ static final Object CONSTRUCTORS_KEY = new Object();
+ /** Key in the class info Map to the get(String|Object) Method */
+ static final Object GENERIC_GET_KEY = new Object();
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Introspection configuration properties:
+
+ // Note: These all must be *declared* final (or else synchronization is needed everywhere where they are accessed).
+
+ final int exposureLevel;
+ final boolean exposeFields;
+ final MethodAppearanceFineTuner methodAppearanceFineTuner;
+ final MethodSorter methodSorter;
+
+ /** See {@link #getHasSharedInstanceRestrictons()} */
+ final private boolean hasSharedInstanceRestrictons;
+
+ /** See {@link #isShared()} */
+ final private boolean shared;
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // State fields:
+
+ private final Object sharedLock;
+ private final Map<Class<?>, Map<Object, Object>> cache
+ = new ConcurrentHashMap<>(0, 0.75f, 16);
+ private final Set<String> cacheClassNames = new HashSet<>(0);
+ private final Set<Class<?>> classIntrospectionsInProgress = new HashSet<>(0);
+
+ private final List<WeakReference<Object/*ClassBasedModelFactory|ModelCache>*/>> modelFactories
+ = new LinkedList<>();
+ private final ReferenceQueue<Object> modelFactoriesRefQueue = new ReferenceQueue<>();
+
+ private int clearingCounter;
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Instantiation:
+
+ /**
+ * Creates a new instance, that is hence surely not shared (singleton) instance.
+ *
+ * @param pa
+ * Stores what the values of the JavaBean properties of the returned instance will be. Not {@code null}.
+ */
+ ClassIntrospector(Builder pa, Object sharedLock) {
+ this(pa, sharedLock, false, false);
+ }
+
+ /**
+ * @param hasSharedInstanceRestrictons
+ * {@code true} exactly if we are creating a new instance with {@link Builder}. Then
+ * it's {@code true} even if it won't put the instance into the cache.
+ */
+ ClassIntrospector(Builder builder, Object sharedLock,
+ boolean hasSharedInstanceRestrictons, boolean shared) {
+ _NullArgumentException.check("sharedLock", sharedLock);
+
+ exposureLevel = builder.getExposureLevel();
+ exposeFields = builder.getExposeFields();
+ methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
+ methodSorter = builder.getMethodSorter();
+
+ this.sharedLock = sharedLock;
+
+ this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons;
+ this.shared = shared;
+
+ if (CLASS_CHANGE_NOTIFIER != null) {
+ CLASS_CHANGE_NOTIFIER.subscribe(this);
+ }
+ }
+
+ /**
+ * Returns a {@link Builder}-s that could be used to invoke an identical {@link #ClassIntrospector}
+ * . The returned {@link Builder} can be modified without interfering with anything.
+ */
+ Builder createBuilder() {
+ return new Builder(this);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------
+ // Introspection:
+
+ /**
+ * Gets the class introspection data from {@link #cache}, automatically creating the cache entry if it's missing.
+ *
+ * @return A {@link Map} where each key is a property/method/field name (or a special {@link Object} key like
+ * {@link #CONSTRUCTORS_KEY}), each value is a {@link PropertyDescriptor} or {@link Method} or
+ * {@link OverloadedMethods} or {@link Field} (but better check the source code...).
+ */
+ Map<Object, Object> get(Class<?> clazz) {
+ {
+ Map<Object, Object> introspData = cache.get(clazz);
+ if (introspData != null) return introspData;
+ }
+
+ String className;
+ synchronized (sharedLock) {
+ Map<Object, Object> introspData = cache.get(clazz);
+ if (introspData != null) return introspData;
+
+ className = clazz.getName();
+ if (cacheClassNames.contains(className)) {
+ onSameNameClassesDetected(className);
+ }
+
+ while (introspData == null && classIntrospectionsInProgress.contains(clazz)) {
+ // Another thread is already introspecting this class;
+ // waiting for its result.
+ try {
+ sharedLock.wait();
+ introspData = cache.get(clazz);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(
+ "Class inrospection data lookup aborded: " + e);
+ }
+ }
+ if (introspData != null) return introspData;
+
+ // This will be the thread that introspects this class.
+ classIntrospectionsInProgress.add(clazz);
+ }
+ try {
+ Map<Object, Object> introspData = createClassIntrospectionData(clazz);
+ synchronized (sharedLock) {
+ cache.put(clazz, introspData);
+ cacheClassNames.add(className);
+ }
+ return introspData;
+ } finally {
+ synchronized (sharedLock) {
+ classIntrospectionsInProgress.remove(clazz);
+ sharedLock.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link Map} with the content as described for the return value of {@link #get(Class)}.
+ */
+ private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
+ final Map<Object, Object> introspData = new HashMap<>();
+
+ if (exposeFields) {
+ addFieldsToClassIntrospectionData(introspData, clazz);
+ }
+
+ final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+
+ addGenericGetToClassIntrospectionData(introspData, accessibleMethods);
+
+ if (exposureLevel != DefaultObjectWrapper.EXPOSE_NOTHING) {
+ try {
+ addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods);
+ } catch (IntrospectionException e) {
+ LOG.warn("Couldn't properly perform introspection for class {}", clazz.getName(), e);
+ introspData.clear(); // FIXME NBC: Don't drop everything here.
+ }
+ }
+
+ addConstructorsToClassIntrospectionData(introspData, clazz);
+
+ if (introspData.size() > 1) {
+ return introspData;
+ } else if (introspData.size() == 0) {
+ return Collections.emptyMap();
+ } else { // map.size() == 1
+ Entry<Object, Object> e = introspData.entrySet().iterator().next();
+ return Collections.singletonMap(e.getKey(), e.getValue());
+ }
+ }
+
+ private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz)
+ throws SecurityException {
+ for (Field field : clazz.getFields()) {
+ if ((field.getModifiers() & Modifier.STATIC) == 0) {
+ introspData.put(field.getName(), field);
+ }
+ }
+ }
+
+ private void addBeanInfoToClassIntrospectionData(
+ Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods)
+ throws IntrospectionException {
+ BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
+ List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz);
+ int pdasLength = pdas.size();
+ // Reverse order shouldn't mater, but we keep it to not risk backward incompatibility.
+ for (int i = pdasLength - 1; i >= 0; --i) {
+ addPropertyDescriptorToClassIntrospectionData(
+ introspData, pdas.get(i), clazz,
+ accessibleMethods);
+ }
+
+ if (exposureLevel < DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
+ final MethodAppearanceFineTuner.Decision decision = new MethodAppearanceFineTuner.Decision();
+ MethodAppearanceFineTuner.DecisionInput decisionInput = null;
+ List<MethodDescriptor> mds = getMethodDescriptors(beanInfo, clazz);
+ sortMethodDescriptors(mds);
+ int mdsSize = mds.size();
+ IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null;
+ for (int i = mdsSize - 1; i >= 0; --i) {
+ final MethodDescriptor md = mds.get(i);
+ final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods);
+ if (method != null && isAllowedToExpose(method)) {
+ decision.setDefaults(method);
+ if (methodAppearanceFineTuner != null) {
+ if (decisionInput == null) {
+ decisionInput = new MethodAppearanceFineTuner.DecisionInput();
+ }
+ decisionInput.setContainingClass(clazz);
+ decisionInput.setMethod(method);
+
+ methodAppearanceFineTuner.process(decisionInput, decision);
+ }
+
+ PropertyDescriptor propDesc = decision.getExposeAsProperty();
+ if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof PropertyDescriptor)) {
+ addPropertyDescriptorToClassIntrospectionData(
+ introspData, propDesc, clazz, accessibleMethods);
+ }
+
+ String methodKey = decision.getExposeMethodAs();
+ if (methodKey != null) {
+ Object previous = introspData.get(methodKey);
+ if (previous instanceof Method) {
+ // Overloaded method - replace Method with a OverloadedMethods
+ OverloadedMethods overloadedMethods = new OverloadedMethods();
+ overloadedMethods.addMethod((Method) previous);
+ overloadedMethods.addMethod(method);
+ introspData.put(methodKey, overloadedMethods);
+ // Remove parameter type information (unless an indexed property reader needs it):
+ if (argTypesUsedByIndexerPropReaders == null
+ || !argTypesUsedByIndexerPropReaders.containsKey(previous)) {
+ getArgTypesByMethod(introspData).remove(previous);
+ }
+ } else if (previous instanceof OverloadedMethods) {
+ // Already overloaded method - add new overload
+ ((OverloadedMethods) previous).addMethod(method);
+ } else if (decision.getMethodShadowsProperty()
+ || !(previous instanceof PropertyDescriptor)) {
+ // Simple method (this far)
+ introspData.put(methodKey, method);
+ Class<?>[] replaced = getArgTypesByMethod(introspData).put(method,
+ method.getParameterTypes());
+ if (replaced != null) {
+ if (argTypesUsedByIndexerPropReaders == null) {
+ argTypesUsedByIndexerPropReaders = new IdentityHashMap<Method, Void>();
+ }
+ argTypesUsedByIndexerPropReaders.put(method, null);
+ }
+ }
+ }
+ }
+ } // for each in mds
+ } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY)
+ }
+
+ /**
+ * Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal with Java 8 default methods too.
+ */
+ private List<PropertyDescriptor> getPropertyDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+ PropertyDescriptor[] introspectorPDsArray = beanInfo.getPropertyDescriptors();
+ List<PropertyDescriptor> introspectorPDs = introspectorPDsArray != null ? Arrays.asList(introspectorPDsArray)
+ : Collections.<PropertyDescriptor>emptyList();
+
+ if (_JavaVersions.JAVA_8 == null) {
+ // java.beans.Introspector was good enough then.
+ return introspectorPDs;
+ }
+
+ // introspectorPDs contains each property exactly once. But as now we will search them manually too, it can
+ // happen that we find the same property for multiple times. Worse, because of indexed properties, it's possible
+ // that we have to merge entries (like one has the normal reader method, the other has the indexed reader
+ // method), instead of just replacing them in a Map. That's why we have introduced PropertyReaderMethodPair,
+ // which holds the methods belonging to the same property name. IndexedPropertyDescriptor is not good for that,
+ // as it can't store two methods whose types are incompatible, and we have to wait until all the merging was
+ // done to see if the incompatibility goes away.
+
+ // This could be Map<String, PropertyReaderMethodPair>, but since we rarely need to do merging, we try to avoid
+ // creating those and use the source objects as much as possible. Also note that we initialize this lazily.
+ LinkedHashMap<String, Object /*PropertyReaderMethodPair|Method|PropertyDescriptor*/> mergedPRMPs = null;
+
+ // Collect Java 8 default methods that look like property readers into mergedPRMPs:
+ // (Note that java.beans.Introspector discovers non-accessible public methods, and to emulate that behavior
+ // here, we don't utilize the accessibleMethods Map, which we might already have at this point.)
+ for (Method method : clazz.getMethods()) {
+ if (_JavaVersions.JAVA_8.isDefaultMethod(method) && method.getReturnType() != void.class
+ && !method.isBridge()) {
+ Class<?>[] paramTypes = method.getParameterTypes();
+ if (paramTypes.length == 0
+ || paramTypes.length == 1 && paramTypes[0] == int.class /* indexed property reader */) {
+ String propName = _MethodUtil.getBeanPropertyNameFromReaderMethodName(
+ method.getName(), method.getReturnType());
+ if (propName != null) {
+ if (mergedPRMPs == null) {
+ // Lazy initialization
+ mergedPRMPs = new LinkedHashMap<String, Object>();
+ }
+ if (paramTypes.length == 0) {
+ mergeInPropertyReaderMethod(mergedPRMPs, propName, method);
+ } else { // It's an indexed property reader method
+ mergeInPropertyReaderMethodPair(mergedPRMPs, propName,
+ new PropertyReaderedMethodPair(null, method));
+ }
+ }
+ }
+ }
+ } // for clazz.getMethods()
+
+ if (mergedPRMPs == null) {
+ // We had no interfering Java 8 default methods, so we can chose the fast route.
+ return introspectorPDs;
+ }
+
+ for (PropertyDescriptor introspectorPD : introspectorPDs) {
+ mergeInPropertyDescriptor(mergedPRMPs, introspectorPD);
+ }
+
+ // Now we convert the PRMPs to PDs, handling case where the normal and the indexed read methods contradict.
+ List<PropertyDescriptor> mergedPDs = new ArrayList<PropertyDescriptor>(mergedPRMPs.size());
+ for (Entry<String, Object> entry : mergedPRMPs.entrySet()) {
+ String propName = entry.getKey();
+ Object propDescObj = entry.getValue();
+ if (propDescObj instanceof PropertyDescriptor) {
+ mergedPDs.add((PropertyDescriptor) propDescObj);
+ } else {
+ Method readMethod;
+ Method indexedReadMethod;
+ if (propDescObj instanceof Method) {
+ readMethod = (Method) propDescObj;
+ indexedReadMethod = null;
+ } else if (propDescObj instanceof PropertyReaderedMethodPair) {
+ PropertyReaderedMethodPair prmp = (PropertyReaderedMethodPair) propDescObj;
+ readMethod = prmp.readMethod;
+ indexedReadMethod = prmp.indexedReadMethod;
+ if (readMethod != null && indexedReadMethod != null
+ && indexedReadMethod.getReturnType() != readMethod.getReturnType().getComponentType()) {
+ // Here we copy the java.beans.Introspector behavior: If the array item class is not exactly the
+ // the same as the indexed read method return type, we say that the property is not indexed.
+ indexedReadMethod = null;
+ }
+ } else {
+ throw new BugException();
+ }
+ try {
+ mergedPDs.add(
+ indexedReadMethod != null
+ ? new IndexedPropertyDescriptor(propName,
+ readMethod, null, indexedReadMethod, null)
+ : new PropertyDescriptor(propName, readMethod, null));
+ } catch (IntrospectionException e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Failed creating property descriptor for " + clazz.getName() + " property " + propName,
+ e);
+ }
+ }
+ }
+ }
+ return mergedPDs;
+ }
+
+ private static class PropertyReaderedMethodPair {
+ private final Method readMethod;
+ private final Method indexedReadMethod;
+
+ PropertyReaderedMethodPair(Method readerMethod, Method indexedReaderMethod) {
+ this.readMethod = readerMethod;
+ this.indexedReadMethod = indexedReaderMethod;
+ }
+
+ PropertyReaderedMethodPair(PropertyDescriptor pd) {
+ this(
+ pd.getReadMethod(),
+ pd instanceof IndexedPropertyDescriptor
+ ? ((IndexedPropertyDescriptor) pd).getIndexedReadMethod() : null);
+ }
+
+ static PropertyReaderedMethodPair from(Object obj) {
+ if (obj instanceof PropertyReaderedMethodPair) {
+ return (PropertyReaderedMethodPair) obj;
+ } else if (obj instanceof PropertyDescriptor) {
+ return new PropertyReaderedMethodPair((PropertyDescriptor) obj);
+ } else if (obj instanceof Method) {
+ return new PropertyReaderedMethodPair((Method) obj, null);
+ } else {
+ throw new BugException("Unexpected obj type: " + obj.getClass().getName());
+ }
+ }
+
+ static PropertyReaderedMethodPair merge(PropertyReaderedMethodPair oldMethods, PropertyReaderedMethodPair newMethods) {
+ return new PropertyReaderedMethodPair(
+ newMethods.readMethod != null ? newMethods.readMethod : oldMethods.readMethod,
+ newMethods.indexedReadMethod != null ? newMethods.indexedReadMethod
+ : oldMethods.indexedReadMethod);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((indexedReadMethod == null) ? 0 : indexedReadMethod.hashCode());
+ result = prime * result + ((readMethod == null) ? 0 : readMethod.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ PropertyReaderedMethodPair other = (PropertyReaderedMethodPair) obj;
+ return other.readMethod == readMethod && other.indexedReadMethod == indexedReadMethod;
+ }
+
+ }
+
+ private void mergeInPropertyDescriptor(LinkedHashMap<String, Object> mergedPRMPs, PropertyDescriptor pd) {
+ String propName = pd.getName();
+ Object replaced = mergedPRMPs.put(propName, pd);
+ if (replaced != null) {
+ PropertyReaderedMethodPair newPRMP = new PropertyReaderedMethodPair(pd);
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRMP);
+ }
+ }
+
+ private void mergeInPropertyReaderMethodPair(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, PropertyReaderedMethodPair newPRM) {
+ Object replaced = mergedPRMPs.put(propName, newPRM);
+ if (replaced != null) {
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName, replaced, newPRM);
+ }
+ }
+
+ private void mergeInPropertyReaderMethod(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, Method readerMethod) {
+ Object replaced = mergedPRMPs.put(propName, readerMethod);
+ if (replaced != null) {
+ putIfMergedPropertyReaderMethodPairDiffers(mergedPRMPs, propName,
+ replaced, new PropertyReaderedMethodPair(readerMethod, null));
+ }
+ }
+
+ private void putIfMergedPropertyReaderMethodPairDiffers(LinkedHashMap<String, Object> mergedPRMPs,
+ String propName, Object replaced, PropertyReaderedMethodPair newPRMP) {
+ PropertyReaderedMethodPair replacedPRMP = PropertyReaderedMethodPair.from(replaced);
+ PropertyReaderedMethodPair mergedPRMP = PropertyReaderedMethodPair.merge(replacedPRMP, newPRMP);
+ if (!mergedPRMP.equals(newPRMP)) {
+ mergedPRMPs.put(propName, mergedPRMP);
+ }
+ }
+
+ /**
+ * Very similar to {@link BeanInfo#getMethodDescriptors()}, but can deal with Java 8 default methods too.
+ */
+ private List<MethodDescriptor> getMethodDescriptors(BeanInfo beanInfo, Class<?> clazz) {
+ MethodDescriptor[] introspectorMDArray = beanInfo.getMethodDescriptors();
+ List<MethodDescriptor> introspectionMDs = introspectorMDArray != null && introspectorMDArray.length != 0
+ ? Arrays.asList(introspectorMDArray) : Collections.<MethodDescriptor>emptyList();
+
+ if (_JavaVersions.JAVA_8 == null) {
+ // java.beans.Introspector was good enough then.
+ return introspectionMDs;
+ }
+
+ Map<String, List<Method>> defaultMethodsToAddByName = null;
+ for (Method method : clazz.getMethods()) {
+ if (_JavaVersions.JAVA_8.isDefaultMethod(method) && !method.isBridge()) {
+ if (defaultMethodsToAddByName == null) {
+ defaultMethodsToAddByName = new HashMap<String, List<Method>>();
+ }
+ List<Method> overloads = defaultMethodsToAddByName.get(method.getName());
+ if (overloads == null) {
+ overloads = new ArrayList<Method>(0);
+ defaultMethodsToAddByName.put(method.getName(), overloads);
+ }
+ overloads.add(method);
+ }
+ }
+
+ if (defaultMethodsToAddByName == null) {
+ // We had no interfering default methods:
+ return introspectionMDs;
+ }
+
+ // Recreate introspectionMDs so that its size can grow:
+ ArrayList<MethodDescriptor> newIntrospectionMDs
+ = new ArrayList<MethodDescriptor>(introspectionMDs.size() + 16);
+ for (MethodDescriptor introspectorMD : introspectionMDs) {
+ Method introspectorM = introspectorMD.getMethod();
+ // Prevent cases where the same method is added with different return types both from the list of default
+ // methods and from the list of Introspector-discovered methods, as that would lead to overloaded method
+ // selection ambiguity later. This is known to happen when the default method in an interface has reified
+ // return type, and then the interface is implemented by a class where the compiler generates an override
+ // for the bridge method only. (Other tricky cases might exist.)
+ if (!containsMethodWithSameParameterTypes(
+ defaultMethodsToAddByName.get(introspectorM.getName()), introspectorM)) {
+ newIntrospectionMDs.add(introspectorMD);
+ }
+ }
+ introspectionMDs = newIntrospectionMDs;
+
+ // Add default methods:
+ for (Entry<String, List<Method>> entry : defaultMethodsToAddByName.entrySet()) {
+ for (Method method : entry.getValue()) {
+ introspectionMDs.add(new MethodDescriptor(method));
+ }
+ }
+
+ return introspectionMDs;
+ }
+
+ private boolean containsMethodWithSameParameterTypes(List<Method> overloads, Method m) {
+ if (overloads == null) {
+ return false;
+ }
+
+ Class<?>[] paramTypes = m.getParameterTypes();
+ for (Method overload : overloads) {
+ if (Arrays.equals(overload.getParameterTypes(), paramTypes)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
+ PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
+ if (pd instanceof IndexedPropertyDescriptor) {
+ IndexedPropertyDescriptor ipd =
+ (IndexedPropertyDescriptor) pd;
+ Method readMethod = ipd.getIndexedReadMethod();
+ Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods);
+ if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) {
+ try {
+ if (readMethod != publicReadMethod) {
+ ipd = new IndexedPropertyDescriptor(
+ ipd.getName(), ipd.getReadMethod(),
+ null, publicReadMethod,
+ null);
+ }
+ introspData.put(ipd.getName(), ipd);
+ getArgTypesByMethod(introspData).put(publicReadMethod, publicReadMethod.getParameterTypes());
+ } catch (IntrospectionException e) {
+ LOG.warn("Failed creating a publicly-accessible property descriptor "
+ + "for {} indexed property {}, read method {}",
+ clazz.getName(), pd.getName(), publicReadMethod,
+ e);
+ }
+ }
+ } else {
+ Method readMethod = pd.getReadMethod();
+ Method publicReadMethod = getMatchingAccessibleMethod(readMethod, accessibleMethods);
+ if (publicReadMethod != null && isAllowedToExpose(publicReadMethod)) {
+ try {
+ if (readMethod != publicReadMethod) {
+ pd = new PropertyDescriptor(pd.getName(), publicReadMethod, null);
+ }
+ introspData.put(pd.getName(), pd);
+ } catch (IntrospectionException e) {
+ LOG.warn("Failed creating a publicly-accessible property descriptor "
+ + "for {} property {}, read method {}",
+ clazz.getName(), pd.getName(), publicReadMethod,
+ e);
+ }
+ }
+ }
+ }
+
+ private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
+ Map<MethodSignature, List<Method>> accessibleMethods) {
+ Method genericGet = getFirstAccessibleMethod(
+ MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+ if (genericGet == null) {
+ genericGet = getFirstAccessibleMethod(
+ MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+ }
+ if (genericGet != null) {
+ introspData.put(GENERIC_GET_KEY, genericGet);
+ }
+ }
+
+ private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData,
+ Class<?> clazz) {
+ try {
+ Constructor<?>[] ctors = clazz.getConstructors();
+ if (ctors.length == 1) {
+ Constructor<?> ctor = ctors[0];
+ introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes()));
+ } else if (ctors.length > 1) {
+ OverloadedMethods overloadedCtors = new OverloadedMethods();
+ for (Constructor<?> ctor : ctors) {
+ overloadedCtors.addConstructor(ctor);
+ }
+ introspData.put(CONSTRUCTORS_KEY, overloadedCtors);
+ }
+ } catch (SecurityException e) {
+ LOG.warn("Can't discover constructors for class {}", clazz.getName(), e);
+ }
+ }
+
+ /**
+ * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the
+ * class is not public, retrieves methods with same signature as its public methods from public superclasses and
+ * interfaces. Basically upcasts every method to the nearest accessible method.
+ */
+ private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
+ Map<MethodSignature, List<Method>> accessibles = new HashMap<>();
+ discoverAccessibleMethods(clazz, accessibles);
+ return accessibles;
+ }
+
+ private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) {
+ if (Modifier.isPublic(clazz.getModifiers())) {
+ try {
+ Method[] methods = clazz.getMethods();
+ for (Method method : methods) {
+ MethodSignature sig = new MethodSignature(method);
+ // Contrary to intuition, a class can actually have several
+ // different methods with same signature *but* different
+ // return types. These can't be constructed using Java the
+ // language, as this is illegal on source code level, but
+ // the compiler can emit synthetic methods as part of
+ // generic type reification that will have same signature
+ // yet different return type than an existing explicitly
+ // declared method. Consider:
+ // public interface I<T> { T m(); }
+ // public class C implements I<Integer> { Integer m() { return 42; } }
+ // C.class will have both "Object m()" and "Integer m()" methods.
+ List<Method> methodList = accessibles.get(sig);
+ if (methodList == null) {
+ // TODO Collection.singletonList is more efficient, though read only.
+ methodList = new LinkedList<>();
+ accessibles.put(sig, methodList);
+ }
+ methodList.add(method);
+ }
+ return;
+ } catch (SecurityException e) {
+ LOG.warn("Could not discover accessible methods of class {}, attemping superclasses/interfaces.",
+ clazz.getName(), e);
+ // Fall through and attempt to discover superclass/interface methods
+ }
+ }
+
+ Class<?>[] interfaces = clazz.getInterfaces();
+ for (Class<?> anInterface : interfaces) {
+ discoverAccessibleMethods(anInterface, accessibles);
+ }
+ Class<?> superclass = clazz.getSuperclass();
+ if (superclass != null) {
+ discoverAccessibleMethods(superclass, accessibles);
+ }
+ }
+
+ private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) {
+ if (m == null) {
+ return null;
+ }
+ MethodSignature sig = new MethodSignature(m);
+ List<Method> ams = accessibles.get(sig);
+ if (ams == null) {
+ return null;
+ }
+ for (Method am : ams) {
+ if (am.getReturnType() == m.getReturnType()) {
+ return am;
+ }
+ }
+ return null;
+ }
+
+ private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) {
+ List<Method> ams = accessibles.get(sig);
+ if (ams == null || ams.isEmpty()) {
+ return null;
+ }
+ return ams.get(0);
+ }
+
+ /**
+ * As of this writing, this is only used for testing if method order really doesn't mater.
+ */
+ private void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors) {
+ if (methodSorter != null) {
+ methodSorter.sortMethodDescriptors(methodDescriptors);
+ }
+ }
+
+ boolean isAllowedToExpose(Method method) {
+ return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method);
+ }
+
+ private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) {
+ @SuppressWarnings("unchecked")
+ Map<Method, Class<?>[]> argTypes = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+ if (argTypes == null) {
+ argTypes = new HashMap<>();
+ classInfo.put(ARG_TYPES_BY_METHOD_KEY, argTypes);
+ }
+ return argTypes;
+ }
+
+ private static final class MethodSignature {
+ private static final MethodSignature GET_STRING_SIGNATURE =
+ new MethodSignature("get", new Class[] { String.class });
+ private static final MethodSignature GET_OBJECT_SIGNATURE =
+ new MethodSignature("get", new Class[] { Object.class });
+
+ private final String name;
+ private final Class<?>[] args;
+
+ private MethodSignature(String name, Class<?>[] args) {
+ this.name = name;
+ this.args = args;
+ }
+
+ MethodSignature(Method method) {
+ this(method.getName(), method.getParameterTypes());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MethodSignature) {
+ MethodSignature ms = (MethodSignature) o;
+ return ms.name.equals(name) && Arrays.equals(args, ms.args);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem?
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Cache management:
+
+ /**
+ * Corresponds to {@link DefaultObjectWrapper#clearClassIntrospecitonCache()}.
+ *
+ * @since 2.3.20
+ */
+ void clearCache() {
+ if (getHasSharedInstanceRestrictons()) {
+ throw new IllegalStateException(
+ "It's not allowed to clear the whole cache in a read-only " + getClass().getName() +
+ "instance. Use removeFromClassIntrospectionCache(String prefix) instead.");
+ }
+ forcedClearCache();
+ }
+
+ private void forcedClearCache() {
+ synchronized (sharedLock) {
+ cache.clear();
+ cacheClassNames.clear();
+ clearingCounter++;
+
+ for (WeakReference<Object> regedMfREf : modelFactories) {
+ Object regedMf = regedMfREf.get();
+ if (regedMf != null) {
+ if (regedMf instanceof ClassBasedModelFactory) {
+ ((ClassBasedModelFactory) regedMf).clearCache();
+ } else {
+ throw new BugException();
+ }
+ }
+ }
+
+ removeClearedModelFactoryReferences();
+ }
+ }
+
+ /**
+ * Corresponds to {@link DefaultObjectWrapper#removeFromClassIntrospectionCache(Class)}.
+ *
+ * @since 2.3.20
+ */
+ void remove(Class<?> clazz) {
+ synchronized (sharedLock) {
+ cache.remove(clazz);
+ cacheClassNames.remove(clazz.getName());
+ clearingCounter++;
+
+ for (WeakReference<Object> regedMfREf : modelFactories) {
+ Object regedMf = regedMfREf.get();
+ if (regedMf != null) {
+ if (regedMf instanceof ClassBasedModelFactory) {
+ ((ClassBasedModelFactory) regedMf).removeFromCache(clazz);
+ } else {
+ throw new BugException();
+ }
+ }
+ }
+
+ removeClearedModelFactoryReferences();
+ }
+ }
+
+ /**
+ * Returns the number of events so far that could make class introspection data returned earlier outdated.
+ */
+ int getClearingCounter() {
+ synchronized (sharedLock) {
+ return clearingCounter;
+ }
+ }
+
+ private void onSameNameClassesDetected(String className) {
+ // TODO: This behavior should be pluggable, as in environments where
+ // some classes are often reloaded or multiple versions of the
+ // same class is normal (OSGi), this will drop the cache contents
+ // too often.
+ LOG.info(
+ "Detected multiple classes with the same name, \"{}\". "
+ + "Assuming it was a class-reloading. Clearing class introspection caches to release old data.",
+ className);
+ forcedClearCache();
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Managing dependent objects:
+
+ void registerModelFactory(ClassBasedModelFactory mf) {
+ registerModelFactory((Object) mf);
+ }
+
+ private void registerModelFactory(Object mf) {
+ // Note that this `synchronized (sharedLock)` is also need for the DefaultObjectWrapper constructor to work safely.
+ synchronized (sharedLock) {
+ modelFactories.add(new WeakReference<>(mf, modelFactoriesRefQueue));
+ removeClearedModelFactoryReferences();
+ }
+ }
+
+ void unregisterModelFactory(ClassBasedModelFactory mf) {
+ unregisterModelFactory((Object) mf);
+ }
+
+ void unregisterModelFactory(Object mf) {
+ synchronized (sharedLock) {
+ for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) {
+ Object regedMf = it.next().get();
+ if (regedMf == mf) {
+ it.remove();
+ }
+ }
+
+ }
+ }
+
+ private void removeClearedModelFactoryReferences() {
+ Reference<?> cleardRef;
+ while ((cleardRef = modelFactoriesRefQueue.poll()) != null) {
+ synchronized (sharedLock) {
+ findClearedRef: for (Iterator<WeakReference<Object>> it = modelFactories.iterator(); it.hasNext(); ) {
+ if (it.next() == cleardRef) {
+ it.remove();
+ break findClearedRef;
+ }
+ }
+ }
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Extracting from introspection info:
+
+ static Class<?>[] getArgTypes(Map<Object, Object> classInfo, Method method) {
+ @SuppressWarnings("unchecked")
+ Map<Method, Class<?>[]> argTypesByMethod = (Map<Method, Class<?>[]>) classInfo.get(ARG_TYPES_BY_METHOD_KEY);
+ return argTypesByMethod.get(method);
+ }
+
+ /**
+ * Returns the number of introspected methods/properties that should be available via the TemplateHashModel
+ * interface.
+ */
+ int keyCount(Class<?> clazz) {
+ Map<Object, Object> map = get(clazz);
+ int count = map.size();
+ if (map.containsKey(CONSTRUCTORS_KEY)) count--;
+ if (map.containsKey(GENERIC_GET_KEY)) count--;
+ if (map.containsKey(ARG_TYPES_BY_METHOD_KEY)) count--;
+ return count;
+ }
+
+ /**
+ * Returns the Set of names of introspected methods/properties that should be available via the TemplateHashModel
+ * interface.
+ */
+ Set<Object> keySet(Class<?> clazz) {
+ Set<Object> set = new HashSet<>(get(clazz).keySet());
+ set.remove(CONSTRUCTORS_KEY);
+ set.remove(GENERIC_GET_KEY);
+ set.remove(ARG_TYPES_BY_METHOD_KEY);
+ return set;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Properties
+
+ int getExposureLevel() {
+ return exposureLevel;
+ }
+
+ boolean getExposeFields() {
+ return exposeFields;
+ }
+
+ MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+ return methodAppearanceFineTuner;
+ }
+
+ MethodSorter getMethodSorter() {
+ return methodSorter;
+ }
+
+ /**
+ * Returns {@code true} if this instance was created with {@link Builder}, even if it wasn't
+ * actually put into the cache (as we reserve the right to do so in later versions).
+ */
+ boolean getHasSharedInstanceRestrictons() {
+ return hasSharedInstanceRestrictons;
+ }
+
+ /**
+ * Tells if this instance is (potentially) shared among {@link DefaultObjectWrapper} instances.
+ *
+ * @see #getHasSharedInstanceRestrictons()
+ */
+ boolean isShared() {
+ return shared;
+ }
+
+ /**
+ * Almost always, you want to use {@link DefaultObjectWrapper#getSharedIntrospectionLock()}, not this! The only exception is
+ * when you get this to set the field returned by {@link DefaultObjectWrapper#getSharedIntrospectionLock()}.
+ */
+ Object getSharedLock() {
+ return sharedLock;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Monitoring:
+
+ /** For unit testing only */
+ Object[] getRegisteredModelFactoriesSnapshot() {
+ synchronized (sharedLock) {
+ return modelFactories.toArray();
+ }
+ }
+
+ static final class Builder implements CommonBuilder<ClassIntrospector>, Cloneable {
+
+ private static final Map/*<PropertyAssignments, Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap();
+ private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue();
+
+ // Properties and their *defaults*:
+ private int exposureLevel = DefaultObjectWrapper.EXPOSE_SAFE;
+ private boolean exposureLevelSet;
+ private boolean exposeFields;
+ private boolean exposeFieldsSet;
+ private MethodAppearanceFineTuner methodAppearanceFineTuner;
+ private boolean methodAppearanceFineTunerSet;
+ private MethodSorter methodSorter;
+ // Attention:
+ // - This is also used as a cache key, so non-normalized field values should be avoided.
+ // - If some field has a default value, it must be set until the end of the constructor. No field that has a
+ // default can be left unset (like null).
+ // - If you add a new field, review all methods in this class, also the ClassIntrospector constructor
+
+ Builder(ClassIntrospector ci) {
+ exposureLevel = ci.exposureLevel;
+ exposeFields = ci.exposeFields;
+ methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
+ methodSorter = ci.methodSorter;
+ }
+
+ Builder(Version incompatibleImprovements) {
+ // Warning: incompatibleImprovements must not affect this object at versions increments where there's no
+ // change in the DefaultObjectWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react
+ // to some version changes that affects DefaultObjectWrapper, but not the other way around.
+ _NullArgumentException.check(incompatibleImprovements);
+ // Currently nothing depends on incompatibleImprovements
+ }
+
+ @Override
+ protected Object clone() {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Failed to deepClone Builder", e);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (exposeFields ? 1231 : 1237);
+ result = prime * result + exposureLevel;
+ result = prime * result + System.identityHashCode(methodAppearanceFineTuner);
+ result = prime * result + System.identityHashCode(methodSorter);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ Builder other = (Builder) obj;
+
+ if (exposeFields != other.exposeFields) return false;
+ if (exposureLevel != other.exposureLevel) return false;
+ if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false;
+ return methodSorter == other.methodSorter;
+ }
+
+ public int getExposureLevel() {
+ return exposureLevel;
+ }
+
+ /** See {@link DefaultObjectWrapper.ExtendableBuilder#setExposureLevel(int)}. */
+ public void setExposureLevel(int exposureLevel) {
+ if (exposureLevel < DefaultObjectWrapper.EXPOSE_ALL || exposureLevel > DefaultObjectWrapper.EXPOSE_NOTHING) {
+ throw new IllegalArgumentException("Illegal exposure level: " + exposureLevel);
+ }
+
+ this.exposureLevel = exposureLevel;
+ exposureLevelSet = true;
+ }
+
+ /**
+ * Tells if the property was explicitly set, as opposed to just holding its default value.
+ */
+ public boolean isExposureLevelSet() {
+ return exposureLevelSet;
+ }
+
+ public boolean getExposeFields() {
+ return exposeFields;
+ }
+
+ /** See {@link DefaultObjectWrapper.ExtendableBuilder#setExposeFields(boolean)}. */
+ public void setExposeFields(boolean exposeFields) {
+ this.exposeFields = exposeFields;
+ exposeFieldsSet = true;
+ }
+
+ /**
+ * Tells if the property was explicitly set, as opposed to just holding its default value.
+ */
+ public boolean isExposeFieldsSet() {
+ return exposeFieldsSet;
+ }
+
+ public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
+ return methodAppearanceFineTuner;
+ }
+
+ public void setMethodAppearanceFineTuner(MethodAppearanceFineTuner methodAppearanceFineTuner) {
+ this.methodAppearanceFineTuner = methodAppearanceFineTuner;
+ methodAppearanceFineTunerSet = true;
+ }
+
+ /**
+ * Tells if the property was explicitly set, as opposed to just holding its default value.
+ */
+ public boolean isMethodAppearanceFineTunerSet() {
+ return methodAppearanceFineTunerSet;
+ }
+
+ public MethodSorter getMethodSorter() {
+ return methodSorter;
+ }
+
+ public void setMethodSorter(MethodSorter methodSorter) {
+ this.methodSorter = methodSorter;
+ }
+
+ private static void removeClearedReferencesFromInstanceCache() {
+ Reference clearedRef;
+ while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
+ synchronized (INSTANCE_CACHE) {
+ findClearedRef: for (Iterator it = INSTANCE_CACHE.values().iterator(); it.hasNext(); ) {
+ if (it.next() == clearedRef) {
+ it.remove();
+ break findClearedRef;
+ }
+ }
+ }
+ }
+ }
+
+ /** For unit testing only */
+ static void clearInstanceCache() {
+ synchronized (INSTANCE_CACHE) {
+ INSTANCE_CACHE.clear();
+ }
+ }
+
+ /** For unit testing only */
+ static Map getInstanceCache() {
+ return INSTANCE_CACHE;
+ }
+
+ /**
+ * Returns an instance that is possibly shared (singleton). Note that this comes with its own "shared lock",
+ * since everyone who uses this object will have to lock with that common object.
+ */
+ @Override
+ public ClassIntrospector build() {
+ if ((methodAppearanceFineTuner == null || methodAppearanceFineTuner instanceof SingletonCustomizer)
+ && (methodSorter == null || methodSorter instanceof SingletonCustomizer)) {
+ // Instance can be cached.
+ ClassIntrospector instance;
+ synchronized (INSTANCE_CACHE) {
+ Reference instanceRef = (Reference) INSTANCE_CACHE.get(this);
+ instance = instanceRef != null ? (ClassIntrospector) instanceRef.get() : null;
+ if (instance == null) {
+ Builder thisClone = (Builder) clone(); // prevent any aliasing issues
+ instance = new ClassIntrospector(thisClone, new Object(), true, true);
+ INSTANCE_CACHE.put(thisClone, new WeakReference(instance, INSTANCE_CACHE_REF_QUEUE));
+ }
+ }
+
+ removeClearedReferencesFromInstanceCache();
+
+ return instance;
+ } else {
+ // If methodAppearanceFineTuner or methodSorter is specified and isn't marked as a singleton, the
+ // ClassIntrospector can't be shared/cached as those objects could contain a back-reference to the
+ // DefaultObjectWrapper.
+ return new ClassIntrospector(this, new Object(), true, false);
+ }
+ }
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
new file mode 100644
index 0000000..e9860ab
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+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.TemplateModelIterator;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+
+/**
+ * Adapts a {@link TemplateCollectionModel} to {@link Collection}.
+ */
+class CollectionAdapter extends AbstractCollection implements TemplateModelAdapter {
+ private final DefaultObjectWrapper wrapper;
+ private final TemplateCollectionModel model;
+
+ CollectionAdapter(TemplateCollectionModel model, DefaultObjectWrapper wrapper) {
+ this.model = model;
+ this.wrapper = wrapper;
+ }
+
+ @Override
+ public TemplateModel getTemplateModel() {
+ return model;
+ }
+
+ @Override
+ public int size() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterator iterator() {
+ try {
+ return new Iterator() {
+ final TemplateModelIterator i = model.iterator();
+
+ @Override
+ public boolean hasNext() {
+ try {
+ return i.hasNext();
+ } catch (TemplateModelException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }
+
+ @Override
+ public Object next() {
+ try {
+ return wrapper.unwrap(i.next());
+ } catch (TemplateModelException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ } catch (TemplateModelException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
new file mode 100644
index 0000000..7979981
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CollectionAndSequence.java
@@ -0,0 +1,111 @@
+/*
+ * 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.util.ArrayList;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+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.TemplateSequenceModel;
+
+/**
+ * Add sequence capabilities to an existing collection, or
+ * vice versa. Used by ?keys and ?values built-ins.
+ */
+// [FM3] FTL sequence should extend FTL collection, so we shouldn't need that direction, only the other.
+final public class CollectionAndSequence implements TemplateCollectionModel, TemplateSequenceModel {
+ private TemplateCollectionModel collection;
+ private TemplateSequenceModel sequence;
+ private ArrayList data;
+
+ public CollectionAndSequence(TemplateCollectionModel collection) {
+ this.collection = collection;
+ }
+
+ public CollectionAndSequence(TemplateSequenceModel sequence) {
+ this.sequence = sequence;
+ }
+
+ @Override
+ public TemplateModelIterator iterator() throws TemplateModelException {
+ if (collection != null) {
+ return collection.iterator();
+ } else {
+ return new SequenceIterator(sequence);
+ }
+ }
+
+ @Override
+ public TemplateModel get(int i) throws TemplateModelException {
+ if (sequence != null) {
+ return sequence.get(i);
+ } else {
+ initSequence();
+ return (TemplateModel) data.get(i);
+ }
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ if (sequence != null) {
+ return sequence.size();
+ } else if (collection instanceof TemplateCollectionModelEx) {
+ return ((TemplateCollectionModelEx) collection).size();
+ } else {
+ initSequence();
+ return data.size();
+ }
+ }
+
+ private void initSequence() throws TemplateModelException {
+ if (data == null) {
+ data = new ArrayList();
+ TemplateModelIterator it = collection.iterator();
+ while (it.hasNext()) {
+ data.add(it.next());
+ }
+ }
+ }
+
+ private static class SequenceIterator
+ implements TemplateModelIterator {
+ private final TemplateSequenceModel sequence;
+ private final int size;
+ private int index = 0;
+
+ SequenceIterator(TemplateSequenceModel sequence) throws TemplateModelException {
+ this.sequence = sequence;
+ size = sequence.size();
+
+ }
+ @Override
+ public TemplateModel next() throws TemplateModelException {
+ return sequence.get(index++);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < size;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
new file mode 100644
index 0000000..2db536d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultArrayAdapter.java
@@ -0,0 +1,378 @@
+/*
+ * 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.lang.reflect.Array;
+
+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.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts an {@code array} of a non-primitive elements to the corresponding {@link TemplateModel} interface(s), most
+ * importantly to {@link TemplateHashModelEx}. If you aren't wrapping an already existing {@code array}, but build a
+ * sequence specifically to be used from a template, also consider using {@link SimpleSequence} (see comparison there).
+ *
+ * <p>
+ * Thread safety: A {@link DefaultListAdapter} is as thread-safe as the array that it wraps is. Normally you only
+ * have to consider read-only access, as the FreeMarker template language doesn't allow writing these sequences (though
+ * of course, Java methods called from the template can violate this rule).
+ *
+ * <p>
+ * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
+ * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
+ *
+ * @see SimpleSequence
+ * @see DefaultListAdapter
+ * @see TemplateSequenceModel
+ *
+ * @since 2.3.22
+ */
+public abstract class DefaultArrayAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
+ AdapterTemplateModel, WrapperTemplateModel, Serializable {
+
+ /**
+ * Factory method for creating new adapter instances.
+ *
+ * @param array
+ * The array to adapt; can't be {@code null}. Must be an array.
+ * @param wrapper
+ * The {@link ObjectWrapper} used to wrap the items in the array. Has to be
+ * {@link ObjectWrapperAndUnwrapper} because of planned future features.
+ */
+ public static DefaultArrayAdapter adapt(Object array, ObjectWrapperAndUnwrapper wrapper) {
+ final Class componentType = array.getClass().getComponentType();
+ if (componentType == null) {
+ throw new IllegalArgumentException("Not an array");
+ }
+
+ if (componentType.isPrimitive()) {
+ if (componentType == int.class) {
+ return new IntArrayAdapter((int[]) array, wrapper);
+ }
+ if (componentType == double.class) {
+ return new DoubleArrayAdapter((double[]) array, wrapper);
+ }
+ if (componentType == long.class) {
+ return new LongArrayAdapter((long[]) array, wrapper);
+ }
+ if (componentType == boolean.class) {
+ return new BooleanArrayAdapter((boolean[]) array, wrapper);
+ }
+ if (componentType == float.class) {
+ return new FloatArrayAdapter((float[]) array, wrapper);
+ }
+ if (componentType == char.class) {
+ return new CharArrayAdapter((char[]) array, wrapper);
+ }
+ if (componentType == short.class) {
+ return new ShortArrayAdapter((short[]) array, wrapper);
+ }
+ if (componentType == byte.class) {
+ return new ByteArrayAdapter((byte[]) array, wrapper);
+ }
+ return new GenericPrimitiveArrayAdapter(array, wrapper);
+ } else {
+ return new ObjectArrayAdapter((Object[]) array, wrapper);
+ }
+ }
+
+ private DefaultArrayAdapter(ObjectWrapper wrapper) {
+ super(wrapper);
+ }
+
+ @Override
+ public final Object getAdaptedObject(Class hint) {
+ return getWrappedObject();
+ }
+
+ private static class ObjectArrayAdapter extends DefaultArrayAdapter {
+
+ private final Object[] array;
+
+ private ObjectArrayAdapter(Object[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(array[index]) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class ByteArrayAdapter extends DefaultArrayAdapter {
+
+ private final byte[] array;
+
+ private ByteArrayAdapter(byte[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Byte.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class ShortArrayAdapter extends DefaultArrayAdapter {
+
+ private final short[] array;
+
+ private ShortArrayAdapter(short[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Short.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class IntArrayAdapter extends DefaultArrayAdapter {
+
+ private final int[] array;
+
+ private IntArrayAdapter(int[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Integer.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class LongArrayAdapter extends DefaultArrayAdapter {
+
+ private final long[] array;
+
+ private LongArrayAdapter(long[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Long.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class FloatArrayAdapter extends DefaultArrayAdapter {
+
+ private final float[] array;
+
+ private FloatArrayAdapter(float[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Float.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class DoubleArrayAdapter extends DefaultArrayAdapter {
+
+ private final double[] array;
+
+ private DoubleArrayAdapter(double[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Double.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class CharArrayAdapter extends DefaultArrayAdapter {
+
+ private final char[] array;
+
+ private CharArrayAdapter(char[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Character.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ private static class BooleanArrayAdapter extends DefaultArrayAdapter {
+
+ private final boolean[] array;
+
+ private BooleanArrayAdapter(boolean[] array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < array.length ? wrap(Boolean.valueOf(array[index])) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return array.length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+ /**
+ * Much slower than the specialized versions; used only as the last resort.
+ */
+ private static class GenericPrimitiveArrayAdapter extends DefaultArrayAdapter {
+
+ private final Object array;
+ private final int length;
+
+ private GenericPrimitiveArrayAdapter(Object array, ObjectWrapper wrapper) {
+ super(wrapper);
+ this.array = array;
+ length = Array.getLength(array);
+ }
+
+ @Override
+ public TemplateModel get(int index) throws TemplateModelException {
+ return index >= 0 && index < length ? wrap(Array.get(array, index)) : null;
+ }
+
+ @Override
+ public int size() throws TemplateModelException {
+ return length;
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return array;
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
new file mode 100644
index 0000000..d5b6989
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultEnumerationAdapter.java
@@ -0,0 +1,128 @@
+/*
+ * 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.ObjectWrapperWithAPISupport;
+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.TemplateModelWithAPISupport;
+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.
+ */
+@SuppressWarnings("serial")
+public class DefaultEnumerationAdapter extends WrappingTemplateModel implements TemplateCollectionModel,
+ AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, 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 enumeration
+ * The enumeration to adapt; can't be {@code null}.
+ */
+ public static DefaultEnumerationAdapter adapt(Enumeration<?> enumeration, ObjectWrapper wrapper) {
+ return new DefaultEnumerationAdapter(enumeration, 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();
+ }
+
+ @Override
+ public TemplateModel getAPI() throws TemplateModelException {
+ return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(enumeration);
+ }
+
+ /**
+ * 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
new file mode 100644
index 0000000..6fd2680
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultIterableAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.Collection;
+import java.util.Iterator;
+
+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.ObjectWrapperWithAPISupport;
+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.TemplateModelWithAPISupport;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+
+/**
+ * Adapts an {@link Iterable} to the corresponding {@link TemplateModel} interface(s), most importantly to
+ * {@link TemplateCollectionModel}. This should only be used if {@link Collection} is not implemented by the adapted
+ * object, because then {@link DefaultListAdapter} and {@link DefaultNonListCollectionAdapter} gives more functionality.
+ *
+ * <p>
+ * Thread safety: A {@link DefaultIterableAdapter} is as thread-safe as the {@link Iterable} that it wraps is. Normally
+ * you only have to consider read-only access, as the FreeMarker template language doesn't provide mean to call
+ * {@link Iterator} modifier methods (though of course, Java methods called from the template can violate this rule).
+ *
+ * @since 2.3.25
+ */
+@SuppressWarnings("serial")
+public class DefaultIterableAdapter extends WrappingTemplateModel implements TemplateCollectionModel,
+ AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, Serializable {
+
+ private final Iterable<?> iterable;
+
+ /**
+ * Factory method for creating new adapter instances.
+ *
+ * @param iterable
+ * The collection to adapt; can't be {@code null}.
+ * @param wrapper
+ * The {@link ObjectWrapper} used to wrap the items in the array. Has to be
+ * {@link ObjectWrapperAndUnwrapper} because of planned future features.
+ */
+ public static DefaultIterableAdapter adapt(Iterable<?> iterable, ObjectWrapperWithAPISupport wrapper) {
+ return new DefaultIterableAdapter(iterable, wrapper);
+ }
+
+ private DefaultIterableAdapter(Iterable<?> iterable, ObjectWrapperWithAPISupport wrapper) {
+ super(wrapper);
+ this.iterable = iterable;
+ }
+
+ @Override
+ public TemplateModelIterator iterator() throws TemplateModelException {
+ return new DefaultUnassignableIteratorAdapter(iterable.iterator(), getObjectWrapper());
+ }
+
+ @Override
+ public Object getWrappedObject() {
+ return iterable;
+ }
+
+ @Override
+ public Object getAdaptedObject(Class hint) {
+ return getWrappedObject();
+ }
+
+ @Override
+ public TemplateModel getAPI() throws TemplateModelException {
+ return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(iterable);
+ }
+
+}