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