java.util.Properties Enhanced
4
This is enhanced version of java.util.Properties that:
Preserves order of Properties read from file if LinkedHashMap used.
Preserves comments in header and scattered throughout file.
It's by no means perfect, but a good start for anyone that once the above features.
Preserves order of Properties read from file if LinkedHashMap used.
Preserves comments in header and scattered throughout file.
It's by no means perfect, but a good start for anyone that once the above features.
/*
* PropertiesMap.java
*
* Created on August 17, 2006, 4:29 PM
*
*/
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
/**
* This class is an enhanced version of java.util.Properties. It can take in any
* map as an input to achieve desired functionality of that map i.e. LinkedHashMap
* preserves order.
* @author hughja01
*/
public class PropertiesMap
{
private static final String KEY_COMMENT_HEADER = "HEADER_6913cjh0-jhgc3-11gda-8cghj6-07002jhg00c9a66";
private static final String LINE_SEPARATOR = "\n\r";
private static final String KEY_VALUE_SEPARATOR = "=:";
private static final String SEPARATOR = "=:\r\n";
private static final String COMMENT = "#!";
private static final String HEX_DIGITS = "0123456789ABCDEF";
private static final String SPECIAL_SAVE_CHARS = "=: \t\r\n\f#!";
private static final char[] hexDigit = {
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
};
private BufferedReader _reader;
private Map _prop;
private Map _defaults;
private LinkedHashMap _comments = new LinkedHashMap();
/**
* Creates a new instance of PropertiesMap using the provided Map. All properties will be stored in
* that map and follows its rules of usage.
*/
public PropertiesMap(Map map)
{
_prop = map;
}
/**
* Creates a new instance of PropertiesMap using java.util.Properties as
* an internal map
*/
public PropertiesMap()
{
_prop = new Properties();
}
/**
* Analogous to java.util.Properties.store.
* Will preserve comments by default.
* @param outputStream
* @throws java.io.IOException
*/
public void store(OutputStream outputStream) throws IOException
{
this.store(outputStream, true);
}
/**
* Analogous to java.util.Properties.
* User can choose to
* retain comments in file, or not. Comments intersparced in file will only be written
* if this class with instantiated with a LinkedHashMap. The header comments do
* not have to meet this requirement.
* @param outputStream
* @param preserveComments
* @throws java.io.IOException
*/
public void store(OutputStream outputStream, boolean preserveComments) throws IOException
{
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outputStream));
Iterator propIt = _prop.keySet().iterator();
Iterator comIt = _comments.keySet().iterator();
Object header = _comments.get(KEY_COMMENT_HEADER);
//Write Header.
if(preserveComments &&
header != null)
out.write(header.toString());
//Write Properties and intersparced comments.
while(propIt.hasNext())
{
Object key = propIt.next();
Object val = _prop.get(key);
Object comVal = null;
//Write comments that are intersparced in file
if(preserveComments &&
_prop instanceof LinkedHashMap &&
_comments.containsKey(key) &&
(comVal = _comments.get(key)) != null)
{
out.write(comVal.toString());
}
out.write(saveConvert(key.toString(), true));
out.write("=");
if(val != null)
{
out.write(saveConvert(val.toString(),false));
}
out.write(System.getProperties().getProperty("line.separator", "\r\n"));
}
out.flush();
}
/*
* Converts unicodes to encoded \uxxxx
* and writes out any of the characters in specialSaveChars
* with a preceding slash
*/
private String saveConvert(String theString, boolean escapeSpace) {
int len = theString.length();
StringBuffer outBuffer = new StringBuffer(len*2);
for(int x=0; x<len; x++) {
char aChar = theString.charAt(x);
switch(aChar) {
case ' ':
if (x == 0 || escapeSpace)
outBuffer.append('\\');
outBuffer.append(' ');
break;
case '\\':outBuffer.append('\\'); outBuffer.append('\\');
break;
case '\t':outBuffer.append('\\'); outBuffer.append('t');
break;
case '\n':outBuffer.append('\\'); outBuffer.append('n');
break;
case '\r':outBuffer.append('\\'); outBuffer.append('r');
break;
case '\f':outBuffer.append('\\'); outBuffer.append('f');
break;
default:
if ((aChar < 0x0020) || (aChar > 0x007e)) {
outBuffer.append('\\');
outBuffer.append('u');
outBuffer.append(toHex((aChar >> 12) & 0xF));
outBuffer.append(toHex((aChar >> 8) & 0xF));
outBuffer.append(toHex((aChar >> 4) & 0xF));
outBuffer.append(toHex( aChar & 0xF));
} else {
if (SPECIAL_SAVE_CHARS.indexOf(aChar) != -1)
outBuffer.append('\\');
outBuffer.append(aChar);
}
}
}
return outBuffer.toString();
}
/**
* Convert a nibble to a hex character
* @param nibble the nibble to convert.
*/
private static char toHex(int nibble) {
return hexDigit[(nibble & 0xF)];
}
/**
* Loads Properties in the same way java.util.Properties does.
* Note: In order to preserve order this class should be instantiated
* with a LinkedHashMap
* @param inputStream
* @throws java.io.IOException
*/
public void load(InputStream inputStream) throws IOException
{
_reader = new BufferedReader(new InputStreamReader(inputStream));
String key = KEY_COMMENT_HEADER;
String comment = processComment();
if(!comment.equals(""))
_comments.put(key, comment);
comment = "";
while (!isAtEndOfStream())
{
// we are at the beginning of a line.
// check whether it is a comment and if it is, preserve it
comment = processComment();
skipWhiteSpace();
if (!isAtEndOfLine())
{
// this line does not consist only of whitespace. the next word is the key
key = readQuotedLine(SEPARATOR, false);
skipWhiteSpace();
// if the next char is a key-value sep, read it and skip the following spaces
int nextChar = peek();
if (nextChar > 0 && KEY_VALUE_SEPARATOR.indexOf((char)nextChar) >=0)
{
_reader.skip(1);
skipWhiteSpace();
}
else
{
key = "";
continue;
}
// Read the value
String value = readQuotedLine(LINE_SEPARATOR, true);
_prop.put(key, value);
//Associate Comment with key below it
//No point in keeping it if it isn't an ordered map
if(!comment.equals("") &&
_prop instanceof LinkedHashMap )
_comments.put(key, comment);
}
skipCharacters(LINE_SEPARATOR);
}
}
private String processComment() throws IOException
{
int nextChar = peek();
StringBuffer sb = new StringBuffer();
String eol = "";
while (!isAtEndOfStream())
{
// we are at the beginning of a line.
// check whether it is a comment and if it is, skip it
if (COMMENT.indexOf(((char)nextChar)) >=0 )
{
String comment = readAnyBut(LINE_SEPARATOR);
eol = readTheseCharacters(LINE_SEPARATOR);
sb.append(comment + eol);
skipWhiteSpace();
}
else
{
return sb.toString();
}
nextChar = peek();
}
return "";
}
private boolean isAtEndOfLine() throws IOException
{
int nextChar = peek();
if (nextChar < 0)
return true;
return (LINE_SEPARATOR.indexOf((char)nextChar) >= 0);
}
private String byteToStr(int iByte)
{
return String.valueOf((char)iByte);
}
private int peek() throws IOException
{
_reader.mark(1);
int nextChar = _reader.read();
_reader.reset();
return nextChar;
}
private boolean isAtEndOfStream() throws IOException
{
int nextChar = peek();
return (nextChar < 0);
}
private String readQuotedLine(String terminators, boolean includeWhiteSpace) throws IOException
{
StringBuffer buf = new StringBuffer();
while (true)
{
// see what the next char is
int nextChar = peek();
// if at end of stream or the char is one of the terminators, stop
if (nextChar < 0 ||
terminators.indexOf((char)nextChar) >= 0 ||
(!includeWhiteSpace &&
Character.isWhitespace((char)nextChar)))
break;
try
{
// read the char (and possibly unquote it)
char ch = readQuotedChar();
buf.append(ch);
}
catch (Exception e)
{
// simply ignore -- no character was read
}
}
return buf.toString();
}
private char readQuotedChar() throws IOException
{
int nextChar = _reader.read();
if (nextChar < 0)
throw new IOException();
char ch = (char) nextChar;
// if the char is not the quotation char, simply return it
if (ch != '\\')
return ch;
// the character is a quotation character. unquote it
nextChar = _reader.read();
// if at the end of the stream, stop
if (nextChar < 0)
throw new IOException();
ch = (char) nextChar;
switch (ch)
{
case 'u' : //Handle Unicode escapes
char res = 0;
for (int i = 0; i < 4; i++)
{
nextChar = _reader.read();
if (nextChar < 0)
throw new IllegalArgumentException("Malformed \\uxxxx encoding.");
char digitChar = (char) nextChar;
int digit = HEX_DIGITS.indexOf(Character.toUpperCase(digitChar));
if (digit < 0)
throw new IllegalArgumentException("Malformed \\uxxxx encoding.");
res = (char) (res * 16 + digit);
}
return res;
case '\r' :
// if the next char is \n, read it and fall through
nextChar = peek();
if (nextChar == '\n')
_reader.read();
case '\n' :
skipWhiteSpace();
throw new IOException();
case 't' : return '\t';
case 'n' : return '\n';
case 'r' : return '\r';
default: return ch;
}
}
private String readTheseCharacters(String readMe) throws IOException
{
int iChar;
StringBuffer sb = new StringBuffer();
while ((iChar = peek()) != -1)
{
if(readMe.indexOf((char)iChar) >= 0)
sb.append((char)_reader.read());
else
return sb.toString();
}
return sb.toString();
}
private void skipCharacters(String skipMe) throws IOException
{
int iChar;
while ((iChar = peek()) != -1)
{
if(skipMe.indexOf((char)iChar) >= 0)
_reader.skip(1);
else
break;
}
}
private void skipWhiteSpace() throws IOException
{
int iChar;
while ((iChar = peek()) != -1)
{
if(Character.isWhitespace((char)iChar) &&
LINE_SEPARATOR.indexOf((char)iChar) < 0)
{
_reader.skip(1);
}
else
break;
}
}
private void skipAnyBut(String dontSkipMe) throws IOException
{
int iChar = peek();
while ((iChar = peek()) != -1)
{
if(dontSkipMe.indexOf((char)iChar) >= 0)
break;
_reader.skip(1);
}
}
private String readAnyBut(String dontSkipMe) throws IOException
{
int nextChar = peek();
StringBuffer sb = new StringBuffer();
while ((nextChar = peek()) != -1)
{
if(dontSkipMe.indexOf((char)nextChar) >= 0)
return sb.toString();
int thisChar = _reader.read();
if(thisChar > 0)
sb.append((char)thisChar);
}
return sb.toString();
}
/**
* Returns a copy of comments to be used in Properties file.
* @return new Map of Comments stored internally
*/
public LinkedHashMap getPropComments()
{
return new LinkedHashMap(_comments);
}
/**
* Method to set comments to be used in Properties file.
* To allow for comments to be placed above a property in a file the key name
* must match the key of the property it is supposed to be placed above. Multilined
* comments will have the # character added if one does not exist Objects value will be
* in this map will be obtained from the obj.toString() method. This class must be intantiated
* with a LinkedHashMap as its internal map.
* @param comments
*/
public void setPropComments(LinkedHashMap comments)
{
Iterator it = comments.keySet().iterator();
while(it.hasNext())
{
Object key = it.next();
Object obj = comments.get(key);
String com = (obj != null) ? obj.toString() : "";
BufferedReader reader = new BufferedReader(new StringReader(com));
StringBuffer buf = new StringBuffer();
String line = null;
try
{
while((line = reader.readLine()) != null)
{
if(!line.startsWith("#") ||
!line.startsWith("!"))
{
buf.append("#");
}
buf.append(line);
buf.append(System.getProperties().getProperty("line.separator", "\r\n"));
}
_comments.put(key, buf.toString());
}
catch(Exception e)
{/*Not prone to error*/}
}
}
/**
*
* @return the header stored in the class
*/
public String getHeader()
{
Object obj = _comments.get(KEY_COMMENT_HEADER);
return ((obj != null)) ? obj.toString() : "";
}
/**
*
* @param str String value of the Properties file header. Each item in array
* indicates a line of the header.
* This does not need to be proceded by a '#' or '!' character.
*/
public void setHeader(String[] str)
{
StringBuffer header = new StringBuffer();
for (int i = 0; i < str.length; i++)
{
if ( !str[i].startsWith("#") ||
!str[i].startsWith("!") )
{
header.append("#");
}
header.append(str[i]);
}
_comments.put(KEY_COMMENT_HEADER, header.toString());
}
/**
* Searches for the property with the specified key in this property list.
* If the key is not found in this property list, the default property list,
* and its defaults, recursively, are then checked. The method returns
* null if the property is not found.
* @param key the property key.
* @return the value in this property list with the specified key value.
* @see #setProperty
* @see #defaults
*/
public String getProperty(String key)
{
Object oval = _prop.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
Object oDefVal = (_defaults != null) ? _defaults.get(key) : null;
return ((sval == null) && (oDefVal != null)) ? oDefVal.toString() : sval;
}
/**
* Searches for the property with the specified key in this property list.
* If the key is not found in this property list, the default property list,
* and its defaults, recursively, are then checked. The method returns the
* default value argument if the property is not found.
*
* @param key the hashtable key.
* @param defaultValue a default value.
*
* @return the value in this property list with the specified key value.
* @see #setProperty
* @see #defaults
*/
public String getProperty(String key, String defaultValue)
{





