# Property List encoding and decoding
# by Mario Vilas (mvilas at gmail.com)

# Copyright (c) 2009-2012, Mario Vilas
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the copyright holder nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#------------------------------------------------------------------------------
# Imports

import types
import base64
import string
import datetime
from xml.etree import ElementTree

#------------------------------------------------------------------------------
# Exports

__all__ = [

    # Static class containing all the methods.
    "PList",

    # User interface.
    "parse",
    "fromstring",
    "fromtree",
    "dump",
    "tostring",
    "totree",
    "fromfile",
    "tofile",
]

#------------------------------------------------------------------------------
# Public methods

class PList:
    """
    Static class to parse C{.plist} files to native Python types and back.

    Example 1: parsing the sample file from the plist man pages::
        >>> from PList import *
        >>> example = \"\"\"
        ... <?xml version="1.0" encoding="UTF-8"?>
        ... <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
        ...         "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        ... <plist version="1.0">
        ... <dict>
        ...     <key>Year Of Birth</key>
        ...     <integer>1965</integer>
        ...     <key>Pets Names</key>
        ...     <array/>
        ...     <key>Picture</key>
        ...     <data>
        ...         PEKBpYGlmYFCPA==
        ...     </data>
        ...     <key>City of Birth</key>
        ...     <string>Springfield</string>
        ...     <key>Name</key>
        ...     <string>John Doe</string>
        ...     <key>Kids Names</key>
        ...     <array>
        ...         <string>John</string>
        ...         <string>Kyra</string>
        ...     </array>
        ... </dict>
        ... </plist>
        \"\"\"
        >>> data = fromstring(example)
        >>> data
        {'Pets Names': [], 'Kids Names': ['John', 'Kyra'], 'Picture': '<B\x81\xa5\x81\xa5\x99\x81B<', 'Year Of Birth': 1965, 'City of Birth': 'Springfield', 'Name': 'John Doe'}
        >>> tostring(data) == example   # False, as this is not guaranteed.
        False

    Example 2: creating a new C{.plist} file from scratch::
        >>> a = dict()
        >>> a["hello"] = "world"
        >>> a["a boolean"] = True
        >>> a["an integer"] = 5
        >>> a["a float"] = 1.0
        >>> a["a non printable string"] = "\0\1\2\3\4\5\6"
        >>> a["a dictionary"] = dict()
        >>> a["a dictionary"]["example"] = "nesting"
        >>> a["a list"] = [0,1,2,3,4,5]
        >>> a["nested lists"] = [ ["are", "possible"], "too", ["of", "course"] ]
        >>> a
        {'a boolean': True, 'an integer': 5, 'a list': [0, 1, 2, 3, 4, 5], 'nested lists': [['are', 'possible'], 'too', ['of', 'course']], 'a non printable string': '\x00\x01\x02\x03\x04\x05\x06', 'a float': 1.0, 'hello': 'world', 'a dictionary': {'example': 'nesting'}}
        >>> from PList import *
        >>> b = tostring(a)
        >>> a == fromstring(b)
        True
        >>> print b,
        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
               "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        <plist version="1.0">
        <dict><key>a boolean</key>
        <true />
        <key>a dictionary</key>
        <dict><key>example</key>
        <string>nesting</string>
        </dict>
        <key>a float</key>
        <real>1.0</real>
        <key>a list</key>
        <array><integer>0</integer>
        <integer>1</integer>
        <integer>2</integer>
        <integer>3</integer>
        <integer>4</integer>
        <integer>5</integer>
        </array>
        <key>a non printable string</key>
        <data>AAECAwQFBg==</data>
        <key>an integer</key>
        <integer>5</integer>
        <key>hello</key>
        <string>world</string>
        <key>nested lists</key>
        <array><array><string>are</string>
        <string>possible</string>
        </array>
        <string>too</string>
        <array><string>of</string>
        <string>course</string>
        </array>
        </array>
        </dict>
        </plist>
"""

    @classmethod
    def parse(self, filename):
        """
        Read a C{.plist} file from disk and return its contents using builtin
        Python types.

        @type  filename: str
        @param filename: Name of the file to read.

        @return: Contents of the file. May be any combination of the following
            builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool
        """
        return self.fromtree( ElementTree.parse(filename) )

    @classmethod
    def fromstring(self, stringdata):
        """
        Parse the contents of a C{.plist} file in the given string and return
        its contents using builtin Python types.

        @type  stringdata: str
        @param stringdata: File contents to parse.

        @return: Contents of the file. May be any combination of the following
            builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool
        """
        return self.fromtree( ElementTree.fromstring( stringdata.strip() ) )

    @classmethod
    def fromtree(self, xml):
        """
        Parse the contents of a C{.plist} file in the given ElementTree node
        and return its contents using builtin Python types.

        @type  xml: xml.etree.ElementTree.Element
        @param xml: Any node in the tree. Parsing will always begin from the
            root node, regardless of which node you pass to this function.

        @return: Contents of the file. May be any combination of the following
            builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool
        """
        if hasattr(xml, 'getroot'):
            xml = xml.getroot()
        children = xml.getchildren()
        if xml.tag != 'plist' or len(children) != 1:
            raise Exception, "Bad property list"
        return self.__build(children[0])

    @classmethod
    def dump(self, filename, plist):
        """
        Marshall the given data and write it into a C{.plist} file.

        @warn: If the file already exists, its contents will be replaced.

        @note: In all platforms the end of line sequence will be C{"\n"},
            even in Windows which normally uses C{"\r\n"} instead.

        @type  filename: str
        @param filename: Name of the file to write.

        @param plist: Contents of the file.  May be any combination of the
            following builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool
        """
        xml = self.tostring(plist)
        fd = open(filename, 'wb')
        try:
            fd.write(xml)
        finally:
            fd.close()

    @classmethod
    def tostring(self, plist):
        """
        Marshall the given data in C{.plist} file format.

        @param plist: Contents of the file.  May be any combination of the
            following builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool

        @rtype:  str
        @return: Marshalled data in XML (C{.plist}) format.
        """
        xml  = '<?xml version="1.0" encoding="UTF-8"?>\n'
        xml += '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"\n'
        xml += '       "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
        xml += '<plist version="1.0">\n'
        xml += self.__marshall(plist)
        xml += '</plist>\n'
        return xml

    @classmethod
    def totree(self, plist):
        """
        Convert the given data to an ElementTree tree in C{.plist} file format.

        @param plist: Contents of the file.  May be any combination of the
            following builtin types:
             * dict
             * list
             * str
             * int
             * long
             * float
             * bool

        @rtype:  xml.etree.ElementTree.Element
        @return: Marshalled data in XML (C{.plist}) format.
        """
        xml  = self.tostring(plist)
        tree = ElementTree.fromstring(xml)
        return tree

    fromfile    = parse
    tofile      = dump

#------------------------------------------------------------------------------
# Internally used worker methods and classes

    @classmethod
    def __marshall(self, data):
        classname       = self.__name__
        typename        = data.__class__.__name__
        marshaller_name = '_%s__marshall_%s' % (classname, typename)
        if not hasattr(self, marshaller_name):
            raise KeyError, marshaller_name
##            marshaller  = self.__marshall_string
        else:
            marshaller  = getattr(self, marshaller_name)
        return marshaller(data)

    @classmethod
    def __build(self, element):
        classname       = self.__name__
        typename        = element.tag
        builder_name    = '_%s__build_%s' % (classname, typename)
        if not hasattr(self, builder_name):
            raise KeyError, builder_name
##            builder     = self.__build_string
        else:
            builder     = getattr(self, builder_name)
        return builder(element)

    @staticmethod
    def __properties(element):
        props = {}
        for key, value in element.items():
            props[key] = value
        return props

    # TO DO: it would be nice to output indented tags.
    @staticmethod
    def __tag(name, value = '', props = {}):
        value = str(value)
        props = ''.join( [ (' %s="%s"' % p) for p in props.iteritems() ] )
        if value:
            return '<%(name)s%(props)s>%(value)s</%(name)s>\n' % vars()
        return '<%(name)s%(props)s />\n' % vars()

#------------------------------------------------------------------------------
# Marshallers

    @classmethod
    def __marshall_dict(self, data):
        m    = ''
        tags = data.keys()
        tags.sort()
        for key in tags:
            value = data[key]
            m    += self.__tag('key', key) + self.__marshall(value)
        return self.__tag('dict', m)

    @classmethod
    def __marshall_list(self, data):
        children = ''.join([ self.__marshall(element) for element in data ])
        return self.__tag('array', children)

    __marshall_tuple = __marshall_list

    @classmethod
    def __marshall_NoneType(self, data):
        return self.__tag('undef', '')

    @classmethod
    def __marshall_bool(self, data):
        if data:
            return self.__tag('true')
        return self.__tag('false')

    # XXX python only supports floats, we need doubles
    @classmethod
    def __marshall_float(self, data):
        return self.__tag('real', repr(data))

    @classmethod
    def __marshall_int(self, data):
        return self.__tag('integer', data)

    __marshall_long = __marshall_int

    __printables = set(string.printable)

    @classmethod
    def __marshall_str(self, data):
        if set(data).issubset(self.__printables):
            return self.__tag('string', data)
        return self.__marshall_buffer(data)

    @classmethod
    def __marshall_buffer(self, data):
        data = base64.encodestring(str(data))
        data = data.replace('\n', '')
        data = data.replace('\r', '')
        data = data.strip()
        return self.__tag('data', data)

    @classmethod
    def __marshall_date(self, data):
        return self.__tag('date', str(d))

#------------------------------------------------------------------------------
# Builders

    @classmethod
    def __build_dict(self, element):
        data     = {}
        children = element.getchildren()
        if (len(children) & 1) != 0:
            raise IndexError, "Incomplete dictionary"
        for index in range(0, len(children), 2):
            key     = children[index]
            value   = children[index + 1]
            if key.tag != 'key':
                raise ValueError, "Bad dictionary data"
            data[key.text] = self.__build(value)
        return data

    @classmethod
    def __build_array(self, element):
        return [ self.__build(child) for child in element.getchildren() ]

    @staticmethod
    def __build_undef(element):
        return None

    @staticmethod
    def __build_true(element):
        return True

    @staticmethod
    def __build_false(element):
        return False

    # XXX python only supports floats, we need doubles
    @staticmethod
    def __build_real(element):
        return float(element.text)

    @staticmethod
    def __build_integer(element):
        try:
            return int(element.text)
        except ValueError:
            return long(element.text)

    @staticmethod
    def __build_string(element):
        return element.text

    @classmethod
    def __build_data(self, element):
        return base64.decodestring(str(element.text))

    @classmethod
    def __build_date(self, element):
        return datetime.fromtimestamp(str(element.text))

#------------------------------------------------------------------------------
# User interface

parse       = PList.parse
fromstring  = PList.fromstring
fromtree    = PList.fromtree
dump        = PList.dump
tostring    = PList.tostring
totree      = PList.totree
fromfile    = PList.fromfile
tofile      = PList.tofile

#------------------------------------------------------------------------------
# Test code

if __name__ == '__main__':
    import sys

    if len(sys.argv) > 1:
        filename = sys.argv[1]
        plist = fromfile(filename)
    else:
        data = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
        "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Year Of Birth</key>
    <integer>1965</integer>
    <key>Pets Names</key>
    <array/>
    <key>Picture</key>
    <data>
        PEKBpYGlmYFCPA==
    </data>
    <key>City of Birth</key>
    <string>Springfield</string>
    <key>Name</key>
    <string>John Doe</string>
    <key>Kids Names</key>
    <array>
        <string>John</string>
        <string>Kyra</string>
    </array>
</dict>
</plist>
"""
        plist = fromstring(data)

    print plist
    print '-' * 79

    xml = tostring(plist)
    print xml