| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package freemarker.ext.beans; |
| |
| import java.beans.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.AccessibleObject; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import freemarker.core.BugException; |
| import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision; |
| import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput; |
| import freemarker.ext.util.ModelCache; |
| import freemarker.log.Logger; |
| import freemarker.template.utility.NullArgumentException; |
| import freemarker.template.utility.SecurityUtilities; |
| |
| /** |
| * 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 = Logger.getLogger("freemarker.beans"); |
| |
| 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."; |
| |
| /** |
| * When this property is true, some things are stricter. This is mostly to catch suspicious things in development |
| * that can otherwise be valid situations. |
| */ |
| static final boolean DEVELOPMENT_MODE = "true".equals(SecurityUtilities.getSystemProperty("freemarker.development", |
| "false")); |
| |
| 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("freemarker.ext.beans.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: |
| |
| private static final Object ARGTYPES_KEY = new Object(); |
| static final Object CONSTRUCTORS_KEY = new Object(); |
| 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; |
| final boolean bugfixed; |
| |
| /** 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<String, 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<ClassBasedModelFactory|ModelCache>> */modelFactories = new LinkedList(); |
| private final ReferenceQueue 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(ClassIntrospectorBuilder pa, Object sharedLock) { |
| this(pa, sharedLock, false, false); |
| } |
| |
| /** |
| * @param hasSharedInstanceRestrictons |
| * {@code true} exactly if we are creating a new instance with {@link ClassIntrospectorBuilder}. Then |
| * it's {@code true} even if it won't put the instance into the cache. |
| */ |
| ClassIntrospector(ClassIntrospectorBuilder builder, Object sharedLock, |
| boolean hasSharedInstanceRestrictons, boolean shared) { |
| NullArgumentException.check("sharedLock", sharedLock); |
| |
| this.exposureLevel = builder.getExposureLevel(); |
| this.exposeFields = builder.getExposeFields(); |
| this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner(); |
| this.methodSorter = builder.getMethodSorter(); |
| this.bugfixed = builder.isBugfixed(); |
| |
| this.sharedLock = sharedLock; |
| |
| this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons; |
| this.shared = shared; |
| |
| if (CLASS_CHANGE_NOTIFIER != null) { |
| CLASS_CHANGE_NOTIFIER.subscribe(this); |
| } |
| } |
| |
| /** |
| * Returns a {@link ClassIntrospectorBuilder}-s that could be used to create an identical {@link #ClassIntrospector} |
| * . The returned {@link ClassIntrospectorBuilder} can be modified without interfering with anything. |
| */ |
| ClassIntrospectorBuilder getPropertyAssignments() { |
| return new ClassIntrospectorBuilder(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 get(Class clazz) { |
| { |
| Map introspData = (Map) cache.get(clazz); |
| if (introspData != null) return introspData; |
| } |
| |
| String className; |
| synchronized (sharedLock) { |
| Map introspData = (Map) 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 = (Map) 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 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 createClassIntrospectionData(Class clazz) { |
| final Map introspData = new HashMap(); |
| |
| if (exposeFields) { |
| addFieldsToClassIntrospectionData(introspData, clazz); |
| } |
| |
| final Map accessibleMethods = discoverAccessibleMethods(clazz); |
| |
| addGenericGetToClassIntrospectionData(introspData, accessibleMethods); |
| |
| if (exposureLevel != BeansWrapper.EXPOSE_NOTHING) { |
| try { |
| addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods); |
| } catch (IntrospectionException e) { |
| LOG.warn("Couldn't properly perform introspection for class " + clazz, 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.EMPTY_MAP; |
| } else { // map.size() == 1 |
| Map.Entry e = (Map.Entry) introspData.entrySet().iterator().next(); |
| return Collections.singletonMap(e.getKey(), e.getValue()); |
| } |
| } |
| |
| private void addFieldsToClassIntrospectionData(Map introspData, Class clazz) |
| throws SecurityException { |
| Field[] fields = clazz.getFields(); |
| for (int i = 0; i < fields.length; i++) { |
| Field field = fields[i]; |
| if ((field.getModifiers() & Modifier.STATIC) == 0) { |
| introspData.put(field.getName(), field); |
| } |
| } |
| } |
| |
| private void addBeanInfoToClassIntrospectionData(Map introspData, Class clazz, Map accessibleMethods) |
| throws IntrospectionException { |
| BeanInfo beanInfo = Introspector.getBeanInfo(clazz); |
| |
| PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors(); |
| if (pda != null) { |
| int pdaLength = pda.length; |
| for (int i = pdaLength - 1; i >= 0; --i) { |
| addPropertyDescriptorToClassIntrospectionData( |
| introspData, pda[i], clazz, |
| accessibleMethods); |
| } |
| } |
| |
| if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) { |
| final MethodAppearanceDecision decision = new MethodAppearanceDecision(); |
| MethodAppearanceDecisionInput decisionInput = null; |
| final MethodDescriptor[] mda = sortMethodDescriptors(beanInfo.getMethodDescriptors()); |
| if (mda != null) { |
| int mdaLength = mda.length; |
| for (int i = mdaLength - 1; i >= 0; --i) { |
| final MethodDescriptor md = mda[i]; |
| final Method method = getMatchingAccessibleMethod(md.getMethod(), accessibleMethods); |
| if (method != null && isAllowedToExpose(method)) { |
| decision.setDefaults(method); |
| if (methodAppearanceFineTuner != null) { |
| if (decisionInput == null) { |
| decisionInput = new MethodAppearanceDecisionInput(); |
| } |
| 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(bugfixed); |
| overloadedMethods.addMethod((Method) previous); |
| overloadedMethods.addMethod(method); |
| introspData.put(methodKey, overloadedMethods); |
| // Remove parameter type information |
| getArgTypes(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); |
| getArgTypes(introspData).put(method, |
| method.getParameterTypes()); |
| } |
| } |
| } |
| } // for each in mda |
| } // if mda != null |
| } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY) |
| } |
| |
| private void addPropertyDescriptorToClassIntrospectionData(Map introspData, |
| PropertyDescriptor pd, Class clazz, Map 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); |
| getArgTypes(introspData).put(publicReadMethod, publicReadMethod.getParameterTypes()); |
| } catch (IntrospectionException e) { |
| LOG.warn("Failed creating a publicly-accessible " + |
| "property descriptor for " + clazz.getName() + |
| " indexed property " + pd.getName() + |
| ", read method " + 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); |
| pd.setReadMethod(publicReadMethod); |
| } |
| introspData.put(pd.getName(), pd); |
| } catch (IntrospectionException e) { |
| LOG.warn("Failed creating a publicly-accessible " + |
| "property descriptor for " + clazz.getName() + |
| " property " + pd.getName() + ", read method " + |
| publicReadMethod, e); |
| } |
| } |
| } |
| } |
| |
| private void addGenericGetToClassIntrospectionData(Map introspData, |
| Map 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 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 ctorMap = new OverloadedMethods(bugfixed); |
| for (int i = 0; i < ctors.length; i++) { |
| ctorMap.addConstructor(ctors[i]); |
| } |
| introspData.put(CONSTRUCTORS_KEY, ctorMap); |
| } |
| } 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 discoverAccessibleMethods(Class clazz) { |
| Map accessibles = new HashMap(); |
| discoverAccessibleMethods(clazz, accessibles); |
| return accessibles; |
| } |
| |
| private static void discoverAccessibleMethods(Class clazz, Map accessibles) { |
| if (Modifier.isPublic(clazz.getModifiers())) { |
| try { |
| Method[] methods = clazz.getMethods(); |
| for (int i = 0; i < methods.length; i++) { |
| Method method = methods[i]; |
| 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 methodList = (List) accessibles.get(sig); |
| if (methodList == null) { |
| methodList = new LinkedList(); |
| accessibles.put(sig, methodList); |
| } |
| methodList.add(method); |
| } |
| return; |
| } catch (SecurityException e) { |
| LOG.warn("Could not discover accessible methods of class " + |
| clazz.getName() + |
| ", attemping superclasses/interfaces.", e); |
| // Fall through and attempt to discover superclass/interface methods |
| } |
| } |
| |
| Class[] interfaces = clazz.getInterfaces(); |
| for (int i = 0; i < interfaces.length; i++) { |
| discoverAccessibleMethods(interfaces[i], accessibles); |
| } |
| Class superclass = clazz.getSuperclass(); |
| if (superclass != null) { |
| discoverAccessibleMethods(superclass, accessibles); |
| } |
| } |
| |
| private static Method getMatchingAccessibleMethod(Method m, Map accessibles) { |
| if (m == null) { |
| return null; |
| } |
| MethodSignature sig = new MethodSignature(m); |
| List l = (List) accessibles.get(sig); |
| if (l == null) { |
| return null; |
| } |
| for (Iterator iterator = l.iterator(); iterator.hasNext(); ) { |
| Method am = (Method) iterator.next(); |
| if (am.getReturnType() == m.getReturnType()) { |
| return am; |
| } |
| } |
| return null; |
| } |
| |
| private static Method getFirstAccessibleMethod(MethodSignature sig, Map accessibles) { |
| List l = (List) accessibles.get(sig); |
| if (l == null || l.isEmpty()) { |
| return null; |
| } |
| return (Method) l.iterator().next(); |
| } |
| |
| /** |
| * As of this writing, this is only used for testing if method order really doesn't mater. |
| */ |
| private MethodDescriptor[] sortMethodDescriptors(MethodDescriptor[] methodDescriptors) { |
| return methodSorter != null ? methodSorter.sortMethodDescriptors(methodDescriptors) : methodDescriptors; |
| } |
| |
| boolean isAllowedToExpose(Method method) { |
| return exposureLevel < BeansWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method); |
| } |
| |
| private static Map getArgTypes(Map classMap) { |
| Map argTypes = (Map) classMap.get(ARGTYPES_KEY); |
| if (argTypes == null) { |
| argTypes = new HashMap(); |
| classMap.put(ARGTYPES_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 BeansWrapper#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 " + this.getClass().getName() + |
| "instance. Use removeFromClassIntrospectionCache(String prefix) instead."); |
| } |
| forcedClearCache(); |
| } |
| |
| private void forcedClearCache() { |
| synchronized (sharedLock) { |
| cache.clear(); |
| cacheClassNames.clear(); |
| clearingCounter++; |
| |
| for (Iterator it = modelFactories.iterator(); it.hasNext(); ) { |
| Object regedMf = ((WeakReference) it.next()).get(); |
| if (regedMf != null) { |
| if (regedMf instanceof ClassBasedModelFactory) { |
| ((ClassBasedModelFactory) regedMf).clearCache(); |
| } else if (regedMf instanceof ModelCache) { |
| ((ModelCache) regedMf).clearCache(); |
| } else { |
| throw new BugException(); |
| } |
| } |
| } |
| |
| removeClearedModelFactoryReferences(); |
| } |
| } |
| |
| /** |
| * Corresponds to {@link BeansWrapper#removeFromClassIntrospectionCache(Class)}. |
| * |
| * @since 2.3.20 |
| */ |
| void remove(Class clazz) { |
| synchronized (sharedLock) { |
| cache.remove(clazz); |
| cacheClassNames.remove(clazz.getName()); |
| clearingCounter++; |
| |
| for (Iterator it = modelFactories.iterator(); it.hasNext(); ) { |
| Object regedMf = ((WeakReference) it.next()).get(); |
| if (regedMf != null) { |
| if (regedMf instanceof ClassBasedModelFactory) { |
| ((ClassBasedModelFactory) regedMf).removeFromCache(clazz); |
| } else if (regedMf instanceof ModelCache) { |
| ((ModelCache) regedMf).clearCache(); // doesn't support selective clearing ATM |
| } 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. |
| if (LOG.isInfoEnabled()) { |
| LOG.info( |
| "Detected multiple classes with the same name, \"" + className + |
| "\". Assuming it was a class-reloading. Clearing class introspection " + |
| "caches to release old data."); |
| } |
| forcedClearCache(); |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| // Managing dependent objects: |
| |
| void registerModelFactory(ClassBasedModelFactory mf) { |
| registerModelFactory((Object) mf); |
| } |
| |
| void registerModelFactory(ModelCache mf) { |
| registerModelFactory((Object) mf); |
| } |
| |
| private void registerModelFactory(Object mf) { |
| // Note that this `synchronized (sharedLock)` is also need for the BeansWrapper constructor to work safely. |
| synchronized (sharedLock) { |
| modelFactories.add(new WeakReference(mf, modelFactoriesRefQueue)); |
| removeClearedModelFactoryReferences(); |
| } |
| } |
| |
| void unregisterModelFactory(ClassBasedModelFactory mf) { |
| unregisterModelFactory((Object) mf); |
| } |
| |
| void unregisterModelFactory(ModelCache mf) { |
| unregisterModelFactory((Object) mf); |
| } |
| |
| void unregisterModelFactory(Object mf) { |
| synchronized (sharedLock) { |
| for (Iterator it = modelFactories.iterator(); it.hasNext(); ) { |
| Object regedMf = ((Reference) it.next()).get(); |
| if (regedMf == mf) { |
| it.remove(); |
| } |
| } |
| |
| } |
| } |
| |
| private void removeClearedModelFactoryReferences() { |
| Reference cleardRef; |
| while ((cleardRef = modelFactoriesRefQueue.poll()) != null) { |
| synchronized (sharedLock) { |
| findCleardRef: for (Iterator it = modelFactories.iterator(); it.hasNext(); ) { |
| if (it.next() == cleardRef) { |
| it.remove(); |
| break findCleardRef; |
| } |
| } |
| } |
| } |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| // Extracting from introspection info: |
| |
| static Class[] getArgTypes(Map classMap, AccessibleObject methodOrCtor) { |
| return (Class[]) ((Map) classMap.get(ARGTYPES_KEY)).get(methodOrCtor); |
| } |
| |
| /** |
| * Returns the number of introspected methods/properties that should be available via the TemplateHashModel |
| * interface. |
| */ |
| int keyCount(Class clazz) { |
| Map map = get(clazz); |
| int count = map.size(); |
| if (map.containsKey(CONSTRUCTORS_KEY)) count--; |
| if (map.containsKey(GENERIC_GET_KEY)) count--; |
| if (map.containsKey(ARGTYPES_KEY)) count--; |
| return count; |
| } |
| |
| /** |
| * Returns the Set of names of introspected methods/properties that should be available via the TemplateHashModel |
| * interface. |
| */ |
| Set keySet(Class clazz) { |
| Set set = new HashSet(get(clazz).keySet()); |
| set.remove(CONSTRUCTORS_KEY); |
| set.remove(GENERIC_GET_KEY); |
| set.remove(ARGTYPES_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 ClassIntrospectorBuilder}, 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 BeansWrapper} instances. |
| * |
| * @see #getHasSharedInstanceRestrictons() |
| */ |
| boolean isShared() { |
| return shared; |
| } |
| |
| /** |
| * Almost always, you want to use {@link BeansWrapper#getSharedIntrospectionLock()}, not this! The only exception is |
| * when you get this to set the field returned by {@link BeansWrapper#getSharedIntrospectionLock()}. |
| */ |
| Object getSharedLock() { |
| return sharedLock; |
| } |
| |
| // ----------------------------------------------------------------------------------------------------------------- |
| // Monitoring: |
| |
| /** For unit testing only */ |
| Object[] getRegisteredModelFactoriesSnapshot() { |
| synchronized (sharedLock) { |
| return modelFactories.toArray(); |
| } |
| } |
| |
| } |