1 /*
   2  * Copyright (c) 2002, 2006, 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 }