summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/btr/proxy/util/PListParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/btr/proxy/util/PListParser.java')
-rw-r--r--src/main/java/com/btr/proxy/util/PListParser.java544
1 files changed, 544 insertions, 0 deletions
diff --git a/src/main/java/com/btr/proxy/util/PListParser.java b/src/main/java/com/btr/proxy/util/PListParser.java
new file mode 100644
index 0000000..b678900
--- /dev/null
+++ b/src/main/java/com/btr/proxy/util/PListParser.java
@@ -0,0 +1,544 @@
+package com.btr.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.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)
+ * <p>
+ * <em>The xml plist dtd can be found at http://www.apple.com/DTDs/PropertyList-1.0.dtd</em>
+ * <p>
+ * The plist spec handles 8 types of objects: booleans, real, integers, dates, binary data,
+ * strings, arrays (lists) and dictionaries (maps).
+ * <p>
+ * The java Plist lib handles converting xml plists to a nested {@code Map<String, Object>}
+ * that can be trivially read from java. It also provides a simple way to convert a nested
+ * {@code Map<String, Object>} into an xml plist representation.
+ * <p>
+ * The following mapping will be done when converting from plist to <tt>Map</tt>:
+ * <pre>
+ * 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
+ * </pre>
+ * <p>
+ * When converting from Map -> plist the conversion is as follows:
+ * <pre>
+ * Boolean -> true/false
+ * Float/Double -> real
+ * Byte/Short/Integer/Long -> integer
+ * byte[] -> data
+ * List -> array
+ * Map -> dict
+ * </pre>
+ *
+ * @author Christoffer Lerno / Modified by Bernd Rosstauscher
+ */
+public final class PListParser
+{
+ /*****************************************************************************
+ * Exception is used for XML parse problems.
+ * @author Bernd Rosstauscher (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+ public static class XmlParseException extends Exception {
+
+ /** Comment for <code>serialVersionUID</code>*/
+ 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 (proxyvole@rosstauscher.de) Copyright 2009
+ ****************************************************************************/
+
+ public static class Dict implements Iterable<Map.Entry<String, Object>> {
+ private Map<String, Object> children;
+
+ /*************************************************************************
+ * Constructor
+ ************************************************************************/
+
+ public Dict() {
+ super();
+ this.children = new HashMap<String, Object>();
+ }
+
+ /*************************************************************************
+ * @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<Entry<String, Object>> 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<String, Object> 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<Class<?>, 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
+ }
+ }
+
+ /*************************************************************************
+ * @param input
+ * @return
+ * @throws XmlParseException
+ ************************************************************************/
+
+ private static Dict parse(InputSource input)
+ throws XmlParseException {
+ try {
+ DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ documentBuilder.setEntityResolver(new EmptyXMLResolver());
+ Document doc = documentBuilder.parse(input);
+ Element element = doc.getDocumentElement();
+ return PLIST.parse(element);
+ } catch (ParserConfigurationException e) {
+ throw new XmlParseException("Error reading input", e);
+ } catch (SAXException e) {
+ throw new XmlParseException("Error reading input", e);
+ } catch (IOException e) {
+ throw new XmlParseException("Error reading input", e);
+ }
+ }
+
+ /**
+ * Create a nested {@code map<String, Object>} 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
+ {
+ FileInputStream byteStream = new FileInputStream(file);
+ try {
+ InputSource input = new InputSource(byteStream);
+ return parse(input);
+ } finally {
+ silentlyClose(byteStream);
+ }
+ }
+
+ /**
+ * 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<Class<?>, ElementType>();
+ 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());
+ }
+ }
+
+ /*************************************************************************
+ * @param n
+ * @return
+ ************************************************************************/
+
+ 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<Object> parseArray(NodeList elements) throws ParseException
+ {
+ ArrayList<Object> list = new ArrayList<Object>();
+ for (int i = 0; i < elements.getLength(); i++) {
+ Node o = elements.item(i);
+ if (o.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ list.add(parseElementRaw(o));
+ }
+ return list;
+ }
+
+ /**
+ * Encode an array of bytes to a string using base64 encoding.
+ *
+ * @param bytes the bytes to convert.
+ * @return the base64 representation of the bytes.
+ */
+ static String base64encode(byte[] bytes)
+ {
+ StringBuilder builder = new StringBuilder(((bytes.length + 2)/ 3) * 4);
+ for (int i = 0; i < bytes.length; i += 3)
+ {
+ byte b0 = bytes[i];
+ byte b1 = i < bytes.length - 1 ? bytes[i + 1] : 0;
+ byte b2 = i < bytes.length - 2 ? bytes[i + 2] : 0;
+ builder.append(BASE64_CHARS[(b0 & 0xFF) >> 2]);
+ builder.append(BASE64_CHARS[((b0 & 0x03) << 4) | ((b1 & 0xF0) >> 4)]);
+ builder.append(i < bytes.length - 1 ? BASE64_CHARS[((b1 & 0x0F) << 2) | ((b2 & 0xC0) >> 6)] : "=");
+ builder.append(i < bytes.length - 2 ? BASE64_CHARS[b2 & 0x3F] : "=");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts a string to a byte array assuming the string uses base64-encoding.
+ *
+ * @param base64 the string to convert.
+ * @return the resulting byte array.
+ */
+ static byte[] base64decode(String base64)
+ {
+ base64 = base64.trim();
+ int endTrim = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
+ int length = (base64.length() / 4) * 3 - endTrim;
+ base64 = base64.replace('=', 'A');
+ byte[] result = new byte[length];
+ int stringLength = base64.length();
+ int index = 0;
+ for (int i = 0; i < stringLength; i += 4)
+ {
+ int i0 = BASE64_STRING.indexOf(base64.charAt(i));
+ int i1 = BASE64_STRING.indexOf(base64.charAt(i + 1));
+ int i2 = BASE64_STRING.indexOf(base64.charAt(i + 2));
+ int i3 = BASE64_STRING.indexOf(base64.charAt(i + 3));
+ byte b0 = (byte) ((i0 << 2) | (i1 >> 4));
+ byte b1 = (byte) ((i1 << 4) | (i2 >> 2));
+ byte b2 = (byte) ((i2 << 6) | i3);
+ result[index++] = b0;
+ if (index < length)
+ {
+ result[index++] = b1;
+ if (index < length)
+ {
+ result[index++] = b2;
+ }
+ }
+ }
+ return result;
+ }
+
+
+
+
+}
+