001 /*
002 * Copyright 2002-2006 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025
026 package java.util.prefs;
027
028 import java.util.*;
029 import java.io.*;
030 import javax.xml.parsers.*;
031 import javax.xml.transform.*;
032 import javax.xml.transform.dom.*;
033 import javax.xml.transform.stream.*;
034 import org.xml.sax.*;
035 import org.w3c.dom.*;
036
037 /**
038 * XML Support for java.util.prefs. Methods to import and export preference
039 * nodes and subtrees.
040 *
041 * @author Josh Bloch and Mark Reinhold
042 * @version 1.17, 05/05/07
043 * @see Preferences
044 * @since 1.4
045 */
046 class XmlSupport {
047 // The required DTD URI for exported preferences
048 private static final String PREFS_DTD_URI = "http://java.sun.com/dtd/preferences.dtd";
049
050 // The actual DTD corresponding to the URI
051 private static final String PREFS_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
052 +
053
054 "<!-- DTD for preferences -->"
055 +
056
057 "<!ELEMENT preferences (root) >"
058 + "<!ATTLIST preferences"
059 + " EXTERNAL_XML_VERSION CDATA \"0.0\" >"
060 +
061
062 "<!ELEMENT root (map, node*) >"
063 + "<!ATTLIST root"
064 + " type (system|user) #REQUIRED >"
065 +
066
067 "<!ELEMENT node (map, node*) >"
068 + "<!ATTLIST node"
069 + " name CDATA #REQUIRED >"
070 +
071
072 "<!ELEMENT map (entry*) >"
073 + "<!ATTLIST map"
074 + " MAP_XML_VERSION CDATA \"0.0\" >"
075 + "<!ELEMENT entry EMPTY >"
076 + "<!ATTLIST entry"
077 + " key CDATA #REQUIRED"
078 + " value CDATA #REQUIRED >";
079 /**
080 * Version number for the format exported preferences files.
081 */
082 private static final String EXTERNAL_XML_VERSION = "1.0";
083
084 /*
085 * Version number for the internal map files.
086 */
087 private static final String MAP_XML_VERSION = "1.0";
088
089 /**
090 * Export the specified preferences node and, if subTree is true, all
091 * subnodes, to the specified output stream. Preferences are exported as
092 * an XML document conforming to the definition in the Preferences spec.
093 *
094 * @throws IOException if writing to the specified output stream
095 * results in an <tt>IOException</tt>.
096 * @throws BackingStoreException if preference data cannot be read from
097 * backing store.
098 * @throws IllegalStateException if this node (or an ancestor) has been
099 * removed with the {@link #removeNode()} method.
100 */
101 static void export(OutputStream os, final Preferences p,
102 boolean subTree) throws IOException, BackingStoreException {
103 if (((AbstractPreferences) p).isRemoved())
104 throw new IllegalStateException("Node has been removed");
105 Document doc = createPrefsDoc("preferences");
106 Element preferences = doc.getDocumentElement();
107 preferences.setAttribute("EXTERNAL_XML_VERSION",
108 EXTERNAL_XML_VERSION);
109 Element xmlRoot = (Element) preferences.appendChild(doc
110 .createElement("root"));
111 xmlRoot.setAttribute("type", (p.isUserNode() ? "user"
112 : "system"));
113
114 // Get bottom-up list of nodes from p to root, excluding root
115 List ancestors = new ArrayList();
116
117 for (Preferences kid = p, dad = kid.parent(); dad != null; kid = dad, dad = kid
118 .parent()) {
119 ancestors.add(kid);
120 }
121 Element e = xmlRoot;
122 for (int i = ancestors.size() - 1; i >= 0; i--) {
123 e.appendChild(doc.createElement("map"));
124 e = (Element) e.appendChild(doc.createElement("node"));
125 e.setAttribute("name", ((Preferences) ancestors.get(i))
126 .name());
127 }
128 putPreferencesInXml(e, doc, p, subTree);
129
130 writeDoc(doc, os);
131 }
132
133 /**
134 * Put the preferences in the specified Preferences node into the
135 * specified XML element which is assumed to represent a node
136 * in the specified XML document which is assumed to conform to
137 * PREFS_DTD. If subTree is true, create children of the specified
138 * XML node conforming to all of the children of the specified
139 * Preferences node and recurse.
140 *
141 * @throws BackingStoreException if it is not possible to read
142 * the preferences or children out of the specified
143 * preferences node.
144 */
145 private static void putPreferencesInXml(Element elt, Document doc,
146 Preferences prefs, boolean subTree)
147 throws BackingStoreException {
148 Preferences[] kidsCopy = null;
149 String[] kidNames = null;
150
151 // Node is locked to export its contents and get a
152 // copy of children, then lock is released,
153 // and, if subTree = true, recursive calls are made on children
154 synchronized (((AbstractPreferences) prefs).lock) {
155 // Check if this node was concurrently removed. If yes
156 // remove it from XML Document and return.
157 if (((AbstractPreferences) prefs).isRemoved()) {
158 elt.getParentNode().removeChild(elt);
159 return;
160 }
161 // Put map in xml element
162 String[] keys = prefs.keys();
163 Element map = (Element) elt.appendChild(doc
164 .createElement("map"));
165 for (int i = 0; i < keys.length; i++) {
166 Element entry = (Element) map.appendChild(doc
167 .createElement("entry"));
168 entry.setAttribute("key", keys[i]);
169 // NEXT STATEMENT THROWS NULL PTR EXC INSTEAD OF ASSERT FAIL
170 entry.setAttribute("value", prefs.get(keys[i], null));
171 }
172 // Recurse if appropriate
173 if (subTree) {
174 /* Get a copy of kids while lock is held */
175 kidNames = prefs.childrenNames();
176 kidsCopy = new Preferences[kidNames.length];
177 for (int i = 0; i < kidNames.length; i++)
178 kidsCopy[i] = prefs.node(kidNames[i]);
179 }
180 // release lock
181 }
182
183 if (subTree) {
184 for (int i = 0; i < kidNames.length; i++) {
185 Element xmlKid = (Element) elt.appendChild(doc
186 .createElement("node"));
187 xmlKid.setAttribute("name", kidNames[i]);
188 putPreferencesInXml(xmlKid, doc, kidsCopy[i], subTree);
189 }
190 }
191 }
192
193 /**
194 * Import preferences from the specified input stream, which is assumed
195 * to contain an XML document in the format described in the Preferences
196 * spec.
197 *
198 * @throws IOException if reading from the specified output stream
199 * results in an <tt>IOException</tt>.
200 * @throws InvalidPreferencesFormatException Data on input stream does not
201 * constitute a valid XML document with the mandated document type.
202 */
203 static void importPreferences(InputStream is) throws IOException,
204 InvalidPreferencesFormatException {
205 try {
206 Document doc = loadPrefsDoc(is);
207 String xmlVersion = doc.getDocumentElement().getAttribute(
208 "EXTERNAL_XML_VERSION");
209 if (xmlVersion.compareTo(EXTERNAL_XML_VERSION) > 0)
210 throw new InvalidPreferencesFormatException(
211 "Exported preferences file format version "
212 + xmlVersion
213 + " is not supported. This java installation can read"
214 + " versions " + EXTERNAL_XML_VERSION
215 + " or older. You may need"
216 + " to install a newer version of JDK.");
217
218 Element xmlRoot = (Element) doc.getDocumentElement()
219 .getChildNodes().item(0);
220 Preferences prefsRoot = (xmlRoot.getAttribute("type")
221 .equals("user") ? Preferences.userRoot()
222 : Preferences.systemRoot());
223 ImportSubtree(prefsRoot, xmlRoot);
224 } catch (SAXException e) {
225 throw new InvalidPreferencesFormatException(e);
226 }
227 }
228
229 /**
230 * Create a new prefs XML document.
231 */
232 private static Document createPrefsDoc(String qname) {
233 try {
234 DOMImplementation di = DocumentBuilderFactory.newInstance()
235 .newDocumentBuilder().getDOMImplementation();
236 DocumentType dt = di.createDocumentType(qname, null,
237 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 */
248 private static Document loadPrefsDoc(InputStream in)
249 throws SAXException, IOException {
250 DocumentBuilderFactory dbf = DocumentBuilderFactory
251 .newInstance();
252 dbf.setIgnoringElementContentWhitespace(true);
253 dbf.setValidating(true);
254 dbf.setCoalescing(true);
255 dbf.setIgnoringComments(true);
256 try {
257 DocumentBuilder db = dbf.newDocumentBuilder();
258 db.setEntityResolver(new Resolver());
259 db.setErrorHandler(new EH());
260 return db.parse(new InputSource(in));
261 } catch (ParserConfigurationException e) {
262 throw new AssertionError(e);
263 }
264 }
265
266 /**
267 * Write XML document to the specified output stream.
268 */
269 private static final void writeDoc(Document doc, OutputStream out)
270 throws IOException {
271 try {
272 TransformerFactory tf = TransformerFactory.newInstance();
273 try {
274 tf.setAttribute("indent-number", new Integer(2));
275 } catch (IllegalArgumentException iae) {
276 //Ignore the IAE. Should not fail the writeout even the
277 //transformer provider does not support "indent-number".
278 }
279 Transformer t = tf.newTransformer();
280 t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc
281 .getDoctype().getSystemId());
282 t.setOutputProperty(OutputKeys.INDENT, "yes");
283 //Transformer resets the "indent" info if the "result" is a StreamResult with
284 //an OutputStream object embedded, creating a Writer object on top of that
285 //OutputStream object however works.
286 t.transform(new DOMSource(doc), new StreamResult(
287 new BufferedWriter(new OutputStreamWriter(out,
288 "UTF-8"))));
289 } catch (TransformerException e) {
290 throw new AssertionError(e);
291 }
292 }
293
294 /**
295 * Recursively traverse the specified preferences node and store
296 * the described preferences into the system or current user
297 * preferences tree, as appropriate.
298 */
299 private static void ImportSubtree(Preferences prefsNode,
300 Element xmlNode) {
301 NodeList xmlKids = xmlNode.getChildNodes();
302 int numXmlKids = xmlKids.getLength();
303 /*
304 * We first lock the node, import its contents and get
305 * child nodes. Then we unlock the node and go to children
306 * Since some of the children might have been concurrently
307 * deleted we check for this.
308 */
309 Preferences[] prefsKids;
310 /* Lock the node */
311 synchronized (((AbstractPreferences) prefsNode).lock) {
312 //If removed, return silently
313 if (((AbstractPreferences) prefsNode).isRemoved())
314 return;
315
316 // Import any preferences at this node
317 Element firstXmlKid = (Element) xmlKids.item(0);
318 ImportPrefs(prefsNode, firstXmlKid);
319 prefsKids = new Preferences[numXmlKids - 1];
320
321 // Get involved children
322 for (int i = 1; i < numXmlKids; i++) {
323 Element xmlKid = (Element) xmlKids.item(i);
324 prefsKids[i - 1] = prefsNode.node(xmlKid
325 .getAttribute("name"));
326 }
327 } // unlocked the node
328 // import children
329 for (int i = 1; i < numXmlKids; i++)
330 ImportSubtree(prefsKids[i - 1], (Element) xmlKids.item(i));
331 }
332
333 /**
334 * Import the preferences described by the specified XML element
335 * (a map from a preferences document) into the specified
336 * preferences node.
337 */
338 private static void ImportPrefs(Preferences prefsNode, Element map) {
339 NodeList entries = map.getChildNodes();
340 for (int i = 0, numEntries = entries.getLength(); i < numEntries; i++) {
341 Element entry = (Element) entries.item(i);
342 prefsNode.put(entry.getAttribute("key"), entry
343 .getAttribute("value"));
344 }
345 }
346
347 /**
348 * Export the specified Map<String,String> to a map document on
349 * the specified OutputStream as per the prefs DTD. This is used
350 * as the internal (undocumented) format for FileSystemPrefs.
351 *
352 * @throws IOException if writing to the specified output stream
353 * results in an <tt>IOException</tt>.
354 */
355 static void exportMap(OutputStream os, Map map) throws IOException {
356 Document doc = createPrefsDoc("map");
357 Element xmlMap = doc.getDocumentElement();
358 xmlMap.setAttribute("MAP_XML_VERSION", MAP_XML_VERSION);
359
360 for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
361 Map.Entry e = (Map.Entry) i.next();
362 Element xe = (Element) xmlMap.appendChild(doc
363 .createElement("entry"));
364 xe.setAttribute("key", (String) e.getKey());
365 xe.setAttribute("value", (String) e.getValue());
366 }
367
368 writeDoc(doc, os);
369 }
370
371 /**
372 * Import Map from the specified input stream, which is assumed
373 * to contain a map document as per the prefs DTD. This is used
374 * as the internal (undocumented) format for FileSystemPrefs. The
375 * key-value pairs specified in the XML document will be put into
376 * the specified Map. (If this Map is empty, it will contain exactly
377 * the key-value pairs int the XML-document when this method returns.)
378 *
379 * @throws IOException if reading from the specified output stream
380 * results in an <tt>IOException</tt>.
381 * @throws InvalidPreferencesFormatException Data on input stream does not
382 * constitute a valid XML document with the mandated document type.
383 */
384 static void importMap(InputStream is, Map m) throws IOException,
385 InvalidPreferencesFormatException {
386 try {
387 Document doc = loadPrefsDoc(is);
388 Element xmlMap = doc.getDocumentElement();
389 // check version
390 String mapVersion = xmlMap.getAttribute("MAP_XML_VERSION");
391 if (mapVersion.compareTo(MAP_XML_VERSION) > 0)
392 throw new InvalidPreferencesFormatException(
393 "Preferences map file format version "
394 + mapVersion
395 + " is not supported. This java installation can read"
396 + " versions " + MAP_XML_VERSION
397 + " or older. You may need"
398 + " to install a newer version of JDK.");
399
400 NodeList entries = xmlMap.getChildNodes();
401 for (int i = 0, numEntries = entries.getLength(); i < numEntries; i++) {
402 Element entry = (Element) entries.item(i);
403 m.put(entry.getAttribute("key"), entry
404 .getAttribute("value"));
405 }
406 } catch (SAXException e) {
407 throw new InvalidPreferencesFormatException(e);
408 }
409 }
410
411 private static class Resolver implements EntityResolver {
412 public InputSource resolveEntity(String pid, String sid)
413 throws SAXException {
414 if (sid.equals(PREFS_DTD_URI)) {
415 InputSource is;
416 is = new InputSource(new StringReader(PREFS_DTD));
417 is.setSystemId(PREFS_DTD_URI);
418 return is;
419 }
420 throw new SAXException("Invalid system identifier: " + sid);
421 }
422 }
423
424 private static class EH implements ErrorHandler {
425 public void error(SAXParseException x) throws SAXException {
426 throw x;
427 }
428
429 public void fatalError(SAXParseException x) throws SAXException {
430 throw x;
431 }
432
433 public void warning(SAXParseException x) throws SAXException {
434 throw x;
435 }
436 }
437 }
|