* 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 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 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 (proxyvole@rosstauscher.de) 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
}
}
/*************************************************************************
* @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} 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, 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