Author: oheger Date: Sat Jul 22 10:02:38 2006 New Revision: 424604 URL: http://svn.apache.org/viewvc?rev=424604&view=rev Log: Introduced new PropertiesConfigurationLayout class for preserving the structure of properties files
Added: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java (with props) Modified: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java Modified: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java?rev=424604&r1=424603&r2=424604&view=diff ============================================================================== --- jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java (original) +++ jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfiguration.java Sat Jul 22 10:02:38 2006 @@ -16,16 +16,14 @@ package org.apache.commons.configuration; -import java.io.BufferedReader; import java.io.File; import java.io.FilterWriter; import java.io.IOException; import java.io.LineNumberReader; import java.io.Reader; -import java.io.StringReader; import java.io.Writer; import java.net.URL; -import java.util.Date; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -134,6 +132,14 @@ * second.prop = ${first.prop}/second * </pre> * + * <p>A <code>PropertiesConfiguration</code> object is associated with an + * instance of the <code>[EMAIL PROTECTED] PropertiesConfigurationLayout}</code> class, + * which is responsible for storing the layout of the parsed properties file + * (i.e. empty lines, comments, and such things). The <code>getLayout()</code> + * method can be used to obtain this layout object. With <code>setLayout()</code> + * a new layout object can be set. This should be done before a properties file + * was loaded. + * * @see java.util.Properties#load * * @author <a href="mailto:[EMAIL PROTECTED]">Stefano Mazzocchi</a> @@ -175,18 +181,21 @@ /** Constant for the platform specific line separator.*/ private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + /** Constant for the supported comment characters.*/ + static final String COMMENT_CHARS = "#!"; + /** Constant for the radix of hex numbers.*/ private static final int HEX_RADIX = 16; /** Constant for the length of a unicode literal.*/ private static final int UNICODE_LEN = 4; + /** Stores the layout object.*/ + private PropertiesConfigurationLayout layout; + /** Allow file inclusion or not */ private boolean includesAllowed; - /** Comment header of the .properties file */ - private String header; - // initialization block to set the encoding before loading the file in the constructors { setEncoding(DEFAULT_ENCODING); @@ -293,7 +302,7 @@ */ public String getHeader() { - return header; + return getLayout().getHeaderComment(); } /** @@ -304,7 +313,47 @@ */ public void setHeader(String header) { - this.header = header; + getLayout().setHeaderComment(header); + } + + /** + * Returns the associated layout object. + * + * @return the associated layout object + * @since 1.3 + */ + public synchronized PropertiesConfigurationLayout getLayout() + { + if (layout == null) + { + layout = createLayout(); + } + return layout; + } + + /** + * Sets the associated layout object. + * + * @param layout the new layout object; can be <b>null</b>, then a new + * layout object will be created + * @since 1.3 + */ + public void setLayout(PropertiesConfigurationLayout layout) + { + this.layout = layout; + } + + /** + * Creates the associated layout object. This method is invoked when the + * layout object is accessed and has not been created yet. Derived classes + * can override this method to hook in a different layout implementation. + * + * @return the layout object to use + * @since 1.3 + */ + protected PropertiesConfigurationLayout createLayout() + { + return new PropertiesConfigurationLayout(this); } /** @@ -319,60 +368,12 @@ */ public synchronized void load(Reader in) throws ConfigurationException { - PropertiesReader reader = new PropertiesReader(in); boolean oldAutoSave = isAutoSave(); setAutoSave(false); try { - while (true) - { - String line = reader.readProperty(); - - if (line == null) - { - break; // EOF - } - - // parse the line - String[] property = parseProperty(line); - String key = property[0]; - String value = property[1]; - - // Though some software (e.g. autoconf) may produce - // empty values like foo=\n, emulate the behavior of - // java.util.Properties by setting the value to the - // empty string. - - if (StringUtils.isNotEmpty(getInclude()) && key.equalsIgnoreCase(getInclude())) - { - if (getIncludesAllowed()) - { - String [] files; - if (!isDelimiterParsingDisabled()) - { - files = StringUtils.split(value, getListDelimiter()); - } - else - { - files = new String[]{value}; - } - for (int i = 0; i < files.length; i++) - { - loadIncludeFile(files[i].trim()); - } - } - } - else - { - addProperty(StringEscapeUtils.unescapeJava(key), unescapeJava(value, getListDelimiter())); - } - - } - } - catch (IOException ioe) - { - throw new ConfigurationException("Could not load configuration from input stream.", ioe); + getLayout().load(in); } finally { @@ -391,71 +392,114 @@ enterNoReload(); try { - PropertiesWriter out = new PropertiesWriter(writer, getListDelimiter()); + getLayout().save(writer); + } + finally + { + exitNoReload(); + } + } - if (header != null) - { - BufferedReader reader = new BufferedReader(new StringReader(header)); - String line; - while ((line = reader.readLine()) != null) - { - out.writeComment(line); - } - out.writeln(null); - } + /** + * Extend the setBasePath method to turn includes + * on and off based on the existence of a base path. + * + * @param basePath The new basePath to set. + */ + public void setBasePath(String basePath) + { + super.setBasePath(basePath); + setIncludesAllowed(StringUtils.isNotEmpty(basePath)); + } - out.writeComment("written by PropertiesConfiguration"); - out.writeComment(new Date().toString()); - out.writeln(null); + /** + * This method is invoked by the associated + * <code>[EMAIL PROTECTED] PropertiesConfigurationLayout}</code> object for each + * property definition detected in the parsed properties file. Its task is + * to check whether this is a special property definition (e.g. the + * <code>include</code> property). If not, the property must be added to + * this configuration. The return value indicates whether the property + * should be treated as a normal property. If it is <b>false</b>, the + * layout object will ignore this property. + * + * @param key the property key + * @param value the property value + * @return a flag whether this is a normal property + * @throws ConfigurationException if an error occurs + * @since 1.3 + */ + boolean propertyLoaded(String key, String value) + throws ConfigurationException + { + boolean result; - Iterator keys = getKeys(); - while (keys.hasNext()) + if (StringUtils.isNotEmpty(getInclude()) + && key.equalsIgnoreCase(getInclude())) + { + if (getIncludesAllowed()) { - String key = (String) keys.next(); - Object value = getProperty(key); - - if (value instanceof List) + String[] files; + if (!isDelimiterParsingDisabled()) { - out.writeProperty(key, (List) value); + files = StringUtils.split(value, getListDelimiter()); } else { - out.writeProperty(key, value); + files = new String[] + { value }; + } + for (int i = 0; i < files.length; i++) + { + loadIncludeFile(files[i].trim()); } } - - out.flush(); - } - catch (IOException e) - { - throw new ConfigurationException(e.getMessage(), e); + result = false; } - finally + + else { - exitNoReload(); + addProperty(key, value); + result = true; } + + return result; } /** - * Extend the setBasePath method to turn includes - * on and off based on the existence of a base path. + * Tests whether a line is a comment, i.e. whether it starts with a comment + * character. * - * @param basePath The new basePath to set. + * @param line the line + * @return a flag if this is a comment line + * @since 1.3 */ - public void setBasePath(String basePath) + static boolean isCommentLine(String line) { - super.setBasePath(basePath); - setIncludesAllowed(StringUtils.isNotEmpty(basePath)); + String s = line.trim(); + // blanc lines are also treated as comment lines + return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; } /** - * This class is used to read properties lines. These lines do + * This class is used to read properties lines. These lines do * not terminate with new-line chars but rather when there is no * backslash sign a the end of the line. This is used to * concatenate multiple lines for readability. */ public static class PropertiesReader extends LineNumberReader { + /** Stores the comment lines for the currently processed property.*/ + private List commentLines; + + /** Stores the name of the last read property.*/ + private String propertyName; + + /** Stores the value of the last read property.*/ + private String propertyValue; + + /** Stores the list delimiter character.*/ + private char delimiter; + /** * Constructor. * @@ -463,13 +507,30 @@ */ public PropertiesReader(Reader reader) { + this(reader, AbstractConfiguration.getDefaultListDelimiter()); + } + + /** + * Creates a new instance of <code>PropertiesReader</code> and sets + * the underlaying reader and the list delimiter. + * + * @param reader the reader + * @param listDelimiter the list delimiter character + * @since 1.3 + */ + public PropertiesReader(Reader reader, char listDelimiter) + { super(reader); + commentLines = new ArrayList(); + delimiter = listDelimiter; } /** - * Read a property. Returns null if Stream is + * Reads a property line. Returns null if Stream is * at EOF. Concatenates lines ending with "\". * Skips lines beginning with "#" or "!" and empty lines. + * The return value is a property definition (<code><name></code> + * = <code><value></code>) * * @return A string containing a property value or null * @@ -477,6 +538,7 @@ */ public String readProperty() throws IOException { + commentLines.clear(); StringBuffer buffer = new StringBuffer(); while (true) @@ -488,14 +550,14 @@ return null; } - line = line.trim(); - - // skip comments and empty lines - if (StringUtils.isEmpty(line) || (line.charAt(0) == '#') || (line.charAt(0) == '!')) + if (isCommentLine(line)) { + commentLines.add(line); continue; } + line = line.trim(); + if (checkCombineLines(line)) { line = line.substring(0, line.length() - 1); @@ -511,6 +573,71 @@ } /** + * Parses the next property from the input stream and stores the found + * name and value in internal fields. These fields can be obtained using + * the provided getter methods. The return value indicates whether EOF + * was reached (<b>false</b>) or whether further properties are + * available (<b>true</b>). + * + * @return a flag if further properties are available + * @throws IOException if an error occurs + * @since 1.3 + */ + public boolean nextProperty() throws IOException + { + String line = readProperty(); + + if (line == null) + { + return false; // EOF + } + + // parse the line + String[] property = parseProperty(line); + propertyName = StringEscapeUtils.unescapeJava(property[0]); + propertyValue = unescapeJava(property[1], delimiter); + return true; + } + + /** + * Returns the comment lines that have been read for the last property. + * + * @return the comment lines for the last property returned by + * <code>readProperty()</code> + * @since 1.3 + */ + public List getCommentLines() + { + return commentLines; + } + + /** + * Returns the name of the last read property. This method can be called + * after <code>[EMAIL PROTECTED] #nextProperty()}</code> was invoked and its + * return value was <b>true</b>. + * + * @return the name of the last read property + * @since 1.3 + */ + public String getPropertyName() + { + return propertyName; + } + + /** + * Returns the value of the last read property. This method can be + * called after <code>[EMAIL PROTECTED] #nextProperty()}</code> was invoked and + * its return value was <b>true</b>. + * + * @return the value of the last read property + * @since 1.3 + */ + public String getPropertyValue() + { + return propertyValue; + } + + /** * Checks if the passed in line should be combined with the following. * This is true, if the line ends with an odd number of backslashes. * @@ -527,6 +654,109 @@ return bsCount % 2 == 1; } + + /** + * Parse a property line and return the key and the value in an array. + * + * @param line the line to parse + * @return an array with the property's key and value + * @since 1.2 + */ + private static String[] parseProperty(String line) + { + // sorry for this spaghetti code, please replace it as soon as + // possible with a regexp when the Java 1.3 requirement is dropped + + String[] result = new String[2]; + StringBuffer key = new StringBuffer(); + StringBuffer value = new StringBuffer(); + + // state of the automaton: + // 0: key parsing + // 1: antislash found while parsing the key + // 2: separator crossing + // 3: value parsing + int state = 0; + + for (int pos = 0; pos < line.length(); pos++) + { + char c = line.charAt(pos); + + switch (state) + { + case 0: + if (c == '\\') + { + state = 1; + } + else if (ArrayUtils.contains(WHITE_SPACE, c)) + { + // switch to the separator crossing state + state = 2; + } + else if (ArrayUtils.contains(SEPARATORS, c)) + { + // switch to the value parsing state + state = 3; + } + else + { + key.append(c); + } + + break; + + case 1: + if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) + { + // this is an escaped separator or white space + key.append(c); + } + else + { + // another escaped character, the '\' is preserved + key.append('\\'); + key.append(c); + } + + // return to the key parsing state + state = 0; + + break; + + case 2: + if (ArrayUtils.contains(WHITE_SPACE, c)) + { + // do nothing, eat all white spaces + state = 2; + } + else if (ArrayUtils.contains(SEPARATORS, c)) + { + // switch to the value parsing state + state = 3; + } + else + { + // any other character indicates we encoutered the beginning of the value + value.append(c); + + // switch to the value parsing state + state = 3; + } + + break; + + case 3: + value.append(c); + break; + } + } + + result[0] = key.toString().trim(); + result[1] = value.toString().trim(); + + return result; + } } // class PropertiesReader /** @@ -559,16 +789,7 @@ */ public void writeProperty(String key, Object value) throws IOException { - write(escapeKey(key)); - write(" = "); - if (value != null) - { - String v = StringEscapeUtils.escapeJava(String.valueOf(value)); - v = StringUtils.replace(v, String.valueOf(delimiter), "\\" + delimiter); - write(v); - } - - writeln(null); + writeProperty(key, value, false); } /** @@ -588,6 +809,48 @@ } /** + * Writes the given property and its value. If the value happens to be a + * list, the <code>forceSingleLine</code> flag is evaluated. If it is + * set, all values are written on a single line using the list delimiter + * as separator. + * + * @param key the property key + * @param value the property value + * @param forceSingleLine the "force single line" flag + * @throws IOException if an error occurs + * @since 1.3 + */ + public void writeProperty(String key, Object value, + boolean forceSingleLine) throws IOException + { + String v; + + if (value instanceof List) + { + List values = (List) value; + if (forceSingleLine) + { + v = makeSingleLineValue(values); + } + else + { + writeProperty(key, values); + return; + } + } + else + { + v = escapeValue(value); + } + + write(escapeKey(key)); + write(" = "); + write(v); + + writeln(null); + } + + /** * Write a comment. * * @param comment the comment to write @@ -629,13 +892,55 @@ } /** + * Escapes the given property value. Delimiter characters in the value + * will be escaped. + * + * @param value the property value + * @return the escaped property value + * @since 1.3 + */ + private String escapeValue(Object value) + { + String v = StringEscapeUtils.escapeJava(String.valueOf(value)); + return StringUtils.replace(v, String.valueOf(delimiter), "\\" + + delimiter); + } + + /** + * Transforms a list of values into a single line value. + * + * @param values the list with the values + * @return a string with the single line value (can be <b>null</b>) + * @since 1.3 + */ + private String makeSingleLineValue(List values) + { + if (!values.isEmpty()) + { + Iterator it = values.iterator(); + StringBuffer buf = new StringBuffer(escapeValue(it.next())); + while (it.hasNext()) + { + buf.append(delimiter); + buf.append(escapeValue(it.next())); + } + return buf.toString(); + } + else + { + return null; + } + } + + /** * Helper method for writing a line with the platform specific line * ending. * * @param s the content of the line (may be <b>null</b>) * @throws IOException if an error occurs + * @since 1.3 */ - private void writeln(String s) throws IOException + public void writeln(String s) throws IOException { if (s != null) { @@ -766,109 +1071,6 @@ } return out.toString(); - } - - /** - * Parse a property line and return the key and the value in an array. - * - * @param line the line to parse - * @return an array with the property's key and value - * @since 1.2 - */ - private String[] parseProperty(String line) - { - // sorry for this spaghetti code, please replace it as soon as - // possible with a regexp when the Java 1.3 requirement is dropped - - String[] result = new String[2]; - StringBuffer key = new StringBuffer(); - StringBuffer value = new StringBuffer(); - - // state of the automaton: - // 0: key parsing - // 1: antislash found while parsing the key - // 2: separator crossing - // 3: value parsing - int state = 0; - - for (int pos = 0; pos < line.length(); pos++) - { - char c = line.charAt(pos); - - switch (state) - { - case 0: - if (c == '\\') - { - state = 1; - } - else if (ArrayUtils.contains(WHITE_SPACE, c)) - { - // switch to the separator crossing state - state = 2; - } - else if (ArrayUtils.contains(SEPARATORS, c)) - { - // switch to the value parsing state - state = 3; - } - else - { - key.append(c); - } - - break; - - case 1: - if (ArrayUtils.contains(SEPARATORS, c) || ArrayUtils.contains(WHITE_SPACE, c)) - { - // this is an escaped separator or white space - key.append(c); - } - else - { - // another escaped character, the '\' is preserved - key.append('\\'); - key.append(c); - } - - // return to the key parsing state - state = 0; - - break; - - case 2: - if (ArrayUtils.contains(WHITE_SPACE, c)) - { - // do nothing, eat all white spaces - state = 2; - } - else if (ArrayUtils.contains(SEPARATORS, c)) - { - // switch to the value parsing state - state = 3; - } - else - { - // any other character indicates we encoutered the beginning of the value - value.append(c); - - // switch to the value parsing state - state = 3; - } - - break; - - case 3: - value.append(c); - break; - } - } - - result[0] = key.toString().trim(); - result[1] = value.toString().trim(); - - return result; } /** Added: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java URL: http://svn.apache.org/viewvc/jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java?rev=424604&view=auto ============================================================================== --- jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java (added) +++ jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java Sat Jul 22 10:02:38 2006 @@ -0,0 +1,802 @@ +/* + * Copyright 2001-2006 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.configuration; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.commons.configuration.event.ConfigurationEvent; +import org.apache.commons.configuration.event.ConfigurationListener; +import org.apache.commons.lang.StringUtils; + +/** + * <p> + * A helper class used by <code>[EMAIL PROTECTED] PropertiesConfiguration}</code> to keep + * the layout of a properties file. + * </p> + * <p> + * Instances of this class are associated with a + * <code>PropertiesConfiguration</code> object. They are responsible for + * analyzing properties files and for extracting as much information about the + * file layout (e.g. empty lines, comments) as possible. When the properties + * file is written back again it should be close to the original. + * </p> + * <p> + * The <code>PropertiesConfigurationLayout</code> object associated with a + * <code>PropertiesConfiguration</code> object can be obtained using the + * <code>getLayout()</code> method of the configuration. Then the methods + * provided by this class can be used to alter the properties file's layout. + * </p> + * <p> + * Implementation note: This is a very simple implementation, which is far away + * from being perfect, i.e. the original layout of a properties file won't be + * reproduced in all cases. One limitation is that comments for multi-valued + * property keys are concatenated. Maybe this implementation can later be + * improved. + * </p> + * <p> + * To get an impression how this class works consider the following properties + * file: + * </p> + * <p> + * + * <pre> + * # A demo configuration file + * # for Demo App 1.42 + * + * # Application name + * AppName=Demo App + * + * # Application vendor + * AppVendor=DemoSoft + * + * + * # GUI properties + * # Window Color + * windowColors=0xFFFFFF,0x000000 + * + * # Include some setting + * include=settings.properties + * # Another vendor + * AppVendor=TestSoft + * </pre> + * + * </p> + * <p> + * For this example the following points are relevant: + * </p> + * <p> + * <ul> + * <li>The first two lines are set as header comment. The header comment is + * determined by the last blanc line before the first property definition.</li> + * <li>For the property <code>AppName</code> one comment line and one + * leading blanc line is stored.</li> + * <li>For the property <code>windowColors</code> two comment lines and two + * leading blanc lines are stored.</li> + * <li>Include files is something this class cannot deal with well. When saving + * the properties configuration back, the included properties are simply + * contained in the original file. The comment before the include property is + * skipped.</li> + * <li>For all properties except for <code>AppVendor</code> the "single + * line" flag is set. This is relevant only for <code>windowColors</code>, + * which has multiple values defined in one line using the separator character.</li> + * <li>The <code>AppVendor</code> property appears twice. The comment lines + * are concatenated, so that <code>layout.getComment("AppVendor");</code> will + * result in <code>Application vendor<CR>Another vendor</code>, whith + * <code><CR></code> meaning the line separator. In addition the + * "single line" flag is set to <b>false</b> for this property. When + * the file is saved, two property definitions will be written (in series).</li> + * </ul> + * </p> + * + * @author <a + * href="http://jakarta.apache.org/commons/configuration/team-list.html">Commons + * Configuration team</a> + * @version $Id$ + * @since 1.3 + */ +public class PropertiesConfigurationLayout implements ConfigurationListener +{ + /** Constant for the line break character. */ + private static final String CR = System.getProperty("line.separator"); + + /** Constant for the default comment prefix. */ + private static final String COMMENT_PREFIX = "# "; + + /** Stores the associated configuration object. */ + private PropertiesConfiguration configuration; + + /** Stores a map with the contained layout information. */ + private Map layoutData; + + /** Stores the header comment. */ + private String headerComment; + + /** A counter for determining nested load calls. */ + private int loadCounter; + + /** Stores the force single line flag. */ + private boolean forceSingleLine; + + /** + * Creates a new instance of <code>PropertiesConfigurationLayout</code> + * and initializes it with the associated configuration object. + * + * @param config the configuration (must not be <b>null</b>) + */ + public PropertiesConfigurationLayout(PropertiesConfiguration config) + { + if (config == null) + { + throw new IllegalArgumentException( + "Configuration must not be null!"); + } + configuration = config; + layoutData = new LinkedMap(); + config.addConfigurationListener(this); + } + + /** + * Returns the associated configuration object. + * + * @return the associated configuration + */ + public PropertiesConfiguration getConfiguration() + { + return configuration; + } + + /** + * Returns the comment for the specified property key in a cononical form. + * "Canonical" means that either all lines start with a comment + * character or none. The <code>commentChar</code> parameter is <b>false</b>, + * all comment characters are removed, so that the result is only the plain + * text of the comment. Otherwise it is ensured that each line of the + * comment starts with a comment character. + * + * @param key the key of the property + * @param commentChar determines whether all lines should start with comment + * characters or not + * @return the canonical comment for this key (can be <b>null</b>) + */ + public String getCanonicalComment(String key, boolean commentChar) + { + String comment = getComment(key); + if (comment == null) + { + return null; + } + else + { + return trimComment(comment, commentChar); + } + } + + /** + * Returns the comment for the specified property key. The comment is + * returned as it was set (either manually by calling + * <code>setComment()</code> or when it was loaded from a properties + * file). No modifications are performed. + * + * @param key the key of the property + * @return the comment for this key (can be <b>null</b>) + */ + public String getComment(String key) + { + return fetchLayoutData(key).getComment(); + } + + /** + * Sets the comment for the specified property key. The comment (or its + * single lines if it is a multi-line comment) can start with a comment + * character. If this is the case, it will be written without changes. + * Otherwise a default comment character is added automatically. + * + * @param key the key of the property + * @param comment the comment for this key (can be <b>null</b>, then the + * comment will be removed) + */ + public void setComment(String key, String comment) + { + fetchLayoutData(key).setComment(comment); + } + + /** + * Returns the number of blanc lines before this property key. If this key + * does not exist, 0 will be returned. + * + * @param key the property key + * @return the number of blanc lines before the property definition for this + * key + */ + public int getBlancLinesBefore(String key) + { + return fetchLayoutData(key).getBlancLines(); + } + + /** + * Sets the number of blanc lines before the given property key. This can be + * used for a logical grouping of properties. + * + * @param key the property key + * @param number the number of blanc lines to add before this property + * definition + */ + public void setBlancLinesBefore(String key, int number) + { + fetchLayoutData(key).setBlancLines(number); + } + + /** + * Returns the header comment of the represented properties file in a + * canonical form. With the <code>commentChar</code> parameter it can be + * specified whether comment characters should be stripped or be always + * present. + * + * @param commentChar determines the presence of comment characters + * @return the header comment (can be <b>null</b>) + */ + public String getCanonicalHeaderComment(boolean commentChar) + { + return (getHeaderComment() == null) ? null : trimComment( + getHeaderComment(), commentChar); + } + + /** + * Returns the header comment of the represented properties file. This + * method returns the header comment exactly as it was set using + * <code>setHeaderComment()</code> or extracted from the loaded properties + * file. + * + * @return the header comment (can be <b>null</b>) + */ + public String getHeaderComment() + { + return headerComment; + } + + /** + * Sets the header comment for the represented properties file. This comment + * will be output on top of the file. + * + * @param comment the comment + */ + public void setHeaderComment(String comment) + { + headerComment = comment; + } + + /** + * Returns a flag whether the specified property is defined on a single + * line. This is meaningful only if this property has multiple values. + * + * @param key the property key + * @return a flag if this property is defined on a single line + */ + public boolean isSingleLine(String key) + { + return fetchLayoutData(key).isSingleLine(); + } + + /** + * Sets the "single line flag" for the specified property key. + * This flag is evaluated if the property has multiple values (i.e. if it is + * a list property). In this case, if the flag is set, all values will be + * written in a single property definition using the list delimiter as + * separator. Otherwise multiple lines will be written for this property, + * each line containing one property value. + * + * @param key the property key + * @param f the single line flag + */ + public void setSingleLine(String key, boolean f) + { + fetchLayoutData(key).setSingleLine(f); + } + + /** + * Returns the "force single line" flag. + * + * @return the force single line flag + * @see #setForceSingleLine(boolean) + */ + public boolean isForceSingleLine() + { + return forceSingleLine; + } + + /** + * Sets the "force single line" flag. If this flag is set, all + * properties with multiple values are written on single lines. This mode + * provides more compatibility with <code>java.lang.Properties</code>, + * which cannot deal with multiple definitions of a single property. + * + * @param f the force single line flag + */ + public void setForceSingleLine(boolean f) + { + forceSingleLine = f; + } + + /** + * Returns a set with all property keys managed by this object. + * + * @return a set with all contained property keys + */ + public Set getKeys() + { + return layoutData.keySet(); + } + + /** + * Reads a properties file and stores its internal structure. The found + * properties will be added to the associated configuration object. + * + * @param in the reader to the properties file + * @throws ConfigurationException if an error occurs + */ + public void load(Reader in) throws ConfigurationException + { + if (++loadCounter == 1) + { + getConfiguration().removeConfigurationListener(this); + } + PropertiesConfiguration.PropertiesReader reader = new PropertiesConfiguration.PropertiesReader( + in, getConfiguration().getListDelimiter()); + + try + { + while (reader.nextProperty()) + { + if (getConfiguration().propertyLoaded(reader.getPropertyName(), + reader.getPropertyValue())) + { + boolean contained = layoutData.containsKey(reader + .getPropertyName()); + int blancLines = 0; + int idx = checkHeaderComment(reader.getCommentLines()); + while (idx < reader.getCommentLines().size() + && ((String) reader.getCommentLines().get(idx)) + .length() < 1) + { + idx++; + blancLines++; + } + String comment = extractComment(reader.getCommentLines(), + idx, reader.getCommentLines().size() - 1); + PropertyLayoutData data = fetchLayoutData(reader + .getPropertyName()); + if (contained) + { + data.addComment(comment); + data.setSingleLine(false); + } + else + { + data.setComment(comment); + data.setBlancLines(blancLines); + } + } + } + } + catch (IOException ioex) + { + throw new ConfigurationException(ioex); + } + finally + { + if (--loadCounter == 0) + { + getConfiguration().addConfigurationListener(this); + } + } + } + + /** + * Writes the properties file to the given writer, preserving as much of its + * structure as possible. + * + * @param out the writer + * @throws ConfigurationException if an error occurs + */ + public void save(Writer out) throws ConfigurationException + { + try + { + PropertiesConfiguration.PropertiesWriter writer = new PropertiesConfiguration.PropertiesWriter( + out, getConfiguration().getListDelimiter()); + if (headerComment != null) + { + writer.writeln(getCanonicalHeaderComment(true)); + writer.writeln(null); + } + + for (Iterator it = layoutData.keySet().iterator(); it.hasNext();) + { + String key = (String) it.next(); + if (getConfiguration().containsKey(key)) + { + + // Output blanc lines before property + for (int i = 0; i < getBlancLinesBefore(key); i++) + { + writer.writeln(null); + } + + // Output the comment + if (getComment(key) != null) + { + writer.writeln(getCanonicalComment(key, true)); + } + + // Output the property and its value + writer.writeProperty(key, getConfiguration().getProperty( + key), isForceSingleLine() || isSingleLine(key)); + } + } + writer.flush(); + } + catch (IOException ioex) + { + throw new ConfigurationException(ioex); + } + } + + /** + * The event listener callback. Here event notifications of the + * configuration object are processed to update the layout object properly. + * + * @param event the event object + */ + public void configurationChanged(ConfigurationEvent event) + { + if (event.isBeforeUpdate()) + { + if (AbstractFileConfiguration.EVENT_RELOAD == event.getType()) + { + clear(); + } + } + + else + { + switch (event.getType()) + { + case AbstractConfiguration.EVENT_ADD_PROPERTY: + boolean contained = layoutData.containsKey(event + .getPropertyName()); + PropertyLayoutData data = fetchLayoutData(event + .getPropertyName()); + data.setSingleLine(!contained); + break; + case AbstractConfiguration.EVENT_CLEAR_PROPERTY: + layoutData.remove(event.getPropertyName()); + break; + case AbstractConfiguration.EVENT_CLEAR: + clear(); + break; + case AbstractConfiguration.EVENT_SET_PROPERTY: + fetchLayoutData(event.getPropertyName()); + break; + } + } + } + + /** + * Returns a layout data object for the specified key. If this is a new key, + * a new object is created and initialized with default values. + * + * @param key the key + * @return the corresponding layout data object + */ + private PropertyLayoutData fetchLayoutData(String key) + { + if (key == null) + { + throw new IllegalArgumentException("Property key must not be null!"); + } + + PropertyLayoutData data = (PropertyLayoutData) layoutData.get(key); + if (data == null) + { + data = new PropertyLayoutData(); + data.setSingleLine(true); + layoutData.put(key, data); + } + + return data; + } + + /** + * Removes all content from this layout object. + */ + private void clear() + { + layoutData.clear(); + setHeaderComment(null); + } + + /** + * Tests whether a line is a comment, i.e. whether it starts with a comment + * character. + * + * @param line the line + * @return a flag if this is a comment line + */ + static boolean isCommentLine(String line) + { + return PropertiesConfiguration.isCommentLine(line); + } + + /** + * Trims a comment. This method either removes all comment characters from + * the given string, leaving only the plain comment text or ensures that + * every line starts with a valid comment character. + * + * @param s the string to be processed + * @param comment if <b>true</b>, a comment character will always be + * enforced; if <b>false</b>, it will be removed + * @return the trimmed comment + */ + static String trimComment(String s, boolean comment) + { + StringBuffer buf = new StringBuffer(s.length()); + int lastPos = 0; + int pos; + + do + { + pos = s.indexOf(CR, lastPos); + if (pos >= 0) + { + String line = s.substring(lastPos, pos); + buf.append(stripCommentChar(line, comment)).append(CR); + lastPos = pos + CR.length(); + } + } while (pos >= 0); + + if (lastPos < s.length()) + { + buf.append(stripCommentChar(s.substring(lastPos), comment)); + } + return buf.toString(); + } + + /** + * Either removes the comment character from the given comment line or + * ensures that the line starts with a comment character. + * + * @param s the comment line + * @param comment if <b>true</b>, a comment character will always be + * enforced; if <b>false</b>, it will be removed + * @return the line without comment character + */ + static String stripCommentChar(String s, boolean comment) + { + if (s.length() < 1 || (isCommentLine(s) == comment)) + { + return s; + } + + else + { + if (!comment) + { + int pos = 0; + // find first comment character + while (PropertiesConfiguration.COMMENT_CHARS.indexOf(s + .charAt(pos)) < 0) + { + pos++; + } + + // Remove leading spaces + pos++; + while (pos < s.length() + && Character.isWhitespace(s.charAt(pos))) + { + pos++; + } + + return (pos < s.length()) ? s.substring(pos) + : StringUtils.EMPTY; + } + else + { + return COMMENT_PREFIX + s; + } + } + } + + /** + * Extracts a comment string from the given range of the specified comment + * lines. The single lines are added using a line feed as separator. + * + * @param commentLines a list with comment lines + * @param from the start index + * @param to the end index (inclusive) + * @return the comment string (<b>null</b> if it is undefined) + */ + private String extractComment(List commentLines, int from, int to) + { + if (to < from) + { + return null; + } + + else + { + StringBuffer buf = new StringBuffer((String) commentLines.get(from)); + for (int i = from + 1; i <= to; i++) + { + buf.append(CR); + buf.append(commentLines.get(i)); + } + return buf.toString(); + } + } + + /** + * Checks if parts of the passed in comment can be used as header comment. + * This method checks whether a header comment can be defined (i.e. whether + * this is the first comment in the loaded file). If this is the case, it is + * searched for the lates blanc line. This line will mark the end of the + * header comment. The return value is the index of the first line in the + * passed in list, which does not belong to the header comment. + * + * @param commentLines the comment lines + * @return the index of the next line after the header comment + */ + private int checkHeaderComment(List commentLines) + { + if (loadCounter == 1 && getHeaderComment() == null + && layoutData.isEmpty()) + { + // This is the first comment. Search for blanc lines. + int index = commentLines.size() - 1; + while (index >= 0 + && ((String) commentLines.get(index)).length() > 0) + { + index--; + } + setHeaderComment(extractComment(commentLines, 0, index - 1)); + return index + 1; + } + else + { + return 0; + } + } + + /** + * A helper class for storing all layout related information for a + * configuration property. + */ + static class PropertyLayoutData + { + /** Stores the comment for the property. */ + private StringBuffer comment; + + /** Stores the number of blanc lines before this property. */ + private int blancLines; + + /** Stores the single line property. */ + private boolean singleLine; + + /** + * Creates a new instance of <code>PropertyLayoutData</code>. + */ + public PropertyLayoutData() + { + singleLine = true; + } + + /** + * Returns the number of blanc lines before this property. + * + * @return the number of blanc lines before this property + */ + public int getBlancLines() + { + return blancLines; + } + + /** + * Sets the number of properties before this property. + * + * @param blancLines the number of properties before this property + */ + public void setBlancLines(int blancLines) + { + this.blancLines = blancLines; + } + + /** + * Returns the single line flag. + * + * @return the single line flag + */ + public boolean isSingleLine() + { + return singleLine; + } + + /** + * Sets the single line flag. + * + * @param singleLine the single line flag + */ + public void setSingleLine(boolean singleLine) + { + this.singleLine = singleLine; + } + + /** + * Adds a comment for this property. If already a comment exists, the + * new comment is added (separated by a newline). + * + * @param s the comment to add + */ + public void addComment(String s) + { + if (s != null) + { + if (comment == null) + { + comment = new StringBuffer(s); + } + else + { + comment.append(CR).append(s); + } + } + } + + /** + * Sets the comment for this property. + * + * @param s the new comment (can be <b>null</b>) + */ + public void setComment(String s) + { + if (s == null) + { + comment = null; + } + else + { + comment = new StringBuffer(s); + } + } + + /** + * Returns the comment for this property. The comment is returned as it + * is, without processing of comment characters. + * + * @return the comment (can be <b>null</b>) + */ + public String getComment() + { + return (comment == null) ? null : comment.toString(); + } + } +} Propchange: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java ------------------------------------------------------------------------------ svn:keywords = Date Author Id Revision HeadURL Propchange: jakarta/commons/proper/configuration/trunk/src/java/org/apache/commons/configuration/PropertiesConfigurationLayout.java ------------------------------------------------------------------------------ svn:mime-type = text/plain --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]