/* * 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 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) { String val = getProperty(key); return (val == null) ? defaultValue : val; } /** * * @return */ public Map getDefaults() { return _defaults; } /** * * @param defaults */ public void setDefaults(Map defaults) { this._defaults = defaults; } /** * * @return */ public Map getProperties() { return _prop; } /** * * @param prop */ public void setProperties(Map prop) { this._prop = prop; } public static void main(String[] args) { try { FileInputStream fis; String testFile = ".\\OasWork\\PRWMF.properties"; fis = new FileInputStream(testFile); LinkedHashMap map = new LinkedHashMap(); PropertiesMap propX = new PropertiesMap(map); propX.load(fis); fis = new FileInputStream(testFile); Properties prop1 = new Properties(); prop1.load(fis); System.out.println("My parser: size='" + Integer.toString(map.size()) + "'"); System.out.println("Java parse output: size='" + Integer.toString(prop1.size()) + "'"); System.out.println("My parser: map=" + map); System.out.println("Java parse output: map=" + prop1); System.out.println("My parser: comments=" + propX.getPropComments()); FileOutputStream fos = new FileOutputStream(".\\PropOutTest.properties"); LinkedHashMap comments = propX.getPropComments(); comments.put("ESCOMPLETE.MESSAGE_FORMAT_LINE9", "A commment above ESCOMPLETE.MESSAGE_FORMAT_LINE9"); comments.put("ESCOMPLETE.MESSAGE_FORMAT_LINE5", "A commment above ESCOMPLETE.MESSAGE_FORMAT_LINE5"); propX.setPropComments(comments); propX.store(new FileOutputStream(".\\PropOutTest.properties"), true); fos.close(); } catch(Exception e) { System.out.println(e); } } }