/*
 * @(#)StringableFactory.java    2.4.0   2 May 2006
 *
 * Copyright 2006
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.util;

import edu.neu.ccs.*;
import edu.neu.ccs.codec.*;
import java.beans.*;
import java.io.Serializable;
import java.lang.reflect.*;
import java.text.ParseException;
import javax.swing.event.SwingPropertyChangeSupport;

/**
 * <P>Class whose instances can construct new instances 
 * of <CODE>{@link Stringable Stringable}</CODE> objects 
 * of a given class using reflection.
 *
 * Use of this type of factory is the preferred technique for
 * <CODE>TypedView</CODE>s and <CODE>GeneralView</CODE>s
 * to create and return data models built
 * from user input data.</P>
 *
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @version 2.4.0
 * @since   1.0
 */
public class StringableFactory 
    implements Cloneable, Serializable 
{
    /** Bound property name for the data type property. */
    public static final String DATA_TYPE = "data.type";

    /** The class of objects to produce with this factory. */
    protected Class type = XString.class;
    
    /** Helper object for property change API. */
    protected SwingPropertyChangeSupport changeAdapter =
        new SwingPropertyChangeSupport(this);

    /////////////////
    // Constructor //
    /////////////////
    
    /**
     * <P>Constructor for a factory capable of producing 
     * objects of the given <CODE>Stringable</CODE> class.</P>
     *
     * <P>If the class is <CODE>null</CODE>, this factory will
     * produce instances of the <CODE>XString</CODE> class.</P>
     *
     * <P>If the given class is not <CODE>Stringable</CODE>,
     * then an exception of type <CODE>Error</CODE> will be
     * thrown.</P>
     * 
     * @param dataType the <CODE>Stringable</CODE> class
     *                 of objects to be produced
     */    
    public StringableFactory(Class dataType) {
        setDataType(dataType);
    }
    
    ////////////////
    // Public API //
    ////////////////

    /**
     * <P>Returns a new instance of the stored class 
     * created using the zero-parameter constructor 
     * for the class.</P>
     * 
     * <P>This method will throw an exception of type
     * <CODE>Error</CODE> if a new instance cannot be
     * created.</P>
     *
     * @see #constructFrom(String)
     */
    public Stringable getDefaultInstance() {
        try {
            return (Stringable)type.newInstance();
        } catch (Exception ex) {
            throw new Error(
                "Stringable could not be instantiated: "
                + ex.getMessage());
        }
    }

    /**
     * Returns a new instance of the stored class 
     * created using the zero-parameter constructor
     * for the class, whose state is then set 
     * from the given <CODE>String</CODE> data.
     *
     * @param data the data <CODE>String</CODE> 
     *      containing state information
     * @see #setDataType(Class)
     */
    public Stringable constructFrom(String data) 
        throws ParseException 
    {
        return constructFrom(type, data);
    }
    
    /**
     * <P>Sets the class of objects produced by this factory
     * to the given <CODE>Stringable</CODE> class.</P>
     *
     * <P>If the given class is <CODE>null</CODE>, this
     * method will not change the stored class.  This behavior
     * represents a change from earlier versions of the method
     * in which a <CODE>null</CODE> parameter led to a change
     * of the stored class to <CODE>XString.class</CODE>.  The
     * new behavior works better in practical usage.</P>
     *
     * <P>If the given class is not <CODE>Stringable</CODE>,
     * then an exception of type <CODE>Error</CODE> will be
     * thrown.</P>
     * 
     * @param dataType the desired class of objects to produce
     * @see #getDataType()
     */
    public void setDataType(Class dataType) {
        // sanity check for null data
        if (dataType == null)
            return;

		// save the old type to fire property change
		Class oldType = getDataType();
    
        // throw error if class is not Stringable
        if (! isStringable(dataType)) {
            throw new Error(
                "Class is not Stringable: " + 
                dataType.getName());
        }

        // set the class
        type = dataType;
        
        // if the data type has changed
        if (getDataType() != oldType) {

            // notify listeners of property change
            changeAdapter.firePropertyChange(
                DATA_TYPE,
                oldType,
                getDataType());
        }
    }
    
    /**
     * Returns the class of objects produced by this factory.
     *
     * @see #setDataType(Class)
     */
    public Class getDataType() {
        return type;
    }
    
    /////////////////////////////
    // Property Change Methods //
    /////////////////////////////

    /**
     * Registers the given object 
     * to listen for property change events 
     * generated by this factory.
     *
     * @param listener the listener to be registered
     */
    public void addPropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(listener);
    }

    /**
     * Registers the given object 
     * to listen for property change events
     * generated by this factory
     * with the given property name.
     *
     * @param propertyName the name of the desired property 
     * @param listener the listener to be registered
     */
    public void addPropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(
            propertyName, 
            listener);
    }

    /**
     * Deregisters the given object 
     * from listening for property change events 
     * generated by this factory.
     *
     * @param listener the listener to be deregistered
     */
    public void removePropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(listener);
    }

    /**
     * Deregisters the given object
     * from listening for property change events
     * generated by this factory
     * with the given property name.
     *
     * @param propertyName the name of the desired property 
     * @param listener the listener to be deregistered
     */
    public void removePropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(
            propertyName, 
            listener);
    }
    
    ////////////////////
    // Static Methods //
    ////////////////////
    
    /**
     * Returns <CODE>true</CODE> if the given class
     * is <CODE>{@link Stringable Stringable}</CODE>,
     * and <CODE>false</CODE> if it is not.
     *
     * @param dataType the class in question
     */
    public static boolean isStringable(Class dataType) {
        return Stringable.class.isAssignableFrom(dataType);
    }
    
    /**
     * Returns a <CODE>String</CODE> encapsulation 
     * of the class and state information
     * for the given <CODE>Stringable</CODE> object.
     *
     * @param obj the object to be encapsulated
     */
    public static String encodeTypeAndData(Stringable obj) {
        return CodecUtilities.encode(
            new String[] {
                obj.getClass().getName(),
                obj.toStringData()
            }
        );
    }
    
    /**
     * Returns a new <CODE>Stringable</CODE> object
     * constructed from the given <CODE>String</CODE>
     * that encapsulates the type and state information
     * of a previously existing <CODE>Stringable</CODE> object.
     *
     * <p>Implementation change: As of 2.4.0 decodes the data
     * parameter using <code>Strings.decode</code>.</p>
     * 
     * @param data the <CODE>String</CODE> to be decoded
     * @throws NullPointerException if the given <CODE>String</CODE>
     *      is <CODE>null</CODE>
     * @throws ParseException if the given <CODE>String</CODE>
     *      does not contain the type and state information
     *      required to construct a new object
     */
    public static Stringable decodeTypeAndData(String data)
        throws ParseException
    {
        // test for null data
        if (data == null)
            throw new NullPointerException();
    
        // decode into a pair of strings
        String[] pair = Strings.decode(data);
        
        // test for properly encoded data
        if ((pair == null) || (pair.length != 2)) {
            throw new ParseException(
                "String does not contain both type and state: " + data, -1);
        }
        
        // find the object data type
        Class dataType = null;
        try {
            dataType = Class.forName(pair[0]);
        }
        catch (ClassNotFoundException ex) {
            throw new ParseException
                ("Error in data type: " + pair[0], -1);
        }
        
        // construct the object from its type and the string data
        return constructFrom(dataType, pair[1]);
    }
    
    /**
     * <P>Constructs a new <CODE>Stringable</CODE> object
     * by constructing a default instance of the given type
     * and setting the state of the object to the state
     * encapsulated by the given <CODE>String</CODE>.</P>
     *
     * <P>Throws an exception of type <CODE>Error</CODE> if
     * a fundamental error occurs while instantiating a new
     * object.</P>
     * 
     * <P>Throws a <CODE>ParseException</CODE> if an object
     * could be instantiated but the data string could not
     * be used to set the object state.</P>
     * 
     * @param dataType the type of object to construct
     * @param data the <CODE>String</CODE> to be used to
     *      set the state of the constructed object
     */
    public static Stringable constructFrom(
            Class dataType, 
            String data)
        throws ParseException
    {
    	// throw error if class is null
    	if (dataType == null) {
    		throw new Error("Class is null");
    	}
    	
        // throw error if class is not Stringable
        if (!isStringable(dataType)) {
            throw new Error(
                "Class is not Stringable: " + dataType.getName());
        }

        // attempt to create an instance and set its state
        try {
            Stringable object = (Stringable)dataType.newInstance();
            object.fromStringData(data);
            return object;
        }

        // catch a parse exception and throw it to caller
        catch (ParseException ex1) {
            throw ex1;
        }
        
        // catch a number format exception and then throw
        // it to caller as a parse exception
        catch (NumberFormatException ex2) {
            throw new ParseException(ex2.getMessage(), -1);
        }
        
        // catch the remaining security and instantiation
        // exceptions thrown by the VM and throw an error
        catch (Exception ex3) {
            throw new Error(
                "Stringable could not be instantiated: "
                + ex3.getMessage());
        }
    }
}
