1. /*
  2. * @(#)XmlSupport.java 1.11 02/02/06
  3. *
  4. * Copyright 2003 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 org.xml.sax.*;
  12. import org.w3c.dom.*;
  13. import org.apache.crimson.tree.*;
  14. /**
  15. * XML Support for java.util.prefs. Methods to import and export preference
  16. * nodes and subtrees.
  17. *
  18. * @author Josh Bloch and Mark Reinhold
  19. * @version 1.11, 02/06/02
  20. * @see Preferences
  21. * @since 1.4
  22. */
  23. class XmlSupport {
  24. // The required DTD URI for exported preferences
  25. private static final String PREFS_DTD_URI =
  26. "http://java.sun.com/dtd/preferences.dtd";
  27. // The actual DTD corresponding to the URI
  28. private static final String PREFS_DTD =
  29. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  30. "<!-- DTD for preferences -->" +
  31. "<!ELEMENT preferences (root) >" +
  32. "<!ATTLIST preferences" +
  33. " EXTERNAL_XML_VERSION CDATA \"0.0\" >" +
  34. "<!ELEMENT root (map, node*) >" +
  35. "<!ATTLIST root" +
  36. " type (system|user) #REQUIRED >" +
  37. "<!ELEMENT node (map, node*) >" +
  38. "<!ATTLIST node" +
  39. " name CDATA #REQUIRED >" +
  40. "<!ELEMENT map (entry*) >" +
  41. "<!ATTLIST map" +
  42. " MAP_XML_VERSION CDATA \"0.0\" >" +
  43. "<!ELEMENT entry EMPTY >" +
  44. "<!ATTLIST entry" +
  45. " key CDATA #REQUIRED" +
  46. " value CDATA #REQUIRED >" ;
  47. /**
  48. * Version number for the format exported preferences files.
  49. */
  50. private static final String EXTERNAL_XML_VERSION = "1.0";
  51. /*
  52. * Version number for the internal map files.
  53. */
  54. private static final String MAP_XML_VERSION = "1.0";
  55. /**
  56. * Export the specified preferences node and, if subTree is true, all
  57. * subnodes, to the specified output stream. Preferences are exported as
  58. * an XML document conforming to the definition in the Preferences spec.
  59. *
  60. * @throws IOException if writing to the specified output stream
  61. * results in an <tt>IOException</tt>.
  62. * @throws BackingStoreException if preference data cannot be read from
  63. * backing store.
  64. * @throws IllegalStateException if this node (or an ancestor) has been
  65. * removed with the {@link #removeNode()} method.
  66. */
  67. static void export(OutputStream os, final Preferences p, boolean subTree)
  68. throws IOException, BackingStoreException {
  69. if (((AbstractPreferences)p).isRemoved())
  70. throw new IllegalStateException("Node has been removed");
  71. XmlDocument doc = new XmlDocument();
  72. doc.setDoctype(null, PREFS_DTD_URI, null);
  73. Element preferences = (Element)
  74. doc.appendChild(doc.createElement("preferences"));
  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. doc.write(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 = load(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. * Load an XML document from specified input stream, which must
  185. * have the requisite DTD URI.
  186. */
  187. private static Document load(InputStream in)
  188. throws SAXException, IOException
  189. {
  190. Resolver r = new Resolver();
  191. DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  192. dbf.setIgnoringElementContentWhitespace(true);
  193. dbf.setValidating(true);
  194. dbf.setCoalescing(true);
  195. dbf.setIgnoringComments(true);
  196. try {
  197. DocumentBuilder db = dbf.newDocumentBuilder();
  198. db.setEntityResolver(new Resolver());
  199. db.setErrorHandler(new EH());
  200. return db.parse(new InputSource(in));
  201. } catch (ParserConfigurationException x) {
  202. throw new Error(x);
  203. }
  204. }
  205. /**
  206. * Recursively traverse the specified preferences node and store
  207. * the described preferences into the system or current user
  208. * preferences tree, as appropriate.
  209. */
  210. private static void ImportSubtree(Preferences prefsNode, Element xmlNode) {
  211. NodeList xmlKids = xmlNode.getChildNodes();
  212. int numXmlKids = xmlKids.getLength();
  213. /*
  214. * We first lock the node, import its contents and get
  215. * child nodes. Then we unlock the node and go to children
  216. * Since some of the children might have been concurrently
  217. * deleted we check for this.
  218. */
  219. Preferences[] prefsKids;
  220. /* Lock the node */
  221. synchronized (((AbstractPreferences)prefsNode).lock) {
  222. //If removed, return silently
  223. if (((AbstractPreferences)prefsNode).isRemoved())
  224. return;
  225. // Import any preferences at this node
  226. Element firstXmlKid = (Element) xmlKids.item(0);
  227. ImportPrefs(prefsNode, firstXmlKid);
  228. prefsKids = new Preferences[numXmlKids - 1];
  229. // Get involved children
  230. for (int i=1; i < numXmlKids; i++) {
  231. Element xmlKid = (Element) xmlKids.item(i);
  232. prefsKids[i-1] = prefsNode.node(xmlKid.getAttribute("name"));
  233. }
  234. } // unlocked the node
  235. // import children
  236. for (int i=1; i < numXmlKids; i++)
  237. ImportSubtree(prefsKids[i-1], (Element)xmlKids.item(i));
  238. }
  239. /**
  240. * Import the preferences described by the specified XML element
  241. * (a map from a preferences document) into the specified
  242. * preferences node.
  243. */
  244. private static void ImportPrefs(Preferences prefsNode, Element map) {
  245. NodeList entries = map.getChildNodes();
  246. for (int i=0, numEntries = entries.getLength(); i < numEntries; i++) {
  247. Element entry = (Element) entries.item(i);
  248. prefsNode.put(entry.getAttribute("key"),
  249. entry.getAttribute("value"));
  250. }
  251. }
  252. /**
  253. * Export the specified Map<String,String> to a map document on
  254. * the specified OutputStream as per the prefs DTD. This is used
  255. * as the internal (undocumented) format for FileSystemPrefs.
  256. *
  257. * @throws IOException if writing to the specified output stream
  258. * results in an <tt>IOException</tt>.
  259. */
  260. static void exportMap(OutputStream os, Map map) throws IOException {
  261. XmlDocument doc = new XmlDocument();
  262. doc.setDoctype(null, PREFS_DTD_URI, null);
  263. Element xmlMap = (Element) doc.appendChild(doc.createElement("map"));
  264. xmlMap.setAttribute("MAP_XML_VERSION", MAP_XML_VERSION);
  265. for (Iterator i = map.entrySet().iterator(); i.hasNext(); ) {
  266. Map.Entry e = (Map.Entry) i.next();
  267. Element xe = (Element)
  268. xmlMap.appendChild(doc.createElement("entry"));
  269. xe.setAttribute("key", (String) e.getKey());
  270. xe.setAttribute("value", (String) e.getValue());
  271. }
  272. doc.write(os);
  273. }
  274. /**
  275. * Import Map from the specified input stream, which is assumed
  276. * to contain a map document as per the prefs DTD. This is used
  277. * as the internal (undocumented) format for FileSystemPrefs. The
  278. * key-value pairs specified in the XML document will be put into
  279. * the specified Map. (If this Map is empty, it will contain exactly
  280. * the key-value pairs int the XML-document when this method returns.)
  281. *
  282. * @throws IOException if reading from the specified output stream
  283. * results in an <tt>IOException</tt>.
  284. * @throws InvalidPreferencesFormatException Data on input stream does not
  285. * constitute a valid XML document with the mandated document type.
  286. */
  287. static void importMap(InputStream is, Map m)
  288. throws IOException, InvalidPreferencesFormatException
  289. {
  290. try {
  291. Document doc = load(is);
  292. Element xmlMap = (Element) doc.getChildNodes().item(1);
  293. // check version
  294. String mapVersion = xmlMap.getAttribute("MAP_XML_VERSION");
  295. if (mapVersion.compareTo(MAP_XML_VERSION) > 0)
  296. throw new InvalidPreferencesFormatException(
  297. "Preferences map file format version " + mapVersion +
  298. " is not supported. This java installation can read" +
  299. " versions " + MAP_XML_VERSION + " or older. You may need" +
  300. " to install a newer version of JDK.");
  301. NodeList entries = xmlMap.getChildNodes();
  302. for (int i=0, numEntries=entries.getLength(); i<numEntries; i++) {
  303. Element entry = (Element) entries.item(i);
  304. m.put(entry.getAttribute("key"), entry.getAttribute("value"));
  305. }
  306. } catch(SAXException e) {
  307. throw new InvalidPreferencesFormatException(e);
  308. }
  309. }
  310. private static class Resolver implements EntityResolver {
  311. public InputSource resolveEntity(String pid, String sid)
  312. throws SAXException
  313. {
  314. if (sid.equals(PREFS_DTD_URI)) {
  315. InputSource is;
  316. is = new InputSource(new StringReader(PREFS_DTD));
  317. is.setSystemId(PREFS_DTD_URI);
  318. return is;
  319. }
  320. throw new SAXException("Invalid system identifier: " + sid);
  321. }
  322. }
  323. private static class EH implements ErrorHandler {
  324. public void error(SAXParseException x) throws SAXException {
  325. throw x;
  326. }
  327. public void fatalError(SAXParseException x) throws SAXException {
  328. throw x;
  329. }
  330. public void warning(SAXParseException x) throws SAXException {
  331. throw x;
  332. }
  333. }
  334. }