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      */
export(OutputStream os, final Preferences p, boolean subTree)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      */
putPreferencesInXml(Element elt, Document doc, Preferences prefs, boolean subTree)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      */
importPreferences(InputStream is)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      */
createPrefsDoc( String qname )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      */
loadPrefsDoc(InputStream in)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      */
writeDoc(Document doc, OutputStream out)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 
getChildElements(Element node)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      */
ImportSubtree(Preferences prefsNode, Element xmlNode)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      */
ImportPrefs(Preferences prefsNode, Element map)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      */
exportMap(OutputStream os, Map<String, String> map)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      */
importMap(InputStream is, Map<String, String> m)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 {
resolveEntity(String pid, String sid)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 {
error(SAXParseException x)443         public void error(SAXParseException x) throws SAXException {
444             throw x;
445         }
fatalError(SAXParseException x)446         public void fatalError(SAXParseException x) throws SAXException {
447             throw x;
448         }
warning(SAXParseException x)449         public void warning(SAXParseException x) throws SAXException {
450             throw x;
451         }
452     }
453 }
454