All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xmlwise.Plist Maven / Gradle / Ivy

There is a newer version: 1.8.2
Show newest version
package xmlwise;

import java.io.Closeable;
import java.io.IOException;
import java.text.DateFormat;
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.TimeZone;

/** Plist xml handling (serialization and deserialization).
 * The xml plist dtd can be found at http://www.apple.com/DTDs/PropertyList-1.0.dtd.
 * @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+/"; //$NON-NLS-1$
	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(final Closeable closeable)
	{
		try
			{
				if (closeable != null) {
					closeable.close();
				}
		}
		catch (final 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(final String xml) throws XmlParseException
	{
		return PLIST.parse(Xmlwise.createXml(xml));
	}

	/**
	 * Create a plist handler.
	 */
	Plist()
	{
		this.m_dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); //$NON-NLS-1$
		this.m_dateFormat.setTimeZone(TimeZone.getTimeZone("Z")); //$NON-NLS-1$
		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);
	}

	/**
	 * 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(final Object o)
	{
		final ElementType type = this.m_simpleTypes.get(o.getClass());
		if (type != null)
		{
			switch (type) {
				case REAL:
					return new XmlElement("real", o.toString()); //$NON-NLS-1$
				case INTEGER:
					return new XmlElement("integer", o.toString()); //$NON-NLS-1$
				case TRUE:
					return new XmlElement(((Boolean) o).booleanValue() ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
				case DATE:
					return new XmlElement("date", this.m_dateFormat.format((Date) o)); //$NON-NLS-1$
				case STRING:
					return new XmlElement("string", (String) o); //$NON-NLS-1$
				case DATA:
					return new XmlElement("data", base64encode((byte[]) o)); //$NON-NLS-1$
			}
		}
		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."); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Convert a list to its plist representation.
	 *
	 * @param list the list to convert.
	 * @return an XmlElement representing the list.
	 */
	private XmlElement toXmlArray(final List list) {
		final XmlElement array = new XmlElement("array"); //$NON-NLS-1$
		for (final 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(final Map map) {
		final XmlElement dict = new XmlElement("dict"); //$NON-NLS-1$
		for (final Map.Entry entry : map.entrySet()) {
			dict.add(new XmlElement("key", entry.getKey())); //$NON-NLS-1$
			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(final XmlElement element) throws XmlParseException {
		if (!"plist".equalsIgnoreCase(element.getName())) { //$NON-NLS-1$
			throw new XmlParseException("Expected plist top element, was: " + element.getName()); //$NON-NLS-1$
		}
		return (Map)parseElement(element.get("dict").getFirst()); //$NON-NLS-1$
	}

	/**
	 * 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(final XmlElement element) throws XmlParseException {
		try {
			return parseElementRaw(element);
		}
		catch (final Exception e) {
			throw new XmlParseException("Failed to parse: " + element.toXml(), e); //$NON-NLS-1$
		}
	}


	/**
	 * 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(final XmlElement element) throws Exception {
		final 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 this.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()); //$NON-NLS-1$
		}
	}

	/** 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 static Number parseInt(final String value)
	{
		final Long l = Long.valueOf(value);
		if (l.intValue() == l.longValue()) {
			return Integer.valueOf(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(final List elements) throws Exception {
		final Iterator element = elements.iterator();
		final HashMap dict = new HashMap<>();
		while (element.hasNext()) {
			final XmlElement key = element.next();
			if (!"key".equals(key.getName())) { //$NON-NLS-1$
				throw new Exception("Expected key but was " + key.getName()); //$NON-NLS-1$
			}
			final 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 parseArray(final List elements) throws Exception
	{
		final ArrayList list = new ArrayList<>(elements.size());
		for (final 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(final byte[] bytes) {
		final StringBuilder builder = new StringBuilder((bytes.length + 2)/ 3 * 4);
		for (int i = 0; i < bytes.length; i += 3) {
			final byte b0 = bytes[i];
			final byte b1 = i < bytes.length - 1 ? bytes[i + 1] : 0;
			final 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 ? Character.valueOf(BASE64_CHARS[(b1 & 0x0F) << 2 | (b2 & 0xC0) >> 6]) : "="); //$NON-NLS-1$
			builder.append(i < bytes.length - 2 ? Character.valueOf(BASE64_CHARS[b2 & 0x3F]) : "="); //$NON-NLS-1$
		}
		return builder.toString();
	}

	/**
	 * Converts a string to a byte array assuming the string uses base64-encoding.
	 *
	 * @param b64 the string to convert.
	 * @return the resulting byte array.
	 */
	static byte[] base64decode(final String b64) {
		String base64 = b64.trim();
		final int endTrim = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0; //$NON-NLS-1$ //$NON-NLS-2$
		final int length = base64.length() / 4 * 3 - endTrim;
		base64 = base64.replace('=', 'A');
		final byte[] result = new byte[length];
		final int stringLength = base64.length();
		int index = 0;
		for (int i = 0; i < stringLength; i += 4) {
			final int i0 = BASE64_STRING.indexOf(base64.charAt(i));
			final int i1 = BASE64_STRING.indexOf(base64.charAt(i + 1));
			final int i2 = BASE64_STRING.indexOf(base64.charAt(i + 2));
			final int i3 = BASE64_STRING.indexOf(base64.charAt(i + 3));
			final byte b0 = (byte) (i0 << 2 | i1 >> 4);
			final byte b1 = (byte) (i1 << 4 | i2 >> 2);
			final byte b2 = (byte) (i2 << 6 | i3);
			result[index++] = b0;
			if (index < length) {
				result[index++] = b1;
				if (index < length) {
					result[index++] = b2;
				}
			}
		}
		return result;
	}


}