001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */
019: package org.apache.openjpa.lib.util;
020:
021: import java.io.BufferedReader;
022: import java.io.FilterInputStream;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.InputStreamReader;
026: import java.io.ObjectInputStream;
027: import java.io.ObjectOutputStream;
028: import java.io.OutputStream;
029: import java.io.OutputStreamWriter;
030: import java.io.PrintWriter;
031: import java.io.Serializable;
032: import java.util.Calendar;
033: import java.util.Collection;
034: import java.util.HashSet;
035: import java.util.Iterator;
036: import java.util.LinkedHashSet;
037: import java.util.LinkedList;
038: import java.util.List;
039: import java.util.Map;
040: import java.util.Properties;
041: import java.util.Set;
042:
043: /*
044: * ### things to add: - should probably be a SourceTracker
045: * - if an entry is removed, should there be an option to remove comments
046: * just before the entry(a la javadoc)?
047: * - should we have an option to clean up whitespace?
048: * - potentially would be interesting to add comments about each
049: * property that OpenJPA adds to this object. We'd want to make the
050: * automatic comment-removing code work first, though, so that if
051: * someone then removed the property, the comments would go away.
052: * - would be neat if DuplicateKeyException could report line numbers of
053: * offending entries.
054: * - putAll() with another FormatPreservingProperties should be smarter
055: */
056:
057: /**
058: * A specialization of {@link Properties} that stores its contents
059: * in the same order and with the same formatting as was used to read
060: * the contents from an input stream. This is useful because it means
061: * that a properties file loaded via this object and then written
062: * back out later on will only be different where changes or
063: * additions were made.
064: * By default, the {@link #store} method in this class does not
065: * behave the same as {@link Properties#store}. You can cause an
066: * instance to approximate the behavior of {@link Properties#store}
067: * by invoking {@link #setDefaultEntryDelimiter} with <code>=</code>,
068: * {@link #setAddWhitespaceAfterDelimiter} with <code>false</code>, and
069: * {@link #setAllowDuplicates} with <code>true</code>. However, this
070: * will only influence how the instance will write new values, not how
071: * it will write existing key-value pairs that are modified.
072: * In conjunction with a conservative output writer, it is
073: * possible to only write to disk changes / additions.
074: * This implementation does not permit escaped ' ', '=', ':'
075: * characters in key names.
076: *
077: * @since 0.3.3
078: */
079: public class FormatPreservingProperties extends Properties {
080:
081: private static Localizer _loc = Localizer
082: .forPackage(FormatPreservingProperties.class);
083:
084: private char defaultEntryDelimiter = ':';
085: private boolean addWhitespaceAfterDelimiter = true;
086: private boolean allowDuplicates = false;
087: private boolean insertTimestamp = false;
088:
089: private PropertySource source;
090: private LinkedHashSet newKeys = new LinkedHashSet();
091: private HashSet modifiedKeys = new HashSet();
092:
093: // marker that indicates that we're not deserializing
094: private transient boolean isNotDeserializing = true;
095: private transient boolean isLoading = false;
096:
097: public FormatPreservingProperties() {
098: this (null);
099: }
100:
101: public FormatPreservingProperties(Properties defaults) {
102: super (defaults);
103: }
104:
105: /**
106: * The character to use as a delimiter between property keys and values.
107: *
108: * @param defaultEntryDelimiter either ':' or '='
109: */
110: public void setDefaultEntryDelimiter(char defaultEntryDelimiter) {
111: this .defaultEntryDelimiter = defaultEntryDelimiter;
112: }
113:
114: /**
115: * See {@link #setDefaultEntryDelimiter}
116: */
117: public char getDefaultEntryDelimiter() {
118: return this .defaultEntryDelimiter;
119: }
120:
121: /**
122: * If set to <code>true</code>, this properties object will add a
123: * space after the delimiter character(if the delimiter is not
124: * the space character). Else, this will not add a space.
125: * Default value: <code>true</code>. Note that {@link
126: * Properties#store} never writes whitespace.
127: */
128: public void setAddWhitespaceAfterDelimiter(boolean add) {
129: this .addWhitespaceAfterDelimiter = add;
130: }
131:
132: /**
133: * If set to <code>true</code>, this properties object will add a
134: * space after the delimiter character(if the delimiter is not
135: * the space character). Else, this will not add a space.
136: * Default value: <code>true</code>. Note that {@link
137: * Properties#store} never writes whitespace.
138: */
139: public boolean getAddWhitespaceAfterDelimiter() {
140: return this .addWhitespaceAfterDelimiter;
141: }
142:
143: /**
144: * If set to <code>true</code>, this properties object will add a
145: * timestamp to the beginning of the file, just after the header
146: * (if any) is printed. Else, this will not add a timestamp.
147: * Default value: <code>false</code>. Note that {@link
148: * Properties#store} always writes a timestamp.
149: */
150: public void setInsertTimestamp(boolean insertTimestamp) {
151: this .insertTimestamp = insertTimestamp;
152: }
153:
154: /**
155: * If set to <code>true</code>, this properties object will add a
156: * timestamp to the beginning of the file, just after the header
157: * (if any) is printed. Else, this will not add a timestamp.
158: * Default value: <code>false</code>. Note that {@link
159: * Properties#store} always writes a timestamp.
160: */
161: public boolean getInsertTimestamp() {
162: return this .insertTimestamp;
163: }
164:
165: /**
166: * If set to <code>true</code>, duplicate properties are allowed, and
167: * the last property setting in the input will overwrite any previous
168: * settings. If set to <code>false</code>, duplicate property definitions
169: * in the input will cause an exception to be thrown during {@link #load}.
170: * Default value: <code>false</code>. Note that {@link
171: * Properties#store} always allows duplicates.
172: */
173: public void setAllowDuplicates(boolean allowDuplicates) {
174: this .allowDuplicates = allowDuplicates;
175: }
176:
177: /**
178: * If set to <code>true</code>, duplicate properties are allowed, and
179: * the last property setting in the input will overwrite any previous
180: * settings. If set to <code>false</code>, duplicate property definitions
181: * in the input will cause an exception to be thrown during {@link #load}.
182: * Default value: <code>false</code>. Note that {@link
183: * Properties#store} always allows duplicates.
184: */
185: public boolean getAllowDuplicates() {
186: return this .allowDuplicates;
187: }
188:
189: public String getProperty(String key) {
190: return super .getProperty(key);
191: }
192:
193: public String getProperty(String key, String defaultValue) {
194: return super .getProperty(key, defaultValue);
195: }
196:
197: public Object setProperty(String key, String value) {
198: return put(key, value);
199: }
200:
201: /**
202: * Circumvents the superclass {@link #putAll} implementation,
203: * putting all the key-value pairs via {@link #put}.
204: */
205: public void putAll(Map m) {
206: Map.Entry e;
207: for (Iterator iter = m.entrySet().iterator(); iter.hasNext();) {
208: e = (Map.Entry) iter.next();
209: put(e.getKey(), e.getValue());
210: }
211: }
212:
213: /**
214: * Removes the key from the bookkeeping collectiotns as well.
215: */
216: public Object remove(Object key) {
217: newKeys.remove(key);
218: return super .remove(key);
219: }
220:
221: public void clear() {
222: super .clear();
223:
224: if (source != null)
225: source.clear();
226:
227: newKeys.clear();
228: modifiedKeys.clear();
229: }
230:
231: public Object clone() {
232: FormatPreservingProperties c = (FormatPreservingProperties) super
233: .clone();
234:
235: if (source != null)
236: c.source = (PropertySource) source.clone();
237:
238: if (modifiedKeys != null)
239: c.modifiedKeys = (HashSet) modifiedKeys.clone();
240:
241: if (newKeys != null) {
242: c.newKeys = new LinkedHashSet();
243: c.newKeys.addAll(newKeys);
244: }
245:
246: return c;
247: }
248:
249: private void writeObject(ObjectOutputStream out) throws IOException {
250: out.defaultWriteObject();
251: }
252:
253: private void readObject(ObjectInputStream in) throws IOException,
254: ClassNotFoundException {
255: in.defaultReadObject();
256:
257: isNotDeserializing = true;
258: }
259:
260: public Object put(Object key, Object val) {
261: Object o = super .put(key, val);
262:
263: // if we're no longer loading from properties and this put
264: // represents an actual change in value, mark the modification
265: // or addition in the bookkeeping collections.
266: if (!isLoading && isNotDeserializing && !val.equals(o)) {
267: if (o != null)
268: modifiedKeys.add(key);
269: else if (!newKeys.contains(key))
270: newKeys.add(key);
271: }
272: return o;
273: }
274:
275: /**
276: * Loads the properties in <code>in</code>, according to the rules
277: * described in {@link Properties#load}. If {@link #getAllowDuplicates}
278: * returns <code>true</code>, this will throw a {@link
279: * DuplicateKeyException} if duplicate property declarations are
280: * encountered.
281: *
282: * @see Properties#load
283: */
284: public void load(InputStream in) throws IOException {
285: isLoading = true;
286: try {
287: loadProperties(in);
288: } finally {
289: isLoading = false;
290: }
291: }
292:
293: private void loadProperties(InputStream in) throws IOException {
294: source = new PropertySource();
295:
296: PropertyLineReader reader = new PropertyLineReader(in, source);
297:
298: Set loadedKeys = new HashSet();
299:
300: for (PropertyLine l; (l = reader.readPropertyLine()) != null
301: && source.add(l);) {
302: String line = l.line.toString();
303:
304: char c = 0;
305: int pos = 0;
306:
307: while (pos < line.length() && isSpace(c = line.charAt(pos)))
308: pos++;
309:
310: if ((line.length() - pos) == 0 || line.charAt(pos) == '#'
311: || line.charAt(pos) == '!')
312: continue;
313:
314: StringBuffer key = new StringBuffer();
315: while (pos < line.length()
316: && !isSpace(c = line.charAt(pos++)) && c != '='
317: && c != ':') {
318: if (c == '\\') {
319: if (pos == line.length()) {
320: l.append(line = reader.readLine());
321: pos = 0;
322: while (pos < line.length()
323: && isSpace(c = line.charAt(pos)))
324: pos++;
325: } else {
326: pos = readEscape(line, pos, key);
327: }
328: } else {
329: key.append(c);
330: }
331: }
332:
333: boolean isDelim = (c == ':' || c == '=');
334:
335: for (; pos < line.length() && isSpace(c = line.charAt(pos)); pos++)
336: ;
337:
338: if (!isDelim && (c == ':' || c == '=')) {
339: pos++;
340: while (pos < line.length()
341: && isSpace(c = line.charAt(pos)))
342: pos++;
343: }
344:
345: StringBuffer element = new StringBuffer(line.length() - pos);
346:
347: while (pos < line.length()) {
348: c = line.charAt(pos++);
349: if (c == '\\') {
350: if (pos == line.length()) {
351: l.append(line = reader.readLine());
352:
353: if (line == null)
354: break;
355:
356: pos = 0;
357: while (pos < line.length()
358: && isSpace(c = line.charAt(pos)))
359: pos++;
360: element.ensureCapacity(line.length() - pos
361: + element.length());
362: } else {
363: pos = readEscape(line, pos, element);
364: }
365: } else
366: element.append(c);
367: }
368:
369: if (!loadedKeys.add(key.toString()) && !allowDuplicates)
370: throw new DuplicateKeyException(key.toString(),
371: getProperty(key.toString()), element.toString());
372:
373: l.setPropertyKey(key.toString());
374: l.setPropertyValue(element.toString());
375: put(key.toString(), element.toString());
376: }
377: }
378:
379: /**
380: * Read the next escaped character: handle newlines, tabs, returns, and
381: * form feeds with the appropriate escaped character, then try to
382: * decode unicode characters. Finally, just add the character explicitly.
383: *
384: * @param source the source of the characters
385: * @param pos the position at which to start reading
386: * @param value the value we are appending to
387: * @return the position after the reading is done
388: */
389: private static int readEscape(String source, int pos,
390: StringBuffer value) {
391: char c = source.charAt(pos++);
392: switch (c) {
393: case 'n':
394: value.append('\n');
395: break;
396: case 't':
397: value.append('\t');
398: break;
399: case 'f':
400: value.append('\f');
401: break;
402: case 'r':
403: value.append('\r');
404: break;
405: case 'u':
406: if (pos + 4 <= source.length()) {
407: char uni = (char) Integer.parseInt(source.substring(
408: pos, pos + 4), 16);
409: value.append(uni);
410: pos += 4;
411: }
412: break;
413: default:
414: value.append(c);
415: break;
416: }
417:
418: return pos;
419: }
420:
421: private static boolean isSpace(char ch) {
422: return Character.isWhitespace(ch);
423: }
424:
425: public void save(OutputStream out, String header) {
426: try {
427: store(out, header);
428: } catch (IOException ex) {
429: }
430: }
431:
432: public void store(OutputStream out, String header)
433: throws IOException {
434: boolean endWithNewline = source != null && source.endsInNewline;
435:
436: // Must be ISO-8859-1 ecoding according to Properties.load javadoc
437: PrintWriter writer = new PrintWriter(new OutputStreamWriter(
438: out, "ISO-8859-1"), false);
439:
440: if (header != null)
441: writer.println("#" + header);
442:
443: if (insertTimestamp)
444: writer.println("#" + Calendar.getInstance().getTime());
445:
446: List lines = new LinkedList();
447: // first write all the existing props as they were initially read
448: if (source != null)
449: lines.addAll(source);
450:
451: // next write out new keys, then the rest of the keys
452: LinkedHashSet keys = new LinkedHashSet();
453: keys.addAll(newKeys);
454: keys.addAll(keySet());
455:
456: lines.addAll(keys);
457:
458: keys.remove(null);
459:
460: boolean needsNewline = false;
461:
462: for (Iterator i = lines.iterator(); i.hasNext();) {
463: Object next = i.next();
464:
465: if (next instanceof PropertyLine) {
466: if (((PropertyLine) next).write(writer, keys,
467: needsNewline))
468: needsNewline = i.hasNext();
469: } else if (next instanceof String) {
470: String key = (String) next;
471: if (keys.remove(key)) {
472: if (writeProperty(key, writer, needsNewline)) {
473: needsNewline = i.hasNext() && keys.size() > 0;
474:
475: // any new or modified properties will cause
476: // the file to end with a newline
477: endWithNewline = true;
478: }
479: }
480: }
481: }
482:
483: // make sure we end in a newline if the source ended in it
484: if (endWithNewline)
485: writer.println();
486:
487: writer.flush();
488: }
489:
490: private boolean writeProperty(String key, PrintWriter writer,
491: boolean needsNewline) {
492: StringBuffer s = new StringBuffer();
493:
494: if (key == null)
495: return false;
496:
497: String val = getProperty(key);
498: if (val == null)
499: return false;
500:
501: formatValue(key, s, true);
502: s.append(defaultEntryDelimiter);
503: if (addWhitespaceAfterDelimiter)
504: s.append(' ');
505: formatValue(val, s, false);
506:
507: if (needsNewline)
508: writer.println();
509:
510: writer.print(s);
511:
512: return true;
513: }
514:
515: /**
516: * Format the given string as an encoded value for storage. This will
517: * perform any necessary escaping of special characters.
518: *
519: * @param str the value to encode
520: * @param buf the buffer to which to append the encoded value
521: * @param isKey if true, then the string is a Property key, otherwise
522: * it is a value
523: */
524: private static void formatValue(String str, StringBuffer buf,
525: boolean isKey) {
526: if (isKey) {
527: buf.setLength(0);
528: buf.ensureCapacity(str.length());
529: } else {
530: buf.ensureCapacity(buf.length() + str.length());
531: }
532:
533: boolean escapeSpace = true;
534: int size = str.length();
535:
536: for (int i = 0; i < size; i++) {
537: char c = str.charAt(i);
538:
539: if (c == '\n')
540: buf.append("\\n");
541: else if (c == '\r')
542: buf.append("\\r");
543: else if (c == '\t')
544: buf.append("\\t");
545: else if (c == '\f')
546: buf.append("\\f");
547: else if (c == ' ')
548: buf.append(escapeSpace ? "\\ " : " ");
549: else if (c == '\\' || c == '!' || c == '#' || c == '='
550: || c == ':')
551: buf.append('\\').append(c);
552: else if (c < ' ' || c > '~')
553: buf.append(
554: "\\u0000".substring(0, 6 - Integer.toHexString(
555: c).length())).append(
556: Integer.toHexString(c));
557: else
558: buf.append(c);
559:
560: if (c != ' ')
561: escapeSpace = isKey;
562: }
563: }
564:
565: public static class DuplicateKeyException extends RuntimeException {
566:
567: public DuplicateKeyException(String key, Object firstVal,
568: String secondVal) {
569: super (_loc.get("dup-key", key, firstVal, secondVal)
570: .getMessage());
571: }
572: }
573:
574: /**
575: * Contains the original line of the properties file: can be a
576: * proper key/value pair, or a comment, or just whitespace.
577: */
578: private class PropertyLine implements Serializable {
579:
580: private final StringBuffer line = new StringBuffer();
581: private String propertyKey;
582: private String propertyValue;
583:
584: public PropertyLine(String line) {
585: this .line.append(line);
586: }
587:
588: public void append(String newline) {
589: line.append(J2DoPrivHelper.getLineSeparator());
590: line.append(newline);
591: }
592:
593: public void setPropertyKey(String propertyKey) {
594: this .propertyKey = propertyKey;
595: }
596:
597: public String getPropertyKey() {
598: return this .propertyKey;
599: }
600:
601: public void setPropertyValue(String propertyValue) {
602: this .propertyValue = propertyValue;
603: }
604:
605: public String getPropertyValue() {
606: return this .propertyValue;
607: }
608:
609: /**
610: * Write the given line. It will only be written if the line is a
611: * comment, or if it is a property and its value is unchanged
612: * from the original.
613: *
614: * @param pw the PrintWriter to which the write
615: * @return whether or not this was a known key
616: */
617: public boolean write(PrintWriter pw, Collection keys,
618: boolean needsNewline) {
619: // no property? It may be a comment or just whitespace
620: if (propertyKey == null) {
621: if (needsNewline)
622: pw.println();
623: pw.print(line.toString());
624: return true;
625: }
626:
627: // check to see if we are the same value we initially read:
628: // if so, then just write it back exactly as it was read
629: if (propertyValue != null
630: && containsKey(propertyKey)
631: && (propertyValue.equals(getProperty(propertyKey)) || (!newKeys
632: .contains(propertyKey) && !modifiedKeys
633: .contains(propertyKey)))) {
634: if (needsNewline)
635: pw.println();
636: pw.print(line.toString());
637:
638: keys.remove(propertyKey);
639:
640: return true;
641: }
642:
643: // if we have modified or added the specified key, then write
644: // it back to the same location in the file from which it
645: // was originally read, so that it will be in the proximity
646: // to the comment
647: if (containsKey(propertyKey)
648: && (modifiedKeys.contains(propertyKey) || newKeys
649: .contains(propertyKey))) {
650: while (keys.remove(propertyKey))
651: ;
652: return writeProperty(propertyKey, pw, needsNewline);
653: }
654:
655: // this is a new or changed property: don't do anything
656: return false;
657: }
658: }
659:
660: private class PropertyLineReader extends BufferedReader {
661:
662: public PropertyLineReader(InputStream in, PropertySource source)
663: throws IOException {
664: // Must be ISO-8859-1 ecoding according to Properties.load javadoc
665: super (new InputStreamReader(
666: new LineEndingStream(in, source), "ISO-8859-1"));
667: }
668:
669: public PropertyLine readPropertyLine() throws IOException {
670: String l = readLine();
671: if (l == null)
672: return null;
673:
674: PropertyLine pl = new PropertyLine(l);
675: return pl;
676: }
677: }
678:
679: /**
680: * Simple FilterInputStream that merely remembers if the last
681: * character that it read was a newline or not.
682: */
683: private static class LineEndingStream extends FilterInputStream {
684:
685: private final PropertySource source;
686:
687: LineEndingStream(InputStream in, PropertySource source) {
688: super (in);
689:
690: this .source = source;
691: }
692:
693: public int read() throws IOException {
694: int c = super .read();
695: source.endsInNewline = (c == '\n' || c == '\r');
696: return c;
697: }
698:
699: public int read(byte[] b, int off, int len) throws IOException {
700: int n = super .read(b, off, len);
701: if (n > 0)
702: source.endsInNewline = (b[n + off - 1] == '\n' || b[n
703: + off - 1] == '\r');
704: return n;
705: }
706: }
707:
708: static class PropertySource extends LinkedList implements
709: Cloneable, Serializable {
710:
711: private boolean endsInNewline = false;
712: }
713: }
|