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             // BEGIN Android-changed: Filter out non-Element nodes.
211             // Use a selector to skip over CDATA / DATA elements.
212             // The selector is specific to children with tag name "root";
213             // export() always creates exactly one such child.
214             // Element xmlRoot = (Element) doc.getDocumentElement().
215             //                                    getChildNodes().item(0);
216             Element xmlRoot = (Element) doc.getDocumentElement();
217 
218             NodeList elements = xmlRoot.getElementsByTagName("root");
219             if (elements == null || elements.getLength() != 1) {
220                 throw new InvalidPreferencesFormatException("invalid root node");
221             }
222 
223             xmlRoot = (Element) elements.item(0);
224             // END Android-changed: Filter out non-Element nodes.
225             Preferences prefsRoot =
226                 (xmlRoot.getAttribute("type").equals("user") ?
227                             Preferences.userRoot() : Preferences.systemRoot());
228             ImportSubtree(prefsRoot, xmlRoot);
229         } catch(SAXException e) {
230             throw new InvalidPreferencesFormatException(e);
231         }
232     }
233 
234     /**
235      * Create a new prefs XML document.
236      */
createPrefsDoc( String qname )237     private static Document createPrefsDoc( String qname ) {
238         try {
239             DOMImplementation di = DocumentBuilderFactory.newInstance().
240                 newDocumentBuilder().getDOMImplementation();
241             DocumentType dt = di.createDocumentType(qname, null, PREFS_DTD_URI);
242             return di.createDocument(null, qname, dt);
243         } catch(ParserConfigurationException e) {
244             throw new AssertionError(e);
245         }
246     }
247 
248     /**
249      * Load an XML document from specified input stream, which must
250      * have the requisite DTD URI.
251      */
loadPrefsDoc(InputStream in)252     private static Document loadPrefsDoc(InputStream in)
253         throws SAXException, IOException
254     {
255         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
256         dbf.setIgnoringElementContentWhitespace(true);
257         // Android-changed: No validating builder implementation.
258         // dbf.setValidating(true);
259         dbf.setCoalescing(true);
260         dbf.setIgnoringComments(true);
261         try {
262             DocumentBuilder db = dbf.newDocumentBuilder();
263             db.setEntityResolver(new Resolver());
264             db.setErrorHandler(new EH());
265             return db.parse(new InputSource(in));
266         } catch (ParserConfigurationException e) {
267             throw new AssertionError(e);
268         }
269     }
270 
271     /**
272      * Write XML document to the specified output stream.
273      */
writeDoc(Document doc, OutputStream out)274     private static final void writeDoc(Document doc, OutputStream out)
275         throws IOException
276     {
277         try {
278             TransformerFactory tf = TransformerFactory.newInstance();
279             try {
280                 tf.setAttribute("indent-number", new Integer(2));
281             } catch (IllegalArgumentException iae) {
282                 //Ignore the IAE. Should not fail the writeout even the
283                 //transformer provider does not support "indent-number".
284             }
285             Transformer t = tf.newTransformer();
286             t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
287             t.setOutputProperty(OutputKeys.INDENT, "yes");
288             //Transformer resets the "indent" info if the "result" is a StreamResult with
289             //an OutputStream object embedded, creating a Writer object on top of that
290             //OutputStream object however works.
291             t.transform(new DOMSource(doc),
292                         new StreamResult(new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))));
293         } catch(TransformerException e) {
294             throw new AssertionError(e);
295         }
296     }
297 
298     // BEGIN Android-added: Filter out non-Element nodes.
299     static class NodeListAdapter implements NodeList {
300         private final List<? extends Node> delegate;
301 
NodeListAdapter(List<? extends Node> delegate)302         public NodeListAdapter(List<? extends Node> delegate) {
303             this.delegate = Objects.requireNonNull(delegate);
304         }
305 
item(int index)306         @Override public Node item(int index) {
307             if (index < 0 || index >= delegate.size()) {
308                 return null;
309             }
310             return delegate.get(index);
311         }
getLength()312         @Override public int getLength() { return delegate.size(); }
313     }
314 
elementNodesOf(NodeList xmlKids)315     private static NodeList elementNodesOf(NodeList xmlKids) {
316         List<Element> elements = new ArrayList<>(xmlKids.getLength());
317         for (int i = 0; i < xmlKids.getLength(); ++i) {
318             Node node = xmlKids.item(i);
319             if (node instanceof Element) {
320                 elements.add((Element) node);
321             }
322         }
323         return new NodeListAdapter(elements);
324     }
325     // END Android-added: Filter out non-Element nodes.
326 
327     /**
328      * Recursively traverse the specified preferences node and store
329      * the described preferences into the system or current user
330      * preferences tree, as appropriate.
331      */
ImportSubtree(Preferences prefsNode, Element xmlNode)332     private static void ImportSubtree(Preferences prefsNode, Element xmlNode) {
333         NodeList xmlKids = xmlNode.getChildNodes();
334         // Android-added: Filter out non-Element nodes.
335         xmlKids = elementNodesOf(xmlKids);
336         int numXmlKids = xmlKids.getLength();
337         /*
338          * We first lock the node, import its contents and get
339          * child nodes. Then we unlock the node and go to children
340          * Since some of the children might have been concurrently
341          * deleted we check for this.
342          */
343         Preferences[] prefsKids;
344         /* Lock the node */
345         synchronized (((AbstractPreferences)prefsNode).lock) {
346             //If removed, return silently
347             if (((AbstractPreferences)prefsNode).isRemoved())
348                 return;
349 
350             // Import any preferences at this node
351             Element firstXmlKid = (Element) xmlKids.item(0);
352             ImportPrefs(prefsNode, firstXmlKid);
353             prefsKids = new Preferences[numXmlKids - 1];
354 
355             // Get involved children
356             for (int i=1; i < numXmlKids; i++) {
357                 Element xmlKid = (Element) xmlKids.item(i);
358                 prefsKids[i-1] = prefsNode.node(xmlKid.getAttribute("name"));
359             }
360         } // unlocked the node
361         // import children
362         for (int i=1; i < numXmlKids; i++)
363             ImportSubtree(prefsKids[i-1], (Element)xmlKids.item(i));
364     }
365 
366     /**
367      * Import the preferences described by the specified XML element
368      * (a map from a preferences document) into the specified
369      * preferences node.
370      */
ImportPrefs(Preferences prefsNode, Element map)371     private static void ImportPrefs(Preferences prefsNode, Element map) {
372         NodeList entries = map.getChildNodes();
373         // Android-added: Filter out non-Element nodes.
374         entries = elementNodesOf(entries);
375         for (int i=0, numEntries = entries.getLength(); i < numEntries; i++) {
376             Element entry = (Element) entries.item(i);
377             prefsNode.put(entry.getAttribute("key"),
378                           entry.getAttribute("value"));
379         }
380     }
381 
382     /**
383      * Export the specified Map<String,String> to a map document on
384      * the specified OutputStream as per the prefs DTD.  This is used
385      * as the internal (undocumented) format for FileSystemPrefs.
386      *
387      * @throws IOException if writing to the specified output stream
388      *         results in an <tt>IOException</tt>.
389      */
exportMap(OutputStream os, Map<String, String> map)390     static void exportMap(OutputStream os, Map<String, String> map) throws IOException {
391         Document doc = createPrefsDoc("map");
392         Element xmlMap = doc.getDocumentElement( ) ;
393         xmlMap.setAttribute("MAP_XML_VERSION", MAP_XML_VERSION);
394 
395         for (Iterator<Map.Entry<String, String>> i = map.entrySet().iterator(); i.hasNext(); ) {
396             Map.Entry<String, String> e = i.next();
397             Element xe = (Element)
398                 xmlMap.appendChild(doc.createElement("entry"));
399             xe.setAttribute("key",   e.getKey());
400             xe.setAttribute("value", e.getValue());
401         }
402 
403         writeDoc(doc, os);
404     }
405 
406     /**
407      * Import Map from the specified input stream, which is assumed
408      * to contain a map document as per the prefs DTD.  This is used
409      * as the internal (undocumented) format for FileSystemPrefs.  The
410      * key-value pairs specified in the XML document will be put into
411      * the specified Map.  (If this Map is empty, it will contain exactly
412      * the key-value pairs int the XML-document when this method returns.)
413      *
414      * @throws IOException if reading from the specified output stream
415      *         results in an <tt>IOException</tt>.
416      * @throws InvalidPreferencesFormatException Data on input stream does not
417      *         constitute a valid XML document with the mandated document type.
418      */
importMap(InputStream is, Map<String, String> m)419     static void importMap(InputStream is, Map<String, String> m)
420         throws IOException, InvalidPreferencesFormatException
421     {
422         try {
423             Document doc = loadPrefsDoc(is);
424             Element xmlMap = doc.getDocumentElement();
425             // check version
426             String mapVersion = xmlMap.getAttribute("MAP_XML_VERSION");
427             if (mapVersion.compareTo(MAP_XML_VERSION) > 0)
428                 throw new InvalidPreferencesFormatException(
429                 "Preferences map file format version " + mapVersion +
430                 " is not supported. This java installation can read" +
431                 " versions " + MAP_XML_VERSION + " or older. You may need" +
432                 " to install a newer version of JDK.");
433 
434             NodeList entries = xmlMap.getChildNodes();
435             for (int i=0, numEntries=entries.getLength(); i<numEntries; i++) {
436                 // BEGIN Android-added: Filter out non-Element nodes.
437                 // Android xml serializer generates one-char Text nodes with a single
438                 // new-line character between expected Element nodes. OpenJDK code wasn't
439                 // expecting anything else than Element nodes.
440                 if (!(entries.item(i) instanceof Element)) {
441                     continue;
442                 }
443                 // END Android-added: Filter out non-Element nodes.
444                 Element entry = (Element) entries.item(i);
445                 m.put(entry.getAttribute("key"), entry.getAttribute("value"));
446             }
447         } catch(SAXException e) {
448             throw new InvalidPreferencesFormatException(e);
449         }
450     }
451 
452     private static class Resolver implements EntityResolver {
resolveEntity(String pid, String sid)453         public InputSource resolveEntity(String pid, String sid)
454             throws SAXException
455         {
456             if (sid.equals(PREFS_DTD_URI)) {
457                 InputSource is;
458                 is = new InputSource(new StringReader(PREFS_DTD));
459                 is.setSystemId(PREFS_DTD_URI);
460                 return is;
461             }
462             throw new SAXException("Invalid system identifier: " + sid);
463         }
464     }
465 
466     private static class EH implements ErrorHandler {
error(SAXParseException x)467         public void error(SAXParseException x) throws SAXException {
468             throw x;
469         }
fatalError(SAXParseException x)470         public void fatalError(SAXParseException x) throws SAXException {
471             throw x;
472         }
warning(SAXParseException x)473         public void warning(SAXParseException x) throws SAXException {
474             throw x;
475         }
476     }
477 }
478