XmlSupport.java revision 44d29b0a9012a66f5db5a8130017568a55c11668
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 * Copyright (c) 2002, 2006, 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
27package java.util.prefs;
28
29import java.util.*;
30import java.io.*;
31import javax.xml.parsers.*;
32import javax.xml.transform.*;
33import javax.xml.transform.dom.*;
34import javax.xml.transform.stream.*;
35import org.xml.sax.*;
36import 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 */
46class 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 #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 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", ((Preferences)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 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 i = map.entrySet().iterator(); i.hasNext(); ) {
374            Map.Entry e = (Map.Entry) i.next();
375            Element xe = (Element)
376                xmlMap.appendChild(doc.createElement("entry"));
377            xe.setAttribute("key",   (String) e.getKey());
378            xe.setAttribute("value", (String) 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 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