diff options
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.java | 775 |
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; + } + } +} |