/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* HUMBOLDT EU Integrated Project #030962
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.util.reflection;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
/**
* Provides several utility methods which use Java Reflections to access hidden
* types, fields and methods.
*
* @author Michel Kraemer
*/
public class ReflectionHelper {
/**
* The package resolver used to retrieve URLs to packages
*/
private static PackageResolver _packageResolver = new DefaultPackageResolver();
/**
* Sets the package resolver used to retrieve URLs to packages
*
* @see #getFilesFromPackage(String)
* @param res the package resolver
*/
public static synchronized void setPackageResolver(PackageResolver res) {
_packageResolver = res;
}
/**
* Returns a public setter method for a given property
*
* @param c the class which would contain the setter method
* @param property the property
* @param propertyType the property's type
* @return the public setter or null if there is no setter for this property
*/
public static Method findSetter(Class<?> c, String property, Class<?> propertyType) {
char[] propertyNameArr = property.toCharArray();
propertyNameArr[0] = Character.toUpperCase(propertyNameArr[0]);
String setterName = "set" + new String(propertyNameArr); //$NON-NLS-1$
Method result = null;
Method[] methods = c.getMethods();
for (Method m : methods) {
if (m.getName().equals(setterName)
&& m.getParameterTypes().length == 1
&& (propertyType == null || m.getParameterTypes()[0]
.isAssignableFrom(propertyType))) {
result = m;
break;
}
}
return result;
}
/**
* Returns a setter method for a given property, no matter if the method is
* visible or hidden or if it is declared in the given class or in a
* superclass or implemented interface.
*
* @param c the class which would contain the setter method
* @param property the property
* @param propertyType the property's type
* @return the setter or null if there is no setter for this property
*/
public static Method findDeepSetter(Class<?> c, String property, Class<?> propertyType) {
if (c == Object.class || c == null) {
return null;
}
char[] propertyNameArr = property.toCharArray();
propertyNameArr[0] = Character.toUpperCase(propertyNameArr[0]);
String setterName = "set" + new String(propertyNameArr); //$NON-NLS-1$
Method result = null;
// search visible and hidden methods in this class
Method[] methods = c.getDeclaredMethods();
for (Method m : methods) {
if (m.getName().equals(setterName)
&& m.getParameterTypes().length == 1
&& (propertyType == null || m.getParameterTypes()[0]
.isAssignableFrom(propertyType))) {
result = m;
break;
}
}
// search superclass and interfaces
if (result == null) {
result = findDeepSetter(c.getSuperclass(), property, propertyType);
if (result == null) {
Class<?>[] interfaces = c.getInterfaces();
for (Class<?> inter : interfaces) {
result = findDeepSetter(inter, property, propertyType);
if (result != null) {
break;
}
}
}
}
return result;
}
/**
* Returns a public getter method for a given property
*
* @param c the class which would contain the getter method
* @param property the property
* @param propertyType the property's type
* @return the public getter or null if there is no getter for this property
*/
public static Method findGetter(Class<?> c, String property, Class<?> propertyType) {
char[] propertyNameArr = property.toCharArray();
propertyNameArr[0] = Character.toUpperCase(propertyNameArr[0]);
String getterName = "get" + new String(propertyNameArr); //$NON-NLS-1$
Method result = null;
Method[] methods = c.getMethods();
for (Method m : methods) {
if (m.getName().equals(getterName) && m.getParameterTypes().length == 0
&& (propertyType == null || propertyType.isAssignableFrom(m.getReturnType()))) {
result = m;
break;
}
}
return result;
}
/**
* Returns a getter method for a given property, no matter if the method is
* visible or hidden or if it is declared in the given class or in a
* superclass or implemented interface.
*
* @param c the class which would contain the getter method
* @param property the property
* @param propertyType the property's type (can be null if the type does not
* matter)
* @return the getter or null if there is no getter for this property
*/
public static Method findDeepGetter(Class<?> c, String property, Class<?> propertyType) {
if (c == Object.class || c == null) {
return null;
}
char[] propertyNameArr = property.toCharArray();
propertyNameArr[0] = Character.toUpperCase(propertyNameArr[0]);
String getterName = "get" + new String(propertyNameArr); //$NON-NLS-1$
Method result = null;
// search visible and hidden methods in this class
Method[] methods = c.getDeclaredMethods();
for (Method m : methods) {
if (m.getName().equals(getterName) && m.getParameterTypes().length == 0
&& (propertyType == null || propertyType.isAssignableFrom(m.getReturnType()))) {
result = m;
break;
}
}
// search superclass and interfaces
if (result == null) {
result = findDeepGetter(c.getSuperclass(), property, propertyType);
if (result == null) {
Class<?>[] interfaces = c.getInterfaces();
for (Class<?> inter : interfaces) {
result = findDeepGetter(inter, property, propertyType);
if (result != null) {
break;
}
}
}
}
return result;
}
/**
* Returns a public field for a given property
*
* @param c the class which would contain the field
* @param property the property
* @param propertyType the property's type
* @return the field or null if there is no field for this property
*/
public static Field findField(Class<?> c, String property, Class<?> propertyType) {
Field result = null;
Field[] fields = c.getFields();
for (Field f : fields) {
String fn = f.getName();
if (fn.charAt(0) == '_') {
// handle code style
fn = fn.substring(1);
}
if (fn.equals(property)
&& (propertyType == null || f.getType().isAssignableFrom(propertyType))) {
result = f;
break;
}
}
return result;
}
/**
* Returns a field for a given property, no matter if the field is visible
* or hidden or if it is declared in the given class or in a superclass.
*
* @param c the class which would contain the field
* @param property the property
* @param propertyType the property's type
* @return the field or null if there is no field for this property
*/
public static Field findDeepField(Class<?> c, String property, Class<?> propertyType) {
if (c == Object.class || c == null) {
return null;
}
Field result = null;
// search visible and hidden fields in this class
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
String fn = f.getName();
if (fn.charAt(0) == '_') {
// handle code style
fn = fn.substring(1);
}
if (fn.equals(property)
&& (propertyType == null || f.getType().isAssignableFrom(propertyType))) {
result = f;
break;
}
}
// search superclass
if (result == null) {
result = findDeepField(c.getSuperclass(), property, propertyType);
}
return result;
}
/**
* Invokes a setter method on a given bean
*
* @param bean the object to invoke the getter on
* @param setter the setter method
* @param value the value to set
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the setter is not accessible
* @throws InvocationTargetException if the setter throws an exception
*/
private static void invokeSetter(Object bean, Method setter, Object value)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
boolean accessible = setter.isAccessible();
setter.setAccessible(true);
try {
setter.invoke(bean, value);
} finally {
setter.setAccessible(accessible);
}
}
/**
* Invokes a public setter for a property
*
* @param bean the object to invoke the public setter on
* @param propertyName the name of the property that shall be updated
* @param value the value passed to the setter
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the setter is not accessible
* @throws InvocationTargetException if the setter throws an exception
* @throws NoSuchMethodException if there is no setter for this property
*/
public static void setProperty(Object bean, String propertyName, Object value)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method setter = findSetter(bean.getClass(), propertyName, value.getClass());
if (setter == null) {
throw new NoSuchMethodException("There is " + //$NON-NLS-1$
"no setter for property " + propertyName); //$NON-NLS-1$
}
invokeSetter(bean, setter, value);
}
/**
* Invokes a setter for a property, no matter if the setter is visible or
* hidden or if it is declared in the given class or in a superclass or
* implemented interface.
*
* @param bean the object to invoke the public setter on
* @param propertyName the name of the property that shall be updated
* @param value the value passed to the setter
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the setter is not accessible
* @throws InvocationTargetException if the setter throws an exception
* @throws NoSuchMethodException if there is no setter for this property
*/
public static void setDeepProperty(Object bean, String propertyName, Object value)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method setter = findDeepSetter(bean.getClass(), propertyName, value.getClass());
if (setter == null) {
throw new NoSuchMethodException("There is " + //$NON-NLS-1$
"no setter for property " + propertyName); //$NON-NLS-1$
}
invokeSetter(bean, setter, value);
}
/**
* Invokes a getter method on a given bean
*
* @param <T> the result type
* @param bean the object to invoke the getter on
* @param getter the getter method
* @return the property's value
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the getter is not accessible
* @throws InvocationTargetException if the getter throws an exception
*/
@SuppressWarnings("unchecked")
private static <T> T invokeGetter(Object bean, Method getter) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
boolean accessible = getter.isAccessible();
getter.setAccessible(true);
T result = null;
try {
result = (T) getter.invoke(bean);
} finally {
getter.setAccessible(accessible);
}
return result;
}
/**
* Invokes a public getter for a property
*
* @param <T> the result type
* @param bean the object to invoke the public getter on
* @param propertyName the name of the property to retrieve
* @return the property's value
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the getter is not accessible
* @throws InvocationTargetException if the getter throws an exception
* @throws NoSuchMethodException if there is no getter for this property
*/
public static <T> T getProperty(Object bean, String propertyName)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method getter = findGetter(bean.getClass(), propertyName, Object.class);
if (getter == null) {
throw new NoSuchMethodException("There is " + //$NON-NLS-1$
"no getter for property " + propertyName); //$NON-NLS-1$
}
return ReflectionHelper.<T> invokeGetter(bean, getter);
}
/**
* Invokes a getter for a property, no matter if the getter is visible or
* hidden or if it is declared in the given class or in a superclass or
* implemented interface.
*
* @param <T> the result type
* @param bean the object to invoke the getter on
* @param propertyName the name of the property to retrieve
* @return the property's value
* @throws IllegalArgumentException if the argument's type is invalid
* @throws IllegalAccessException if the setter is not accessible
* @throws InvocationTargetException if the setter throws an exception
* @throws NoSuchMethodException if there is no setter for this property
*/
public static <T> T getDeepProperty(Object bean, String propertyName)
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method getter = findDeepGetter(bean.getClass(), propertyName, null);
if (getter == null) {
throw new NoSuchMethodException("There is " + //$NON-NLS-1$
"no getter for property " + propertyName); //$NON-NLS-1$
}
return ReflectionHelper.<T> invokeGetter(bean, getter);
}
/**
* Invokes a getter for a property, or gets the matching field, no matter
* what.
*
* @param bean the object to invoke the getter on
* @param propertyName the name of the property to retrieve
* @param valueClass the class of the property or field
* @return the property's value
* @throws IllegalArgumentException if the argument's type is invalid
* @throws InvocationTargetException if the getter throws an exception
* @throws IllegalStateException is the field could be found or accessed
*/
@SuppressWarnings("unchecked")
public static <T> T getDeepPropertyOrField(Object bean, String propertyName, Class<T> valueClass)
throws IllegalArgumentException, InvocationTargetException {
try {
return ReflectionHelper.getDeepProperty(bean, propertyName);
} catch (NoSuchMethodException e) {/* ignore */
} catch (IllegalAccessException e) {/* ignore */
}
// there is no getter for the property. try to get the field directly
Field f = findDeepField(bean.getClass(), propertyName, valueClass);
if (f == null) {
throw new IllegalStateException("Could not find " + "field for property "
+ propertyName + " in class " + bean.getClass().getCanonicalName());
}
boolean access = f.isAccessible();
f.setAccessible(true);
try {
return (T) f.get(bean);
} catch (Exception e) {
throw new IllegalStateException("Could not get " + "field for property " + propertyName
+ "in class " + bean.getClass().getCanonicalName(), e);
} finally {
f.setAccessible(access);
}
}
/**
* @return the URL to the JAR file this class is in or null
* @throws MalformedURLException if the URL to the jar file could not be
* created
*/
public static URL getCurrentJarURL() throws MalformedURLException {
String name = ReflectionHelper.class.getCanonicalName();
name = name.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
name = name + ".class"; //$NON-NLS-1$
URL url = ReflectionHelper.class.getClassLoader().getResource(name);
String str = url.toString();
int to = str.indexOf("!/"); //$NON-NLS-1$
if (to == -1) {
url = ClassLoader.getSystemResource(name);
if (url != null) {
str = url.toString();
to = str.indexOf("!/"); //$NON-NLS-1$
}
else {
return null;
}
}
return new URL(str.substring(0, to + 2));
}
/**
* Returns an array of all files contained by a given package
*
* @param pkg the package (e.g. "de.igd.fhg.CityServer3D")
* @return an array of files
* @throws IOException if the package could not be found
*/
public static synchronized File[] getFilesFromPackage(String pkg) throws IOException {
File[] files;
JarFile jarFile = null;
try {
URL u = _packageResolver.resolve(pkg);
if (u != null && !u.toString().startsWith("jar:")) { //$NON-NLS-1$
// we got the package as an URL. Simply create a file
// from this URL
File dir;
try {
dir = new File(u.toURI());
} catch (URISyntaxException e) {
// if the URL contains spaces and they have not been
// replaced by %20 then we'll have to use the following line
dir = new File(u.getFile());
}
if (!dir.isDirectory()) {
// try another method
dir = new File(u.getFile());
}
files = null;
if (dir.isDirectory()) {
files = dir.listFiles();
}
}
else {
// the package may be in a jar file
// get the current jar file and search it
if (u != null && u.toString().startsWith("jar:file:")) { //$NON-NLS-1$
// first try using URL and File
try {
String p = u.toString().substring(4);
p = p.substring(0, p.indexOf("!/")); //$NON-NLS-1$
File file = new File(URI.create(p));
p = file.getAbsolutePath();
try {
jarFile = new JarFile(p);
} catch (ZipException e) {
throw new IllegalArgumentException("No zip file: " + p, e); //$NON-NLS-1$
}
} catch (Throwable e1) {
// second try directly using path
String p = u.toString().substring(9);
p = p.substring(0, p.indexOf("!/")); //$NON-NLS-1$
try {
jarFile = new JarFile(p);
} catch (ZipException e2) {
throw new IllegalArgumentException("No zip file: " + p, e2); //$NON-NLS-1$
}
}
}
else {
u = getCurrentJarURL();
// open jar file
JarURLConnection juc = (JarURLConnection) u.openConnection();
jarFile = juc.getJarFile();
}
// enumerate entries and add those that match the package path
Enumeration<JarEntry> entries = jarFile.entries();
ArrayList<String> file_names = new ArrayList<String>();
String package_path = pkg.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
boolean slashed = false;
if (package_path.charAt(0) == '/') {
package_path = package_path.substring(1);
slashed = true;
}
while (entries.hasMoreElements()) {
JarEntry j = entries.nextElement();
if (j.getName().matches("^" + package_path + ".+\\..+")) { //$NON-NLS-1$ //$NON-NLS-2$
if (slashed) {
file_names.add("/" + j.getName()); //$NON-NLS-1$
}
else {
file_names.add(j.getName());
}
}
}
// convert list to array
files = new File[file_names.size()];
Iterator<String> i = file_names.iterator();
int n = 0;
while (i.hasNext()) {
files[n++] = new File(i.next());
}
}
} catch (Throwable e) {
throw new IOException("Could not find package: " + pkg, e); //$NON-NLS-1$
} finally {
if (jarFile != null) {
jarFile.close();
}
}
if (files != null && files.length == 0)
return null; // let's not require paranoid callers
return files;
}
/**
* Gets a list of all classes in the given package and all subpackages
* recursively.
*
* @param pkg the package
* @param classLoader the class loader to use
* @return the list of classes
* @throws IOException if a subpackage or a class could not be loaded
*/
public static List<Class<?>> getClassesFromPackage(String pkg, ClassLoader classLoader)
throws IOException {
return getClassesFromPackage(pkg, classLoader, true);
}
/**
* Gets a list of all classes in the given package
*
* @param pkg the package
* @param classLoader the class loader to use
* @param recursive true if all subpackages shall be traversed too
* @return the list of classes
* @throws IOException if a subpackage or a class could not be loaded
*/
public static List<Class<?>> getClassesFromPackage(String pkg, ClassLoader classLoader,
boolean recursive) throws IOException {
List<Class<?>> result = new ArrayList<Class<?>>();
getClassesFromPackage(pkg, result, classLoader, recursive);
return result;
}
/**
* Gets a list of all subpackages in the given package
*
* @param pkg the package
* @return the list of classes
* @throws IOException if a subpackage or a class could not be loaded
*/
public static List<String> getSubPackagesFromPackage(String pkg) throws IOException {
return getSubPackagesFromPackage(pkg, true);
}
/**
* Gets a list of all subpackages in the given package
*
* @param pkg the package
* @param recursive true if all subpackages shall be traversed too
* @return the list of classes
* @throws IOException if a subpackage or a class could not be loaded
*/
public static List<String> getSubPackagesFromPackage(String pkg, boolean recursive)
throws IOException {
List<String> result = new ArrayList<String>();
getSubPackagesFromPackage(pkg, result, recursive);
return result;
}
private static void getSubPackagesFromPackage(String pkg, List<String> l, boolean recursive)
throws IOException {
File[] files = getFilesFromPackage(pkg);
for (File f : files) {
String name = f.getName();
if (f.isDirectory() && !name.startsWith(".")) { //$NON-NLS-1$
l.add(pkg + "." + name); //$NON-NLS-1$
if (recursive) {
getSubPackagesFromPackage(pkg + "." + name, l, true); //$NON-NLS-1$
}
}
}
}
private static void getClassesFromPackage(String pkg, List<Class<?>> l,
ClassLoader classLoader, boolean recursive) throws IOException {
File[] files = getFilesFromPackage(pkg);
for (File f : files) {
String name = f.getName();
if (f.isDirectory() && recursive) {
if (!name.startsWith(".")) { //$NON-NLS-1$
getClassesFromPackage(pkg + "." + name, l, classLoader, true); //$NON-NLS-1$
}
}
else if (name.toLowerCase().endsWith(".class")) { //$NON-NLS-1$
// the following lines make sure we also handle classes
// in subpackages. These subpackages may be returned by
// ApplicationContext.getFilesFromPackage() when we are
// in a jar file
String classPath = f.toURI().toString().replace('/', '.').replace('\\', '.');
String className = classPath.substring(classPath.lastIndexOf(pkg),
classPath.lastIndexOf('.'));
Class<?> c;
try {
c = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException e) {
throw new IOException("Could not load class: " + //$NON-NLS-1$
e.getMessage());
}
l.add(c);
}
}
}
/**
* <p>
* Find the most specialised class from group compatible with clazz. A
* direct superclass match is searched and returned if found.
* </p>
* <p>
* If not and checkAssignability is true, the most derived assignable class
* is being searched.
* </p>
* <p>
* See The Java Language Specification, sections 5.1.1 and 5.1.4 , for
* details.
* </p>
*
* @param clazz a class
* @param group a collection of classes to match against
* @param checkAssignability whether to use assignability when no direct
* match is found
* @return null or the most specialised match from group
*/
public static Class<?> findMostSpecificMatch(Class<?> clazz, Collection<Class<?>> group,
boolean checkAssignability) {
if (clazz == null || group == null)
throw new IllegalArgumentException(""); //$NON-NLS-1$
// scale up the type hierarchy until we have found a matching class
for (Class<?> c = clazz; c != Object.class; c = c.getSuperclass()) {
if (group.contains(c))
return c;
}
if (checkAssignability) {
// in lieu of a direct match, check assignability (likely clazz is
// an interface)
Class<?> result = null;
for (Class<?> c : group) {
if (clazz.isAssignableFrom(c)) {
// result null or less specialized -> overwrite
if (result == null || result.isAssignableFrom(c))
result = c;
}
}
return result;
}
else {
return null;
}
}
/**
* Performs a shallow copy of all fields defined by the class of src and all
* superclasses.
*
* @param <T> the type of the source and destination object
* @param src the source object
* @param dst the destination object
* @throws IllegalArgumentException if a field is unaccessible
* @throws IllegalAccessException if a field is not accessible
*/
public static <T> void shallowEnforceDeepProperties(T src, T dst)
throws IllegalArgumentException, IllegalAccessException {
Class<?> cls = src.getClass();
shallowEnforceDeepProperties(cls, src, dst);
}
private static <T> void shallowEnforceDeepProperties(Class<?> cls, T src, T dst)
throws IllegalArgumentException, IllegalAccessException {
if (cls == Object.class || cls == null) {
return;
}
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
continue;
}
boolean accessible = f.isAccessible();
f.setAccessible(true);
try {
Object val = f.get(src);
f.set(dst, val);
} finally {
f.setAccessible(accessible);
}
}
shallowEnforceDeepProperties(cls.getSuperclass(), src, dst);
}
}