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