// ApplicationProperties

//
// Description:
//	  
//	  
// Author:
//	Peter Pilgrim
//	Thu Jun 24 16:35:50 BST 1999
//	
// RCS HEADER 
// 
// $Author: peterp $
// $Date: 1999/06/28 13:28:50 $
// $Source: /new/peterp/JavaApp/Humanoid-0.3/ApplicationProperties.java,v $
// $Revision: 1.1 $    $State: Exp $    $Locker:  $
//
// ================================================================================
// History
//	  $Log: ApplicationProperties.java,v $
//	  Revision 1.1  1999/06/28 13:28:50  peterp
//	  Registered in CVS
//
// 
// 

package db.otcdev.utils;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;

import java.util.*;				// for Properties, Hashtable


/**
 * A convenience class to load and save application properties
 *
 * <p>
 * <B>NOTE</B>: This is a singleton object; you can only create it
 * once.
 *
 * <p>
 * This is very simple application properties controller
 * obvioulsy needs a lot more work.
 *
 * <p>
 * The class tries to the load files with 
 * `<ApplicationClass>.properties'
 * where you supply the application class name.
 *
 * <p>
 * You can supply optional <b>fallback properties</b>.
 * These are an array of strings of the form <code>NAME=VALUE</code>.
 * 
 * <p>
 * The class load property files using the following algorithm
 * 
 * <ol>
 * <li>get system property `app.resources.dir'
 * If it does not exists use a system default.
 * 
 * <li> check if a file `<ApplicationClass>.properties' exists
 * in the `app.resources.dir', if it does exists then load it
 * and merge the properties.
 * Otherwise load the file `SystemPreferences.properties'
 * from the `app.resource.dir' if that file exists.
 * Essentially, these are the system wide resources where
 * System Administrations can set up wide preferences. 
 * Alternative if the system property `JAPPLRESDIR' is set
 * then this directory is used instead. In other words
 * this may be easier to set in a script:
 * <pre>
 *  `<b>java -DJAPPLRESDIR=/usr/local/apps/resources ...</b>'
 * </pre>
 * 
 * <li> check if a file `<ApplicationClass>.properties' exists
 * in the `user.home', if it does exists then load it and
 * merge the properties.
 * Otherwise load the file `UserPreferences.properties'
 * from the `app.resource.dir' if that file exists.
 * Essentially, these are the user resources where
 * individual can set up customized preferences. 
 * 
 * <li> check if a file `<ApplicationClass>.properties' exists
 * in the `user.dir', if it does exists then load it and
 * merge the properties.
 * 
 * 
 * </ol>
 * 
 * <p>
 * There convenience getter methods to retrieve values of properties
 * They retrieve resources by prefixing the given property name
 * with the application class.
 *
 * <p>
 * In short instead of coding:
 * <pre>
 *	String gradient = 
 *		System.getProperty("CADDesigner.widget.gradient"); 
 * </pre>
 *
 * <p>
 * You can now write:
 *
 * <pre>
 *	String [] fallbacks = {
 *	   "widget.gradient=2.014"
 *
 *	ApplicationProperties.createInstance( "CADDesigner", fallbacks );
 *
 *	...
 *
 *	ap = ApplicationProperties.getInstance()
 *	double gradValue = 
 *		ap.getDouble("CADDesigner.widget.gradient"); 
 * </pre>
 *
 * <p>
 * Now tell me, how better is that?
 * 
 * <p>
 * It is possible to debug the loading process.
 * If you set the system property <b>app.resources.debug</b>to `true' or
 * if you set the system property <b>DEBUG_RESOURCES</b> to `true'.
 * This may be useful in a script file:
 *
 * <pre>
 *  `<b>java -DDEBUG_RESOURCES=${DEBUG_RESOURCES} ...</b>'
 * </pre>
 *
 * <p>
 *
 * @author Peter Pilgrim Thu Jun 24 12:15:55 BST 1999
 * @version $Id: ApplicationProperties.java,v 1.1 1999/06/28 13:28:50 peterp Exp $
 */
public final class ApplicationProperties extends Properties {
    
    protected static boolean		debug=false;

    /** The application class name */
    protected String			appClassName;
    
    private static Object		locker = new Object();
    private static ApplicationProperties TheAppProps=null;

    // Private Constructor
    private ApplicationProperties( 
	String appClassName, 
	String [] fallbacks ) 
    {
	// super( System.getProperties() );
	super();

	this.appClassName = appClassName;

	String temp = System.getProperty( "app.resources.debug", null );
	if ( temp == null )
	    temp = System.getProperty( "DEBUG_RESOURCES", "false" );

	if ( temp.equalsIgnoreCase("T") || 
	     temp.equalsIgnoreCase("TRUE") || 
	     temp.equalsIgnoreCase("YES") || 
	     temp.equalsIgnoreCase("ON") || 
	     temp.equalsIgnoreCase("1") )
	    debug=true;
	else
	    debug=false;
	
	loadProperties( fallbacks );
    }

    /**
     * Create the singleton instance 
     * 
     * @param appClassName the application class name
     * @param fallbacks the optional fallback properties
     */
    public static ApplicationProperties createInstance( 
	String appClassName, String [] fallbacks )
    {
	if (TheAppProps == null ) {
	    synchronized (locker) {
		if (TheAppProps == null ) {
		    TheAppProps = 
			new ApplicationProperties( appClassName, fallbacks );
		}
	    }
	}
	
	return (TheAppProps);
    }
    

    /**
     * Get the singleton instance 
     * 
     * @param appClassName the application class name
     * @param fallbacks the optional fallback properties
     */
    public static ApplicationProperties getInstance()
    {
	if (TheAppProps == null ) {
	    synchronized (locker) {
		if (TheAppProps == null ) {
		    throw new NullPointerException(
			"application properties singleton was never initialised."+
			"Please call `ApplicationProperties.createInstance()' first" );
		}
	    }
	}
	

	return (TheAppProps);
    }

    /**
     * gets the application class name 
     */
    public String getApplicationClassName()
    {
	return appClassName;
    }

    /**
     * gets the application resources directory path as a string.
     * This is a convenience method for returning the value of the 
     * system property <b>app.resources.dir</b>.
     * If this value is not set equals null then the system default 
     * application resources directory is returned instead.
     */
    public static String getApplicationResourcesDir( ) 
    {
	String resDir = System.getProperty( "app.resources.dir" );
	if ( resDir == null )
	    resDir = System.getProperty( "JAPPLRESDIR", null );
	// System.out.println( "app.resources.dir=`"+resDir+"'" );
	if ( resDir == null ) 
	    resDir = getSystemApplicationResourcesDir();
	// System.out.println( "resDir=`"+resDir+"' *FINAL*" );

	return (resDir);
    }
    
    /**
     * gets the <em>system</em> application resources directory 
     * as a string
     */
    public static String getSystemApplicationResourcesDir( ) 
    {
	String  osname =  System.getProperty( "os.name" );
	String  fileSep = System.getProperty( "file.separator" );
	String  pathSep = System.getProperty( "path.separator" );

	String  resDir;
	
	if ( osname.startsWith( "Windows 95" ) ||
	     osname.startsWith( "Windows 98" ) ||
	     osname.startsWith( "Windows 2000" ) ||
	     osname.startsWith( "OS/2" ) )
	    // Windows/NT  `C:\Application\Resources'
	    resDir="C:"+fileSep+"Application"+fileSep+"Resources";
	else if ( osname.startsWith( "MacOS" ) )
	    resDir="Macintosh HD"+fileSep+"Application"+fileSep+"Resources";
	else {
	    // Default UNIX
	    // `/usr/local/java/app/resources'"
	    
	    resDir=fileSep+"usr"+fileSep+"local"+fileSep+"java"+
		fileSep+"app"+fileSep+"resources";


	    // This is kludge for Deutsche Bank OTC/LONDON
	    // It should not be really here
	    // ----------------------------------------------------------------------
	    if ( ! (new File( resDir )).exists() ) {
		if ( ( new File( "/apps/summit/DMGUtils/properties")).exists() )
		    resDir = "/apps/summit/DMGUtils/properties";
	    }
	    // ----------------------------------------------------------------------


	}

	return (resDir);
    }
    

    /** implementation defined */
    protected void debugFile( File file )
    {
	System.out.println( "    file:`"+file.getPath()+"'  exists?:"+file.exists());
	if (file.exists() )	
	    System.out.println( 
		"\tread:"+file.canRead()+" write:"+file.canWrite()+
		" isDir:"+file.isDirectory()+" isFile:"+file.isFile() );
    }
    
    /** implementation defined */
    protected void loadProperties( String [] fallbacks )
    {
	loadFallbackProperties( fallbacks );

	String  osname =  System.getProperty( "os.name" );
	String  fileSep = System.getProperty( "file.separator" );
	String  pathSep = System.getProperty( "path.separator" );

	String applResDir = getApplicationResourcesDir();
	String userHome = System.getProperty( "user.home" );
	String currDir  = System.getProperty( "user.dir" );
	String baseAppName = appClassName+".properties";

	File appFile;

	if (debug) {
	    System.out.println( getClass().getName()+".loadProperties() DEBUG:" );
	    System.out.println( " applResDir="+applResDir );
	    System.out.println( "   userHome="+userHome );
	    System.out.println( "    currDir="+currDir );
	    System.out.println( "baseAppName="+baseAppName );
	}
	    
	appFile = new File( applResDir, baseAppName );
	if ( debug ) debugFile( appFile );
	if ( appFile.exists() ) 
	    mergePropertyFile( this, appFile, true );
	else {
	    appFile = new File( applResDir, "SystemPreferences.properties" );
	    if ( debug ) debugFile( appFile );
	    if ( appFile.exists() ) 
		mergePropertyFile( this, appFile, true );
	}
	
	appFile = new File( userHome, baseAppName );
	if ( debug ) debugFile( appFile );
	if ( appFile.exists() ) 
	    mergePropertyFile( this, appFile, true );
	else {
	    appFile = new File( userHome, "UserPreferences.properties" );
	    if ( debug ) debugFile( appFile );
	    if ( appFile.exists() ) {
		appFile = new File( userHome, baseAppName );
	    }
	}
	
	appFile = new File( currDir, baseAppName );
	if ( debug ) debugFile( appFile );
	if ( appFile.exists() ) 
	    mergePropertyFile( this, appFile, true );
    }
    
    
    /** implementation defined */
    protected void loadFallbackProperties( String [] fallbacks )
    {
	if (debug) System.out.println("loadFallbackProperties() DEBUG:");
	
	if (fallbacks == null) return;
	
	for (int k=0; k<fallbacks.length; ++k ) {
	    String line = fallbacks[k];
	    if ( line.indexOf("=") < 0 ) 
		throw new IllegalArgumentException(
		    "fallback resources not in the format `<NAME> = <VALUE>'\n"+ 
		    "line:"+k+": "+line );

	    StringTokenizer st = new StringTokenizer( line, "=" );
	    String name = appClassName+"."+st.nextToken().trim();
	    String value = st.nextToken().trim();
	    if (debug) System.out.println( "name:`"+name+"'  value:`"+value+"'" );
	    put( name, value );
	}
    }
    
    /**
     * setProperty : add a property and value to the list of properties
     * The property name is automatically prepended with 
     * application class name.
     *
     * @param name the resource name 
     * @param value the value as a string
     */
    public void setProperty( String name, String value )
    {
	if ( name == null )
	    throw new NullPointerException( "expected non null property name." );
	name = name.trim();
	if ( name.length() < 0 )
	    throw new IllegalArgumentException( "empty property name!" );
	put( appClassName+"."+name, value );
    }
    

    /**
     * mergePropertyFile - loads the properties of a source file 
     * in to temporary properties list, then merges the source 
     * properties into the target.
     *
     * @param propTarget the target properties to merge
     * @param file the source property default filename
     * @param override if true then override the targe property if
     * the source property tuple exists.
     *
     */
    public boolean mergePropertyFile( 
	Properties propTarget, File propFile, boolean override ) 
    {
	if ( !propFile.exists() ) 
	    return (false);
	
	//
	// load the default application properties files.
	//
	if (debug) System.out.println("mergePropertyFile( `"+propFile.getPath()+"' )  DEBUG" );
	if ( !propFile.canRead() ) {
	    printWarning("Cannot read default properties file:`"+propFile.getPath()+"'" );
	    return (false);
	}
	if ( !propFile.isFile() ) {
	    printWarning("this is not a regular file:`"+propFile.getPath()+"'" );
	    return (false);
	}
	
	//
	// Merge the default properties into the system properties
	//
	FileInputStream fis=null;
	boolean	   retval=false;
	Properties propTemp = new Properties();
	try {
	    fis = new FileInputStream( propFile );
	    propTemp.load(fis);
	    Enumeration keys = propTemp.propertyNames();
	    while( keys.hasMoreElements() ) {
		String name = (String)keys.nextElement();
		String value1 = propTemp.getProperty( name );
		String value2 = propTarget.getProperty( name );
		if (value2 == null || override) {
		    // DEBUG: 
		    if (debug) System.out.println( "    Merging ==> "+name+"="+value1 );
		    propTarget.put( name, value1 );
		}
	    }
	}
	catch (IOException ioe) {
	    retval = false;
	    System.err.println( 
		"I/O Exception occurred reading file:`" + 
		propFile.getPath() + "'  message:`" + ioe.getMessage()+"'" );
	}
	finally {
	    if (fis!=null) 
		try { fis.close(); } catch (IOException e1) { }
	}
	
	return (retval);
    }
    
    /** 
     *
     * save properties as a complete output with a different
     * application class name to a generic output stream
     *
     * the application class name is renamed when it written
     * the output stream. For example:
     * 
     * <pre>
     *  `Test.java.book=Core Java 1.2 ' 
     *  --> `<b>NewFile</b>.java.book=Core Java 1.2'
     * </pre>
     * 
     * 
     * @param ps the output print stream
     * @param newAppClassName the new application class name
     * @param header the header string optional 
     *
     */
    public synchronized void saveNamedAs( 
	OutputStream out, String newAppClassName, String header )
    {
	if (debug) {
	    System.out.println( getClass().getName()+".saveNamedAs() DEBUG:" );
	    System.out.println( "\tnew app class name:`"+newAppClassName+"'" );
	}
	
	Properties propRenamed = new Properties();
	
	Enumeration keys = propertyNames();
	while( keys.hasMoreElements() ) {
	    String name = (String)keys.nextElement();
	    String value = getProperty( name );
	    
	    // Transform name here.
	    if ( name.startsWith( appClassName ) ) {
		String newName = newAppClassName + 
		    name.substring( appClassName.length() );
		if (debug) System.out.println( "    "+name+" -> "+newName );
		name = newName;
	    }

	    propRenamed.put( name, value );
	}

	PrintStream pout = new PrintStream( out, false);
	pout.println( "# `"+newAppClassName+".properties'" );
	pout.println( "# " );
	pout.println( "# Output generated by `"+getClass().getName()+"'" );
	pout.println( "# Java is the best! Written by Peter Pilgrim 1999" );
	pout.println( "# " );
	pout.flush();
	
	propRenamed.save( out, header );
    }
    
    /** 
     * a convenience method to save the application properties
     * as a read named <em>properties</em> file
     *
     *
     * @param outfile the output file.
     * @param header the header string
     *
     * @return true if the file save was successful
     *
     * @see #saveNamedAs
     *
     */
    public boolean saveNamedAsFile( String outfile, String header )
    {
	//
	// Merge the default properties into the system properties
	//
	boolean retval = false;
	
	FileOutputStream fos=null;
	try {
	    // Get the basename root of the property file `/aps/doc/Test' -> `Test'
	    String newBaseName = (new File(outfile)).getName();

	    // Add the suffix `properties' file `/aps/doc/Test' 
	    // -> `/aps/doc/Test.properties'
	    File propertiesFile = new File( outfile+".properties" );
	    fos = new FileOutputStream( propertiesFile );
	    saveNamedAs( fos, newBaseName, header );
	    retval = true;
	}
	catch (IOException ioe) {
	    retval = false;
	    System.err.println( 
		"I/O Exception occurred writing file:`" + 
		outfile+"'  message:`" + ioe.getMessage()+"'" );
	}
	finally {
	    if (fos!=null) 
		try { fos.close(); } catch (IOException e1) { }
	}

	return (retval);
    }
    
    /** 
     * Save current user preferences (the current application 
     * properties) in the user's home directory.
     * But first back up the original user preferences file by 
     * renaming if the exist. The backup file will have the
     * extension suffix <b>`.bak'</b>.
     *
     *
     * @param header the header string
     *
     * @return true if the file save was successful
     *
     * @see #saveNamedAs
     * @see #saveNamedAsFile
     */
    public boolean updatePreferencesFile( String header ) 
    {
	String userHome = System.getProperty( "user.home" );
	String baseAppName = appClassName+".properties";

	File prefFile = new File( userHome, baseAppName );
	File backupFile = new File( userHome, baseAppName+".bak" );

	if ( backupFile.exists() )
	    backupFile.delete();
	if ( prefFile.exists() )
	    prefFile.renameTo( backupFile );

	String  fileSep = System.getProperty( "file.separator" );
	return saveNamedAsFile( userHome+fileSep+appClassName, header );
    }
	
    /** This is convenience helper to get the resource as a boolean value
     * from the application resources. 
     * @param name the resource name
     */
    public boolean getBoolean( String name )
    {
	return getBoolean( name, true );
    }

    /** This is convenience helper to get the resource as a string value
     * from the application resources. 
     * @param name the resource name
     */
    public String getString( String name )
    {
	return getString( name, "" );
    }
    
    /** This is convenience helper to get the resource as an intvalue
     * from the application resources. 
     * @param name the resource name
     */
    public int getInteger( String name )
    {
	return getInteger( name, 0 );
    }
    
    /** This is convenience helper to get the resource as a long value
     * from the application resources. 
     * @param name the resource name
     */
    public long getLong( String name )
    {
	return getLong( name, 0L );
    }
    
    /** This is convenience helper to get the resource as a float value
     * from the application resources. 
     * @param name the resource name
     */
    public float getFloat( String name )
    {
	return getFloat( name, 0.0F );
    }
    
    /** This is convenience helper to get the resource as a double value
     * from the application resources. 
     * @param name the resource name
     */
    public double getDouble( String name )
    {
	return getDouble( name, 0.0 );
    }
    

    /** This is convenience helper to get the resource as a string value
     * from the application resources. 
     * @param name the resource name
     * @param def the default integer value
     */
    public String getString( String name, String def )
    {
	return getProperty( appClassName+"."+name, def );
    }

    /** This is convenience helper to get the resource as a boolean value
     * from the application resources. 
     * @param name the resource name
     * @param def the default integer value
     */
    public boolean getBoolean( String name, boolean def )
    {
        boolean ret;

	String resname = appClassName+"."+name;
	String temp = getProperty( resname, "" ).trim();
	if ( temp.equalsIgnoreCase("T") || 
	     temp.equalsIgnoreCase("TRUE") || 
	     temp.equalsIgnoreCase("YES") || 
	     temp.equalsIgnoreCase("ON") || 
	     temp.equalsIgnoreCase("1") )
	    ret = true;
	else if ( temp.equalsIgnoreCase("F") || 
	     temp.equalsIgnoreCase("FALSE") || 
	     temp.equalsIgnoreCase("NO") || 
	     temp.equalsIgnoreCase("OFF") || 
	     temp.equalsIgnoreCase("0") )
	    ret = false;
	else {
	    System.err.println("WARNING: cannot convert to boolean resource:`"+
			       resname+"' value:`"+temp+"'" );
	    ret = def;
	}
	
	
	return (ret);
    }


    /** This is convenience helper to get the resource as an int value
     * from the application resources. 
     * @param name the resource name
     * @param def the default integer value
     */
    public int getInteger( String name, int def )
    {
        int ret;
	String temp=null, resname=appClassName+"."+name;
	
	try { 
	    temp = getProperty( resname, "" );
	    // System.out.println( "temp:" + temp );
	    ret = (temp != null) ? Integer.parseInt( temp.trim() ) : def;
	    
	}
	catch (NumberFormatException nfe) { 
	    System.err.println("WARNING: cannot convert to int resource:`"+
			       resname+"' value:`"+temp+"'" );
	    ret = def; 
	}
	return (ret);
    }

    /** This is convenience helper to get the resource as a long integer value
     * from the application resources. 
     * @param name the resource name
     * @param def the default long integer value
     */
    public long getLong( String name, long def )
    {
        long ret;
	String temp=null, resname = appClassName+"."+name;
	
	try { 
	    temp = getProperty( resname, "" );
	    // System.out.println( "temp:" + temp );
	    ret = (temp != null) ? Long.parseLong( temp.trim() ) : def;
	    
	}
	catch (NumberFormatException nfe) { 
	    System.err.println("WARNING: cannot convert to long resource:`"+
			       resname+"' value:`"+temp+"'" );
	    ret = def;
	}
	return (ret);
    }

    /** This is convenience helper to get the resource as a float value
     * from the application resources. 
     * @param name the resource name
     * @param def the default float value

     */
    public float getFloat( String name, float def )
    {
        float ret;
	String temp=null, resname=appClassName+"."+name;
	
	try { 
	    temp = getProperty( resname, "" );
	    // System.out.println( "temp:" + temp );
	    ret = (temp != null) ? Float.valueOf( temp.trim() ).floatValue() : def;
	    
	}
	catch (NumberFormatException nfe) { 
	    System.err.println("WARNING: cannot convert to float resource:`"+
			       resname+"' value:`"+temp+"'" );
	    ret = def;
	}
	return (ret);
    }

    /** This is convenience helper to get the resource as a double value
     * from the application resources. 
     * @param name the resource name
     * @param def the default double value
     */
    public double getDouble( String name, double def )
    {
        double ret;
	String temp=null, resname=appClassName+"."+name;
	
	try { 
	    temp = getProperty( resname, "" );
	    // System.out.println( "temp:" + temp );
	    ret = (temp != null) ? Double.valueOf( temp.trim() ).doubleValue() : def;
	    
	}
	catch (NumberFormatException nfe) { 
	    System.err.println("WARNING: cannot convert to double resource:`"+
			       resname+"' value:`"+temp+"'" );
	    ret = def;
	}
	return (ret);
    }

    /** implementation defined */
    protected void printWarning( String msg )
    {
	System.out.println( "*WARNING* : "+msg );
    }
    

    private final static String	FALLBACKS[] = {
	"publisher=O'Reilly Associates",
	"book=Java In A Nutshell",
	"author=David Flanagan",
    };
    
    public static void main( String [] args ) 
    {
	ApplicationProperties.createInstance( "Demo", FALLBACKS );

	ApplicationProperties ap = ApplicationProperties.getInstance();

	System.out.println( "Printing out application resources" );
	System.out.println( "==================================================");
	
	System.out.println( "publisher: "+ap.getString("publisher") );
	System.out.println( "book: "+ap.getString("book") );
	System.out.println( "author: "+ap.getString("author") );
	
	ap.list( System.out );

	String outFile = "Test";
	System.out.println( "Saving new properties file:`"+outFile+".properties'" );
	ap.saveNamedAsFile( outFile, null );
    }
    
}


// fini
