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