/* * plist - An open source library to parse and generate property lists * Copyright (C) 2014 Daniel Dreibrodt * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.dd.plist; import org.w3c.dom.*; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.List; /** * Parses XML property lists. * * @author Daniel Dreibrodt */ public class XMLPropertyListParser { /** * Instantiation is prohibited by outside classes. */ protected XMLPropertyListParser() { /** empty **/ } private static DocumentBuilderFactory docBuilderFactory = null; /** * Initialize the document builder factory so that it can be reused and does not need to * be reinitialized for each parse action. */ private static synchronized void initDocBuilderFactory() { docBuilderFactory = DocumentBuilderFactory.newInstance(); docBuilderFactory.setIgnoringComments(true); docBuilderFactory.setCoalescing(true); } /** * Gets a DocumentBuilder to parse a XML property list. * As DocumentBuilders are not thread-safe a new DocBuilder is generated for each request. * * @return A new DocBuilder that can parse property lists w/o an internet connection. * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list * could not be created. This should not occur. */ private static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException { if (docBuilderFactory == null) initDocBuilderFactory(); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); docBuilder.setEntityResolver(new EntityResolver() { public InputSource resolveEntity(String publicId, String systemId) { if ("-//Apple Computer//DTD PLIST 1.0//EN".equals(publicId) || // older publicId "-//Apple//DTD PLIST 1.0//EN".equals(publicId)) { // newer publicId // return a dummy, zero length DTD so we don't have to fetch // it from the network. return new InputSource(new ByteArrayInputStream(new byte[0])); } return null; } }); return docBuilder; } /** * Parses a XML property list file. * * @param f The XML property list file. * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list * could not be created. This should not occur. * @throws java.io.IOException If any IO error occurs while reading the file. * @throws org.xml.sax.SAXException If any parse error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. * @throws java.text.ParseException If a date string could not be parsed. */ public static NSObject parse(File f) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { DocumentBuilder docBuilder = getDocBuilder(); Document doc = docBuilder.parse(f); return parse(doc); } /** * Parses a XML property list from a byte array. * * @param bytes The byte array containing the property list's data. * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray. * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list * could not be created. This should not occur. * @throws java.io.IOException If any IO error occurs while reading the file. * @throws org.xml.sax.SAXException If any parse error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. * @throws java.text.ParseException If a date string could not be parsed. */ public static NSObject parse(final byte[] bytes) throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException { ByteArrayInputStream bis = new ByteArrayInputStream(bytes); return parse(bis); } /** * Parses a XML property list from an input stream. * * @param is The input stream pointing to the property list's data. * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list * could not be created. This should not occur. * @throws java.io.IOException If any IO error occurs while reading the file. * @throws org.xml.sax.SAXException If any parse error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. * @throws java.text.ParseException If a date string could not be parsed. */ public static NSObject parse(InputStream is) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { DocumentBuilder docBuilder = getDocBuilder(); Document doc = docBuilder.parse(is); return parse(doc); } /** * Parses a property list from an XML document. * * @param doc The XML document. * @return The root NSObject of the property list contained in the XML document. * @throws java.io.IOException If any IO error occurs while reading the file. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. * @throws java.text.ParseException If a date string could not be parsed. */ public static NSObject parse(Document doc) throws PropertyListFormatException, IOException, ParseException { DocumentType docType = doc.getDoctype(); if (docType == null) { if (!doc.getDocumentElement().getNodeName().equals("plist")) { throw new UnsupportedOperationException("The given XML document is not a property list."); } } else if (!docType.getName().equals("plist")) { throw new UnsupportedOperationException("The given XML document is not a property list."); } Node rootNode; if (doc.getDocumentElement().getNodeName().equals("plist")) { //Root element wrapped in plist tag List<Node> rootNodes = filterElementNodes(doc.getDocumentElement().getChildNodes()); if (rootNodes.isEmpty()) { throw new PropertyListFormatException("The given XML property list has no root element!"); } else if (rootNodes.size() == 1) { rootNode = rootNodes.get(0); } else { throw new PropertyListFormatException("The given XML property list has more than one root element!"); } } else { //Root NSObject not wrapped in plist-tag rootNode = doc.getDocumentElement(); } return parseObject(rootNode); } /** * Parses a node in the XML structure and returns the corresponding NSObject * * @param n The XML node. * @return The corresponding NSObject. * @throws java.io.IOException If any IO error occurs while parsing a Base64 encoded NSData object. * @throws java.text.ParseException If a date string could not be parsed. */ private static NSObject parseObject(Node n) throws ParseException, IOException { String type = n.getNodeName(); if (type.equals("dict")) { NSDictionary dict = new NSDictionary(); List<Node> children = filterElementNodes(n.getChildNodes()); for (int i = 0; i < children.size(); i += 2) { Node key = children.get(i); Node val = children.get(i + 1); String keyString = getNodeTextContents(key); dict.put(keyString, parseObject(val)); } return dict; } else if (type.equals("array")) { List<Node> children = filterElementNodes(n.getChildNodes()); NSArray array = new NSArray(children.size()); for (int i = 0; i < children.size(); i++) { array.setValue(i, parseObject(children.get(i))); } return array; } else if (type.equals("true")) { return new NSNumber(true); } else if (type.equals("false")) { return new NSNumber(false); } else if (type.equals("integer")) { return new NSNumber(getNodeTextContents(n)); } else if (type.equals("real")) { return new NSNumber(getNodeTextContents(n)); } else if (type.equals("string")) { return new NSString(getNodeTextContents(n)); } else if (type.equals("data")) { return new NSData(getNodeTextContents(n)); } else if (type.equals("date")) { return new NSDate(getNodeTextContents(n)); } return null; } /** * Returns all element nodes that are contained in a list of nodes. * * @param list The list of nodes to search. * @return The sub-list containing only nodes representing actual elements. */ private static List<Node> filterElementNodes(NodeList list) { List<Node> result = new ArrayList<Node>(list.getLength()); for (int i = 0; i < list.getLength(); i++) { if (list.item(i).getNodeType() == Node.ELEMENT_NODE) { result.add(list.item(i)); } } return result; } /** * Returns a node's text content. * This method will return the text value represented by the node's direct children. * If the given node is a TEXT or CDATA node, then its value is returned. * * @param n The node. * @return The node's text content. */ private static String getNodeTextContents(Node n) { if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) { Text txtNode = (Text) n; String content = txtNode.getWholeText(); //This concatenates any adjacent text/cdata/entity nodes if (content == null) return ""; else return content; } else { if (n.hasChildNodes()) { NodeList children = n.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { //Skip any non-text nodes, like comments or entities Node child = children.item(i); if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) { Text txtNode = (Text) child; String content = txtNode.getWholeText(); //This concatenates any adjacent text/cdata/entity nodes if (content == null) return ""; else return content; } } return ""; } else { return ""; } } } }