/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package java.util; import java.io.IOException; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringReader; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; import java.security.AccessController; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; // BEGIN android-added import org.w3c.dom.Node; import org.w3c.dom.Text; // END android-added import org.apache.harmony.luni.internal.nls.Messages; import org.apache.harmony.luni.util.PriviAction; /** * A {@code Properties} object is a {@code Hashtable} where the keys and values * must be {@code String}s. Each property can have a default * {@code Properties} list which specifies the default * values to be used when a given key is not found in this {@code Properties} * instance. * * @see Hashtable * @see java.lang.System#getProperties */ public class Properties extends Hashtable<Object, Object> { private static final long serialVersionUID = 4112578634029874840L; private transient DocumentBuilder builder = null; private static final String PROP_DTD_NAME = "http://java.sun.com/dtd/properties.dtd"; private static final String PROP_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + " <!ELEMENT properties (comment?, entry*) >" + " <!ATTLIST properties version CDATA #FIXED \"1.0\" >" + " <!ELEMENT comment (#PCDATA) >" + " <!ELEMENT entry (#PCDATA) >" + " <!ATTLIST entry key CDATA #REQUIRED >"; /** * The default values for keys not found in this {@code Properties} * instance. */ protected Properties defaults; private static final int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, KEY_DONE = 4, IGNORE = 5; /** * Constructs a new {@code Properties} object. */ public Properties() { super(); } /** * Constructs a new {@code Properties} object using the specified default * {@code Properties}. * * @param properties * the default {@code Properties}. */ public Properties(Properties properties) { defaults = properties; } private void dumpString(StringBuilder buffer, String string, boolean key) { int i = 0; if (!key && i < string.length() && string.charAt(i) == ' ') { buffer.append("\\ "); //$NON-NLS-1$ i++; } for (; i < string.length(); i++) { char ch = string.charAt(i); switch (ch) { case '\t': buffer.append("\\t"); //$NON-NLS-1$ break; case '\n': buffer.append("\\n"); //$NON-NLS-1$ break; case '\f': buffer.append("\\f"); //$NON-NLS-1$ break; case '\r': buffer.append("\\r"); //$NON-NLS-1$ break; default: if ("\\#!=:".indexOf(ch) >= 0 || (key && ch == ' ')) { buffer.append('\\'); } if (ch >= ' ' && ch <= '~') { buffer.append(ch); } else { String hex = Integer.toHexString(ch); buffer.append("\\u"); //$NON-NLS-1$ for (int j = 0; j < 4 - hex.length(); j++) { buffer.append("0"); //$NON-NLS-1$ } buffer.append(hex); } } } } /** * Searches for the property with the specified name. If the property is not * found, the default {@code Properties} are checked. If the property is not * found in the default {@code Properties}, {@code null} is returned. * * @param name * the name of the property to find. * @return the named property value, or {@code null} if it can't be found. */ public String getProperty(String name) { Object result = super.get(name); String property = result instanceof String ? (String) result : null; if (property == null && defaults != null) { property = defaults.getProperty(name); } return property; } /** * Searches for the property with the specified name. If the property is not * found, it looks in the default {@code Properties}. If the property is not * found in the default {@code Properties}, it returns the specified * default. * * @param name * the name of the property to find. * @param defaultValue * the default value. * @return the named property value. */ public String getProperty(String name, String defaultValue) { Object result = super.get(name); String property = result instanceof String ? (String) result : null; if (property == null && defaults != null) { property = defaults.getProperty(name); } if (property == null) { return defaultValue; } return property; } /** * Lists the mappings in this {@code Properties} to the specified * {@code PrintStream} in a * human readable form. * * @param out * the {@code PrintStream} to write the content to in human readable * form. */ public void list(PrintStream out) { if (out == null) { throw new NullPointerException(); } StringBuilder buffer = new StringBuilder(80); Enumeration<?> keys = propertyNames(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); buffer.append(key); buffer.append('='); String property = (String) super.get(key); Properties def = defaults; while (property == null) { property = (String) def.get(key); def = def.defaults; } if (property.length() > 40) { buffer.append(property.substring(0, 37)); buffer.append("..."); //$NON-NLS-1$ } else { buffer.append(property); } out.println(buffer.toString()); buffer.setLength(0); } } /** * Lists the mappings in this {@code Properties} to the specified * {@code PrintWriter} in a * human readable form. * * @param writer * the {@code PrintWriter} to write the content to in human * readable form. */ public void list(PrintWriter writer) { if (writer == null) { throw new NullPointerException(); } StringBuilder buffer = new StringBuilder(80); Enumeration<?> keys = propertyNames(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); buffer.append(key); buffer.append('='); String property = (String) super.get(key); Properties def = defaults; while (property == null) { property = (String) def.get(key); def = def.defaults; } if (property.length() > 40) { buffer.append(property.substring(0, 37)); buffer.append("..."); //$NON-NLS-1$ } else { buffer.append(property); } writer.println(buffer.toString()); buffer.setLength(0); } } /** * Loads properties from the specified {@code InputStream}. The encoding is * ISO8859-1. The {@code Properties} file is interpreted according to the * following rules: * <ul> * <li>Empty lines are ignored.</li> * <li>Lines starting with either a "#" or a "!" are comment lines and are * ignored.</li> * <li>A backslash at the end of the line escapes the following newline * character ("\r", "\n", "\r\n"). If there's a whitespace after the * backslash it will just escape that whitespace instead of concatenating * the lines. This does not apply to comment lines.</li> * <li>A property line consists of the key, the space between the key and * the value, and the value. The key goes up to the first whitespace, "=" or * ":" that is not escaped. The space between the key and the value contains * either one whitespace, one "=" or one ":" and any number of additional * whitespaces before and after that character. The value starts with the * first character after the space between the key and the value.</li> * <li>Following escape sequences are recognized: "\ ", "\\", "\r", "\n", * "\!", "\#", "\t", "\b", "\f", and "\uXXXX" (unicode character).</li> * </ul> * * @param in * the {@code InputStream}. * @throws IOException * if error occurs during reading from the {@code InputStream}. */ @SuppressWarnings("fallthrough") public synchronized void load(InputStream in) throws IOException { if (in == null) { throw new NullPointerException(); } int mode = NONE, unicode = 0, count = 0; char nextChar, buf[] = new char[40]; int offset = 0, keyLength = -1, intVal; boolean firstChar = true; // BEGIN android-changed BufferedInputStream bis = new BufferedInputStream(in, 8192); // END android-changed while (true) { intVal = bis.read(); if (intVal == -1) { // if mode is UNICODE but has less than 4 hex digits, should // throw an IllegalArgumentException // luni.08=Invalid Unicode sequence: expected format \\uxxxx if (mode == UNICODE && count < 4) { throw new IllegalArgumentException(Messages.getString("luni.08")); //$NON-NLS-1$ } // if mode is SLASH and no data is read, should append '\u0000' // to buf if (mode == SLASH) { buf[offset++] = '\u0000'; } break; } nextChar = (char) (intVal & 0xff); if (offset == buf.length) { char[] newBuf = new char[buf.length * 2]; System.arraycopy(buf, 0, newBuf, 0, offset); buf = newBuf; } if (mode == UNICODE) { int digit = Character.digit(nextChar, 16); if (digit >= 0) { unicode = (unicode << 4) + digit; if (++count < 4) { continue; } } else if (count <= 4) { // luni.09=Invalid Unicode sequence: illegal character throw new IllegalArgumentException(Messages.getString("luni.09")); //$NON-NLS-1$ } mode = NONE; buf[offset++] = (char) unicode; if (nextChar != '\n') { continue; } } if (mode == SLASH) { mode = NONE; switch (nextChar) { case '\r': mode = CONTINUE; // Look for a following \n continue; case '\n': mode = IGNORE; // Ignore whitespace on the next line continue; case 'b': nextChar = '\b'; break; case 'f': nextChar = '\f'; break; case 'n': nextChar = '\n'; break; case 'r': nextChar = '\r'; break; case 't': nextChar = '\t'; break; case 'u': mode = UNICODE; unicode = count = 0; continue; } } else { switch (nextChar) { case '#': case '!': if (firstChar) { while (true) { intVal = bis.read(); if (intVal == -1) { break; } // & 0xff not required nextChar = (char) intVal; if (nextChar == '\r' || nextChar == '\n') { break; } } continue; } break; case '\n': if (mode == CONTINUE) { // Part of a \r\n sequence mode = IGNORE; // Ignore whitespace on the next line continue; } // fall into the next case case '\r': mode = NONE; firstChar = true; if (offset > 0 || (offset == 0 && keyLength == 0)) { if (keyLength == -1) { keyLength = offset; } String temp = new String(buf, 0, offset); put(temp.substring(0, keyLength), temp .substring(keyLength)); } keyLength = -1; offset = 0; continue; case '\\': if (mode == KEY_DONE) { keyLength = offset; } mode = SLASH; continue; case ':': case '=': if (keyLength == -1) { // if parsing the key mode = NONE; keyLength = offset; continue; } break; } if (Character.isWhitespace(nextChar)) { if (mode == CONTINUE) { mode = IGNORE; } // if key length == 0 or value length == 0 if (offset == 0 || offset == keyLength || mode == IGNORE) { continue; } if (keyLength == -1) { // if parsing the key mode = KEY_DONE; continue; } } if (mode == IGNORE || mode == CONTINUE) { mode = NONE; } } firstChar = false; if (mode == KEY_DONE) { keyLength = offset; mode = NONE; } buf[offset++] = nextChar; } if (keyLength == -1 && offset > 0) { keyLength = offset; } if (keyLength >= 0) { String temp = new String(buf, 0, offset); put(temp.substring(0, keyLength), temp.substring(keyLength)); } } /** * Returns all of the property names that this {@code Properties} object * contains. * * @return an {@code Enumeration} containing the names of all properties * that this {@code Properties} object contains. */ public Enumeration<?> propertyNames() { if (defaults == null) { return keys(); } Hashtable<Object, Object> set = new Hashtable<Object, Object>(defaults .size() + size()); Enumeration<?> keys = defaults.propertyNames(); while (keys.hasMoreElements()) { set.put(keys.nextElement(), set); } keys = keys(); while (keys.hasMoreElements()) { set.put(keys.nextElement(), set); } return set.keys(); } /** * Saves the mappings in this {@code Properties} to the specified {@code * OutputStream}, putting the specified comment at the beginning. The output * from this method is suitable for being read by the * {@link #load(InputStream)} method. * * @param out the {@code OutputStream} to write to. * @param comment the comment to add at the beginning. * @throws ClassCastException if the key or value of a mapping is not a * String. * @deprecated This method ignores any {@code IOException} thrown while * writing -- use {@link #store} instead for better exception * handling. */ @Deprecated public void save(OutputStream out, String comment) { try { store(out, comment); } catch (IOException e) { } } /** * Maps the specified key to the specified value. If the key already exists, * the old value is replaced. The key and value cannot be {@code null}. * * @param name * the key. * @param value * the value. * @return the old value mapped to the key, or {@code null}. */ public Object setProperty(String name, String value) { return put(name, value); } private static String lineSeparator; /** * Stores the mappings in this {@code Properties} to the specified {@code * OutputStream}, putting the specified comment at the beginning. The output * from this method is suitable for being read by the * {@link #load(InputStream)} method. * * @param out the {@code OutputStream} to write to. * @param comment the comment to put at the beginning. * @throws IOException if an error occurs during the write to the {@code * OutputStream}. * @throws ClassCastException if the key or value of a mapping is not a * {@code String}. */ public synchronized void store(OutputStream out, String comment) throws IOException { if (lineSeparator == null) { lineSeparator = AccessController .doPrivileged(new PriviAction<String>("line.separator")); //$NON-NLS-1$ } StringBuilder buffer = new StringBuilder(200); OutputStreamWriter writer = new OutputStreamWriter(out, "ISO8859_1"); //$NON-NLS-1$ if (comment != null) { writer.write("#"); //$NON-NLS-1$ writer.write(comment); writer.write(lineSeparator); } writer.write("#"); //$NON-NLS-1$ writer.write(new Date().toString()); writer.write(lineSeparator); for (Map.Entry<Object, Object> entry : entrySet()) { String key = (String) entry.getKey(); dumpString(buffer, key, true); buffer.append('='); dumpString(buffer, (String) entry.getValue(), false); buffer.append(lineSeparator); writer.write(buffer.toString()); buffer.setLength(0); } writer.flush(); } /** * Loads the properties from an {@code InputStream} containing the * properties in XML form. The XML document must begin with (and conform to) * following DOCTYPE: * * <pre> * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> * </pre> * * Also the content of the XML data must satisfy the DTD but the xml is not * validated against it. The DTD is not loaded from the SYSTEM ID. After * this method returns the InputStream is not closed. * * @param in the InputStream containing the XML document. * @throws IOException in case an error occurs during a read operation. * @throws InvalidPropertiesFormatException if the XML data is not a valid * properties file. */ public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException { if (in == null) { throw new NullPointerException(); } if (builder == null) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // BEGIN android-removed // factory.setValidating(true); // END android-removed try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new Error(e); } builder.setErrorHandler(new ErrorHandler() { public void warning(SAXParseException e) throws SAXException { throw e; } public void error(SAXParseException e) throws SAXException { throw e; } public void fatalError(SAXParseException e) throws SAXException { throw e; } }); builder.setEntityResolver(new EntityResolver() { public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { if (systemId.equals(PROP_DTD_NAME)) { InputSource result = new InputSource(new StringReader( PROP_DTD)); result.setSystemId(PROP_DTD_NAME); return result; } throw new SAXException("Invalid DOCTYPE declaration: " + systemId); } }); } try { Document doc = builder.parse(in); NodeList entries = doc.getElementsByTagName("entry"); if (entries == null) { return; } int entriesListLength = entries.getLength(); for (int i = 0; i < entriesListLength; i++) { Element entry = (Element) entries.item(i); String key = entry.getAttribute("key"); // BEGIN android-removed // String value = entry.getTextContent(); // END android-removed // BEGIN android-added String value = getTextContent(entry); // END android-added /* * key != null & value != null but key or(and) value can be * empty String */ put(key, value); } } catch (IOException e) { throw e; } catch (SAXException e) { throw new InvalidPropertiesFormatException(e); } } /** * Writes all properties stored in this instance into the {@code * OutputStream} in XML representation. The DOCTYPE is * * <pre> * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> * </pre> * * If the comment is null, no comment is added to the output. UTF-8 is used * as the encoding. The {@code OutputStream} is not closed at the end. A * call to this method is the same as a call to {@code storeToXML(os, * comment, "UTF-8")}. * * @param os the {@code OutputStream} to write to. * @param comment the comment to add. If null, no comment is added. * @throws IOException if an error occurs during writing to the output. */ public void storeToXML(OutputStream os, String comment) throws IOException { storeToXML(os, comment, "UTF-8"); //$NON-NLS-1$ } /** * Writes all properties stored in this instance into the {@code * OutputStream} in XML representation. The DOCTYPE is * * <pre> * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> * </pre> * * If the comment is null, no comment is added to the output. The parameter * {@code encoding} defines which encoding should be used. The {@code * OutputStream} is not closed at the end. * * @param os the {@code OutputStream} to write to. * @param comment the comment to add. If null, no comment is added. * @param encoding the code identifying the encoding that should be used to * write into the {@code OutputStream}. * @throws IOException if an error occurs during writing to the output. */ public synchronized void storeToXML(OutputStream os, String comment, String encoding) throws IOException { if (os == null || encoding == null) { throw new NullPointerException(); } /* * We can write to XML file using encoding parameter but note that some * aliases for encodings are not supported by the XML parser. Thus we * have to know canonical name for encoding used to store data in XML * since the XML parser must recognize encoding name used to store data. */ String encodingCanonicalName; try { encodingCanonicalName = Charset.forName(encoding).name(); } catch (IllegalCharsetNameException e) { System.out.println("Warning: encoding name " + encoding + " is illegal, using UTF-8 as default encoding"); encodingCanonicalName = "UTF-8"; } catch (UnsupportedCharsetException e) { System.out.println("Warning: encoding " + encoding + " is not supported, using UTF-8 as default encoding"); encodingCanonicalName = "UTF-8"; } PrintStream printStream = new PrintStream(os, false, encodingCanonicalName); printStream.print("<?xml version=\"1.0\" encoding=\""); printStream.print(encodingCanonicalName); printStream.println("\"?>"); printStream.print("<!DOCTYPE properties SYSTEM \""); printStream.print(PROP_DTD_NAME); printStream.println("\">"); printStream.println("<properties>"); if (comment != null) { printStream.print("<comment>"); printStream.print(substitutePredefinedEntries(comment)); printStream.println("</comment>"); } for (Map.Entry<Object, Object> entry : entrySet()) { String keyValue = (String) entry.getKey(); String entryValue = (String) entry.getValue(); printStream.print("<entry key=\""); printStream.print(substitutePredefinedEntries(keyValue)); printStream.print("\">"); printStream.print(substitutePredefinedEntries(entryValue)); printStream.println("</entry>"); } printStream.println("</properties>"); printStream.flush(); } private String substitutePredefinedEntries(String s) { /* * substitution for predefined character entities to use them safely in * XML */ return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll( ">", ">").replaceAll("\u0027", "'").replaceAll("\"", """); } // BEGIN android-added private String getTextContent(Node node) { String result = (node instanceof Text ? ((Text) node).getData() : ""); Node child = node.getFirstChild(); while (child != null) { result = result + getTextContent(child); child = child.getNextSibling(); } return result; } // END android-added }