/*
 * (c) Copyright Hewlett-Packard Company 2001
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE and no warranty
 * that the program does not infringe the Intellectual Property rights of
 * a third party.  See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 *
 */

package jade.util;

//#MIDP_EXCLUDE_FILE

// DO NOT ADD ANY IMPORTS FOR CLASSES NOT DEFINED IN J2ME CDC!
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Date;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.PrintStream;
import java.io.IOException;
import java.io.EOFException;

import jade.util.leap.Properties;

/**
 * Provides enhanced property management.<br> 
 * 1) Allows specifying property values containing strings of the form <b><tt>${key}</tt></b>. 
 * Such strings are properly replaced with the value of the <tt>key</tt> variable
 * that may represent another property, a system property or an environment variable. 
 * <p>
 * 2) Allows specifying boolean properties in the form <tt>-key</tt>. Such format is 
 * equivalent to <tt>key=true</tt>.
 * <p>
 * 3) Allows importing properties from external property files by means of the special key
 * <tt>import</tt>. E.g. specifying the property<br>
 * <tt>import = a-property-file-name</tt><br>
 * results in automatically adding all properties defined in the specified property file
 * <p>
 * 4) Allows declaring a property as read-only.
 * In order to do that it is sufficient to end its key with a '!'. For example:
 * <pre>
 *      agentClass!=com.hp.agent.Foo
 * </pre>
 * The value of the property can be accessed including the '!' in the key or not indifferently 
 * <p>
 * This class is designed to be usable in the restrictive J2ME CDC environment.
 * @author Dick Cowan - HP Labs
 */
public class ExtendedProperties extends Properties {
	public static final String IMPORT_KEY="import";

	boolean CRState = false;
	Hashtable keyNames = new Hashtable();  // for detecting circular definitions
	Vector sortVector = null;   // only used by sortedKeys

	private Logger logger = Logger.getMyLogger(getClass().getName());
	
	/**
	 * For testing. Simply pass command line arguments to constructor then display
	 * all key=value pairs using sorted enumeration.
	 */
	public static void main(String[] args) {
		ExtendedProperties prop = new ExtendedProperties(args);
		prop.list(System.out);
	}

	/**
	 * Construct empty property collection.
	 */
	public ExtendedProperties() {
	}

	/**
	 * Construct a ExtendedProperties object from an array of stringified properties of the form
	 * <key>=<value>.
	 * @param propesStr The applications original arguments.
	 */
	public ExtendedProperties(String[] propesStr) {
		this();
		addProperties(propesStr);
	}

	/**
	 * Add properties from a specified InputStream. Properties
	 * will be added to any existing collection.
	 * @param aFileName The name of the file.
	 * @throws IOException if anything goes wrong.
	 */
	public synchronized void load(InputStream inStream) throws IOException {
		addFromReader(new InputStreamReader(inStream, "8859_1"));
	}

	/**
	 * Writes this property collection to the output stream in a format suitable for
	 * loading into a Properties table using the load method.
	 * @param out An output stream.
	 * @param header A description of the property list - may be null.
	 * @throws IOException if anything goes wrong.
	 */
	public synchronized void store(OutputStream out, String header) throws IOException {
		String lineSeparator = System.getProperty("line.separator");
		Writer writer = new OutputStreamWriter(out, "8859_1");
		if (header != null) {
			writer.write("#" + header);
			writer.write(lineSeparator);
		}
		writer.write("#" + new Date().toString());
		writer.write(lineSeparator);
		for (Enumeration e = sortedKeys(); e.hasMoreElements();) {
			String key = (String)e.nextElement();
			Object data = super.get(key);
			if (data != null) {
				writer.write(key + "=" + data.toString());
				writer.write(lineSeparator);
			}
		}
		writer.flush();
	}

	/**
	 * Return a sorted enumeration of this properties keys.
	 * @return Enumeration Sorted enumeration.
	 */
	public synchronized Enumeration sortedKeys() {
		if (sortVector == null) {
			sortVector = new Vector();
		} else {
			sortVector.removeAllElements();
		}
		for (Enumeration e = super.keys(); e.hasMoreElements(); ) {
			String key = (String) e.nextElement();
			int i = 0;
			while (i < sortVector.size()) {
				if (key.compareTo((String)sortVector.elementAt(i)) < 0) {
					break;
				}
				i++;
			}
			sortVector.insertElementAt(key, i);
		}
		return new Enumeration() {
			Enumeration en = ExtendedProperties.this.sortVector.elements();

			public boolean hasMoreElements() {
				return en.hasMoreElements();
			} 

			public Object nextElement() {
				return en.nextElement();
			} 

		};
	} 


	/**
	 * Add to this Properties object the stringified properties included in a given array.
	 * @param propsStr The array of stringified properties. If null, this method does nothing.
	 */
	public synchronized void addProperties(String[] propsStr) {
		if (propsStr != null) {
			for (int i = 0; i < propsStr.length; ++i) {
				addProperty(propsStr[i]);
			}
		}
	}

	/**
	 * Add to this Properties object a stringified property of the form
	 * <tt>key = value</tt> or <tt>-key</tt>
	 * @param propStr The string representation of the property to be parsed
	 */
	protected void addProperty(String propStr) {
		if (propStr != null) {
			propStr.trim();

			String key = null;
			String value = null;
			int separatorIndex = getSeparatorIndex(propStr);    // key=value or key:value
			if (separatorIndex > 0) {
				key = propStr.substring(0, separatorIndex).trim();
				if (key.length() == 0) {
					// Case:   =value  (Not permitted)
					throw new PropertiesException("Unable to identify key part in property: " + propStr);
				}
				if (separatorIndex == propStr.length() - 1) {
					// Case:   key=
					value = null;
				}
				else {
					// Case:   key=value
					value = propStr.substring(separatorIndex+1).trim();
				}
			}
			else if ((propStr.length() > 1) && (propStr.startsWith("-"))) {
				// Case: -key
				key = propStr.substring(1);
				value = "true";
			}
			else {
				// Case: xyz  (Not permitted)
				throw new PropertiesException("Wrong property format: " + propStr);
			}
			setProperty(key, value);
		}
		else {
			throw new PropertiesException("Null property format");
		}
	}

	/**
	 * Retrieve the position of the first valid key-value separator character ('=' or ':') in
	 * a stringified property. 
	 * @param propStr The stringified property.
	 */
	protected int getSeparatorIndex(String propStr) {
		int idxA = propStr.indexOf('=');
		int idxB = propStr.indexOf(':');

		if (idxA == -1)  // key:value 
			return idxB;
		if (idxB == -1)  // key=value
			return idxA;
		if (idxA < idxB) // key=value with :
			return idxA;
		else             // key:value with =
			return idxB;   
	}

	/**
	 * Copy a data from standard Properties.
	 * @param source The properties to copy from.
	 */
	public synchronized void copyProperties(ExtendedProperties source) {
		for (Enumeration e = source.keys(); e.hasMoreElements(); ) {
			String key = (String)e.nextElement();
			super.put(key, source.getRawProperty(key));
		}
	}

	/**
	 * Create a new Properties object by coping those
	 * properties whose key begins with a particular prefix string.
	 * The prefix is removed from the keys inserted into the extracted Properties object
	 * @param prefix The prefix string. Ex: "server."
	 */
	public synchronized ExtendedProperties extractSubset(String prefix) {
		ExtendedProperties result = new ExtendedProperties();
		for (Enumeration e = super.keys(); e.hasMoreElements(); ) {
			String originalKey = (String) e.nextElement();
			String newKey = null;

			if (originalKey.startsWith(prefix)) {
				newKey = originalKey.substring(prefix.length());
				result.setProperty(newKey, getRawProperty(originalKey));
			} 
		}

		return result;
	}

	/**
	 * Get the object associated with a key. Note that, unlike getProperty(), this method does not 
	 * perform substitutions of variables of the form ${x} in property values since such values may 
	 * be non-string objects.
	 * @param aKey Key for desired property.
	 * @return The object associated with this key or null if none exits.
	 */
	public Object get(Object aKey) {
		Object value = null;
		if (aKey instanceof String) {
			String strKey = (String) aKey;
			String testKey = (strKey.endsWith("!")) ? strKey.substring(0, strKey.length()) : strKey;
			// Try WITHOUT the '!' for sure
			value = super.get(testKey);
			if (value == null) {
				// Not found --> Try WITH the '!' for sure
				value = super.get(testKey + "!" );
			}
			
			if (value != null && value instanceof String) {
				String strValue = (String) value;
				// This synchronized block prevents a "Circular argument substitution key" error in case two threads
				// search for the same key in parallel
				synchronized (keyNames) {
					if (keyNames.put(testKey, "x") != null) {  // value doesn't matter
						throw new PropertiesException("Circular argument substitution with key: " + aKey);
					}
					if (strValue.length() >= 4) {    // shortest possible value: ${x}
						strValue = doSubstitutions(strValue);
					}
					keyNames.remove(testKey);
				}
				strValue = valueFilter(strKey, strValue);
				value = strValue;
			}
		}
		else {
			value = super.get(aKey);
		}
		return value;
	}        

	/**
	 * Set property value to specified object.
	 * @param aKey The key used to store the data. The key may contain strings of
	 * the form <b><tt>${key}</tt></b> which will be evaluated first.
	 * @param aValue The object to be stored.
	 * @return The previous value of the specified key, or null if it did not have one.
	 */
	public Object put(Object aKey, Object aValue) {
		if (aKey instanceof String) {
			// Substitutions on keys are done at insertion time
			String actualKey = doSubstitutions((String) aKey);
	
			// aKey may have the form kkk or kkk!. In both cases if a property with key = kkk! exists throws an exception
			String testKey = (actualKey.endsWith("!")) ? actualKey.substring(0, actualKey.length()) : actualKey;
			if (super.containsKey(testKey + "!")) {
				throw new PropertiesException("Attempt to alter read only property:" + testKey);
			}
			
			// If the key to be inserted is the "import" key, do not insert it, but add all properties 
			// defined in the indicated import file
			if (actualKey.equals(IMPORT_KEY) && aValue != null) {
				String importFile = doSubstitutions(aValue.toString());
				try {
					// Try in the classpath first
					InputStream stream = getClass().getClassLoader().getResourceAsStream(importFile);
					if (stream == null) {
						// Not found: try in the file system
						stream = new FileInputStream(importFile);
					}
					load(stream);
				} catch (IOException ioe) {
					logger.log(Logger.WARNING, "Cannot import properties from import-file "+importFile);
				}
				return null;
			}
			else {
				return super.put(actualKey, aValue);
			}
		}
		else {
			return super.put(aKey, aValue);
		}
	}        

	/**
	 * Override getProperty in base class so all occurances of
	 * the form <b><tt>${key}</tt></b> are replaced by their
	 * associated value.
	 * @param aKey Key for desired property.
	 * @return The keys value with substitutions done.
	 */
	public String getProperty(String aKey) {
		return getProperty(aKey, null);
	}

	/**
	 * Perform substitution when a value is fetched. Traps circular definitions,
	 * and calls valueFilter with value prior to returning it.
	 * @param aKey The property key.
	 * @param defaultValue Value to return if property not defined. May be null.
	 * If non null it will be passes to valueFilter first.
	 * @return The resultant value - could be null or empty.
	 * @throws PropertiesException if circular definition.
	 */
	public String getProperty(String aKey, String defaultValue) {
		Object value = get(aKey);
		if (value != null) {
			return value.toString();
		}
		else {
			return defaultValue;
		}
	}
	
	/**
	 * Set property value. If value is null the property (key and value) will be removed.
	 * @param aKey The key used to store the data. The key may contain strings of
	 * the form <b><tt>${key}</tt></b> which will be evaluated first.
	 * @param aValue The value to be stored, if null they property will be removed.
	 * @return The previous value of the specified key, or null if it did not have one.
	 */
	/*public Object setProperty(String aKey, String aValue) {
		String actualKey = doSubstitutions(aKey);

		String testKey = (actualKey.endsWith("!")) ? actualKey.substring(0, actualKey.length()) : actualKey;
		if (super.containsKey(testKey + "!")) {
			throw new PropertiesException("Attempt to alter read only property:" + testKey);
		}

		if (aValue == null) {
			return super.remove(actualKey);
		} else {
			return super.put(actualKey, aValue);
		}
	}*/

	/**
	 * Set property value only if its not set already.
	 * @param aKey The key used to store the data. The key may contain strings of
	 * the form <b><tt>${key}</tt></b> which will be evaluated first.
	 * @param value The value to be stored.
	 * @return Null if store was done, non-null indicates store not done and the
	 * returned value in the current properties value.
	 */
	public Object setPropertyIfNot(String aKey, String value) {
		String current = getProperty(aKey);
		if (current == null) {
			return setProperty(aKey, value);
		}
		return current;
	}

	/**
	 * Fetch property value for key which may contain strings
	 * of the form <b><tt>${key}</tt></b>. 
	 * @param aKey Key for desired property.
	 * @return The keys value with no substitutions done.
	 */
	public String getRawProperty(String aKey) {
		Object data = super.get(aKey);
		return (data != null) ? data.toString() : null;
	}

	/**
	 * Use this method to fetch a property ignoring case of key.
	 * @param aKey The key of the environment property.
	 * @return The key's value or null if not found.
	 */
	public String getPropertyIgnoreCase(String aKey) {
		for (Enumeration e = super.keys(); e.hasMoreElements(); ) {
			String key = (String) e.nextElement();

			if (aKey.equalsIgnoreCase(key)) {
				return getProperty(key);
			}
		}
		return null;
	}


	/**
	 * Called by getProperty(key, default) to perform any post processing of the
	 * value string. By default, this method provides special processing on the value
	 * associated with any property whose key name has the string "path" as part of it
	 * (ex: "classpath", "sourcepath", "mypath"). When the value for such keys is fetched
	 * any occurance of '|' will be converted to a ':' on Unix systems and a ';' on
	 * Windows systems. Therefore to increase the direct reuse of your property files,
	 * always use a '|' as a separator and always assign a key name which has "path" as
	 * part of it.
	 * @param key The properties key.
	 * @param value The properties value.
	 * @return String New potentially altered value. 
	 */
	protected String valueFilter(String key, String value) {
		if (key.toLowerCase().indexOf("path") >= 0) {    // convert separators to be correct for this system
			String correctSeparator = System.getProperty("path.separator");

			if (correctSeparator.equals(";")) {
				value = value.replace('|', ';');
			} else {
				value = value.replace('|', ':');
			}
		}
		return value;
	}

	/**
	 * Extract a string value and convert it to an integer.
	 * If there isn't one or there is a problem with the conversion,
	 * return the default value.
	 * @param aKey The key which will be used to fetch the attribute.
	 * @param aDefaultValue Specifies the default value for the int.
	 * @return int The result.
	 */
	public int getIntProperty(String aKey, int aDefaultValue) {
		int result = aDefaultValue;

		try {
			result = Integer.parseInt(getProperty(aKey));
		} catch (Exception e) {}

		return result;
	}

	/**
	 * Store an int as a string with the specified key.
	 * @param aKey The key which will be used to store the attribute.
	 * @param aValue The int value.
	 */
	public int setIntProperty(String aKey, int aValue) {
		setProperty(aKey, Integer.toString(aValue));
		return aValue;
	}

	/**
	 * Extract a string value ("true" or "false") and convert it to
	 * a boolean. If there isn't one or there is a problem with the
	 * conversion, return the default value.
	 * @param aKey The key which will be used to fetch the attribute.
	 * @param aDefaultValue Specifies the default value for the boolean.
	 * @return boolean The result.
	 */
	public boolean getBooleanProperty(String aKey, boolean aDefaultValue) {
		boolean result = aDefaultValue;

		try {
			String value = getProperty(aKey);

			result = value.equalsIgnoreCase("true");
		} catch (Exception e) {}

		return result;
	}

	/**
	 * Store a boolean as a string ("true" or "false") with the specified key.
	 * @param aKey The key which will be used to store the attribute.
	 * @param aValue The boolean value.
	 */
	public void setBooleanProperty(String aKey, boolean aValue) {
		setProperty(aKey, (aValue) ? "true" : "false");
	}

	/**
	 * Change key string associated with existing value.
	 * @param existintKey The current key.
	 * @param newKey The new key.
	 * @return Non null is former value of object associated with new key.
	 * Null indicates that either the existing key didn't exist or there
	 * was no former value associated with the new key. i.e. null => success.
	 */
	public Object renameKey(String existingKey, String newKey) {
		Object value = remove(doSubstitutions(existingKey));
		if (value != null) {
			return put(newKey, value);
		}
		return null;
	}

	/**
	 * Replace all substrings of the form ${xxx} with the property value
	 * using the key xxx. Calls doSubstitutions(anInputString, false).
	 * @param anInputString The input string - may be null.
	 * @return The resultant line with all substitutions done or null if input string was.
	 */
	public String doSubstitutions(String anInputString) {
		return doSubstitutions(anInputString, false);
	}

	/**
	 * Replace all substrings of the form ${xxx} with the property value
	 * using the key xxx. If the key is all caps then the property is
	 * considered to be a system property.
	 * @param anInputString The input string - may be null.
	 * @param allowUndefined If true, undefined strings will remain as is,
	 * if false, an exception will be thrown.
	 * @return The resultant line with all substitutions done or null if input string was.
	 */
	public String doSubstitutions(String anInputString, boolean allowUndefined) {
		if (anInputString == null) {
			return null;
		}
		StringBuffer result = new StringBuffer();
		int si = 0;    // source index
		int oi = 0;    // opening index
		int ci = 0;    // closing index

		do {
			oi = anInputString.indexOf("${", si);
			ci = anInputString.indexOf('}', si);

			if (oi > si) {    // xxxxxx${key}
				result.append(anInputString.substring(si, oi));

				si = oi;
			}

			if ((oi == si) && (ci > oi + 2)) {    // ${key}xxxxx
				String key = anInputString.substring(oi + 2, ci);

				// Try as another property first (this allows the user to override system or environment setting)
				String value = getProperty(key, null);

				if ((value == null)) {
					// Not found --> Try as a System property (java -D....) 
					value = System.getProperty(key);

					//#J2ME_EXCLUDE_BEGIN
					if (value == null) {
						// Not found --> Try as an environment variable
						value = System.getenv(key);
					}
					//#J2ME_EXCLUDE_END
				}

				if (value == null) {
					if (allowUndefined) {
						value = "${" + key + "}";
					} else {
						throw new PropertiesException("Unable to get property value for key: " + key);
					}
				}

				if (oi > si) {
					result.append(anInputString.substring(si, oi));
				}

				result.append(value);

				si = ci + 1;
			} else {
				if (oi == -1) {    // xxxxxxxxx
					result.append(anInputString.substring(si, anInputString.length()));

					si = anInputString.length();
				} else {    // xxxxxx${xxxxxx
					result.append(anInputString.substring(si, oi + 2));

					si = oi + 2;
				}
			}
		} while (si < anInputString.length());

		return result.toString();
	}

	/**
	 * Add properties from Reader. Explicitly handled so as to enable
	 * handling of import=<file> directive. Blank lines as well as
	 * those beginning with a '#' character (comments) are ignored.
	 * @param reader The buffered reader to read from.
	 * to catch circular imports.
	 * @throws IOException if anything goes wrong.
	 */
	protected void addFromReader(Reader reader) throws IOException {
		String line = null;
		do {
			line = getOneLine(reader);

			if (line != null) {
				line = line.trim();

				if (line.length() == 0) {
					continue;    // empty line
				}

				if (line.startsWith("#") || line.startsWith("!")) {
					continue;    // comment line
				}

				addProperty(line);
			}
		} while (line != null);
	}

	/**
	 * Get a logical line. Any physical line ending in '\' is considered
	 * to continue on the next line.
	 * @param reader The input reader to read.
	 * @return The resultant logical line which may have been constructed
	 * from one or more physical lines.
	 * @throws IOException if anything goes wrong.
	 */
	protected String getOneLine(Reader reader) throws IOException {
		StringBuffer sb = null;
		String line = null;
		boolean continued;

		do {
			continued = false;

			try {
				line = readLine(reader);

				if (line != null) {
					line = line.trim();
					// If we already have something going ignore blank lines and comments
					if ((sb != null) && ((line.length() == 0) || (line.startsWith("#") || line.startsWith("!")))) {
						continued = true;
						continue;
					}

					continued = line.endsWith("\\");

					if (continued) {    // delete the ending slash
						line = line.substring(0, line.length() - 1);
					}
					if (sb == null) {
						sb = new StringBuffer();
					}
					sb.append(line);
				}
			} catch (EOFException eof) {
				continued = false;
			}
		} while (continued);

		return (sb == null) ? null : sb.toString();
	}

	/**
	 * Read one line from the Reader. A line may be terminated
	 * by a single CR or LF, or the pair CR LF.
	 * @param aReader The Reader to read characters from.
	 * @return Next physical line.
	 * @throws IOException if anything goes wrong.
	 */
	protected String readLine(Reader aReader) throws IOException {
		StringBuffer sb = new StringBuffer();
		boolean done = false;
		while (!done) {
			int result = aReader.read();
			if (result == -1) {
				if (sb.length() > 0) {
					break;
				}
				throw new EOFException();
			} else {
				char ch = (char)result;
				if (ch == '\n') {  // LF
					if (CRState) {
						CRState = false;
						continue;                  
					}
					break;
				} else {
					if (ch == '\r') {
						CRState = true;
						break;
					} else {
						sb.append(ch);
						CRState = false;
					}
				}
			}
		}
		return sb.toString();
	}        

	/**
	 * List properties to provided PrintStream.
	 * Output will be in sorted key sequence.
	 * If a value is null, it will appear as "key=".
	 * @param out The print stream.
	 */
	public void list(PrintStream out) {
		for (Enumeration e = sortedKeys(); e.hasMoreElements(); ) {
			String key = (String) e.nextElement();
			String value = getProperty(key);
			if (value != null) {
				out.println(key + "=" + value);
			} else {
				out.println(key + "=");
			}
		}
	}

	/**
	 * Create a String[] for the properties with one key=value pair per array entry.
	 * If a value is null, it will appear as "key=".
	 * @return The resultant String[].
	 */
	public String[] toStringArray() {
		String[] result = new String[super.size()];
		int i = 0;
		for (Enumeration e = sortedKeys(); e.hasMoreElements(); ) {
			String key = (String) e.nextElement();
			String value = getProperty(key);
			if (value != null) {
				result[i++] = key + "=" + value;
			} else {
				result[i++] = key + "=";
			}
		}
		return result;
	}

}
