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><name></code>
837 * = <code><value></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 }