From 75899750abfdfbdcb60c2aba9f86e2f12364a867 Mon Sep 17 00:00:00 2001 From: Kevin Jackson Date: Wed, 20 Aug 2008 18:12:57 +0000 Subject: [PATCH] apply patch for #36901 LayoutPreservingProprties (includes tests) git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@687402 13f79535-47bb-0310-9956-ffa450edef68 --- docs/manual/OptionalTasks/propertyfile.html | 44 +- src/etc/testcases/util/simple.properties | 9 + src/etc/testcases/util/unusual.properties | 23 + .../ant/taskdefs/optional/PropertyFile.java | 17 +- .../ant/util/LayoutPreservingProperties.java | 689 ++++++++++++++++++ .../util/LayoutPreservingPropertiesTest.java | 284 ++++++++ 6 files changed, 1053 insertions(+), 13 deletions(-) create mode 100644 src/etc/testcases/util/simple.properties create mode 100644 src/etc/testcases/util/unusual.properties create mode 100644 src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java create mode 100644 src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java diff --git a/docs/manual/OptionalTasks/propertyfile.html b/docs/manual/OptionalTasks/propertyfile.html index b57398303..d914b2ebd 100644 --- a/docs/manual/OptionalTasks/propertyfile.html +++ b/docs/manual/OptionalTasks/propertyfile.html @@ -44,8 +44,8 @@

Ant provides an optional task for editing property files. This is very useful when wanting to make unattended modifications to configuration files for application servers and applications. Currently, the task maintains a working property file with -the ability to add properties or make changes to existing ones. However, any comments -are lost.

+the ability to add properties or make changes to existing ones. Comments and layout +of the original properties file are preserved.


PropertyFile Task

@@ -66,8 +66,15 @@ are lost.

Header for the file itself no + + jdkproperties + Use java.lang.Properties, which will loose comments and layout of file (default is 'false') + no + +

The boolean attribute 'jdkproperties' is provided to recover the previous behaviour of the task, in which the layout and any comments in the properties file were lost by the task.

+

Parameters specified as nested elements

Entry

Use nested <entry> @@ -151,20 +158,35 @@ operation occurs after these rules are considered.

The following changes the my.properties file. Assume my.properties look like:

-
# A comment
-akey=novalue
+
# A string value
+akey=original value
+
+# The following is a counter, which will be incremented by 1 for
+# each time the build is run.
+anint=1

After running, the file would now look like

-
#Thu Nov 02 23:41:47 EST 2000
+
#My properties
+#Wed Aug 31 13:47:19 BST 2005
+# A string value
 akey=avalue
-adate=2000/11/02 23\:41
-anint=1
+
+# The following is a counter, which will be incremented by 1 for
+# each time the build is run.
+anint=2
+
+adate=2005/08/31 13\:47
+
 formated.int=0014
-formated.date=028 17\:34
-
+ +formated.date=243 13\:47
+

+The slashes conform to the expectations of the Properties class. The file will be stored in a manner so that each character is examined and escaped if necessary. +

+

-The slashes conform to the expectations of the Properties class. The file will be stored in a manner so that each character is examined and escaped if necessary. Note that the original comment is now lost. Please keep this in mind when running this task against heavily commented properties files. It may be best to have a commented version in the source tree, copy it to a deployment area, and then run the modifications on the copy. Future versions of PropertyFile will hopefully eliminate this shortcoming. +The layout and comment of the original file is preserved. New properties are added at the end of the file. Existing properties are overwritten in place.

<propertyfile
@@ -172,7 +194,7 @@ The slashes conform to the expectations of the Properties class.  The file will
     comment="My properties">
   <entry  key="akey" value="avalue"/>
   <entry  key="adate" type="date" value="now"/>
-  <entry  key="anint" type="int" operation="+"/>
+  <entry  key="anint" type="int" default="0" operation="+"/>
   <entry  key="formated.int" type="int" default="0013" operation="+" pattern="0000"/>
   <entry  key="formated.date" type="date" value="now" pattern="DDD HH:mm"/>
 </propertyfile>
diff --git a/src/etc/testcases/util/simple.properties b/src/etc/testcases/util/simple.properties
new file mode 100644
index 000000000..8716ea660
--- /dev/null
+++ b/src/etc/testcases/util/simple.properties
@@ -0,0 +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.
diff --git a/src/etc/testcases/util/unusual.properties b/src/etc/testcases/util/unusual.properties
new file mode 100644
index 000000000..8ef6d0338
--- /dev/null
+++ b/src/etc/testcases/util/unusual.properties
@@ -0,0 +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
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/PropertyFile.java b/src/main/org/apache/tools/ant/taskdefs/optional/PropertyFile.java
index bc03f755a..f247a6614 100644
--- a/src/main/org/apache/tools/ant/taskdefs/optional/PropertyFile.java
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/PropertyFile.java
@@ -38,6 +38,7 @@ import java.util.Vector;
 import org.apache.tools.ant.BuildException;
 import org.apache.tools.ant.Task;
 import org.apache.tools.ant.util.FileUtils;
+import org.apache.tools.ant.util.LayoutPreservingProperties;
 import org.apache.tools.ant.types.EnumeratedAttribute;
 
 /**
@@ -118,6 +119,7 @@ public class PropertyFile extends Task {
 
     private Properties          properties;
     private File                propertyfile;
+    private boolean             useJDKProperties;
 
     private Vector entries = new Vector();
 
@@ -160,8 +162,12 @@ public class PropertyFile extends Task {
     }
 
     private void readFile() throws BuildException {
-        // Create the PropertyFile
-        properties = new Properties();
+        if (useJDKProperties) {
+            // user chose to use standard Java properties, which loose comments and layout
+            properties = new Properties();
+        } else {
+            properties = new LayoutPreservingProperties();
+        }
         try {
             if (propertyfile.exists()) {
                 log("Updating property file: "
@@ -216,6 +222,13 @@ public class PropertyFile extends Task {
         comment = hdr;
     }
 
+    /**
+     * optional flag to use original Java properties (as opposed to layout preserving properties)
+     */
+    public void setJDKProperties(boolean val) {
+        useJDKProperties = val;
+    }
+    
     private void writeFile() throws BuildException {
         BufferedOutputStream bos = null;
         try {
diff --git a/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java b/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java
new file mode 100644
index 000000000..55fdf4808
--- /dev/null
+++ b/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java
@@ -0,0 +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 diff --git a/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java b/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java new file mode 100644 index 000000000..0115f74cf --- /dev/null +++ b/src/tests/junit/org/apache/tools/ant/util/LayoutPreservingPropertiesTest.java @@ -0,0 +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; + } +}