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