From e03bfc445eec32f9d566e42c48eb8e1198a94034 Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Thu, 21 Aug 2008 07:27:27 +0000 Subject: [PATCH] line-ends git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@687601 13f79535-47bb-0310-9956-ffa450edef68 --- src/etc/testcases/util/simple.properties | 18 +- src/etc/testcases/util/unusual.properties | 46 +- .../ant/util/LayoutPreservingProperties.java | 1378 ++++++++--------- .../util/LayoutPreservingPropertiesTest.java | 568 +++---- 4 files changed, 1005 insertions(+), 1005 deletions(-) diff --git a/src/etc/testcases/util/simple.properties b/src/etc/testcases/util/simple.properties index 8716ea660..6930a65e9 100644 --- a/src/etc/testcases/util/simple.properties +++ b/src/etc/testcases/util/simple.properties @@ -1,9 +1,9 @@ -# a comment -prop.alpha=first property - -! more comment -prop.beta=simple - -# now a line wrapping one -prop.gamma=This is a long comment which \ - contains a line wrap. +# a comment +prop.alpha=first property + +! more comment +prop.beta=simple + +# now a line wrapping one +prop.gamma=This is a long comment which \ + contains a line wrap. diff --git a/src/etc/testcases/util/unusual.properties b/src/etc/testcases/util/unusual.properties index 8ef6d0338..f9bd283bc 100644 --- a/src/etc/testcases/util/unusual.properties +++ b/src/etc/testcases/util/unusual.properties @@ -1,23 +1,23 @@ - -\ prop\ one\ =\ \ leading and trailing spaces - -prop\ttwo=contains\ttab - -prop\nthree=contains\nnewline - -prop\rfour=contains\rcarraige return - -prop\ffive=contains\fform feed - -prop\\six=contains\\backslash - -prop\:seven=contains\:colon - -prop\=eight=contains\=equals - -prop\#nine=contains\#hash - -prop\!ten=contains\!exclamation - -alpha:set with a colon -beta set with a space + +\ prop\ one\ =\ \ leading and trailing spaces + +prop\ttwo=contains\ttab + +prop\nthree=contains\nnewline + +prop\rfour=contains\rcarraige return + +prop\ffive=contains\fform feed + +prop\\six=contains\\backslash + +prop\:seven=contains\:colon + +prop\=eight=contains\=equals + +prop\#nine=contains\#hash + +prop\!ten=contains\!exclamation + +alpha:set with a colon +beta set with a space diff --git a/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java b/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java index 55fdf4808..3e6b275a0 100644 --- a/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java +++ b/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java @@ -1,689 +1,689 @@ -/* - * 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.tools.ant.util; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Properties; - -/** - *

A Properties collection which preserves comments and whitespace - * present in the input stream from which it was loaded.

- *

The class defers the usual work of the java.util.Properties - * class to there, but it also keeps track of the contents of the - * input stream from which it was loaded (if applicable), so that in can - * write out the properties in as close a form as possible to the input.

- * If no changes occur to property values, the output should be the same - * as the input, except for the leading date stamp, as normal for a - * properties file. Properties added are appended to the file. Properties - * whose values are changed are changed in place. Properties that are - * removed are excised. If the removeComments flag is set, - * then the comments immediately preceding the property are also removed.

- *

If a second set of properties is loaded into an existing set, the - * lines of the second set are added to the end. Note however, that if a - * property already stored is present in a stream subsequently loaded, then - * that property is removed before the new value is set. For example, - * consider the file

- *
 # the first line
- * alpha=one
- *
- * # the second line
- * beta=two
- *

This file is loaded, and then the following is also loaded into the - * same LayoutPreservingProperties object

- *
 # association
- * beta=band
- *
- * # and finally
- * gamma=rays
- *

The resulting collection sequence of logical lines depends on whether - * or not removeComments was set at the time the second stream - * is loaded. If it is set, then the resulting list of lines is

- *
 # the first line
- * alpha=one
- *
- * # association
- * beta=band
- *
- * # and finally
- * gamma=rays
- *

If the flag is not set, then the comment "the second line" is retained, - * although the key-value pair beta=two is removed.

- */ -public class LayoutPreservingProperties extends Properties { - private static final String LS = System.getProperty("line.separator"); - - /** - * Logical lines have escaping and line continuation taken care of. Comments - * and blank lines are logical lines; they are not removed. - */ - private ArrayList logicalLines = new ArrayList(); - - /** - * Position in the logicalLines list, keyed by property name. - */ - private HashMap keyedPairLines = new HashMap(); - - /** - * Flag to indicate that, when we remove a property from the file, we - * also want to remove the comments that precede it. - */ - private boolean removeComments; - - /** - * Create a new, empty, Properties collection, with no defaults. - */ - public LayoutPreservingProperties() { - super(); - } - - /** - * Create a new, empty, Properties collection, with the specified defaults. - * @param defaults the default property values - */ - public LayoutPreservingProperties(Properties defaults) { - super(defaults); - } - - /** - * Returns true if comments are removed along with properties, or - * false otherwise. If true, then when a property is - * removed, the comment preceding it in the original file is removed also. - * @return true if leading comments are removed when a property is - * removed; false otherwise - */ - public boolean isRemoveComments() { - return removeComments; - } - - /** - * Sets the behaviour for comments accompanying properties that are being - * removed. If true, then when a property is removed, the comment - * preceding it in the original file is removed also. - * @param val true if leading comments are to be removed when a property is - * removed; false otherwise - */ - public void setRemoveComments(boolean val) { - removeComments = val; - } - - public void load(InputStream inStream) throws IOException { - String s = readLines(inStream); - byte[] ba = s.getBytes("ISO-8859-1"); - ByteArrayInputStream bais = new ByteArrayInputStream(ba); - super.load(bais); - } - - public Object put(Object key, Object value) throws NullPointerException { - Object obj = super.put(key, value); - // the above call will have failed if key or value are null - innerSetProperty(key.toString(), value.toString()); - return obj; - } - - public Object setProperty(String key, String value) throws NullPointerException { - Object obj = super.setProperty(key, value); - // the above call will have failed if key or value are null - innerSetProperty(key, value); - return obj; - } - - /** - * Store a new key-value pair, or add a new one. The normal functionality is - * taken care of by the superclass in the call to {@link #setProperty}; this - * method takes care of this classes extensions. - * @param key the key of the property to be stored - * @param value the value to be stored - */ - private void innerSetProperty(String key, String value) { - value = escapeValue(value); - - if (keyedPairLines.containsKey(key)) { - Integer i = (Integer) keyedPairLines.get(key); - Pair p = (Pair) logicalLines.get(i.intValue()); - p.setValue(value); - } else { - key = escapeName(key); - Pair p = new Pair(key, value); - p.setNew(true); - keyedPairLines.put(key, new Integer(logicalLines.size())); - logicalLines.add(p); - } - } - - public void clear() { - super.clear(); - keyedPairLines.clear(); - logicalLines.clear(); - } - - public Object remove(Object key) { - Object obj = super.remove(key); - Integer i = (Integer) keyedPairLines.remove(key); - if (null != i) { - if (removeComments) { - removeCommentsEndingAt(i.intValue()); - } - logicalLines.set(i.intValue(), null); - } - return obj; - } - - public Object clone() { - LayoutPreservingProperties dolly = (LayoutPreservingProperties) super.clone(); - dolly.keyedPairLines = (HashMap) this.keyedPairLines.clone(); - dolly.logicalLines = (ArrayList) this.logicalLines.clone(); - for (int j = 0; j < dolly.logicalLines.size(); j++) { - LogicalLine line = (LogicalLine) dolly.logicalLines.get(j); - if (line instanceof Pair) { - Pair p = (Pair) line; - dolly.logicalLines.set(j, p.clone()); - } - // no reason to clone other lines are they are immutable - } - return dolly; - } - - /** - * Echo the lines of the properties (including blanks and comments) to the - * stream. - * @param out the stream to write to - */ - public void listLines(PrintStream out) { - out.println("-- logical lines --"); - Iterator i = logicalLines.iterator(); - while (i.hasNext()) { - LogicalLine line = (LogicalLine) i.next(); - if (line instanceof Blank) { - out.println("blank: \"" + line + "\""); - } - else if (line instanceof Comment) { - out.println("comment: \"" + line + "\""); - } - else if (line instanceof Pair) { - out.println("pair: \"" + line + "\""); - } - } - } - - /** - * Save the properties to a file. - * @param dest the file to write to - */ - public void saveAs(File dest) throws IOException { - FileOutputStream fos = new FileOutputStream(dest); - store(fos, null); - fos.close(); - } - - public void store(OutputStream out, String header) throws IOException { - OutputStreamWriter osw = new OutputStreamWriter(out, "ISO-8859-1"); - - if (header != null) { - osw.write("#" + header + LS); - } - osw.write("#" + (new Date()).toString() + LS); - - boolean writtenSep = false; - for (Iterator i = logicalLines.iterator();i.hasNext();) { - LogicalLine line = (LogicalLine) i.next(); - if (line instanceof Pair) { - if (((Pair)line).isNew()) { - if (!writtenSep) { - osw.write(LS); - } - } - osw.write(line.toString() + LS); - } - else if (line != null) { - osw.write(line.toString() + LS); - } - } - osw.close(); - } - - /** - * Reads a properties file into an internally maintained collection of logical - * lines (possibly spanning physcial lines), which make up the comments, blank - * lines and properties of the file. - * @param is the stream from which to read the data - */ - private String readLines(InputStream is) throws IOException { - InputStreamReader isr = new InputStreamReader(is, "ISO-8859-1"); - BufferedReader br = new BufferedReader(isr); - - if (logicalLines.size() > 0) { - // we add a blank line for spacing - logicalLines.add(new Blank()); - } - - String s = br.readLine(); - - boolean continuation = false; - boolean comment = false; - StringBuffer fileBuffer = new StringBuffer(); - StringBuffer logicalLineBuffer = new StringBuffer(); - while (s != null) { - fileBuffer.append(s).append(LS); - - if (continuation) { - // put in the line feed that was removed - s = "\n" + s; - } else { - // could be a comment, if first non-whitespace is a # or ! - comment = s.matches("^( |\t|\f)*(#|!).*"); - } - - // continuation if not a comment and the line ends is an odd number of backslashes - if (!comment) { - continuation = requiresContinuation(s); - } - - logicalLineBuffer.append(s); - - if (!continuation) { - LogicalLine line = null; - if (comment) { - line = new Comment(logicalLineBuffer.toString()); - } else if (logicalLineBuffer.toString().trim().length() == 0) { - line = new Blank(); - } else { - line = new Pair(logicalLineBuffer.toString()); - String key = unescape(((Pair)line).getName()); - if (keyedPairLines.containsKey(key)) { - // this key is already present, so we remove it and add - // the new one - remove(key); - } - keyedPairLines.put(key, new Integer(logicalLines.size())); - } - logicalLines.add(line); - logicalLineBuffer.setLength(0); - } - s = br.readLine(); - } - return fileBuffer.toString(); - } - - /** - * Returns true if the line represented by s is to be continued - * on the next line of the file, or false otherwise. - * @param s the contents of the line to examine - * @return true if the line is to be continued, false otherwise - */ - private boolean requiresContinuation(String s) { - char[] ca = s.toCharArray(); - int i = ca.length - 1; - while (i > 0 && ca[i] == '\\') { - i--; - } - // trailing backslashes - int tb = ca.length - i - 1; - return tb % 2 == 1; - } - - /** - * Unescape the string according to the rules for a Properites file, as laid out in - * the docs for java.util.Properties. - * @param s the string to unescape (coming from the source file) - * @return the unescaped string - */ - private String unescape(String s) { - /* - * The following combinations are converted: - * \n newline - * \r carraige return - * \f form feed - * \t tab - * \\ backslash - * \u0000 unicode character - * Any other slash is ignored, so - * \b becomes 'b'. - */ - - char[] ch = new char[s.length() + 1]; - s.getChars(0, s.length(), ch, 0); - ch[s.length()] = '\n'; - StringBuffer buffy = new StringBuffer(s.length()); - for (int i = 0; i < ch.length; i++) { - char c = ch[i]; - if (c == '\n') { - // we have hit out end-of-string marker - break; - } - else if (c == '\\') { - // possibly an escape sequence - c = ch[++i]; - if (c == 'n') - buffy.append('\n'); - else if (c == 'r') - buffy.append('\r'); - else if (c == 'f') - buffy.append('\f'); - else if (c == 't') - buffy.append('\t'); - else if (c == 'u') { - // handle unicode escapes - c = unescapeUnicode(ch, i+1); - i += 4; - buffy.append(c); - } - else - buffy.append(c); - } - else { - buffy.append(c); - } - } - return buffy.toString(); - } - - /** - * Retrieve the unicode character whose code is listed at position i - * in the character array ch. - * @param ch the character array containing the unicode character code - * @return the character extracted - */ - private char unescapeUnicode(char[] ch, int i) { - String s = new String(ch, i, 4); - return (char) Integer.parseInt(s, 16); - } - - /** - * Escape the string s according to the rules in the docs for - * java.util.Properties. - * @param s the string to escape - * @return the escaped string - */ - private String escapeValue(String s) { - return escape(s, false); - } - - /** - * Escape the string s according to the rules in the docs for - * java.util.Properties. - * This method escapes all the whitespace, not just the stuff at the beginning. - * @param s the string to escape - * @return the escaped string - */ - private String escapeName(String s) { - return escape(s, true); - } - - /** - * Escape the string s according to the rules in the docs for - * java.util.Properties. - * @param s the string to escape - * @param escapeAllSpaces if true the method escapes all the spaces, - * if false, it escapes only the leading whitespace - * @return the escaped string - */ - private String escape(String s, boolean escapeAllSpaces) { - if (s == null) { - return null; - } - - char[] ch = new char[s.length()]; - s.getChars(0, s.length(), ch, 0); - String forEscaping = "\t\f\r\n\\:=#!"; - String escaped = "tfrn\\:=#!"; - StringBuffer buffy = new StringBuffer(s.length()); - boolean leadingSpace = true; - for (int i = 0; i < ch.length; i++) { - char c = ch[i]; - if (c == ' ') { - if (escapeAllSpaces || leadingSpace) { - buffy.append("\\"); - } - } else { - leadingSpace = false; - } - int p = forEscaping.indexOf(c); - if (p != -1) { - buffy.append("\\").append(escaped.substring(p,p+1)); - } else if (c < 0x0020 || c > 0x007e) { - buffy.append(escapeUnicode(c)); - } else { - buffy.append(c); - } - } - return buffy.toString(); - } - - /** - * Return the unicode escape sequence for a character, in the form \u005CuNNNN. - * @param ch the character to encode - * @return the unicode escape sequence - */ - private String escapeUnicode(char ch) { - StringBuffer buffy = new StringBuffer("\\u"); - String hex = Integer.toHexString((int)ch); - buffy.append("0000".substring(4-hex.length())); - buffy.append(hex); - return buffy.toString(); - } - - /** - * Remove the comments in the leading up the {@link logicalLines} list leading - * up to line pos. - * @param pos the line number to which the comments lead - */ - private void removeCommentsEndingAt(int pos) { - /* We want to remove comments preceding this position. Step back counting - * blank lines (call this range B1) until we hit something non-blank. If - * what we hit is not a comment, then exit. If what we hit is a comment, - * then step back counting comment lines (call this range C1). Nullify - * lines in C1 and B1. - */ - - int end = pos - 1; - - // step pos back until it hits something non-blank - for (pos = end; pos > 0; pos--) { - if (!(logicalLines.get(pos) instanceof Blank)) { - break; - } - } - - // if the thing it hits is not a comment, then we have nothing to remove - if (!(logicalLines.get(pos) instanceof Comment)) { - return; - } - - // step back until we hit the start of the comment - for (; pos >= 0; pos--) { - if (!(logicalLines.get(pos) instanceof Comment)) { - break; - } - } - - // now we want to delete from pos+1 to end - for (pos++ ;pos <= end; pos++) { - logicalLines.set(pos, null); - } - } - - /** - * A logical line of the properties input stream. - */ - private static abstract class LogicalLine { - private String text; - - public LogicalLine(String text) { - this.text = text; - } - - public void setText(String text) { - this.text = text; - } - - public String toString() { - return text; - } - } - - /** - * A blank line of the input stream. - */ - private static class Blank extends LogicalLine { - public Blank() { - super(""); - } - } - - /** - * A comment line of the input stream. - */ - private class Comment extends LogicalLine { - public Comment(String text) { - super(text); - } - } - - /** - * A key-value pair from the input stream. This may span more than one physical - * line, but it is constitutes as a single logical line. - */ - private static class Pair extends LogicalLine implements Cloneable { - private String name; - private String value; - private boolean added; - - public Pair(String text) { - super(text); - parsePair(text); - } - - public Pair(String name, String value) { - this(name + "=" + value); - } - - public String getName() { - return name; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - setText(name + "=" + value); - } - - public boolean isNew() { - return added; - } - - public void setNew(boolean val) { - added = val; - } - - public Object clone() { - Object dolly = null; - try { - dolly = super.clone(); - } - catch (CloneNotSupportedException e) { - // should be fine - e.printStackTrace(); - } - return dolly; - } - - private void parsePair(String text) { - // need to find first non-escaped '=', ':', '\t' or ' '. - int pos = findFirstSeparator(text); - if (pos == -1) { - // trim leading whitespace only - name = text; - value = null; - } - else { - name = text.substring(0, pos); - value = text.substring(pos+1, text.length()); - } - // trim leading whitespace only - name = stripStart(name, " \t\f"); - } - - private String stripStart(String s, String chars) { - if (s == null) { - return null; - } - - int i = 0; - for (;i < s.length(); i++) { - if (chars.indexOf(s.charAt(i)) == -1) { - break; - } - } - if (i == s.length()) { - return ""; - } - return s.substring(i); - } - - private int findFirstSeparator(String s) { - // Replace double backslashes with underscores so that they don't - // confuse us looking for '\t' or '\=', for example, but they also - // don't change the position of other characters - s = s.replaceAll("\\\\\\\\", "__"); - - // Replace single backslashes followed by separators, so we don't - // pick them up - s = s.replaceAll("\\\\=", "__"); - s = s.replaceAll("\\\\:", "__"); - s = s.replaceAll("\\\\ ", "__"); - s = s.replaceAll("\\\\t", "__"); - - // Now only the unescaped separators are left - return indexOfAny(s, " :=\t"); - } - - private int indexOfAny(String s, String chars) { - if (s == null || chars == null) { - return -1; - } - - int p = s.length() + 1; - for (int i = 0; i < chars.length(); i++) { - int x = s.indexOf(chars.charAt(i)); - if (x != -1 && x < p) { - p = x; - } - } - if (p == s.length() + 1) { - return -1; - } - return p; - } - } -} \ No newline at end of file +/* + * 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.tools.ant.util; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Properties; + +/** + *

A Properties collection which preserves comments and whitespace + * present in the input stream from which it was loaded.

+ *

The class defers the usual work of the java.util.Properties + * class to there, but it also keeps track of the contents of the + * input stream from which it was loaded (if applicable), so that in can + * write out the properties in as close a form as possible to the input.

+ * If no changes occur to property values, the output should be the same + * as the input, except for the leading date stamp, as normal for a + * properties file. Properties added are appended to the file. Properties + * whose values are changed are changed in place. Properties that are + * removed are excised. If the removeComments flag is set, + * then the comments immediately preceding the property are also removed.

+ *

If a second set of properties is loaded into an existing set, the + * lines of the second set are added to the end. Note however, that if a + * property already stored is present in a stream subsequently loaded, then + * that property is removed before the new value is set. For example, + * consider the file

+ *
 # the first line
+ * alpha=one
+ *
+ * # the second line
+ * beta=two
+ *

This file is loaded, and then the following is also loaded into the + * same LayoutPreservingProperties object

+ *
 # association
+ * beta=band
+ *
+ * # and finally
+ * gamma=rays
+ *

The resulting collection sequence of logical lines depends on whether + * or not removeComments was set at the time the second stream + * is loaded. If it is set, then the resulting list of lines is

+ *
 # the first line
+ * alpha=one
+ *
+ * # association
+ * beta=band
+ *
+ * # and finally
+ * gamma=rays
+ *

If the flag is not set, then the comment "the second line" is retained, + * although the key-value pair beta=two is removed.

+ */ +public class LayoutPreservingProperties extends Properties { + private static final String LS = System.getProperty("line.separator"); + + /** + * Logical lines have escaping and line continuation taken care of. Comments + * and blank lines are logical lines; they are not removed. + */ + private ArrayList logicalLines = new ArrayList(); + + /** + * Position in the logicalLines list, keyed by property name. + */ + private HashMap keyedPairLines = new HashMap(); + + /** + * Flag to indicate that, when we remove a property from the file, we + * also want to remove the comments that precede it. + */ + private boolean removeComments; + + /** + * Create a new, empty, Properties collection, with no defaults. + */ + public LayoutPreservingProperties() { + super(); + } + + /** + * Create a new, empty, Properties collection, with the specified defaults. + * @param defaults the default property values + */ + public LayoutPreservingProperties(Properties defaults) { + super(defaults); + } + + /** + * Returns true if comments are removed along with properties, or + * false otherwise. If true, then when a property is + * removed, the comment preceding it in the original file is removed also. + * @return true if leading comments are removed when a property is + * removed; false otherwise + */ + public boolean isRemoveComments() { + return removeComments; + } + + /** + * Sets the behaviour for comments accompanying properties that are being + * removed. If true, then when a property is removed, the comment + * preceding it in the original file is removed also. + * @param val true if leading comments are to be removed when a property is + * removed; false otherwise + */ + public void setRemoveComments(boolean val) { + removeComments = val; + } + + public void load(InputStream inStream) throws IOException { + String s = readLines(inStream); + byte[] ba = s.getBytes("ISO-8859-1"); + ByteArrayInputStream bais = new ByteArrayInputStream(ba); + super.load(bais); + } + + public Object put(Object key, Object value) throws NullPointerException { + Object obj = super.put(key, value); + // the above call will have failed if key or value are null + innerSetProperty(key.toString(), value.toString()); + return obj; + } + + public Object setProperty(String key, String value) throws NullPointerException { + Object obj = super.setProperty(key, value); + // the above call will have failed if key or value are null + innerSetProperty(key, value); + return obj; + } + + /** + * Store a new key-value pair, or add a new one. The normal functionality is + * taken care of by the superclass in the call to {@link #setProperty}; this + * method takes care of this classes extensions. + * @param key the key of the property to be stored + * @param value the value to be stored + */ + private void innerSetProperty(String key, String value) { + value = escapeValue(value); + + if (keyedPairLines.containsKey(key)) { + Integer i = (Integer) keyedPairLines.get(key); + Pair p = (Pair) logicalLines.get(i.intValue()); + p.setValue(value); + } else { + key = escapeName(key); + Pair p = new Pair(key, value); + p.setNew(true); + keyedPairLines.put(key, new Integer(logicalLines.size())); + logicalLines.add(p); + } + } + + public void clear() { + super.clear(); + keyedPairLines.clear(); + logicalLines.clear(); + } + + public Object remove(Object key) { + Object obj = super.remove(key); + Integer i = (Integer) keyedPairLines.remove(key); + if (null != i) { + if (removeComments) { + removeCommentsEndingAt(i.intValue()); + } + logicalLines.set(i.intValue(), null); + } + return obj; + } + + public Object clone() { + LayoutPreservingProperties dolly = (LayoutPreservingProperties) super.clone(); + dolly.keyedPairLines = (HashMap) this.keyedPairLines.clone(); + dolly.logicalLines = (ArrayList) this.logicalLines.clone(); + for (int j = 0; j < dolly.logicalLines.size(); j++) { + LogicalLine line = (LogicalLine) dolly.logicalLines.get(j); + if (line instanceof Pair) { + Pair p = (Pair) line; + dolly.logicalLines.set(j, p.clone()); + } + // no reason to clone other lines are they are immutable + } + return dolly; + } + + /** + * Echo the lines of the properties (including blanks and comments) to the + * stream. + * @param out the stream to write to + */ + public void listLines(PrintStream out) { + out.println("-- logical lines --"); + Iterator i = logicalLines.iterator(); + while (i.hasNext()) { + LogicalLine line = (LogicalLine) i.next(); + if (line instanceof Blank) { + out.println("blank: \"" + line + "\""); + } + else if (line instanceof Comment) { + out.println("comment: \"" + line + "\""); + } + else if (line instanceof Pair) { + out.println("pair: \"" + line + "\""); + } + } + } + + /** + * Save the properties to a file. + * @param dest the file to write to + */ + public void saveAs(File dest) throws IOException { + FileOutputStream fos = new FileOutputStream(dest); + store(fos, null); + fos.close(); + } + + public void store(OutputStream out, String header) throws IOException { + OutputStreamWriter osw = new OutputStreamWriter(out, "ISO-8859-1"); + + if (header != null) { + osw.write("#" + header + LS); + } + osw.write("#" + (new Date()).toString() + LS); + + boolean writtenSep = false; + for (Iterator i = logicalLines.iterator();i.hasNext();) { + LogicalLine line = (LogicalLine) i.next(); + if (line instanceof Pair) { + if (((Pair)line).isNew()) { + if (!writtenSep) { + osw.write(LS); + } + } + osw.write(line.toString() + LS); + } + else if (line != null) { + osw.write(line.toString() + LS); + } + } + osw.close(); + } + + /** + * Reads a properties file into an internally maintained collection of logical + * lines (possibly spanning physcial lines), which make up the comments, blank + * lines and properties of the file. + * @param is the stream from which to read the data + */ + private String readLines(InputStream is) throws IOException { + InputStreamReader isr = new InputStreamReader(is, "ISO-8859-1"); + BufferedReader br = new BufferedReader(isr); + + if (logicalLines.size() > 0) { + // we add a blank line for spacing + logicalLines.add(new Blank()); + } + + String s = br.readLine(); + + boolean continuation = false; + boolean comment = false; + StringBuffer fileBuffer = new StringBuffer(); + StringBuffer logicalLineBuffer = new StringBuffer(); + while (s != null) { + fileBuffer.append(s).append(LS); + + if (continuation) { + // put in the line feed that was removed + s = "\n" + s; + } else { + // could be a comment, if first non-whitespace is a # or ! + comment = s.matches("^( |\t|\f)*(#|!).*"); + } + + // continuation if not a comment and the line ends is an odd number of backslashes + if (!comment) { + continuation = requiresContinuation(s); + } + + logicalLineBuffer.append(s); + + if (!continuation) { + LogicalLine line = null; + if (comment) { + line = new Comment(logicalLineBuffer.toString()); + } else if (logicalLineBuffer.toString().trim().length() == 0) { + line = new Blank(); + } else { + line = new Pair(logicalLineBuffer.toString()); + String key = unescape(((Pair)line).getName()); + if (keyedPairLines.containsKey(key)) { + // this key is already present, so we remove it and add + // the new one + remove(key); + } + keyedPairLines.put(key, new Integer(logicalLines.size())); + } + logicalLines.add(line); + logicalLineBuffer.setLength(0); + } + s = br.readLine(); + } + return fileBuffer.toString(); + } + + /** + * Returns true if the line represented by s is to be continued + * on the next line of the file, or false otherwise. + * @param s the contents of the line to examine + * @return true if the line is to be continued, false otherwise + */ + private boolean requiresContinuation(String s) { + char[] ca = s.toCharArray(); + int i = ca.length - 1; + while (i > 0 && ca[i] == '\\') { + i--; + } + // trailing backslashes + int tb = ca.length - i - 1; + return tb % 2 == 1; + } + + /** + * Unescape the string according to the rules for a Properites file, as laid out in + * the docs for java.util.Properties. + * @param s the string to unescape (coming from the source file) + * @return the unescaped string + */ + private String unescape(String s) { + /* + * The following combinations are converted: + * \n newline + * \r carraige return + * \f form feed + * \t tab + * \\ backslash + * \u0000 unicode character + * Any other slash is ignored, so + * \b becomes 'b'. + */ + + char[] ch = new char[s.length() + 1]; + s.getChars(0, s.length(), ch, 0); + ch[s.length()] = '\n'; + StringBuffer buffy = new StringBuffer(s.length()); + for (int i = 0; i < ch.length; i++) { + char c = ch[i]; + if (c == '\n') { + // we have hit out end-of-string marker + break; + } + else if (c == '\\') { + // possibly an escape sequence + c = ch[++i]; + if (c == 'n') + buffy.append('\n'); + else if (c == 'r') + buffy.append('\r'); + else if (c == 'f') + buffy.append('\f'); + else if (c == 't') + buffy.append('\t'); + else if (c == 'u') { + // handle unicode escapes + c = unescapeUnicode(ch, i+1); + i += 4; + buffy.append(c); + } + else + buffy.append(c); + } + else { + buffy.append(c); + } + } + return buffy.toString(); + } + + /** + * Retrieve the unicode character whose code is listed at position i + * in the character array ch. + * @param ch the character array containing the unicode character code + * @return the character extracted + */ + private char unescapeUnicode(char[] ch, int i) { + String s = new String(ch, i, 4); + return (char) Integer.parseInt(s, 16); + } + + /** + * Escape the string s according to the rules in the docs for + * java.util.Properties. + * @param s the string to escape + * @return the escaped string + */ + private String escapeValue(String s) { + return escape(s, false); + } + + /** + * Escape the string s according to the rules in the docs for + * java.util.Properties. + * This method escapes all the whitespace, not just the stuff at the beginning. + * @param s the string to escape + * @return the escaped string + */ + private String escapeName(String s) { + return escape(s, true); + } + + /** + * Escape the string s according to the rules in the docs for + * java.util.Properties. + * @param s the string to escape + * @param escapeAllSpaces if true the method escapes all the spaces, + * if false, it escapes only the leading whitespace + * @return the escaped string + */ + private String escape(String s, boolean escapeAllSpaces) { + if (s == null) { + return null; + } + + char[] ch = new char[s.length()]; + s.getChars(0, s.length(), ch, 0); + String forEscaping = "\t\f\r\n\\:=#!"; + String escaped = "tfrn\\:=#!"; + StringBuffer buffy = new StringBuffer(s.length()); + boolean leadingSpace = true; + for (int i = 0; i < ch.length; i++) { + char c = ch[i]; + if (c == ' ') { + if (escapeAllSpaces || leadingSpace) { + buffy.append("\\"); + } + } else { + leadingSpace = false; + } + int p = forEscaping.indexOf(c); + if (p != -1) { + buffy.append("\\").append(escaped.substring(p,p+1)); + } else if (c < 0x0020 || c > 0x007e) { + buffy.append(escapeUnicode(c)); + } else { + buffy.append(c); + } + } + return buffy.toString(); + } + + /** + * Return the unicode escape sequence for a character, in the form \u005CuNNNN. + * @param ch the character to encode + * @return the unicode escape sequence + */ + private String escapeUnicode(char ch) { + StringBuffer buffy = new StringBuffer("\\u"); + String hex = Integer.toHexString((int)ch); + buffy.append("0000".substring(4-hex.length())); + buffy.append(hex); + return buffy.toString(); + } + + /** + * Remove the comments in the leading up the {@link logicalLines} list leading + * up to line pos. + * @param pos the line number to which the comments lead + */ + private void removeCommentsEndingAt(int pos) { + /* We want to remove comments preceding this position. Step back counting + * blank lines (call this range B1) until we hit something non-blank. If + * what we hit is not a comment, then exit. If what we hit is a comment, + * then step back counting comment lines (call this range C1). Nullify + * lines in C1 and B1. + */ + + int end = pos - 1; + + // step pos back until it hits something non-blank + for (pos = end; pos > 0; pos--) { + if (!(logicalLines.get(pos) instanceof Blank)) { + break; + } + } + + // if the thing it hits is not a comment, then we have nothing to remove + if (!(logicalLines.get(pos) instanceof Comment)) { + return; + } + + // step back until we hit the start of the comment + for (; pos >= 0; pos--) { + if (!(logicalLines.get(pos) instanceof Comment)) { + break; + } + } + + // now we want to delete from pos+1 to end + for (pos++ ;pos <= end; pos++) { + logicalLines.set(pos, null); + } + } + + /** + * A logical line of the properties input stream. + */ + private static abstract class LogicalLine { + private String text; + + public LogicalLine(String text) { + this.text = text; + } + + public void setText(String text) { + this.text = text; + } + + public String toString() { + return text; + } + } + + /** + * A blank line of the input stream. + */ + private static class Blank extends LogicalLine { + public Blank() { + super(""); + } + } + + /** + * A comment line of the input stream. + */ + private class Comment extends LogicalLine { + public Comment(String text) { + super(text); + } + } + + /** + * A key-value pair from the input stream. This may span more than one physical + * line, but it is constitutes as a single logical line. + */ + private static class Pair extends LogicalLine implements Cloneable { + private String name; + private String value; + private boolean added; + + public Pair(String text) { + super(text); + parsePair(text); + } + + public Pair(String name, String value) { + this(name + "=" + value); + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + setText(name + "=" + value); + } + + public boolean isNew() { + return added; + } + + public void setNew(boolean val) { + added = val; + } + + public Object clone() { + Object dolly = null; + try { + dolly = super.clone(); + } + catch (CloneNotSupportedException e) { + // should be fine + e.printStackTrace(); + } + return dolly; + } + + private void parsePair(String text) { + // need to find first non-escaped '=', ':', '\t' or ' '. + int pos = findFirstSeparator(text); + if (pos == -1) { + // trim leading whitespace only + name = text; + value = null; + } + else { + name = text.substring(0, pos); + value = text.substring(pos+1, text.length()); + } + // trim leading whitespace only + name = stripStart(name, " \t\f"); + } + + private String stripStart(String s, String chars) { + if (s == null) { + return null; + } + + int i = 0; + for (;i < s.length(); i++) { + if (chars.indexOf(s.charAt(i)) == -1) { + break; + } + } + if (i == s.length()) { + return ""; + } + return s.substring(i); + } + + private int findFirstSeparator(String s) { + // Replace double backslashes with underscores so that they don't + // confuse us looking for '\t' or '\=', for example, but they also + // don't change the position of other characters + s = s.replaceAll("\\\\\\\\", "__"); + + // Replace single backslashes followed by separators, so we don't + // pick them up + s = s.replaceAll("\\\\=", "__"); + s = s.replaceAll("\\\\:", "__"); + s = s.replaceAll("\\\\ ", "__"); + s = s.replaceAll("\\\\t", "__"); + + // Now only the unescaped separators are left + return indexOfAny(s, " :=\t"); + } + + private int indexOfAny(String s, String chars) { + if (s == null || chars == null) { + return -1; + } + + int p = s.length() + 1; + for (int i = 0; i < chars.length(); i++) { + int x = s.indexOf(chars.charAt(i)); + if (x != -1 && x < p) { + p = x; + } + } + if (p == s.length() + 1) { + return -1; + } + return p; + } + } +} diff --git a/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java b/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java index 0115f74cf..0cfdd8f44 100644 --- a/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java +++ b/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java @@ -1,284 +1,284 @@ -/* - * 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.tools.ant.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.IOException; -import java.util.Properties; - -import junit.framework.TestCase; - -public class LayoutPreservingPropertiesTest extends TestCase { - public LayoutPreservingPropertiesTest(String s) { - super(s); - } - - /** - * Tests that a properties file read by the LayoutPreservingPropertiesFile - * and then saves the properties in it. - */ - public void testPreserve() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // now compare original and tmp for property equivalence - Properties originalProps = new Properties(); - originalProps.load(new FileInputStream(simple)); - - Properties tmpProps = new Properties(); - tmpProps.load(new FileInputStream(tmp)); - - assertEquals("properties corrupted", originalProps, tmpProps); - - // and now make sure that the comments made it into the new file - String s = readFile(tmp); - assertTrue("missing comment", s.indexOf("# a comment") > -1); - assertTrue("missing comment", s.indexOf("! more comment") > -1); - } - - /** - * Tests that names and value are properly escaped when being - * written out. - */ - public void testEscaping() throws Exception { - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - - lpf.setProperty(" prop one ", " leading and trailing spaces "); - lpf.setProperty("prop\ttwo", "contains\ttab"); - lpf.setProperty("prop\nthree", "contains\nnewline"); - lpf.setProperty("prop\rfour", "contains\rcarraige return"); - lpf.setProperty("prop\ffive", "contains\fform feed"); - lpf.setProperty("prop\\six", "contains\\backslash"); - lpf.setProperty("prop:seven", "contains:colon"); - lpf.setProperty("prop=eight", "contains=equals"); - lpf.setProperty("prop#nine", "contains#hash"); - lpf.setProperty("prop!ten", "contains!exclamation"); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue(s.indexOf("\\ prop\\ one\\ =\\ \\ leading and trailing spaces ") > -1); - assertTrue(s.indexOf("prop\\ttwo=contains\\ttab") > -1); - assertTrue(s.indexOf("prop\\nthree=contains\\nnewline") > -1); - assertTrue(s.indexOf("prop\\rfour=contains\\rcarraige return") > -1); - assertTrue(s.indexOf("prop\\\\six=contains\\\\backslash") > -1); - assertTrue(s.indexOf("prop\\:seven=contains\\:colon") > -1); - assertTrue(s.indexOf("prop\\=eight=contains\\=equals") > -1); - assertTrue(s.indexOf("prop\\#nine=contains\\#hash") > -1); - assertTrue(s.indexOf("prop\\!ten=contains\\!exclamation") > -1); - } - - /** - * Tests that properties are correctly indexed, so that when we set - * an existing property, it updates the logical line, and it doesn't - * append a new one. - */ - public void testOverwrite() throws Exception { - File unusual = new File(System.getProperty("root"), "src/etc/testcases/util/unusual.properties"); - FileInputStream fis = new FileInputStream(unusual); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - lpf.setProperty(" prop one ", "new one"); - lpf.setProperty("prop\ttwo", "new two"); - lpf.setProperty("prop\nthree", "new three"); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue(s.indexOf("\\ prop\\ one\\ =\\ \\ leading and trailing spaces ") == -1); - assertTrue(s.indexOf("\\ prop\\ one\\ =new one") > -1); - assertTrue(s.indexOf("prop\\ttwo=contains\\ttab") == -1); - assertTrue(s.indexOf("prop\\ttwo=new two") > -1); - assertTrue(s.indexOf("prop\\nthree=contains\\nnewline") == -1); - assertTrue(s.indexOf("prop\\nthree=new three") > -1); - } - - public void testStoreWithHeader() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - FileOutputStream fos = new FileOutputStream(tmp); - lpf.store(fos, "file-header"); - fos.close(); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue("should have had header ", s.startsWith("#file-header")); - } - - public void testClear() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - lpf.clear(); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue("should have had no properties ", s.indexOf("prop.alpha") == -1); - assertTrue("should have had no properties ", s.indexOf("prop.beta") == -1); - assertTrue("should have had no properties ", s.indexOf("prop.gamma") == -1); - - assertTrue("should have had no comments", s.indexOf("# a comment") == -1); - assertTrue("should have had no comments", s.indexOf("! more comment") == -1); - assertTrue("should have had no comments", s.indexOf("# now a line wrapping one") == -1); - } - - public void testRemove() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - lpf.remove("prop.beta"); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue("should not have had prop.beta", s.indexOf("prop.beta") == -1); - assertTrue("should have had prop.beta's comment", s.indexOf("! more comment") > -1); - } - - public void testRemoveWithComment() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - lpf.load(fis); - - lpf.setRemoveComments(true); - - lpf.remove("prop.beta"); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue("should not have had prop.beta", s.indexOf("prop.beta") == -1); - assertTrue("should not have had prop.beta's comment", s.indexOf("! more comment") == -1); - } - - public void testClone() throws Exception { - File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); - FileInputStream fis = new FileInputStream(simple); - LayoutPreservingProperties lpf1 = new LayoutPreservingProperties(); - lpf1.load(fis); - - LayoutPreservingProperties lpf2 = (LayoutPreservingProperties) lpf1.clone(); - - lpf2.setProperty("prop.new", "a new property"); - lpf2.setProperty("prop.beta", "a new value for beta"); - - assertEquals("size of original is wrong", 3, lpf1.size()); - assertEquals("size of clone is wrong", 4, lpf2.size()); - - File tmp1 = File.createTempFile("tmp", "props"); - tmp1.deleteOnExit(); - lpf1.saveAs(tmp1); - String s1 = readFile(tmp1); - - File tmp2 = File.createTempFile("tmp", "props"); - tmp2.deleteOnExit(); - lpf2.saveAs(tmp2); - String s2 = readFile(tmp2); - - // check original is untouched - assertTrue("should have had 'simple'", s1.indexOf("simple") > -1); - assertTrue("should not have had prop.new", s1.indexOf("prop.new") == -1); - - // check clone has the changes - assertTrue("should have had 'a new value for beta'", s2.indexOf("a new value for beta") > -1); - assertTrue("should have had prop.new", s2.indexOf("prop.new") > -1); - } - - public void testPreserveEsacpeName() throws Exception { - LayoutPreservingProperties lpf = new LayoutPreservingProperties(); - File unusual = new File(System.getProperty("root"), "src/etc/testcases/util/unusual.properties"); - FileInputStream fis = new FileInputStream(unusual); - lpf.load(fis); - - lpf.setProperty("prop:seven", "new value for seven"); - lpf.setProperty("prop=eight", "new value for eight"); - lpf.setProperty("prop eleven", "new value for eleven"); - - lpf.setProperty("alpha", "new value for alpha"); - lpf.setProperty("beta", "new value for beta"); - - File tmp = File.createTempFile("tmp", "props"); - tmp.deleteOnExit(); - lpf.saveAs(tmp); - - // and check that the resulting file looks okay - String s = readFile(tmp); - - assertTrue(s.indexOf("prop\\:seven=new value for seven") > -1); - assertTrue(s.indexOf("prop\\=eight=new value for eight") > -1); - assertTrue(s.indexOf("prop\\ eleven=new value for eleven") > -1); - assertTrue(s.indexOf("alpha=new value for alpha") > -1); - assertTrue(s.indexOf("beta=new value for beta") > -1); - - assertTrue(s.indexOf("prop\\:seven=contains\\:colon") == -1); - assertTrue(s.indexOf("prop\\=eight=contains\\=equals") == -1); - assertTrue(s.indexOf("alpha:set with a colon") == -1); - assertTrue(s.indexOf("beta set with a space") == -1); - } - - private String readFile(File f) throws IOException { - FileInputStream fis = new FileInputStream(f); - InputStreamReader isr = new InputStreamReader(fis); - String s = FileUtils.readFully(isr); - isr.close(); - fis.close(); - return s; - } -} +/* + * 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.tools.ant.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.util.Properties; + +import junit.framework.TestCase; + +public class LayoutPreservingPropertiesTest extends TestCase { + public LayoutPreservingPropertiesTest(String s) { + super(s); + } + + /** + * Tests that a properties file read by the LayoutPreservingPropertiesFile + * and then saves the properties in it. + */ + public void testPreserve() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // now compare original and tmp for property equivalence + Properties originalProps = new Properties(); + originalProps.load(new FileInputStream(simple)); + + Properties tmpProps = new Properties(); + tmpProps.load(new FileInputStream(tmp)); + + assertEquals("properties corrupted", originalProps, tmpProps); + + // and now make sure that the comments made it into the new file + String s = readFile(tmp); + assertTrue("missing comment", s.indexOf("# a comment") > -1); + assertTrue("missing comment", s.indexOf("! more comment") > -1); + } + + /** + * Tests that names and value are properly escaped when being + * written out. + */ + public void testEscaping() throws Exception { + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + + lpf.setProperty(" prop one ", " leading and trailing spaces "); + lpf.setProperty("prop\ttwo", "contains\ttab"); + lpf.setProperty("prop\nthree", "contains\nnewline"); + lpf.setProperty("prop\rfour", "contains\rcarraige return"); + lpf.setProperty("prop\ffive", "contains\fform feed"); + lpf.setProperty("prop\\six", "contains\\backslash"); + lpf.setProperty("prop:seven", "contains:colon"); + lpf.setProperty("prop=eight", "contains=equals"); + lpf.setProperty("prop#nine", "contains#hash"); + lpf.setProperty("prop!ten", "contains!exclamation"); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue(s.indexOf("\\ prop\\ one\\ =\\ \\ leading and trailing spaces ") > -1); + assertTrue(s.indexOf("prop\\ttwo=contains\\ttab") > -1); + assertTrue(s.indexOf("prop\\nthree=contains\\nnewline") > -1); + assertTrue(s.indexOf("prop\\rfour=contains\\rcarraige return") > -1); + assertTrue(s.indexOf("prop\\\\six=contains\\\\backslash") > -1); + assertTrue(s.indexOf("prop\\:seven=contains\\:colon") > -1); + assertTrue(s.indexOf("prop\\=eight=contains\\=equals") > -1); + assertTrue(s.indexOf("prop\\#nine=contains\\#hash") > -1); + assertTrue(s.indexOf("prop\\!ten=contains\\!exclamation") > -1); + } + + /** + * Tests that properties are correctly indexed, so that when we set + * an existing property, it updates the logical line, and it doesn't + * append a new one. + */ + public void testOverwrite() throws Exception { + File unusual = new File(System.getProperty("root"), "src/etc/testcases/util/unusual.properties"); + FileInputStream fis = new FileInputStream(unusual); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + lpf.setProperty(" prop one ", "new one"); + lpf.setProperty("prop\ttwo", "new two"); + lpf.setProperty("prop\nthree", "new three"); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue(s.indexOf("\\ prop\\ one\\ =\\ \\ leading and trailing spaces ") == -1); + assertTrue(s.indexOf("\\ prop\\ one\\ =new one") > -1); + assertTrue(s.indexOf("prop\\ttwo=contains\\ttab") == -1); + assertTrue(s.indexOf("prop\\ttwo=new two") > -1); + assertTrue(s.indexOf("prop\\nthree=contains\\nnewline") == -1); + assertTrue(s.indexOf("prop\\nthree=new three") > -1); + } + + public void testStoreWithHeader() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + FileOutputStream fos = new FileOutputStream(tmp); + lpf.store(fos, "file-header"); + fos.close(); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue("should have had header ", s.startsWith("#file-header")); + } + + public void testClear() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + lpf.clear(); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue("should have had no properties ", s.indexOf("prop.alpha") == -1); + assertTrue("should have had no properties ", s.indexOf("prop.beta") == -1); + assertTrue("should have had no properties ", s.indexOf("prop.gamma") == -1); + + assertTrue("should have had no comments", s.indexOf("# a comment") == -1); + assertTrue("should have had no comments", s.indexOf("! more comment") == -1); + assertTrue("should have had no comments", s.indexOf("# now a line wrapping one") == -1); + } + + public void testRemove() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + lpf.remove("prop.beta"); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue("should not have had prop.beta", s.indexOf("prop.beta") == -1); + assertTrue("should have had prop.beta's comment", s.indexOf("! more comment") > -1); + } + + public void testRemoveWithComment() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + lpf.load(fis); + + lpf.setRemoveComments(true); + + lpf.remove("prop.beta"); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue("should not have had prop.beta", s.indexOf("prop.beta") == -1); + assertTrue("should not have had prop.beta's comment", s.indexOf("! more comment") == -1); + } + + public void testClone() throws Exception { + File simple = new File(System.getProperty("root"), "src/etc/testcases/util/simple.properties"); + FileInputStream fis = new FileInputStream(simple); + LayoutPreservingProperties lpf1 = new LayoutPreservingProperties(); + lpf1.load(fis); + + LayoutPreservingProperties lpf2 = (LayoutPreservingProperties) lpf1.clone(); + + lpf2.setProperty("prop.new", "a new property"); + lpf2.setProperty("prop.beta", "a new value for beta"); + + assertEquals("size of original is wrong", 3, lpf1.size()); + assertEquals("size of clone is wrong", 4, lpf2.size()); + + File tmp1 = File.createTempFile("tmp", "props"); + tmp1.deleteOnExit(); + lpf1.saveAs(tmp1); + String s1 = readFile(tmp1); + + File tmp2 = File.createTempFile("tmp", "props"); + tmp2.deleteOnExit(); + lpf2.saveAs(tmp2); + String s2 = readFile(tmp2); + + // check original is untouched + assertTrue("should have had 'simple'", s1.indexOf("simple") > -1); + assertTrue("should not have had prop.new", s1.indexOf("prop.new") == -1); + + // check clone has the changes + assertTrue("should have had 'a new value for beta'", s2.indexOf("a new value for beta") > -1); + assertTrue("should have had prop.new", s2.indexOf("prop.new") > -1); + } + + public void testPreserveEsacpeName() throws Exception { + LayoutPreservingProperties lpf = new LayoutPreservingProperties(); + File unusual = new File(System.getProperty("root"), "src/etc/testcases/util/unusual.properties"); + FileInputStream fis = new FileInputStream(unusual); + lpf.load(fis); + + lpf.setProperty("prop:seven", "new value for seven"); + lpf.setProperty("prop=eight", "new value for eight"); + lpf.setProperty("prop eleven", "new value for eleven"); + + lpf.setProperty("alpha", "new value for alpha"); + lpf.setProperty("beta", "new value for beta"); + + File tmp = File.createTempFile("tmp", "props"); + tmp.deleteOnExit(); + lpf.saveAs(tmp); + + // and check that the resulting file looks okay + String s = readFile(tmp); + + assertTrue(s.indexOf("prop\\:seven=new value for seven") > -1); + assertTrue(s.indexOf("prop\\=eight=new value for eight") > -1); + assertTrue(s.indexOf("prop\\ eleven=new value for eleven") > -1); + assertTrue(s.indexOf("alpha=new value for alpha") > -1); + assertTrue(s.indexOf("beta=new value for beta") > -1); + + assertTrue(s.indexOf("prop\\:seven=contains\\:colon") == -1); + assertTrue(s.indexOf("prop\\=eight=contains\\=equals") == -1); + assertTrue(s.indexOf("alpha:set with a colon") == -1); + assertTrue(s.indexOf("beta set with a space") == -1); + } + + private String readFile(File f) throws IOException { + FileInputStream fis = new FileInputStream(f); + InputStreamReader isr = new InputStreamReader(fis); + String s = FileUtils.readFully(isr); + isr.close(); + fis.close(); + return s; + } +}