0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017:
0018: package org.apache.commons.configuration;
0019:
0020: import java.io.File;
0021: import java.io.FilterWriter;
0022: import java.io.IOException;
0023: import java.io.LineNumberReader;
0024: import java.io.Reader;
0025: import java.io.Writer;
0026: import java.net.URL;
0027: import java.util.ArrayList;
0028: import java.util.Iterator;
0029: import java.util.List;
0030:
0031: import org.apache.commons.lang.ArrayUtils;
0032: import org.apache.commons.lang.StringEscapeUtils;
0033: import org.apache.commons.lang.StringUtils;
0034:
0035: /**
0036: * This is the "classic" Properties loader which loads the values from
0037: * a single or multiple files (which can be chained with "include =".
0038: * All given path references are either absolute or relative to the
0039: * file name supplied in the constructor.
0040: * <p>
0041: * In this class, empty PropertyConfigurations can be built, properties
0042: * added and later saved. include statements are (obviously) not supported
0043: * if you don't construct a PropertyConfiguration from a file.
0044: *
0045: * <p>The properties file syntax is explained here, basically it follows
0046: * the syntax of the stream parsed by {@link java.util.Properties#load} and
0047: * adds several useful extensions:
0048: *
0049: * <ul>
0050: * <li>
0051: * Each property has the syntax <code>key <separator> value</code>. The
0052: * separators accepted are <code>'='</code>, <code>':'</code> and any white
0053: * space character. Examples:
0054: * <pre>
0055: * key1 = value1
0056: * key2 : value2
0057: * key3 value3</pre>
0058: * </li>
0059: * <li>
0060: * The <i>key</i> may use any character, separators must be escaped:
0061: * <pre>
0062: * key\:foo = bar</pre>
0063: * </li>
0064: * <li>
0065: * <i>value</i> may be separated on different lines if a backslash
0066: * is placed at the end of the line that continues below.
0067: * </li>
0068: * <li>
0069: * <i>value</i> can contain <em>value delimiters</em> and will then be interpreted
0070: * as a list of tokens. Default value delimiter is the comma ','. So the
0071: * following property definition
0072: * <pre>
0073: * key = This property, has multiple, values
0074: * </pre>
0075: * will result in a property with three values. You can change the value
0076: * delimiter using the <code>{@link AbstractConfiguration#setListDelimiter(char)}</code>
0077: * method. Setting the delimiter to 0 will disable value splitting completely.
0078: * </li>
0079: * <li>
0080: * Commas in each token are escaped placing a backslash right before
0081: * the comma.
0082: * </li>
0083: * <li>
0084: * If a <i>key</i> is used more than once, the values are appended
0085: * like if they were on the same line separated with commas.
0086: * </li>
0087: * <li>
0088: * Blank lines and lines starting with character '#' or '!' are skipped.
0089: * </li>
0090: * <li>
0091: * If a property is named "include" (or whatever is defined by
0092: * setInclude() and getInclude() and the value of that property is
0093: * the full path to a file on disk, that file will be included into
0094: * the configuration. You can also pull in files relative to the parent
0095: * configuration file. So if you have something like the following:
0096: *
0097: * include = additional.properties
0098: *
0099: * Then "additional.properties" is expected to be in the same
0100: * directory as the parent configuration file.
0101: *
0102: * The properties in the included file are added to the parent configuration,
0103: * they do not replace existing properties with the same key.
0104: *
0105: * </li>
0106: * </ul>
0107: *
0108: * <p>Here is an example of a valid extended properties file:
0109: *
0110: * <p><pre>
0111: * # lines starting with # are comments
0112: *
0113: * # This is the simplest property
0114: * key = value
0115: *
0116: * # A long property may be separated on multiple lines
0117: * longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
0118: * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
0119: *
0120: * # This is a property with many tokens
0121: * tokens_on_a_line = first token, second token
0122: *
0123: * # This sequence generates exactly the same result
0124: * tokens_on_multiple_lines = first token
0125: * tokens_on_multiple_lines = second token
0126: *
0127: * # commas may be escaped in tokens
0128: * commas.escaped = Hi\, what'up?
0129: *
0130: * # properties can reference other properties
0131: * base.prop = /base
0132: * first.prop = ${base.prop}/first
0133: * second.prop = ${first.prop}/second
0134: * </pre>
0135: *
0136: * <p>A <code>PropertiesConfiguration</code> object is associated with an
0137: * instance of the <code>{@link PropertiesConfigurationLayout}</code> class,
0138: * which is responsible for storing the layout of the parsed properties file
0139: * (i.e. empty lines, comments, and such things). The <code>getLayout()</code>
0140: * method can be used to obtain this layout object. With <code>setLayout()</code>
0141: * a new layout object can be set. This should be done before a properties file
0142: * was loaded.
0143: *
0144: * @see java.util.Properties#load
0145: *
0146: * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
0147: * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
0148: * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
0149: * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
0150: * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
0151: * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
0152: * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
0153: * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
0154: * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
0155: * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
0156: * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
0157: * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
0158: * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger</a>
0159: * @author <a href="mailto:ebourg@apache.org">Emmanuel Bourg</a>
0160: * @version $Id: PropertiesConfiguration.java 439648 2006-09-02 20:42:10Z oheger $
0161: */
0162: public class PropertiesConfiguration extends AbstractFileConfiguration {
0163: /** Constant for the supported comment characters.*/
0164: static final String COMMENT_CHARS = "#!";
0165:
0166: /**
0167: * This is the name of the property that can point to other
0168: * properties file for including other properties files.
0169: */
0170: private static String include = "include";
0171:
0172: /** The list of possible key/value separators */
0173: private static final char[] SEPARATORS = new char[] { '=', ':' };
0174:
0175: /** The white space characters used as key/value separators. */
0176: private static final char[] WHITE_SPACE = new char[] { ' ', '\t',
0177: '\f' };
0178:
0179: /**
0180: * The default encoding (ISO-8859-1 as specified by
0181: * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
0182: */
0183: private static final String DEFAULT_ENCODING = "ISO-8859-1";
0184:
0185: /** Constant for the platform specific line separator.*/
0186: private static final String LINE_SEPARATOR = System
0187: .getProperty("line.separator");
0188:
0189: /** Constant for the radix of hex numbers.*/
0190: private static final int HEX_RADIX = 16;
0191:
0192: /** Constant for the length of a unicode literal.*/
0193: private static final int UNICODE_LEN = 4;
0194:
0195: /** Stores the layout object.*/
0196: private PropertiesConfigurationLayout layout;
0197:
0198: /** Allow file inclusion or not */
0199: private boolean includesAllowed;
0200:
0201: // initialization block to set the encoding before loading the file in the constructors
0202: {
0203: setEncoding(DEFAULT_ENCODING);
0204: }
0205:
0206: /**
0207: * Creates an empty PropertyConfiguration object which can be
0208: * used to synthesize a new Properties file by adding values and
0209: * then saving().
0210: */
0211: public PropertiesConfiguration() {
0212: layout = createLayout();
0213: setIncludesAllowed(false);
0214: }
0215:
0216: /**
0217: * Creates and loads the extended properties from the specified file.
0218: * The specified file can contain "include = " properties which then
0219: * are loaded and merged into the properties.
0220: *
0221: * @param fileName The name of the properties file to load.
0222: * @throws ConfigurationException Error while loading the properties file
0223: */
0224: public PropertiesConfiguration(String fileName)
0225: throws ConfigurationException {
0226: super (fileName);
0227: }
0228:
0229: /**
0230: * Creates and loads the extended properties from the specified file.
0231: * The specified file can contain "include = " properties which then
0232: * are loaded and merged into the properties.
0233: *
0234: * @param file The properties file to load.
0235: * @throws ConfigurationException Error while loading the properties file
0236: */
0237: public PropertiesConfiguration(File file)
0238: throws ConfigurationException {
0239: super (file);
0240: }
0241:
0242: /**
0243: * Creates and loads the extended properties from the specified URL.
0244: * The specified file can contain "include = " properties which then
0245: * are loaded and merged into the properties.
0246: *
0247: * @param url The location of the properties file to load.
0248: * @throws ConfigurationException Error while loading the properties file
0249: */
0250: public PropertiesConfiguration(URL url)
0251: throws ConfigurationException {
0252: super (url);
0253: }
0254:
0255: /**
0256: * Gets the property value for including other properties files.
0257: * By default it is "include".
0258: *
0259: * @return A String.
0260: */
0261: public static String getInclude() {
0262: return PropertiesConfiguration.include;
0263: }
0264:
0265: /**
0266: * Sets the property value for including other properties files.
0267: * By default it is "include".
0268: *
0269: * @param inc A String.
0270: */
0271: public static void setInclude(String inc) {
0272: PropertiesConfiguration.include = inc;
0273: }
0274:
0275: /**
0276: * Controls whether additional files can be loaded by the include = <xxx>
0277: * statement or not. Base rule is, that objects created by the empty
0278: * C'tor can not have included files.
0279: *
0280: * @param includesAllowed includesAllowed True if Includes are allowed.
0281: */
0282: protected void setIncludesAllowed(boolean includesAllowed) {
0283: this .includesAllowed = includesAllowed;
0284: }
0285:
0286: /**
0287: * Reports the status of file inclusion.
0288: *
0289: * @return True if include files are loaded.
0290: */
0291: public boolean getIncludesAllowed() {
0292: return this .includesAllowed;
0293: }
0294:
0295: /**
0296: * Return the comment header.
0297: *
0298: * @return the comment header
0299: * @since 1.1
0300: */
0301: public String getHeader() {
0302: return getLayout().getHeaderComment();
0303: }
0304:
0305: /**
0306: * Set the comment header.
0307: *
0308: * @param header the header to use
0309: * @since 1.1
0310: */
0311: public void setHeader(String header) {
0312: getLayout().setHeaderComment(header);
0313: }
0314:
0315: /**
0316: * Returns the associated layout object.
0317: *
0318: * @return the associated layout object
0319: * @since 1.3
0320: */
0321: public synchronized PropertiesConfigurationLayout getLayout() {
0322: if (layout == null) {
0323: layout = createLayout();
0324: }
0325: return layout;
0326: }
0327:
0328: /**
0329: * Sets the associated layout object.
0330: *
0331: * @param layout the new layout object; can be <b>null</b>, then a new
0332: * layout object will be created
0333: * @since 1.3
0334: */
0335: public synchronized void setLayout(
0336: PropertiesConfigurationLayout layout) {
0337: // only one layout must exist
0338: if (this .layout != null) {
0339: removeConfigurationListener(this .layout);
0340: }
0341:
0342: if (layout == null) {
0343: this .layout = createLayout();
0344: } else {
0345: this .layout = layout;
0346: }
0347: }
0348:
0349: /**
0350: * Creates the associated layout object. This method is invoked when the
0351: * layout object is accessed and has not been created yet. Derived classes
0352: * can override this method to hook in a different layout implementation.
0353: *
0354: * @return the layout object to use
0355: * @since 1.3
0356: */
0357: protected PropertiesConfigurationLayout createLayout() {
0358: return new PropertiesConfigurationLayout(this );
0359: }
0360:
0361: /**
0362: * Load the properties from the given reader.
0363: * Note that the <code>clear()</code> method is not called, so
0364: * the properties contained in the loaded file will be added to the
0365: * actual set of properties.
0366: *
0367: * @param in An InputStream.
0368: *
0369: * @throws ConfigurationException if an error occurs
0370: */
0371: public synchronized void load(Reader in)
0372: throws ConfigurationException {
0373: boolean oldAutoSave = isAutoSave();
0374: setAutoSave(false);
0375:
0376: try {
0377: getLayout().load(in);
0378: } finally {
0379: setAutoSave(oldAutoSave);
0380: }
0381: }
0382:
0383: /**
0384: * Save the configuration to the specified stream.
0385: *
0386: * @param writer the output stream used to save the configuration
0387: * @throws ConfigurationException if an error occurs
0388: */
0389: public void save(Writer writer) throws ConfigurationException {
0390: enterNoReload();
0391: try {
0392: getLayout().save(writer);
0393: } finally {
0394: exitNoReload();
0395: }
0396: }
0397:
0398: /**
0399: * Extend the setBasePath method to turn includes
0400: * on and off based on the existence of a base path.
0401: *
0402: * @param basePath The new basePath to set.
0403: */
0404: public void setBasePath(String basePath) {
0405: super .setBasePath(basePath);
0406: setIncludesAllowed(StringUtils.isNotEmpty(basePath));
0407: }
0408:
0409: /**
0410: * Creates a copy of this object.
0411: *
0412: * @return the copy
0413: */
0414: public Object clone() {
0415: PropertiesConfiguration copy = (PropertiesConfiguration) super
0416: .clone();
0417: if (layout != null) {
0418: copy.setLayout(new PropertiesConfigurationLayout(copy,
0419: layout));
0420: }
0421: return copy;
0422: }
0423:
0424: /**
0425: * This method is invoked by the associated
0426: * <code>{@link PropertiesConfigurationLayout}</code> object for each
0427: * property definition detected in the parsed properties file. Its task is
0428: * to check whether this is a special property definition (e.g. the
0429: * <code>include</code> property). If not, the property must be added to
0430: * this configuration. The return value indicates whether the property
0431: * should be treated as a normal property. If it is <b>false</b>, the
0432: * layout object will ignore this property.
0433: *
0434: * @param key the property key
0435: * @param value the property value
0436: * @return a flag whether this is a normal property
0437: * @throws ConfigurationException if an error occurs
0438: * @since 1.3
0439: */
0440: boolean propertyLoaded(String key, String value)
0441: throws ConfigurationException {
0442: boolean result;
0443:
0444: if (StringUtils.isNotEmpty(getInclude())
0445: && key.equalsIgnoreCase(getInclude())) {
0446: if (getIncludesAllowed()) {
0447: String[] files;
0448: if (!isDelimiterParsingDisabled()) {
0449: files = StringUtils
0450: .split(value, getListDelimiter());
0451: } else {
0452: files = new String[] { value };
0453: }
0454: for (int i = 0; i < files.length; i++) {
0455: loadIncludeFile(files[i].trim());
0456: }
0457: }
0458: result = false;
0459: }
0460:
0461: else {
0462: addProperty(key, value);
0463: result = true;
0464: }
0465:
0466: return result;
0467: }
0468:
0469: /**
0470: * Tests whether a line is a comment, i.e. whether it starts with a comment
0471: * character.
0472: *
0473: * @param line the line
0474: * @return a flag if this is a comment line
0475: * @since 1.3
0476: */
0477: static boolean isCommentLine(String line) {
0478: String s = line.trim();
0479: // blanc lines are also treated as comment lines
0480: return s.length() < 1
0481: || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
0482: }
0483:
0484: /**
0485: * This class is used to read properties lines. These lines do
0486: * not terminate with new-line chars but rather when there is no
0487: * backslash sign a the end of the line. This is used to
0488: * concatenate multiple lines for readability.
0489: */
0490: public static class PropertiesReader extends LineNumberReader {
0491: /** Stores the comment lines for the currently processed property.*/
0492: private List commentLines;
0493:
0494: /** Stores the name of the last read property.*/
0495: private String propertyName;
0496:
0497: /** Stores the value of the last read property.*/
0498: private String propertyValue;
0499:
0500: /** Stores the list delimiter character.*/
0501: private char delimiter;
0502:
0503: /**
0504: * Constructor.
0505: *
0506: * @param reader A Reader.
0507: */
0508: public PropertiesReader(Reader reader) {
0509: this (reader, AbstractConfiguration
0510: .getDefaultListDelimiter());
0511: }
0512:
0513: /**
0514: * Creates a new instance of <code>PropertiesReader</code> and sets
0515: * the underlaying reader and the list delimiter.
0516: *
0517: * @param reader the reader
0518: * @param listDelimiter the list delimiter character
0519: * @since 1.3
0520: */
0521: public PropertiesReader(Reader reader, char listDelimiter) {
0522: super (reader);
0523: commentLines = new ArrayList();
0524: delimiter = listDelimiter;
0525: }
0526:
0527: /**
0528: * Reads a property line. Returns null if Stream is
0529: * at EOF. Concatenates lines ending with "\".
0530: * Skips lines beginning with "#" or "!" and empty lines.
0531: * The return value is a property definition (<code><name></code>
0532: * = <code><value></code>)
0533: *
0534: * @return A string containing a property value or null
0535: *
0536: * @throws IOException in case of an I/O error
0537: */
0538: public String readProperty() throws IOException {
0539: commentLines.clear();
0540: StringBuffer buffer = new StringBuffer();
0541:
0542: while (true) {
0543: String line = readLine();
0544: if (line == null) {
0545: // EOF
0546: return null;
0547: }
0548:
0549: if (isCommentLine(line)) {
0550: commentLines.add(line);
0551: continue;
0552: }
0553:
0554: line = line.trim();
0555:
0556: if (checkCombineLines(line)) {
0557: line = line.substring(0, line.length() - 1);
0558: buffer.append(line);
0559: } else {
0560: buffer.append(line);
0561: break;
0562: }
0563: }
0564: return buffer.toString();
0565: }
0566:
0567: /**
0568: * Parses the next property from the input stream and stores the found
0569: * name and value in internal fields. These fields can be obtained using
0570: * the provided getter methods. The return value indicates whether EOF
0571: * was reached (<b>false</b>) or whether further properties are
0572: * available (<b>true</b>).
0573: *
0574: * @return a flag if further properties are available
0575: * @throws IOException if an error occurs
0576: * @since 1.3
0577: */
0578: public boolean nextProperty() throws IOException {
0579: String line = readProperty();
0580:
0581: if (line == null) {
0582: return false; // EOF
0583: }
0584:
0585: // parse the line
0586: String[] property = parseProperty(line);
0587: propertyName = StringEscapeUtils.unescapeJava(property[0]);
0588: propertyValue = unescapeJava(property[1], delimiter);
0589: return true;
0590: }
0591:
0592: /**
0593: * Returns the comment lines that have been read for the last property.
0594: *
0595: * @return the comment lines for the last property returned by
0596: * <code>readProperty()</code>
0597: * @since 1.3
0598: */
0599: public List getCommentLines() {
0600: return commentLines;
0601: }
0602:
0603: /**
0604: * Returns the name of the last read property. This method can be called
0605: * after <code>{@link #nextProperty()}</code> was invoked and its
0606: * return value was <b>true</b>.
0607: *
0608: * @return the name of the last read property
0609: * @since 1.3
0610: */
0611: public String getPropertyName() {
0612: return propertyName;
0613: }
0614:
0615: /**
0616: * Returns the value of the last read property. This method can be
0617: * called after <code>{@link #nextProperty()}</code> was invoked and
0618: * its return value was <b>true</b>.
0619: *
0620: * @return the value of the last read property
0621: * @since 1.3
0622: */
0623: public String getPropertyValue() {
0624: return propertyValue;
0625: }
0626:
0627: /**
0628: * Checks if the passed in line should be combined with the following.
0629: * This is true, if the line ends with an odd number of backslashes.
0630: *
0631: * @param line the line
0632: * @return a flag if the lines should be combined
0633: */
0634: private static boolean checkCombineLines(String line) {
0635: int bsCount = 0;
0636: for (int idx = line.length() - 1; idx >= 0
0637: && line.charAt(idx) == '\\'; idx--) {
0638: bsCount++;
0639: }
0640:
0641: return bsCount % 2 == 1;
0642: }
0643:
0644: /**
0645: * Parse a property line and return the key and the value in an array.
0646: *
0647: * @param line the line to parse
0648: * @return an array with the property's key and value
0649: * @since 1.2
0650: */
0651: private static String[] parseProperty(String line) {
0652: // sorry for this spaghetti code, please replace it as soon as
0653: // possible with a regexp when the Java 1.3 requirement is dropped
0654:
0655: String[] result = new String[2];
0656: StringBuffer key = new StringBuffer();
0657: StringBuffer value = new StringBuffer();
0658:
0659: // state of the automaton:
0660: // 0: key parsing
0661: // 1: antislash found while parsing the key
0662: // 2: separator crossing
0663: // 3: value parsing
0664: int state = 0;
0665:
0666: for (int pos = 0; pos < line.length(); pos++) {
0667: char c = line.charAt(pos);
0668:
0669: switch (state) {
0670: case 0:
0671: if (c == '\\') {
0672: state = 1;
0673: } else if (ArrayUtils.contains(WHITE_SPACE, c)) {
0674: // switch to the separator crossing state
0675: state = 2;
0676: } else if (ArrayUtils.contains(SEPARATORS, c)) {
0677: // switch to the value parsing state
0678: state = 3;
0679: } else {
0680: key.append(c);
0681: }
0682:
0683: break;
0684:
0685: case 1:
0686: if (ArrayUtils.contains(SEPARATORS, c)
0687: || ArrayUtils.contains(WHITE_SPACE, c)) {
0688: // this is an escaped separator or white space
0689: key.append(c);
0690: } else {
0691: // another escaped character, the '\' is preserved
0692: key.append('\\');
0693: key.append(c);
0694: }
0695:
0696: // return to the key parsing state
0697: state = 0;
0698:
0699: break;
0700:
0701: case 2:
0702: if (ArrayUtils.contains(WHITE_SPACE, c)) {
0703: // do nothing, eat all white spaces
0704: state = 2;
0705: } else if (ArrayUtils.contains(SEPARATORS, c)) {
0706: // switch to the value parsing state
0707: state = 3;
0708: } else {
0709: // any other character indicates we encoutered the beginning of the value
0710: value.append(c);
0711:
0712: // switch to the value parsing state
0713: state = 3;
0714: }
0715:
0716: break;
0717:
0718: case 3:
0719: value.append(c);
0720: break;
0721: }
0722: }
0723:
0724: result[0] = key.toString().trim();
0725: result[1] = value.toString().trim();
0726:
0727: return result;
0728: }
0729: } // class PropertiesReader
0730:
0731: /**
0732: * This class is used to write properties lines.
0733: */
0734: public static class PropertiesWriter extends FilterWriter {
0735: /** The delimiter for multi-valued properties.*/
0736: private char delimiter;
0737:
0738: /**
0739: * Constructor.
0740: *
0741: * @param writer a Writer object providing the underlying stream
0742: * @param delimiter the delimiter character for multi-valued properties
0743: */
0744: public PropertiesWriter(Writer writer, char delimiter) {
0745: super (writer);
0746: this .delimiter = delimiter;
0747: }
0748:
0749: /**
0750: * Write a property.
0751: *
0752: * @param key the key of the property
0753: * @param value the value of the property
0754: *
0755: * @throws IOException if an I/O error occurs
0756: */
0757: public void writeProperty(String key, Object value)
0758: throws IOException {
0759: writeProperty(key, value, false);
0760: }
0761:
0762: /**
0763: * Write a property.
0764: *
0765: * @param key The key of the property
0766: * @param values The array of values of the property
0767: *
0768: * @throws IOException if an I/O error occurs
0769: */
0770: public void writeProperty(String key, List values)
0771: throws IOException {
0772: for (int i = 0; i < values.size(); i++) {
0773: writeProperty(key, values.get(i));
0774: }
0775: }
0776:
0777: /**
0778: * Writes the given property and its value. If the value happens to be a
0779: * list, the <code>forceSingleLine</code> flag is evaluated. If it is
0780: * set, all values are written on a single line using the list delimiter
0781: * as separator.
0782: *
0783: * @param key the property key
0784: * @param value the property value
0785: * @param forceSingleLine the "force single line" flag
0786: * @throws IOException if an error occurs
0787: * @since 1.3
0788: */
0789: public void writeProperty(String key, Object value,
0790: boolean forceSingleLine) throws IOException {
0791: String v;
0792:
0793: if (value instanceof List) {
0794: List values = (List) value;
0795: if (forceSingleLine) {
0796: v = makeSingleLineValue(values);
0797: } else {
0798: writeProperty(key, values);
0799: return;
0800: }
0801: } else {
0802: v = escapeValue(value);
0803: }
0804:
0805: write(escapeKey(key));
0806: write(" = ");
0807: write(v);
0808:
0809: writeln(null);
0810: }
0811:
0812: /**
0813: * Write a comment.
0814: *
0815: * @param comment the comment to write
0816: * @throws IOException if an I/O error occurs
0817: */
0818: public void writeComment(String comment) throws IOException {
0819: writeln("# " + comment);
0820: }
0821:
0822: /**
0823: * Escape the separators in the key.
0824: *
0825: * @param key the key
0826: * @return the escaped key
0827: * @since 1.2
0828: */
0829: private String escapeKey(String key) {
0830: StringBuffer newkey = new StringBuffer();
0831:
0832: for (int i = 0; i < key.length(); i++) {
0833: char c = key.charAt(i);
0834:
0835: if (ArrayUtils.contains(SEPARATORS, c)
0836: || ArrayUtils.contains(WHITE_SPACE, c)) {
0837: // escape the separator
0838: newkey.append('\\');
0839: newkey.append(c);
0840: } else {
0841: newkey.append(c);
0842: }
0843: }
0844:
0845: return newkey.toString();
0846: }
0847:
0848: /**
0849: * Escapes the given property value. Delimiter characters in the value
0850: * will be escaped.
0851: *
0852: * @param value the property value
0853: * @return the escaped property value
0854: * @since 1.3
0855: */
0856: private String escapeValue(Object value) {
0857: String v = StringEscapeUtils.escapeJava(String
0858: .valueOf(value));
0859: return StringUtils.replace(v, String.valueOf(delimiter),
0860: "\\" + delimiter);
0861: }
0862:
0863: /**
0864: * Transforms a list of values into a single line value.
0865: *
0866: * @param values the list with the values
0867: * @return a string with the single line value (can be <b>null</b>)
0868: * @since 1.3
0869: */
0870: private String makeSingleLineValue(List values) {
0871: if (!values.isEmpty()) {
0872: Iterator it = values.iterator();
0873: StringBuffer buf = new StringBuffer(escapeValue(it
0874: .next()));
0875: while (it.hasNext()) {
0876: buf.append(delimiter);
0877: buf.append(escapeValue(it.next()));
0878: }
0879: return buf.toString();
0880: } else {
0881: return null;
0882: }
0883: }
0884:
0885: /**
0886: * Helper method for writing a line with the platform specific line
0887: * ending.
0888: *
0889: * @param s the content of the line (may be <b>null</b>)
0890: * @throws IOException if an error occurs
0891: * @since 1.3
0892: */
0893: public void writeln(String s) throws IOException {
0894: if (s != null) {
0895: write(s);
0896: }
0897: write(LINE_SEPARATOR);
0898: }
0899:
0900: } // class PropertiesWriter
0901:
0902: /**
0903: * <p>Unescapes any Java literals found in the <code>String</code> to a
0904: * <code>Writer</code>.</p> This is a slightly modified version of the
0905: * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
0906: * drop escaped separators (i.e '\,').
0907: *
0908: * @param str the <code>String</code> to unescape, may be null
0909: * @param delimiter the delimiter for multi-valued properties
0910: * @return the processed string
0911: * @throws IllegalArgumentException if the Writer is <code>null</code>
0912: */
0913: protected static String unescapeJava(String str, char delimiter) {
0914: if (str == null) {
0915: return null;
0916: }
0917: int sz = str.length();
0918: StringBuffer out = new StringBuffer(sz);
0919: StringBuffer unicode = new StringBuffer(UNICODE_LEN);
0920: boolean hadSlash = false;
0921: boolean inUnicode = false;
0922: for (int i = 0; i < sz; i++) {
0923: char ch = str.charAt(i);
0924: if (inUnicode) {
0925: // if in unicode, then we're reading unicode
0926: // values in somehow
0927: unicode.append(ch);
0928: if (unicode.length() == UNICODE_LEN) {
0929: // unicode now contains the four hex digits
0930: // which represents our unicode character
0931: try {
0932: int value = Integer.parseInt(
0933: unicode.toString(), HEX_RADIX);
0934: out.append((char) value);
0935: unicode.setLength(0);
0936: inUnicode = false;
0937: hadSlash = false;
0938: } catch (NumberFormatException nfe) {
0939: throw new ConfigurationRuntimeException(
0940: "Unable to parse unicode value: "
0941: + unicode, nfe);
0942: }
0943: }
0944: continue;
0945: }
0946:
0947: if (hadSlash) {
0948: // handle an escaped value
0949: hadSlash = false;
0950:
0951: if (ch == '\\') {
0952: out.append('\\');
0953: } else if (ch == '\'') {
0954: out.append('\'');
0955: } else if (ch == '\"') {
0956: out.append('"');
0957: } else if (ch == 'r') {
0958: out.append('\r');
0959: } else if (ch == 'f') {
0960: out.append('\f');
0961: } else if (ch == 't') {
0962: out.append('\t');
0963: } else if (ch == 'n') {
0964: out.append('\n');
0965: } else if (ch == 'b') {
0966: out.append('\b');
0967: } else if (ch == delimiter) {
0968: out.append('\\');
0969: out.append(delimiter);
0970: } else if (ch == 'u') {
0971: // uh-oh, we're in unicode country....
0972: inUnicode = true;
0973: } else {
0974: out.append(ch);
0975: }
0976:
0977: continue;
0978: } else if (ch == '\\') {
0979: hadSlash = true;
0980: continue;
0981: }
0982: out.append(ch);
0983: }
0984:
0985: if (hadSlash) {
0986: // then we're in the weird case of a \ at the end of the
0987: // string, let's output it anyway.
0988: out.append('\\');
0989: }
0990:
0991: return out.toString();
0992: }
0993:
0994: /**
0995: * Helper method for loading an included properties file. This method is
0996: * called by <code>load()</code> when an <code>include</code> property
0997: * is encountered. It tries to resolve relative file names based on the
0998: * current base path. If this fails, a resolution based on the location of
0999: * this properties file is tried.
1000: *
1001: * @param fileName the name of the file to load
1002: * @throws ConfigurationException if loading fails
1003: */
1004: private void loadIncludeFile(String fileName)
1005: throws ConfigurationException {
1006: URL url = ConfigurationUtils.locate(getBasePath(), fileName);
1007: if (url == null) {
1008: URL baseURL = getURL();
1009: if (baseURL != null) {
1010: url = ConfigurationUtils.locate(baseURL.toString(),
1011: fileName);
1012: }
1013: }
1014:
1015: if (url == null) {
1016: throw new ConfigurationException(
1017: "Cannot resolve include file " + fileName);
1018: }
1019: load(url);
1020: }
1021: }
|