| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package java.util.prefs; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.util.ArrayList; |
| import java.util.Properties; |
| import java.util.StringTokenizer; |
| import java.util.UUID; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.FactoryConfigurationError; |
| import javax.xml.parsers.ParserConfigurationException; |
| import libcore.io.IoUtils; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.EntityResolver; |
| import org.xml.sax.ErrorHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXParseException; |
| |
| /** |
| * Utility class for the Preferences import/export from XML file. |
| */ |
| class XMLParser { |
| |
| /* |
| * Constant - the specified DTD URL |
| */ |
| static final String PREFS_DTD_NAME = "http://java.sun.com/dtd/preferences.dtd"; |
| |
| /* |
| * Constant - the DTD string |
| */ |
| static final String PREFS_DTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" |
| + " <!ELEMENT preferences (root)>" |
| + " <!ATTLIST preferences EXTERNAL_XML_VERSION CDATA \"0.0\" >" |
| + " <!ELEMENT root (map, node*) >" |
| + " <!ATTLIST root type (system|user) #REQUIRED >" |
| + " <!ELEMENT node (map, node*) >" |
| + " <!ATTLIST node name CDATA #REQUIRED >" |
| + " <!ELEMENT map (entry*) >" |
| + " <!ELEMENT entry EMPTY >" |
| + " <!ATTLIST entry key CDATA #REQUIRED value CDATA #REQUIRED >"; |
| |
| /* |
| * Constant - the specified header |
| */ |
| static final String HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; |
| |
| /* |
| * Constant - the specified DOCTYPE |
| */ |
| static final String DOCTYPE = "<!DOCTYPE preferences SYSTEM"; |
| |
| /* |
| * Constant - used by FilePreferencesImpl, which is default implementation of Linux platform |
| */ |
| private static final String FILE_PREFS = "<!DOCTYPE map SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>"; |
| |
| /* |
| * Constant - specify the DTD version |
| */ |
| private static final float XML_VERSION = 1.0f; |
| |
| /* |
| * DOM builder |
| */ |
| private static final DocumentBuilder builder; |
| |
| /* |
| * specify the indent level |
| */ |
| private static int indent = -1; |
| |
| /* |
| * init DOM builder |
| */ |
| static { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setValidating(false); |
| try { |
| builder = factory.newDocumentBuilder(); |
| } catch (ParserConfigurationException e) { |
| throw new Error(e); |
| } |
| builder.setEntityResolver(new EntityResolver() { |
| public InputSource resolveEntity(String publicId, String systemId) |
| throws SAXException, IOException { |
| if (systemId.equals(PREFS_DTD_NAME)) { |
| InputSource result = new InputSource(new StringReader( |
| PREFS_DTD)); |
| result.setSystemId(PREFS_DTD_NAME); |
| return result; |
| } |
| throw new SAXException("Invalid DOCTYPE declaration " + systemId); |
| } |
| }); |
| builder.setErrorHandler(new ErrorHandler() { |
| public void warning(SAXParseException e) throws SAXException { |
| throw e; |
| } |
| |
| public void error(SAXParseException e) throws SAXException { |
| throw e; |
| } |
| |
| public void fatalError(SAXParseException e) throws SAXException { |
| throw e; |
| } |
| }); |
| } |
| |
| private XMLParser() {// empty constructor |
| } |
| |
| /*************************************************************************** |
| * utilities for Preferences export |
| **************************************************************************/ |
| static void exportPrefs(Preferences prefs, OutputStream stream, |
| boolean withSubTree) throws IOException, BackingStoreException { |
| indent = -1; |
| BufferedWriter out = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8")); |
| out.write(HEADER); |
| out.newLine(); |
| out.newLine(); |
| |
| out.write(DOCTYPE); |
| out.write(" '"); |
| out.write(PREFS_DTD_NAME); |
| out.write("'>"); |
| out.newLine(); |
| out.newLine(); |
| |
| flushStartTag("preferences", new String[] { "EXTERNAL_XML_VERSION" }, |
| new String[] { String.valueOf(XML_VERSION) }, out); |
| flushStartTag("root", new String[] { "type" }, |
| new String[] { prefs.isUserNode() ? "user" : "system" }, out); |
| flushEmptyElement("map", out); |
| |
| StringTokenizer ancestors = new StringTokenizer(prefs.absolutePath(), "/"); |
| exportNode(ancestors, prefs, withSubTree, out); |
| |
| flushEndTag("root", out); |
| flushEndTag("preferences", out); |
| out.flush(); |
| out = null; |
| } |
| |
| private static void exportNode(StringTokenizer ancestors, |
| Preferences prefs, boolean withSubTree, BufferedWriter out) |
| throws IOException, BackingStoreException { |
| if (ancestors.hasMoreTokens()) { |
| String name = ancestors.nextToken(); |
| flushStartTag("node", new String[] { "name" }, new String[] { name }, out); |
| if (ancestors.hasMoreTokens()) { |
| flushEmptyElement("map", out); |
| exportNode(ancestors, prefs, withSubTree, out); |
| } else { |
| exportEntries(prefs, out); |
| if (withSubTree) { |
| exportSubTree(prefs, out); |
| } |
| } |
| flushEndTag("node", out); |
| } |
| } |
| |
| private static void exportSubTree(Preferences prefs, BufferedWriter out) |
| throws BackingStoreException, IOException { |
| String[] names = prefs.childrenNames(); |
| if (names.length > 0) { |
| for (int i = 0; i < names.length; i++) { |
| Preferences child = prefs.node(names[i]); |
| flushStartTag("node", new String[] { "name" }, new String[] { names[i] }, out); |
| exportEntries(child, out); |
| exportSubTree(child, out); |
| flushEndTag("node", out); |
| } |
| } |
| } |
| |
| private static void exportEntries(Preferences prefs, BufferedWriter out) |
| throws BackingStoreException, IOException { |
| String[] keys = prefs.keys(); |
| String[] values = new String[keys.length]; |
| for (int i = 0; i < keys.length; i++) { |
| values[i] = prefs.get(keys[i], null); |
| } |
| exportEntries(keys, values, out); |
| } |
| |
| private static void exportEntries(String[] keys, String[] values, |
| BufferedWriter out) throws IOException { |
| if (keys.length == 0) { |
| flushEmptyElement("map", out); |
| return; |
| } |
| flushStartTag("map", out); |
| for (int i = 0; i < keys.length; i++) { |
| if (values[i] != null) { |
| flushEmptyElement("entry", new String[] { "key", "value" }, |
| new String[] { keys[i], values[i] }, out); |
| } |
| } |
| flushEndTag("map", out); |
| } |
| |
| private static void flushEndTag(String tagName, BufferedWriter out) |
| throws IOException { |
| flushIndent(indent--, out); |
| out.write("</"); |
| out.write(tagName); |
| out.write(">"); |
| out.newLine(); |
| } |
| |
| private static void flushEmptyElement(String tagName, BufferedWriter out) |
| throws IOException { |
| flushIndent(++indent, out); |
| out.write("<"); |
| out.write(tagName); |
| out.write(" />"); |
| out.newLine(); |
| indent--; |
| } |
| |
| private static void flushEmptyElement(String tagName, String[] attrKeys, |
| String[] attrValues, BufferedWriter out) throws IOException { |
| flushIndent(++indent, out); |
| out.write("<"); |
| out.write(tagName); |
| flushPairs(attrKeys, attrValues, out); |
| out.write(" />"); |
| out.newLine(); |
| indent--; |
| } |
| |
| private static void flushPairs(String[] attrKeys, String[] attrValues, |
| BufferedWriter out) throws IOException { |
| for (int i = 0; i < attrKeys.length; i++) { |
| out.write(" "); |
| out.write(attrKeys[i]); |
| out.write("=\""); |
| out.write(htmlEncode(attrValues[i])); |
| out.write("\""); |
| } |
| } |
| |
| private static void flushIndent(int ind, BufferedWriter out) |
| throws IOException { |
| for (int i = 0; i < ind; i++) { |
| out.write(" "); |
| } |
| } |
| |
| private static void flushStartTag(String tagName, String[] attrKeys, |
| String[] attrValues, BufferedWriter out) throws IOException { |
| flushIndent(++indent, out); |
| out.write("<"); |
| out.write(tagName); |
| flushPairs(attrKeys, attrValues, out); |
| out.write(">"); |
| out.newLine(); |
| } |
| |
| private static void flushStartTag(String tagName, BufferedWriter out) |
| throws IOException { |
| flushIndent(++indent, out); |
| out.write("<"); |
| out.write(tagName); |
| out.write(">"); |
| out.newLine(); |
| } |
| |
| private static String htmlEncode(String s) { |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < s.length(); i++) { |
| char c = s.charAt(i); |
| switch (c) { |
| case '<': |
| sb.append("<"); |
| break; |
| case '>': |
| sb.append(">"); |
| break; |
| case '&': |
| sb.append("&"); |
| break; |
| case '"': |
| sb.append("""); |
| break; |
| default: |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /*************************************************************************** |
| * utilities for Preferences import |
| **************************************************************************/ |
| static void importPrefs(InputStream in) throws IOException, InvalidPreferencesFormatException { |
| try { |
| // load XML document |
| Document doc = builder.parse(new InputSource(in)); |
| |
| // check preferences' export version |
| Element preferences; |
| preferences = doc.getDocumentElement(); |
| String version = preferences.getAttribute("EXTERNAL_XML_VERSION"); |
| if (version != null && Float.parseFloat(version) > XML_VERSION) { |
| throw new InvalidPreferencesFormatException("Preferences version " + version + |
| " is not supported"); |
| } |
| |
| // check preferences root's type |
| Element root = (Element) preferences |
| .getElementsByTagName("root").item(0); |
| Preferences prefsRoot = null; |
| String type = root.getAttribute("type"); |
| if (type.equals("user")) { |
| prefsRoot = Preferences.userRoot(); |
| } else { |
| prefsRoot = Preferences.systemRoot(); |
| } |
| |
| // load node |
| loadNode(prefsRoot, root); |
| } catch (FactoryConfigurationError e) { |
| throw new InvalidPreferencesFormatException(e); |
| } catch (SAXException e) { |
| throw new InvalidPreferencesFormatException(e); |
| } |
| } |
| |
| private static void loadNode(Preferences prefs, Element node) { |
| // load preferences |
| NodeList children = selectNodeList(node, "node"); |
| NodeList entries = selectNodeList(node, "map/entry"); |
| int childNumber = children.getLength(); |
| Preferences[] prefChildren = new Preferences[childNumber]; |
| int entryNumber = entries.getLength(); |
| synchronized (((AbstractPreferences) prefs).lock) { |
| if (((AbstractPreferences) prefs).isRemoved()) { |
| return; |
| } |
| for (int i = 0; i < entryNumber; i++) { |
| Element entry = (Element) entries.item(i); |
| String key = entry.getAttribute("key"); |
| String value = entry.getAttribute("value"); |
| prefs.put(key, value); |
| } |
| // get children preferences node |
| for (int i = 0; i < childNumber; i++) { |
| Element child = (Element) children.item(i); |
| String name = child.getAttribute("name"); |
| prefChildren[i] = prefs.node(name); |
| } |
| } |
| |
| // load children nodes after unlock |
| for (int i = 0; i < childNumber; i++) { |
| loadNode(prefChildren[i], (Element) children.item(i)); |
| } |
| } |
| |
| // TODO dirty implementation of a method from javax.xml.xpath |
| // should be replaced with a call to a good impl of this method |
| private static NodeList selectNodeList(Element documentElement, String string) { |
| |
| NodeList result = null; |
| |
| ArrayList<Node> input = new ArrayList<Node>(); |
| |
| String[] path = string.split("/"); |
| |
| NodeList childNodes = documentElement.getChildNodes(); |
| |
| if(path[0].equals("entry") || path[0].equals("node")) { |
| for (int i = 0; i < childNodes.getLength(); i++) { |
| Object next = childNodes.item(i); |
| if(next instanceof Element) { |
| if(((Element) next).getNodeName().equals(path[0])) { |
| input.add((Node)next); |
| } |
| } |
| } |
| } else if(path[0].equals("map") && path[1].equals("entry")) { |
| for (int i = 0; i < childNodes.getLength(); i++) { |
| Object next = childNodes.item(i); |
| if(next instanceof Element) { |
| if(((Element) next).getNodeName().equals(path[0])) { |
| NodeList nextChildNodes = ((Node)next).getChildNodes(); |
| for (int j = 0; j < nextChildNodes.getLength(); j++) { |
| Object subnext = nextChildNodes.item(j); |
| if(subnext instanceof Element) { |
| if(((Element)subnext).getNodeName().equals(path[1])) { |
| input.add((Node)subnext); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| result = new NodeSet(input.iterator()); |
| |
| return result; |
| } |
| |
| /** |
| * Returns the preferences from {@code xmlFile}. Returns empty properties if |
| * any errors occur. |
| */ |
| static Properties readXmlPreferences(File xmlFile) { |
| Properties result = new Properties(); |
| if (!xmlFile.exists()) { |
| xmlFile.getParentFile().mkdirs(); |
| } else if (xmlFile.canRead()) { |
| Reader reader = null; |
| try { |
| reader = new InputStreamReader(new FileInputStream(xmlFile), "UTF-8"); |
| Document document = builder.parse(new InputSource(reader)); |
| NodeList entries = selectNodeList(document.getDocumentElement(), "entry"); |
| int length = entries.getLength(); |
| for (int i = 0; i < length; i++) { |
| Element node = (Element) entries.item(i); |
| String key = node.getAttribute("key"); |
| String value = node.getAttribute("value"); |
| result.setProperty(key, value); |
| } |
| } catch (IOException ignored) { |
| } catch (SAXException ignored) { |
| } finally { |
| IoUtils.closeQuietly(reader); |
| } |
| } else { |
| // the prefs API requires this to be hostile towards pre-existing files |
| xmlFile.delete(); |
| } |
| return result; |
| } |
| |
| /** |
| * Writes the preferences to {@code xmlFile}. |
| */ |
| static void writeXmlPreferences(File xmlFile, Properties properties) throws IOException { |
| File parent = xmlFile.getParentFile(); |
| File temporaryForWriting = new File(parent, "prefs-" + UUID.randomUUID() + ".xml.tmp"); |
| |
| BufferedWriter out = null; |
| try { |
| out = new BufferedWriter(new OutputStreamWriter( |
| new FileOutputStream(temporaryForWriting), "UTF-8")); |
| out.write(HEADER); |
| out.newLine(); |
| out.write(FILE_PREFS); |
| out.newLine(); |
| String[] keys = properties.keySet().toArray(new String[properties.size()]); |
| int length = keys.length; |
| String[] values = new String[length]; |
| for (int i = 0; i < length; i++) { |
| values[i] = properties.getProperty(keys[i]); |
| } |
| exportEntries(keys, values, out); |
| out.close(); |
| if (!temporaryForWriting.renameTo(xmlFile)) { |
| throw new IOException("Failed to write preferences to " + xmlFile); |
| } |
| } finally { |
| IoUtils.closeQuietly(out); |
| temporaryForWriting.delete(); // no-op unless something failed |
| } |
| } |
| } |