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