[FELIX-2571] Have fileinstall listen for configuration changes and write them back to the config files
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1027254 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java b/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java
new file mode 100644
index 0000000..795e408
--- /dev/null
+++ b/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.felix.utils.collections;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A wrapper around a dictionary access it as a Map
+ */
+public class DictionaryAsMap<U, V> extends AbstractMap<U, V>
+{
+
+ private Dictionary<U, V> dict;
+
+ public DictionaryAsMap(Dictionary<U, V> dict)
+ {
+ this.dict = dict;
+ }
+
+ @Override
+ public Set<Entry<U, V>> entrySet()
+ {
+ return new AbstractSet<Entry<U, V>>()
+ {
+ @Override
+ public Iterator<Entry<U, V>> iterator()
+ {
+ final Enumeration<U> e = dict.keys();
+ return new Iterator<Entry<U, V>>()
+ {
+ private U key;
+ public boolean hasNext()
+ {
+ return e.hasMoreElements();
+ }
+
+ public Entry<U, V> next()
+ {
+ key = e.nextElement();
+ return new KeyEntry(key);
+ }
+
+ public void remove()
+ {
+ if (key == null)
+ {
+ throw new IllegalStateException();
+ }
+ dict.remove(key);
+ }
+ };
+ }
+
+ @Override
+ public int size()
+ {
+ return dict.size();
+ }
+ };
+ }
+
+ @Override
+ public V put(U key, V value) {
+ return dict.put(key, value);
+ }
+
+ class KeyEntry implements Map.Entry<U,V> {
+
+ private final U key;
+
+ KeyEntry(U key) {
+ this.key = key;
+ }
+
+ public U getKey() {
+ return key;
+ }
+
+ public V getValue() {
+ return dict.get(key);
+ }
+
+ public V setValue(V value) {
+ return DictionaryAsMap.this.put(key, value);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java b/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
new file mode 100644
index 0000000..36d3c7f
--- /dev/null
+++ b/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.felix.utils.properties;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.*;
+
+/**
+ * <p>
+ * Enhancement of the standard <code>Properties</code>
+ * managing the maintain of comments, etc.
+ * </p>
+ *
+ * @author gnodet, jbonofre
+ */
+public class InterpolationHelper {
+
+ private InterpolationHelper() {
+ }
+
+ private static final char ESCAPE_CHAR = '\\';
+ private static final String DELIM_START = "${";
+ private static final String DELIM_STOP = "}";
+
+ /**
+ * Perform substitution on a property set
+ *
+ * @param properties the property set to perform substitution on
+ */
+ public static void performSubstitution(Map<String,String> properties)
+ {
+ for (String name : properties.keySet())
+ {
+ String value = properties.get(name);
+ properties.put(name, substVars(value, name, null, properties));
+ }
+ }
+
+ /**
+ * <p>
+ * This method performs property variable substitution on the
+ * specified value. If the specified value contains the syntax
+ * <tt>${<prop-name>}</tt>, where <tt><prop-name></tt>
+ * refers to either a configuration property or a system property,
+ * then the corresponding property value is substituted for the variable
+ * placeholder. Multiple variable placeholders may exist in the
+ * specified value as well as nested variable placeholders, which
+ * are substituted from inner most to outer most. Configuration
+ * properties override system properties.
+ * </p>
+ * @param val The string on which to perform property substitution.
+ * @param currentKey The key of the property being evaluated used to
+ * detect cycles.
+ * @param cycleMap Map of variable references used to detect nested cycles.
+ * @param configProps Set of configuration properties.
+ * @return The value of the specified string after system property substitution.
+ * @throws IllegalArgumentException If there was a syntax error in the
+ * property placeholder syntax or a recursive variable reference.
+ **/
+ public static String substVars(String val, String currentKey, Map<String,String> cycleMap, Map<String,String> configProps)
+ throws IllegalArgumentException
+ {
+ if (cycleMap == null)
+ {
+ cycleMap = new HashMap<String,String>();
+ }
+
+ // Put the current key in the cycle map.
+ cycleMap.put(currentKey, currentKey);
+
+ // Assume we have a value that is something like:
+ // "leading ${foo.${bar}} middle ${baz} trailing"
+
+ // Find the first ending '}' variable delimiter, which
+ // will correspond to the first deepest nested variable
+ // placeholder.
+ int stopDelim = val.indexOf(DELIM_STOP);
+ while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR)
+ {
+ stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+ }
+
+ // Find the matching starting "${" variable delimiter
+ // by looping until we find a start delimiter that is
+ // greater than the stop delimiter we have found.
+ int startDelim = val.indexOf(DELIM_START);
+ while (stopDelim >= 0)
+ {
+ int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+ if ((idx < 0) || (idx > stopDelim))
+ {
+ break;
+ }
+ else if (idx < stopDelim)
+ {
+ startDelim = idx;
+ }
+ }
+
+ // If we do not have a start or stop delimiter, then just
+ // return the existing value.
+ if ((startDelim < 0) || (stopDelim < 0))
+ {
+ return unescape(val);
+ }
+
+ // At this point, we have found a variable placeholder so
+ // we must perform a variable substitution on it.
+ // Using the start and stop delimiter indices, extract
+ // the first, deepest nested variable placeholder.
+ String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
+
+ // Verify that this is not a recursive variable reference.
+ if (cycleMap.get(variable) != null)
+ {
+ throw new IllegalArgumentException("recursive variable reference: " + variable);
+ }
+
+ // Get the value of the deepest nested variable placeholder.
+ // Try to configuration properties first.
+ String substValue = (String) ((configProps != null) ? configProps.get(variable) : null);
+ if (substValue == null)
+ {
+ // Ignore unknown property values.
+ substValue = variable.length() > 0 ? System.getProperty(variable, "") : "";
+ }
+
+ // Remove the found variable from the cycle map, since
+ // it may appear more than once in the value and we don't
+ // want such situations to appear as a recursive reference.
+ cycleMap.remove(variable);
+
+ // Append the leading characters, the substituted value of
+ // the variable, and the trailing characters to get the new
+ // value.
+ val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length(), val.length());
+
+ // Now perform substitution again, since there could still
+ // be substitutions to make.
+ val = substVars(val, currentKey, cycleMap, configProps);
+
+ // Remove escape characters preceding {, } and \
+ val = unescape(val);
+
+ // Return the value.
+ return val;
+ }
+
+ private static String unescape(String val) {
+ int escape = val.indexOf(ESCAPE_CHAR);
+ while (escape >= 0 && escape < val.length() - 1)
+ {
+ char c = val.charAt(escape + 1);
+ if (c == '{' || c == '}' || c == ESCAPE_CHAR)
+ {
+ val = val.substring(0, escape) + val.substring(escape + 1);
+ }
+ escape = val.indexOf(ESCAPE_CHAR, escape + 1);
+ }
+ return val;
+ }
+
+}
\ No newline at end of file
diff --git a/utils/src/main/java/org/apache/felix/utils/properties/Properties.java b/utils/src/main/java/org/apache/felix/utils/properties/Properties.java
new file mode 100644
index 0000000..4edce27
--- /dev/null
+++ b/utils/src/main/java/org/apache/felix/utils/properties/Properties.java
@@ -0,0 +1,928 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.felix.utils.properties;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>
+ * Enhancement of the standard <code>Properties</code>
+ * managing the maintain of comments, etc.
+ * </p>
+ *
+ * @author gnodet, jbonofre
+ */
+public class Properties extends AbstractMap<String, String> {
+
+ /** Constant for the supported comment characters.*/
+ private static final String COMMENT_CHARS = "#!";
+
+ /** The list of possible key/value separators */
+ private static final char[] SEPARATORS = new char[] {'=', ':'};
+
+ /** The white space characters used as key/value separators. */
+ private static final char[] WHITE_SPACE = new char[] {' ', '\t', '\f'};
+
+ /**
+ * The default encoding (ISO-8859-1 as specified by
+ * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
+ */
+ private static final String DEFAULT_ENCODING = "ISO-8859-1";
+
+ /** Constant for the platform specific line separator.*/
+ private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+ /** 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;
+
+ private Map<String,String> storage = new LinkedHashMap<String,String>();
+ private Map<String,Layout> layout = new LinkedHashMap<String,Layout>();
+ private List<String> header;
+ private List<String> footer;
+ private File location;
+
+ public Properties() {
+ }
+
+ public Properties(File location) throws IOException {
+ this.location = location;
+ if(location.exists())
+ load(location);
+ }
+
+ public void load(File location) throws IOException {
+ InputStream is = new FileInputStream(location);
+ try {
+ load(is);
+ } finally {
+ is.close();
+ }
+ }
+
+ public void load(URL location) throws IOException {
+ InputStream is = location.openStream();
+ try {
+ load(is);
+ } finally {
+ is.close();
+ }
+ }
+
+ public void load(InputStream is) throws IOException {
+ load(new InputStreamReader(is, DEFAULT_ENCODING));
+ }
+
+ public void load(Reader reader) throws IOException {
+ loadLayout(reader);
+ }
+
+ public void save() throws IOException {
+ save(this.location);
+ }
+
+ public void save(File location) throws IOException {
+ OutputStream os = new FileOutputStream(location);
+ try {
+ save(os);
+ } finally {
+ os.close();
+ }
+ }
+
+ public void save(OutputStream os) throws IOException {
+ save(new OutputStreamWriter(os, DEFAULT_ENCODING));
+ }
+
+ public void save(Writer writer) throws IOException {
+ saveLayout(writer);
+ }
+
+ @Override
+ public Set<Entry<String, String>> entrySet() {
+ return storage.entrySet();
+ }
+
+ @Override
+ public String put(String key, String value) {
+ String old = storage.put(key, value);
+ if (old == null || !old.equals(value)) {
+ Layout l = layout.get(key);
+ if (l != null) {
+ l.clearValue();
+ }
+ }
+ return old;
+ }
+
+ @Override
+ public String remove(Object key) {
+ Layout l = layout.get(key);
+ if (l != null) {
+ l.clearValue();
+ }
+ return storage.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ for (Layout l : layout.values()) {
+ l.clearValue();
+ }
+ storage.clear();
+ }
+
+ /**
+ * Return the comment header.
+ *
+ * @return the comment header
+ */
+ public List<String> getHeader()
+ {
+ return header;
+ }
+
+ /**
+ * Set the comment header.
+ *
+ * @param header the header to use
+ */
+ public void setHeader(List<String> header)
+ {
+ this.header = header;
+ }
+
+ /**
+ * Return the comment footer.
+ *
+ * @return the comment footer
+ */
+ public List<String> getFooter()
+ {
+ return footer;
+ }
+
+ /**
+ * Set the comment footer.
+ *
+ * @param footer the footer to use
+ */
+ public void setFooter(List<String> footer)
+ {
+ this.footer = footer;
+ }
+
+ /**
+ * 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 java.io.IOException if an error occurs
+ */
+ protected void loadLayout(Reader in) throws IOException
+ {
+ PropertiesReader reader = new PropertiesReader(in);
+ while (reader.nextProperty())
+ {
+ storage.put(reader.getPropertyName(), reader.getPropertyValue());
+ int idx = checkHeaderComment(reader.getCommentLines());
+ layout.put(reader.getPropertyName(),
+ new Layout(idx < reader.getCommentLines().size() ?
+ new ArrayList<String>(reader.getCommentLines().subList(idx, reader.getCommentLines().size())) :
+ null,
+ new ArrayList<String>(reader.getValueLines())));
+ }
+ footer = new ArrayList<String>(reader.getCommentLines());
+ InterpolationHelper.performSubstitution(storage);
+ }
+
+ /**
+ * Writes the properties file to the given writer, preserving as much of its
+ * structure as possible.
+ *
+ * @param out the writer
+ * @throws java.io.IOException if an error occurs
+ */
+ protected void saveLayout(Writer out) throws IOException
+ {
+ PropertiesWriter writer = new PropertiesWriter(out);
+ if (header != null)
+ {
+ for (String s : header)
+ {
+ writer.writeln(s);
+ }
+ }
+
+ for (String key : storage.keySet())
+ {
+ Layout l = layout.get(key);
+ if (l != null && l.getCommentLines() != null)
+ {
+ for (String s : l.getCommentLines())
+ {
+ writer.writeln(s);
+ }
+ }
+ if (l != null && l.getValueLines() != null)
+ {
+ for (String s : l.getValueLines())
+ {
+ writer.writeln(s);
+ }
+ }
+ else
+ {
+ writer.writeProperty(key, storage.get(key));
+ }
+ }
+ if (footer != null)
+ {
+ for (String s : footer)
+ {
+ writer.writeln(s);
+ }
+ }
+ writer.flush();
+ }
+
+ /**
+ * 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 blank 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<String> commentLines)
+ {
+ if (getHeader() == null && layout.isEmpty())
+ {
+ // This is the first comment. Search for blank lines.
+ int index = commentLines.size() - 1;
+ while (index >= 0 && commentLines.get(index).length() > 0)
+ {
+ index--;
+ }
+ setHeader(new ArrayList<String>(commentLines.subList(0, index + 1)));
+ return index + 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ /**
+ * 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) {
+ String s = line.trim();
+ // blank lines are also treated as comment lines
+ return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
+ }
+
+ /**
+ * <p>Unescapes any Java literals found in the <code>String</code> to a
+ * <code>Writer</code>.</p> This is a slightly modified version of the
+ * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
+ * drop escaped separators (i.e '\,').
+ *
+ * @param str the <code>String</code> to unescape, may be null
+ * @return the processed string
+ * @throws IllegalArgumentException if the Writer is <code>null</code>
+ */
+ protected static String unescapeJava(String str) {
+ if (str == null) {
+ return null;
+ }
+ int sz = str.length();
+ StringBuffer out = new StringBuffer(sz);
+ StringBuffer unicode = new StringBuffer(UNICODE_LEN);
+ boolean hadSlash = false;
+ boolean inUnicode = false;
+ for (int i = 0; i < sz; i++) {
+ char ch = str.charAt(i);
+ if (inUnicode) {
+ // if in unicode, then we're reading unicode
+ // values in somehow
+ unicode.append(ch);
+ if (unicode.length() == UNICODE_LEN) {
+ // unicode now contains the four hex digits
+ // which represents our unicode character
+ try {
+ int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
+ out.append((char) value);
+ unicode.setLength(0);
+ inUnicode = false;
+ hadSlash = false;
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Unable to parse unicode value: " + unicode, nfe);
+ }
+ }
+ continue;
+ }
+
+ if (hadSlash) {
+ // handle an escaped value
+ hadSlash = false;
+ switch (ch) {
+ case '\\' :
+ out.append('\\');
+ break;
+ case '\'' :
+ out.append('\'');
+ break;
+ case '\"' :
+ out.append('"');
+ break;
+ case 'r' :
+ out.append('\r');
+ break;
+ case 'f' :
+ out.append('\f');
+ break;
+ case 't' :
+ out.append('\t');
+ break;
+ case 'n' :
+ out.append('\n');
+ break;
+ case 'b' :
+ out.append('\b');
+ break;
+ case 'u' :
+ // uh-oh, we're in unicode country....
+ inUnicode = true;
+ break;
+ default :
+ out.append(ch);
+ break;
+ }
+ continue;
+ } else if (ch == '\\') {
+ hadSlash = true;
+ continue;
+ }
+ out.append(ch);
+ }
+
+ if (hadSlash) {
+ // then we're in the weird case of a \ at the end of the
+ // string, let's output it anyway.
+ out.append('\\');
+ }
+
+ return out.toString();
+ }
+
+ /**
+ * <p>Escapes the characters in a <code>String</code> using Java String rules.</p>
+ *
+ * <p>Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) </p>
+ *
+ * <p>So a tab becomes the characters <code>'\\'</code> and
+ * <code>'t'</code>.</p>
+ *
+ * <p>The only difference between Java strings and JavaScript strings
+ * is that in JavaScript, a single quote must be escaped.</p>
+ *
+ * <p>Example:
+ * <pre>
+ * input string: He didn't say, "Stop!"
+ * output string: He didn't say, \"Stop!\"
+ * </pre>
+ * </p>
+ *
+ * @param str String to escape values in, may be null
+ * @return String with escaped values, <code>null</code> if null string input
+ */
+ protected static String escapeJava(String str) {
+ if (str == null) {
+ return null;
+ }
+ int sz = str.length();
+ StringBuffer out = new StringBuffer(sz * 2);
+ for (int i = 0; i < sz; i++) {
+ char ch = str.charAt(i);
+ // handle unicode
+ if (ch > 0xfff) {
+ out.append("\\u").append(hex(ch));
+ } else if (ch > 0xff) {
+ out.append("\\u0").append(hex(ch));
+ } else if (ch > 0x7f) {
+ out.append("\\u00").append(hex(ch));
+ } else if (ch < 32) {
+ switch (ch) {
+ case '\b' :
+ out.append('\\');
+ out.append('b');
+ break;
+ case '\n' :
+ out.append('\\');
+ out.append('n');
+ break;
+ case '\t' :
+ out.append('\\');
+ out.append('t');
+ break;
+ case '\f' :
+ out.append('\\');
+ out.append('f');
+ break;
+ case '\r' :
+ out.append('\\');
+ out.append('r');
+ break;
+ default :
+ if (ch > 0xf) {
+ out.append("\\u00").append(hex(ch));
+ } else {
+ out.append("\\u000").append(hex(ch));
+ }
+ break;
+ }
+ } else {
+ switch (ch) {
+ case '"' :
+ out.append('\\');
+ out.append('"');
+ break;
+ case '\\' :
+ out.append('\\');
+ out.append('\\');
+ break;
+ default :
+ out.append(ch);
+ break;
+ }
+ }
+ }
+ return out.toString();
+ }
+
+ /**
+ * <p>Returns an upper case hexadecimal <code>String</code> for the given
+ * character.</p>
+ *
+ * @param ch The character to convert.
+ * @return An upper case hexadecimal <code>String</code>
+ */
+ protected static String hex(char ch) {
+ return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
+ }
+
+ /**
+ * <p>Checks if the value is in the given array.</p>
+ *
+ * <p>The method returns <code>false</code> if a <code>null</code> array is passed in.</p>
+ *
+ * @param array the array to search through
+ * @param valueToFind the value to find
+ * @return <code>true</code> if the array contains the object
+ */
+ public static boolean contains(char[] array, char valueToFind) {
+ if (array == null) {
+ return false;
+ }
+ for (int i = 0; i < array.length; i++) {
+ if (valueToFind == array[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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<String> commentLines;
+
+ /** Stores the value lines for the currently processed property.*/
+ private List<String> valueLines;
+
+ /** Stores the name of the last read property.*/
+ private String propertyName;
+
+ /** Stores the value of the last read property.*/
+ private String propertyValue;
+
+ /**
+ * Creates a new instance of <code>PropertiesReader</code> and sets
+ * the underlaying reader and the list delimiter.
+ *
+ * @param reader the reader
+ */
+ public PropertiesReader(Reader reader)
+ {
+ super(reader);
+ commentLines = new ArrayList<String>();
+ valueLines = new ArrayList<String>();
+ }
+
+ /**
+ * 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
+ *
+ * @throws java.io.IOException in case of an I/O error
+ */
+ public String readProperty() throws IOException
+ {
+ commentLines.clear();
+ valueLines.clear();
+ StringBuffer buffer = new StringBuffer();
+
+ while (true)
+ {
+ String line = readLine();
+ if (line == null)
+ {
+ // EOF
+ return null;
+ }
+
+ if (isCommentLine(line))
+ {
+ commentLines.add(line);
+ continue;
+ }
+
+ valueLines.add(line);
+ line = line.trim();
+
+ if (checkCombineLines(line))
+ {
+ line = line.substring(0, line.length() - 1);
+ buffer.append(line);
+ }
+ else
+ {
+ buffer.append(line);
+ break;
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * 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 java.io.IOException if an error occurs
+ */
+ public boolean nextProperty() throws IOException
+ {
+ String line = readProperty();
+
+ if (line == null)
+ {
+ return false; // EOF
+ }
+
+ // parse the line
+ String[] property = parseProperty(line);
+ propertyName = unescapeJava(property[0]);
+ propertyValue = unescapeJava(property[1]);
+ 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>
+ */
+ public List<String> getCommentLines()
+ {
+ return commentLines;
+ }
+
+ /**
+ * Returns the value lines that have been read for the last property.
+ *
+ * @return the raw value lines for the last property returned by
+ * <code>readProperty()</code>
+ */
+ public List<String> getValueLines()
+ {
+ return valueLines;
+ }
+
+ /**
+ * Returns the name of the last read property. This method can be called
+ * after <code>{@link #nextProperty()}</code> was invoked and its
+ * return value was <b>true</b>.
+ *
+ * @return the name of the last read property
+ */
+ public String getPropertyName()
+ {
+ return propertyName;
+ }
+
+ /**
+ * Returns the value of the last read property. This method can be
+ * called after <code>{@link #nextProperty()}</code> was invoked and
+ * its return value was <b>true</b>.
+ *
+ * @return the value of the last read property
+ */
+ 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.
+ *
+ * @param line the line
+ * @return a flag if the lines should be combined
+ */
+ private static boolean checkCombineLines(String line)
+ {
+ int bsCount = 0;
+ for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
+ {
+ bsCount++;
+ }
+
+ return bsCount % 2 != 0;
+ }
+
+ /**
+ * 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
+ */
+ 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 (contains(WHITE_SPACE, c))
+ {
+ // switch to the separator crossing state
+ state = 2;
+ }
+ else if (contains(SEPARATORS, c))
+ {
+ // switch to the value parsing state
+ state = 3;
+ }
+ else
+ {
+ key.append(c);
+ }
+
+ break;
+
+ case 1:
+ if (contains(SEPARATORS, c) || 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 (contains(WHITE_SPACE, c))
+ {
+ // do nothing, eat all white spaces
+ state = 2;
+ }
+ else if (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
+
+ /**
+ * This class is used to write properties lines.
+ */
+ public static class PropertiesWriter extends FilterWriter
+ {
+ /**
+ * Constructor.
+ *
+ * @param writer a Writer object providing the underlying stream
+ */
+ public PropertiesWriter(Writer writer)
+ {
+ super(writer);
+ }
+
+ /**
+ * Writes the given property and its value.
+ *
+ * @param key the property key
+ * @param value the property value
+ * @throws java.io.IOException if an error occurs
+ */
+ public void writeProperty(String key, String value) throws IOException
+ {
+ write(escapeKey(key));
+ write(" = ");
+ write(escapeJava(value));
+ writeln(null);
+ }
+
+ /**
+ * Escape the separators in the key.
+ *
+ * @param key the key
+ * @return the escaped key
+ */
+ private String escapeKey(String key)
+ {
+ StringBuffer newkey = new StringBuffer();
+
+ for (int i = 0; i < key.length(); i++)
+ {
+ char c = key.charAt(i);
+
+ if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c))
+ {
+ // escape the separator
+ newkey.append('\\');
+ newkey.append(c);
+ }
+ else
+ {
+ newkey.append(c);
+ }
+ }
+
+ return newkey.toString();
+ }
+
+ /**
+ * 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 java.io.IOException if an error occurs
+ */
+ public void writeln(String s) throws IOException
+ {
+ if (s != null)
+ {
+ write(s);
+ }
+ write(LINE_SEPARATOR);
+ }
+
+ } // class PropertiesWriter
+
+ /**
+ * TODO
+ */
+ protected static class Layout {
+
+ private List<String> commentLines;
+ private List<String> valueLines;
+
+ public Layout() {
+ }
+
+ public Layout(List<String> commentLines, List<String> valueLines) {
+ this.commentLines = commentLines;
+ this.valueLines = valueLines;
+ }
+
+ public List<String> getCommentLines() {
+ return commentLines;
+ }
+
+ public void setCommentLines(List<String> commentLines) {
+ this.commentLines = commentLines;
+ }
+
+ public List<String> getValueLines() {
+ return valueLines;
+ }
+
+ public void setValueLines(List<String> valueLines) {
+ this.valueLines = valueLines;
+ }
+
+ public void clearValue() {
+ this.valueLines = null;
+ }
+
+ } // class Layout
+
+}
\ No newline at end of file
diff --git a/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java b/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
new file mode 100644
index 0000000..5e4dcc4
--- /dev/null
+++ b/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.felix.utils.properties;
+
+import junit.framework.TestCase;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public class InterpolationHelperTest extends TestCase {
+
+ public void testBasicSubstitution()
+ {
+ System.setProperty("value1", "sub_value1");
+ Hashtable props = new Hashtable();
+ props.put("key0", "value0");
+ props.put("key1", "${value1}");
+ props.put("key2", "${value2}");
+
+ for (Enumeration e = props.keys(); e.hasMoreElements();)
+ {
+ String name = (String) e.nextElement();
+ props.put(name, InterpolationHelper.substVars((String) props.get(name), name, null, props));
+ }
+
+ assertEquals("value0", props.get("key0"));
+ assertEquals("sub_value1", props.get("key1"));
+ assertEquals("", props.get("key2"));
+
+ }
+
+ public void testSubstitutionFailures()
+ {
+ assertEquals("a}", InterpolationHelper.substVars("a}", "b", null, new Hashtable()));
+ assertEquals("${a", InterpolationHelper.substVars("${a", "b", null, new Hashtable()));
+ }
+
+ public void testEmptyVariable() {
+ assertEquals("", InterpolationHelper.substVars("${}", "b", null, new Hashtable()));
+ }
+
+ public void testInnerSubst() {
+ Hashtable props = new Hashtable();
+ props.put("a", "b");
+ props.put("b", "c");
+ assertEquals("c", InterpolationHelper.substVars("${${a}}", "z", null, props));
+ }
+
+ public void testSubstLoop() {
+ try {
+ InterpolationHelper.substVars("${a}", "a", null, new Hashtable());
+ fail("Should have thrown an exception");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testSubstitutionEscape()
+ {
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a${#}\\}", "b", null, new Hashtable()));
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}${#}", "b", null, new Hashtable()));
+ assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}", "b", null, new Hashtable()));
+ }
+
+}
diff --git a/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java b/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java
new file mode 100644
index 0000000..7809943
--- /dev/null
+++ b/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.felix.utils.properties;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import junit.framework.TestCase;
+
+/**
+ * <p>
+ * Unit tests on <code>Properties</code>.
+ * </p>
+ *
+ * @author jbonofre
+ */
+public class PropertiesTest extends TestCase {
+
+ private final static String TEST_PROPERTIES_FILE = "test.properties";
+
+ private Properties properties;
+
+ /*
+ * (non-Javadoc)
+ * @see junit.framework.TestCase#setUp()
+ */
+ public void setUp() throws Exception {
+ properties = new Properties();
+ properties.load(this.getClass().getClassLoader().getResourceAsStream(TEST_PROPERTIES_FILE));
+ }
+
+ /**
+ * <p>
+ * Test getting property.
+ * </p>
+ *
+ * @throws Exception
+ */
+ public void testGettingProperty() throws Exception {
+ assertEquals("test", properties.get("test"));
+ }
+
+ public void testLoadSave() throws IOException {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ pw.println("# ");
+ pw.println("# The Main ");
+ pw.println("# ");
+ pw.println("# Comment ");
+ pw.println("# ");
+ pw.println("");
+ pw.println("# Another comment");
+ pw.println("");
+ pw.println("# A value comment");
+ pw.println("key1 = val1");
+ pw.println("");
+ pw.println("# Another value comment");
+ pw.println("key2 = ${key1}/foo");
+ pw.println("");
+ pw.println("# A third comment");
+ pw.println("key3 = val3");
+ pw.println("");
+
+
+ Properties props = new Properties();
+ props.load(new StringReader(sw.toString()));
+ props.save(System.err);
+ System.err.println("=====");
+
+ props.put("key2", props.get("key2"));
+ props.put("key3", "foo");
+ props.save(System.err);
+ System.err.println("=====");
+ }
+}
diff --git a/utils/src/test/resources/test.properties b/utils/src/test/resources/test.properties
new file mode 100644
index 0000000..2856e72
--- /dev/null
+++ b/utils/src/test/resources/test.properties
@@ -0,0 +1,21 @@
+##---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements. See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You 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.
+##---------------------------------------------------------------------------
+#
+# test.properties
+# Used in the PropertiesTest
+#
+test=test
\ No newline at end of file