/*
 * @(#)PropertyDescriptor.java	1.56 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package java.beans;

import java.lang.reflect.*;

/**
 * A PropertyDescriptor describes one property that a Java Bean
 * exports via a pair of accessor methods.
 */

public class PropertyDescriptor extends FeatureDescriptor {

    private Class propertyType;
    private Method readMethod;
    private Method writeMethod;
    private boolean bound;
    private boolean constrained;
    private Class propertyEditorClass;

    /**
     * Constructs a PropertyDescriptor for a property that follows
     * the standard Java convention by having getFoo and setFoo
     * accessor methods.  Thus if the argument name is "fred", it will
     * assume that the writer method is "setFred" and the reader method
     * is "getFred" (or "isFred" for a boolean property).  Note that the
     * property name should start with a lower case character, which will
     * be capitalized in the method names.
     *
     * @param propertyName The programmatic name of the property.
     * @param beanClass The Class object for the target bean.  For
     *		example sun.beans.OurButton.class.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Class beanClass)
		throws IntrospectionException {
	if (propertyName == null || propertyName.length() == 0) {
	    throw new IntrospectionException("bad property name");
	}
	setName(propertyName);
	String base = capitalize(propertyName);

	// Since there can be multiple setter methods but only one getter
        // method, find the getter method first so that you know what the
        // property type is.  For booleans, there can be "is" and "get"
        // methods.  If an "is" method exists, this is the official
	// reader method so look for this one first.
        try {
	    readMethod = Introspector.findMethod(beanClass, "is" + base, 0);
	} catch (Exception getterExc) {
            // no "is" method, so look for a "get" method.
	    readMethod = Introspector.findMethod(beanClass, "get" + base, 0);
	}
        Class params[] = { readMethod.getReturnType() };
        writeMethod = Introspector.findMethod(beanClass, "set" + base, 1,
                                              params);
	propertyType = findPropertyType(readMethod, writeMethod);
    }

    /**
     * This constructor takes the name of a simple property, and method
     * names for reading and writing the property.
     *
     * @param propertyName The programmatic name of the property.
     * @param beanClass The Class object for the target bean.  For
     *		example sun.beans.OurButton.class.
     * @param getterName The name of the method used for reading the property
     *		 value.  May be null if the property is write-only.
     * @param setterName The name of the method used for writing the property
     *		 value.  May be null if the property is read-only.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Class beanClass,
		String getterName, String setterName)
		throws IntrospectionException {
	if (propertyName == null || propertyName.length() == 0) {
	    throw new IntrospectionException("bad property name");
	}
	setName(propertyName);
	readMethod = Introspector.findMethod(beanClass, getterName, 0);
	if (readMethod != null) {
	    Class params[] = { readMethod.getReturnType() };
	    writeMethod = Introspector.findMethod(beanClass, setterName, 1,
                                                  params);
	} else {
	    writeMethod = Introspector.findMethod(beanClass, setterName, 1);
	}
	propertyType = findPropertyType(readMethod, writeMethod);
    }

    /**
     * This constructor takes the name of a simple property, and Method
     * objects for reading and writing the property.
     *
     * @param propertyName The programmatic name of the property.
     * @param getter The method used for reading the property value.
     *		May be null if the property is write-only.
     * @param setter The method used for writing the property value.
     *		May be null if the property is read-only.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Method getter, Method setter)
		throws IntrospectionException {
	if (propertyName == null || propertyName.length() == 0) {
	    throw new IntrospectionException("bad property name");
	}
	setName(propertyName);
	readMethod = getter;
	writeMethod = setter;
	propertyType = findPropertyType(readMethod, writeMethod);
    }

    /**
     * Gets the Class object for the property.
     *
     * @return The Java type info for the property.  Note that
     * the "Class" object may describe a built-in Java type such as "int".
     * The result may be "null" if this is an indexed property that
     * does not support non-indexed access.
     * <p>
     * This is the type that will be returned by the ReadMethod.
     */
    public Class getPropertyType() {
	return propertyType;
    }

    /**
     * Gets the method that should be used to read the property value.
     *
     * @return The method that should be used to read the property value.
     * May return null if the property can't be read.
     */
    public Method getReadMethod() {
	return readMethod;
    }

    /**
     * Sets the method that should be used to read the property value.
     *
     * @param getter The new getter method.
     */
    public void setReadMethod(Method getter)
				throws IntrospectionException {
	readMethod = getter;
	propertyType = findPropertyType(readMethod, writeMethod);
    }

    /**
     * Gets the method that should be used to write the property value.
     *
     * @return The method that should be used to write the property value.
     * May return null if the property can't be written.
     */
    public Method getWriteMethod() {
	return writeMethod;
    }

    /**
     * Sets the method that should be used to write the property value.
     *
     * @param setter The new setter method.
     */
    public void setWriteMethod(Method setter)
				throws IntrospectionException {
	writeMethod = setter;
	propertyType = findPropertyType(readMethod, writeMethod);
    }

    /**
     * Updates to "bound" properties will cause a "PropertyChange" event to
     * get fired when the property is changed.
     *
     * @return True if this is a bound property.
     */
    public boolean isBound() {
	return bound;
    }

    /**
     * Updates to "bound" properties will cause a "PropertyChange" event to
     * get fired when the property is changed.
     *
     * @param bound True if this is a bound property.
     */
    public void setBound(boolean bound) {
	this.bound = bound;
    }

    /**
     * Attempted updates to "Constrained" properties will cause a "VetoableChange"
     * event to get fired when the property is changed.
     *
     * @return True if this is a constrained property.
     */
    public boolean isConstrained() {
	return constrained;
    }

    /**
     * Attempted updates to "Constrained" properties will cause a "VetoableChange"
     * event to get fired when the property is changed.
     *
     * @param constrained True if this is a constrained property.
     */
    public void setConstrained(boolean constrained) {
	this.constrained = constrained;
    }


    /**
     * Normally PropertyEditors will be found using the PropertyEditorManager.
     * However if for some reason you want to associate a particular
     * PropertyEditor with a given property, then you can do it with
     * this method.
     *
     * @param propertyEditorClass  The Class for the desired PropertyEditor.
     */
    public void setPropertyEditorClass(Class propertyEditorClass) {
	this.propertyEditorClass = propertyEditorClass;
    }

    /**
     * Gets any explicit PropertyEditor Class that has been registered
     * for this property.
     *
     * @return Any explicit PropertyEditor Class that has been registered
     *		for this property.  Normally this will return "null",
     *		indicating that no special editor has been registered,
     *		so the PropertyEditorManager should be used to locate
     *		a suitable PropertyEditor.
     */
    public Class getPropertyEditorClass() {
	return propertyEditorClass;
    }

    /**
     * Compares this <code>PropertyDescriptor</code> against the specified object.
     * Returns true if the objects are the same. Two <code>PropertyDescriptor</code>s
     * are the same if the read, write, property types, property editor and
     * flags  are equivalent.
     *
     * @since 1.4
     */
    public boolean equals(Object obj) {
	if (obj != null && obj instanceof PropertyDescriptor) {
	    PropertyDescriptor other = (PropertyDescriptor)obj;
	    Method otherReadMethod = other.getReadMethod();
	    Method otherWriteMethod = other.getWriteMethod();

	    if (!compareMethods(readMethod, otherReadMethod)) {
		return false;
	    }

	    if (!compareMethods(writeMethod, otherWriteMethod)) {
		return false;
	    }

	    if (propertyType == other.getPropertyType() && 
		propertyEditorClass == other.getPropertyEditorClass() &&
		bound == other.isBound() && constrained == other.isConstrained()) {
		return true;
	    }
	}
	return false;
    }

    /**
     * Package private helper method for Descriptor .equals methods.
     *
     * @param a first method to compare
     * @param b second method to compare
     * @return boolean to indicate that the methods are equivalent
     */
    boolean compareMethods(Method a, Method b) {
	// Note: perhaps this should be a protected method in FeatureDescriptor
	if ((a == null) != (b == null)) {
	    return false;
	}

	if (a != null && b != null) {
	    if (!a.equals(b)) {
		return false;
	    }
	}
	return true;
    }


    /**
     * Package-private constructor.
     * Merge two property descriptors.  Where they conflict, give the
     * second argument (y) priority over the first argument (x).
     *
     * @param x  The first (lower priority) PropertyDescriptor
     * @param y  The second (higher priority) PropertyDescriptor
     */
    PropertyDescriptor(PropertyDescriptor x, PropertyDescriptor y) {
	super(x,y);

	// Figure out the merged read method.
	Method xr = x.readMethod;
	Method yr = y.readMethod;
	readMethod = xr;
	// Normally give priority to y's readMethod.
	if (yr != null) {
	    readMethod = yr;
	}
	// However, if both x and y reference read methods in the same class,
	// give priority to a boolean "is" method over a boolean "get" method.
	if (xr != null && yr != null &&
		   xr.getDeclaringClass() == yr.getDeclaringClass() &&
		   xr.getReturnType() == boolean.class &&
		   yr.getReturnType() == boolean.class &&
		   xr.getName().indexOf("is") == 0 &&
		   yr.getName().indexOf("get") == 0) {
	    readMethod = xr;
	}

	writeMethod = x.writeMethod;
	if (y.writeMethod != null) {
	    writeMethod = y.writeMethod;
	}

	propertyEditorClass = x.propertyEditorClass;
	if (y.propertyEditorClass != null) {
	    propertyEditorClass = y.propertyEditorClass;
	}

	bound = x.bound | y.bound;
	constrained = x.constrained | y.constrained;
	try {
	    propertyType = findPropertyType(readMethod, writeMethod);
	} catch (IntrospectionException ex) {
	    // Given we're merging two valid PDs, this "should never happen".
	    throw new Error("PropertyDescriptor: internal error while merging PDs: " + ex.getMessage());
	}
    }

    /*
     * Package-private dup constructor.
     * This must isolate the new object from any changes to the old object.
     */
    PropertyDescriptor(PropertyDescriptor old) {
	super(old);
	readMethod = old.readMethod;;
	writeMethod = old.writeMethod;
	propertyEditorClass = old.propertyEditorClass;
	bound = old.bound;
	constrained = old.constrained;
	propertyType = old.propertyType;
    }

    /**
     * Returns the property type that corresponds to the read and write method.
     */
    private Class findPropertyType(Method readMethod, Method writeMethod)
	throws IntrospectionException {
	Class propertyType = null;
	try {
	    if (readMethod != null) {
		if (readMethod.getParameterTypes().length != 0) {
		    throw new IntrospectionException("bad read method arg count");
		}
		propertyType = readMethod.getReturnType();
		if (propertyType == Void.TYPE) {
		    throw new IntrospectionException("read method " +
					readMethod.getName() + " returns void");
		}
	    }
	    if (writeMethod != null) {
		Class params[] = writeMethod.getParameterTypes();
		if (params.length != 1) {
		    throw new IntrospectionException("bad write method arg count");
		}
		if (propertyType != null && propertyType != params[0]) {
		    throw new IntrospectionException("type mismatch between read and write methods");
		}
		propertyType = params[0];
	    }
	} catch (IntrospectionException ex) {
	    throw ex;
	}
	return propertyType;
    }

    static String capitalize(String s) {
	if (s.length() == 0) {
 	    return s;
	}
	char chars[] = s.toCharArray();
	chars[0] = Character.toUpperCase(chars[0]);
	return new String(chars);
    }

}
