/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.faces.util; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; import java.util.logging.Logger; import java.util.logging.Level; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.beans.PropertyDescriptor; import java.beans.Introspector; import java.beans.BeanInfo; import java.beans.IntrospectionException; import static java.beans.Introspector.getBeanInfo; import java.beans.PropertyEditor; import static java.beans.PropertyEditorManager.findEditor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; /** * <p>A set of utility methods to make working with * Classes and Reflection a little easier.</p> */ public final class ReflectionUtils { private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger(); /** * <p>Cache</p> */ private static final Map<ClassLoader, ConcurrentMap<String, MetaData>> REFLECTION_CACHE = new WeakHashMap<>(); // ------------------------------------------------------------ Constructors private ReflectionUtils() { } // ---------------------------------------------------------- Public Methods /** * Sets a collection of properties of a given object to the values associated with those properties. * <p> * In the map that represents these properties, each key represents the name of the property, with the value * associated with that key being the value that is set for the property. * <p> * E.g. map entry key = foo, value = "bar", which "bar" an instance of String, will conceptually result in the * following call: <code>object.setFoo("string");</code> * * <p> * NOTE: This particular method assumes that there's a write method for each property in the map with the right * type. No specific checking is done whether this is indeed the case. * * @param object * the object on which properties will be set * @param propertiesToSet * the map containing properties and their values to be set on the object */ public static void setProperties(Object object, Map<String, Object> propertiesToSet) { try { Map<String, PropertyDescriptor> availableProperties = new HashMap<>(); for (PropertyDescriptor propertyDescriptor : getBeanInfo(object.getClass()).getPropertyDescriptors()) { availableProperties.put(propertyDescriptor.getName(), propertyDescriptor); } for (Map.Entry<String, Object> propertyToSet : propertiesToSet.entrySet()) { availableProperties.get(propertyToSet.getKey()).getWriteMethod().invoke(object, propertyToSet.getValue()); } } catch (IntrospectionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new IllegalStateException(e); } } /** * Sets a collection of properties of a given object to the (optionally coerced) values associated with those properties. * <p> * In the map that represents these properties, each key represents the name of the property, with the value * associated with that key being the value that is set for the property. * <p> * E.g. map entry key = foo, value = "bar", which "bar" an instance of String, will conceptually result in the * following call: <code>object.setFoo("string");</code> * * <p> * NOTE 1: In case the value is a String, and the target type is not String, the standard property editor mechanism * will be used to attempt a conversion. * * <p> * Note 2: This method operates somewhat as the reverse of {@link Reflection#setProperties(Object, Map)}. Here only * the available writable properties of the object are matched against the map with properties to set. Properties * in the map for which there isn't a corresponding writable property on the object are ignored. * * <p> * Following the above two notes, use this method when attempting to set properties on an object in a lenient best effort * basis. Use {@link Reflection#setProperties(Object, Map)} when all properties need to be set with the exact type as the value * appears in the map. * * * @param object * the object on which properties will be set * @param propertiesToSet * the map containing properties and their values to be set on the object */ public static void setPropertiesWithCoercion(Object object, Map<String, Object> propertiesToSet) { try { for (PropertyDescriptor property : getBeanInfo(object.getClass()).getPropertyDescriptors()) { Method setter = property.getWriteMethod(); if (setter == null) { continue; } if (propertiesToSet.containsKey(property.getName())) { Object value = propertiesToSet.get(property.getName()); if (value instanceof String && !property.getPropertyType().equals(String.class)) { // Try to convert Strings to the type expected by the converter PropertyEditor editor = findEditor(property.getPropertyType()); editor.setAsText((String) value); value = editor.getValue(); } property.getWriteMethod().invoke(object, value); } } } catch (Exception e) { // NOPMD throw new IllegalStateException(e); } } /** * Finds a method based on the method name, amount of parameters and limited typing, if necessary prefixed with "get". * <p> * Note that this supports overloading, but a limited one. Given an actual parameter of type Long, this will select * a method accepting Number when the choice is between Number and a non-compatible type like String. However, * it will NOT select the best match if the choice is between Number and Long. * * @param base the object in which the method is to be found * @param methodName name of the method to be found * @param params the method parameters * @return a method if one is found, null otherwise */ public static Method findMethod(Object base, String methodName, Object[] params) { List<Method> methods = new ArrayList<>(); for (Method method : base.getClass().getMethods()) { if (method.getName().equals(methodName) && method.getParameterTypes().length == params.length) { methods.add(method); } } if (methods.size() == 1) { return methods.get(0); } if (methods.size() > 1) { // Overloaded methods were found. Try to get a match for (Method method : methods) { boolean match = true; Class<?>[] candidateParams = method.getParameterTypes(); for (int i = 0; i < params.length; i++) { if (!candidateParams[i].isInstance(params[i])) { match = false; break; } } // If all candidate parameters were expected and for none of them the actual // parameter was NOT an instance, we have a match if (match) { return method; } // Else, at least one parameter was not an instance // Go ahead a test then next methods } } return null; } /** * Returns the Class instance associated with the class of the given string, using the context class * loader and if that fails the defining class loader of the current class. * * @param className fully qualified class name of the class for which a Class instance needs to be created * @return the Class object for the class with the given name. * @throws IllegalStateException if the class cannot be found. */ public static Class<?> toClass(String className) { try { return (Class.forName(className, true, Thread.currentThread().getContextClassLoader())); } catch (ClassNotFoundException e) { try { return Class.forName(className); } catch (Exception ignore) { ignore = null; // Just continue to IllegalStateException on original ClassNotFoundException. } throw new IllegalStateException(e); } } /** * Creates an instance of a class with the given fully qualified class name. * * @param <T> The generic object type. * @param className fully qualified class name of the class for which an instance needs to be created * @return an instance of the class denoted by className * @throws IllegalStateException if the class cannot be found */ @SuppressWarnings("unchecked") public static <T> T instance(String className) { return (T) instance(toClass(className)); } /** * Creates a new instance of the class represented by the given Class object * * @param <T> The generic object type. * @param clazz the Class object for which an instance needs to be created * @return an instance of the class as given by the clazz parameter * @throws IllegalStateException if the class cannot be found, or cannot be instantiated or when a security manager prevents this operation */ public static <T> T instance(Class<T> clazz) { try { return clazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalStateException(e); } } /** * <p>Clears the cache for the specified <code>ClassLoader</code>.</p> * <p>This method <em>MUST</em> be called when <code>ConfigureListener * .contextDestroyed()</code> is called.</p> * @param loader the <code>ClassLoader</code> whose associated cache * should be cleared */ public static synchronized void clearCache(ClassLoader loader) { REFLECTION_CACHE.remove(loader); } public static synchronized void initCache(ClassLoader loader) { if (REFLECTION_CACHE.get(loader) == null) { REFLECTION_CACHE.put(loader, new ConcurrentHashMap<>()); } } /** * <p>Returns the <code>Constructor</code> appropriate to the specified * Class and parameters.</p> * @param clazz the Class of interest * @param params the parameters for the constructor of the provided Class * @return a Constructor that can be invoked with the specified parameters */ public static Constructor lookupConstructor(Class<?> clazz, Class<?>... params) { ClassLoader loader = Util.getCurrentLoader(clazz); if (loader == null) { return null; } return getMetaData(loader, clazz).lookupConstructor(params); } /** * <p>Returns the <code>Method</code> appropriate to the specified * object instance, method name, and parameters.</p> * @param object the Object instance of interest * @param methodName the name of the method * @param params the parameters for the specified method * @return a Method that can be invoked with the specified parameters */ public static Method lookupMethod(Object object, String methodName, Class<?>... params) { Class<?> clazz = object.getClass(); ClassLoader loader = Util.getCurrentLoader(clazz); if (loader == null) { return null; } return getMetaData(loader, clazz).lookupMethod(methodName, params); } /** * <p>Returns the <code>Method</code> appropriate to the specified * Class, method name, and parameters.</p> * @param clazz the Class of interest * @param methodName the name of the method * @param params the parameters for the specified method * @return a Method that can be invoked with the specified parameters */ public static Method lookupMethod(Class<?> clazz, String methodName, Class<?>... params) { ClassLoader loader = Util.getCurrentLoader(clazz); if (loader == null) { return null; } return getMetaData(loader, clazz).lookupMethod(methodName, params); } /** * <p>Constructs a new object instance based off the * provided class name.</p> * @param className the class of the object to instantiate * @return a new instances of said class * @throws InstantiationException if the class cannot be instantiated * @throws IllegalAccessException if there is a security violation */ public static Object newInstance(String className) throws InstantiationException, IllegalAccessException { ClassLoader loader = Util.getCurrentLoader(null); if (loader == null) { return null; } return getMetaData(loader, className).lookupClass().newInstance(); } /** * <p>Obtain a <code>Class</code> instance based on the provided * String name.</p> * @param className the class to look up * @return the <code>Class</code> corresponding to <code>className</code> */ public static Class<?> lookupClass(String className) { ClassLoader loader = Util.getCurrentLoader(null); if (loader == null) { return null; } return getMetaData(loader, className).lookupClass(); } /** * @param className the fully qualified class name * @param propertyName a JavaBeans property name * @return a method suitable for setting a JavaBeans property, or * <code>null</code> if the property doesn't exist or is readonly. */ public static Method lookupWriteMethod(String className, String propertyName) { ClassLoader loader = Util.getCurrentLoader(null); if (loader == null) { return null; } return getMetaData(loader, className).lookupWriteMethod(propertyName); } /** * @param className the fully qualified class name * @param propertyName a JavaBeans property name * @return a method suitable for obtaining the value of a JavaBeans property, * or <code>null</code> if the property doesn't exist or can't be read. */ public static Method lookupReadMethod(String className, String propertyName) { ClassLoader loader = Util.getCurrentLoader(null); if (loader == null) { return null; } return getMetaData(loader, className).lookupReadMethod(propertyName); } // --------------------------------------------------------- Private Methods /** * <p>Return the <code>MetaData</code> for the specified Class.</p> * * <p>This will check the cache associated with the specified * <code>ClassLoader</code>. If there is no cache hit, then a new * <code>MetaData</code> instance will be created and stored. * * @param loader <code>ClassLoader</code> * @param clazz the Class of interest * @return a <code>MetaData</code> object for the specified Class */ private static MetaData getMetaData(ClassLoader loader, Class<?> clazz) { ConcurrentMap<String, MetaData> cache = REFLECTION_CACHE.get(loader); if (cache == null) { initCache(loader); cache = REFLECTION_CACHE.get(loader); } MetaData meta = cache.get(clazz.getName()); if (meta == null) { meta = new MetaData(clazz); cache.put(clazz.getName(), meta); } return meta; } /** * <p>Return the <code>MetaData</code> for the specified className.</p> * * <p>This will check the cache associated with the specified * <code>ClassLoader</code>. If there is no cache hit, then a new * <code>MetaData</code> instance will be created and stored. * * @param loader <code>ClassLoader</code> * @param className the class of interest * @return a <code>MetaData</code> object for the specified Class */ private static MetaData getMetaData(ClassLoader loader, String className) { ConcurrentMap<String, MetaData> cache = REFLECTION_CACHE.get(loader); if (cache == null) { initCache(loader); cache = REFLECTION_CACHE.get(loader); } MetaData meta = cache.get(className); if (meta == null) { try { Class<?> clazz = Util.loadClass(className, cache); meta = new MetaData(clazz); cache.put(className, meta); } catch (ClassNotFoundException cnfe) { return null; } } return meta; } /** * <p>MetaData contains lookup methods for <code>Constructor</code>s and * <code>Method</code>s of a particular Class. */ private static final class MetaData { Map<Integer,Constructor> constructors; Map<String,HashMap<Integer,Method>> methods; Map<String,HashMap<Integer,Method>> declaredMethods; Map<String, PropertyDescriptor> propertyDescriptors; Class<?> clazz; // ------------------------------------------------------------ Constructors /** * <p>Constructs a new <code>MetaData</code> instance for the specified * class.</p> * @param clazz class to construct a new MetaData instance from. */ public MetaData(Class<?> clazz) { String name; this.clazz = clazz; Constructor[] ctors = clazz.getConstructors(); constructors = new HashMap<>(ctors.length, 1.0f); for (int i = 0, len = ctors.length; i < len; i++) { constructors.put(getKey(ctors[i].getParameterTypes()), ctors[i]); } Method[] meths = clazz.getMethods(); methods = new HashMap<>(meths.length, 1.0f); for (int i = 0, len = meths.length; i < len; i++) { name = meths[i].getName(); HashMap<Integer,Method> methodsMap = methods.get(name); if (methodsMap == null) { methodsMap = new HashMap<>(4, 1.0f); methods.put(name, methodsMap); } methodsMap.put(getKey(meths[i].getParameterTypes()), meths[i]); } meths = clazz.getDeclaredMethods(); declaredMethods = new HashMap<>(meths.length, 1.0f); for (int i = 0, len = meths.length; i < len; i++) { name = meths[i].getName(); HashMap<Integer,Method> declaredMethodsMap = declaredMethods.get(name); if (declaredMethodsMap == null) { declaredMethodsMap = new HashMap<>(4, 1.0f); declaredMethods.put(name, declaredMethodsMap); } declaredMethodsMap.put(getKey(meths[i].getParameterTypes()), meths[i]); } try { BeanInfo info = Introspector.getBeanInfo(clazz); PropertyDescriptor[] pds = info.getPropertyDescriptors(); if (pds != null) { if (propertyDescriptors == null) { propertyDescriptors = new HashMap<>(pds.length, 1.0f); } for (PropertyDescriptor pd : pds) { propertyDescriptors.put(pd.getName(), pd); } } } catch (IntrospectionException ie) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, ie.toString(), ie); } } } // ---------------------------------------------------------- Public Methods /** * <p>Looks up a <code>Constructor</code> based off the specified * <code>params</code>.</p> * @param params constructor parameters * @return the <code>Constructor</code> appropriate to the specified * parameters or <code>null</code> */ public Constructor lookupConstructor(Class<?>... params) { return constructors.get(getKey(params)); } /** * <p>Looks up a <code>Method</code> based off the specified method * name and <code>params</code>.</p> * @param name the name of the <cod>Method</code> * @param params the <code>Method</code> parameters * @return the <code>Method</code> appropriate to the specified * name and parameters or <code>null</code> */ public Method lookupMethod(String name, Class<?>... params) { Map<Integer,Method> map = methods.get(name); Integer key = getKey(params); Method result = null; if ((null == map) || null == (result = map.get(key))) { map = declaredMethods.get(name); if (null != map) { result = map.get(key); } } return result; } /** * <p>Looks up the class for this MetaData instance.</p> * @return the <code>Class</code> for this MetaData instance */ public Class<?> lookupClass() { return clazz; } /** * @param propName a JavaBeans property name * @return a method suitable for setting a JavaBeans property, or * <code>null</code> if the property doesn't exist or is readonly. */ public Method lookupWriteMethod(String propName) { if (propertyDescriptors == null) { return null; } PropertyDescriptor pd = propertyDescriptors.get(propName); if (pd != null) { return pd.getWriteMethod(); } return null; } /** * @param propName a JavaBeans property name * @return a method suitable for obtaining the value of a JavaBeans property, * or <code>null</code> if the property doesn't exist or can't be read. */ public Method lookupReadMethod(String propName) { if (propertyDescriptors == null) { return null; } PropertyDescriptor pd = propertyDescriptors.get(propName); if (pd != null) { return pd.getReadMethod(); } return null; } // --------------------------------------------------------- Private Methods /** * Return a hashcode of all the class parameters. * @param params the parameters to a <code>Constructor</code> or * a <code>Method</code> instance * @return the result of <code>Arrays.deepHashCode</code> */ private static Integer getKey(Class<?>... params) { return Arrays.deepHashCode(params); } } } // END ReflectionUtils