UberProperties.java Source Code

  • UberProperties Documentation and Examples
  • UberProperties Javadoc
    /*
     * Copyright (C) 2002-2011 Stephen Ostermiller
     * http://ostermiller.org/contact.pl?regarding=Java+Utilities
     *
     * Copyright (C) 2003 Carlo Magnaghi <software at tecnosoft dot net>
     *
     * This program is free software; you can redistribute it and/or modify
     * it under the terms of the GNU 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.  See the
     * GNU General Public License for more details.
     *
     * See LICENSE.txt for details.
     */
    package com.Ostermiller.util;
    
    import java.io.*;
    import java.util.*;
    
    /**
     * The Properties class represents a persistent set of properties. The
     * Properties can be saved to a stream or loaded from a stream. Each key and
     * its corresponding value in the property list is a string.
     * More information about this class is available from <a target="_top" href=
     * "http://ostermiller.org/utils/UberProperties.html">ostermiller.org</a>.
     * <p>
     * A property list can contain another property list as its "defaults"; this
     * second property list is searched if the property key is not found in the
     * original property list.
     * <p>
     * When saving properties to a stream or loading them from a stream, the ISO
     * 8859-1 character encoding is used. For characters that cannot be directly
     * represented in this encoding, Unicode escapes are used; however, only a
     * single 'u' character is allowed in an escape sequence. The native2ascii tool
     * can be used to convert property files to and from other character encodings.
     * <p>
     * Unlike the java.util.Properties, UberProperties does not inherit from
     * java.util.Hashtable, so Objects other than strings cannot be stored in it.
     * Also, comments from a files are preserved, and there can be several
     * properties for a given name.
     * <p>
     * This class is not synchronized, so it should not be used in a
     * multi-threaded environment without external synchronization.
     * <p>
     * The file format that UberProperties uses is as follows:
     * <blockquote>
     * The file is assumed to be using the ISO 8859-1 character encoding. All of the
     * comment lines (starting with a '#' or '!') at the beginning of the file before the
     * first line that is not a comment, are the comment associated with the file.
     * After that, each comment will be associated with the next property.  If there
     * is more than one property with the same name, the first comment will be the
     * only one that is loaded.
     * <p>
     * Every property occupies one line of the input stream. Each line is terminated
     * by a line terminator (\n or \r or \r\n).
     * <p>
     * A line that contains only whitespace or whose first non-whitespace character
     * is an ASCII # or ! is ignored (thus, # or ! indicate comment lines).
     * <p>
     * Every line other than a blank line or a comment line describes one property
     * to be added to the table (except that if a line ends with \, then the
     * following line, if it exists, is treated as a continuation line,
     * as described below). The key consists of all the characters in the line
     * starting with the first non-whitespace character and up to, but not
     * including, the first ASCII =, :, or whitespace character. All of the key
     * termination characters may be included in the key by preceding them with a \.
     * Any whitespace after the key is skipped; if the first non-whitespace
     * character after the key is = or :, then it is ignored and any whitespace
     * characters after it are also skipped. All remaining characters on the line
     * become part of the associated element string. Within the element string, the
     * ASCII escape sequences \t, \n, \r, \\, \", \', \ (a backslash and a space),
     * and \\uxxxx are recognized and converted to single characters. Moreover, if
     * the last character on the line is \, then the next line is treated as a
     * continuation of the current line; the \ and line terminator are simply
     * discarded, and any leading whitespace characters on the continuation line are
     * also discarded and are not part of the element string.
     * <p>
     * As an example, each of the following four lines specifies the key "Truth"
     * and the associated element value "Beauty":<br>
     * <pre>Truth = Beauty
     * 	   Truth:Beauty
     *   Truth			:Beauty</pre>
     * <p>
     * As another example, the following three lines specify a single property:<br>
     * <pre>fruits				apple, banana, pear, \
     *                                cantaloupe, watermelon, \
     *                                kiwi, mango</pre>
     * <p>
     * The key is "fruits" and the associated element is:<br>
     * "apple,&nbsp;banana,&nbsp;pear,&nbsp;cantaloupe,&nbsp;watermelon,&nbsp;kiwi,&nbsp;mango"<br>
     * Note that a space appears before each \ so that a space will appear after
     * each comma in the final result; the \, line terminator, and leading
     * whitespace on the continuation line are merely discarded and are not replaced
     * by one or more other characters.
     * <p>
     * As a third example, the line:<br>
     * cheeses<br>
     * specifies that the key is "cheeses" and the associated element is the empty
     * string.
     * </blockquote>
     *
     * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
     * @since ostermillerutils 1.00.00
     */
    public class UberProperties {
    
    	/**
    	 * A hash map that contains all the properties.
    	 * This should never be null, but may be empty.
    	 * This should hold objects of type Property.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	private HashMap<String,Property> properties = new HashMap<String,Property>();
    
    	/**
    	 * Comment for this set of properties.
    	 * This may be either null or empty.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	private String comment = null;
    
    	/**
    	 * The object type that goes in the HashMap.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	private class Property {
    
    		/**
    		 * List of values for this property.
    		 * This should never be null or empty.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		private ArrayList<String> list;
    
    		/**
    		 * Comment for this set of properties.
    		 * This may be either null or empty.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		private String comment = null;
    
    		/**
    		 * Set the comment associated with this property.
    		 *
    		 * @param comment the comment for this property, or null to clear.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public void setComment(String comment){
    			this.comment = comment;
    		}
    
    		/**
    		 * Get the comment associated with this property.
    		 *
    		 * @return comment for this property, or null if none is set.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public String getComment(){
    			return this.comment;
    		}
    
    		/**
    		 * Construct a new property with the given value.
    		 *
    		 * @param value initial value for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public Property(String value){
    			list = new ArrayList<String>(1);
    			add(value);
    		}
    
    		/**
    		 * Construct a new property with the given values.
    		 *
    		 * @param values initial values for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public Property(String[] values){
    			list = new ArrayList<String>(values.length);
    			add(values);
    		}
    
    		/**
    		 * Set this property to have this single value.
    		 *
    		 * @param value lone value for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public void set(String value){
    			list.clear();
    			add(value);
    		}
    
    		/**
    		 * Set this property to have only these values.
    		 *
    		 * @param values lone values for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public void set(String[] values){
    			list.clear();
    			add(values);
    		}
    
    		/**
    		 * Add this value to the list of values for this property.
    		 *
    		 * @param value another value for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public void add(String value){
    			list.add(value);
    		}
    
    		/**
    		 * Add these values to the list of values for this property.
    		 *
    		 * @param values other values for this property.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public void add(String[] values){
    			list.ensureCapacity(list.size() + values.length);
    			for (String element: values) {
    				add(element);
    			}
    		}
    
    		/**
    		 * Get the last value for this property.
    		 *
    		 * @return the last value.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public String getValue(){
    			return list.get(list.size() - 1);
    		}
    
    		/**
    		 * Get all the values for this property.
    		 *
    		 * @return a list of all the values.
    		 *
    		 * @since ostermillerutils 1.00.00
    		 */
    		public String[] getValues(){
    			return list.toArray(new String[list.size()]);
    		}
    	}
    
    	/**
    	 * Creates an empty property list with no default values.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public UberProperties(){
    		// Create empty properties
    	}
    
    	/**
    	 * Creates an empty property list with the specified defaults.
    	 *
    	 * @param defaults the defaults.
    	 * @throws NullPointerException if defaults is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public UberProperties(UberProperties defaults){
    		merge(defaults);
    	}
    
    	/**
    	 * Put all the properties from the defaults in this.
    	 * Calling this from a constructor will clone (deep)
    	 * the default properties.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	private void merge(UberProperties defaults){
    		setComment(defaults.getComment());
    		String[] names = defaults.propertyNames();
    		for (String element: names) {
    			setProperties(element, defaults.getProperties(element));
    			setComment(element, defaults.getComment(element));
    		}
    	}
    
    	/**
    	 * Test to see if a property with the given name exists.
    	 *
    	 * @param name the name of the property.
    	 * @return true if the property existed and was removed, false if it did not exist.
    	 * @throws NullPointerException if name is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public boolean contains(String name){
    		if (name == null) throw new NullPointerException();
    		return properties.containsKey(name);
    	}
    
    	/**
    	 * Remove any property with the given name.
    	 *
    	 * @param name the name of the property.
    	 * @return true if the property existed and was removed, false if it did not exist.
    	 * @throws NullPointerException if name is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public boolean remove(String name){
    		if (!contains(name)) return false;
    		properties.remove(name);
    		return true;
    	}
    
    	/**
    	 * Replaces all properties of the given name with
    	 * a single property with the given value.
    	 *
    	 * @param name the name of the property.
    	 * @param value the value of the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void setProperty(String name, String value){
    		if (name == null) throw new NullPointerException();
    		if (value == null){
    			properties.remove(name);
    		} else {
    			Property property;
    			if (properties.containsKey(name)){
    				property = properties.get(name);
    				property.set(value);
    			} else {
    				property = new Property(value);
    				properties.put(name, property);
    			}
    		}
    	}
    
    	/**
    	 * Replaces all properties of the given name with
    	 * properties with the given values.
    	 *
    	 * @param name the name of the property.
    	 * @param values for the property.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if values is null.
    	 * @throws IllegalArgumentException if values is empty.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void setProperties(String name, String[] values){
    		if (name == null) throw new NullPointerException();
    		if (values.length == 0) throw new IllegalArgumentException();
    		Property property;
    		if (properties.containsKey(name)){
    			property = properties.get(name);
    			property.set(values);
    		} else {
    			property = new Property(values);
    			properties.put(name, property);
    		}
    	}
    
    	/**
    	 * Replaces all properties of the given name with
    	 * a single property with the given value.
    	 *
    	 * @param name the name of the property.
    	 * @param value the value of the property or null to remove it.
    	 * @param comment the comment for the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if comment is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void setProperty(String name, String value, String comment){
    		if (name == null) throw new NullPointerException();
    		if (value == null){
    			properties.remove(name);
    		} else {
    			setProperty(name, value);
    			setComment(name, comment);
    		}
    	}
    
    	/**
    	 * Replaces all properties of the given name with
    	 * properties with the given values.
    	 *
    	 * @param name the name of the property.
    	 * @param values value of the property.
    	 * @param comment the comment for the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if values is null.
    	 * @throws IllegalArgumentException if values is empty.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void setProperties(String name, String[] values, String comment){
    		if (name == null) throw new NullPointerException();
    		if (values.length == 0) throw new IllegalArgumentException();
    		setProperties(name, values);
    		setComment(name, comment);
    	}
    
    	/**
    	 * Set the comment on the property of the given name.
    	 * The property must exist before this method is called.
    	 *
    	 * @param name the name of the property.
    	 * @param comment the comment for the property.
    	 * @param comment the comment for the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 * @throws IllegalArgumentException if name is not a known key.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	private void setComment(String name, String comment){
    		if (name == null) throw new NullPointerException();
    		if (!properties.containsKey(name)) throw new IllegalArgumentException();
    		(properties.get(name)).setComment(comment);
    	}
    
    	/**
    	 * Adds a value to the list of properties with the
    	 * given name.
    	 *
    	 * @param name the name of the property.
    	 * @param value the values for the property, or null to remove.
    	 * @param comment the comment for the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if value is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void addProperty(String name, String value, String comment){
    		if (name == null) throw new NullPointerException();
    		if (value == null) throw new NullPointerException();
    		addProperty(name, value);
    		setComment(name, comment);
    	}
    
    	/**
    	 * Adds the values to the list of properties with the
    	 * given name.
    	 *
    	 * @param name the name of the property.
    	 * @param values the values for the property.
    	 * @param comment the comment for the property, or null to remove it.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if values is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void addProperties(String name, String[] values, String comment){
    		if (name == null) throw new NullPointerException();
    		if (values == null) throw new NullPointerException();
    		addProperties(name, values);
    		setComment(name, comment);
    	}
    
    	/**
    	 * Adds a value to the list of properties with the
    	 * given name.
    	 *
    	 * @param name the name of the property.
    	 * @param value the values for the property.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if value is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void addProperty(String name, String value){
    		if (name == null) throw new NullPointerException();
    		if (value == null) throw new NullPointerException();
    		Property property;
    		if (properties.containsKey(name)){
    			property = properties.get(name);
    			property.add(value);
    		} else {
    			property = new Property(value);
    			properties.put(name, property);
    		}
    	}
    
    	/**
    	 * Adds the values to the list of properties with the
    	 * given name.
    	 *
    	 * @param name the name of the property.
    	 * @param values the values for the property.
    	 * @throws NullPointerException if name is null.
    	 * @throws NullPointerException if values is null.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void addProperties(String name, String[] values){
    		if (name == null) throw new NullPointerException();
    		if (values == null) throw new NullPointerException();
    		Property property;
    		if (properties.containsKey(name)){
    			property = properties.get(name);
    			property.add(values);
    		} else {
    			property = new Property(values);
    			properties.put(name, property);
    		}
    	}
    
    	private static int hexDigitValue(char c){
    		switch (c){
    			case '0': return 0;
    			case '1': return 1;
    			case '2': return 2;
    			case '3': return 3;
    			case '4': return 4;
    			case '5': return 5;
    			case '6': return 6;
    			case '7': return 7;
    			case '8': return 8;
    			case '9': return 9;
    			case 'a': case 'A': return 10;
    			case 'b': case 'B': return 11;
    			case 'c': case 'C': return 12;
    			case 'd': case 'D': return 13;
    			case 'e': case 'E': return 14;
    			case 'f': case 'F': return 15;
    			default: return -1;
    		}
    	}
    
    	private static String unescape(String s){
    		StringBuffer sb = new StringBuffer(s.length());
    		for(int i=0; i<s.length(); i++){
    			char c = s.charAt(i);
    			if (c == '\\'){
    				i++;
    				if (i < s.length()){
    					c = s.charAt(i);
    					switch (c){
    						case 'n': {
    							sb.append('\n');
    						} break;
    						case 'r': {
    							sb.append('\r');
    						} break;
    						case 't': {
    								sb.append('\t');
    						} break;
    						case 'f': {
    							sb.append('\f');
    						} break;
    						case 'u': {
    							boolean foundUnicode = false;
    							if (i+4 < s.length()){
    								int unicodeValue = 0;
    								for (int j = 3; unicodeValue >= 0 && j >= 0; j--){
    									int val = hexDigitValue(s.charAt(i+(4-j)));
    									if (val == -1){
    										unicodeValue = -1;
    									} else {
    										unicodeValue |= (val << (j << 2));
    									}
    								}
    								if (unicodeValue >= 0) {
    									i+=4;
    										foundUnicode = true;
    										sb.append((char)unicodeValue);
    								}
    							}
    							if (!foundUnicode) sb.append(c);
    						} break;
    						default: {
    							sb.append(c);
    						} break;
    					}
    				}
    			} else {
    				sb.append(c);
    			}
    		}
    		return sb.toString();
    	}
    
    	/**
    	 * Load these properties from a user file with default properties
    	 * from a system resource.
    	 * <p>
    	 * Example:
    	 * <pre>load(
    	 *     new String(){".java","tld","company","package","component.properties"}
    		 *     "tld/company/package/component.properties",
    	 * )</pre>
    	 * This will load the properties file relative to the classpath as the
    	 * defaults and the file &lt;%userhome%&gt;/.java/tld/company/package/component.properties
    	 * if the file exists.  The .java directory is recommended as it is a common,
    	 * possibly hidden, directory in the users home directory commonly used by
    	 * Java programs.
    	 *
    	 * This method is meant to be used with the save(String systemResource) method
    	 * which will save modified properties back to the user directory.
    	 *
    	 * @param userFile array of Strings representing a path and file name relative to the user home directory.
    	 * @param systemResource name relative to classpath of default properties, or null to ignore.
    	 * @throws IOException if an error occurs when reading.
    	 * @throws NullPointerException if userFile is null.
    	 * @throws IllegalArgumentException if userFile is empty.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void load(String[] userFile, String systemResource) throws IOException {
    		int length = userFile.length;
    		if (userFile.length == 0) throw new IllegalArgumentException();
    		InputStream in = ClassLoader.getSystemResourceAsStream(systemResource);
    		if (in==null) throw new FileNotFoundException(systemResource);
    		if (systemResource != null) load(in);
    		File f = new File(System.getProperty("user.home"));
    		for (int i=0; f.exists() && i<length; i++){
    			f = new File(f, userFile[i]);
    		}
    		if (f.exists()) load(new FileInputStream(f));
    	}
    
    	/**
    	 * Add the properties from the input stream to this
    	 * UberProperties.
    	 * <p>
    	 * The input stream in parsed as ISO-8859-1 (Latin 1) text.
    	 *
    	 * @param in InputStream containing properties.
    	 * @param add whether parameters should add to parameters with the same name or replace them.
    	 * @throws IOException if an error occurs when reading.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void load(InputStream in, boolean add) throws IOException {
    		load(new InputStreamReader(in, "ISO-8859-1"), add);
    	}
    
    	/**
    	 * Add the properties from the reader to this
    	 * UberProperties.
    	 *
    	 * @param reader Reader containing properties.
    	 * @param add whether parameters should add to parameters with the same name or replace them.
    	 * @throws IOException if an error occurs when reading.
    	 *
    	 * @since ostermillerutils 1.07.01
    	 */
    	public void load(Reader reader, boolean add) throws IOException {
    		PropertiesLexer lex = new PropertiesLexer(reader);
    		PropertiesToken t;
    		HashSet<String> names = new HashSet<String>();
    		StringBuffer comment = new StringBuffer();
    		boolean foundComment = false;
    		StringBuffer name = new StringBuffer();
    		StringBuffer value = new StringBuffer();
    		boolean atStart = true;
    		String lastSeparator = null;
    		while ((t = lex.getNextToken()) != null){
    			if (t.getID() == PropertiesToken.COMMENT){
    				int start = 1;
    				String commentText = t.getContents();
    				if (commentText.startsWith("# ")) start = 2;
    				comment.append(commentText.substring(start, commentText.length()));
    				comment.append("\n");
    				lex.getNextToken();
    				foundComment = true;
    			} else if (t.getID() == PropertiesToken.NAME){
    				if (atStart){
    					setComment(comment.toString());
    					comment.setLength(0);
    					atStart = false;
    				}
    				name.append(t.getContents());
    			} else if (t.getID() == PropertiesToken.VALUE){
    				if (atStart){
    					setComment(comment.toString());
    					comment.setLength(0);
    					atStart = false;
    				}
    				value.append(t.getContents());
    			} else if (t.getID() == PropertiesToken.SEPARATOR){
    				lastSeparator = t.getContents();
    			} else if (t.getID() == PropertiesToken.END_LINE_WHITE_SPACE){
    				if (atStart){
    					setComment(comment.toString());
    					comment.setLength(0);
    					atStart = false;
    				}
    				String stName = unescape(name.toString());
    				String stValue = unescape(value.toString());
    				if (lastSeparator != null || stName.length() > 0 || stValue.length() > 0 ){
    					if (add || names.contains(stName)){
    						addProperty(stName, stValue);
    					} else {
    						setProperty(stName, stValue);
    						names.add(stName);
    					}
    					if (foundComment) setComment(stName, unescape(comment.toString()));
    				}
    				comment.setLength(0);
    				name.setLength(0);
    				value.setLength(0);
    				foundComment = false;
    				lastSeparator = null;
    			}
    		}
    	}
    
    	/**
    	 * Add the properties from the input stream to this
    	 * UberProperties.
    	 * <p>
    	 * The input stream in parsed as ISO-8859-1 (Latin 1) text.
    	 * <p>
    	 * Properties that are found replace any properties that
    	 * were there before.
    	 *
    	 * @param in InputStream containing properties.
    	 * @throws IOException if an error occurs when reading.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void load(InputStream in) throws IOException {
    		load(in, false);
    	}
    
    
    	/**
    	 * Add the properties from the reader to this
    	 * UberProperties.
    	 * <p>
    	 * Properties that are found replace any properties that
    	 * were there before.
    	 *
    	 * @param in Reader containing properties.
    	 * @throws IOException if an error occurs when reading.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void load(Reader in) throws IOException {
    		load(in, false);
    	}
    
    	/**
    	 * Save these properties from a user file.
    	 * <p>
    	 * Example:
    	 * <pre>save(
    	 *     new String(){"tld","company","package","component.properties"}
    	 * )</pre>
    	 * This will save the properties file relative to the user directory:
    	 * &lt;%userhome%&gt;/tld/company/package/component.properties
    	 * Directories will be created as needed.
    	 *
    	 * @param userFile array of Strings representing a path and file name relative to the user home directory.
    	 * @throws IOException if an error occurs when reading.
    	 * @throws NullPointerException if userFile is null.
    	 * @throws IllegalArgumentException if userFile is empty.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void save(String[] userFile) throws IOException {
    		int length = userFile.length;
    		if (length == 0) throw new IllegalArgumentException();
    		File f = new File(System.getProperty("user.home"));
    		for (int i=0; i<length; i++){
    			f = new File(f, userFile[i]);
    			if (i == length - 2 && !f.exists()){
    				f.mkdirs();
    			}
    		}
    		OutputStream out = new FileOutputStream(f);
    		save(out);
    		out.close();
    	}
    
    	/**
    	 * Save these properties to the given writer.  The properties are saved without escaping
    	 * high byte characters with backslash u style escape sequences.  This method is intended
    	 * for those who wish to store properties in UTF-8 (or other high byte capable) text file.
    	 * When saved via this method, the properties can only be read back using the read(Reader)
    	 * method, as the read(InputStream) and read(File) methods assume ISO-8859-1 encoded bytes.
    	 * <p>
    	 * Note that this method in NOT compatible with the java.util.Properties class.  This method
    	 * will store unicode characters un-escaped.
    	 *
    	 * @param out writer
    	 * @throws IOException if an input/output error occurs
    	 * @since ostermillerutils 1.07.01
    	 */
    	public void save(Writer out) throws IOException {
    		writeComment(out, comment);
    		out.write('\n');
    		String[] names = propertyNames();
    		Arrays.sort(names);
    		for (String element: names) {
    			writeComment(out, getComment(element));
    			String[] values = getProperties(element);
    			for (String element2: values) {
    				writeProperty(out, element, element2);
    			}
    		}
    		out.flush();
    	}
    
    	/**
    	 * Save these properties to the given stream.
    	 *
    	 * @param out OutputStream to which these properties should be written.
    	 * @throws IOException if an error occurs when writing.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void save(OutputStream out) throws IOException {
    		writeComment(out, comment);
    		out.write('\n');
    		String[] names = propertyNames();
    		Arrays.sort(names);
    		for (String element: names) {
    			writeComment(out, getComment(element));
    			String[] values = getProperties(element);
    			for (String element2: values) {
    				writeProperty(out, element, element2);
    			}
    		}
    		out.flush();
    	}
    
    	private static void writeProperty(OutputStream out, String name, String value) throws IOException {
    		writeEscapedISO88591(out, name, TYPE_NAME);
    		out.write('=');
    		writeEscapedISO88591(out, value, TYPE_VALUE);
    		out.write('\n');
    	}
    
    
    	private static void writeProperty(Writer out, String name, String value) throws IOException {
    		writeEscapedText(out, name, TYPE_NAME);
    		out.write('=');
    		writeEscapedText(out, value, TYPE_VALUE);
    		out.write('\n');
    	}
    
    	private static void writeComment(OutputStream out, String comment) throws IOException {
    		if (comment != null){
    			java.util.StringTokenizer tok = new java.util.StringTokenizer(comment, "\r\n");
    			while (tok.hasMoreTokens()){
    				out.write('#');
    				out.write(' ');
    				writeEscapedISO88591(out, tok.nextToken(), TYPE_COMMENT);
    				out.write('\n');
    			}
    		}
    	}
    
    	private static void writeComment(Writer out, String comment) throws IOException {
    		if (comment != null){
    			java.util.StringTokenizer tok = new java.util.StringTokenizer(comment, "\r\n");
    			while (tok.hasMoreTokens()){
    				out.write('#');
    				out.write(' ');
    				writeEscapedText(out, tok.nextToken(), TYPE_COMMENT);
    				out.write('\n');
    			}
    		}
    	}
    
    	private static final int TYPE_COMMENT = 0;
    	private static final int TYPE_NAME = 1;
    	private static final int TYPE_VALUE = 2;
    
    	private static void writeEscapedISO88591(OutputStream out, String s, int type) throws IOException {
    		for (int i=0; i<s.length(); i++){
    			int c = s.charAt(i);
    			if (c < 0x100){
    				boolean escape = false;
    				if (c == '\r' || c == '\n' || c == '\\'){
    					escape = true;
    				} else if (c == ' ' || c == '\t' || c == '\f'){
    					if(type == TYPE_NAME){
    						escape = true;
    					} else if (type == TYPE_VALUE && (i==0 || i == s.length() - 1)){
    						escape = true;
    					}
    				} else if (type == TYPE_NAME && (c == '=' || c == ':')){
    					escape = true;
    				}
    				if (escape){
    					switch (c){
    						case '\n': {
    							switch (type){
    								case TYPE_COMMENT: {
    									out.write('\n');
    									out.write('#');
    									out.write(' ');
    								} break;
    								case TYPE_NAME: {
    									out.write('\\');
    									out.write('n');
    									out.write('\\');
    									out.write('\n');
    									out.write('\t');
    								} break;
    								case TYPE_VALUE: {
    									out.write('\\');
    									out.write('n');
    									out.write('\\');
    									out.write('\n');
    									out.write('\t');
    									out.write('\t');
    								} break;
    							}
    						} break;
    						case '\\': {
    							out.write('\\');
    							out.write('\\');
    						} break;
    						case '\r': {
    							out.write('\\');
    							out.write('r');
    						} break;
    						case '\t': {
    							out.write('\\');
    							out.write('t');
    						} break;
    						case '\f': {
    							out.write('\\');
    							out.write('f');
    						} break;
    						default : {
    							out.write('\\');
    							out.write((byte)c);
    						} break;
    					}
    				} else {
    					out.write((byte)c);
    				}
    			} else {
    				out.write('\\');
    				out.write('u');
    				out.write(StringHelper.prepad(Integer.toHexString(c), 4, '0').getBytes("ISO-8859-1"));
    			}
    		}
    	}
    	private static void writeEscapedText(Writer out, String s, int type) throws IOException {
    		for (int i=0; i<s.length(); i++){
    			int c = s.charAt(i);
    			boolean escape = false;
    			if (c == '\r' || c == '\n' || c == '\\'){
    				escape = true;
    			} else if (c == ' ' || c == '\t' || c == '\f'){
    				if(type == TYPE_NAME){
    					escape = true;
    				} else if (type == TYPE_VALUE && (i==0 || i == s.length() - 1)){
    					escape = true;
    				}
    			} else if (type == TYPE_NAME && (c == '=' || c == ':')){
    				escape = true;
    			}
    			if (escape){
    				switch (c){
    					case '\n': {
    						switch (type){
    							case TYPE_COMMENT: {
    								out.write("\n# ");
    							} break;
    							case TYPE_NAME: {
    								out.write("\\n\\\n\t");
    							} break;
    							case TYPE_VALUE: {
    								out.write("\\n\\\n\t\t");
    							} break;
    						}
    					} break;
    					case '\\': {
    						out.write("\\\\");
    					} break;
    					case '\r': {
    						out.write("\\r");
    					} break;
    					case '\t': {
    						out.write("\\t");
    					} break;
    					case '\f': {
    						out.write("\\f");
    					} break;
    					default : {
    						out.write('\\');
    						out.write(c);
    					} break;
    				}
    			} else {
    				out.write(c);
    			}
    		}
    	}
    	/**
    	 * Get the first property with the given name.
    	 * If the property is not specified in this UberProperties
    	 * but it is in the default UberProperties, the default is
    	 * used.  If no default is found, null is returned.
    	 *
    	 * @param name Parameter name
    	 * @return the first value of this property, or null if the property does not exist.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String getProperty(String name){
    		String value = null;
    		if (properties.containsKey(name)){
    			value = (properties.get(name)).getValue();
    		}
    		return value;
    	}
    
    	/**
    	 * Get the first property with the given name.
    	 * If the property is not specified in this UberProperties
    	 * but it is in the default UberProperties, the default
    	 * UberProperties is consulted, otherwise, the supplied
    	 * default is used.
    	 *
    	 * @param name Parameter name
    	 * @param defaultValue Value to use when property not present
    	 * @return the first value of this property.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String getProperty(String name, String defaultValue){
    		String value = getProperty(name);
    		if (value == null) value = defaultValue;
    		return value;
    	}
    
    	/**
    	 * Get the values for a property.
    	 * Properties returned in the same order in which
    	 * they were added.
    	 * <p>
    	 * If the property is not specified in this UberProperties
    	 * but it is in the default UberProperties, the default is
    	 * used.  If no default is found, null is returned.
    	 *
    	 * @param name Parameter name
    	 * @return all the values associated with the given key, or null if the property does not exist.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String[] getProperties(String name){
    		String[] values = null;
    		if (properties.containsKey(name)){
    			values = (properties.get(name)).getValues();
    		}
    		return values;
    	}
    
    	/**
    	 * Convert this UberProperties into a java.util.Properties.
    	 *
    	 * @return java properties object.
    	 * @since ostermillerutils 1.07.01
    	 */
    	public Properties toJavaUtilProperties(){
    		Properties p = new Properties();
    		for(String name: propertyNames()){
    			p.put(name, getProperty(name));
    		}
    		return p;
    	}
    
    	/**
    	 * Get the values for a property.
    	 * Properties returned in the same order in which
    	 * they were added.
    	 * <p>
    	 * If the property is not specified in this UberProperties
    	 * but it is in the default UberProperties, the default
    	 * UberProperties is consulted, otherwise, the supplied
    	 * defaults are used.
    	 *
    	 * @param name Parameter name
    	 * @param defaultValues Values to use when property not present
    	 * @return all the values associated with the given key, or null if the property does not exist.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String[] getProperties(String name, String[] defaultValues){
    		String[] values = getProperties(name);
    		if (values == null) values = defaultValues;
    		return values;
    	}
    
    	/**
    	 * Get the comment associated with this property.
    	 * <p>
    	 * If the property is not specified in this UberProperties
    	 * but it is in the default UberProperties, the default is
    	 * used.  If no default is found, null is returned.
    	 *
    	 * @param name Parameter name
    	 * @return the comment for this property, or null if there is no comment or the property does not exist.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String getComment(String name){
    		String comment = null;
    		if (properties.containsKey(name)){
    			comment = (properties.get(name)).getComment();
    		}
    		return comment;
    	}
    
    	/**
    	 * Returns an enumeration of all the keys in this property list, including
    	 * distinct keys in the default property list if a key of the same name has
    	 * not already been found from the main properties list.
    	 *
    	 * @return an enumeration of all the keys in this property list, including the keys in the default property list.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String[] propertyNames(){
    		Set<String> names = properties.keySet();
    		return names.toArray(new String[names.size()]);
    	}
    
    	/**
    	 * Set the comment associated with this set of properties.
    	 *
    	 * @param comment the comment for entire set of properties, or null to clear.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public void setComment(String comment){
    		this.comment = comment;
    	}
    
    	/**
    	 * Get the comment associated with this set of properties.
    	 *
    	 * @return comment for entire set of properties, or null if there is no comment.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public String getComment(){
    		return this.comment;
    	}
    
    	/**
    	 * Get the number of unique names for properties stored
    	 * in this UberProperties.
    	 *
    	 * @return number of names.
    	 *
    	 * @since ostermillerutils 1.00.00
    	 */
    	public int getPropertyNameCount(){
    		return properties.keySet().size();
    	}
    
    	/**
    	 * Save these properties to a string.
    	 *
    	 * @return Serialized String version of these properties.
    	 *
    	 * @since ostermillerutils 1.02.23
    	 */
    	@Override public String toString(){
    		ByteArrayOutputStream out = new ByteArrayOutputStream();
    		try {
    			this.save(out);
    		} catch (IOException iox){
    			throw new Error("IO constructed on memory, this shouldn't happen.", iox);
    		}
    		String s = null;
    		try {
    			s = new String(out.toByteArray(), "ISO-8859-1");
    		} catch (UnsupportedEncodingException uee){
    			throw new Error("ISO-8859-1 should be recognized.", uee);
    		}
    		return s;
    	}
    
    	/**
    	 * Liberally parse the property value as an integer.
    	 * Uses StringHelper.parseInteger() to parse the integer.
    	 *
    	 * @param name the property name
    	 * @return the parsed property value, or null if the property does not exist or cannot be parsed.
    	 * @see com.Ostermiller.util.StringHelper#parseInteger(String)
    	 * @since ostermillerutils 1.07.01
    	 */
    	public Integer getIntegerProperty(String name){
    		return StringHelper.parseInteger(getProperty(name));
    	}
    
    	/**
    	 * Liberally parse the property value as an integer.
    	 * Uses StringHelper.parseInt() to parse the integer.
    	 *
    	 * @param name the property name
    	 * @param defaultValue default value to be return in case of error
    	 * @return the parsed property value, or the default if the property does not exist or cannot be parsed.
    	 * @see com.Ostermiller.util.StringHelper#parseInt(String, int)
    	 * @since ostermillerutils 1.07.01
    	 */
    	public int getIntProperty(String name, int defaultValue){
    		return StringHelper.parseInt(getProperty(name), defaultValue);
    	}
    
    	/**
    	 * Liberally parse the property value as a boolean.
    	 * Uses StringHelper.parseBoolean() to parse the boolean.
    	 *
    	 * @param name the property name
    	 * @return the parsed property value, or null if the property does not exist or cannot be parsed.
    	 * @see com.Ostermiller.util.StringHelper#parseBoolean(String)
    	 * @since ostermillerutils 1.07.01
    	 */
    	public Boolean getBooleanProperty(String name){
    		return StringHelper.parseBoolean(getProperty(name));
    	}
    
    	/**
    	 * Liberally parse the property value as a boolean.
    	 * Uses StringHelper.parseBoolean() to parse the boolean.
    	 *
    	 * @param name the property name
    	 * @param defaultValue default value to be return in case of error
    	 * @return the parsed property value, or the default if the property does not exist or cannot be parsed.
    	 * @see com.Ostermiller.util.StringHelper#parseBoolean(String, boolean)
    	 * @since ostermillerutils 1.07.01
    	 */
    	public boolean getBooleanProperty(String name, boolean defaultValue){
    		return StringHelper.parseBoolean(getProperty(name), defaultValue);
    	}
    }