Home | History | Annotate | Download | only in util
      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"><h3>Character Encoding</h3></a>
     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      */
     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      */
    102     public Properties(Properties properties) {
    103         defaults = properties;
    104     }
    105 
    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      */
    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      */
    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      */
    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      */
    200     public void list(PrintWriter out) {
    201         listToAppendable(out);
    202     }
    203 
    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      */
    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")
    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      */
    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      */
    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 
    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
    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      */
    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      */
    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      */
    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">;
    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      */
    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">;
    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      */
    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">;
    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      */
    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 
    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