/*
 * Copyright 2006 - Gary Bentley
 *
 * 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 com.gentlyweb.utils;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * This class is used to perform access into a Java object using a 
 * String value with a specific notation.
 * <p>
 * The Accessor uses a dot notation such as <b>field1.method1.method2</b> to
 * perform the access on an object.  Each value in the notation refers to
 * a field or method (a no argument method) of the type of the previous
 * value.
 * For instance if you have the following class structure:
 * </p>
 * <pre>
 * public class A 
 * {
 *    public B = new B ();
 * }
 * 
 * public class B
 * {
 *    public C = new C ();
 * }
 * 
 * public class C
 * {
 *    String d = "";
 * }
 * </pre>
 * <p>
 * You would then use the notation: <b>B.C.d</b> to get access to 
 * field <b>d</b> in Class C.
 * <br /><br />
 * The Accessor also supports a <b>[ ]</b> notation for accessing
 * into Lists/Maps and Arrays.  If the value between the <b>[ ]</b>
 * is an integer then we look for the associated type to be either
 * an array or a List, we then index into it with the integer.  If
 * the value is <b>NOT</b> an integer then we use assume the
 * type is a Map and use it as a key into the Map.
 * <br /><br />
 * For instance changing the example above:
 * </p>
 * <pre>
 * public class A 
 * {
 *    public List vals = new ArrayList ();
 * }
 * </pre>
 * <p>
 * Now we could use: <b>vals[X]</b> where <b>X</b> is a positive integer.
 * Or changing again:
 * </p>
 * <pre>
 * public class A 
 * {
 *    public Map vals = new HashMap ();
 * }
 * </pre>
 * <p>
 * We could use: <b>vals[VALUE]</b> where <b>VALUE</b> would then be
 * used as a Key into the vals HashMap.
 * <br /><br />
 * Note: The Accessor is <b>NOT</b> designed to be an all purpose
 * method of gaining access to a class.  It has specific uses and for
 * most will be of no use at all.  It should be used for general purpose
 * applications where you want to access specific fields of an object
 * without having to know the exact type.  One such application is in
 * the {@link GeneralComparator}, in that case arbitrary Objects can 
 * be sorted without having to write complex Comparators or implementing
 * the Comparable interface AND it gives the flexibility that sorting
 * can be changed ad-hoc.
 * <br /><br />
 * The Accessor looks for in the following order:
 * <ul>
 *   <li>Public fields with the specified name.</li>
 *   <li>If no field is found then the name is converted to a "JavaBeans" 
 *       <b>get</b> method, so a field name of <b>value</b> would be converted
 *       to <b>getValue</b> and that method is looked for.  The method must take
 *       no arguments.</li>
 *   <li>If we don't find the <b>get*</b> method then we look for a method with 
 *       the specified name.  So a field name of <b>value</b> would mean that
 *       a method (that again takes no arguments) is looked for.</li>
 * </ul>
 * <p>
 * Note: we have had to add the 3rd type to allow for methods that don't follow
 * JavaBeans conventions (there are loads in the standard Java APIs which makes 
 * accessing impossible otherwise).
 */
public class Getter
{

    private List chain = new ArrayList ();

    private Class clazz = null;
    private int cs = 0;
    private String acc = null;

    /**
     * Get the getter associated with the named reference.  Return
     * null if there isn't one, or if we can't access it.
     *
     * @param ref The reference for the getter.
     * @param clazz The Class to get the field from.
     */
    public Getter (String ref,
		   Class  clazz)
	                  throws IllegalArgumentException
    {

	if (clazz == null)
	{

	    throw new IllegalArgumentException ("Class must be specified");

	}

	this.acc = ref;
	this.clazz = clazz;

	StringTokenizer t = new StringTokenizer (ref,
						 ".");

	Class c = clazz;

	while (t.hasMoreTokens ())
	{

	    String tok = t.nextToken ();

	    String index = "";

	    // Get the Fields.
	    Field[] fields = c.getFields ();
	    
	    Field f = null;

	    // See if the token matches...
	    for (int i = 0; i < fields.length; i++)
	    {
		
		if (fields[i].getName ().equals (tok))
		{
		    
		    // Found it...
		    f = fields[i];
		
		    break;

		}

	    }

	    if (f != null)
	    {

		c = f.getType ();

		this.chain.add (f);

	    } else {

		Method m = this.getNoParmJavaGetMethod (tok,
							c);

		if (m == null)
		{
		    
		    throw new IllegalArgumentException ("Cannot find method with name: " +
							tok +
							" in class: " +
							c.getName ());
		    
		}

		// Need to set the method as being accessible here to workaround
		// an annoying Java reflection bug that seems to have been around 
		// since the year dot.  See bug: 4071957.
		m.setAccessible (true);

		c = m.getReturnType ();
		    
		if (Void.class.isAssignableFrom (c))
		{
			
		    throw new IllegalArgumentException ("Method: " + 
							m.getName () +
							" cannot be called on class: " +
							c.getName () + 
							" since return type is void");
			
		}
		    
		this.chain.add (m);
		    
	    }

	}

	this.cs = this.chain.size ();

    }

    public Class getBaseClass ()
    {

	return this.clazz;

    }

    /**
     * Get the class of the type of object we would return from the {@link #getValue(Object)}
     * method.
     *
     * @return The class.
     */
    public Class getType ()
    {

	Object o = this.chain.get (this.chain.size () - 1);

	// See what type the accessor is...
	if (o instanceof Method)
	{
		
	    Method m = (Method) o;
		
	    return m.getReturnType ();

	}
	    
	if (o instanceof Field)
	{
		
	    // It's a field...so...
	    Field f = (Field) o;

	    return f.getType ();

	}

	return null;

    }

    public Object getValue (Object obj)
	                    throws IllegalAccessException,
                                   InvocationTargetException
    {
	
	// If the object is null then return null.
	if (obj == null)
	{

	    return null;

	}

	// For our accessor chain, use the Field and Methods
	// to get the actual value.
	Object retdata = obj;
	
	for (int i = 0; i < this.cs; i++)
	{

	    Object o = this.chain.get (i);

	    // See what type the accessor is...
	    if (o instanceof Method)
	    {
		
		Method m = (Method) o;
		
		Object[] parms = {};
		
		// Invoke the method...
		try
		{

		    retdata = m.invoke (retdata,
					parms);

		} catch (Exception e) {

		    this.throwException (obj,
					 e);

		}
		
		if (retdata == null)
		{
		    
		    return null;
		    
		}
				
	    }
	    
	    if (o instanceof Field)
	    {
		
		// It's a field...so...
		Field f = (Field) o;
		
		// Now get the value...
		try
		{

		    retdata = f.get (retdata);

		} catch (Exception e) {

		    this.throwException (obj,
					 e);

		}
		
	    }

	}
	
	return retdata;
	
    }

    private void throwException (Object    o,
				 Exception e)
    {

	throw new RuntimeException ("Unable to get value from instance of: " +
				    o.getClass ().getName () + 
				    ", using accessor: " +
				    this.acc +
				    " expected type to be: " + 
				    this.clazz.getName (),
				    e);


    }

    public static Method getNoParmJavaGetMethod (String method,
						 Class  c)
    {

	StringBuffer b = new StringBuffer (method);

	Method m = null;

	// First look for a "get" method.
	try
	{
	    
	    b.setCharAt (0,
			 Character.toUpperCase (method.charAt (0)));
	    
	    b.insert (0,
		      "get");
	    
	    String getMN = b.toString ();
	    
	    m = c.getMethod (getMN,
			     null);
	    
	    if (m != null)
	    {

		return m;
		
	    }

	} catch (Exception e) {

	    // Painful to have to do it this way...

	}
	    
	try
	{

	    b = new StringBuffer (method);
	    
	    b.setCharAt (0,
			 Character.toUpperCase (method.charAt (0)));
	    
	    b.insert (0,
		      "is");
	    
	    String isMN = b.toString ();
	    
	    m = c.getMethod (isMN,
			     null);
	    
	    if (m != null)
	    {

		return m;
		
	    }
	    
	} catch (Exception e) {

	    // Sigh...

	}

	try
	{

	    return c.getMethod (method,
				null);

	} catch (Exception e) {

	    // Ignore...

	}

	return null;

    }

    public String getAccessor ()
    {

	return this.acc;

    }

    public String toString ()
    {

	return "Accessor: " + this.acc + " from class: " + this.clazz.getName ();

    }

}
