
//THIS CODE IS DERIVED FROM THE TAPESTRY WEB APPLICATION FRAMEWORK
//BY HOWARD LEWIS SHIP. EXCELLENT CODE.

//ALL EXTENSIONS AND MODIFICATIONS BY MARCUS MUELLER <znek@mulle-kybernetik.com>,
//EVERYTHING AVAILABLE UNDER THE TERMS AND CONDITIONS OF
//THE GNU LESSER GENERAL PUBLIC LICENSE (LGPL). SEE BELOW FOR MORE DETAILS.

//Tapestry Web Application Framework
//Copyright (c) 2000-2002 by Howard Lewis Ship

//Howard Lewis Ship
//http://sf.net/projects/tapestry
//mailto:hship@users.sf.net

//This library is free software.

//You may redistribute it and/or modify it under the terms of the GNU
//Lesser General Public License as published by the Free Software Foundation.

//Version 2.1 of the license should be included with this distribution in
//the file LICENSE, as well as License.html. If the license is not
//included with this distribution, you may find a copy at the FSF web
//site at 'www.gnu.org' or 'www.fsf.org', or you may write to the
//Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA.

//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied waranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//Lesser General Public License for more details.

package org.opengroupware.jope.foundation.kvc;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.foundation.NSKeyValueCodingAdditions;

/**
 *  Streamlines access to all the properties of a given
 *  JavaBean.  Static methods acts as a factory for PropertyHelper instances, which
 *  are specific to a particular Bean class.
 *
 *  <p>A <code>PropertyHelper</code> for a bean class simplifies getting and setting properties
 *  on the bean, handling (and caching) the lookup of methods as well as the dynamic
 *  invocation of those methods.  It uses an instance of {@link IPropertyAccessor}
 *  for each property.
 *
 *  <p>PropertyHelper allows properties to be specified in terms of a path.  A path is a
 *  series of property names seperate by periods.  So a property path of
 *  'visit.user.address.street' is effectively the same as
 *  the code <code>getVisit().getUser().getAddress().getStreet()</code>
 *  (and just as likely to throw a <code>NullPointerException</code>).
 *
 *  <p>Typical usage:
 *
 *  <pre>
 *  ProperyHelper helper = PropertyHelper.forInstance(instance);
 *  helper.set(instance, "propertyName", newValue);
 *  </pre>
 *
 *  <p>Only single-valued properties (not indexed properties) are supported, and a minimum
 *  of type checking is performed.
 *
 *  <p>A mechanism exists to register custom <code>PropertyHelper</code> subclasses
 *  for specific classes.  The two default registrations are
 *  {@link PublicBeanPropertyHelper} for the {@link IPublicBean} interface, and
 *  {@link MapHelper} for the {@link Map} interface.
 *
 *  @version $Id: KVCWrapper.java,v 1.4 2002/11/20 14:47:27 znek Exp $
 *  @author Howard Lewis Ship
 *
 **/

public class KVCWrapper extends Object {

  /**
   *  Registry of helper classes.  Key is the Class of the bean.  Value
   *  is the Class of the Helper.
   **/

  private static Map<Class,Class> registry = new HashMap<Class,Class>();

  /**
   *  Cache of helpers, keyed on the Class of the bean.
   **/

  private static Map<Class,KVCWrapper> helpers = 
    new HashMap<Class,KVCWrapper>();

  static {
    register(Map.class, MapKVCWrapper.class);
  }

  private static final Log logger = LogFactory.getLog(KVCWrapper.class);

  /**
   *  Map of PropertyAccessors for the helper's
   *  bean class. The keys are the names of the properties.
   **/

  protected Map<String,IPropertyAccessor> accessors;

  /**
   *  The Java Beans class for which this helper is configured.
   **/

  protected Class                     clazz;

  /**
   *  The separator character used to divide up different
   *  properties in a nested property name.
   **/

  /**
   * A {@link StringSplitter} used for parsing apart property paths.
   **/

 
  protected KVCWrapper(Class _class) {
    this.clazz = _class;
  }

  public String[] getPropertyGetterSearchList(String _name) {
    char[] chars = _name.toCharArray();
    chars[0] = Character.toUpperCase(chars[0]);
    String cuddlyCapsedName = new String(chars);
    return new String[] { _name, "get" + cuddlyCapsedName, "_" + _name };
  }

  private static Map<Class,Method[]> declaredMethodCache =
    new ConcurrentHashMap<Class,Method[]>();

  @SuppressWarnings("unchecked")
  private static synchronized Method[] getPublicDeclaredMethods(Class _class) {
    final Class fclz = _class;
    Method methods[] = declaredMethodCache.get(fclz);
    if (methods != null) return methods;
    methods = (Method[]) AccessController.doPrivileged(new PrivilegedAction() {
      public Object run() {
        return fclz.getMethods();
      }

    });
    for (int i = 0; i < methods.length; i++) {
      Method method = methods[i];
      int    j      = method.getModifiers();
      if (!Modifier.isPublic(j))
        methods[i] = null;
    }

    declaredMethodCache.put(_class, methods);

    return methods;
  }

  public PropertyDescriptor[] getPropertyDescriptors(Class _class)
      throws Exception
  {
    /**
     * Our idea of KVC differs from what the Bean API proposes. Instead of
     * having get<name> and set<name> methods, we expect <name> and 
     * set<name> methods.
     */

    Map<String,Method> settersMap = new HashMap<String, Method>();
    Map<String,Method> gettersMap = new HashMap<String, Method>();

    Method methods[] = getPublicDeclaredMethods(_class);

    for (int i = 0; i < methods.length; i++) {
      Method method = methods[i];
      if (method == null) continue;

      String name = method.getName();
      if (name.startsWith("set")) {
        if (method.getReturnType()            != Void.TYPE) continue;
        if (method.getParameterTypes().length != 1)         continue;
        if (name.length()                     == 3)         continue;

        char[] chars = name.substring(3).toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        String decapsedName = new String(chars);

        if (logger.isDebugEnabled())
          logger.debug("Recording setter method [" + method + "] for name \""
              + decapsedName + "\"");
        settersMap.put(decapsedName, method);
      }
      else {
        if (method.getReturnType() == Void.TYPE) continue;
        if (method.getParameterTypes().length > 0) continue;
        if (logger.isDebugEnabled())
          logger.debug("Recording getter method [" + method + "] for name \""
              + name + "\"");
        gettersMap.put(name, method);
      }

    }

    Set<PropertyDescriptor> pds = new HashSet<PropertyDescriptor>();

    /* merge all names from getters and setters */
    Set<String> names = new HashSet<String>(gettersMap.keySet());
    names.addAll(settersMap.keySet());

    for (String name : names) {
      Method getter = gettersMap.get(name);
      Method setter = settersMap.get(name);
      if (getter == null && setter == null) continue;

      PropertyDescriptor descriptor = 
        new PropertyDescriptor(name, getter, setter);
      pds.add(descriptor);
    }
    return pds.toArray(new PropertyDescriptor[0]);
  }

  /**
   *  Uses JavaBeans introspection to find all the properties of the
   *  bean class.  This method sets the {@link #accessors} variable (it will
   *  have been null), and adds all the well-defined JavaBeans properties.
   *
   *  <p>Subclasses may invoke this method before adding thier own accessors.
   *
   *  <p>This method is invoked from within a synchronized block.  Subclasses
   *  do not have to worry about synchronization.
   **/

  public void buildPropertyAccessors() {
    /*
     * Acquire all usable field accessors first.
     */
    
    /**
     * Construct field accessors for names which aren't occupied
     * by properties, yet. Imagine this as a "last resort".
     */

    Map<String,FieldAccessor> propertyFieldAccessorMap = 
      new HashMap<String,FieldAccessor>();
    Field fields[] = this.clazz.getFields();

    for (Field field : fields) {
      int mods = field.getModifiers();

      // Skip static variables and non-public instance variables.
      if ((Modifier.isPublic(mods) == false) || (Modifier.isStatic(mods)))
        continue;

      propertyFieldAccessorMap.put(field.getName(), new FieldAccessor(field));
    }

    /**
     * Retrieve all property descriptors now
     */
    PropertyDescriptor[] props;

    if (this.accessors != null) return;

    try {
      props = this.getPropertyDescriptors(this.clazz);
    }
    catch (Exception e) {
      logger.error("Error during getPropertyDescriptors(): " + e);
      throw new DynamicInvocationException(e);
    }

    this.accessors = new HashMap<String,IPropertyAccessor>();

    if (logger.isDebugEnabled())
      logger.debug("Recording properties for \"" + this.clazz.getName()
          + "\"");

    for (PropertyDescriptor pd : props) {
      String name = pd.getName();

      if (logger.isDebugEnabled())
        logger.debug("Recording property \"" + name + "\"");
      
      Method getter      = pd.getReadMethod();
      Method setter      = pd.getWriteMethod();
      FieldAccessor fa   = propertyFieldAccessorMap.get(name);
      Class         type = pd.getPropertyType();
 
      PropertyAccessor pa =
        PropertyAccessor.getPropertyAccessor(name, type, getter, setter, fa);
      this.accessors.put(name, pa);
    }

    /**
     * Use field accessors for names which are not occupied, yet.
     * This is the default fallback.
     */
    for (String name : propertyFieldAccessorMap.keySet()) {
      if (!this.accessors.containsKey(name))
        this.accessors.put(name, propertyFieldAccessorMap.get(name));
    }
  }

  /**
   *  A convienience method; simply invokes
   *  {@link #forClass(Class)}.
   **/

  public static KVCWrapper forInstance(Object instance) {
    return forClass(instance.getClass());
  }

  /**
   *  Factory method which returns a <code>KVCWrapper</code> for the given
   *  JavaBean class.
   *
   *  <p>Finding the right helper class involves a sequential lookup, first for an
   *  exact match, then for an exact match on the superclass, the a search
   *  by interface.  If no specific
   *  match is found, then <code>KVCWrapper</code> itself is used, which is
   *  the most typical case.
   *
   *  @see #register(Class, Class)
   **/

  public synchronized static KVCWrapper forClass(Class beanClass) {
    Class       helperClass = null;
    Constructor constructor;
    Class[]     linheritance;
    Class       candidate;

    if (logger.isDebugEnabled())
      logger.debug("Getting property helper for class " + beanClass.getName());

    // TODO: helpers can't be null?
    if (helpers == null) helpers = new HashMap<Class,KVCWrapper>();

    KVCWrapper helper = helpers.get(beanClass);
    if (helper != null) return helper;

    if (registry != null) {

      // Do a quick search for an exact match.

      helperClass = registry.get(beanClass);

      if (helperClass == null) {
        // Do a more exhaustive search based
        // on the inheritance (classes and interfaces)
        // of the bean class.

        linheritance = getInheritance(beanClass);
        for (int i = 0; i < linheritance.length; i++) {
          candidate = linheritance[i];
          helperClass = registry.get(candidate);

          if (helperClass != null) break;
        }
      }

    }

    // If no specific class registered, then use the standard implementation.

    if (helperClass == null) {
      helper = new KVCWrapper(beanClass);
      helpers.put(beanClass, helper);

      return helper;
    }

    // Here it gets tricky, we need to invoke a constructor.  We want the constructor
    // which takes a Class as a parameter.

    try {
      if (logger.isDebugEnabled())
        logger.debug("Creating new KVCWrapper: " + helperClass.getName() + " for "
            + beanClass.getName());

      constructor = helperClass.getConstructor(new Class[] { Class.class });
    }
    catch (NoSuchMethodException ex) {
      throw new DynamicInvocationException(KVC.getString(
          "KVCWrapper.missing-constructor", helperClass.getName()), ex);
    }

    // This is equivalent to invoking the constructor FooClass.FooClass(beanClass).

    try {
      helper = (KVCWrapper) constructor.newInstance(new Object[] { beanClass });
    }
    catch (Exception ex) {
      throw new DynamicInvocationException(KVC
          .getString("KVCWrapper.unable-to-invoke-constructor"), ex);
    }

    // We don't want to go through this again, so record permanently the correct
    // helper for this class.

    helpers.put(beanClass, helper);

    return helper;
  }

  // These are only accessed from getInheritance().
  // getInheritance() is only invoked from forClass(), and
  // forClass() is synchronized.

  private static List<Class>       inheritance     = null;
  private static LinkedList<Class> queue           = null;
  private static Set<Class>        addedInterfaces = null;

  /**
   *  Builds a List of all the classes and interfaces the beanClass
   *  inherits from.  The first elements in the list is the super-class of
   *  the beanClass,
   *  followed by its super-classes (not including java.lang.Object).
   *  Following this are the interfaces implemented by the beanClass
   *  and each of its super-classes, following by interfaces further
   *  up the interface inheritance chain.
   **/

  private static Class[] getInheritance(Class beanClass) {
    Class[] result;
    Class[] interfaces;
    Class   candidate;
    boolean first = true;

    if (inheritance == null) inheritance = new ArrayList<Class>();

    while (beanClass != null) {
      // Don't include java.lang.Object

      if (beanClass.equals(Object.class)) break;

      // Add any interfaces (possibly zero) implemented by
      // the class to the interface queue.

      interfaces = beanClass.getInterfaces();

      for (int i = 0; i < interfaces.length; i++) {
        if (queue == null) queue = new LinkedList<Class>();

        queue.add(interfaces[i]);
      }

      // Don't write the bean class itself.  Add any superclasses

      if (first)
        first = false;
      else
        inheritance.add(beanClass);

      beanClass = beanClass.getSuperclass();

    }

    // Add all the interfaces (and super-interfaces) to the list.
    // This is kind of breadth-first searching.  We need to do some
    // filtering because multiple super-classes may implement the same
    // methods, or multiple interfaces may extend the same interfaces.

    while (queue != null && !queue.isEmpty()) {
      candidate = queue.removeFirst();

      if (addedInterfaces == null)
        addedInterfaces = new HashSet<Class>();
      else if (addedInterfaces.contains(candidate)) continue;

      inheritance.add(candidate);
      addedInterfaces.add(candidate);

      interfaces = candidate.getInterfaces();

      for (int i = 0; i < interfaces.length; i++)
        queue.add(interfaces[i]);
    }

    // Convert the result to an array, so that we
    // can clear out our three collections (inheritance, addedInterfaces
    // and queue).

    result = new Class[inheritance.size()];

    result = inheritance.toArray(result);

    inheritance.clear();
    if (addedInterfaces != null) addedInterfaces.clear();

    if (queue != null) queue.clear();

    // Return the final result as an array.

    return result;
  }

  /*
   * Reflects our understanding of how to coerce generic object values
   * into booleans.
   */
  public static boolean booleanValueFromObject(Object _value) {
    if (_value instanceof Boolean)
      return ((Boolean)_value).booleanValue();
    else if (_value instanceof Number)
      return ((Number)_value).intValue() != 0;
    else if (_value instanceof String) {
      if ("true".equals(_value) ||
          "YES".equals(_value)  ||
          "1".equals(_value))
        return true;
      else if ("false".equals(_value) ||
               "NO".equals(_value)    ||
               "0".equals(_value))
        return false;
      else {
        int i;

        try {
           i = Integer.parseInt(_value.toString());
        }
        catch (NumberFormatException e) {
          i = 1; /* isn't a number, but non-empty nevertheless */ 
        }
        return i != 0;
      }
    }
    return _value != null;
  }

  /**
   *  Returns the value of the named property for the given object.
   *
   *  <p>propertyName must be a simple property name, not a path,
   *  use {@link #getPath(Object,String)} to use a property path.
   **/

  public Object valueForKey(Object object, String propertyName) {
    // Get the helper for the current object.
    // Get the accessor for the property to access
    // within the current object.  Get the new
    // current object from it.

    IPropertyAccessor accessor = getAccessor(object, propertyName);
    if (accessor == null)
      throw new MissingPropertyException(object, propertyName);

    return accessor.get(object);
  }


  /**
   *  Finds an accessor for the given property name.  Returns the
   *  accessor if the class has the named property, or null
   *  otherwise.
   *
   *  @param propertyName the <em>simple</em> property name of the property to
   *  get.
   *
   **/

  public IPropertyAccessor getAccessor(Object instance, String propertyName) {
    synchronized (this) {
      if (this.accessors == null) buildPropertyAccessors();
    }

    String[] searchList = this.getPropertyGetterSearchList(propertyName);
    IPropertyAccessor accessor = null;

    for (int i = 0; i < searchList.length; i++) {
      synchronized (this.accessors) {
        accessor = this.accessors.get(searchList[i]);
      }
      if (accessor != null) break;
    }
    return accessor;
  }

  /**
   *  Finds an accessor using a split property path.
   **/

  public IPropertyAccessor getAccessorPath(Object instance,
      String[] propertyPath) {
    Object current = instance;
    KVCWrapper helper = this;
    int i = 0;

    while (true) {
      String propertyName = propertyPath[i];

      IPropertyAccessor accessor = helper.getAccessor(current, propertyName);

      if (accessor == null)
        throw new MissingPropertyException(instance, buildPath(propertyPath),
            current, propertyName);

      // When the last element in property path is reached, we don't advance again, we
      // just return the accessor we'd use to advance.

      if (i + 1 == propertyPath.length) return accessor;

      try {
        current = accessor.get(current);
      }
      catch (MissingAccessorException e) {
        throw new MissingAccessorException(instance, buildPath(propertyPath),
            current, propertyName);
      }

      helper = forClass(current.getClass());

      i++;
    }
  }

  
  /**
   *  Registers a particular <code>KVCWrapper</code> subclass as the correct
   *  class to use with a specific class of bean (or any class derived from
   *  the bean class).  An interface may be specified as well, in which case
   *  beans that match the interface (i.e., implement the interface directly
   *  or indirectly) will use the registered helper class.
   *
   **/

  public static synchronized void register(Class _beanClass, Class helperClass) {

    registry.put(_beanClass, helperClass);

    // Note: it would be nice to log the registration, but because
    // registration often occurs from static initializers, before
    // log4j is configured, that causes bad, bad problems.  Using
    // the debugger seems to excacerbate this.
  }

  /**
   *  Sets the value of a property of the named object.
   *  propertyName must be a simple propertyName, not a property path
   *  (use {@link #setPath(Object,String,Object)} instead.
   *
   *  @param object the object to change
   *  @param propertyName the name of the property to change
   *  @param value the value to assign to the property
   **/

  public void takeValueForKey
    (Object object, Object value, String propertyName)
  {
    IPropertyAccessor accessor = getAccessor(object, propertyName);
    if (accessor == null)
      throw new MissingPropertyException(object, propertyName);

    Class type = accessor.getWriteType();

    if (type == Boolean.TYPE || type == Boolean.class)
      value = KVCWrapper.booleanValueFromObject(value)
                         ? Boolean.TRUE : Boolean.FALSE;
    accessor.set(object, value);
  }

  public String toString() {
    StringBuffer buffer;

    buffer = new StringBuffer("<KVCWrapper @" + this.hashCode() + ": ");
    buffer.append(this.clazz.getName());
    buffer.append('>');

    return buffer.toString();
  }

  /**
   *  Used with some error messages to reconstruct a property path
   *  from its split state.
   **/

  private String buildPath(String[] path) {
    StringBuffer buffer;

    if (path.length == 1) return path[0];

    buffer = new StringBuffer();
    for (int i = 0; i < path.length; i++) {
      if (i > 0) buffer.append(NSKeyValueCodingAdditions.KeyPathSeparator);

      buffer.append(path[i]);
    }

    return buffer.toString();
  }

   /**
   *  Returns a collection of {@link IPropertyAccessor} instances.
   *
   *  <p>These are generated from the "natural" JavaBeans properties.
   *  Many subclasses create "synthesized" properties, which should
   *  be added to the result and returned.
   *
   *  <p>The collection returned may be modified freely.  Subclasses
   *  should invoke this implementation, then add additional
   *  values as necessary.
   *
   *  <p>TODO:  Deal with conflicts between natural and synthesized
   *  properties.
   **/

  public Collection getAccessors(Object instance) {
    Collection<IPropertyAccessor> result;

    if (this.accessors == null) {
      synchronized (this) {
        buildPropertyAccessors();
      }
    }

    synchronized (this.accessors) {
      result = new ArrayList<IPropertyAccessor>(this.accessors.values());
    }

    Collection names = this.getSyntheticPropertyNames(instance);

    if (names != null) {
      Iterator i = names.iterator();
      while (i.hasNext()) {
        String name = (String) i.next();
        result.add(this.getAccessor(instance, name));
      }
    }

    return result;
  }

  /**
   *  Returns a Collection of the names of any synthetic properties.
   *
   *  <p>Subclasses should override this to interrogate the instance
   *  in an appropriate way, so as to extract this list of properties.
   *  For example, {@link MapHelper} casts the instance to {@link Map},
   *  and returns the map's keySet.
   *
   *  <p>Subclasses may override this implementation without invoking it.  This
   *  implementation returns null.
   **/

  protected Collection getSyntheticPropertyNames(Object instance) {
    return null;
  }
}
