com.github.markusbernhardt.proxy.util.PListParser Maven / Gradle / Ivy
package com.github.markusbernhardt.proxy.util;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Plist xml handling (serialization and deserialization)
*
* The xml plist dtd can be found at http://www.apple.com/DTDs/PropertyList-1.0.dtd
*
* The plist spec handles 8 types of objects: booleans, real, integers, dates,
* binary data, strings, arrays (lists) and dictionaries (maps).
*
* The java Plist lib handles converting xml plists to a nested
* {@code Map} that can be trivially read from java. It also
* provides a simple way to convert a nested {@code Map} into an
* xml plist representation.
*
* The following mapping will be done when converting from plist to Map
* :
*
*
* true/false -> Boolean
* real -> Double
* integer -> Integer/Long (depends on size, values exceeding an int will be rendered as longs)
* data -> byte[]
* string -> String
* array -> List
* dict -> Map
*
*
* When converting from Map -> plist the conversion is as follows:
*
*
* Boolean -> true/false
* Float/Double -> real
* Byte/Short/Integer/Long -> integer
* byte[] -> data
* List -> array
* Map -> dict
*
*
* @author Markus Bernhardt, Copyright 2016
* @author Bernd Rosstauscher, Copyright 2009
* @author Christoffer Lerno / Modified by Bernd Rosstauscher
*/
public final class PListParser {
/*****************************************************************************
* Exception is used for XML parse problems.
*
* @author Bernd Rosstauscher ([email protected]) Copyright 2009
****************************************************************************/
public static class XmlParseException extends Exception {
/** Comment for serialVersionUID
*/
private static final long serialVersionUID = 1L;
/*************************************************************************
* Constructor
************************************************************************/
public XmlParseException() {
super();
}
/*************************************************************************
* Constructor
*
* @param msg
* the error message
************************************************************************/
public XmlParseException(String msg) {
super(msg);
}
/*************************************************************************
* Constructor
*
* @param msg
* error message
* @param e
* the cause.
************************************************************************/
public XmlParseException(String msg, Exception e) {
super(msg, e);
}
}
/*****************************************************************************
* Small helper class representing a tree node.
*
* @author Bernd Rosstauscher ([email protected]) Copyright 2009
****************************************************************************/
public static class Dict implements Iterable> {
private Map children;
/*************************************************************************
* Constructor
************************************************************************/
public Dict() {
super();
this.children = new HashMap();
}
/*************************************************************************
* @param key
* of the child node.
* @return the child node, null if not existing.
************************************************************************/
public Object get(String key) {
return this.children.get(key);
}
/*************************************************************************
* iterator
*
* @see java.lang.Iterable#iterator()
************************************************************************/
public Iterator> iterator() {
return this.children.entrySet().iterator();
}
/*************************************************************************
* @return the size of this dictionary.
************************************************************************/
public int size() {
return this.children.size();
}
/*************************************************************************
* Dumps a dictionary with all sub-nodes to the console.
************************************************************************/
public void dump() {
System.out.println("PList");
dumpInternal(this, 1);
}
/*************************************************************************
* @param plist
* @param indent
************************************************************************/
private static void dumpInternal(Dict plist, int indent) {
for (Map.Entry child : plist) {
if (child.getValue() instanceof Dict) {
for (int j = 0; j < indent; j++) {
System.out.print(" ");
}
System.out.println(child.getKey());
dumpInternal((Dict) child.getValue(), indent + 1);
} else {
for (int j = 0; j < indent; j++) {
System.out.print(" ");
}
System.out.println(child.getKey() + " = " + child.getValue());
}
}
}
/*************************************************************************
* Get a node at a given path.
*
* @param path
* a / separated path into the plist hirarchy.
* @return the object located at the given path, null if it does not
* exist.
************************************************************************/
public Object getAtPath(String path) {
Dict currentNode = this;
String[] pathSegments = path.trim().split("/");
for (int i = 0; i < pathSegments.length; i++) {
String segment = pathSegments[i].trim();
if (segment.length() == 0) {
continue;
}
Object o = currentNode.get(segment);
if (i >= pathSegments.length - 1) {
return o;
}
if (o == null || !(o instanceof Dict)) {
break;
}
currentNode = (Dict) o;
}
return null;
}
}
/**
* Singleton instance.
*/
private final static PListParser PLIST = new PListParser();
/**
* All element types possible for a plist.
*/
private static enum ElementType {
INTEGER, STRING, REAL, DATA, DATE, DICT, ARRAY, TRUE, FALSE,
}
private static final String BASE64_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static final char[] BASE64_CHARS = BASE64_STRING.toCharArray();
private final DateFormat m_dateFormat;
private final Map, ElementType> m_simpleTypes;
/**
* Utility method to close a closeable.
*
* @param closeable
* or null.
*/
static void silentlyClose(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
// Ignore
}
}
/*************************************************************************
* Reads from the given input stream and parses it to a Dict.
*
* @param input
* the input stream to read from.
* @return the parsed dictionary.
* @throws XmlParseException
* on parsing error.
************************************************************************/
private static Dict parse(InputSource input) throws XmlParseException {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
documentBuilder.setEntityResolver(new EmptyXMLResolver());
Document doc = documentBuilder.parse(input);
Element element = doc.getDocumentElement();
return PLIST.parse(element);
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new XmlParseException("Error reading input", e);
}
}
/**
* Create a nested {@code map} from a plist xml file using
* the default mapping.
*
* @param file
* the File containing the the plist xml.
* @return the resulting map as read from the plist data.
* @throws XmlParseException
* if the plist could not be properly parsed.
* @throws IOException
* if there was an issue reading the plist file.
*/
public static Dict load(File file) throws XmlParseException, IOException {
try (FileInputStream byteStream = new FileInputStream(file)) {
InputSource input = new InputSource(byteStream);
return parse(input);
}
}
/**
* Create a plist handler.
*/
PListParser() {
this.m_dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
this.m_dateFormat.setTimeZone(TimeZone.getTimeZone("Z"));
this.m_simpleTypes = new HashMap<>();
this.m_simpleTypes.put(Integer.class, ElementType.INTEGER);
this.m_simpleTypes.put(Byte.class, ElementType.INTEGER);
this.m_simpleTypes.put(Short.class, ElementType.INTEGER);
this.m_simpleTypes.put(Short.class, ElementType.INTEGER);
this.m_simpleTypes.put(Long.class, ElementType.INTEGER);
this.m_simpleTypes.put(String.class, ElementType.STRING);
this.m_simpleTypes.put(Float.class, ElementType.REAL);
this.m_simpleTypes.put(Double.class, ElementType.REAL);
this.m_simpleTypes.put(byte[].class, ElementType.DATA);
this.m_simpleTypes.put(Boolean.class, ElementType.TRUE);
this.m_simpleTypes.put(Date.class, ElementType.DATE);
}
/**
* Parses a plist top element into a map dictionary containing all the data
* in the plist.
*
* @param element
* the top plist element.
* @return the resulting data tree structure.
* @throws XmlParseException
* if there was any error parsing the xml.
*/
Dict parse(Element element) throws XmlParseException {
if (!"plist".equalsIgnoreCase(element.getNodeName())) {
throw new XmlParseException("Expected plist top element, was: " + element.getNodeName());
}
Node n = element.getFirstChild();
while (n != null && !n.getNodeName().equals("dict")) {
n = n.getNextSibling();
}
Dict result = (Dict) parseElement(n);
return result;
}
/**
* Parses a (non-top) xml element.
*
* @param element
* the element to parse.
* @return the resulting object.
* @throws XmlParseException
* if there was some error in the xml.
*/
private Object parseElement(Node element) throws XmlParseException {
try {
return parseElementRaw(element);
} catch (Exception e) {
throw new XmlParseException("Failed to parse: " + element.getNodeName(), e);
}
}
/**
* Parses a (non-top) xml element.
*
* @param element
* the element to parse.
* @return the resulting object.
* @throws ParseException
* if there was some error parsing the xml.
*/
private Object parseElementRaw(Node element) throws ParseException {
ElementType type = ElementType.valueOf(element.getNodeName().toUpperCase());
switch (type) {
case INTEGER:
return parseInt(getValue(element));
case REAL:
return Double.valueOf(getValue(element));
case STRING:
return getValue(element);
case DATE:
return this.m_dateFormat.parse(getValue(element));
case DATA:
return base64decode(getValue(element));
case ARRAY:
return parseArray(element.getChildNodes());
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case DICT:
return parseDict(element.getChildNodes());
default:
throw new RuntimeException("Unexpected type: " + element.getNodeName());
}
}
/*************************************************************************
* Parses a string value from a node.
*
* @param n
* the node to extract the value from.
* @return the content of the node
************************************************************************/
private String getValue(Node n) {
StringBuilder sb = new StringBuilder();
Node c = n.getFirstChild();
while (c != null) {
if (c.getNodeType() == Node.TEXT_NODE) {
sb.append(c.getNodeValue());
}
c = c.getNextSibling();
}
return sb.toString();
}
/**
* Parses a string into a Long or Integer depending on size.
*
* @param value
* the value as a string.
* @return the long value of this string is the value doesn't fit in an
* integer, otherwise the int value of the string.
*/
private Number parseInt(String value) {
Long l = Long.valueOf(value);
if (l.intValue() == l) {
return l.intValue();
}
return l;
}
/**
* Parse a list of xml elements as a plist dict.
*
* @param elements
* the elements to parse.
* @return the dict deserialized as a map.
* @throws ParseException
* if there are any problems deserializing the map.
*/
private Dict parseDict(NodeList elements) throws ParseException {
Dict dict = new Dict();
for (int i = 0; i < elements.getLength(); i++) {
Node key = elements.item(i);
if (key.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
if (!"key".equals(key.getNodeName())) {
throw new ParseException("Expected key but was " + key.getNodeName(), -1);
}
i++;
Node value = elements.item(i);
while (value.getNodeType() != Node.ELEMENT_NODE) {
i++;
value = elements.item(i);
}
Object o = parseElementRaw(value);
String dictName = getValue(key);
dict.children.put(dictName, o);
}
return dict;
}
/**
* Parse a list of xml elements as a plist array.
*
* @param elements
* the elements to parse.
* @return the array deserialized as a list.
* @throws ParseException
* if there are any problems deserializing the list.
*/
private List