blob: 6ccd872929d0fa6dd606db3b667e8e7f1c321790 [file] [log] [blame]
/*
* 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();
}
}
}