0001: /**
0002: *
0003: * Licensed to the Apache Software Foundation (ASF) under one or more
0004: * contributor license agreements. See the NOTICE file distributed with
0005: * this work for additional information regarding copyright ownership.
0006: * The ASF licenses this file to You under the Apache License, Version 2.0
0007: * (the "License"); you may not use this file except in compliance with
0008: * the License. You may obtain a copy of the License at
0009: *
0010: * http://www.apache.org/licenses/LICENSE-2.0
0011: *
0012: * Unless required by applicable law or agreed to in writing, software
0013: * distributed under the License is distributed on an "AS IS" BASIS,
0014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0015: * See the License for the specific language governing permissions and
0016: * limitations under the License.
0017: */package org.apache.openejb.util;
0018:
0019: import java.io.ByteArrayInputStream;
0020: import java.io.IOException;
0021: import java.io.InputStream;
0022: import java.io.OutputStream;
0023: import java.io.OutputStreamWriter;
0024: import java.io.PrintStream;
0025: import java.io.PrintWriter;
0026: import java.io.StringReader;
0027: import java.util.Arrays;
0028: import java.util.Collection;
0029: import java.util.Collections;
0030: import java.util.Enumeration;
0031: import java.util.Hashtable;
0032: import java.util.InvalidPropertiesFormatException;
0033: import java.util.LinkedHashMap;
0034: import java.util.Map;
0035: import java.util.Properties;
0036: import java.util.Set;
0037: import javax.xml.parsers.DocumentBuilder;
0038: import javax.xml.parsers.DocumentBuilderFactory;
0039: import javax.xml.parsers.ParserConfigurationException;
0040:
0041: import org.w3c.dom.Comment;
0042: import org.w3c.dom.Document;
0043: import org.w3c.dom.Element;
0044: import org.w3c.dom.Node;
0045: import org.w3c.dom.NodeList;
0046: import org.xml.sax.EntityResolver;
0047: import org.xml.sax.ErrorHandler;
0048: import org.xml.sax.InputSource;
0049: import org.xml.sax.SAXException;
0050: import org.xml.sax.SAXParseException;
0051:
0052: /**
0053: * Properties is a Hashtable where the keys and values must be Strings. Each Properties can have a default
0054: * Properties which specifies the default values which are used if the key is not in this Properties.
0055: *
0056: * @see Hashtable
0057: * @see java.lang.System#getProperties
0058: */
0059: public class SuperProperties extends Properties {
0060:
0061: private static final String PROP_DTD_NAME = "http://java.sun.com/dtd/properties.dtd";
0062:
0063: private static final String PROP_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
0064: + " <!ELEMENT properties (comment?, entry*) >"
0065: + " <!ATTLIST properties version CDATA #FIXED \"1.0\" >"
0066: + " <!ELEMENT comment (#PCDATA) >"
0067: + " <!ELEMENT entry (#PCDATA) >"
0068: + " <!ATTLIST entry key CDATA #REQUIRED >";
0069:
0070: /**
0071: * Actual property values.
0072: */
0073: protected LinkedHashMap<Object, Object> properties = new LinkedHashMap<Object, Object>();
0074:
0075: /**
0076: * Comments for the properties.
0077: */
0078: protected LinkedHashMap<String, String> comments = new LinkedHashMap<String, String>();
0079:
0080: /**
0081: * Attributes for the properties.
0082: */
0083: protected LinkedHashMap<String, LinkedHashMap<String, String>> attributes = new LinkedHashMap<String, LinkedHashMap<String, String>>();
0084:
0085: /**
0086: * The default property values.
0087: */
0088: protected Properties defaults;
0089:
0090: /**
0091: * Are lookups case insensitive?
0092: */
0093: protected boolean caseInsensitive;
0094:
0095: /**
0096: * The text between a key and the value.
0097: */
0098: protected String keyValueSeparator = "=";
0099:
0100: /**
0101: * The line separator to use when storing. Defaults to system line separator.
0102: */
0103: protected String lineSeparator = System
0104: .getProperty("line.separator");
0105:
0106: /**
0107: * Number of spaces to indent each line of the properties file.
0108: */
0109: protected String indent = "";
0110:
0111: /**
0112: * Number of spaces to indent comment after '#' character.
0113: */
0114: protected String commentIndent = " ";
0115:
0116: /**
0117: * Should there be a blank line between properties.
0118: */
0119: protected boolean spaceBetweenProperties = true;
0120:
0121: /**
0122: * Should there be a blank line between a comment and the property.
0123: */
0124: protected boolean spaceAfterComment = false;
0125:
0126: /**
0127: * Used for loadFromXML.
0128: */
0129: private DocumentBuilder builder = null;
0130:
0131: /**
0132: * Constructs a new Properties object.
0133: */
0134: public SuperProperties() {
0135: super ();
0136: }
0137:
0138: /**
0139: * Constructs a new Properties object using the specified default properties.
0140: *
0141: * @param properties the default properties
0142: */
0143: public SuperProperties(Properties properties) {
0144: super (properties);
0145: defaults = properties;
0146: }
0147:
0148: /**
0149: * Are lookups case insensitive?
0150: * @return true if lookups are insensitive
0151: */
0152: public boolean isCaseInsensitive() {
0153: return caseInsensitive;
0154: }
0155:
0156: /**
0157: * Sets the sensitive of lookups.
0158: * @param caseInsensitive if looks are insensitive
0159: */
0160: public void setCaseInsensitive(boolean caseInsensitive) {
0161: this .caseInsensitive = caseInsensitive;
0162: }
0163:
0164: /**
0165: * Gets the text that separates keys and values.
0166: * The default is "=".
0167: * @return the text that separates keys and values
0168: */
0169: public String getKeyValueSeparator() {
0170: return keyValueSeparator;
0171: }
0172:
0173: /**
0174: * Sets the text that separates keys and values.
0175: * @param keyValueSeparator the text that separates keys and values
0176: */
0177: public void setKeyValueSeparator(String keyValueSeparator) {
0178: if (keyValueSeparator == null)
0179: throw new NullPointerException("keyValueSeparator is null");
0180: if (keyValueSeparator.length() == 0)
0181: throw new NullPointerException("keyValueSeparator is empty");
0182: this .keyValueSeparator = keyValueSeparator;
0183: }
0184:
0185: /**
0186: * Gets the text that separates lines while storing.
0187: * The default is the system line.separator.
0188: * @return the text that separates keys and values
0189: */
0190: public String getLineSeparator() {
0191: return lineSeparator;
0192: }
0193:
0194: /**
0195: * Sets the text that separates lines while storing
0196: * @param lineSeparator the text that separates lines
0197: */
0198: public void setLineSeparator(String lineSeparator) {
0199: if (lineSeparator == null)
0200: throw new NullPointerException("lineSeparator is null");
0201: if (lineSeparator.length() == 0)
0202: throw new NullPointerException("lineSeparator is empty");
0203: this .lineSeparator = lineSeparator;
0204: }
0205:
0206: /**
0207: * Gets the number of spaces to indent each line of the properties file.
0208: * @return the number of spaces to indent each line of the properties file
0209: */
0210: public int getIndent() {
0211: return indent.length();
0212: }
0213:
0214: /**
0215: * Sets the number of spaces to indent each line of the properties file.
0216: * @param indent the number of spaces to indent each line of the properties file
0217: */
0218: public void setIndent(int indent) {
0219: char[] chars = new char[indent];
0220: Arrays.fill(chars, ' ');
0221: this .indent = new String(chars);
0222: }
0223:
0224: /**
0225: * Gets the number of spaces to indent comment after '#' character.
0226: * @return the number of spaces to indent comment after '#' character
0227: */
0228: public int getCommentIndent() {
0229: return commentIndent.length();
0230: }
0231:
0232: /**
0233: * Sets the number of spaces to indent comment after '#' character.
0234: * @param commentIndent the number of spaces to indent comment after '#' character
0235: */
0236: public void setCommentIndent(int commentIndent) {
0237: char[] chars = new char[commentIndent];
0238: Arrays.fill(chars, ' ');
0239: this .commentIndent = new String(chars);
0240: }
0241:
0242: /**
0243: * Should a blank line be added between properties?
0244: * @return true if a blank line should be added between properties; false otherwise
0245: */
0246: public boolean isSpaceBetweenProperties() {
0247: return spaceBetweenProperties;
0248: }
0249:
0250: /**
0251: * If true a blank line will be added between properties.
0252: * @param spaceBetweenProperties if true a blank line will be added between properties
0253: */
0254: public void setSpaceBetweenProperties(boolean spaceBetweenProperties) {
0255: this .spaceBetweenProperties = spaceBetweenProperties;
0256: }
0257:
0258: /**
0259: * Should there be a blank line between a comment and the property?
0260: * @return true if a blank line should be added between a comment and the property
0261: */
0262: public boolean isSpaceAfterComment() {
0263: return spaceAfterComment;
0264: }
0265:
0266: /**
0267: * If true a blank line will be added between a comment and the property.
0268: * @param spaceAfterComment if true a blank line will be added between a comment and the property
0269: */
0270: public void setSpaceAfterComment(boolean spaceAfterComment) {
0271: this .spaceAfterComment = spaceAfterComment;
0272: }
0273:
0274: public String getProperty(String name) {
0275: Object result = get(name);
0276: String property = result instanceof String ? (String) result
0277: : null;
0278: if (property == null && defaults != null) {
0279: property = defaults.getProperty(name);
0280: }
0281: return property;
0282: }
0283:
0284: public String getProperty(String name, String defaultValue) {
0285: Object result = get(name);
0286: String property = result instanceof String ? (String) result
0287: : null;
0288: if (property == null && defaults != null) {
0289: property = defaults.getProperty(name);
0290: }
0291: if (property == null) {
0292: return defaultValue;
0293: }
0294: return property;
0295: }
0296:
0297: public synchronized Object setProperty(String name, String value) {
0298: return put(name, value);
0299: }
0300:
0301: /**
0302: * Searches for the comment associated with the specified property. If the property is not found, look
0303: * in the default properties. If the property is not found in the default properties, answer null.
0304: *
0305: * @param name the name of the property to find
0306: * @return the named property value
0307: */
0308: public String getComment(String name) {
0309: name = normalize(name);
0310: String comment = comments.get(name);
0311: if (comment == null && defaults instanceof SuperProperties) {
0312: comment = ((SuperProperties) defaults).getComment(name);
0313: }
0314: return comment;
0315: }
0316:
0317: /**
0318: * Sets the comment associated with a property.
0319: * @param name the property name; not null
0320: * @param comment the comment; not null
0321: */
0322: public void setComment(String name, String comment) {
0323: if (name == null)
0324: throw new NullPointerException("name is null");
0325: if (comment == null)
0326: throw new NullPointerException("comment is null");
0327:
0328: name = normalize(name);
0329: comments.put(name, comment);
0330: }
0331:
0332: /**
0333: * Searches for the attributes associated with the specified property. If the property is not found, look
0334: * in the default properties. If the property is not found in the default properties, answer null.
0335: *
0336: * @param name the name of the property to find
0337: * @return the attributes for an existing property (not null); null for non-existant properties
0338: */
0339: public Map<String, String> getAttributes(String name) {
0340: if (name == null)
0341: throw new NullPointerException("name is null");
0342:
0343: name = normalize(name);
0344: Map<String, String> attributes = this .attributes.get(name);
0345: if (attributes == null && defaults instanceof SuperProperties) {
0346: attributes = ((SuperProperties) defaults)
0347: .getAttributes(name);
0348: }
0349: return attributes;
0350: }
0351:
0352: public void list(PrintStream out) {
0353: if (out == null) {
0354: throw new NullPointerException();
0355: }
0356: StringBuffer buffer = new StringBuffer(80);
0357: Enumeration<?> keys = propertyNames();
0358: while (keys.hasMoreElements()) {
0359: String key = (String) keys.nextElement();
0360: buffer.append(key);
0361: buffer.append('=');
0362: String property = (String) get(key);
0363: if (property == null) {
0364: property = defaults.getProperty(key);
0365: }
0366: if (property.length() > 40) {
0367: buffer.append(property.substring(0, 37));
0368: buffer.append("...");
0369: } else {
0370: buffer.append(property);
0371: }
0372: out.println(buffer.toString());
0373: buffer.setLength(0);
0374: }
0375: }
0376:
0377: public void list(PrintWriter writer) {
0378: if (writer == null) {
0379: throw new NullPointerException();
0380: }
0381: StringBuffer buffer = new StringBuffer(80);
0382: Enumeration<?> keys = propertyNames();
0383: while (keys.hasMoreElements()) {
0384: String key = (String) keys.nextElement();
0385: buffer.append(key);
0386: buffer.append('=');
0387: String property = (String) get(key);
0388: while (property == null) {
0389: property = defaults.getProperty(key);
0390: }
0391: if (property.length() > 40) {
0392: buffer.append(property.substring(0, 37));
0393: buffer.append("...");
0394: } else {
0395: buffer.append(property);
0396: }
0397: writer.println(buffer.toString());
0398: buffer.setLength(0);
0399: }
0400: }
0401:
0402: public synchronized void load(InputStream in) throws IOException {
0403: // never null, when empty we are processing the white space at the beginning of the line
0404: StringBuilder key = new StringBuilder();
0405: // null when processing key
0406: StringBuilder value = null;
0407: // never null, contains the comment for the property or nothing if no comment
0408: StringBuilder comment = new StringBuilder();
0409: // never null, contains attributes for a property
0410: LinkedHashMap<String, String> attributes = new LinkedHashMap<String, String>();
0411:
0412: int indent = 0;
0413: boolean globalIndentSet = false;
0414:
0415: int commentIndent = -1;
0416: boolean globalCommentIndentSet = false;
0417:
0418: // true when processing the separator between a key and value
0419: boolean inSeparator = false;
0420:
0421: while (true) {
0422: int nextByte = decodeNextCharacter(in);
0423: if (nextByte == EOF)
0424: break;
0425: char nextChar = (char) (nextByte & 0xff);
0426:
0427: switch (nextByte) {
0428: case ' ':
0429: case '\t':
0430: //
0431: // End of key if parsing key
0432: //
0433: // if parsing the key, this is the end of the key
0434: if (key.length() > 0 && value == null) {
0435: inSeparator = true;
0436: value = new StringBuilder();
0437: continue;
0438: }
0439: break;
0440: case ':':
0441: case '=':
0442: //
0443: // End of key
0444: //
0445: if (inSeparator) {
0446: inSeparator = false;
0447: continue;
0448: }
0449: if (value == null) {
0450: value = new StringBuilder();
0451: continue;
0452: }
0453: break;
0454: case LINE_ENDING:
0455: //
0456: // End of Line
0457: //
0458: if (key.length() > 0) {
0459: // add property
0460: put(key.toString(), value == null ? "" : value
0461: .toString());
0462: // add comment
0463: if (comment.length() > 0) {
0464: setComment(key.toString(), comment.toString());
0465: comment = new StringBuilder();
0466: }
0467: // add attributes
0468: this .attributes.put(normalize(key.toString()),
0469: attributes);
0470: attributes = new LinkedHashMap<String, String>();
0471: // set line indent
0472: if (!globalIndentSet) {
0473: setIndent(indent);
0474: globalIndentSet = true;
0475: }
0476: indent = 0;
0477: }
0478: key = new StringBuilder();
0479: value = null;
0480: continue;
0481: case '#':
0482: case '!':
0483: //
0484: // Comment
0485: //
0486: if (key.length() == 0) {
0487: // set global line indent
0488: if (!globalIndentSet) {
0489: setIndent(indent);
0490: globalIndentSet = true;
0491: }
0492: indent = 0;
0493:
0494: // read comment Line
0495: StringBuilder commentLine = new StringBuilder();
0496: int commentLineIndent = 0;
0497: boolean inIndent = true;
0498: while (true) {
0499: nextByte = in.read();
0500: if (nextByte < 0)
0501: break;
0502: nextChar = (char) nextByte; // & 0xff
0503:
0504: if (inIndent && nextChar == ' ') {
0505: commentLineIndent++;
0506: commentLine.append(' ');
0507: } else if (inIndent && nextChar == '\t') {
0508: commentLineIndent += 4;
0509: commentLine.append(" ");
0510: } else if (nextChar == '\r' || nextChar == '\n') {
0511: break;
0512: } else {
0513: inIndent = false;
0514: commentLine.append(nextChar);
0515: }
0516: }
0517:
0518: // Determine indent
0519: if (comment.length() == 0) {
0520: // if this is a new comment block, the comment indent size for this
0521: // block is based the first line of the comment
0522: commentIndent = commentLineIndent;
0523: if (!globalCommentIndentSet) {
0524: setCommentIndent(commentIndent);
0525: globalCommentIndentSet = true;
0526: }
0527: }
0528: commentLineIndent = Math.min(commentIndent,
0529: commentLineIndent);
0530:
0531: if (commentLine.toString().trim().startsWith("@")) {
0532: // process property attribute
0533: String attribute = commentLine.toString()
0534: .trim().substring(1);
0535: String[] parts = attribute.split("=", 2);
0536: String attributeName = parts[0].trim();
0537: String attributeValue = parts.length == 2 ? parts[1]
0538: .trim()
0539: : "";
0540: attributes.put(attributeName, attributeValue);
0541: } else {
0542: // append comment
0543: if (comment.length() != 0) {
0544: comment.append(lineSeparator);
0545: }
0546: comment.append(commentLine.toString()
0547: .substring(commentLineIndent));
0548: }
0549: continue;
0550: }
0551: break;
0552: }
0553:
0554: if (nextByte >= 0 && Character.isWhitespace(nextChar)) {
0555: // count leading white space
0556: if (key.length() == 0) {
0557: if (nextChar == '\t') {
0558: indent += 4;
0559: } else {
0560: indent++;
0561: }
0562: }
0563:
0564: // if key length == 0 or value length == 0
0565: if (key.length() == 0 || value == null
0566: || value.length() == 0) {
0567: continue;
0568: }
0569: }
0570:
0571: // Decode encoded separator characters
0572: switch (nextByte) {
0573: case ENCODED_EQUALS:
0574: nextChar = '=';
0575: break;
0576: case ENCODED_COLON:
0577: nextChar = ':';
0578: break;
0579: case ENCODED_SPACE:
0580: nextChar = ' ';
0581: break;
0582: case ENCODED_TAB:
0583: nextChar = '\t';
0584: break;
0585: case ENCODED_NEWLINE:
0586: nextChar = '\n';
0587: break;
0588: case ENCODED_CARRIAGE_RETURN:
0589: nextChar = '\r';
0590: break;
0591: }
0592:
0593: inSeparator = false;
0594: if (value == null) {
0595: key.append(nextChar);
0596: } else {
0597: value.append(nextChar);
0598: }
0599: }
0600:
0601: // if buffer has data, there is a property we still need toadd
0602: if (key.length() > 0) {
0603: // add property
0604: put(key.toString(), value == null ? "" : value.toString());
0605: // add comment
0606: if (comment.length() > 0) {
0607: setComment(key.toString(), comment.toString());
0608: }
0609: // add attributes
0610: this .attributes.put(normalize(key.toString()), attributes);
0611: // set line indent
0612: if (!globalIndentSet) {
0613: setIndent(indent);
0614: }
0615: }
0616: }
0617:
0618: private static final int EOF = -1;
0619: private static final int LINE_ENDING = -4200;
0620: private static final int ENCODED_EQUALS = -5000;
0621: private static final int ENCODED_COLON = -5001;
0622: private static final int ENCODED_SPACE = -5002;
0623: private static final int ENCODED_TAB = -5003;
0624: private static final int ENCODED_NEWLINE = -5004;
0625: private static final int ENCODED_CARRIAGE_RETURN = -5005;
0626:
0627: private int decodeNextCharacter(InputStream in) throws IOException {
0628: boolean lineContinuation = false;
0629: boolean carriageReturnLineContinuation = false;
0630: boolean encoded = false;
0631: while (true) {
0632: // read character
0633: int nextByte = in.read();
0634: if (nextByte < 0)
0635: return EOF;
0636: char nextChar = (char) (nextByte & 0xff);
0637:
0638: // if line continuation character was '\r', we need to ignore an optional '\n'
0639: // immediately following the \r
0640: if (carriageReturnLineContinuation) {
0641: carriageReturnLineContinuation = false;
0642: if (nextChar == '\n') {
0643: continue;
0644: }
0645: }
0646:
0647: // If escape sequence \x or line continuation, decode it
0648: if (nextChar == '\\') {
0649: // next character is the escaped character
0650: nextByte = in.read();
0651: if (nextByte < 0) {
0652: // line continuation to end of stream
0653: // sun vm returns 0 character for this case
0654: nextChar = '\u0000';
0655: } else {
0656: nextChar = (char) (nextByte & 0xff);
0657: }
0658:
0659: switch (nextChar) {
0660: case '\r':
0661: // line continuation using '\r', which optionally can have a following '\n'
0662: carriageReturnLineContinuation = true;
0663: // fall through
0664: case '\n':
0665: // line continuation
0666: lineContinuation = true;
0667: continue;
0668: case 'u':
0669: nextChar = readUnicode(in);
0670: break;
0671: default:
0672: encoded = true;
0673: nextChar = decodeEscapeChar(nextChar);
0674: break;
0675: }
0676: } else {
0677: // if line ending character, we return the special value LINE_ENDING so
0678: // caller can differentiate between an encoded "\n" sequence and a real
0679: // line ending character in the file
0680: if (nextChar == '\n' || nextChar == '\r') {
0681: return LINE_ENDING;
0682: }
0683: }
0684:
0685: // in a line continuation we ignore spaces and tabs until the first real character
0686: if (lineContinuation
0687: && (nextChar == ' ' || nextChar == '\t')) {
0688: continue;
0689: }
0690:
0691: if (encoded) {
0692: switch (nextChar) {
0693: case '=':
0694: return ENCODED_EQUALS;
0695: case ':':
0696: return ENCODED_COLON;
0697: case ' ':
0698: return ENCODED_SPACE;
0699: case '\t':
0700: return ENCODED_TAB;
0701: case '\n':
0702: return ENCODED_NEWLINE;
0703: case '\r':
0704: return ENCODED_CARRIAGE_RETURN;
0705: }
0706: }
0707: return nextChar;
0708: }
0709: }
0710:
0711: private char decodeEscapeChar(char nextChar) {
0712: switch (nextChar) {
0713: case 'b':
0714: return '\b';
0715: case 'f':
0716: return '\f';
0717: case 'n':
0718: return '\n';
0719: case 'r':
0720: return '\r';
0721: case 't':
0722: return '\t';
0723: case 'u':
0724: throw new IllegalArgumentException(
0725: "decodeEscapeChar can not decode an unicode sequence");
0726: default:
0727: return nextChar;
0728: }
0729: }
0730:
0731: private char readUnicode(InputStream in) throws IOException {
0732: char[] buf = new char[4];
0733: int unicode = 0;
0734: for (int i = 0; i < buf.length; i++) {
0735: int nextByte = in.read();
0736:
0737: // we must get exactally 4 bytes
0738: if (nextByte < 0) {
0739: throw new IllegalArgumentException(
0740: "Invalid unicode sequence: expected format \\uxxxx, but got \\u"
0741: + new String(buf, 0, i));
0742: }
0743:
0744: // convert to character
0745: char nextChar = (char) (nextByte & 0xff);
0746: buf[i] = nextChar;
0747:
0748: // convert to digit
0749: int nextDigit = Character.digit(nextChar, 16);
0750:
0751: // all bytes must be valid hex digits
0752: if (nextDigit < 0) {
0753: throw new IllegalArgumentException("Illegal character "
0754: + nextChar + " in unicode sequence \\u"
0755: + new String(buf, 0, i + 1));
0756: }
0757:
0758: unicode = (unicode << 4) + nextDigit;
0759: }
0760:
0761: return (char) unicode;
0762: }
0763:
0764: public Enumeration<?> propertyNames() {
0765: if (defaults == null) {
0766: return keys();
0767: }
0768:
0769: Hashtable<Object, Object> set = new Hashtable<Object, Object>(
0770: defaults.size() + size());
0771: Enumeration<?> keys = defaults.propertyNames();
0772: while (keys.hasMoreElements()) {
0773: set.put(keys.nextElement(), set);
0774: }
0775: keys = keys();
0776: while (keys.hasMoreElements()) {
0777: set.put(keys.nextElement(), set);
0778: }
0779: return set.keys();
0780: }
0781:
0782: @SuppressWarnings({"deprecation"})
0783: public void save(OutputStream out, String comment) {
0784: try {
0785: store(out, comment);
0786: } catch (IOException e) {
0787: }
0788: }
0789:
0790: public synchronized void store(OutputStream out, String headComment)
0791: throws IOException {
0792: OutputStreamWriter writer = new OutputStreamWriter(out,
0793: "ISO8859_1");
0794: if (headComment != null) {
0795: writer.write(indent);
0796: writer.write("#");
0797: writer.write(commentIndent);
0798: writer.write(headComment);
0799: writer.write(lineSeparator);
0800: }
0801:
0802: boolean firstProperty = true;
0803: StringBuilder buffer = new StringBuilder(200);
0804: for (Map.Entry<Object, Object> entry : entrySet()) {
0805: String key = (String) entry.getKey();
0806: String value = (String) entry.getValue();
0807:
0808: if (!firstProperty && spaceBetweenProperties) {
0809: buffer.append(lineSeparator);
0810: }
0811:
0812: String comment = comments.get(key);
0813: Map<String, String> attributes = this .attributes.get(key);
0814: if (comment != null || !attributes.isEmpty()) {
0815: dumpComment(buffer, comment, attributes, "#");
0816: if (spaceAfterComment) {
0817: buffer.append(lineSeparator);
0818: }
0819: }
0820:
0821: // ${indent}${key}=${value}
0822: buffer.append(indent);
0823: dumpString(buffer, key, true);
0824: if (value != null && value.length() > 0) {
0825: buffer.append(keyValueSeparator);
0826: dumpString(buffer, value, false);
0827: }
0828: buffer.append(lineSeparator);
0829:
0830: writer.write(buffer.toString());
0831: buffer.setLength(0);
0832:
0833: firstProperty = false;
0834: }
0835: writer.flush();
0836: }
0837:
0838: private void dumpString(StringBuilder buffer, String string,
0839: boolean key) {
0840: int i = 0;
0841: if (!key && i < string.length() && string.charAt(i) == ' ') {
0842: buffer.append("\\ ");
0843: i++;
0844: }
0845:
0846: for (; i < string.length(); i++) {
0847: char ch = string.charAt(i);
0848: switch (ch) {
0849: case '\t':
0850: buffer.append("\\t");
0851: break;
0852: case '\n':
0853: buffer.append("\\n");
0854: break;
0855: case '\f':
0856: buffer.append("\\f");
0857: break;
0858: case '\r':
0859: buffer.append("\\r");
0860: break;
0861: default:
0862: if ("\\".indexOf(ch) >= 0
0863: || (key && "#!=: ".indexOf(ch) >= 0)) {
0864: buffer.append('\\');
0865: }
0866: if (ch >= ' ' && ch <= '~') {
0867: buffer.append(ch);
0868: } else {
0869: String hex = Integer.toHexString(ch);
0870: buffer.append("\\u");
0871: for (int j = 0; j < 4 - hex.length(); j++) {
0872: buffer.append("0");
0873: }
0874: buffer.append(hex);
0875: }
0876: }
0877: }
0878: }
0879:
0880: private void dumpComment(StringBuilder buffer, String comment,
0881: Map<String, String> attributes, String commentToken) {
0882: if (comment != null) {
0883: boolean startOfLine = true;
0884:
0885: char ch = 0;
0886: for (int i = 0; i < comment.length(); i++) {
0887: ch = comment.charAt(i);
0888:
0889: if (startOfLine) {
0890: buffer.append(indent);
0891: buffer.append(commentToken);
0892: buffer.append(commentIndent);
0893: startOfLine = false;
0894: }
0895:
0896: switch (ch) {
0897: case '\r':
0898: // if next character is not \n, this is the line break
0899: if (i + 1 < comment.length()
0900: && comment.charAt(i + 1) != '\n') {
0901: buffer.append(lineSeparator);
0902: startOfLine = true;
0903: }
0904: break;
0905: case '\n':
0906: buffer.append(lineSeparator);
0907: startOfLine = true;
0908: break;
0909: default:
0910: buffer.append(ch);
0911: }
0912: }
0913:
0914: // if the last character written was not a line break, write one now
0915: if (ch != '\r' && ch != '\n') {
0916: buffer.append(lineSeparator);
0917: }
0918: }
0919:
0920: // ${indent}#${commentIndent}@${attributeName}=${attributeValue}
0921: for (Map.Entry<String, String> entry : attributes.entrySet()) {
0922: buffer.append(indent);
0923: buffer.append("#");
0924: buffer.append(commentIndent);
0925: buffer.append("@");
0926: buffer.append(entry.getKey());
0927: if (entry.getValue() != null
0928: && entry.getValue().length() > 0) {
0929: buffer.append("=");
0930: buffer.append(entry.getValue());
0931: }
0932: buffer.append(lineSeparator);
0933: }
0934: }
0935:
0936: public synchronized void loadFromXML(InputStream in)
0937: throws IOException {
0938: if (in == null) {
0939: throw new NullPointerException();
0940: }
0941:
0942: DocumentBuilder builder = getDocumentBuilder();
0943:
0944: try {
0945: Document doc = builder.parse(in);
0946: NodeList entries = doc.getElementsByTagName("entry");
0947: if (entries == null) {
0948: return;
0949: }
0950:
0951: int entriesListLength = entries.getLength();
0952: for (int i = 0; i < entriesListLength; i++) {
0953: Element entry = (Element) entries.item(i);
0954: String key = entry.getAttribute("key");
0955: String value = entry.getTextContent();
0956: put(key, value);
0957:
0958: // search backwards for a comment
0959: for (Node node = entry.getPreviousSibling(); node != null
0960: && !(node instanceof Element); node = node
0961: .getPreviousSibling()) {
0962: if (node instanceof Comment) {
0963: InputStream cin = new ByteArrayInputStream(
0964: ((Comment) node).getData().getBytes());
0965:
0966: // read comment line by line
0967: StringBuilder comment = new StringBuilder();
0968: LinkedHashMap<String, String> attributes = new LinkedHashMap<String, String>();
0969:
0970: int nextByte;
0971: char nextChar;
0972: boolean firstLine = true;
0973: int commentIndent = Integer.MAX_VALUE;
0974: do {
0975: // read one line
0976: StringBuilder commentLine = new StringBuilder();
0977: int commentLineIndent = 0;
0978: boolean inIndent = true;
0979: while (true) {
0980: nextByte = cin.read();
0981: if (nextByte < 0)
0982: break;
0983: nextChar = (char) nextByte; // & 0xff
0984: if (inIndent && nextChar == ' ') {
0985: commentLineIndent++;
0986: commentLine.append(' ');
0987: } else if (inIndent && nextChar == '\t') {
0988: commentLineIndent += 4;
0989: commentLine.append(" ");
0990: } else if (nextChar == '\r'
0991: || nextChar == '\n') {
0992: break;
0993: } else {
0994: inIndent = false;
0995: commentLine.append(nextChar);
0996: }
0997: }
0998:
0999: // Determine indent
1000: if (!firstLine
1001: && commentIndent == Integer.MAX_VALUE
1002: && commentLine.length() > 0) {
1003: // if this is a new comment block, the comment indent size for this
1004: // block is based the first full line of the comment (ignoring the
1005: // line with the <!--
1006: commentIndent = commentLineIndent;
1007: }
1008: commentLineIndent = Math.min(commentIndent,
1009: commentLineIndent);
1010:
1011: if (commentLine.toString().trim()
1012: .startsWith("@")) {
1013: // process property attribute
1014: String attribute = commentLine
1015: .toString().trim().substring(1);
1016: String[] parts = attribute
1017: .split("=", 2);
1018: String attributeName = parts[0].trim();
1019: String attributeValue = parts.length == 2 ? parts[1]
1020: .trim()
1021: : "";
1022: attributes.put(attributeName,
1023: attributeValue);
1024: } else {
1025: // append comment
1026: if (comment.length() != 0) {
1027: comment.append(lineSeparator);
1028: }
1029: comment.append(commentLine.toString()
1030: .substring(commentLineIndent));
1031: }
1032:
1033: firstLine = false;
1034: } while (nextByte > 0);
1035:
1036: if (comment.length() > 0) {
1037: setComment(key, comment.toString());
1038: }
1039: this .attributes.put(normalize(key), attributes);
1040:
1041: break;
1042: }
1043: }
1044:
1045: }
1046: } catch (SAXException e) {
1047: throw new InvalidPropertiesFormatException(e);
1048: }
1049: }
1050:
1051: private DocumentBuilder getDocumentBuilder() {
1052: if (builder == null) {
1053: DocumentBuilderFactory factory = DocumentBuilderFactory
1054: .newInstance();
1055: factory.setValidating(true);
1056:
1057: try {
1058: builder = factory.newDocumentBuilder();
1059: } catch (ParserConfigurationException e) {
1060: throw new Error(e);
1061: }
1062:
1063: builder.setErrorHandler(new ErrorHandler() {
1064: public void warning(SAXParseException e)
1065: throws SAXException {
1066: throw e;
1067: }
1068:
1069: public void error(SAXParseException e)
1070: throws SAXException {
1071: throw e;
1072: }
1073:
1074: public void fatalError(SAXParseException e)
1075: throws SAXException {
1076: throw e;
1077: }
1078: });
1079:
1080: builder.setEntityResolver(new EntityResolver() {
1081: public InputSource resolveEntity(String publicId,
1082: String systemId) throws SAXException,
1083: IOException {
1084: if (systemId.equals(PROP_DTD_NAME)) {
1085: InputSource result = new InputSource(
1086: new StringReader(PROP_DTD));
1087: result.setSystemId(PROP_DTD_NAME);
1088: return result;
1089: }
1090: throw new SAXException(
1091: "Invalid DOCTYPE declaration: " + systemId);
1092: }
1093: });
1094: }
1095: return builder;
1096: }
1097:
1098: public void storeToXML(OutputStream os, String comment)
1099: throws IOException {
1100: storeToXML(os, comment, "UTF-8");
1101: }
1102:
1103: public synchronized void storeToXML(OutputStream os,
1104: String headComment, String encoding) throws IOException {
1105: if (os == null || encoding == null) {
1106: throw new NullPointerException();
1107: }
1108:
1109: // for somereason utf-8 is always used
1110: String encodingCanonicalName = "UTF-8";
1111:
1112: // header
1113: OutputStreamWriter osw = new OutputStreamWriter(os,
1114: encodingCanonicalName);
1115: StringBuilder buf = new StringBuilder(200);
1116: buf.append("<?xml version=\"1.0\" encoding=\"").append(
1117: encodingCanonicalName).append("\"?>").append(
1118: lineSeparator);
1119: buf.append(
1120: "<!DOCTYPE properties SYSTEM \"" + PROP_DTD_NAME
1121: + "\">").append(lineSeparator);
1122: buf.append("<properties>").append(lineSeparator);
1123:
1124: // comment
1125: if (headComment != null) {
1126: buf.append(indent);
1127: buf.append("<comment>");
1128: buf.append(substitutePredefinedEntries(headComment));
1129: buf.append("</comment>");
1130: buf.append(lineSeparator);
1131:
1132: if (!isEmpty()
1133: && (spaceBetweenProperties || spaceAfterComment)) {
1134: buf.append(lineSeparator);
1135: }
1136: }
1137:
1138: // properties
1139: boolean firstProperty = true;
1140: for (Map.Entry<Object, Object> entry : entrySet()) {
1141: String key = (String) entry.getKey();
1142: String value = (String) entry.getValue();
1143:
1144: if (!firstProperty && spaceBetweenProperties) {
1145: buf.append(lineSeparator);
1146: }
1147:
1148: // property comment
1149: String comment = comments.get(key);
1150: Map<String, String> attributes = this .attributes.get(key);
1151: if (comment != null || !attributes.isEmpty()) {
1152: buf.append(indent);
1153: buf.append("<!--");
1154: buf.append(lineSeparator);
1155:
1156: // comments can't contain "--" so we shrink all sequences of them to a single "-"
1157: comment = comment.replaceAll("--*", "-");
1158: dumpComment(buf, comment, attributes, "");
1159:
1160: buf.append(indent);
1161: buf.append("-->");
1162: buf.append(lineSeparator);
1163:
1164: if (spaceAfterComment) {
1165: buf.append(lineSeparator);
1166: }
1167: }
1168:
1169: // property
1170: buf.append(indent);
1171: buf.append("<entry key=\"");
1172: buf.append(substitutePredefinedEntries(key));
1173: buf.append("\">");
1174: buf.append(substitutePredefinedEntries(value));
1175: buf.append("</entry>");
1176: buf.append(lineSeparator);
1177:
1178: firstProperty = false;
1179: }
1180:
1181: buf.append("</properties>").append(lineSeparator);
1182:
1183: osw.write(buf.toString());
1184: osw.flush();
1185: }
1186:
1187: private String substitutePredefinedEntries(String s) {
1188: /*
1189: * substitution for predefined character entities
1190: * to use them safely in XML
1191: */
1192: return s.replaceAll("&", "&").replaceAll("<", "<")
1193: .replaceAll(">", ">").replaceAll("\u0027", "'")
1194: .replaceAll("\"", """);
1195: }
1196:
1197: //
1198: // Delegate all remaining methods to the properties object
1199: //
1200:
1201: public boolean isEmpty() {
1202: return properties.isEmpty();
1203: }
1204:
1205: public int size() {
1206: return properties.size();
1207: }
1208:
1209: public Object get(Object key) {
1210: key = normalize(key);
1211: return properties.get(key);
1212: }
1213:
1214: public Object put(Object key, Object value) {
1215: key = normalize(key);
1216: if (key instanceof String) {
1217: String name = (String) key;
1218: if (!attributes.containsKey(name)) {
1219: attributes.put(name,
1220: new LinkedHashMap<String, String>());
1221: }
1222: }
1223: return properties.put(key, value);
1224: }
1225:
1226: public Object remove(Object key) {
1227: key = normalize(key);
1228: comments.remove(key);
1229: attributes.remove(key);
1230: return properties.remove(key);
1231: }
1232:
1233: public void putAll(Map<?, ?> t) {
1234: for (Map.Entry<?, ?> entry : t.entrySet()) {
1235: put(entry.getKey(), entry.getValue());
1236: }
1237: if (t instanceof SuperProperties) {
1238: SuperProperties super Properties = (SuperProperties) t;
1239: for (Map.Entry<String, String> entry : super Properties.comments
1240: .entrySet()) {
1241: comments.put(normalize(entry.getKey()), entry
1242: .getValue());
1243: }
1244: for (Map.Entry<String, LinkedHashMap<String, String>> entry : super Properties.attributes
1245: .entrySet()) {
1246: attributes.put(normalize(entry.getKey()), entry
1247: .getValue());
1248: }
1249: }
1250: }
1251:
1252: /**
1253: * Returns an unmodifiable view of the keys.
1254: * @return an unmodifiable view of the keys
1255: */
1256: public Set<Object> keySet() {
1257: return Collections.unmodifiableSet(properties.keySet());
1258: }
1259:
1260: public Enumeration<Object> keys() {
1261: return Collections.enumeration(properties.keySet());
1262: }
1263:
1264: /**
1265: * Returns an unmodifiable view of the values.
1266: * @return an unmodifiable view of the values
1267: */
1268: public Collection<Object> values() {
1269: return Collections.unmodifiableCollection(properties.values());
1270: }
1271:
1272: /**
1273: * Returns an unmodifiable view of the entries.
1274: * @return an unmodifiable view of the entries
1275: */
1276: public Set<Map.Entry<Object, Object>> entrySet() {
1277: return Collections.unmodifiableSet(properties.entrySet());
1278: }
1279:
1280: public Enumeration<Object> elements() {
1281: return Collections.enumeration(properties.values());
1282: }
1283:
1284: public boolean containsKey(Object key) {
1285: key = normalize(key);
1286: return properties.containsKey(key);
1287: }
1288:
1289: public boolean containsValue(Object value) {
1290: return properties.containsValue(value);
1291: }
1292:
1293: public boolean contains(Object value) {
1294: return properties.containsValue(value);
1295: }
1296:
1297: public void clear() {
1298: properties.clear();
1299: comments.clear();
1300: attributes.clear();
1301: }
1302:
1303: @SuppressWarnings({"unchecked"})
1304: public Object clone() {
1305: SuperProperties clone = (SuperProperties) super .clone();
1306: clone.properties = (LinkedHashMap<Object, Object>) properties
1307: .clone();
1308: clone.comments = (LinkedHashMap<String, String>) comments
1309: .clone();
1310: clone.attributes = (LinkedHashMap<String, LinkedHashMap<String, String>>) attributes
1311: .clone();
1312: for (Map.Entry<String, LinkedHashMap<String, String>> entry : clone.attributes
1313: .entrySet()) {
1314: entry.setValue((LinkedHashMap<String, String>) entry
1315: .getValue().clone());
1316: }
1317: return clone;
1318: }
1319:
1320: @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass"})
1321: public boolean equals(Object o) {
1322: return properties.equals(o);
1323: }
1324:
1325: public int hashCode() {
1326: return properties.hashCode();
1327: }
1328:
1329: public String toString() {
1330: return properties.toString();
1331: }
1332:
1333: protected void rehash() {
1334: }
1335:
1336: private Object normalize(Object key) {
1337: if (key instanceof String) {
1338: return normalize((String) key);
1339: }
1340: return key;
1341: }
1342:
1343: private String normalize(String property) {
1344: if (!caseInsensitive) {
1345: return property;
1346: }
1347:
1348: if (super .containsKey(property)) {
1349: return property;
1350: }
1351:
1352: for (Object o : keySet()) {
1353: if (o instanceof String) {
1354: String key = (String) o;
1355: if (key.equalsIgnoreCase(property))
1356: return key;
1357: }
1358: }
1359:
1360: if (defaults != null) {
1361: for (Object o : defaults.keySet()) {
1362: if (o instanceof String) {
1363: String key = (String) o;
1364: if (key.equalsIgnoreCase(property))
1365: return key;
1366: }
1367: }
1368: }
1369:
1370: return property;
1371: }
1372: }
|