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