/*
 * Copyright 2004,2004 The Apache Software Foundation.
 * 
 * Licensed 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 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 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.
 *
 * @author   Sanjiva Weerawarana
 * @author   Joseph Kesselman
 */
public class ReflectionUtils {

  //////////////////////////////////////////////////////////////////////////

  /**
   * 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 (Object source, String eventSetName,
									   EventProcessor processor)
	   throws IntrospectionException, IllegalArgumentException,
			  IllegalAccessException, InstantiationException,
			  InvocationTargetException {
	// find the event set descriptor for this event
	BeanInfo bi = Introspector.getBeanInfo (source.getClass ());
	EventSetDescriptor esd = (EventSetDescriptor)
	  findFeatureByName ("event", eventSetName, bi.getEventSetDescriptors ());

	if (esd == null)        // no events found, maybe a proxy from OpenOffice.org?
        {
          throw new IllegalArgumentException ("event set '" + eventSetName +
                                              "' unknown for source type '" + source.getClass () + "'");
	}

	// get the class object for the event
	Class listenerType=esd.getListenerType(); // get ListenerType class object from EventSetDescriptor

	// find an event adapter class of the right type
	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
	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 (ClassLoader cld, String className,
								 Class[] argTypes, Object[] args)
	   throws ClassNotFoundException, NoSuchMethodException,
			  InstantiationException, IllegalAccessException,
			  IllegalArgumentException, InvocationTargetException,
			  IOException {
	if (argTypes != null) {
	  // find the right constructor and use that to create bean
	  Class cl = (cld != null) ? cld.loadClass (className)
				   : Thread.currentThread().getContextClassLoader().loadClass (className); // rgf, 2006-01-05
                                   // : Class.forName (className);

	  Constructor c = MethodUtils.getConstructor (cl, argTypes);
	  return new Bean (cl, c.newInstance (args));
	} else {
	  // create the bean with no args constructor
	  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 (ClassLoader cld, String className,
								 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 (String featureType, String name,
									   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 (Object target, String fieldName)
	  throws IllegalArgumentException, IllegalAccessException {
	// This is to handle how we do static fields.
	Class targetClass = (target instanceof Class)
						? (Class) target
						: target.getClass ();

	try {
	  Field f = targetClass.getField (fieldName);
	  Class fieldType = f.getType ();

	  // Get the value and return it.
	  Object value = f.get (target);
	  return new Bean (fieldType, value);
	} catch (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 (Object target, String propName,
								  Integer index)
	   throws IntrospectionException, IllegalArgumentException,
			  IllegalAccessException, InvocationTargetException {
	// find the property descriptor
	BeanInfo bi = Introspector.getBeanInfo (target.getClass ());
	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");
	  }
	  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 (Object target, String fieldName, Bean value,
							   TypeConvertorRegistry tcr)
	  throws IllegalArgumentException, IllegalAccessException {
	// This is to handle how we do static fields.
	Class targetClass = (target instanceof Class)
						? (Class) target
						: target.getClass ();

	try {
	  Field f = targetClass.getField (fieldName);
	  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) {
		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 (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 (Object target, String propName,
								  Integer index, Object value,
								  Class valueType, TypeConvertorRegistry tcr)
	   throws IntrospectionException, IllegalArgumentException,
			  IllegalAccessException, InvocationTargetException {
	// find the property descriptor
	BeanInfo bi = Introspector.getBeanInfo (target.getClass ());
	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");
	  }
	  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) {
	  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});
	}
  }
}
