/* ==============================================
* Simtools : The tools library used in JSynoptic
* ==============================================
*
* Project Info: http://jsynoptic.sourceforge.net/index.html
*
* This library 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.1 of the License, or (at your option) any later version.
*
* This library 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.
* 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.
*
* (C) Copyright 1999-2003, by :
* Corporate:
* Astrium SAS
* EADS CRC
* Individual:
* Claude Cazenave
* Nicolas Brodu
*
*
* $Id: UserProperties.java,v 1.9 2006/11/23 17:10:34 ogor Exp $
*
* Changes
* -------
* 25-Sep-2003 : Initial public release (NB);
*
*/
package simtools.ui;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;
import java.util.StringTokenizer;
/**
* This class returns a user preferences as saved at previous use of the tool
*
* @author N. Brodu
* @author C. Cazenave
* @author A. Reglat
*/
public class UserProperties extends Properties {
/**
* The file to read and write properties to
*/
protected File _file;
/**
* The product name to apply properties to
*/
protected String _productName;
/**
* List of informations to write sorted properties in file.
*/
protected LinkedList _infoList = new LinkedList();
/**
* The list of MasterProperties
*/
private ArrayList masterPropertiesList;
/**
* The information class
*/
protected static class Info {
public static final String IMPORT_FILE_PREFIX = "@import";
public static int KEY = 0;
public static int COMMENT = 1;
public static int EMPTY_LINE = 2;
public static int MASTER_LINE = 3;
// The info type. This is one of the 3 previous static values.
public final int _type;
// If KEY type, _value may be the key value
// If COMMENT type, _value may be the comment itself
// If EMPTY_LINE, _value may be null
public final String _value;
public Info(int type, String value) {
_type = type;
_value = value;
}
}
/**
* Creates user properties without reading values from the file.
* If system property <code>productName + ".home"</code> is set then look
* for properties file into this directory, else merely use the user home
* directory.
* Unlike the <code>Properties</code> class, there is no property list for
* default values.
*
* @param productName the product name
*/
public UserProperties(String productName) {
this(productName, false);
}
/**
* Creates user properties without reading values from the file.
* If system property <code>productName + ".home"</code> is set then look
* for properties file into this directory, else merely use the user home
* directory.
* Unlike the <code>Properties</code> class, there is no property list for
* default values.
*
* @param productName the product name
* @param useSystemDefaultValues true to use system properties defined
* with -Dproperty=value at launch time. These default values are used only
* when a property is not defined in user property file.
*/
public UserProperties(String productName, boolean useSystemDefaultValues) {
super(useSystemDefaultValues ? System.getProperties() : null);
String home =
System.getProperty(
productName + ".home",
System.getProperty("user.home", ""));
_file = new File(home, "." + productName);
// look in .productname/config which is now the default
// But keep compatibility with the .productname file
if ((!_file.exists()) && _file.mkdirs()) {
_file = new File(_file, "config");
}
else if (_file.isDirectory()) _file = new File(_file, "config");
_productName = productName;
}
/**
* Contructor UserProperties
* <br><b>Summary:</b><br>
* The constructor of the class UserProperties.
* Creates user properties without reading values from the file.
* If system property <code>productName + ".home"</code> is set then look
* for properties file into this directory, else merely use the user home
* directory.
* Unlike the <code>Properties</code> class, there is no property list for
* default values.
* @param propertyFile The file that contains the properties to be loaded.
*/
public UserProperties(File propertyFile) {
super(null);
_file = propertyFile;
_productName = propertyFile.getName();
}
/**
* @returns the file to read and write properties to
*/
public File getFile() {
return _file;
}
protected boolean _propertiesLoadedFromFile = false;
/**
* Reads properties from the file defined in the constructor.
* In case of error, default values will be used
*/
public void read() {
try {
FileInputStream fis = new FileInputStream(_file);
load(fis);
_propertiesLoadedFromFile = true;
} catch (FileNotFoundException fnfe) {
// default values will be used
} catch (IOException ioe) {
// default values will be used
}
}
/**
* Writes properties to the file defined in the constructor.
* In case of error, nothing happens
*/
public void write() {
try {
FileOutputStream fos = new FileOutputStream(_file);
store(fos, null);
} catch (FileNotFoundException fnfe) {
// nothing happens
} catch (IOException ioe) {
// nothing happens
}
}
/**
* Reads properties from the file defined in the constructor.
*/
public void read2() throws FileNotFoundException, IOException {
FileInputStream fis = new FileInputStream(_file);
load(fis);
_propertiesLoadedFromFile = true;
}
/**
* Method <b>getProperty</b>
* <br><b>Summary:</b><br>
* Use this method to get a property.
* Use the override order:
* MasterFiles < local property < System property.
* Means that: if a property is defined in local and in on of a master property file, the local one will be returned.
* If defined in local and in system property, system property will be returned.
* If the property cannot be found, return null.
* Parameters:
* @param key The key of the property to find, or null if not found.
*/
public String getProperty(String key){
//The result of the method.
String result = null;
//We will parse the master properties, looking for the searched properties.
if (masterPropertiesList!=null){
for(Iterator masterFilesIterator = masterPropertiesList.iterator(); masterFilesIterator.hasNext();){
UserProperties masterFile = (UserProperties) masterFilesIterator.next();
String masterResult = masterFile.getProperty(key);
if(masterResult != null){
result = masterResult;
}
}
}
//Then, try to read it from property file, using super method.
String localResult = super.getProperty(key);
//If non null, override the previously found one.
if(localResult != null){
result = localResult;
}
//And then, read from system properties, to permits properties overrides throught command line.
String systemProperty = System.getProperty(key);
//If non null, override the previously found one.
if(systemProperty != null){
result = systemProperty;
}
//return the result
return result;
}
/**
* Writes properties to the file defined in the constructor.
*/
public void write2() throws FileNotFoundException, IOException {
FileOutputStream fos = new FileOutputStream(_file);
store(fos, null);
}
/**
* Gets a string
*
* @param name property name
* @param value the default value
* @return the value
*/
public String getString(String name, String value) {
return getProperty(name, value);
}
/**
* Sets a string
*
* @param name property name
* @param value its new value
*/
public void setString(String name, String value) {
setProperty(name, value);
}
/**
* Gets a boolean
*
* @param name property name
* @param value the default value
* @return the value
*/
public boolean getBoolean(String name, boolean value) {
String res=getProperty(name, value ? "TRUE" : "FALSE");
return res.toUpperCase().equals("TRUE");
}
/**
* Sets a boolean
*
* @param name property name
* @param value the default value
*/
public void setBoolean(String name, boolean value) {
setProperty(name, value ? "TRUE" : "FALSE");
}
/**
* Gets a double
*
* @param name property name
* @param value the default value
* @return the value
*/
public double getDouble(String name, double value) {
String res = getProperty(name);
if (res == null) {
return value;
}
try {
double rd = Double.parseDouble(res);
return rd;
} catch (NumberFormatException nfe) {
return value;
}
}
/**
* Gets a float
*
* @param name property name
* @param value the default value
* @return the value
*/
public float getFloat(String name, float value) {
return (float) getDouble(name, value);
}
/**
* Sets a double
*
* @param name property name
* @param value its new value
*/
public void setDouble(String name, double value) {
setProperty(name, String.valueOf(value));
}
/**
* Sets a float
*
* @param name property name
* @param value its new value
*/
public void setFloat(String name, float value) {
setProperty(name, String.valueOf(value));
}
/**
* Gets a long
*
* @param name property name
* @param value the default value
* @return the value
*/
public long getLong(String name, long value) {
String res = getProperty(name);
if (res == null) {
return value;
}
try {
return Long.decode(res).longValue();
} catch (NumberFormatException nfe) {
return value;
}
}
/**
* Gets an int
*
* @param name property name
* @param value the default value
* @return the value
*/
public int getInt(String name, int value) {
return (int) getLong(name, value);
}
/**
* Gets a short
*
* @param name property name
* @param value the default value
* @return the value
*/
public short getShort(String name, short value) {
return (short) getLong(name, value);
}
/**
* Gets a byte
*
* @param name property name
* @param value the default value
* @return the value
*/
public byte getByte(String name, byte value) {
return (byte) getLong(name, value);
}
/**
* Sets a long
*
* @param name property name
* @param value its new value
*/
public void setLong(String name, long value) {
setProperty(name, String.valueOf(value));
}
/**
* Sets an int
*
* @param name property name
* @param value its new value
*/
public void setInt(String name, int value) {
setProperty(name, String.valueOf(value));
}
/**
* Sets a short
*
* @param name property name
* @param value its new value
*/
public void setShort(String name, short value) {
setProperty(name, String.valueOf(value));
}
/**
* Sets a byte
*
* @param name property name
* @param value its new value
*/
public void setByte(String name, byte value) {
setProperty(name, String.valueOf(value));
}
/**
* Calls the <tt>Hashtable</tt> method <code>put</code>. Provided for
* parallelism with the <tt>getProperty</tt> method. Enforces use of
* strings for property keys and values. The value returned is the
* result of the <tt>Hashtable</tt> call to <code>put</code>.
*
* @param key the key to be placed into this property list.
* @param value the value corresponding to <tt>key</tt>.
* @return the previous value of the specified key in this property
* list, or <code>null</code> if it did not have one.
*/
public synchronized Object setProperty(String key, String value) {
Object result = null;
String localValue = super.getProperty(key);
if(localValue != null){
//If local value was previously defined. Set it.
result = put(key, value);
}else{
//Else check in the Master files.
String masterProperties = null;
//We will parse the master properties, looking for the searched properties.
if (masterPropertiesList!=null){
Iterator masterFilesIterator = masterPropertiesList.iterator();
while( masterFilesIterator.hasNext() && masterProperties == null){
UserProperties masterFile = (UserProperties) masterFilesIterator.next();
masterProperties = masterFile.getProperty(key);
}
}
//Now check the result.
if(masterProperties != null && masterProperties.equals(value)){
//We want to set the same properties/value as the master one.
//Do nothing, master properties will be retrieved with getProperty.
//Use master property as result.
result = masterProperties;
}else if(masterProperties == null || (masterProperties != null && !masterProperties.equals(value))){
//This is a newly added property or
//We have locally override the master value.
// set it as local.
_infoList.add(new Info(Info.KEY, key));
result = put(key, value);
}
}
return result;
}
/**
* Add a comment at the end of the properties sequence. Do nothing if
* properties file already exists.
* The comment will appear prefixed by '#' in the properties file.
*
* @param comment the comment line
*/
public void addComment(String comment) {
if (_propertiesLoadedFromFile) return;
comment = comment.trim();
if (comment.length() == 0) {
comment = "#";
} else if (comment.charAt(0) != '#') {
comment = "# " + comment;
}
_infoList.add(new Info(Info.COMMENT, comment));
}
/**
* Add a new empty line at the end of the properties sequence. Do nothing if
* properties file already exists.
* Useful to separate group of properties.
*/
public void addNewLine() {
if (_propertiesLoadedFromFile) return;
_infoList.add(new Info(Info.EMPTY_LINE, null));
}
protected static final String keyValueSeparators = "=: \t\r\n\f";
protected static final String strictKeyValueSeparators = "=:";
protected static final String specialSaveChars = "=: \t\r\n\f#!";
protected static final String whiteSpaceChars = " \t\r\n\f";
/**
* Reads a property sequence from the input stream.
* The stream is assumed to be using the ISO 8859-1 character encoding.
* Comments, property definitions with value, and empty line are recorded,
* keeping them in order.
*
* @param inStream the input stream.
* @exception IOException if an error occurred when reading from the
* input stream.
*/
public synchronized void load(InputStream inStream) throws IOException {
_infoList = new LinkedList();
masterPropertiesList = new ArrayList();
BufferedReader in =
new BufferedReader(new InputStreamReader(inStream, "8859_1"));
while (true) {
// Get next line
String line = in.readLine();
if (line == null)
return;
if (line.trim().length() == 0) {
// Detect an empty line
_infoList.add(new Info(Info.EMPTY_LINE, null));
}
if (line.length() > 0) {
// Continue lines that end in slashes if they are not comments
char firstChar = line.charAt(0);
if ((firstChar == '#') || (firstChar == '!')) {
// This is a comment line
_infoList.add(new Info(Info.COMMENT, line));
} else if(line.startsWith(Info.IMPORT_FILE_PREFIX)){
//This is a master line.
//retrieve master fileName.
StringTokenizer stk = new StringTokenizer(line);
if(stk.countTokens() > 1){
//The first token is the Info.IMPORT_FILE_PREFIX, throw it away.
stk.nextToken();
//The next one should be the master file.
String masterFile = stk.nextToken();
//Create a UserProperties on it.
UserProperties up = new UserProperties(new File(masterFile));
try{
up.read2();
//Add it to master list.
masterPropertiesList.add(up);
}catch(IOException e){
System.err.println("Failed to read imported file:"+masterFile);
}
//Add it to info list, to keep write it in property file.
_infoList.add(new Info(Info.MASTER_LINE, masterFile));
}//Else bad number of tokens, do nothing.
} else {
// This is a property line (key + value)
while (continueLine(line)) {
String nextLine = in.readLine();
if (nextLine == null)
nextLine = "";
String loppedLine =
line.substring(0, line.length() - 1);
// Advance beyond whitespace on new line
int startIndex = 0;
for (startIndex = 0;
startIndex < nextLine.length();
startIndex++)
if (whiteSpaceChars
.indexOf(nextLine.charAt(startIndex))
== -1)
break;
nextLine =
nextLine.substring(startIndex, nextLine.length());
line = new String(loppedLine + nextLine);
}
// Find start of key
int len = line.length();
int keyStart;
for (keyStart = 0; keyStart < len; keyStart++) {
if (whiteSpaceChars.indexOf(line.charAt(keyStart))
== -1)
break;
}
// Blank lines are ignored
if (keyStart == len)
continue;
// Find separation between key and value
int separatorIndex;
for (separatorIndex = keyStart;
separatorIndex < len;
separatorIndex++) {
char currentChar = line.charAt(separatorIndex);
if (currentChar == '\\')
separatorIndex++;
else if (keyValueSeparators.indexOf(currentChar) != -1)
break;
}
// Skip over whitespace after key if any
int valueIndex;
for (valueIndex = separatorIndex;
valueIndex < len;
valueIndex++)
if (whiteSpaceChars.indexOf(line.charAt(valueIndex))
== -1)
break;
// Skip over one non whitespace key value separators if any
if (valueIndex < len)
if (strictKeyValueSeparators
.indexOf(line.charAt(valueIndex))
!= -1)
valueIndex++;
// Skip over white space after other separators if any
while (valueIndex < len) {
if (whiteSpaceChars.indexOf(line.charAt(valueIndex))
== -1)
break;
valueIndex++;
}
String key = line.substring(keyStart, separatorIndex);
String value =
(separatorIndex < len)
? line.substring(valueIndex, len)
: "";
// Convert then store key and value
key = loadConvert(key);
value = loadConvert(value);
put(key, value);
_infoList.add(new Info(Info.KEY, key));
}
}
}
}
/*
* Returns true if the given line is a line that must
* be appended to the next line
*/
protected boolean continueLine(String line) {
int slashCount = 0;
int index = line.length() - 1;
while ((index >= 0) && (line.charAt(index--) == '\\'))
slashCount++;
return (slashCount % 2 == 1);
}
/*
* Converts encoded \uxxxx to unicode chars
* and changes special saved chars to their original forms
*/
protected String loadConvert(String theString) {
char aChar;
int len = theString.length();
StringBuffer outBuffer = new StringBuffer(len);
for (int x = 0; x < len;) {
aChar = theString.charAt(x++);
if (aChar == '\\') {
aChar = theString.charAt(x++);
if (aChar == 'u') {
// Read the xxxx
int value = 0;
for (int i = 0; i < 4; i++) {
aChar = theString.charAt(x++);
switch (aChar) {
case '0' :
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
case '6' :
case '7' :
case '8' :
case '9' :
value = (value << 4) + aChar - '0';
break;
case 'a' :
case 'b' :
case 'c' :
case 'd' :
case 'e' :
case 'f' :
value = (value << 4) + 10 + aChar - 'a';
break;
case 'A' :
case 'B' :
case 'C' :
case 'D' :
case 'E' :
case 'F' :
value = (value << 4) + 10 + aChar - 'A';
break;
default :
throw new IllegalArgumentException(
"Malformed \\uxxxx encoding.");
}
}
outBuffer.append((char) value);
} else {
if (aChar == 't')
aChar = '\t';
else if (aChar == 'r')
aChar = '\r';
else if (aChar == 'n')
aChar = '\n';
else if (aChar == 'f')
aChar = '\f';
outBuffer.append(aChar);
}
} else
outBuffer.append(aChar);
}
return outBuffer.toString();
}
/*
* Converts unicodes to encoded \uxxxx
* and writes out any of the characters in specialSaveChars
* with a preceding slash
*/
protected String saveConvert(String theString, boolean escapeSpace) {
int len = theString.length();
StringBuffer outBuffer = new StringBuffer(len * 2);
for (int x = 0; x < len; x++) {
char aChar = theString.charAt(x);
switch (aChar) {
case ' ' :
if (x == 0 || escapeSpace)
outBuffer.append('\\');
outBuffer.append(' ');
break;
case '\\' :
outBuffer.append('\\');
outBuffer.append('\\');
break;
case '\t' :
outBuffer.append('\\');
outBuffer.append('t');
break;
case '\n' :
outBuffer.append('\\');
outBuffer.append('n');
break;
case '\r' :
outBuffer.append('\\');
outBuffer.append('r');
break;
case '\f' :
outBuffer.append('\\');
outBuffer.append('f');
break;
default :
if ((aChar < 0x0020) || (aChar > 0x007e)) {
outBuffer.append('\\');
outBuffer.append('u');
outBuffer.append(toHex((aChar >> 12) & 0xF));
outBuffer.append(toHex((aChar >> 8) & 0xF));
outBuffer.append(toHex((aChar >> 4) & 0xF));
outBuffer.append(toHex(aChar & 0xF));
} else {
if (specialSaveChars.indexOf(aChar) != -1)
outBuffer.append('\\');
outBuffer.append(aChar);
}
}
}
return outBuffer.toString();
}
/**
* Writes a property sequence in this <code>Properties</code> table to the
* output stream in a format suitable for loading into a
* <code>Properties</code> table using the <code>load</code> method. The
* stream is written using the ISO 8859-1 character encoding.
* Comments, property definitions with value, and empty line are written,
* keeping them in creating order.
*
* @param out an output stream.
* @param header Not used in this method
* @exception IOException if writing this property list to the
* specified output stream throws an <tt>IOException</tt>.
* @exception ClassCastException if this <code>Properties</code>
* object contains any keys or values
* that are not <code>Strings</code>.
* @exception NullPointerException if <code>out</code> is null.
*/
public synchronized void store(OutputStream out, String header)
throws IOException {
BufferedWriter awriter;
awriter = new BufferedWriter(new OutputStreamWriter(out, "8859_1"));
for (int i = 0; i < _infoList.size(); i++) {
Info info = (Info) _infoList.get(i);
if (info._type == Info.COMMENT) {
// Add a comment line
awriter.write(info._value);
awriter.newLine();
} else if (info._type == Info.EMPTY_LINE) {
// Add a empty line
awriter.newLine();
} else if (info._type == Info.MASTER_LINE) {
awriter.write(Info.IMPORT_FILE_PREFIX+ " "+info._value);
awriter.newLine();
} else {
// Add a property (key + value) line
String key = info._value;
String val = (String) get(key);
key = saveConvert(key, true);
/* No need to escape embedded and trailing spaces for value,
* hence pass false to flag.
*/
val = saveConvert(val, false);
awriter.write(key + "=" + val);
awriter.newLine();
}
}
awriter.flush();
}
/**
* Convert a nibble to a hex character
*
* @param nibble the nibble to convert.
*/
protected static char toHex(int nibble) {
return hexDigit[(nibble & 0xF)];
}
/** A table of hex digits */
protected static final char[] hexDigit =
{
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F' };
}