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 #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 }