| /* |
| * 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.openjpa.lib.util; |
| |
| import java.io.BufferedReader; |
| import java.io.FilterInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.Serializable; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| /* |
| * ### things to add: - should probably be a SourceTracker |
| * - if an entry is removed, should there be an option to remove comments |
| * just before the entry(a la javadoc)? |
| * - should we have an option to clean up whitespace? |
| * - potentially would be interesting to add comments about each |
| * property that OpenJPA adds to this object. We'd want to make the |
| * automatic comment-removing code work first, though, so that if |
| * someone then removed the property, the comments would go away. |
| * - would be neat if DuplicateKeyException could report line numbers of |
| * offending entries. |
| * - putAll() with another FormatPreservingProperties should be smarter |
| */ |
| |
| /** |
| * A specialization of {@link Properties} that stores its contents |
| * in the same order and with the same formatting as was used to read |
| * the contents from an input stream. This is useful because it means |
| * that a properties file loaded via this object and then written |
| * back out later on will only be different where changes or |
| * additions were made. |
| * By default, the {@link #store} method in this class does not |
| * behave the same as {@link Properties#store}. You can cause an |
| * instance to approximate the behavior of {@link Properties#store} |
| * by invoking {@link #setDefaultEntryDelimiter} with <code>=</code>, |
| * {@link #setAddWhitespaceAfterDelimiter} with <code>false</code>, and |
| * {@link #setAllowDuplicates} with <code>true</code>. However, this |
| * will only influence how the instance will write new values, not how |
| * it will write existing key-value pairs that are modified. |
| * In conjunction with a conservative output writer, it is |
| * possible to only write to disk changes / additions. |
| * This implementation does not permit escaped ' ', '=', ':' |
| * characters in key names. |
| * |
| * @since 0.3.3 |
| */ |
| public class FormatPreservingProperties extends Properties { |
| |
| |
| private static final long serialVersionUID = 1L; |
| |
| private static Localizer _loc = Localizer.forPackage |
| (FormatPreservingProperties.class); |
| |
| private char defaultEntryDelimiter = ':'; |
| private boolean addWhitespaceAfterDelimiter = true; |
| private boolean allowDuplicates = false; |
| private boolean insertTimestamp = false; |
| |
| private PropertySource source; |
| private LinkedHashSet newKeys = new LinkedHashSet(); |
| private HashSet modifiedKeys = new HashSet(); |
| |
| // marker that indicates that we're not deserializing |
| private transient boolean isNotDeserializing = true; |
| private transient boolean isLoading = false; |
| |
| public FormatPreservingProperties() { |
| this(null); |
| } |
| |
| public FormatPreservingProperties(Properties defaults) { |
| super(defaults); |
| } |
| |
| /** |
| * The character to use as a delimiter between property keys and values. |
| * |
| * @param defaultEntryDelimiter either ':' or '=' |
| */ |
| public void setDefaultEntryDelimiter(char defaultEntryDelimiter) { |
| this.defaultEntryDelimiter = defaultEntryDelimiter; |
| } |
| |
| /** |
| * See {@link #setDefaultEntryDelimiter} |
| */ |
| public char getDefaultEntryDelimiter() { |
| return this.defaultEntryDelimiter; |
| } |
| |
| /** |
| * If set to <code>true</code>, this properties object will add a |
| * space after the delimiter character(if the delimiter is not |
| * the space character). Else, this will not add a space. |
| * Default value: <code>true</code>. Note that {@link |
| * Properties#store} never writes whitespace. |
| */ |
| public void setAddWhitespaceAfterDelimiter(boolean add) { |
| this.addWhitespaceAfterDelimiter = add; |
| } |
| |
| /** |
| * If set to <code>true</code>, this properties object will add a |
| * space after the delimiter character(if the delimiter is not |
| * the space character). Else, this will not add a space. |
| * Default value: <code>true</code>. Note that {@link |
| * Properties#store} never writes whitespace. |
| */ |
| public boolean getAddWhitespaceAfterDelimiter() { |
| return this.addWhitespaceAfterDelimiter; |
| } |
| |
| /** |
| * If set to <code>true</code>, this properties object will add a |
| * timestamp to the beginning of the file, just after the header |
| * (if any) is printed. Else, this will not add a timestamp. |
| * Default value: <code>false</code>. Note that {@link |
| * Properties#store} always writes a timestamp. |
| */ |
| public void setInsertTimestamp(boolean insertTimestamp) { |
| this.insertTimestamp = insertTimestamp; |
| } |
| |
| /** |
| * If set to <code>true</code>, this properties object will add a |
| * timestamp to the beginning of the file, just after the header |
| * (if any) is printed. Else, this will not add a timestamp. |
| * Default value: <code>false</code>. Note that {@link |
| * Properties#store} always writes a timestamp. |
| */ |
| public boolean getInsertTimestamp() { |
| return this.insertTimestamp; |
| } |
| |
| /** |
| * If set to <code>true</code>, duplicate properties are allowed, and |
| * the last property setting in the input will overwrite any previous |
| * settings. If set to <code>false</code>, duplicate property definitions |
| * in the input will cause an exception to be thrown during {@link #load}. |
| * Default value: <code>false</code>. Note that {@link |
| * Properties#store} always allows duplicates. |
| */ |
| public void setAllowDuplicates(boolean allowDuplicates) { |
| this.allowDuplicates = allowDuplicates; |
| } |
| |
| /** |
| * If set to <code>true</code>, duplicate properties are allowed, and |
| * the last property setting in the input will overwrite any previous |
| * settings. If set to <code>false</code>, duplicate property definitions |
| * in the input will cause an exception to be thrown during {@link #load}. |
| * Default value: <code>false</code>. Note that {@link |
| * Properties#store} always allows duplicates. |
| */ |
| public boolean getAllowDuplicates() { |
| return this.allowDuplicates; |
| } |
| |
| @Override |
| public String getProperty(String key) { |
| return super.getProperty(key); |
| } |
| |
| @Override |
| public String getProperty(String key, String defaultValue) { |
| return super.getProperty(key, defaultValue); |
| } |
| |
| @Override |
| public Object setProperty(String key, String value) { |
| return put(key, value); |
| } |
| |
| /** |
| * Circumvents the superclass {@link #putAll} implementation, |
| * putting all the key-value pairs via {@link #put}. |
| */ |
| @Override |
| public synchronized void putAll(Map m) { |
| Map.Entry e; |
| for (Object o : m.entrySet()) { |
| e = (Map.Entry) o; |
| put(e.getKey(), e.getValue()); |
| } |
| } |
| |
| /** |
| * Removes the key from the bookkeeping collectiotns as well. |
| */ |
| @Override |
| public synchronized Object remove(Object key) { |
| newKeys.remove(key); |
| return super.remove(key); |
| } |
| |
| @Override |
| public synchronized void clear() { |
| super.clear(); |
| |
| if (source != null) |
| source.clear(); |
| |
| newKeys.clear(); |
| modifiedKeys.clear(); |
| } |
| |
| @Override |
| public synchronized Object clone() { |
| FormatPreservingProperties c = (FormatPreservingProperties) |
| super.clone(); |
| |
| if (source != null) |
| c.source = (PropertySource) source.clone(); |
| |
| if (modifiedKeys != null) |
| c.modifiedKeys = (HashSet) modifiedKeys.clone(); |
| |
| if (newKeys != null) { |
| c.newKeys = new LinkedHashSet(); |
| c.newKeys.addAll(newKeys); |
| } |
| |
| return c; |
| } |
| |
| private void writeObject(ObjectOutputStream out) throws IOException { |
| out.defaultWriteObject(); |
| } |
| |
| private void readObject(ObjectInputStream in) |
| throws IOException, ClassNotFoundException { |
| in.defaultReadObject(); |
| |
| isNotDeserializing = true; |
| } |
| |
| @Override |
| public synchronized Object put(Object key, Object val) { |
| Object o = super.put(key, val); |
| |
| // if we're no longer loading from properties and this put |
| // represents an actual change in value, mark the modification |
| // or addition in the bookkeeping collections. |
| if (!isLoading && isNotDeserializing && !val.equals(o)) { |
| if (o != null) |
| modifiedKeys.add(key); |
| else if (!newKeys.contains(key)) |
| newKeys.add(key); |
| } |
| return o; |
| } |
| |
| /** |
| * Loads the properties in <code>in</code>, according to the rules |
| * described in {@link Properties#load}. If {@link #getAllowDuplicates} |
| * returns <code>true</code>, this will throw a {@link |
| * DuplicateKeyException} if duplicate property declarations are |
| * encountered. |
| * |
| * @see Properties#load |
| */ |
| @Override |
| public synchronized void load(InputStream in) throws IOException { |
| isLoading = true; |
| try { |
| loadProperties(in); |
| } finally { |
| isLoading = false; |
| } |
| } |
| |
| private void loadProperties(InputStream in) throws IOException { |
| source = new PropertySource(); |
| |
| PropertyLineReader reader = new PropertyLineReader(in, source); |
| |
| Set loadedKeys = new HashSet(); |
| |
| for (PropertyLine l; |
| (l = reader.readPropertyLine()) != null && source.add(l);) { |
| String line = l.line.toString(); |
| |
| char c = 0; |
| int pos = 0; |
| |
| while (pos < line.length() && isSpace(c = line.charAt(pos))) |
| pos++; |
| |
| if ((line.length() - pos) == 0 |
| || line.charAt(pos) == '#' || line.charAt(pos) == '!') |
| continue; |
| |
| StringBuilder key = new StringBuilder(); |
| while (pos < line.length() && !isSpace(c = line.charAt(pos++)) |
| && c != '=' && c != ':') { |
| if (c == '\\') { |
| if (pos == line.length()) { |
| l.append(line = reader.readLine()); |
| pos = 0; |
| while (pos < line.length() |
| && isSpace(c = line.charAt(pos))) |
| pos++; |
| } else { |
| pos = readEscape(line, pos, key); |
| } |
| } else { |
| key.append(c); |
| } |
| } |
| |
| boolean isDelim = (c == ':' || c == '='); |
| |
| for (; pos < line.length() |
| && isSpace(c = line.charAt(pos)); pos++) |
| ; |
| |
| if (!isDelim && (c == ':' || c == '=')) { |
| pos++; |
| while (pos < line.length() && isSpace(c = line.charAt(pos))) |
| pos++; |
| } |
| |
| StringBuilder element = new StringBuilder(line.length() - pos); |
| |
| while (pos < line.length()) { |
| c = line.charAt(pos++); |
| if (c == '\\') { |
| if (pos == line.length()) { |
| l.append(line = reader.readLine()); |
| |
| if (line == null) |
| break; |
| |
| pos = 0; |
| while (pos < line.length() |
| && isSpace(c = line.charAt(pos))) |
| pos++; |
| element.ensureCapacity(line.length() - pos + |
| element.length()); |
| } else { |
| pos = readEscape(line, pos, element); |
| } |
| } else |
| element.append(c); |
| } |
| |
| if (!loadedKeys.add(key.toString()) && !allowDuplicates) |
| throw new DuplicateKeyException(key.toString(), |
| getProperty(key.toString()), element.toString()); |
| |
| l.setPropertyKey(key.toString()); |
| l.setPropertyValue(element.toString()); |
| put(key.toString(), element.toString()); |
| } |
| } |
| |
| /** |
| * Read the next escaped character: handle newlines, tabs, returns, and |
| * form feeds with the appropriate escaped character, then try to |
| * decode unicode characters. Finally, just add the character explicitly. |
| * |
| * @param source the source of the characters |
| * @param pos the position at which to start reading |
| * @param value the value we are appending to |
| * @return the position after the reading is done |
| */ |
| private static int readEscape(String source, int pos, StringBuilder value) { |
| char c = source.charAt(pos++); |
| switch (c) { |
| case 'n': |
| value.append('\n'); |
| break; |
| case 't': |
| value.append('\t'); |
| break; |
| case 'f': |
| value.append('\f'); |
| break; |
| case 'r': |
| value.append('\r'); |
| break; |
| case 'u': |
| if (pos + 4 <= source.length()) { |
| char uni = (char) Integer.parseInt |
| (source.substring(pos, pos + 4), 16); |
| value.append(uni); |
| pos += 4; |
| } |
| break; |
| default: |
| value.append(c); |
| break; |
| } |
| |
| return pos; |
| } |
| |
| private static boolean isSpace(char ch) { |
| return Character.isWhitespace(ch); |
| } |
| |
| @Override |
| public void save(OutputStream out, String header) { |
| try { |
| store(out, header); |
| } catch (IOException ex) { |
| } |
| } |
| |
| @Override |
| public void store(OutputStream out, String header) throws IOException { |
| boolean endWithNewline = source != null && source.endsInNewline; |
| |
| // Must be ISO-8859-1 ecoding according to Properties.load javadoc |
| PrintWriter writer = new PrintWriter |
| (new OutputStreamWriter(out, "ISO-8859-1"), false); |
| |
| if (header != null) |
| writer.println("#" + header); |
| |
| if (insertTimestamp) |
| writer.println("#" + Calendar.getInstance().getTime()); |
| |
| List lines = new LinkedList(); |
| // first write all the existing props as they were initially read |
| if (source != null) |
| lines.addAll(source); |
| |
| // next write out new keys, then the rest of the keys |
| LinkedHashSet keys = new LinkedHashSet(); |
| keys.addAll(newKeys); |
| keys.addAll(keySet()); |
| |
| lines.addAll(keys); |
| |
| keys.remove(null); |
| |
| boolean needsNewline = false; |
| |
| for (Iterator i = lines.iterator(); i.hasNext();) { |
| Object next = i.next(); |
| |
| if (next instanceof PropertyLine) { |
| if (((PropertyLine) next).write(writer, keys, needsNewline)) |
| needsNewline = i.hasNext(); |
| } else if (next instanceof String) { |
| String key = (String) next; |
| if (keys.remove(key)) { |
| if (writeProperty(key, writer, needsNewline)) { |
| needsNewline = i.hasNext() && keys.size() > 0; |
| |
| // any new or modified properties will cause |
| // the file to end with a newline |
| endWithNewline = true; |
| } |
| } |
| } |
| } |
| |
| // make sure we end in a newline if the source ended in it |
| if (endWithNewline) |
| writer.println(); |
| |
| writer.flush(); |
| } |
| |
| private boolean writeProperty(String key, PrintWriter writer, |
| boolean needsNewline) { |
| StringBuilder s = new StringBuilder(); |
| |
| if (key == null) |
| return false; |
| |
| String val = getProperty(key); |
| if (val == null) |
| return false; |
| |
| formatValue(key, s, true); |
| s.append(defaultEntryDelimiter); |
| if (addWhitespaceAfterDelimiter) |
| s.append(' '); |
| formatValue(val, s, false); |
| |
| if (needsNewline) |
| writer.println(); |
| |
| writer.print(s); |
| |
| return true; |
| } |
| |
| /** |
| * Format the given string as an encoded value for storage. This will |
| * perform any necessary escaping of special characters. |
| * |
| * @param str the value to encode |
| * @param buf the buffer to which to append the encoded value |
| * @param isKey if true, then the string is a Property key, otherwise |
| * it is a value |
| */ |
| private static void formatValue(String str, StringBuilder buf, |
| boolean isKey) { |
| if (isKey) { |
| buf.setLength(0); |
| buf.ensureCapacity(str.length()); |
| } else { |
| buf.ensureCapacity(buf.length() + str.length()); |
| } |
| |
| boolean escapeSpace = true; |
| int size = str.length(); |
| |
| for (int i = 0; i < size; i++) { |
| char c = str.charAt(i); |
| |
| if (c == '\n') |
| buf.append("\\n"); |
| else if (c == '\r') |
| buf.append("\\r"); |
| else if (c == '\t') |
| buf.append("\\t"); |
| else if (c == '\f') |
| buf.append("\\f"); |
| else if (c == ' ') |
| buf.append(escapeSpace ? "\\ " : " "); |
| else if (c == '\\' || c == '!' || c == '#' || c == '=' || c == ':') |
| buf.append('\\').append(c); |
| else if (c < ' ' || c > '~') |
| buf.append("\\u0000".substring(0, 6 - Integer.toHexString(c). |
| length())).append(Integer.toHexString(c)); |
| else |
| buf.append(c); |
| |
| if (c != ' ') |
| escapeSpace = isKey; |
| } |
| } |
| |
| public static class DuplicateKeyException extends RuntimeException { |
| |
| |
| private static final long serialVersionUID = 1L; |
| |
| public DuplicateKeyException(String key, Object firstVal, |
| String secondVal) { |
| super(_loc.get("dup-key", key, firstVal, secondVal).getMessage()); |
| } |
| } |
| |
| /** |
| * Contains the original line of the properties file: can be a |
| * proper key/value pair, or a comment, or just whitespace. |
| */ |
| private class PropertyLine implements Serializable { |
| |
| |
| private static final long serialVersionUID = 1L; |
| private final StringBuilder line = new StringBuilder(); |
| private String propertyKey; |
| private String propertyValue; |
| |
| public PropertyLine(String line) { |
| this.line.append(line); |
| } |
| |
| public void append(String newline) { |
| line.append(J2DoPrivHelper.getLineSeparator()); |
| line.append(newline); |
| } |
| |
| public void setPropertyKey(String propertyKey) { |
| this.propertyKey = propertyKey; |
| } |
| |
| public String getPropertyKey() { |
| return this.propertyKey; |
| } |
| |
| public void setPropertyValue(String propertyValue) { |
| this.propertyValue = propertyValue; |
| } |
| |
| public String getPropertyValue() { |
| return this.propertyValue; |
| } |
| |
| /** |
| * Write the given line. It will only be written if the line is a |
| * comment, or if it is a property and its value is unchanged |
| * from the original. |
| * |
| * @param pw the PrintWriter to which the write |
| * @return whether or not this was a known key |
| */ |
| public boolean write(PrintWriter pw, Collection keys, |
| boolean needsNewline) { |
| // no property? It may be a comment or just whitespace |
| if (propertyKey == null) { |
| if (needsNewline) |
| pw.println(); |
| pw.print(line.toString()); |
| return true; |
| } |
| |
| // check to see if we are the same value we initially read: |
| // if so, then just write it back exactly as it was read |
| if (propertyValue != null && containsKey(propertyKey) && |
| (propertyValue.equals(getProperty(propertyKey)) || |
| (!newKeys.contains(propertyKey) && |
| !modifiedKeys.contains(propertyKey)))) { |
| if (needsNewline) |
| pw.println(); |
| pw.print(line.toString()); |
| |
| keys.remove(propertyKey); |
| |
| return true; |
| } |
| |
| // if we have modified or added the specified key, then write |
| // it back to the same location in the file from which it |
| // was originally read, so that it will be in the proximity |
| // to the comment |
| if (containsKey(propertyKey) && |
| (modifiedKeys.contains(propertyKey) || |
| newKeys.contains(propertyKey))) { |
| while (keys.remove(propertyKey)) ; |
| return writeProperty(propertyKey, pw, needsNewline); |
| } |
| |
| // this is a new or changed property: don't do anything |
| return false; |
| } |
| } |
| |
| private class PropertyLineReader extends BufferedReader { |
| |
| public PropertyLineReader(InputStream in, PropertySource source) |
| throws IOException { |
| // Must be ISO-8859-1 ecoding according to Properties.load javadoc |
| super(new InputStreamReader(new LineEndingStream(in, source), |
| "ISO-8859-1")); |
| } |
| |
| public PropertyLine readPropertyLine() throws IOException { |
| String l = readLine(); |
| if (l == null) |
| return null; |
| |
| PropertyLine pl = new PropertyLine(l); |
| return pl; |
| } |
| } |
| |
| /** |
| * Simple FilterInputStream that merely remembers if the last |
| * character that it read was a newline or not. |
| */ |
| private static class LineEndingStream extends FilterInputStream { |
| |
| private final PropertySource source; |
| |
| LineEndingStream(InputStream in, PropertySource source) { |
| super(in); |
| |
| this.source = source; |
| } |
| |
| @Override |
| public int read() throws IOException { |
| int c = super.read(); |
| source.endsInNewline = (c == '\n' || c == '\r'); |
| return c; |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| int n = super.read(b, off, len); |
| if (n > 0) |
| source.endsInNewline = |
| (b[n + off - 1] == '\n' || b[n + off - 1] == '\r'); |
| return n; |
| } |
| } |
| |
| static class PropertySource extends LinkedList |
| implements Cloneable, Serializable { |
| |
| |
| private static final long serialVersionUID = 1L; |
| private boolean endsInNewline = false; |
| } |
| } |