aboutsummaryrefslogtreecommitdiffstats
path: root/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java
diff options
context:
space:
mode:
Diffstat (limited to 'framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java')
-rw-r--r--framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java775
1 files changed, 775 insertions, 0 deletions
diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java
new file mode 100644
index 00000000..aed6f371
--- /dev/null
+++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/util/LayoutPreservingProperties.java
@@ -0,0 +1,775 @@
+/*
+ * 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.io.PushbackReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Properties;
+
+/**
+ * <p>A Properties collection which preserves comments and whitespace
+ * present in the input stream from which it was loaded.</p>
+ * <p>The class defers the usual work of the <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>
+ * class to there, but it also keeps track of the contents of the
+ * input stream from which it was loaded (if applicable), so that it can
+ * write out the properties in as close a form as possible to the input.</p>
+ * <p>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 <code>removeComments</code> flag is set,
+ * then the comments immediately preceding the property are also removed.</p>
+ * <p>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</p>
+ * <pre> # the first line
+ * alpha=one
+ *
+ * # the second line
+ * beta=two</pre>
+ * <p>This file is loaded, and then the following is also loaded into the
+ * same <code>LayoutPreservingProperties</code> object</p>
+ * <pre> # association
+ * beta=band
+ *
+ * # and finally
+ * gamma=rays</pre>
+ * <p>The resulting collection sequence of logical lines depends on whether
+ * or not <code>removeComments</code> was set at the time the second stream
+ * is loaded. If it is set, then the resulting list of lines is</p>
+ * <pre> # the first line
+ * alpha=one
+ *
+ * # association
+ * beta=band
+ *
+ * # and finally
+ * gamma=rays</pre>
+ * <p>If the flag is not set, then the comment "the second line" is retained,
+ * although the key-value pair <code>beta=two</code> is removed.</p>
+ */
+public class LayoutPreservingProperties extends Properties {
+ private String LS = StringUtils.LINE_SEP;
+
+ /**
+ * 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 <code>logicalLines</code> 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(final Properties defaults) {
+ super(defaults);
+ }
+
+ /**
+ * Returns <code>true</code> if comments are removed along with
+ * properties, or <code>false</code> otherwise. If
+ * <code>true</code>, then when a property is removed, the comment
+ * preceding it in the original file is removed also.
+ * @return <code>true</code> if leading comments are removed when
+ * a property is removed; <code>false</code> otherwise
+ */
+ public boolean isRemoveComments() {
+ return removeComments;
+ }
+
+ /**
+ * Sets the behaviour for comments accompanying properties that
+ * are being removed. If <code>true</code>, then when a property
+ * is removed, the comment preceding it in the original file is
+ * removed also.
+ * @param val <code>true</code> if leading comments are to be
+ * removed when a property is removed; <code>false</code>
+ * otherwise
+ */
+ public void setRemoveComments(final boolean val) {
+ removeComments = val;
+ }
+
+ @Override
+ public void load(final InputStream inStream) throws IOException {
+ final String s = readLines(inStream);
+ final byte[] ba = s.getBytes(ResourceUtils.ISO_8859_1);
+ final ByteArrayInputStream bais = new ByteArrayInputStream(ba);
+ super.load(bais);
+ }
+
+ @Override
+ public Object put(final Object key, final Object value) throws NullPointerException {
+ final 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;
+ }
+
+ @Override
+ public Object setProperty(final String key, final String value)
+ throws NullPointerException {
+ final 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)) {
+ final Integer i = (Integer) keyedPairLines.get(key);
+ final Pair p = (Pair) logicalLines.get(i.intValue());
+ p.setValue(value);
+ } else {
+ key = escapeName(key);
+ final Pair p = new Pair(key, value);
+ p.setNew(true);
+ keyedPairLines.put(key, new Integer(logicalLines.size()));
+ logicalLines.add(p);
+ }
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ keyedPairLines.clear();
+ logicalLines.clear();
+ }
+
+ @Override
+ public Object remove(final Object key) {
+ final Object obj = super.remove(key);
+ final Integer i = (Integer) keyedPairLines.remove(key);
+ if (null != i) {
+ if (removeComments) {
+ removeCommentsEndingAt(i.intValue());
+ }
+ logicalLines.set(i.intValue(), null);
+ }
+ return obj;
+ }
+
+ @Override
+ public Object clone() {
+ final LayoutPreservingProperties dolly =
+ (LayoutPreservingProperties) super.clone();
+ dolly.keyedPairLines = (HashMap) this.keyedPairLines.clone();
+ dolly.logicalLines = (ArrayList) this.logicalLines.clone();
+ final int size = dolly.logicalLines.size();
+ for (int j = 0; j < size; j++) {
+ final LogicalLine line = (LogicalLine) dolly.logicalLines.get(j);
+ if (line instanceof Pair) {
+ final 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(final PrintStream out) {
+ out.println("-- logical lines --");
+ final Iterator i = logicalLines.iterator();
+ while (i.hasNext()) {
+ final 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(final File dest) throws IOException {
+ final FileOutputStream fos = new FileOutputStream(dest);
+ store(fos, null);
+ fos.close();
+ }
+
+ @Override
+ public void store(final OutputStream out, final String header) throws IOException {
+ final OutputStreamWriter osw = new OutputStreamWriter(out, ResourceUtils.ISO_8859_1);
+
+ int skipLines = 0;
+ final int totalLines = logicalLines.size();
+
+ if (header != null) {
+ osw.write("#" + header + LS);
+ if (totalLines > 0
+ && logicalLines.get(0) instanceof Comment
+ && header.equals(logicalLines.get(0).toString().substring(1))) {
+ skipLines = 1;
+ }
+ }
+
+ // we may be updatiung a file written by this class, replace
+ // the date comment instead of adding a new one and preserving
+ // the one written last time
+ if (totalLines > skipLines
+ && logicalLines.get(skipLines) instanceof Comment) {
+ try {
+ DateUtils.parseDateFromHeader(logicalLines
+ .get(skipLines)
+ .toString().substring(1));
+ skipLines++;
+ } catch (final java.text.ParseException pe) {
+ // not an existing date comment
+ }
+ }
+ osw.write("#" + DateUtils.getDateForHeader() + LS);
+
+ boolean writtenSep = false;
+ for (final Iterator i = logicalLines.subList(skipLines, totalLines).iterator();
+ i.hasNext();) {
+ final LogicalLine line = (LogicalLine) i.next();
+ if (line instanceof Pair) {
+ if (((Pair)line).isNew()) {
+ if (!writtenSep) {
+ osw.write(LS);
+ writtenSep = true;
+ }
+ }
+ 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(final InputStream is) throws IOException {
+ final InputStreamReader isr = new InputStreamReader(is, ResourceUtils.ISO_8859_1);
+ final PushbackReader pbr = new PushbackReader(isr, 1);
+
+ if (logicalLines.size() > 0) {
+ // we add a blank line for spacing
+ logicalLines.add(new Blank());
+ }
+
+ String s = readFirstLine(pbr);
+ final BufferedReader br = new BufferedReader(pbr);
+
+ boolean continuation = false;
+ boolean comment = false;
+ final StringBuffer fileBuffer = new StringBuffer();
+ final 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());
+ final 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();
+ }
+
+ /**
+ * Reads the first line and determines the EOL-style of the file
+ * (relies on the style to be consistent, of course).
+ *
+ * <p>Sets LS as a side-effect.</p>
+ *
+ * @return the first line without any line separator, leaves the
+ * reader positioned after the first line separator
+ *
+ * @since Ant 1.8.2
+ */
+ private String readFirstLine(final PushbackReader r) throws IOException {
+ final StringBuffer sb = new StringBuffer(80);
+ int ch = r.read();
+ boolean hasCR = false;
+ // when reaching EOF before the first EOL, assume native line
+ // feeds
+ LS = StringUtils.LINE_SEP;
+
+ while (ch >= 0) {
+ if (hasCR && ch != '\n') {
+ // line feed is sole CR
+ r.unread(ch);
+ break;
+ }
+
+ if (ch == '\r') {
+ LS = "\r";
+ hasCR = true;
+ } else if (ch == '\n') {
+ LS = hasCR ? "\r\n" : "\n";
+ break;
+ } else {
+ sb.append((char) ch);
+ }
+ ch = r.read();
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns <code>true</code> if the line represented by
+ * <code>s</code> is to be continued on the next line of the file,
+ * or <code>false</code> otherwise.
+ * @param s the contents of the line to examine
+ * @return <code>true</code> if the line is to be continued,
+ * <code>false</code> otherwise
+ */
+ private boolean requiresContinuation(final String s) {
+ final char[] ca = s.toCharArray();
+ int i = ca.length - 1;
+ while (i > 0 && ca[i] == '\\') {
+ i--;
+ }
+ // trailing backslashes
+ final 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 <a
+ * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
+ * @param s the string to unescape (coming from the source file)
+ * @return the unescaped string
+ */
+ private String unescape(final 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'.
+ */
+
+ final char[] ch = new char[s.length() + 1];
+ s.getChars(0, s.length(), ch, 0);
+ ch[s.length()] = '\n';
+ final 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
+ * <code>i</code> in the character array <code>ch</code>.
+ * @param ch the character array containing the unicode character code
+ * @return the character extracted
+ */
+ private char unescapeUnicode(final char[] ch, final int i) {
+ final String s = new String(ch, i, 4);
+ return (char) Integer.parseInt(s, 16);
+ }
+
+ /**
+ * Escape the string <code>s</code> according to the rules in the
+ * docs for <a
+ * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
+ * @param s the string to escape
+ * @return the escaped string
+ */
+ private String escapeValue(final String s) {
+ return escape(s, false);
+ }
+
+ /**
+ * Escape the string <code>s</code> according to the rules in the
+ * docs for <a
+ * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
+ * 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(final String s) {
+ return escape(s, true);
+ }
+
+ /**
+ * Escape the string <code>s</code> according to the rules in the
+ * docs for <a
+ * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
+ * @param s the string to escape
+ * @param escapeAllSpaces if <code>true</code> the method escapes
+ * all the spaces, if <code>false</code>, it escapes only the
+ * leading whitespace
+ * @return the escaped string
+ */
+ private String escape(final String s, final boolean escapeAllSpaces) {
+ if (s == null) {
+ return null;
+ }
+
+ final char[] ch = new char[s.length()];
+ s.getChars(0, s.length(), ch, 0);
+ final String forEscaping = "\t\f\r\n\\:=#!";
+ final String escaped = "tfrn\\:=#!";
+ final StringBuffer buffy = new StringBuffer(s.length());
+ boolean leadingSpace = true;
+ for (int i = 0; i < ch.length; i++) {
+ final char c = ch[i];
+ if (c == ' ') {
+ if (escapeAllSpaces || leadingSpace) {
+ buffy.append("\\");
+ }
+ } else {
+ leadingSpace = false;
+ }
+ final 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(final char ch) {
+ return "\\" + UnicodeUtil.EscapeUnicode(ch);
+ }
+
+ /**
+ * Remove the comments in the leading up the {@link logicalLines}
+ * list leading up to line <code>pos</code>.
+ * @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.
+ */
+
+ final 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 abstract static class LogicalLine {
+ private String text;
+
+ public LogicalLine(final String text) {
+ this.text = text;
+ }
+
+ public void setText(final String text) {
+ this.text = text;
+ }
+
+ @Override
+ 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(final 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(final String text) {
+ super(text);
+ parsePair(text);
+ }
+
+ public Pair(final String name, final String value) {
+ this(name + "=" + value);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(final String value) {
+ this.value = value;
+ setText(name + "=" + value);
+ }
+
+ public boolean isNew() {
+ return added;
+ }
+
+ public void setNew(final boolean val) {
+ added = val;
+ }
+
+ @Override
+ public Object clone() {
+ Object dolly = null;
+ try {
+ dolly = super.clone();
+ } catch (final CloneNotSupportedException e) {
+ // should be fine
+ e.printStackTrace();
+ }
+ return dolly;
+ }
+
+ private void parsePair(final String text) {
+ // need to find first non-escaped '=', ':', '\t' or ' '.
+ final 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(final String s, final 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(final String s, final String chars) {
+ if (s == null || chars == null) {
+ return -1;
+ }
+
+ int p = s.length() + 1;
+ for (int i = 0; i < chars.length(); i++) {
+ final int x = s.indexOf(chars.charAt(i));
+ if (x != -1 && x < p) {
+ p = x;
+ }
+ }
+ if (p == s.length() + 1) {
+ return -1;
+ }
+ return p;
+ }
+ }
+}