/**
* Copyright (C) 2006 Mark Bednarczyk, Sly Technologies, Inc.
*
* 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
*
*/
package com.slytechs.utils.properties;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* <P>A little more advanced properties where any type of object can be stored
* as long as it can convert to/from a string for storage. Also listeners can be
* registered on properties when property values change.</P>
*
* <P>Default properties can and should be defined although not strictly required.</P>
* <P> You start out with a blank properties object, if you want to load properties
* just set the SmartProperties.FILENAME property to the filename you wish to read
* and SmartProperites will do the rest.</P>
*
* @author Mark Bednarczyk
* @author Sly Technologies, Inc.
*/
public class SmartProperties {
public static final String D_FILENAME = "properties.cfg";
/**
* File property for this object. The file property specifies the
* filename of where the properties are stored. If null properties are
* not written to disk. By changing this property a new file is instantly
* created and all existing properties stored immediately.
*/
public static final String FILENAME = "properties.filename";
public static final Log logger = LogFactory.getLog(SmartProperties.class);
private static SmartProperties globaltProperties;
private Exception exception = null;
/**
* Creates a new instance of the provided object.
*
* @param c The class to invoke "valueOf(String)" method on.
* @return An instance of the object as generated by valueOf(String) invocation.
* @throws InstantiationException
*/
@SuppressWarnings("unchecked")
private static <T> T invokeValueOf(Class<T> c, String propertyValue) throws Exception {
Object res = null;
java.lang.reflect.Method valueOf;
try {
valueOf = c.getDeclaredMethod("valueOf", String.class);
} catch (NoSuchMethodException e) {
/*
* If Class.valueOf(String) doesn't exist, try a direct constructor
* Class<T>(String)
*/
Constructor constructor = c.getDeclaredConstructor(String.class);
return (T)constructor.newInstance(propertyValue);
}
res = valueOf.invoke(/* This is a static method obj=null */null, propertyValue);
return (T) res;
}
private SmartProperties defaults;
private Map<String, String> descriptions;
private Map<String,Integer> lineNumber;
private PropertyChangeSupport listeners;
/**
* File from and to which the properties are read and written to.
*/
private File propertiesFile;
private Map<String, Boolean> storeFlags;
private Map<String, List<String>> values;
private VetoableChangeSupport vetoables;
private Map<String, Object> cache;
private Boolean DEFAULT_STORE_FLAG = false;
public String PROPERTY_MODIFIED = "modified";
public String PROPERTY_EXCEPTION = "exception";
public String PROPERTY_NEW = "new property";
public String PROPERTY_REMOVE = "removed property";
private boolean modified = false;
/**
* Initialize properties without defaults.
*/
public SmartProperties() {
values = new HashMap<String, List<String>>();
descriptions = new HashMap<String, String>();
listeners = new PropertyChangeSupport(this);
vetoables = new VetoableChangeSupport(this);
storeFlags = new HashMap<String, Boolean>();
lineNumber = new HashMap<String,Integer>();
cache = new HashMap<String, Object>();
initProperties();
setModified(false);
}
/**
* <P>
* Initialize properties with defaults.
* </P><P>
* If a new property is set then the default if overriden and removed.
* </P>
*
* @param defaults the defaults for these properties.
*/
public SmartProperties(SmartProperties defaults) {
this();
this.defaults = defaults;
}
/**
* Add a listener for a all property changes.
*
* @param propertyName name of the property to listen on for changes.
* @param listener the listener that will be notified when property
* changes.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (listeners == null) {
listeners = new PropertyChangeSupport(this);
}
listeners.addPropertyChangeListener(listener);
}
/**
* Add a listener for a named property change.
*
* @param propertyName name of the property to listen on for changes.
* @param listener the listener that will be notified when property
* changes.
*/
public void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listeners == null) {
listeners = new PropertyChangeSupport(this);
}
listeners.addPropertyChangeListener(propertyName, listener);
}
/**
* Add a vetoable listener for a named property change. The listener can
* throw a veto exception to interrupt and disallow the property to be
* set to this value.
*
* @param propertyName name of the property to listen on for changes.
* @param listener the listener that will be notified when property
* changes.
*/
public void addVetoableChangeListener(String propertyName,
VetoableChangeListener listener) {
if (listeners == null) {
listeners = new PropertyChangeSupport(this);
}
vetoables.addVetoableChangeListener(propertyName, listener);
}
/**
* Return a boolean value from the object.
*
* @param name name of the property to lookup.
* @return "boolean" value.
* @param def default value to return when the lookup fails.
* @exception Exception a runtime exception is thrown if object can
* not be converted to indicated primitive.
*/
public boolean booleanValue(String name, boolean def) {
return get(Boolean.class, name, def);
}
/**
* Return a float value from the object.
*
* @param name name of the property to lookup.
* @return "float" value.
* @param def default value to return when the lookup fails.
* @exception Exception a runtime exception is thrown if object can
* not be converted to indicated primitive.
*/
public float floatValue(String name, float def) {
return get(Float.class, name, def);
}
public synchronized <T>T get(Class<T> c, String key, T defaultValue) {
List<String> value = values.get(key);
if (value != null && value.isEmpty() == false) {
try {
return invokeValueOf(c, value.get(0));
} catch (Exception e) {
setException(e);
return defaultValue;
}
} else if (defaults != null) {
return defaults.get(c, key, defaultValue);
} else {
return defaultValue;
}
}
@SuppressWarnings("unchecked")
public synchronized <T extends Object> List<T> getList(Class<T> c, String key, List<T> defaultValue) {
List<String> value = values.get(key);
if (value != null && value.isEmpty() == false) {
// TODO: redo for better generic type checking
List<T> cachedValue = (List<T>) cache.get(key);
if (cachedValue == null) {
cachedValue = new ArrayList<T>(value.size());
for (String v: value) {
try {
cachedValue.add(invokeValueOf(c, v));
} catch (Exception e) {
setException(e);
}
}
}
return cachedValue;
} else if (defaults != null) {
return defaults.getList(c, key, defaultValue);
} else {
return defaultValue;
}
}
/**
* Sets the internal property exception and fires off notification
*
* @param e
* Exception being thrown
*/
private void setException(Exception e) {
Exception old = exception;
exception = e;
listeners.firePropertyChange(PROPERTY_EXCEPTION, old, e);
}
/**
* Return property value as object. If a defaults property manager was not
* specified in the constructor the user given default value is returned
* when all lookups fail.
*
* @param name
* property name to lookup
* @param defaultObject
* default value to return when the lookup fails.
* @return String value of the property
* @exception Exception
* a runtime exception is thrown if object can not be
* converted to indicated primitive.
*/
public String get(String name, String defaultObject) {
return get(String.class, name, defaultObject);
}
/**
* Returns the description of the named property.
*
* @param name name of the property to retrieve description for.
*
* @return property description. If property description has not been set
* before, an empty string is returned "".
*/
public synchronized String getDescription(String name) {
String d = (String) descriptions.get(name);
if (d != null) {
return d;
} else if (defaults != null) {
return defaults.getDescription(name);
} else {
return "";
}
}
/**
* Returns the currently set filename where these properites are stored.
*
* @return filename of the properties file.
*/
public String getFilename() {
return stringValue(FILENAME, D_FILENAME);
}
public int getLineno(String propertyName) {
return lineNumber.get(propertyName);
}
/**
* Initialize properties
*/
private void initProperties() {
/*
* define the FILENAME property which will cause the properties to
* be read from the filename
*/
setDescription(FILENAME, "Filename of this properties file");
set(FILENAME, D_FILENAME);
setStore(FILENAME, false); // We dont store this property, for runtime only
/*
* Handle the property change for FILENAME property
*/
addVetoableChangeListener(FILENAME, new VetoableChangeListener() {
/**
* Handle property change event. SmartProperties object monitors certain
* properties which have direct effect on this object. Main property
* being monitored is FILENAME.
*/
@SuppressWarnings("unchecked")
public void vetoableChange(PropertyChangeEvent event)
throws PropertyVetoException {
/* Open file for reading and read properties */
if (event.getPropertyName().equals(FILENAME) == true) {
// TODO: redo for better generic type checking
List<String> fn = (List<String>) event.getNewValue();
propertiesFile = new File(fn.get(0));
try {
if (propertiesFile.canRead() == true) {
FileReader in = new FileReader(propertiesFile);
logger.trace("Reading properties from "+ propertiesFile);
readAllProperties(in);
in.close();
} else {
propertiesFile.createNewFile();
}
} catch (IOException ioe) {
PropertyVetoException e = new PropertyVetoException(
"Error property file=" + fn + ", " + ioe.toString(),
event);
e.initCause(ioe);
throw e;
}
}
}
});
}
/**
* Return an int value from the object.
*
* @param name name of the property to lookup.
* @param def default value to return when the lookup fails.
* @return "int" value.
* @exception Exception a runtime exception is thrown if object can
* not be converted to indicated primitive.
*/
public int intValue(String name, int def) {
return get(Integer.class, name, def);
}
/**
* Load all properties from a file. The filename is defined by property
* FILENAME.
*/
public void load() throws IOException {
readAllProperties(stringValue(FILENAME, D_FILENAME));
}
public void logFatal(Log log, String property, String msg) {
log.error(msg);
log.error("Please correct the configuration file '"
+ getFilename() + "'");
/* Write out the comment ahead of the key */
String c[] = getDescription(property).trim().split("\n");
for (int i = 0; i < c.length; i++) {
log.error("# " + c[i]);
}
log.error(property + "=" + get(property, "") + " (on line number "
+ getLineno(property) + " in '" + getFilename() + "')");
}
/**
* Return a long value from the object.
*
* @param name name of the property to lookup.
* @return "long" value.
* @param default default value to return when the lookup fails.
* @exception Exception a runtime exception is thrown if object can
* not be converted to indicated primitive.
*/
public long longValue(String name, long def) {
return get(Long.class, name, def);
}
/**
* Reads all properties from a reader stream.
*
* @param reader Reader used to reader from its inputstream.
*/
private void readAllProperties(Reader reader) throws IOException {
BufferedReader in = new BufferedReader(reader);
String l;
String d = "";
int lineno = 0;
while ((l = in.readLine()) != null) {
lineno ++;
/* trim off white space and skip empty lines */
if ((l = l.trim()).equals("") == true) {
continue;
}
if (l.startsWith("#") == true) {
if (l.length() >= 2) {
d += l.substring(2); // Skip # and space
} else {
d += l.substring(1); // Skip # and space
}
d += "\n";
} else {
/* Must be a property=value pair */
String[] r = l.split("=", 2); // match n - 1 times
if (r.length != 2) {
continue; // Invalid format
}
/*
* 1st = property name
* 2nd = value
*/
String name = r[0].trim();
String value = r[1].trim();
lineNumber.put(name, lineno);
setDescription(name, d);
setStore(name, true); // All read properties are storable by default
add(name, value);
d = "";
}
}
}
/**
* Reads properties from a named file.
*
* @param filename filename to open up and read properties from.
*/
private void readAllProperties(String filename) throws IOException {
FileReader reader = new FileReader(filename);
readAllProperties(reader);
}
/**
* Sets or replaces the current value for the named property.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
*/
public synchronized <T extends Object> void set(String name, T value) {
add(name, value, true);
}
/**
* Appends or adds another value for the named property.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
*/
public synchronized <T extends Object> void add(String name, T value) {
add(name, value, false);
}
/**
* Sets an opaque value for the named property.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
*/
@SuppressWarnings("unchecked")
private synchronized <T extends Object> void add(String name, T value, boolean clear) {
// TODO: redo for better generic type checking
List<T> old = (List<T>) getList(value.getClass(), name, null);
if (storeFlags.get(name) == null) {
storeFlags.put(name, DEFAULT_STORE_FLAG );
}
cache.remove(name);
if (clear) {
values.remove(name);
}
List<String> v = values.get(name);
if (v == null) {
v = new ArrayList<String>();
values.put(name, v);
listeners.firePropertyChange(PROPERTY_NEW, null, name);
}
v.add(value.toString());
if ((old == null) || (old.equals(value) == false)) {
listeners.firePropertyChange(name, old, getList(value.getClass(), name, null));
}
setModified(true);
}
/**
* <P>
* Set a description for a property.
* Every property can have a description to go along with it. This is
* usefull if you want to have some kind of comment/description above
* a property in a configuration or properties file.
* </P><P>
* Multi-line description separated by newline are usually put on
* a separate line but still prefixed with appropriate character to
* indicate a comment in the config file.
* </P><P>
* Listeners for property change events are not notified of the description
* change. Only if the value of the property changes.
* </P>
*
* @param name name of the property to set the description for.
* @param description description for the property.
*/
public synchronized void setDescription(String name, String description) {
descriptions.put(name, description);
setModified(true);
}
/**
* Set FILENAME property. This causes the object to first check and read
* properties from this file and then store/update changes to it.
*
* @param filename filename of the properties file.
*/
public void setFilename(String filename) {
try {
setWithVeto(FILENAME, filename);
} catch (PropertyVetoException pve) {
logger.warn("Filename on properites vetoed by " + pve.toString());
}
}
/**
* Allows a proproperty to be saved or not saved. If true the property
* will be saved when the store() method is called. Otherwise when set
* to false it will not be.
*
*
* @param store True store the named property otherwise the property will
* not be stored.
*/
public synchronized void setStore(String property, boolean store) {
storeFlags.put(property, new Boolean(store));
}
/**
* Sets an opaque value for the named property with Veto capability.
* A PropertyVetoException can be thrown by the object if the property
* is out of the bounded range for this property type. I.e. if a filename
* property is set and the filename reffers to an invalid filename.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
* @throws PropertyVetoException
*/
public synchronized <T> void setWithVeto(String name, T value) throws PropertyVetoException {
addWithVeto(name, value, true);
}
/**
* Sets an opaque value for the named property with Veto capability.
* A PropertyVetoException can be thrown by the object if the property
* is out of the bounded range for this property type. I.e. if a filename
* property is set and the filename reffers to an invalid filename.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
* @throws PropertyVetoException
*/
public synchronized <T> void addWithVeto(String name, T value) throws PropertyVetoException {
addWithVeto(name, value, false);
}
/**
* Sets an opaque value for the named property with Veto capability.
* A PropertyVetoException can be thrown by the object if the property
* is out of the bounded range for this property type. I.e. if a filename
* property is set and the filename reffers to an invalid filename.
*
* @param name name of the property being modified.
* @param value new opaque value of the named property.
*/
@SuppressWarnings("unchecked")
private synchronized <T> void addWithVeto(String name, T value, boolean clear)
throws PropertyVetoException {
// TODO: redo for better generic type checking
List<T> old = (List<T>) getList(value.getClass(), name, null);
cache.remove(name);
if (clear) {
values.remove(name);
}
boolean newProperty = false;
List<String> v = values.get(name);
if (v == null) {
v = new ArrayList<String>();
values.put(name, v);
newProperty = true;
}
v.add(value.toString());
if ((old == null) || (old.equals(value) == false)) {
try {
vetoables.fireVetoableChange(name, old, getList(value.getClass(), name, null));
} catch (PropertyVetoException e) {
v.remove(v.size() - 1);
cache.remove(name);
throw e;
}
}
if (storeFlags.get(name) == null) {
storeFlags.put(name, DEFAULT_STORE_FLAG );
}
if (newProperty) {
listeners.firePropertyChange(PROPERTY_NEW, null, name);
}
setModified(true);
}
/**
* Store all properties in a file. The filename is defined by property
* FILENAME.
*/
public void store() throws IOException {
logger.trace("propertyFile=" + propertiesFile);
writeAllProperties();
}
/**
* Return a string value from the object.
*
* @param name name of the property to lookup.
* @return "string" value.
* @param def default value to return when the lookup fails.
* @exception Exception a runtime exception is thrown if object can
* not be converted to indicated primitive.
*/
public String stringValue(String name, String def) {
return get(name, def);
}
/**
* Writes all the properties to file
*/
private void writeAllProperties() throws IOException {
if (propertiesFile == null) {
return;
}
setModified(false);
logger.trace("Writting properties");
FileWriter fout = new FileWriter(propertiesFile);
PrintWriter out = new PrintWriter(fout);
Set<String> set = values.keySet();
List<String> list = new LinkedList<String>(set);
Collections.sort(list);
int lineno = 1;
for (String k: list) {
boolean s = (Boolean) storeFlags.get(k);
if (s == false) {
continue;
}
List<String> v = values.get(k);
/* Write out the comment ahead of the key */
String c[] = getDescription(k).trim().split("\n");
if (c.length != 0) {
out.println(""); // Extra empty line
}
for (int i = 0; i < c.length; i++) {
out.println("# " + c[i]);
lineno ++;
}
/* Write out the property=value pair */
for (String sv: v) {
out.println(k + "=" + sv);
lineno ++;
}
}
fout.close();
}
public boolean isModified() {
return modified;
}
public boolean remove(String name) {
values.remove(name);
storeFlags.remove(name);
listeners.firePropertyChange(PROPERTY_REMOVE, name, name);
for (PropertyChangeListener l: listeners.getPropertyChangeListeners(name)) {
listeners.removePropertyChangeListener(name, l);
}
for (VetoableChangeListener l: vetoables.getVetoableChangeListeners(name)) {
vetoables.removeVetoableChangeListener(name, l);
}
return true;
}
public void setModified(boolean modified) {
if (this.modified == modified) {
return;
}
this.modified = modified;
listeners.firePropertyChange(PROPERTY_MODIFIED, !modified, modified);
}
public static SmartProperties getProperites() {
if (globaltProperties == null) {
globaltProperties = new SmartProperties();
}
return globaltProperties;
}
public Exception getException() {
return exception;
}
} /* END OF: SmartProperties */