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); + } + +}
