/*
 * Copyright 2004,2008 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.
 *
 * 2009-09-10, ---rgf, correct wrong expectation, that a context class loader is always set
 *
 * 2008-06-23, ---rgf, addEventListener(): in case an event set was not found, supply
 *                     available event sets, if possible at all;
 */

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.
 *
 * @author   Sanjiva Weerawarana
 * @author   Joseph Kesselman
 */
 /*  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
 */
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 (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 ());

// System.err.println(">>>>> source=["+source+"], source.getClass()=["+source.getClass()+"]<<<<<");
// System.err.println(">>>>> bi.getEventSetDescriptors(): "+bi.getEventSetDescriptors().length+"<<<<<\n");

        EventSetDescriptor arrESD[]=bi.getEventSetDescriptors ();
	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): ";



/*
              for (int i=0;i<arrESD.length;i++) // iterate over available event set, add them to the error message
              {
                  if (i>0)
                  {
                      errMsg=errMsg+", ";
                  }
                  errMsg=errMsg+"\n\t"+'\''+arrESD[i].getName()+"'={";  // event set name

                    // iterate over listener methods and display their names
                  Method m[]=arrESD[i].getListenerMethods();
                  if (m.length>0)   // listener methods available
                  {
                      for (int j=0;j<m.length;j++)
                      {
                          if (j>0)
                          {
                              errMsg=errMsg+',';
                          }
                          errMsg=errMsg+'\''+m[j].getName()+'\'';
                      }
                  }
                  errMsg=errMsg+'}';    // close event method set

              }
*/
              // sort ESD by Name
              TreeSet ts=new TreeSet(new Comparator () {
                          public int    compare(Object o1, Object o2) {return ((EventSetDescriptor)o1).getName().compareToIgnoreCase(((EventSetDescriptor)o2).getName());}
                          public boolean equals(Object o1, Object o2) {return ((EventSetDescriptor)o1).getName().equalsIgnoreCase   (((EventSetDescriptor)o2).getName());}
                         });

              for (int i=0;i<arrESD.length;i++)
              {
                  ts.add(arrESD[i]);
              }
              Iterator it=ts.iterator();    // get iterator

              int i=0;
              while (it.hasNext())          // iterate in sorted order
              {
                  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
                  Method m[]=tmpESD.getListenerMethods();
                  TreeSet tsM=new TreeSet(new Comparator () {
                          public int    compare(Object o1, Object o2) {return ((Method)o1).getName().compareToIgnoreCase(((Method)o2).getName());}
                          public boolean equals(Object o1, Object o2) {return ((Method)o1).getName().equalsIgnoreCase   (((Method)o2).getName());}
                         });

                  for (int j=0;j<m.length;j++)
                  {
                      tsM.add(m[j]);
                  }
                  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
              errMsg=errMsg+".";       // close set of event sets
          }
          throw new IllegalArgumentException (errMsg);
	}

	// 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) {

/* original (remarked 20070917, rgf)
	  // 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);
*/
            // TODO: final decision about class loading strategy
            // rgf, 20070917: if class loader given, use that one, else try
            //                the Thread's context class loader 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 (ClassNotFoundException e02) {
                  exCTX=e02;
              }
          }

          if (cl==null) {
              // rgf, 2009-09-10
              ClassLoader tccl=Thread.currentThread().getContextClassLoader();
              if (tccl!=null)
              {
                  try {         // CTXCL
                      cl=tccl.loadClass(className);
                  }
                  catch (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 {    // already , throw exception
                  throw exCTX;        // re-throw very first exception
              }
          }
// -----------------------------

/*
          try {     // try supplied class loader
              if (cld !=null)
                  cl=cld.loadClass(className);
          }
          catch (ClassNotFoundException e01) {
              try {
                  cl=Thread.currentThread().getContextClassLoader().loadClass(className);
              }
              catch (ClassNotFoundException e02)  {
                  ClassLoader defCL=BSFManager.getDefinedClassLoader();
                  if (cld != defCL) {
                      cl=defCL.loadClass(className);
                  }
                  else
                  {
                      throw e01;        // re-throw original class not found exception
                  }
              }
          }
*/

	  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;
  }

  /**
   * locate the item in the fds array whose name is as given. returns
   * null if not found.
   */
  private static
  String getEventSetNameInfos (FeatureDescriptor[] fds)
  {
      if (fds.length==0)
      {
          return "Impossible to create an event adapter, because no methods following the event listener pattern could be found!";
      }

      String tmpStr="";
      for (int i = 0; i < fds.length; i++)
      {
	  // if (name.equals (fds[i].getName()))  // event set name!{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});
	}
  }
}


