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