View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.props;
20  
21  import java.io.FilterWriter;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.LineNumberReader;
26  import java.io.OutputStream;
27  import java.io.OutputStreamWriter;
28  import java.io.Reader;
29  import java.io.Writer;
30  import java.net.URL;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.util.AbstractMap;
35  import java.util.AbstractSet;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.Enumeration;
39  import java.util.Iterator;
40  import java.util.LinkedHashMap;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  import java.util.Set;
45  import java.util.function.Function;
46  
47  import org.apache.maven.internal.impl.model.DefaultInterpolator;
48  
49  /**
50   * Enhancement of the standard <code>Properties</code>
51   * managing the maintain of comments, etc.
52   */
53  public class MavenProperties extends AbstractMap<String, String> {
54  
55      /** Constant for the supported comment characters.*/
56      private static final String COMMENT_CHARS = "#!";
57  
58      /** The list of possible key/value separators */
59      private static final char[] SEPARATORS = new char[] {'=', ':'};
60  
61      /** The white space characters used as key/value separators. */
62      private static final char[] WHITE_SPACE = new char[] {' ', '\t', '\f'};
63  
64      /**
65       * Unless standard java props, use UTF-8
66       */
67      static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
68  
69      /** Constant for the platform specific line separator.*/
70      private static final String LINE_SEPARATOR = System.lineSeparator();
71  
72      /** Constant for the radix of hex numbers.*/
73      private static final int HEX_RADIX = 16;
74  
75      /** Constant for the length of a unicode literal.*/
76      private static final int UNICODE_LEN = 4;
77  
78      private final Map<String, String> storage = new LinkedHashMap<>();
79      private final Map<String, Layout> layout = new LinkedHashMap<>();
80      private List<String> header;
81      private List<String> footer;
82      private Path location;
83      private Function<String, String> callback;
84      boolean substitute = true;
85      boolean typed;
86  
87      public MavenProperties() {}
88  
89      public MavenProperties(Path location) throws IOException {
90          this(location, null);
91      }
92  
93      public MavenProperties(Path location, Function<String, String> callback) throws IOException {
94          this.location = location;
95          this.callback = callback;
96          if (Files.exists(location)) {
97              load(location);
98          }
99      }
100 
101     public MavenProperties(boolean substitute) {
102         this.substitute = substitute;
103     }
104 
105     public MavenProperties(Path location, boolean substitute) {
106         this.location = location;
107         this.substitute = substitute;
108     }
109 
110     public void load(Path location) throws IOException {
111         try (InputStream is = Files.newInputStream(location)) {
112             load(is);
113         }
114     }
115 
116     public void load(URL location) throws IOException {
117         try (InputStream is = location.openStream()) {
118             load(is);
119         }
120     }
121 
122     public void load(InputStream is) throws IOException {
123         load(new InputStreamReader(is, DEFAULT_ENCODING));
124     }
125 
126     public void load(Reader reader) throws IOException {
127         loadLayout(reader, false);
128     }
129 
130     public void save() throws IOException {
131         save(this.location);
132     }
133 
134     public void save(Path location) throws IOException {
135         try (OutputStream os = Files.newOutputStream(location)) {
136             save(os);
137         }
138     }
139 
140     public void save(OutputStream os) throws IOException {
141         save(new OutputStreamWriter(os, DEFAULT_ENCODING));
142     }
143 
144     public void save(Writer writer) throws IOException {
145         saveLayout(writer, typed);
146     }
147 
148     /**
149      * Store a properties into a output stream, preserving comments, special character, etc.
150      * This method is mainly to be compatible with the java.util.Properties class.
151      *
152      * @param os an output stream.
153      * @param comment this parameter is ignored as this Properties
154      * @throws IOException If storing fails
155      */
156     public void store(OutputStream os, String comment) throws IOException {
157         this.save(os);
158     }
159 
160     /**
161      * Searches for the property with the specified key in this property list.
162      *
163      * @param key the property key.
164      * @return the value in this property list with the specified key value.
165      */
166     public String getProperty(String key) {
167         return this.get(key);
168     }
169 
170     /**
171      * Searches for the property with the specified key in this property list. If the key is not found in this property
172      * list, the default property list, and its defaults, recursively, are then checked. The method returns the default
173      * value argument if the property is not found.
174      *
175      * @param key the property key.
176      * @param defaultValue a default value.
177      * @return The property value of the default value
178      */
179     public String getProperty(String key, String defaultValue) {
180         if (this.get(key) != null) {
181             return this.get(key);
182         }
183         return defaultValue;
184     }
185 
186     @Override
187     public Set<Entry<String, String>> entrySet() {
188         return new AbstractSet<>() {
189             @Override
190             public Iterator<Entry<String, String>> iterator() {
191                 return new Iterator<>() {
192                     final Iterator<Entry<String, String>> keyIterator =
193                             storage.entrySet().iterator();
194 
195                     public boolean hasNext() {
196                         return keyIterator.hasNext();
197                     }
198 
199                     public Entry<String, String> next() {
200                         final Entry<String, String> entry = keyIterator.next();
201                         return new Entry<String, String>() {
202                             public String getKey() {
203                                 return entry.getKey();
204                             }
205 
206                             public String getValue() {
207                                 return entry.getValue();
208                             }
209 
210                             public String setValue(String value) {
211                                 String old = entry.setValue(value);
212                                 if (old == null || !old.equals(value)) {
213                                     Layout l = layout.get(entry.getKey());
214                                     if (l != null) {
215                                         l.clearValue();
216                                     }
217                                 }
218                                 return old;
219                             }
220                         };
221                     }
222 
223                     public void remove() {
224                         keyIterator.remove();
225                     }
226                 };
227             }
228 
229             @Override
230             public int size() {
231                 return storage.size();
232             }
233         };
234     }
235 
236     /**
237      * Returns an enumeration of all the keys in this property list, including distinct keys in the default property
238      * list if a key of the same name has not already been found from the main properties list.
239      *
240      * @return an enumeration of all the keys in this property list, including the keys in the default property list.
241      */
242     public Enumeration<?> propertyNames() {
243         return Collections.enumeration(storage.keySet());
244     }
245 
246     /**
247      * Calls the map method put. Provided for parallelism with the getProperty method.
248      * Enforces use of strings for property keys and values. The value returned is the result of the map call to put.
249      *
250      * @param key the key to be placed into this property list.
251      * @param value the value corresponding to the key.
252      * @return the previous value of the specified key in this property list, or null if it did not have one.
253      */
254     public Object setProperty(String key, String value) {
255         return this.put(key, value);
256     }
257 
258     @Override
259     public String put(String key, String value) {
260         String old = storage.put(key, value);
261         if (old == null || !old.equals(value)) {
262             Layout l = layout.get(key);
263             if (l != null) {
264                 l.clearValue();
265             }
266         }
267         return old;
268     }
269 
270     void putAllSubstituted(Map<? extends String, ? extends String> m) {
271         storage.putAll(m);
272     }
273 
274     public String put(String key, List<String> commentLines, List<String> valueLines) {
275         commentLines = new ArrayList<>(commentLines);
276         valueLines = new ArrayList<>(valueLines);
277         String escapedKey = escapeKey(key);
278         StringBuilder sb = new StringBuilder();
279         // int lastLine = valueLines.size() - 1;
280         if (valueLines.isEmpty()) {
281             valueLines.add(escapedKey + "=");
282             sb.append(escapedKey).append("=");
283         } else {
284             String val0 = valueLines.get(0);
285             String rv0 = typed ? val0 : escapeJava(val0);
286             if (!val0.trim().startsWith(escapedKey)) {
287                 valueLines.set(0, escapedKey + " = " + rv0 /*+ (0 < lastLine? "\\": "")*/);
288                 sb.append(escapedKey).append(" = ").append(rv0);
289             } else {
290                 valueLines.set(0, rv0 /*+ (0 < lastLine? "\\": "")*/);
291                 sb.append(rv0);
292             }
293         }
294         for (int i = 1; i < valueLines.size(); i++) {
295             String val = valueLines.get(i);
296             valueLines.set(i, typed ? val : escapeJava(val) /*+ (i < lastLine? "\\": "")*/);
297             while (!val.isEmpty() && Character.isWhitespace(val.charAt(0))) {
298                 val = val.substring(1);
299             }
300             sb.append(val);
301         }
302         String[] property = PropertiesReader.parseProperty(sb.toString());
303         this.layout.put(key, new Layout(commentLines, valueLines));
304         return storage.put(key, property[1]);
305     }
306 
307     public String put(String key, List<String> commentLines, String value) {
308         commentLines = new ArrayList<>(commentLines);
309         this.layout.put(key, new Layout(commentLines, null));
310         return storage.put(key, value);
311     }
312 
313     public String put(String key, String comment, String value) {
314         return put(key, Collections.singletonList(comment), value);
315     }
316 
317     public boolean update(Map<String, String> props) {
318         MavenProperties properties;
319         if (props instanceof MavenProperties) {
320             properties = (MavenProperties) props;
321         } else {
322             properties = new MavenProperties();
323             properties.putAll(props);
324         }
325         return update(properties);
326     }
327 
328     public boolean update(MavenProperties properties) {
329         boolean modified = false;
330         // Remove "removed" properties from the cfg file
331         for (String key : new ArrayList<String>(this.keySet())) {
332             if (!properties.containsKey(key)) {
333                 this.remove(key);
334                 modified = true;
335             }
336         }
337         // Update existing keys
338         for (String key : properties.keySet()) {
339             String v = this.get(key);
340             List<String> comments = properties.getComments(key);
341             List<String> value = properties.getRaw(key);
342             if (v == null) {
343                 this.put(key, comments, value);
344                 modified = true;
345             } else if (!v.equals(properties.get(key))) {
346                 if (comments.isEmpty()) {
347                     comments = this.getComments(key);
348                 }
349                 this.put(key, comments, value);
350                 modified = true;
351             }
352         }
353         return modified;
354     }
355 
356     public List<String> getRaw(String key) {
357         if (layout.containsKey(key)) {
358             if (layout.get(key).getValueLines() != null) {
359                 return new ArrayList<String>(layout.get(key).getValueLines());
360             }
361         }
362         List<String> result = new ArrayList<String>();
363         if (storage.containsKey(key)) {
364             result.add(storage.get(key));
365         }
366         return result;
367     }
368 
369     public List<String> getComments(String key) {
370         if (layout.containsKey(key)) {
371             if (layout.get(key).getCommentLines() != null) {
372                 return new ArrayList<String>(layout.get(key).getCommentLines());
373             }
374         }
375         return new ArrayList<String>();
376     }
377 
378     @Override
379     public String remove(Object key) {
380         Layout l = layout.get(key);
381         if (l != null) {
382             l.clearValue();
383         }
384         return storage.remove(key);
385     }
386 
387     @Override
388     public void clear() {
389         for (Layout l : layout.values()) {
390             l.clearValue();
391         }
392         storage.clear();
393     }
394 
395     /**
396      * Return the comment header.
397      *
398      * @return the comment header
399      */
400     public List<String> getHeader() {
401         return header;
402     }
403 
404     /**
405      * Set the comment header.
406      *
407      * @param header the header to use
408      */
409     public void setHeader(List<String> header) {
410         this.header = header;
411     }
412 
413     /**
414      * Return the comment footer.
415      *
416      * @return the comment footer
417      */
418     public List<String> getFooter() {
419         return footer;
420     }
421 
422     /**
423      * Set the comment footer.
424      *
425      * @param footer the footer to use
426      */
427     public void setFooter(List<String> footer) {
428         this.footer = footer;
429     }
430 
431     /**
432      * Reads a properties file and stores its internal structure. The found
433      * properties will be added to the associated configuration object.
434      *
435      * @param in the reader to the properties file
436      * @throws IOException if an error occurs
437      */
438     protected void loadLayout(Reader in, boolean maybeTyped) throws IOException {
439         PropertiesReader reader = new PropertiesReader(in, maybeTyped);
440         boolean hasProperty = false;
441         while (reader.nextProperty()) {
442             hasProperty = true;
443             storage.put(reader.getPropertyName(), reader.getPropertyValue());
444             int idx = checkHeaderComment(reader.getCommentLines());
445             layout.put(
446                     reader.getPropertyName(),
447                     new Layout(
448                             idx < reader.getCommentLines().size()
449                                     ? new ArrayList<>(reader.getCommentLines()
450                                             .subList(
451                                                     idx,
452                                                     reader.getCommentLines().size()))
453                                     : null,
454                             new ArrayList<>(reader.getValueLines())));
455         }
456         typed = maybeTyped && reader.typed != null && reader.typed;
457         if (!typed) {
458             for (Entry<String, String> e : storage.entrySet()) {
459                 e.setValue(unescapeJava(e.getValue()));
460             }
461         }
462         if (hasProperty) {
463             footer = new ArrayList<>(reader.getCommentLines());
464         } else {
465             header = new ArrayList<>(reader.getCommentLines());
466         }
467         if (substitute) {
468             substitute();
469         }
470     }
471 
472     public void substitute() {
473         substitute(callback);
474     }
475 
476     public void substitute(Function<String, String> callback) {
477         new DefaultInterpolator().interpolate(storage, callback);
478     }
479 
480     /**
481      * Writes the properties file to the given writer, preserving as much of its
482      * structure as possible.
483      *
484      * @param out the writer
485      * @throws IOException if an error occurs
486      */
487     protected void saveLayout(Writer out, boolean typed) throws IOException {
488         PropertiesWriter writer = new PropertiesWriter(out, typed);
489         if (header != null) {
490             for (String s : header) {
491                 writer.writeln(s);
492             }
493         }
494 
495         for (String key : storage.keySet()) {
496             Layout l = layout.get(key);
497             if (l != null && l.getCommentLines() != null) {
498                 for (String s : l.getCommentLines()) {
499                     writer.writeln(s);
500                 }
501             }
502             if (l != null && l.getValueLines() != null) {
503                 for (int i = 0; i < l.getValueLines().size(); i++) {
504                     String s = l.getValueLines().get(i);
505                     if (i < l.getValueLines().size() - 1) {
506                         writer.writeln(s + "\\");
507                     } else {
508                         writer.writeln(s);
509                     }
510                 }
511             } else {
512                 writer.writeProperty(key, storage.get(key));
513             }
514         }
515         if (footer != null) {
516             for (String s : footer) {
517                 writer.writeln(s);
518             }
519         }
520         writer.flush();
521     }
522 
523     /**
524      * Checks if parts of the passed in comment can be used as header comment.
525      * This method checks whether a header comment can be defined (i.e. whether
526      * this is the first comment in the loaded file). If this is the case, it is
527      * searched for the lates blank line. This line will mark the end of the
528      * header comment. The return value is the index of the first line in the
529      * passed in list, which does not belong to the header comment.
530      *
531      * @param commentLines the comment lines
532      * @return the index of the next line after the header comment
533      */
534     private int checkHeaderComment(List<String> commentLines) {
535         if (getHeader() == null && layout.isEmpty()) {
536             // This is the first comment. Search for blank lines.
537             int index = commentLines.size() - 1;
538             while (index >= 0 && !commentLines.get(index).isEmpty()) {
539                 index--;
540             }
541             setHeader(new ArrayList<String>(commentLines.subList(0, index + 1)));
542             return index + 1;
543         } else {
544             return 0;
545         }
546     }
547 
548     /**
549      * Tests whether a line is a comment, i.e. whether it starts with a comment
550      * character.
551      *
552      * @param line the line
553      * @return a flag if this is a comment line
554      */
555     static boolean isCommentLine(String line) {
556         String s = line.trim();
557         // blank lines are also treated as comment lines
558         return s.isEmpty() || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
559     }
560 
561     /**
562      * <p>Unescapes any Java literals found in the <code>String</code> to a
563      * <code>Writer</code>.</p> This is a slightly modified version of the
564      * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
565      * drop escaped separators (i.e '\,').
566      *
567      * @param str  the <code>String</code> to unescape, may be null
568      * @return the processed string
569      * @throws IllegalArgumentException if the Writer is <code>null</code>
570      */
571     protected static String unescapeJava(String str) {
572         if (str == null) {
573             return null;
574         }
575         int sz = str.length();
576         StringBuilder out = new StringBuilder(sz);
577         StringBuilder unicode = new StringBuilder(UNICODE_LEN);
578         boolean hadSlash = false;
579         boolean inUnicode = false;
580         for (int i = 0; i < sz; i++) {
581             char ch = str.charAt(i);
582             if (inUnicode) {
583                 // if in unicode, then we're reading unicode
584                 // values in somehow
585                 unicode.append(ch);
586                 if (unicode.length() == UNICODE_LEN) {
587                     // unicode now contains the four hex digits
588                     // which represents our unicode character
589                     try {
590                         int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
591                         out.append((char) value);
592                         unicode.setLength(0);
593                         inUnicode = false;
594                         hadSlash = false;
595                     } catch (NumberFormatException nfe) {
596                         throw new IllegalArgumentException("Unable to parse unicode value: " + unicode, nfe);
597                     }
598                 }
599                 continue;
600             }
601 
602             if (hadSlash) {
603                 // handle an escaped value
604                 hadSlash = false;
605                 switch (ch) {
606                     case '\\':
607                         out.append('\\');
608                         break;
609                     case '\'':
610                         out.append('\'');
611                         break;
612                     case '\"':
613                         out.append('"');
614                         break;
615                     case 'r':
616                         out.append('\r');
617                         break;
618                     case 'f':
619                         out.append('\f');
620                         break;
621                     case 't':
622                         out.append('\t');
623                         break;
624                     case 'n':
625                         out.append('\n');
626                         break;
627                     case 'b':
628                         out.append('\b');
629                         break;
630                     case 'u':
631                         // uh-oh, we're in unicode country....
632                         inUnicode = true;
633                         break;
634                     default:
635                         out.append(ch);
636                         break;
637                 }
638                 continue;
639             } else if (ch == '\\') {
640                 hadSlash = true;
641                 continue;
642             }
643             out.append(ch);
644         }
645 
646         if (hadSlash) {
647             // then we're in the weird case of a \ at the end of the
648             // string, let's output it anyway.
649             out.append('\\');
650         }
651 
652         return out.toString();
653     }
654 
655     /**
656      * <p>Escapes the characters in a <code>String</code> using Java String rules.</p>
657      *
658      * <p>Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) </p>
659      *
660      * <p>So a tab becomes the characters <code>'\\'</code> and
661      * <code>'t'</code>.</p>
662      *
663      * <p>The only difference between Java strings and JavaScript strings
664      * is that in JavaScript, a single quote must be escaped.</p>
665      *
666      * <p>Example:</p>
667      * <pre>
668      * input string: He didn't say, "Stop!"
669      * output string: He didn't say, \"Stop!\"
670      * </pre>
671      *
672      *
673      * @param str  String to escape values in, may be null
674      * @return String with escaped values, <code>null</code> if null string input
675      */
676     @SuppressWarnings("checkstyle:MagicNumber")
677     protected static String escapeJava(String str) {
678         if (str == null) {
679             return null;
680         }
681         int sz = str.length();
682         StringBuilder out = new StringBuilder(sz * 2);
683         for (int i = 0; i < sz; i++) {
684             char ch = str.charAt(i);
685             // handle unicode
686             if (ch > 0xfff) {
687                 out.append("\\u").append(hex(ch));
688             } else if (ch > 0xff) {
689                 out.append("\\u0").append(hex(ch));
690             } else if (ch > 0x7f) {
691                 out.append("\\u00").append(hex(ch));
692             } else if (ch < 32) {
693                 switch (ch) {
694                     case '\b':
695                         out.append('\\');
696                         out.append('b');
697                         break;
698                     case '\n':
699                         out.append('\\');
700                         out.append('n');
701                         break;
702                     case '\t':
703                         out.append('\\');
704                         out.append('t');
705                         break;
706                     case '\f':
707                         out.append('\\');
708                         out.append('f');
709                         break;
710                     case '\r':
711                         out.append('\\');
712                         out.append('r');
713                         break;
714                     default:
715                         if (ch > 0xf) {
716                             out.append("\\u00").append(hex(ch));
717                         } else {
718                             out.append("\\u000").append(hex(ch));
719                         }
720                         break;
721                 }
722             } else {
723                 switch (ch) {
724                     case '"':
725                         out.append('\\');
726                         out.append('"');
727                         break;
728                     case '\\':
729                         out.append('\\');
730                         out.append('\\');
731                         break;
732                     default:
733                         out.append(ch);
734                         break;
735                 }
736             }
737         }
738         return out.toString();
739     }
740 
741     /**
742      * <p>Returns an upper case hexadecimal <code>String</code> for the given
743      * character.</p>
744      *
745      * @param ch The character to convert.
746      * @return An upper case hexadecimal <code>String</code>
747      */
748     protected static String hex(char ch) {
749         return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
750     }
751 
752     /**
753      * <p>Checks if the value is in the given array.</p>
754      *
755      * <p>The method returns <code>false</code> if a <code>null</code> array is passed in.</p>
756      *
757      * @param array  the array to search through
758      * @param valueToFind  the value to find
759      * @return <code>true</code> if the array contains the object
760      */
761     public static boolean contains(char[] array, char valueToFind) {
762         if (array == null) {
763             return false;
764         }
765         for (char c : array) {
766             if (valueToFind == c) {
767                 return true;
768             }
769         }
770         return false;
771     }
772 
773     /**
774      * Escape the separators in the key.
775      *
776      * @param key the key
777      * @return the escaped key
778      */
779     private static String escapeKey(String key) {
780         StringBuilder newkey = new StringBuilder();
781 
782         for (int i = 0; i < key.length(); i++) {
783             char c = key.charAt(i);
784 
785             if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) {
786                 // escape the separator
787                 newkey.append('\\');
788                 newkey.append(c);
789             } else {
790                 newkey.append(c);
791             }
792         }
793 
794         return newkey.toString();
795     }
796 
797     /**
798      * This class is used to read properties lines. These lines do
799      * not terminate with new-line chars but rather when there is no
800      * backslash sign a the end of the line.  This is used to
801      * concatenate multiple lines for readability.
802      */
803     public static class PropertiesReader extends LineNumberReader {
804         /** Stores the comment lines for the currently processed property.*/
805         private final List<String> commentLines;
806 
807         /** Stores the value lines for the currently processed property.*/
808         private final List<String> valueLines;
809 
810         /** Stores the name of the last read property.*/
811         private String propertyName;
812 
813         /** Stores the value of the last read property.*/
814         private String propertyValue;
815 
816         private boolean maybeTyped;
817 
818         /** Stores if the properties are typed or not */
819         Boolean typed;
820 
821         /**
822          * Creates a new instance of <code>PropertiesReader</code> and sets
823          * the underlaying reader and the list delimiter.
824          *
825          * @param reader the reader
826          */
827         public PropertiesReader(Reader reader, boolean maybeTyped) {
828             super(reader);
829             commentLines = new ArrayList<>();
830             valueLines = new ArrayList<>();
831             this.maybeTyped = maybeTyped;
832         }
833 
834         /**
835          * Reads a property line. Returns null if Stream is
836          * at EOF. Concatenates lines ending with "\".
837          * Skips lines beginning with "#" or "!" and empty lines.
838          * The return value is a property definition (<code>&lt;name&gt;</code>
839          * = <code>&lt;value&gt;</code>)
840          *
841          * @return A string containing a property value or null
842          *
843          * @throws IOException in case of an I/O error
844          */
845         public String readProperty() throws IOException {
846             commentLines.clear();
847             valueLines.clear();
848             StringBuilder buffer = new StringBuilder();
849 
850             while (true) {
851                 String line = readLine();
852                 if (line == null) {
853                     // EOF
854                     return null;
855                 }
856 
857                 if (isCommentLine(line)) {
858                     commentLines.add(line);
859                     continue;
860                 }
861 
862                 boolean combine = checkCombineLines(line);
863                 if (combine) {
864                     line = line.substring(0, line.length() - 1);
865                 }
866                 valueLines.add(line);
867                 while (line.length() > 0 && contains(WHITE_SPACE, line.charAt(0))) {
868                     line = line.substring(1, line.length());
869                 }
870                 buffer.append(line);
871                 if (!combine) {
872                     break;
873                 }
874             }
875             return buffer.toString();
876         }
877 
878         /**
879          * Parses the next property from the input stream and stores the found
880          * name and value in internal fields. These fields can be obtained using
881          * the provided getter methods. The return value indicates whether EOF
882          * was reached (<b>false</b>) or whether further properties are
883          * available (<b>true</b>).
884          *
885          * @return a flag if further properties are available
886          * @throws IOException if an error occurs
887          */
888         public boolean nextProperty() throws IOException {
889             String line = readProperty();
890 
891             if (line == null) {
892                 return false; // EOF
893             }
894 
895             // parse the line
896             String[] property = parseProperty(line);
897             boolean typed = false;
898             if (maybeTyped && property[1].length() >= 2) {
899                 typed = property[1].matches(
900                         "\\s*[TILFDXSCBilfdxscb]?(\\[[\\S\\s]*\\]|\\([\\S\\s]*\\)|\\{[\\S\\s]*\\}|\"[\\S\\s]*\")\\s*");
901             }
902             if (this.typed == null) {
903                 this.typed = typed;
904             } else {
905                 this.typed = this.typed & typed;
906             }
907             propertyName = unescapeJava(property[0]);
908             propertyValue = property[1];
909             return true;
910         }
911 
912         /**
913          * Returns the comment lines that have been read for the last property.
914          *
915          * @return the comment lines for the last property returned by
916          * <code>readProperty()</code>
917          */
918         public List<String> getCommentLines() {
919             return commentLines;
920         }
921 
922         /**
923          * Returns the value lines that have been read for the last property.
924          *
925          * @return the raw value lines for the last property returned by
926          * <code>readProperty()</code>
927          */
928         public List<String> getValueLines() {
929             return valueLines;
930         }
931 
932         /**
933          * Returns the name of the last read property. This method can be called
934          * after <code>{@link #nextProperty()}</code> was invoked and its
935          * return value was <b>true</b>.
936          *
937          * @return the name of the last read property
938          */
939         public String getPropertyName() {
940             return propertyName;
941         }
942 
943         /**
944          * Returns the value of the last read property. This method can be
945          * called after <code>{@link #nextProperty()}</code> was invoked and
946          * its return value was <b>true</b>.
947          *
948          * @return the value of the last read property
949          */
950         public String getPropertyValue() {
951             return propertyValue;
952         }
953 
954         /**
955          * Checks if the passed in line should be combined with the following.
956          * This is true, if the line ends with an odd number of backslashes.
957          *
958          * @param line the line
959          * @return a flag if the lines should be combined
960          */
961         private static boolean checkCombineLines(String line) {
962             int bsCount = 0;
963             for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) {
964                 bsCount++;
965             }
966 
967             return bsCount % 2 != 0;
968         }
969 
970         /**
971          * Parse a property line and return the key and the value in an array.
972          *
973          * @param line the line to parse
974          * @return an array with the property's key and value
975          */
976         private static String[] parseProperty(String line) {
977             // sorry for this spaghetti code, please replace it as soon as
978             // possible with a regexp when the Java 1.3 requirement is dropped
979 
980             String[] result = new String[2];
981             StringBuilder key = new StringBuilder();
982             StringBuilder value = new StringBuilder();
983 
984             // state of the automaton:
985             // 0: key parsing
986             // 1: antislash found while parsing the key
987             // 2: separator crossing
988             // 3: white spaces
989             // 4: value parsing
990             int state = 0;
991 
992             for (int pos = 0; pos < line.length(); pos++) {
993                 char c = line.charAt(pos);
994 
995                 switch (state) {
996                     case 0:
997                         if (c == '\\') {
998                             state = 1;
999                         } else if (contains(WHITE_SPACE, c)) {
1000                             // switch to the separator crossing state
1001                             state = 2;
1002                         } else if (contains(SEPARATORS, c)) {
1003                             // switch to the value parsing state
1004                             state = 3;
1005                         } else {
1006                             key.append(c);
1007                         }
1008 
1009                         break;
1010 
1011                     case 1:
1012                         if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) {
1013                             // this is an escaped separator or white space
1014                             key.append(c);
1015                         } else {
1016                             // another escaped character, the '\' is preserved
1017                             key.append('\\');
1018                             key.append(c);
1019                         }
1020 
1021                         // return to the key parsing state
1022                         state = 0;
1023 
1024                         break;
1025 
1026                     case 2:
1027                         if (contains(WHITE_SPACE, c)) {
1028                             // do nothing, eat all white spaces
1029                             state = 2;
1030                         } else if (contains(SEPARATORS, c)) {
1031                             // switch to the value parsing state
1032                             state = 3;
1033                         } else {
1034                             // any other character indicates we encoutered the beginning of the value
1035                             value.append(c);
1036 
1037                             // switch to the value parsing state
1038                             state = 4;
1039                         }
1040 
1041                         break;
1042 
1043                     case 3:
1044                         if (contains(WHITE_SPACE, c)) {
1045                             // do nothing, eat all white spaces
1046                             state = 3;
1047                         } else {
1048                             // any other character indicates we encoutered the beginning of the value
1049                             value.append(c);
1050 
1051                             // switch to the value parsing state
1052                             state = 4;
1053                         }
1054 
1055                         break;
1056 
1057                     case 4:
1058                         value.append(c);
1059                         break;
1060 
1061                     default:
1062                         throw new IllegalStateException();
1063                 }
1064             }
1065 
1066             result[0] = key.toString();
1067             result[1] = value.toString();
1068 
1069             return result;
1070         }
1071     } // class PropertiesReader
1072 
1073     /**
1074      * This class is used to write properties lines.
1075      */
1076     public static class PropertiesWriter extends FilterWriter {
1077         private boolean typed;
1078 
1079         /**
1080          * Constructor.
1081          *
1082          * @param writer a Writer object providing the underlying stream
1083          */
1084         public PropertiesWriter(Writer writer, boolean typed) {
1085             super(writer);
1086             this.typed = typed;
1087         }
1088 
1089         /**
1090          * Writes the given property and its value.
1091          *
1092          * @param key the property key
1093          * @param value the property value
1094          * @throws IOException if an error occurs
1095          */
1096         public void writeProperty(String key, String value) throws IOException {
1097             write(escapeKey(key));
1098             write(" = ");
1099             write(typed ? value : escapeJava(value));
1100             writeln(null);
1101         }
1102 
1103         /**
1104          * Helper method for writing a line with the platform specific line
1105          * ending.
1106          *
1107          * @param s the content of the line (may be <b>null</b>)
1108          * @throws IOException if an error occurs
1109          */
1110         public void writeln(String s) throws IOException {
1111             if (s != null) {
1112                 write(s);
1113             }
1114             write(LINE_SEPARATOR);
1115         }
1116     } // class PropertiesWriter
1117 
1118     /**
1119      * TODO
1120      */
1121     protected static class Layout {
1122 
1123         private List<String> commentLines;
1124         private List<String> valueLines;
1125 
1126         public Layout() {}
1127 
1128         public Layout(List<String> commentLines, List<String> valueLines) {
1129             this.commentLines = commentLines;
1130             this.valueLines = valueLines;
1131         }
1132 
1133         public List<String> getCommentLines() {
1134             return commentLines;
1135         }
1136 
1137         public void setCommentLines(List<String> commentLines) {
1138             this.commentLines = commentLines;
1139         }
1140 
1141         public List<String> getValueLines() {
1142             return valueLines;
1143         }
1144 
1145         public void setValueLines(List<String> valueLines) {
1146             this.valueLines = valueLines;
1147         }
1148 
1149         public void clearValue() {
1150             this.valueLines = null;
1151         }
1152     } // class Layout
1153 }