1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17 
18 package java.util;
19 
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.OutputStream;
25 import java.io.OutputStreamWriter;
26 import java.io.PrintStream;
27 import java.io.PrintWriter;
28 import java.io.Reader;
29 import java.io.StringReader;
30 import java.io.Writer;
31 import java.nio.charset.Charset;
32 import java.nio.charset.IllegalCharsetNameException;
33 import java.nio.charset.UnsupportedCharsetException;
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 import org.w3c.dom.Document;
38 import org.w3c.dom.Element;
39 import org.w3c.dom.Node;
40 import org.w3c.dom.NodeList;
41 import org.w3c.dom.Text;
42 import org.xml.sax.EntityResolver;
43 import org.xml.sax.ErrorHandler;
44 import org.xml.sax.InputSource;
45 import org.xml.sax.SAXException;
46 import org.xml.sax.SAXParseException;
47 
48 /**
49  * A {@code Properties} object is a {@code Hashtable} where the keys and values
50  * must be {@code String}s. Each property can have a default
51  * {@code Properties} list which specifies the default
52  * values to be used when a given key is not found in this {@code Properties}
53  * instance.
54  *
55  * <a name="character_encoding"></a><h3>Character Encoding</h3>
56  * <p>Note that in some cases {@code Properties} uses ISO-8859-1 instead of UTF-8.
57  * ISO-8859-1 is only capable of representing a tiny subset of Unicode.
58  * Use either the {@code loadFromXML}/{@code storeToXML} methods (which use UTF-8 by
59  * default) or the {@code load}/{@code store} overloads that take
60  * an {@code OutputStreamWriter} (so you can supply a UTF-8 instance) instead.
61  *
62  * @see Hashtable
63  * @see java.lang.System#getProperties
64  */
65 public class Properties extends Hashtable<Object, Object> {
66 
67     private static final long serialVersionUID = 4112578634029874840L;
68 
69     private transient DocumentBuilder builder = null;
70 
71     private static final String PROP_DTD_NAME = "http://java.sun.com/dtd/properties.dtd";
72 
73     private static final String PROP_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
74             + "    <!ELEMENT properties (comment?, entry*) >"
75             + "    <!ATTLIST properties version CDATA #FIXED \"1.0\" >"
76             + "    <!ELEMENT comment (#PCDATA) >"
77             + "    <!ELEMENT entry (#PCDATA) >"
78             + "    <!ATTLIST entry key CDATA #REQUIRED >";
79 
80     /**
81      * The default values for keys not found in this {@code Properties}
82      * instance.
83      */
84     protected Properties defaults;
85 
86     private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3,
87             KEY_DONE = 4, IGNORE = 5;
88 
89     /**
90      * Constructs a new {@code Properties} object.
91      */
Properties()92     public Properties() {
93     }
94 
95     /**
96      * Constructs a new {@code Properties} object using the specified default
97      * {@code Properties}.
98      *
99      * @param properties
100      *            the default {@code Properties}.
101      */
Properties(Properties properties)102     public Properties(Properties properties) {
103         defaults = properties;
104     }
105 
dumpString(StringBuilder buffer, String string, boolean key)106     private void dumpString(StringBuilder buffer, String string, boolean key) {
107         int i = 0;
108         if (!key && i < string.length() && string.charAt(i) == ' ') {
109             buffer.append("\\ ");
110             i++;
111         }
112 
113         for (; i < string.length(); i++) {
114             char ch = string.charAt(i);
115             switch (ch) {
116             case '\t':
117                 buffer.append("\\t");
118                 break;
119             case '\n':
120                 buffer.append("\\n");
121                 break;
122             case '\f':
123                 buffer.append("\\f");
124                 break;
125             case '\r':
126                 buffer.append("\\r");
127                 break;
128             default:
129                 if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) {
130                     buffer.append('\\');
131                 }
132                 if (ch >= ' ' && ch <= '~') {
133                     buffer.append(ch);
134                 } else {
135                     String hex = Integer.toHexString(ch);
136                     buffer.append("\\u");
137                     for (int j = 0; j < 4 - hex.length(); j++) {
138                         buffer.append("0");
139                     }
140                     buffer.append(hex);
141                 }
142             }
143         }
144     }
145 
146     /**
147      * Searches for the property with the specified name. If the property is not
148      * found, the default {@code Properties} are checked. If the property is not
149      * found in the default {@code Properties}, {@code null} is returned.
150      *
151      * @param name
152      *            the name of the property to find.
153      * @return the named property value, or {@code null} if it can't be found.
154      */
getProperty(String name)155     public String getProperty(String name) {
156         Object result = super.get(name);
157         String property = result instanceof String ? (String) result : null;
158         if (property == null && defaults != null) {
159             property = defaults.getProperty(name);
160         }
161         return property;
162     }
163 
164     /**
165      * Searches for the property with the specified name. If the property is not
166      * found, it looks in the default {@code Properties}. If the property is not
167      * found in the default {@code Properties}, it returns the specified
168      * default.
169      *
170      * @param name
171      *            the name of the property to find.
172      * @param defaultValue
173      *            the default value.
174      * @return the named property value.
175      */
getProperty(String name, String defaultValue)176     public String getProperty(String name, String defaultValue) {
177         Object result = super.get(name);
178         String property = result instanceof String ? (String) result : null;
179         if (property == null && defaults != null) {
180             property = defaults.getProperty(name);
181         }
182         if (property == null) {
183             return defaultValue;
184         }
185         return property;
186     }
187 
188     /**
189      * Lists the mappings in this {@code Properties} to {@code out} in a human-readable form.
190      * Note that values are truncated to 37 characters, so this method is rarely useful.
191      */
list(PrintStream out)192     public void list(PrintStream out) {
193         listToAppendable(out);
194     }
195 
196     /**
197      * Lists the mappings in this {@code Properties} to {@code out} in a human-readable form.
198      * Note that values are truncated to 37 characters, so this method is rarely useful.
199      */
list(PrintWriter out)200     public void list(PrintWriter out) {
201         listToAppendable(out);
202     }
203 
listToAppendable(Appendable out)204     private void listToAppendable(Appendable out) {
205         try {
206             if (out == null) {
207                 throw new NullPointerException("out == null");
208             }
209             StringBuilder sb = new StringBuilder(80);
210             Enumeration<?> keys = propertyNames();
211             while (keys.hasMoreElements()) {
212                 String key = (String) keys.nextElement();
213                 sb.append(key);
214                 sb.append('=');
215                 String property = (String) super.get(key);
216                 Properties def = defaults;
217                 while (property == null) {
218                     property = (String) def.get(key);
219                     def = def.defaults;
220                 }
221                 if (property.length() > 40) {
222                     sb.append(property.substring(0, 37));
223                     sb.append("...");
224                 } else {
225                     sb.append(property);
226                 }
227                 sb.append(System.lineSeparator());
228                 out.append(sb.toString());
229                 sb.setLength(0);
230             }
231         } catch (IOException ex) {
232             // Appendable.append throws IOException, but PrintStream and PrintWriter don't.
233             throw new AssertionError(ex);
234         }
235     }
236 
237     /**
238      * Loads properties from the specified {@code InputStream}, assumed to be ISO-8859-1.
239      * See "<a href="#character_encoding">Character Encoding</a>".
240      *
241      * @param in the {@code InputStream}
242      * @throws IOException
243      */
load(InputStream in)244     public synchronized void load(InputStream in) throws IOException {
245         if (in == null) {
246             throw new NullPointerException("in == null");
247         }
248         load(new InputStreamReader(in, "ISO-8859-1"));
249     }
250 
251     /**
252      * Loads properties from the specified {@code Reader}.
253      * The properties file is interpreted according to the following rules:
254      * <ul>
255      * <li>Empty lines are ignored.</li>
256      * <li>Lines starting with either a "#" or a "!" are comment lines and are
257      * ignored.</li>
258      * <li>A backslash at the end of the line escapes the following newline
259      * character ("\r", "\n", "\r\n"). If there's whitespace after the
260      * backslash it will just escape that whitespace instead of concatenating
261      * the lines. This does not apply to comment lines.</li>
262      * <li>A property line consists of the key, the space between the key and
263      * the value, and the value. The key goes up to the first whitespace, "=" or
264      * ":" that is not escaped. The space between the key and the value contains
265      * either one whitespace, one "=" or one ":" and any amount of additional
266      * whitespace before and after that character. The value starts with the
267      * first character after the space between the key and the value.</li>
268      * <li>Following escape sequences are recognized: "\ ", "\\", "\r", "\n",
269      * "\!", "\#", "\t", "\b", "\f", and "&#92;uXXXX" (unicode character).</li>
270      * </ul>
271      *
272      * @param in the {@code Reader}
273      * @throws IOException
274      * @since 1.6
275      */
276     @SuppressWarnings("fallthrough")
load(Reader in)277     public synchronized void load(Reader in) throws IOException {
278         if (in == null) {
279             throw new NullPointerException("in == null");
280         }
281         int mode = NONE, unicode = 0, count = 0;
282         char nextChar, buf[] = new char[40];
283         int offset = 0, keyLength = -1, intVal;
284         boolean firstChar = true;
285 
286         BufferedReader br = new BufferedReader(in);
287 
288         while (true) {
289             intVal = br.read();
290             if (intVal == -1) {
291                 break;
292             }
293             nextChar = (char) intVal;
294 
295             if (offset == buf.length) {
296                 char[] newBuf = new char[buf.length * 2];
297                 System.arraycopy(buf, 0, newBuf, 0, offset);
298                 buf = newBuf;
299             }
300             if (mode == UNICODE) {
301                 int digit = Character.digit(nextChar, 16);
302                 if (digit >= 0) {
303                     unicode = (unicode << 4) + digit;
304                     if (++count < 4) {
305                         continue;
306                     }
307                 } else if (count <= 4) {
308                     throw new IllegalArgumentException("Invalid Unicode sequence: illegal character");
309                 }
310                 mode = NONE;
311                 buf[offset++] = (char) unicode;
312                 if (nextChar != '\n') {
313                     continue;
314                 }
315             }
316             if (mode == SLASH) {
317                 mode = NONE;
318                 switch (nextChar) {
319                 case '\r':
320                     mode = CONTINUE; // Look for a following \n
321                     continue;
322                 case '\n':
323                     mode = IGNORE; // Ignore whitespace on the next line
324                     continue;
325                 case 'b':
326                     nextChar = '\b';
327                     break;
328                 case 'f':
329                     nextChar = '\f';
330                     break;
331                 case 'n':
332                     nextChar = '\n';
333                     break;
334                 case 'r':
335                     nextChar = '\r';
336                     break;
337                 case 't':
338                     nextChar = '\t';
339                     break;
340                 case 'u':
341                     mode = UNICODE;
342                     unicode = count = 0;
343                     continue;
344                 }
345             } else {
346                 switch (nextChar) {
347                 case '#':
348                 case '!':
349                     if (firstChar) {
350                         while (true) {
351                             intVal = br.read();
352                             if (intVal == -1) {
353                                 break;
354                             }
355                             nextChar = (char) intVal;
356                             if (nextChar == '\r' || nextChar == '\n') {
357                                 break;
358                             }
359                         }
360                         continue;
361                     }
362                     break;
363                 case '\n':
364                     if (mode == CONTINUE) { // Part of a \r\n sequence
365                         mode = IGNORE; // Ignore whitespace on the next line
366                         continue;
367                     }
368                     // fall into the next case
369                 case '\r':
370                     mode = NONE;
371                     firstChar = true;
372                     if (offset > 0 || (offset == 0 && keyLength == 0)) {
373                         if (keyLength == -1) {
374                             keyLength = offset;
375                         }
376                         String temp = new String(buf, 0, offset);
377                         put(temp.substring(0, keyLength), temp
378                                 .substring(keyLength));
379                     }
380                     keyLength = -1;
381                     offset = 0;
382                     continue;
383                 case '\\':
384                     if (mode == KEY_DONE) {
385                         keyLength = offset;
386                     }
387                     mode = SLASH;
388                     continue;
389                 case ':':
390                 case '=':
391                     if (keyLength == -1) { // if parsing the key
392                         mode = NONE;
393                         keyLength = offset;
394                         continue;
395                     }
396                     break;
397                 }
398                 if (Character.isWhitespace(nextChar)) {
399                     if (mode == CONTINUE) {
400                         mode = IGNORE;
401                     }
402                     // if key length == 0 or value length == 0
403                     if (offset == 0 || offset == keyLength || mode == IGNORE) {
404                         continue;
405                     }
406                     if (keyLength == -1) { // if parsing the key
407                         mode = KEY_DONE;
408                         continue;
409                     }
410                 }
411                 if (mode == IGNORE || mode == CONTINUE) {
412                     mode = NONE;
413                 }
414             }
415             firstChar = false;
416             if (mode == KEY_DONE) {
417                 keyLength = offset;
418                 mode = NONE;
419             }
420             buf[offset++] = nextChar;
421         }
422         if (mode == UNICODE && count <= 4) {
423             throw new IllegalArgumentException("Invalid Unicode sequence: expected format \\uxxxx");
424         }
425         if (keyLength == -1 && offset > 0) {
426             keyLength = offset;
427         }
428         if (keyLength >= 0) {
429             String temp = new String(buf, 0, offset);
430             String key = temp.substring(0, keyLength);
431             String value = temp.substring(keyLength);
432             if (mode == SLASH) {
433                 value += "\u0000";
434             }
435             put(key, value);
436         }
437     }
438 
439     /**
440      * Returns all of the property names (keys) in this {@code Properties} object.
441      */
propertyNames()442     public Enumeration<?> propertyNames() {
443         Hashtable<Object, Object> selected = new Hashtable<Object, Object>();
444         selectProperties(selected, false);
445         return selected.keys();
446     }
447 
448     /**
449      * Returns those property names (keys) in this {@code Properties} object for which
450      * both key and value are strings.
451      *
452      * @return a set of keys in the property list
453      * @since 1.6
454      */
stringPropertyNames()455     public Set<String> stringPropertyNames() {
456         Hashtable<String, Object> stringProperties = new Hashtable<String, Object>();
457         selectProperties(stringProperties, true);
458         return Collections.unmodifiableSet(stringProperties.keySet());
459     }
460 
selectProperties(Hashtable<K, Object> selectProperties, final boolean isStringOnly)461     private <K> void selectProperties(Hashtable<K, Object> selectProperties, final boolean isStringOnly) {
462         if (defaults != null) {
463             defaults.selectProperties(selectProperties, isStringOnly);
464         }
465         Enumeration<Object> keys = keys();
466         while (keys.hasMoreElements()) {
467             @SuppressWarnings("unchecked")
468             K key = (K) keys.nextElement();
469             if (isStringOnly && !(key instanceof String)) {
470                 // Only select property with string key and value
471                 continue;
472             }
473             Object value = get(key);
474             selectProperties.put(key, value);
475         }
476     }
477 
478     /**
479      * Saves the mappings in this {@code Properties} to the specified {@code
480      * OutputStream}, putting the specified comment at the beginning. The output
481      * from this method is suitable for being read by the
482      * {@link #load(InputStream)} method.
483      *
484      * @param out the {@code OutputStream} to write to.
485      * @param comment the comment to add at the beginning.
486      * @throws ClassCastException if the key or value of a mapping is not a
487      *                String.
488      * @deprecated This method ignores any {@code IOException} thrown while
489      *             writing &mdash; use {@link #store} instead for better exception
490      *             handling.
491      */
492     @Deprecated
save(OutputStream out, String comment)493     public void save(OutputStream out, String comment) {
494         try {
495             store(out, comment);
496         } catch (IOException e) {
497         }
498     }
499 
500     /**
501      * Maps the specified key to the specified value. If the key already exists,
502      * the old value is replaced. The key and value cannot be {@code null}.
503      *
504      * @param name
505      *            the key.
506      * @param value
507      *            the value.
508      * @return the old value mapped to the key, or {@code null}.
509      */
setProperty(String name, String value)510     public Object setProperty(String name, String value) {
511         return put(name, value);
512     }
513 
514     /**
515      * Stores properties to the specified {@code OutputStream}, using ISO-8859-1.
516      * See "<a href="#character_encoding">Character Encoding</a>".
517      *
518      * @param out the {@code OutputStream}
519      * @param comment an optional comment to be written, or null
520      * @throws IOException
521      * @throws ClassCastException if a key or value is not a string
522      */
store(OutputStream out, String comment)523     public synchronized void store(OutputStream out, String comment) throws IOException {
524         store(new OutputStreamWriter(out, "ISO-8859-1"), comment);
525     }
526 
527     /**
528      * Stores the mappings in this {@code Properties} object to {@code out},
529      * putting the specified comment at the beginning.
530      *
531      * @param writer the {@code Writer}
532      * @param comment an optional comment to be written, or null
533      * @throws IOException
534      * @throws ClassCastException if a key or value is not a string
535      * @since 1.6
536      */
store(Writer writer, String comment)537     public synchronized void store(Writer writer, String comment) throws IOException {
538         if (comment != null) {
539             writer.write("#");
540             writer.write(comment);
541             writer.write(System.lineSeparator());
542         }
543         writer.write("#");
544         writer.write(new Date().toString());
545         writer.write(System.lineSeparator());
546 
547         StringBuilder sb = new StringBuilder(200);
548         for (Map.Entry<Object, Object> entry : entrySet()) {
549             String key = (String) entry.getKey();
550             dumpString(sb, key, true);
551             sb.append('=');
552             dumpString(sb, (String) entry.getValue(), false);
553             sb.append(System.lineSeparator());
554             writer.write(sb.toString());
555             sb.setLength(0);
556         }
557         writer.flush();
558     }
559 
560     /**
561      * Loads the properties from an {@code InputStream} containing the
562      * properties in XML form. The XML document must begin with (and conform to)
563      * following DOCTYPE:
564      *
565      * <pre>
566      * &lt;!DOCTYPE properties SYSTEM &quot;http://java.sun.com/dtd/properties.dtd&quot;&gt;
567      * </pre>
568      *
569      * Also the content of the XML data must satisfy the DTD but the xml is not
570      * validated against it. The DTD is not loaded from the SYSTEM ID. After
571      * this method returns the InputStream is not closed.
572      *
573      * @param in the InputStream containing the XML document.
574      * @throws IOException in case an error occurs during a read operation.
575      * @throws InvalidPropertiesFormatException if the XML data is not a valid
576      *             properties file.
577      */
loadFromXML(InputStream in)578     public synchronized void loadFromXML(InputStream in) throws IOException,
579             InvalidPropertiesFormatException {
580         if (in == null) {
581             throw new NullPointerException("in == null");
582         }
583 
584         if (builder == null) {
585             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
586             // BEGIN android-removed: we still don't support validation.
587             // factory.setValidating(true);
588             // END android-removed
589 
590             try {
591                 builder = factory.newDocumentBuilder();
592             } catch (ParserConfigurationException e) {
593                 throw new Error(e);
594             }
595 
596             builder.setErrorHandler(new ErrorHandler() {
597                 public void warning(SAXParseException e) throws SAXException {
598                     throw e;
599                 }
600 
601                 public void error(SAXParseException e) throws SAXException {
602                     throw e;
603                 }
604 
605                 public void fatalError(SAXParseException e) throws SAXException {
606                     throw e;
607                 }
608             });
609 
610             builder.setEntityResolver(new EntityResolver() {
611                 public InputSource resolveEntity(String publicId,
612                         String systemId) throws SAXException, IOException {
613                     if (systemId.equals(PROP_DTD_NAME)) {
614                         InputSource result = new InputSource(new StringReader(
615                                 PROP_DTD));
616                         result.setSystemId(PROP_DTD_NAME);
617                         return result;
618                     }
619                     throw new SAXException("Invalid DOCTYPE declaration: "
620                             + systemId);
621                 }
622             });
623         }
624 
625         try {
626             Document doc = builder.parse(in);
627             NodeList entries = doc.getElementsByTagName("entry");
628             if (entries == null) {
629                 return;
630             }
631             int entriesListLength = entries.getLength();
632 
633             for (int i = 0; i < entriesListLength; i++) {
634                 Element entry = (Element) entries.item(i);
635                 String key = entry.getAttribute("key");
636                 String value = entry.getTextContent();
637 
638                 /*
639                  * key != null & value != null but key or(and) value can be
640                  * empty String
641                  */
642                 put(key, value);
643             }
644         } catch (IOException e) {
645             throw e;
646         } catch (SAXException e) {
647             throw new InvalidPropertiesFormatException(e);
648         }
649     }
650 
651     /**
652      * Writes all properties stored in this instance into the {@code
653      * OutputStream} in XML representation. The DOCTYPE is
654      *
655      * <pre>
656      * &lt;!DOCTYPE properties SYSTEM &quot;http://java.sun.com/dtd/properties.dtd&quot;&gt;
657      * </pre>
658      *
659      * If the comment is null, no comment is added to the output. UTF-8 is used
660      * as the encoding. The {@code OutputStream} is not closed at the end. A
661      * call to this method is the same as a call to {@code storeToXML(os,
662      * comment, "UTF-8")}.
663      *
664      * @param os the {@code OutputStream} to write to.
665      * @param comment the comment to add. If null, no comment is added.
666      * @throws IOException if an error occurs during writing to the output.
667      */
storeToXML(OutputStream os, String comment)668     public void storeToXML(OutputStream os, String comment) throws IOException {
669         storeToXML(os, comment, "UTF-8");
670     }
671 
672     /**
673      * Writes all properties stored in this instance into the {@code
674      * OutputStream} in XML representation. The DOCTYPE is
675      *
676      * <pre>
677      * &lt;!DOCTYPE properties SYSTEM &quot;http://java.sun.com/dtd/properties.dtd&quot;&gt;
678      * </pre>
679      *
680      * If the comment is null, no comment is added to the output. The parameter
681      * {@code encoding} defines which encoding should be used. The {@code
682      * OutputStream} is not closed at the end.
683      *
684      * @param os the {@code OutputStream} to write to.
685      * @param comment the comment to add. If null, no comment is added.
686      * @param encoding the code identifying the encoding that should be used to
687      *            write into the {@code OutputStream}.
688      * @throws IOException if an error occurs during writing to the output.
689      */
storeToXML(OutputStream os, String comment, String encoding)690     public synchronized void storeToXML(OutputStream os, String comment,
691             String encoding) throws IOException {
692 
693         if (os == null) {
694             throw new NullPointerException("os == null");
695         } else if (encoding == null) {
696             throw new NullPointerException("encoding == null");
697         }
698 
699         /*
700          * We can write to XML file using encoding parameter but note that some
701          * aliases for encodings are not supported by the XML parser. Thus we
702          * have to know canonical name for encoding used to store data in XML
703          * since the XML parser must recognize encoding name used to store data.
704          */
705 
706         String encodingCanonicalName;
707         try {
708             encodingCanonicalName = Charset.forName(encoding).name();
709         } catch (IllegalCharsetNameException e) {
710             System.out.println("Warning: encoding name " + encoding
711                     + " is illegal, using UTF-8 as default encoding");
712             encodingCanonicalName = "UTF-8";
713         } catch (UnsupportedCharsetException e) {
714             System.out.println("Warning: encoding " + encoding
715                     + " is not supported, using UTF-8 as default encoding");
716             encodingCanonicalName = "UTF-8";
717         }
718 
719         PrintStream printStream = new PrintStream(os, false,
720                 encodingCanonicalName);
721 
722         printStream.print("<?xml version=\"1.0\" encoding=\"");
723         printStream.print(encodingCanonicalName);
724         printStream.println("\"?>");
725 
726         printStream.print("<!DOCTYPE properties SYSTEM \"");
727         printStream.print(PROP_DTD_NAME);
728         printStream.println("\">");
729 
730         printStream.println("<properties>");
731 
732         if (comment != null) {
733             printStream.print("<comment>");
734             printStream.print(substitutePredefinedEntries(comment));
735             printStream.println("</comment>");
736         }
737 
738         for (Map.Entry<Object, Object> entry : entrySet()) {
739             String keyValue = (String) entry.getKey();
740             String entryValue = (String) entry.getValue();
741             printStream.print("<entry key=\"");
742             printStream.print(substitutePredefinedEntries(keyValue));
743             printStream.print("\">");
744             printStream.print(substitutePredefinedEntries(entryValue));
745             printStream.println("</entry>");
746         }
747         printStream.println("</properties>");
748         printStream.flush();
749     }
750 
substitutePredefinedEntries(String s)751     private String substitutePredefinedEntries(String s) {
752         // substitution for predefined character entities to use them safely in XML.
753         s = s.replaceAll("&", "&amp;");
754         s = s.replaceAll("<", "&lt;");
755         s = s.replaceAll(">", "&gt;");
756         s = s.replaceAll("'", "&apos;");
757         s = s.replaceAll("\"", "&quot;");
758         return s;
759     }
760 }
761