1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 2002, 2006, 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 #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 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", ((Preferences)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 map)368 static void exportMap(OutputStream os, Map 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 i = map.entrySet().iterator(); i.hasNext(); ) { 374 Map.Entry e = (Map.Entry) i.next(); 375 Element xe = (Element) 376 xmlMap.appendChild(doc.createElement("entry")); 377 xe.setAttribute("key", (String) e.getKey()); 378 xe.setAttribute("value", (String) 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 m)397 static void importMap(InputStream is, Map 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