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