/*
* (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;
}
}