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