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