package net.pms.xmlwise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
@SuppressWarnings("unchecked")
/**
* @deprecated This package is a copy of a third-party library (xmlwise). Future releases will use the original library.
*
* 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
*/
@Deprecated
public final class Plist {
private static final Logger logger = LoggerFactory.getLogger(Plist.class);
/**
* 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<Class<?>, ElementType> m_simpleTypes;
/**
* Convert a nested {@code map<String, Object>} 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<String, Object> data) {
StringBuilder builder = new StringBuilder(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
+ "<plist version=\"1.0\">");
builder.append(PLIST.objectToXml(data).toXml());
return builder.append("</plist>").toString();
}
/**
* Store a nested {@code map<String, Object>} 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<String, Object> data, String filename) throws IOException {
store(data, new File(filename));
}
/**
* Store a nested {@code map<String, Object>} 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<String, Object> 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) {
logger.debug("Caught exception", e);
}
}
/**
* Create a nested {@code map<String, Object>} 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<String, Object> fromXml(String xml) throws XmlParseException {
return PLIST.parse(Xmlwise.createXml(xml));
}
/**
* 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 Map<String, Object> load(File file) throws XmlParseException, IOException {
return PLIST.parse(Xmlwise.loadXml(file));
}
/**
* Create a nested {@code map<String, Object>} 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<String, Object> 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<Class<?>, 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 <tt>XmlElement</tt> 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<String, Object>) 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 <tt>XmlElement</tt> 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 <tt>XmlElement</tt> representing the map.
*/
private XmlElement toXmlDict(Map<String, Object> map) {
XmlElement dict = new XmlElement("dict");
for (Map.Entry<String, Object> 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<String, Object> 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<String, Object>) 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 parseDouble(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;
}
/**
* Parses a string into a Double
*
* @param value the value as a string.
* @return the Double value of this string.
*/
private Number parseDouble(String value) {
if (value.equals("NaN"))
{
return Double.NaN;
}
if (value.equals("-inf"))
{
return Double.NEGATIVE_INFINITY;
}
if (value.equals("inf"))
{
return Double.POSITIVE_INFINITY;
}
return Double.valueOf(value);
}
/**
* 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<String, Object> parseDict(List<XmlElement> elements) throws Exception {
Iterator<XmlElement> element = elements.iterator();
HashMap<String, Object> dict = new HashMap<String, Object>();
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<Object> parseArray(List<XmlElement> elements) throws Exception {
ArrayList<Object> list = new ArrayList<Object>(elements.size());
for (XmlElement element : elements) {
list.add(parseElementRaw(element));
}
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;
}
}