blob: ba09f7f419b6c61d1daa6d8dca0b7ac3a0adb69c [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 org.apache.bsf.util;
import org.apache.bsf.BSFManager; // rgf, 20070917
import java.beans.BeanInfo;
import java.beans.Beans;
import java.beans.EventSetDescriptor;
import java.beans.FeatureDescriptor;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.TreeSet;
import java.util.Comparator;
import java.util.Iterator;
import org.apache.bsf.util.event.EventAdapter;
import org.apache.bsf.util.event.EventAdapterRegistry;
import org.apache.bsf.util.event.EventProcessor;
import org.apache.bsf.util.type.TypeConvertor;
import org.apache.bsf.util.type.TypeConvertorRegistry;
/**
* This file is a collection of reflection utilities. There are utilities for creating beans, getting bean infos, setting/getting properties, and binding
* events.
*/
/*
* 2007-09-21: Rony G. Flatscher, new class loading sequence:
*
* - supplied class loader (given as an argument) - Thread's context class loader - BSFManager's defining class loader
*
* 2011-10-29: Rony G. Flatscher, in case an event is not found, create a user-friendly error message that lists all available event names
*
* 2011-10-29: Rony G. Flatscher, make sure that the context class loader is used only, if not null
*/
public class ReflectionUtils {
// rgf, 20070921: class loaders that we might need to load classes
static ClassLoader bsfManagerDefinedCL = BSFManager.getDefinedClassLoader();
//////////////////////////////////////////////////////////////////////////
/**
* Add an event processor as a listener to some event coming out of an object.
*
* @param source event source
* @param eventSetName name of event set from event src to bind to
* @param processor event processor the event should be delegated to when it occurs; either via processEvent or processExceptionableEvent.
*
* @exception IntrospectionException if unable to introspect
* @exception IllegalArgumentException if event set is unknown
* @exception IllegalAccessException if the event adapter class or initializer is not accessible.
* @exception InstantiationException if event adapter instantiation fails
* @exception InvocationTargetException if something goes wrong while running add event listener method
*/
public static void addEventListener(final Object source, final String eventSetName, final EventProcessor processor)
throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException {
// find the event set descriptor for this event
final BeanInfo bi = Introspector.getBeanInfo(source.getClass());
final EventSetDescriptor[] arrESD = bi.getEventSetDescriptors();
final EventSetDescriptor esd = (EventSetDescriptor) findFeatureByName("event", eventSetName, arrESD);
if (esd == null) // no events found, maybe a proxy from OpenOffice.org?
{
String errMsg = "event set '" + eventSetName + "' unknown for source type '" + source.getClass() + "': ";
if (arrESD.length == 0) // no event sets found in class!
{
errMsg = errMsg + "class does not implement any event methods following Java's event pattern!";
} else {
// errMsg=errMsg+"class defines the following event set(s): {";
errMsg = errMsg + "class defines the following event set(s): ";
// sort ESD by Name
final TreeSet ts = new TreeSet(new Comparator() {
public int compare(final Object o1, final Object o2) {
return ((EventSetDescriptor) o1).getName().compareToIgnoreCase(((EventSetDescriptor) o2).getName());
}
public boolean equals(final Object o1, final Object o2) {
return ((EventSetDescriptor) o1).getName().equalsIgnoreCase(((EventSetDescriptor) o2).getName());
}
});
for (int i = 0; i < arrESD.length; i++) {
ts.add(arrESD[i]);
}
final Iterator it = ts.iterator(); // get iterator
int i = 0;
while (it.hasNext()) // iterate in sorted order
{
final EventSetDescriptor tmpESD = (EventSetDescriptor) it.next();
if (i > 0) {
errMsg = errMsg + ", ";
}
errMsg = errMsg + "\n\t" + '\'' + tmpESD.getName() + "'={"; // event set name
// iterate over listener methods and display their names in sorted order
final Method[] m = tmpESD.getListenerMethods();
final TreeSet tsM = new TreeSet(new Comparator() {
public int compare(final Object o1, final Object o2) {
return ((Method) o1).getName().compareToIgnoreCase(((Method) o2).getName());
}
public boolean equals(final Object o1, final Object o2) {
return ((Method) o1).getName().equalsIgnoreCase(((Method) o2).getName());
}
});
for (int j = 0; j < m.length; j++) {
tsM.add(m[j]);
}
final Iterator itM = tsM.iterator();
int j = 0;
while (itM.hasNext()) {
if (j > 0) {
errMsg = errMsg + ',';
}
errMsg = errMsg + '\'' + ((Method) itM.next()).getName() + '\'';
j++;
}
errMsg = errMsg + '}'; // close event method set
i++;
}
errMsg = errMsg + "."; // close set of event sets
}
throw new IllegalArgumentException(errMsg);
}
// get the class object for the event
final Class listenerType = esd.getListenerType(); // get ListenerType class object from EventSetDescriptor
// find an event adapter class of the right type
final Class adapterClass = EventAdapterRegistry.lookup(listenerType);
if (adapterClass == null) {
throw new IllegalArgumentException("event adapter for listener type " + "'" + listenerType + "' (eventset " + "'" + eventSetName + "') unknown");
}
// create the event adapter and give it the event processor
final EventAdapter adapter = (EventAdapter) adapterClass.newInstance();
adapter.setEventProcessor(processor);
// bind the adapter to the source bean
Method addListenerMethod;
Object[] args;
if (eventSetName.equals("propertyChange") || eventSetName.equals("vetoableChange")) {
// In Java 1.2, beans may have direct listener adding methods
// for property and vetoable change events which take the
// property name as a filter to be applied at the event source.
// The filter property of the event processor should be used
// in this case to support the source-side filtering.
//
// ** TBD **: the following two lines need to change appropriately
addListenerMethod = esd.getAddListenerMethod();
args = new Object[] { adapter };
} else {
addListenerMethod = esd.getAddListenerMethod();
args = new Object[] { adapter };
}
addListenerMethod.invoke(source, args);
}
//////////////////////////////////////////////////////////////////////////
/**
* Create a bean using given class loader and using the appropriate constructor for the given args of the given arg types.
*
* @param cld the class loader to use. If null, Class.forName is used.
* @param className name of class to instantiate
* @param argTypes array of argument types
* @param args array of arguments
*
* @return the newly created bean
*
* @exception ClassNotFoundException if class is not loaded
* @exception NoSuchMethodException if constructor can't be found
* @exception InstantiationException if class can't be instantiated
* @exception IllegalAccessException if class is not accessible
* @exception IllegalArgumentException if argument problem
* @exception InvocationTargetException if constructor excepted
* @exception IOException if I/O error in beans.instantiate
*/
public static Bean createBean(final ClassLoader cld, final String className, final Class[] argTypes, final Object[] args) throws ClassNotFoundException,
NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
if (argTypes != null) {
// if class loader given, use that one, else try
// the Thread's context class loader (if set) and then
// the BSFMananger defining class loader
Class cl = null;
ClassNotFoundException exCTX = null;
// -----------------------------
if (cld != null) { // class loader supplied as argument
try { // CL passed as argument
cl = cld.loadClass(className);
} catch (final ClassNotFoundException e02) {
exCTX = e02;
}
}
if (cl == null) {
// load context class loader, only use it, if not null
final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl != null) {
try { // CTXCL
cl = tccl.loadClass(className);
} catch (final ClassNotFoundException e01) {
}
}
}
if (cl == null) { // class not loaded yet
// defined CL
if (cld != bsfManagerDefinedCL) { // if not used already, attempt to load
cl = bsfManagerDefinedCL.loadClass(className);
} else { // classloader was already used, hence re-throw exception
throw exCTX; // re-throw very first exception
}
}
// -----------------------------
final Constructor c = MethodUtils.getConstructor(cl, argTypes);
return new Bean(cl, c.newInstance(args));
} else {
// create the bean with no args constructor
final Object obj = Beans.instantiate(cld, className);
return new Bean(obj.getClass(), obj);
}
}
//////////////////////////////////////////////////////////////////////////
/**
* Create a bean using given class loader and using the appropriate constructor for the given args. Figures out the arg types and calls above.
*
* @param cld the class loader to use. If null, Class.forName is used.
* @param className name of class to instantiate
* @param args array of arguments
*
* @return the newly created bean
*
* @exception ClassNotFoundException if class is not loaded
* @exception NoSuchMethodException if constructor can't be found
* @exception InstantiationException if class can't be instantiated
* @exception IllegalAccessException if class is not accessible
* @exception IllegalArgumentException if argument problem
* @exception InvocationTargetException if constructor excepted
* @exception IOException if I/O error in beans.instantiate
*/
public static Bean createBean(final ClassLoader cld, final String className, final Object[] args) throws ClassNotFoundException, NoSuchMethodException,
InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
Class[] argTypes = null;
if (args != null) {
argTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = (args[i] != null) ? args[i].getClass() : null;
}
}
return createBean(cld, className, argTypes, args);
}
//////////////////////////////////////////////////////////////////////////
/**
* locate the item in the fds array whose name is as given. returns null if not found.
*/
private static FeatureDescriptor findFeatureByName(final String featureType, final String name, final FeatureDescriptor[] fds) {
for (int i = 0; i < fds.length; i++) {
if (name.equals(fds[i].getName())) {
return fds[i];
}
}
return null;
}
public static Bean getField(final Object target, final String fieldName) throws IllegalArgumentException, IllegalAccessException {
// This is to handle how we do static fields.
final Class targetClass = (target instanceof Class) ? (Class) target : target.getClass();
try {
final Field f = targetClass.getField(fieldName);
final Class fieldType = f.getType();
// Get the value and return it.
final Object value = f.get(target);
return new Bean(fieldType, value);
} catch (final NoSuchFieldException e) {
throw new IllegalArgumentException("field '" + fieldName + "' is " + "unknown for '" + target + "'");
}
}
//////////////////////////////////////////////////////////////////////////
/**
* Get a property of a bean.
*
* @param target the object whose prop is to be gotten
* @param propName name of the property to set
* @param index index to get (if property is indexed)
*
* @exception IntrospectionException if unable to introspect
* @exception IllegalArgumentException if problems with args: if the property is unknown, or if the property is given an index when its not, or if the
* property is not writeable, or if the given value cannot be assigned to the it (type mismatch).
* @exception IllegalAccessException if read method is not accessible
* @exception InvocationTargetException if read method excepts
*/
public static Bean getProperty(final Object target, final String propName, final Integer index)
throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
// find the property descriptor
final BeanInfo bi = Introspector.getBeanInfo(target.getClass());
final PropertyDescriptor pd = (PropertyDescriptor) findFeatureByName("property", propName, bi.getPropertyDescriptors());
if (pd == null) {
throw new IllegalArgumentException("property '" + propName + "' is " + "unknown for '" + target + "'");
}
// get read method and type of property
Method rm;
Class propType;
if (index != null) {
// if index != null, then property is indexed - pd better be so too
if (!(pd instanceof IndexedPropertyDescriptor)) {
throw new IllegalArgumentException("attempt to get non-indexed " + "property '" + propName + "' as being indexed");
}
final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
rm = ipd.getIndexedReadMethod();
propType = ipd.getIndexedPropertyType();
} else {
rm = pd.getReadMethod();
propType = pd.getPropertyType();
}
if (rm == null) {
throw new IllegalArgumentException("property '" + propName + "' is not readable");
}
// now get the value
Object propVal = null;
if (index != null) {
propVal = rm.invoke(target, new Object[] { index });
} else {
propVal = rm.invoke(target, null);
}
return new Bean(propType, propVal);
}
public static void setField(final Object target, final String fieldName, final Bean value, final TypeConvertorRegistry tcr)
throws IllegalArgumentException, IllegalAccessException {
// This is to handle how we do static fields.
final Class targetClass = (target instanceof Class) ? (Class) target : target.getClass();
try {
final Field f = targetClass.getField(fieldName);
final Class fieldType = f.getType();
// type convert the value if necessary
Object fieldVal = null;
boolean okeydokey = true;
if (fieldType.isAssignableFrom(value.type)) {
fieldVal = value.value;
} else if (tcr != null) {
final TypeConvertor cvtor = tcr.lookup(value.type, fieldType);
if (cvtor != null) {
fieldVal = cvtor.convert(value.type, fieldType, value.value);
} else {
okeydokey = false;
}
} else {
okeydokey = false;
}
if (!okeydokey) {
throw new IllegalArgumentException("unable to assign '" + value.value + "' to field '" + fieldName + "'");
}
// now set the value
f.set(target, fieldVal);
} catch (final NoSuchFieldException e) {
throw new IllegalArgumentException("field '" + fieldName + "' is " + "unknown for '" + target + "'");
}
}
//////////////////////////////////////////////////////////////////////////
/**
* Set a property of a bean to a given value.
*
* @param target the object whose prop is to be set
* @param propName name of the property to set
* @param index index to set (if property is indexed)
* @param value the property value
* @param valueType the type of the above (needed when its null)
* @param tcr type convertor registry to use to convert value type to property type if necessary
*
* @exception IntrospectionException if unable to introspect
* @exception IllegalArgumentException if problems with args: if the property is unknown, or if the property is given an index when its not, or if the
* property is not writeable, or if the given value cannot be assigned to the it (type mismatch).
* @exception IllegalAccessException if write method is not accessible
* @exception InvocationTargetException if write method excepts
*/
public static void setProperty(final Object target, final String propName, final Integer index, final Object value, final Class valueType,
final TypeConvertorRegistry tcr) throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
// find the property descriptor
final BeanInfo bi = Introspector.getBeanInfo(target.getClass());
final PropertyDescriptor pd = (PropertyDescriptor) findFeatureByName("property", propName, bi.getPropertyDescriptors());
if (pd == null) {
throw new IllegalArgumentException("property '" + propName + "' is " + "unknown for '" + target + "'");
}
// get write method and type of property
Method wm;
Class propType;
if (index != null) {
// if index != null, then property is indexed - pd better be so too
if (!(pd instanceof IndexedPropertyDescriptor)) {
throw new IllegalArgumentException("attempt to set non-indexed " + "property '" + propName + "' as being indexed");
}
final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
wm = ipd.getIndexedWriteMethod();
propType = ipd.getIndexedPropertyType();
} else {
wm = pd.getWriteMethod();
propType = pd.getPropertyType();
}
if (wm == null) {
throw new IllegalArgumentException("property '" + propName + "' is not writeable");
}
// type convert the value if necessary
Object propVal = null;
boolean okeydokey = true;
if (propType.isAssignableFrom(valueType)) {
propVal = value;
} else if (tcr != null) {
final TypeConvertor cvtor = tcr.lookup(valueType, propType);
if (cvtor != null) {
propVal = cvtor.convert(valueType, propType, value);
} else {
okeydokey = false;
}
} else {
okeydokey = false;
}
if (!okeydokey) {
throw new IllegalArgumentException("unable to assign '" + value + "' to property '" + propName + "'");
}
// now set the value
if (index != null) {
wm.invoke(target, new Object[] { index, propVal });
} else {
wm.invoke(target, new Object[] { propVal });
}
}
}