* 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
*/
public final class Plist {
/**
* Singleton instance.
*/
private final static Plist PLIST = new Plist();
/**
* 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;
/**
* Convert a nested {@code map} as a plist xml string
* using the default mapping.
*
* @param data the nested data to store as a plist.
* @return the resulting xml as a string.
*/
public static String toXml(Map data) {
StringBuilder builder = new StringBuilder(
"\n"
+ "\n"
+ "");
builder.append(PLIST.objectToXml(data).toXml());
return builder.append("").toString();
}
/**
* Store a nested {@code map} as a plist using the default mapping.
*
* @param data the nested data to store as a plist.
* @param filename the destination file to store the data to.
* @throws IOException if there was an IO error saving the file.
*/
public static void store(Map data, String filename) throws IOException {
store(data, new File(filename));
}
/**
* Store a nested {@code map} as a plist using the default mapping.
*
* @param data the nested data to store as a plist.
* @param file the destination File to store the data to.
* @throws IOException if there was an IO error saving the file.
*/
public static void store(Map data, File file) throws IOException {
FileOutputStream stream = null;
try {
stream = new FileOutputStream(file);
stream.write(toXml(data).getBytes());
} finally {
silentlyClose(stream);
}
}
/**
* 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
}
}
/**
* Create a nested {@code map} from a plist xml string using the default mapping.
*
* @param xml the plist xml data as a string.
* @return the resulting map as read from the plist data.
* @throws XmlParseException if the plist could not be properly parsed.
*/
public static Map fromXml(String xml) throws XmlParseException {
return PLIST.parse(Xmlwise.createXml(xml));
}
/**
* 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 Map load(File file) throws XmlParseException, IOException {
return PLIST.parse(Xmlwise.loadXml(file));
}
/**
* Create a nested {@code map} from a plist xml file using the default mapping.
*
* @param filename 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 Map load(String filename) throws XmlParseException, IOException {
return load(new File(filename));
}
/**
* Create a plist handler.
*/
Plist() {
m_dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
m_dateFormat.setTimeZone(TimeZone.getTimeZone("Z"));
m_simpleTypes = new HashMap, ElementType>();
m_simpleTypes.put(Integer.class, ElementType.INTEGER);
m_simpleTypes.put(Byte.class, ElementType.INTEGER);
m_simpleTypes.put(Short.class, ElementType.INTEGER);
m_simpleTypes.put(Short.class, ElementType.INTEGER);
m_simpleTypes.put(Long.class, ElementType.INTEGER);
m_simpleTypes.put(String.class, ElementType.STRING);
m_simpleTypes.put(Float.class, ElementType.REAL);
m_simpleTypes.put(Double.class, ElementType.REAL);
m_simpleTypes.put(byte[].class, ElementType.DATA);
m_simpleTypes.put(Boolean.class, ElementType.TRUE);
m_simpleTypes.put(Date.class, ElementType.DATE);
}
/**
* Convert an object to its plist representation.
*
* @param o the object to convert, must be Integer, Double, String, Date, Boolean, byte[],
* Map or List.
* @return an XmlElement containing the serialized version of the object.
*/
XmlElement objectToXml(Object o) {
ElementType type = m_simpleTypes.get(o.getClass());
if (type != null) {
switch (type) {
case REAL:
return new XmlElement("real", o.toString());
case INTEGER:
return new XmlElement("integer", o.toString());
case TRUE:
return new XmlElement(((Boolean) o) ? "true" : "false");
case DATE:
return new XmlElement("date", m_dateFormat.format((Date) o));
case STRING:
return new XmlElement("string", (String) o);
case DATA:
return new XmlElement("data", base64encode((byte[]) o));
}
}
if (o instanceof Map) {
return toXmlDict((Map) o);
} else if (o instanceof List) {
return toXmlArray((List>) o);
} else {
throw new RuntimeException("Cannot use " + o.getClass() + " in plist.");
}
}
/**
* Convert a list to its plist representation.
*
* @param list the list to convert.
* @return an XmlElement representing the list.
*/
private XmlElement toXmlArray(List> list) {
XmlElement array = new XmlElement("array");
for (Object o : list) {
array.add(objectToXml(o));
}
return array;
}
/**
* Convert a map to its plist representation.
*
* @param map the map to convert, assumed to have string keys.
* @return an XmlElement representing the map.
*/
private XmlElement toXmlDict(Map map) {
XmlElement dict = new XmlElement("dict");
for (Map.Entry entry : map.entrySet()) {
dict.add(new XmlElement("key", entry.getKey()));
dict.add(objectToXml(entry.getValue()));
}
return dict;
}
/**
* 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.
*/
Map parse(XmlElement element) throws XmlParseException {
if (!"plist".equalsIgnoreCase(element.getName())) {
throw new XmlParseException("Expected plist top element, was: " + element.getName());
}
// Assure that the top element is a dict and the single child element.
if (element.size() != 1) {
throw new XmlParseException("Expected single 'dict' child element.");
}
element.getUnique("dict");
return (Map) parseElement(element.getUnique("dict"));
}
/**
* 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(XmlElement element) throws XmlParseException {
try {
return parseElementRaw(element);
} catch (Exception e) {
throw new XmlParseException("Failed to parse: " + element.toXml(), e);
}
}
/**
* Parses a (non-top) xml element.
*
* @param element the element to parse.
* @return the resulting object.
* @throws Exception if there was some error parsing the xml.
*/
private Object parseElementRaw(XmlElement element) throws Exception {
ElementType type = ElementType.valueOf(element.getName().toUpperCase());
switch (type) {
case INTEGER:
return parseInt(element.getValue());
case REAL:
return Double.valueOf(element.getValue());
case STRING:
return element.getValue();
case DATE:
return m_dateFormat.parse(element.getValue());
case DATA:
return base64decode(element.getValue());
case ARRAY:
return parseArray(element);
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case DICT:
return parseDict(element);
default:
throw new RuntimeException("Unexpected type: " + element.getName());
}
}
/**
* 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 Exception if there are any problems deserializing the map.
*/
private Map parseDict(List elements) throws Exception {
Iterator element = elements.iterator();
HashMap dict = new HashMap();
while (element.hasNext()) {
XmlElement key = element.next();
if (!"key".equals(key.getName())) {
throw new Exception("Expected key but was " + key.getName());
}
Object o = parseElementRaw(element.next());
dict.put(key.getValue(), 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 Exception if there are any problems deserializing the list.
*/
private List