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