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