/* * Copyright (C) 2002-2007 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 COPYING.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, banana, pear, cantaloupe, watermelon, kiwi, 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 <%userhome%>/.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. * * @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 { PropertiesLexer lex = new PropertiesLexer(new InputStreamReader(in, "ISO-8859-1")); 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> * 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); } /** * 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: * <%userhome%>/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 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 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 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")); } } } /** * 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; } /** * 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; } }