Home | History | Annotate | Download | only in prefs
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  * Copyright (c) 2002, 2012, Oracle and/or its affiliates. All rights reserved.
      4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      5  *
      6  * This code is free software; you can redistribute it and/or modify it
      7  * under the terms of the GNU General Public License version 2 only, as
      8  * published by the Free Software Foundation.  Oracle designates this
      9  * particular file as subject to the "Classpath" exception as provided
     10  * by Oracle in the LICENSE file that accompanied this code.
     11  *
     12  * This code is distributed in the hope that it will be useful, but WITHOUT
     13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     15  * version 2 for more details (a copy is included in the LICENSE file that
     16  * accompanied this code).
     17  *
     18  * You should have received a copy of the GNU General Public License version
     19  * 2 along with this work; if not, write to the Free Software Foundation,
     20  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     21  *
     22  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     23  * or visit www.oracle.com if you need additional information or have any
     24  * questions.
     25  */
     26 
     27 package java.util.prefs;
     28 
     29 import java.util.*;
     30 import java.io.*;
     31 import javax.xml.parsers.*;
     32 import javax.xml.transform.*;
     33 import javax.xml.transform.dom.*;
     34 import javax.xml.transform.stream.*;
     35 import org.xml.sax.*;
     36 import org.w3c.dom.*;
     37 
     38 /**
     39  * XML Support for java.util.prefs. Methods to import and export preference
     40  * nodes and subtrees.
     41  *
     42  * @author  Josh Bloch and Mark Reinhold
     43  * @see     Preferences
     44  * @since   1.4
     45  */
     46 class XmlSupport {
     47     // The required DTD URI for exported preferences
     48     private static final String PREFS_DTD_URI =
     49         "http://java.sun.com/dtd/preferences.dtd";
     50 
     51     // The actual DTD corresponding to the URI
     52     private static final String PREFS_DTD =
     53         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
     54 
     55         "<!-- DTD for preferences -->"               +
     56 
     57         "<!ELEMENT preferences (root) >"             +
     58         "<!ATTLIST preferences"                      +
     59         " EXTERNAL_XML_VERSION CDATA \"0.0\"  >"     +
     60 
     61         "<!ELEMENT root (map, node*) >"              +
     62         "<!ATTLIST root"                             +
     63         "          type (system|user) #REQUIRED >"   +
     64 
     65         "<!ELEMENT node (map, node*) >"              +
     66         "<!ATTLIST node"                             +
     67         "          name CDATA #REQUIRED >"           +
     68 
     69         "<!ELEMENT map (entry*) >"                   +
     70         "<!ATTLIST map"                              +
     71         "  MAP_XML_VERSION CDATA \"0.0\"  >"         +
     72         "<!ELEMENT entry EMPTY >"                    +
     73         "<!ATTLIST entry"                            +
     74         "          key CDATA #REQUIRED"              +
     75         "          value CDATA #REQUIRED >"          ;
     76     /**
     77      * Version number for the format exported preferences files.
     78      */
     79     private static final String EXTERNAL_XML_VERSION = "1.0";
     80 
     81     /*
     82      * Version number for the internal map files.
     83      */
     84     private static final String MAP_XML_VERSION = "1.0";
     85 
     86     /**
     87      * Export the specified preferences node and, if subTree is true, all
     88      * subnodes, to the specified output stream.  Preferences are exported as
     89      * an XML document conforming to the definition in the Preferences spec.
     90      *
     91      * @throws IOException if writing to the specified output stream
     92      *         results in an <tt>IOException</tt>.
     93      * @throws BackingStoreException if preference data cannot be read from
     94      *         backing store.
     95      * @throws IllegalStateException if this node (or an ancestor) has been
     96      *         removed with the {@link Preferences#removeNode()} method.
     97      */
     98     static void export(OutputStream os, final Preferences p, boolean subTree)
     99         throws IOException, BackingStoreException {
    100         if (((AbstractPreferences)p).isRemoved())
    101             throw new IllegalStateException("Node has been removed");
    102         Document doc = createPrefsDoc("preferences");
    103         Element preferences =  doc.getDocumentElement() ;
    104         preferences.setAttribute("EXTERNAL_XML_VERSION", EXTERNAL_XML_VERSION);
    105         Element xmlRoot =  (Element)
    106         preferences.appendChild(doc.createElement("root"));
    107         xmlRoot.setAttribute("type", (p.isUserNode() ? "user" : "system"));
    108 
    109         // Get bottom-up list of nodes from p to root, excluding root
    110         List<Preferences> ancestors = new ArrayList<>();
    111 
    112         for (Preferences kid = p, dad = kid.parent(); dad != null;
    113                                    kid = dad, dad = kid.parent()) {
    114             ancestors.add(kid);
    115         }
    116         Element e = xmlRoot;
    117         for (int i=ancestors.size()-1; i >= 0; i--) {
    118             e.appendChild(doc.createElement("map"));
    119             e = (Element) e.appendChild(doc.createElement("node"));
    120             e.setAttribute("name", ancestors.get(i).name());
    121         }
    122         putPreferencesInXml(e, doc, p, subTree);
    123 
    124         writeDoc(doc, os);
    125     }
    126 
    127     /**
    128      * Put the preferences in the specified Preferences node into the
    129      * specified XML element which is assumed to represent a node
    130      * in the specified XML document which is assumed to conform to
    131      * PREFS_DTD.  If subTree is true, create children of the specified
    132      * XML node conforming to all of the children of the specified
    133      * Preferences node and recurse.
    134      *
    135      * @throws BackingStoreException if it is not possible to read
    136      *         the preferences or children out of the specified
    137      *         preferences node.
    138      */
    139     private static void putPreferencesInXml(Element elt, Document doc,
    140                Preferences prefs, boolean subTree) throws BackingStoreException
    141     {
    142         Preferences[] kidsCopy = null;
    143         String[] kidNames = null;
    144 
    145         // Node is locked to export its contents and get a
    146         // copy of children, then lock is released,
    147         // and, if subTree = true, recursive calls are made on children
    148         synchronized (((AbstractPreferences)prefs).lock) {
    149             // Check if this node was concurrently removed. If yes
    150             // remove it from XML Document and return.
    151             if (((AbstractPreferences)prefs).isRemoved()) {
    152                 elt.getParentNode().removeChild(elt);
    153                 return;
    154             }
    155             // Put map in xml element
    156             String[] keys = prefs.keys();
    157             Element map = (Element) elt.appendChild(doc.createElement("map"));
    158             for (int i=0; i<keys.length; i++) {
    159                 Element entry = (Element)
    160                     map.appendChild(doc.createElement("entry"));
    161                 entry.setAttribute("key", keys[i]);
    162                 // NEXT STATEMENT THROWS NULL PTR EXC INSTEAD OF ASSERT FAIL
    163                 entry.setAttribute("value", prefs.get(keys[i], null));
    164             }
    165             // Recurse if appropriate
    166             if (subTree) {
    167                 /* Get a copy of kids while lock is held */
    168                 kidNames = prefs.childrenNames();
    169                 kidsCopy = new Preferences[kidNames.length];
    170                 for (int i = 0; i <  kidNames.length; i++)
    171                     kidsCopy[i] = prefs.node(kidNames[i]);
    172             }
    173             // release lock
    174         }
    175 
    176         if (subTree) {
    177             for (int i=0; i < kidNames.length; i++) {
    178                 Element xmlKid = (Element)
    179                     elt.appendChild(doc.createElement("node"));
    180                 xmlKid.setAttribute("name", kidNames[i]);
    181                 putPreferencesInXml(xmlKid, doc, kidsCopy[i], subTree);
    182             }
    183         }
    184     }
    185 
    186     /**
    187      * Import preferences from the specified input stream, which is assumed
    188      * to contain an XML document in the format described in the Preferences
    189      * spec.
    190      *
    191      * @throws IOException if reading from the specified output stream
    192      *         results in an <tt>IOException</tt>.
    193      * @throws InvalidPreferencesFormatException Data on input stream does not
    194      *         constitute a valid XML document with the mandated document type.
    195      */
    196     static void importPreferences(InputStream is)
    197         throws IOException, InvalidPreferencesFormatException
    198     {
    199         try {
    200             Document doc = loadPrefsDoc(is);
    201             String xmlVersion =
    202                 doc.getDocumentElement().getAttribute("EXTERNAL_XML_VERSION");
    203             if (xmlVersion.compareTo(EXTERNAL_XML_VERSION) > 0)
    204                 throw new InvalidPreferencesFormatException(
    205                 "Exported preferences file format version " + xmlVersion +
    206                 " is not supported. This java installation can read" +
    207                 " versions " + EXTERNAL_XML_VERSION + " or older. You may need" +
    208                 " to install a newer version of JDK.");
    209 
    210             Element xmlRoot = (Element) doc.getDocumentElement();
    211 
    212             // Android-changed: Use a selector to skip over CDATA / DATA elements.
    213             NodeList elements = xmlRoot.getElementsByTagName("root");
    214             if (elements == null || elements.getLength() != 1) {
    215                 throw new InvalidPreferencesFormatException("invalid root node");
    216             }
    217 
    218             xmlRoot = (Element) elements.item(0);
    219             // End android changes.
    220 
    221             Preferences prefsRoot =
    222                 (xmlRoot.getAttribute("type").equals("user") ?
    223                             Preferences.userRoot() : Preferences.systemRoot());
    224             ImportSubtree(prefsRoot, xmlRoot);
    225         } catch(SAXException e) {
    226             throw new InvalidPreferencesFormatException(e);
    227         }
    228     }
    229 
    230     /**
    231      * Create a new prefs XML document.
    232      */
    233     private static Document createPrefsDoc( String qname ) {
    234         try {
    235             DOMImplementation di = DocumentBuilderFactory.newInstance().
    236                 newDocumentBuilder().getDOMImplementation();
    237             DocumentType dt = di.createDocumentType(qname, null, PREFS_DTD_URI);
    238             return di.createDocument(null, qname, dt);
    239         } catch(ParserConfigurationException e) {
    240             throw new AssertionError(e);
    241         }
    242     }
    243 
    244     /**
    245      * Load an XML document from specified input stream, which must
    246      * have the requisite DTD URI.
    247      */
    248     private static Document loadPrefsDoc(InputStream in)
    249         throws SAXException, IOException
    250     {
    251         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    252         dbf.setIgnoringElementContentWhitespace(true);
    253         // Android-changed: No validating builder implementation.
    254         // dbf.setValidating(true);
    255         dbf.setCoalescing(true);
    256         dbf.setIgnoringComments(true);
    257         try {
    258             DocumentBuilder db = dbf.newDocumentBuilder();
    259             db.setEntityResolver(new Resolver());
    260             db.setErrorHandler(new EH());
    261             return db.parse(new InputSource(in));
    262         } catch (ParserConfigurationException e) {
    263             throw new AssertionError(e);
    264         }
    265     }
    266 
    267     /**
    268      * Write XML document to the specified output stream.
    269      */
    270     private static final void writeDoc(Document doc, OutputStream out)
    271         throws IOException
    272     {
    273         try {
    274             TransformerFactory tf = TransformerFactory.newInstance();
    275             try {
    276                 tf.setAttribute("indent-number", new Integer(2));
    277             } catch (IllegalArgumentException iae) {
    278                 //Ignore the IAE. Should not fail the writeout even the
    279                 //transformer provider does not support "indent-number".
    280             }
    281             Transformer t = tf.newTransformer();
    282             t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
    283             t.setOutputProperty(OutputKeys.INDENT, "yes");
    284 
    285             //Transformer resets the "indent" info if the "result" is a StreamResult with
    286             //an OutputStream object embedded, creating a Writer object on top of that
    287             //OutputStream object however works.
    288             t.transform(new DOMSource(doc),
    289                     new StreamResult(new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))));
    290         } catch(TransformerException e) {
    291             throw new AssertionError(e);
    292         }
    293     }
    294 
    295     private static List<Element> getChildElements(Element node) {
    296         NodeList xmlKids = node.getChildNodes();
    297         ArrayList<Element> elements = new ArrayList<>(xmlKids.getLength());
    298         for (int i = 0; i < xmlKids.getLength(); ++i) {
    299             if (xmlKids.item(i) instanceof Element) {
    300                 elements.add((Element) xmlKids.item(i));
    301             }
    302         }
    303 
    304         return elements;
    305     }
    306 
    307     /**
    308      * Recursively traverse the specified preferences node and store
    309      * the described preferences into the system or current user
    310      * preferences tree, as appropriate.
    311      */
    312     private static void ImportSubtree(Preferences prefsNode, Element xmlNode) {
    313         // Android-changed: filter out non-element nodes.
    314         List<Element> xmlKids = getChildElements(xmlNode);
    315 
    316         /*
    317          * We first lock the node, import its contents and get
    318          * child nodes. Then we unlock the node and go to children
    319          * Since some of the children might have been concurrently
    320          * deleted we check for this.
    321          */
    322         Preferences[] prefsKids;
    323         /* Lock the node */
    324         synchronized (((AbstractPreferences)prefsNode).lock) {
    325             //If removed, return silently
    326             if (((AbstractPreferences)prefsNode).isRemoved())
    327                 return;
    328 
    329             // Import any preferences at this node
    330             // Android
    331             Element firstXmlKid = xmlKids.get(0);
    332             ImportPrefs(prefsNode, firstXmlKid);
    333             prefsKids = new Preferences[xmlKids.size() - 1];
    334 
    335             // Get involved children
    336             for (int i=1; i < xmlKids.size(); i++) {
    337                 Element xmlKid = xmlKids.get(i);
    338                 prefsKids[i-1] = prefsNode.node(xmlKid.getAttribute("name"));
    339             }
    340         } // unlocked the node
    341         // import children
    342         for (int i=1; i < xmlKids.size(); i++)
    343             ImportSubtree(prefsKids[i-1], xmlKids.get(i));
    344     }
    345 
    346     /**
    347      * Import the preferences described by the specified XML element
    348      * (a map from a preferences document) into the specified
    349      * preferences node.
    350      */
    351     private static void ImportPrefs(Preferences prefsNode, Element map) {
    352         // Android-changed: Use getChildElements.
    353         List<Element> entries = getChildElements(map);
    354         for (int i=0, numEntries = entries.size(); i < numEntries; i++) {
    355             Element entry = entries.get(i);
    356             prefsNode.put(entry.getAttribute("key"), entry.getAttribute("value"));
    357         }
    358     }
    359 
    360     /**
    361      * Export the specified Map<String,String> to a map document on
    362      * the specified OutputStream as per the prefs DTD.  This is used
    363      * as the internal (undocumented) format for FileSystemPrefs.
    364      *
    365      * @throws IOException if writing to the specified output stream
    366      *         results in an <tt>IOException</tt>.
    367      */
    368     static void exportMap(OutputStream os, Map<String, String> map) throws IOException {
    369         Document doc = createPrefsDoc("map");
    370         Element xmlMap = doc.getDocumentElement( ) ;
    371         xmlMap.setAttribute("MAP_XML_VERSION", MAP_XML_VERSION);
    372 
    373         for (Iterator<Map.Entry<String, String>> i = map.entrySet().iterator(); i.hasNext(); ) {
    374             Map.Entry<String, String> e = i.next();
    375             Element xe = (Element)
    376                 xmlMap.appendChild(doc.createElement("entry"));
    377             xe.setAttribute("key",   e.getKey());
    378             xe.setAttribute("value", e.getValue());
    379         }
    380 
    381         writeDoc(doc, os);
    382     }
    383 
    384     /**
    385      * Import Map from the specified input stream, which is assumed
    386      * to contain a map document as per the prefs DTD.  This is used
    387      * as the internal (undocumented) format for FileSystemPrefs.  The
    388      * key-value pairs specified in the XML document will be put into
    389      * the specified Map.  (If this Map is empty, it will contain exactly
    390      * the key-value pairs int the XML-document when this method returns.)
    391      *
    392      * @throws IOException if reading from the specified output stream
    393      *         results in an <tt>IOException</tt>.
    394      * @throws InvalidPreferencesFormatException Data on input stream does not
    395      *         constitute a valid XML document with the mandated document type.
    396      */
    397     static void importMap(InputStream is, Map<String, String> m)
    398         throws IOException, InvalidPreferencesFormatException
    399     {
    400         try {
    401             Document doc = loadPrefsDoc(is);
    402             Element xmlMap = doc.getDocumentElement();
    403             // check version
    404             String mapVersion = xmlMap.getAttribute("MAP_XML_VERSION");
    405             if (mapVersion.compareTo(MAP_XML_VERSION) > 0)
    406                 throw new InvalidPreferencesFormatException(
    407                 "Preferences map file format version " + mapVersion +
    408                 " is not supported. This java installation can read" +
    409                 " versions " + MAP_XML_VERSION + " or older. You may need" +
    410                 " to install a newer version of JDK.");
    411 
    412             NodeList entries = xmlMap.getChildNodes();
    413             for (int i=0, numEntries=entries.getLength(); i<numEntries; i++) {
    414                 // Android-added, android xml serializer generates one-char Text nodes with a single
    415                 // new-line character between expected Element nodes. openJdk code wasn't
    416                 // expecting anything else than Element node.
    417                 if (!(entries.item(i) instanceof Element)) {
    418                     continue;
    419                 }
    420                 Element entry = (Element) entries.item(i);
    421                 m.put(entry.getAttribute("key"), entry.getAttribute("value"));
    422             }
    423         } catch(SAXException e) {
    424             throw new InvalidPreferencesFormatException(e);
    425         }
    426     }
    427 
    428     private static class Resolver implements EntityResolver {
    429         public InputSource resolveEntity(String pid, String sid)
    430             throws SAXException
    431         {
    432             if (sid.equals(PREFS_DTD_URI)) {
    433                 InputSource is;
    434                 is = new InputSource(new StringReader(PREFS_DTD));
    435                 is.setSystemId(PREFS_DTD_URI);
    436                 return is;
    437             }
    438             throw new SAXException("Invalid system identifier: " + sid);
    439         }
    440     }
    441 
    442     private static class EH implements ErrorHandler {
    443         public void error(SAXParseException x) throws SAXException {
    444             throw x;
    445         }
    446         public void fatalError(SAXParseException x) throws SAXException {
    447             throw x;
    448         }
    449         public void warning(SAXParseException x) throws SAXException {
    450             throw x;
    451         }
    452     }
    453 }
    454