blob: dce20377215d63d691b4bd2e9a5462ef5365e800 [file] [log] [blame]
/*
* 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;
}
}