Java tutorial
/* $Id: XProperties.java,v 1.10 2007/12/04 13:22:01 mke Exp $ * $Revision: 1.10 $ * $Date: 2007/12/04 13:22:01 $ * $Author: mke $ * * The SB Util Library. * Copyright (C) 2005-2007 The State and University Library of Denmark * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package dk.statsbiblioteket.util; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.StreamException; import com.thoughtworks.xstream.io.xml.DomDriver; import dk.statsbiblioteket.util.qa.QAInfo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.*; import java.net.URL; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; /** * Human Readable Properties with XStream backend. * * An extension of java.util.properties, that uses XStream from Thoughtworks to * create human readable property files for many different objects. The * {@link #store}, {@link #load}, {@link #storeToXML} and {@link #loadFromXML} * calls are overwritten. * * XProperties provides storage in human readable XML. * It also allows for easy storage of arbitrary complex objects, within the * bounds of XStream. * See <a href="http://xstream.codehaus.org">xstream.codehaus.org</a> * for details. * * The properties can be overridden from the command line, by setting the * environment in the following manner: * <code>-DXProperty:foo=bar -DXProperty:mysubproperty/foo=bar</code> * Slashes separates sub properties. {@code -?[0-9]+} are stored as integers, * {@code -?[0-9]+\.[0-9]+} are stored as doubles, {@code true} and * {@code false} are stored as booleans, all other values are stored as * {@link String}s. * * <h2>File Format</h2> * FIXME: The file format is currently broken. * See * <a href="https://gforge.statsbiblioteket.dk/tracker/index.php?&aid=1189">bug 1189</a> */ @QAInfo(state = QAInfo.State.QA_NEEDED, level = QAInfo.Level.NORMAL) public class XProperties extends Properties implements Converter { public Log log = LogFactory.getLog(XProperties.class); /** * The xstream instance used for storing properties in human readable * format */ private XStream xstream; /** * The default path for storing properties resource. Defaults to current * directory. */ protected File defaultPath = new File("."); /** * The current name of the resource */ protected String resourceName; /** * Initialise a set of properties with defaults. * * @param defaults Default properties. */ public XProperties(XProperties defaults) { super(defaults); //when writing HR properties, write entries as "entry" rather than //this classname xstream = new XStream(new DomDriver()); xstream.alias("entry", XPropertiesEntry.class); xstream.alias("xproperties", XProperties.class); xstream.registerConverter(this); } /** * Initialise a set of properties with defaults, and load properties from * resourceName. If resourceName is not found, initialise an empty set of * resources, but use the name as filename on next call to {@link #store}. * See {@link #load(String, boolean, boolean)} for details about how * resources are loaded. * * @param resourceName Name of resource. * @param defaults Default properties. * @throws InvalidPropertiesException if resourceName is found but not * well-formed. * @throws IOException if resourceName is found but has io errors while * reading. */ public XProperties(String resourceName, XProperties defaults) throws InvalidPropertiesException, IOException { this(defaults); load(resourceName, true, false); fillFromEnvironment(); } /** * Initialise an empty set of properties. */ public XProperties() { this((XProperties) null); fillFromEnvironment(); } /** * Initialise a set of properties, and load properties from resourceName. * If resourceName is not found, initialise an empty set of resources, but * use the name as filename on next call to {@link #store}. * * @param resourceName Name of resource. * @throws InvalidPropertiesException if resourceName is found but not * well-formed. * @throws IOException if resourceName is found but has io errors while * reading. * @see #load(String, boolean, boolean) for details about how resources are * loaded. */ public XProperties(String resourceName) throws InvalidPropertiesException, IOException { this(resourceName, null); } /** * Initialize a set of properties. If fetchProperties is true, the * properties is filled with stored values. If false, a clean properties * is created. * * @param resourceName the name of the resource for these properties * @param fetchProperties true if the properties should be fetched * @throws InvalidPropertiesException if resourceName is found but not * well-formed. * @throws IOException if resourceName is found but has io errors while * reading. */ public XProperties(String resourceName, boolean fetchProperties) throws InvalidPropertiesException, IOException { this(); if (fetchProperties) { load(resourceName, true, false); } else { this.resourceName = resourceName; } fillFromEnvironment(); } /** * If createNew is true, create a blank set of properties. If createNew is * false, attempt to retrieve the properties from resource. If the attempt * fails because the resource could not be localed and failOnNotfound is * true, throw an IOException. * * @param resource the resource to use as basis for the XProperties. * @param createNew if true, a clean XProperties (filled from the * environment) is created with resource as its name. * @param failOnNotFound if true and createNew is false and the resource is * not available, an IOException will be thrown. * @throws InvalidPropertiesException if the resource was not proper * XProperties XML. * @throws IOException if the resource was invalid or if the resource did * not exist and failOnNotFound was true. */ public XProperties(String resource, boolean createNew, boolean failOnNotFound) throws InvalidPropertiesException, IOException { if (createNew) { this.resourceName = resource; } else { load(resource, !failOnNotFound, false); } fillFromEnvironment(); } /** * Construct a XProperties, potentially without letting the environment * override any properties. * * @param override if true, let the environment override properties. */ public XProperties(boolean override) { this((XProperties) null); if (override) { fillFromEnvironment(); } } /** * Fills the current XProperties with key-value pairs specified in the * environment. See the XProperties class documentation for syntax. */ protected void fillFromEnvironment() { String HEADER = "XProperty:"; for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) { assert entry.getKey() instanceof String; assert entry.getValue() instanceof String; String key = (String) entry.getKey(); if (key.startsWith(HEADER)) { parseAndPutObject(key.substring(HEADER.length(), key.length()), (String) entry.getValue()); } } } private Pattern intPattern = Pattern.compile("-?[0-9]+"); private Pattern doublePattern = Pattern.compile("-?[0-9]+\\.[0-9]+"); protected void parseAndPutObject(String key, String value) { if (key.contains("/")) { log.trace("Encountered sub property with key '" + key + "'"); String[] tokens = key.split("/", 2); XProperties sub; if (contains(tokens[0])) { sub = getSubProperty(tokens[0]); } else { sub = new XProperties(false); put(tokens[0], sub); } sub.parseAndPutObject(tokens[1], value); } else if ("true".equals(value)) { log.trace("Adding boolean true to properties"); put(key, true); } else if ("false".equals(value)) { log.trace("Adding boolean false to properties"); put(key, false); } else if (intPattern.matcher(value).matches()) { log.trace("Adding integer '" + value + "' to properties"); try { put(key, Integer.parseInt(value)); } catch (NumberFormatException e) { log.warn("Could not parse the expected integer '" + value + "'. Defaulting to String", e); put(key, value); } } else if (doublePattern.matcher(value).matches()) { log.trace("Adding double '" + value + "' to properties"); try { put(key, Double.parseDouble(value)); } catch (NumberFormatException e) { log.warn("Could not parse the expected double '" + value + "'. Defaulting to String", e); put(key, value); } } else { log.trace("Adding String '" + value + "' to properties"); put(key, value); } } /** * Convenience method for reading an object from the properties. Searches * default properties if not found in these properties. * * @param key The key for the object property. * @return the object corresponding the provided key * @throws NullPointerException if the value for the key could not be * located. */ public Object getObject(String key) throws NullPointerException { Object val = super.get(key); // No fallback to properties if (val == null && defaults != null) { val = ((XProperties) defaults).getObject(key); } if (val == null) { throw new NullPointerException("Could not locate value for '" + key + "'"); } return val; } /** * Wrapper for the get method inherited from HashTable, in order to mark * it as deprecated. The get from HashTable doesn't fall back to the * defaults, if the object isn't present in the properties. * * @param key the key for the object property * @return an object, or null, if it doesn't exist in the first layer * of properties * @deprecated replaced by {link #getObject(String)} which supports defaults */ @Deprecated public Object get(Object key) { return super.get(key); } /** * Convenience method for reading a string from the properties. Searches * default properties if not found in these properties. * * @param key The key for the string property. * @return A String. * @throws ClassCastException if key does not denote a string value * @throws NullPointerException if the value for the key could not be * located. */ public String getString(String key) { return (String) getObject(key); } /** * Convenience method for reading an integer from the properties. Searches * default properties if not found in these properties. * * @param key The key for the integer property. * @return An integer. * @throws ClassCastException if key does not denote an integer value * @throws NullPointerException if the value for the key could not be * located. */ public int getInteger(String key) { return (Integer) getObject(key); } /** * Convenience method for reading a boolean from the properties. Searches * default properties if not found in these properties. * * @param key The key for the boolean property. * @return A boolean. * @throws ClassCastException if key does not denote a boolean value * @throws NullPointerException if the value for the key could not be * located. */ public boolean getBoolean(String key) { return (Boolean) getObject(key); } /** * Convenience method for reading a double from the properties. Searches * default properties if not found in these properties. * * @param key The key for the double property. * @return A double. * @throws ClassCastException if key does not denote a double value */ public double getDouble(String key) { return (Double) getObject(key); } /** * Convenience method for reading a character from the properties. Searches * default properties if not found in these properties. * * @param key The key for the character property. * @return A character. * @throws ClassCastException if key does not denote a character value * @throws NullPointerException if the value for the key could not be * located. */ public char getChar(String key) { return (Character) getObject(key); } /** * Convenience method for getting a sub-property from the properties. * Searches default properties if not found in these properties. * * @param key The key for the sub-property. * @return a XProperty. * @throws ClassCastException if key does not denote a XProperty value * @throws NullPointerException if the value for the key could not be * located. */ public XProperties getSubProperty(String key) { return (XProperties) getObject(key); } /** * Set default value for a key. This will be overridden by any values set * with {@link java.util.Hashtable#put}, setproperty, or {@link #load} calls. * Useful for setting default property values for a class. * * @param key The property key. * @param value The property value. * @return The previous default value for this object, if any. */ public Object putDefault(String key, Object value) { if (defaults == null) { defaults = new XProperties(); } return defaults.put(key, value); } /** * Promotes properties in the defaults set to properties in the main set. * This is useful to make store() store an xml file with default settings * for users to change at will. * Note that this will recursively populate defaults up through the default * settings. */ public void populateWithDefaults() { if (defaults != null) { ((XProperties) defaults).populateWithDefaults(); for (Map.Entry<Object, Object> entry : defaults.entrySet()) { if (!containsKey(entry.getKey())) { put(entry.getKey(), entry.getValue()); } } } } /** * Prints this property list out to the specified output stream. * This method is useful for debugging. * * @param out an output stream. */ public void list(PrintStream out) { try { store(out, "Current properties"); } catch (IOException e) { out.println("Unlistable properties."); e.printStackTrace(out); } } /** * Prints this property list out to the specified output stream. * This method is useful for debugging. * * @param out an output writer. */ public void list(PrintWriter out) { try { store(out); } catch (IOException e) { out.println("Unlistable properties."); e.printStackTrace(out); } } /** * Provides access to the XStream instance. This is useful for extending * XStream handling for specific objects. If the extension is for a generic * object, consider changing the XProperties, so that other projects can * benefit from the work. * * @return The XStream instance used by the XProperties instance when * storing and loading objects. */ public XStream getXStream() { return xstream; } /** * Get the default path for storing and reading properties. * * @return default path for storing and reading properties. */ public File getDefaultPath() { return defaultPath; } /** * Set the default path for storing and reading properties. * Defaults to current directory if not set. Must be an existing directory. * * @param defaultPath The path, * @throws IllegalArgumentException if defaultPath is not an existing, * writable directory. */ public synchronized void setDefaultPath(File defaultPath) { if (!defaultPath.isDirectory() || !defaultPath.canWrite()) { throw new IllegalArgumentException("defaultPath must be an existing" + " writable directory"); } this.defaultPath = defaultPath; } /** * Assign all available attributes from the given properties. This is a * shallow assignment, so sub properties will be shared. * * @param properties another set of propeerties to assign to this. */ public synchronized void assignFrom(XProperties properties) { log.trace("Clearing"); clear(); log.trace("Assigning " + properties.size() + " pairs"); for (Map.Entry<Object, Object> entry : properties.entrySet()) { put(entry.getKey(), entry.getValue()); } log.trace("Assigning other attributes"); defaultPath = properties.defaultPath; resourceName = properties.resourceName; log.trace("Finished assigning"); } /** * Fetch stored properties from the given resource. This uses * ContextClassLoader, so as long as the * resource is in the CLASSPATH, it should be accessible. * * The resource is searched for in the following order: * - If the resource can be found in defaultPath, use this * - Else if the resource can be found in current directory use this * - Else if the resource can be found in classpath, use this * * @param resourceName the name of the resource containing the properties * @param ignoreNonExisting don't throw an exception if the resource can not * be found * @param ignoreMalformed don't throw an exception if the resource is * malformed * @throws InvalidPropertiesException if the ignores aren't true and the * resource contains unknown classes * @throws IOException thrown if there are IO errors during read OR if * resource is not found and ignoreNonExisting is false. */ public synchronized void load(String resourceName, boolean ignoreNonExisting, boolean ignoreMalformed) throws InvalidPropertiesException, IOException { log.trace(String.format("Loading resource %s with" + " ignoreNonExisting %s and" + " ignoreMalformed %s", resourceName, ignoreNonExisting, ignoreMalformed)); this.resourceName = resourceName; clear(); InputStream instream; log.trace("Locating resource '" + resourceName + "'"); ClassLoader loader = Thread.currentThread().getContextClassLoader(); URL resourceURL = loader.getResource(resourceName); if (new File(defaultPath, resourceName).isFile()) { instream = new FileInputStream(new File(defaultPath, resourceName)); } else if (new File(resourceName).isFile()) { instream = new FileInputStream(new File(resourceName)); } else if (resourceURL != null) { instream = resourceURL.openStream(); } else { String msg = String.format("Could not locate resource %s", resourceName); if (ignoreNonExisting) { log.debug(msg + ", ignoring"); return; } log.warn(msg); throw new FileNotFoundException(msg); } log.trace("Loading resource"); if (ignoreMalformed) { try { load(instream); } catch (InvalidPropertiesException e) { //Ignore } } else { load(instream); } log.debug(String.format("Properties resource \"%s\" loaded", resourceName)); } /** * Fetch stored properties from the given stream. * * @param instream Input stream to read properties from. * @throws InvalidPropertiesException if the stream contains unknown * or invalid classes * @throws IOException thrown if there are IO errors during read OR if * resource is not found and ignoreNonExisting is false. */ public void load(InputStream instream) throws IOException { InputStreamReader inreader = new InputStreamReader(instream); ObjectInputStream objectIn; try { objectIn = xstream.createObjectInputStream(inreader); Object o = objectIn.readObject(); XProperties properties = (XProperties) o; for (Map.Entry<Object, Object> entries : properties.getEntries()) { put(entries.getKey(), entries.getValue()); } // ArrayList<XPropertiesEntry> entries // = (ArrayList<XPropertiesEntry>) objectIn.readObject(); // for (XPropertiesEntry entry : entries) { // put(entry.key, entry.value); // } objectIn.close(); } catch (ClassNotFoundException excl) { clear(); String msg = String.format("ClassNotFoundException loading properties from" + " resource %s", resourceName); log.warn(msg); throw new InvalidPropertiesException(msg, excl); } catch (StreamException exst) { clear(); String msg = String.format("StreamException loading properties from" + " resource %s", resourceName); log.warn(msg); throw new InvalidPropertiesException(msg, exst); } catch (ClassCastException e) { throw new InvalidPropertiesException( "Input stream does not look " + "like a valid XProperties " + "file", e); } finally { instream.close(); } } /** * Fetch stored properties from the given stream. * * @param instream Input stream to read properties from. * @throws InvalidPropertiesException if the stream contains unknown * or invalid classes * @throws IOException thrown if there are IO errors during read OR if * resource is not found and ignoreNonExisting is false. */ public void loadFromXML(InputStream instream) throws IOException { load(instream); } /** * Store the properties as the resource given in previous load(resourceName) * or store(resourceName) calls. * Equivalent to calling store(resourceName). * * @throws IOException if the resource could not be stored * @throws IllegalStateException if no calls have set a resourceName */ public void store() throws IOException { if (resourceName == null) { throw new IllegalStateException("No resource name has been set"); } store(resourceName); } /** * Store the properties as the resource named by resourceName. * The ressource is first searched for in the same order as for load(). If * found and in writable directory, it is replaced. * If it does not exist, a new file is created at the default directory, * with the name resourceName. * Note: Trying to replace a resource placed anywhere else than a * write-enabled directory, will give an IOException. * * @param resourceName the name of the resource to store the properties to * @throws IOException if the resource could not be stored */ public synchronized void store(String resourceName) throws IOException { log.trace(String.format("Storing properties to resource %s", resourceName)); log.trace("Locating resource"); ClassLoader loader = Thread.currentThread().getContextClassLoader(); URL resourceURL = loader.getResource(resourceName); File f; if (new File(defaultPath, resourceName).isFile()) { f = new File(defaultPath, resourceName); } else if (new File(resourceName).isFile()) { f = new File(resourceName); } else if (resourceURL != null && new File(resourceURL.getPath()).exists()) { f = new File(resourceURL.getPath()); } else { // TODO: Does not handle "C:\whatever if (resourceName.startsWith(File.separator)) { f = new File(resourceName); } else { f = new File(defaultPath, resourceName); } } FileOutputStream filestream = new FileOutputStream(f); store(filestream, null); } /** * Store the properties in the given outputStream. * * @param out the stream to store the properties to * @param comments ignored - only for compatibility with * java.util.Properties. * @throws IOException if the resource could not be stored */ public synchronized void store(OutputStream out, String comments) throws IOException { store(new PrintWriter(out)); } /** * Store the properties in the given outputStream. * * @param out the stream to store the properties to * @param comments ignored - only for compatibility with * java.util.Properties. * @throws IOException if the resource could not be stored */ public synchronized void storeToXML(OutputStream out, String comments) throws IOException { store(new PrintWriter(out)); } /** * Store the properties in the given outputStream. * * @param out the stream to store the properties to * @param comments ignored - only for compatibility with * java.util.Properties. * @param encoding Encoding to store in * @throws IOException if the resource could not be stored */ public synchronized void storeToXML(OutputStream out, String comments, String encoding) throws IOException { store(new OutputStreamWriter(out, encoding)); } /** * Store the properties as in the given writer. * * @param out the writer to store the properties to * @throws IOException if the resource could not be stored */ private void store(Writer out) throws IOException { log.debug("Storing resource"); // xmlns="http://statsbiblioteket.dk/dtd/XProperties.dtd" ObjectOutputStream objectOut = xstream.createObjectOutputStream(out, "xstream"); objectOut.writeObject(this); objectOut.close(); } protected Set<Map.Entry<Object, Object>> getEntries() { return entrySet(); } public boolean canConvert(Class aClass) { return XProperties.class.equals(aClass); } public void marshal(Object xpropertiesObject, HierarchicalStreamWriter writer, MarshallingContext context) { for (Map.Entry<Object, Object> entry : ((XProperties) xpropertiesObject).getEntries()) { String key = (String) entry.getKey(); Object value = entry.getValue(); XPropertiesEntry hrentry = new XPropertiesEntry(key, value); writer.startNode("entry"); context.convertAnother(hrentry); writer.endNode(); } } public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { XProperties properties = new XProperties(); while (reader.hasMoreChildren()) { reader.moveDown(); XPropertiesEntry entry = (XPropertiesEntry) context.convertAnother(properties, XPropertiesEntry.class); reader.moveUp(); properties.put(entry.key, entry.value); } return properties; } /** * Helper-class for XProperties pair. Used for persistence. */ private static class XPropertiesEntry { /** * First part of the property-pair. */ String key; /** * Second part of the property-pair. */ Object value; /** * A simple key-value pair, used for incapsulation, when properties are * stored. * * @param key first part of the pair * @param value second part of the pair */ XPropertiesEntry(String key, Object value) { this.key = key; this.value = value; } } }