# -*- coding: utf-8 -*-
#
#      Licensed to the Apache Software Foundation (ASF) under one
#      or more contributor license agreements.  See the NOTICE file
#      distributed with this work for additional information
#      regarding copyright ownership.  The ASF licenses this file
#      to you under the Apache License, Version 2.0 (the
#      "License"); you may not use this file except in compliance
#      with the License.  You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#      Unless required by applicable law or agreed to in writing,
#      software distributed under the License is distributed on an
#      "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#      KIND, either express or implied.  See the License for the
#      specific language governing permissions and limitations
#      under the License.
#
"""
Module containing the browser binding-specific objects used to work with a CMIS
provider.
"""
from cmislib.cmis_services import Binding, RepositoryServiceIfc
from cmislib.domain import CmisId, CmisObject, ObjectType, ACL, ACE, ChangeEntry
from cmislib.exceptions import CmisException, InvalidArgumentException,\
                               NotSupportedException, ObjectNotFoundException
from cmislib.net import RESTService as Rest
from cmislib.util import parsePropValueByType, parseDateTimeValue, safe_quote,\
                        safe_urlencode
from cmislib import messages
import json
import logging
import StringIO
import time

CMIS_FORM_TYPE = 'application/x-www-form-urlencoded;charset=utf-8'

moduleLogger = logging.getLogger('cmislib.browser.binding')


class BrowserBinding(Binding):

    """
    Implements the Browser Binding to communicate with the CMIS server. The
    Browser Binding uses only GET and POST. It sends JSON and HTML forms and
    gets back JSON.
    """

    def __init__(self, **kwargs):
        self.extArgs = kwargs

    def getRepositoryService(self):
        return RepositoryService()

    def get(self, url, username, password, **kwargs):

        """
        Does a get against the CMIS service. More than likely, you will not
        need to call this method. Instead, let the other objects do it for you.

        For example, if you need to get a specific object by object id, try
        :class:`Repository.getObject`. If you have a path instead of an object
        id, use :class:`Repository.getObjectByPath`. Or, you could start with
        the root folder (:class:`Repository.getRootFolder`) and drill down from
        there.
        """

        # merge the cmis client extended args with the ones that got passed in
        if len(self.extArgs) > 0:
            kwargs.update(self.extArgs)

        resp, content = Rest().get(url,
                                   username=username,
                                   password=password,
                                   **kwargs)
        result = None
        if resp['status'] != '200':
            self._processCommonErrors(resp, url)
        else:
            result = json.loads(content)
        return result

    def post(self, url, payload, contentType, username, password, **kwargs):

        """
        Does a post against the CMIS service. More than likely, you will not
        need to call this method. Instead, let the other objects do it for you.

        For example, to update the properties on an object, you'd call
        :class:`CmisObject.updateProperties`. Or, to check in a document that's
        been checked out, you'd call :class:`Document.checkin` on the PWC.
        """

        # merge the cmis client extended args with the ones that got passed in
        if len(self.extArgs) > 0:
            kwargs.update(self.extArgs)

        result = None
        resp, content = Rest().post(url,
                                    payload,
                                    contentType,
                                    username=username,
                                    password=password,
                                    **kwargs)
        if resp['status'] != '200' and resp['status'] != '201':
            self._processCommonErrors(resp, url)
        elif content is not None and content != "":
            result = json.loads(content)
        return result


class RepositoryService(RepositoryServiceIfc):

    """
    Defines the repository service for the Browser Binding.
    """

    def getRepository(self, client, repositoryId):

        """
        Gets the repository for the specified repository ID.
        """

        result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)

        if repositoryId in result:
            return BrowserRepository(client, result[repositoryId])

        raise ObjectNotFoundException(url=client.repositoryUrl)

    def getRepositories(self, client):

        """
        Gets all of the repositories for this client.
        """

        result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)

        repositories = []
        for repo in result.itervalues():
            repositories.append({'repositoryId': repo['repositoryId'],
                                 'repositoryName': repo['repositoryName']})
        return repositories

    def getDefaultRepository(self, client):

        """
        Gets the default repository for this server. The spec doesn't include
        the notion of a default, so this just returns the first one in the
        list.
        """

        result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
        # instantiate a Repository object with the first workspace
        # element we find
        repository = None
        for repo in result.itervalues():
            repository = BrowserRepository(client, repo)
        return repository


class BrowserCmisObject(object):

    """
    Common ancestor class for other CMIS domain objects such as
    :class:`Document` and :class:`Folder`.
    """

    def __init__(self, cmisClient, repository, objectId=None, data=None, **kwargs):
        """ Constructor """
        self._cmisClient = cmisClient
        self._repository = repository
        self._objectId = objectId
        self._properties = {}
        self._allowableActions = {}
        self.data = data
        self._extArgs = kwargs
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserCmisObject')
        self.logger.debug('Creating an instance of BrowserCmisObject')

    def __str__(self):
        """To string"""
        return self.getObjectId()

    def _initData(self):

        """
        An internal method used to clear out any member variables that
        might be out of sync if we were to fetch new data from the
        service.
        """

        self._properties = {}
        self._allowableActions = {}

    def reload(self, **kwargs):

        """
        Fetches the latest representation of this object from the CMIS service.
        Some methods, like :class:`^Document.checkout` do this for you.

        If you call reload with a properties filter, the filter will be in
        effect on subsequent calls until the filter argument is changed. To
        reset to the full list of properties, call reload with filter set to
        '*'.
        """

        if self._extArgs:
            self._extArgs.update(kwargs)
        else:
            self._extArgs = kwargs

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=object"
        self.data = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                                 self._cmisClient.username,
                                                 self._cmisClient.password,
                                                 **self._extArgs)
        self._initData()

        # if a returnVersion arg was passed in, it is possible we got back
        # a different object ID than the value we started with, so it needs
        # to be cleared out as well
        if self._extArgs.has_key('returnVersion'):
            self._objectId = None

    def getObjectId(self):

        """
        Returns the object ID for this object.

        >>> doc = resultSet.getResults()[0]
        >>> doc.getObjectId()
        u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339'
        """

        if self._objectId is None:
            if self.data is None:
                self.logger.debug('Both objectId and data were None, reloading')
                self.reload()
            props = self.getProperties()
            self._objectId = CmisId(props['cmis:objectId'])
        return self._objectId

    def getObjectParents(self, **kwargs):
        """
        Gets the parents of this object as a :class:`ResultSet`.

        The following optional arguments are supported:
         - filter
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
         - includeRelativePathSegment
        """

        if not self.getAllowableActions()['canGetObjectParents']:
            raise NotSupportedException('Object does not support getObjectParents')

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=parents"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, {'objects': result}, serializer=ChildrenSerializer())

    def getPaths(self):
        """
        Returns the object's paths as a list of strings.
        """
        # see sub-classes for implementation
        pass

    def getAllowableActions(self):

        """
        Returns a dictionary of allowable actions, keyed off of the action name.

        >>> actions = doc.getAllowableActions()
        >>> for a in actions:
        ...     print "%s:%s" % (a,actions[a])
        ...
        canDeleteContentStream:True
        canSetContentStream:True
        canCreateRelationship:True
        canCheckIn:False
        canApplyACL:False
        canDeleteObject:True
        canGetAllVersions:True
        canGetObjectParents:True
        canGetProperties:True
        """

        if self._allowableActions == {}:
            self.reload(includeAllowableActions=True)
            assert self.data.has_key('allowableActions'), "Expected object data to have an allowableActions key"
            allowableActions = self.data['allowableActions']
            self._allowableActions = allowableActions

        return self._allowableActions

    def getProperties(self):

        """
        Returns a dict of the object's properties. If CMIS returns an
        empty element for a property, the property will be in the
        dict with a value of None.

        >>> props = doc.getProperties()
        >>> for p in props:
        ...     print "%s: %s" % (p, props[p])
        ...
        cmis:contentStreamMimeType: text/html
        cmis:creationDate: 2009-12-15T09:45:35.369-06:00
        cmis:baseTypeId: cmis:document
        cmis:isLatestMajorVersion: false
        cmis:isImmutable: false
        cmis:isMajorVersion: false
        cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339

        The optional filter argument is not yet implemented.
        """

        if self._properties == {}:
            if self.data is None:
                self.reload()
            for prop in self.data['properties'].itervalues():
                # property could be multi-valued
                if type(prop['value']) is list:
                    propVal = []
                    for val in prop['value']:
                        propVal.append(parsePropValueByType(val, prop['type']))
                    self._properties[prop['id']] = propVal
                else:
                    self._properties[prop['id']] = parsePropValueByType(prop['value'], prop['type'])

        return self._properties

    def getName(self):

        """
        Returns the value of cmis:name from the getProperties() dictionary.
        We don't need a getter for every standard CMIS property, but name
        is a pretty common one so it seems to make sense.

        >>> doc.getName()
        u'system-overview.html'
        """

        return self.getProperties()['cmis:name']

    def updateProperties(self, properties):

        """
        Updates the properties of an object with the properties provided.
        Only provide the set of properties that need to be updated.

        >>> folder = repo.getObjectByPath('/someFolder2')
        >>> folder.getName()
        u'someFolder2'
        >>> props = {'cmis:name': 'someFolderFoo'}
        >>> folder.updateProperties(props)
        <cmislib.model.Folder object at 0x103ab1210>
        >>> folder.getName()
        u'someFolderFoo'

        """

        # get the root folder URL
        updateUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id

        props = {"cmisaction": "update"}

        setProps(properties, props, initialIndex=0)

        # invoke the URL
        result = self._cmisClient.binding.post(updateUrl.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        self.data = result
        self._initData()
        return self

    def move(self, sourceFolder, targetFolder):

        """
        Moves an object from the source folder to the target folder.

        >>> sub1 = repo.getObjectByPath('/cmislib/sub1')
        >>> sub2 = repo.getObjectByPath('/cmislib/sub2')
        >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1')
        >>> doc.move(sub1, sub2)
        """

        moveUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "move",
                 "sourceFolderId": sourceFolder.id,
                 "targetFolderId": targetFolder.id}

        # invoke the URL
        self._cmisClient.binding.post(moveUrl.encode('utf-8'),
                                      safe_urlencode(props),
                                      'application/x-www-form-urlencoded',
                                      self._cmisClient.username,
                                      self._cmisClient.password)

        return

    def delete(self, **kwargs):

        """
        Deletes this :class:`CmisObject` from the repository. Note that in the
        case of a :class:`Folder` object, some repositories will refuse to
        delete it if it contains children and some will delete it without
        complaint. If what you really want to do is delete the folder and all
        of its descendants, use :meth:`~Folder.deleteTree` instead.

        >>> folder.delete()

        The optional allVersions argument is supported.
        """

        delUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "delete"}

        # invoke the URL
        self._cmisClient.binding.post(delUrl.encode('utf-8'),
                                      safe_urlencode(props),
                                      'application/x-www-form-urlencoded',
                                      self._cmisClient.username,
                                      self._cmisClient.password,
                                      **kwargs)

        return

    def applyPolicy(self, policyId):

        """
        This is not yet implemented.
        """

        pass

    def createRelationship(self, targetObj, relTypeId):

        """
        Creates a relationship between this object and a specified target
        object using the relationship type specified. Returns the new
        :class:`Relationship` object.

        >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc')
        >>> rel.getProperties()
        {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None}

        """

        if not self.allowableActions['canCreateRelationship']:
            raise CmisException('Not allowed to create a relationship')
        url = self._repository.getRepositoryUrl()
        props = {
            'cmisaction': 'createRelationship',
        }

        setProps({
            'cmis:sourceId': self.getObjectId(),
            'cmis:targetId': targetObj.getObjectId(),
            'cmis:objectTypeId': relTypeId
        }, props)

        # invoke the URL
        result = self._cmisClient.binding.post(url.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password)
        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result))

    def getRelationships(self, **kwargs):

        """
        Returns a :class:`ResultSet` of :class:`Relationship` objects for each
        relationship where the source is this object.

        >>> rels = tstDoc1.getRelationships()
        >>> len(rels.getResults())
        1
        >>> rel = rels.getResults().values()[0]
        >>> rel.getProperties()
        {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None}

        The following optional arguments are supported:
         - includeSubRelationshipTypes
         - relationshipDirection
         - typeId
         - maxItems
         - skipCount
         - filter
         - includeAllowableActions
        """

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=relationships"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, result, serializer=RelationShipsSerializer())

    def removePolicy(self, policyId):

        """
        This is not yet implemented.
        """

        pass

    def getAppliedPolicies(self):

        """
        This is not yet implemented.
        """

        pass

    def getACL(self):

        """
        Repository.getCapabilities['ACL'] must return manage or discover.

        >>> acl = folder.getACL()
        >>> acl.getEntries()
        {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}

        The optional onlyBasicPermissions argument is currently not supported.
        """

        if self._repository.getCapabilities()['ACL']:
            # if the ACL capability is discover or manage, this must be
            # supported
            aclUrl = self._repository.getRootFolderUrl() + "?cmisselector=object&objectId=" + self.getObjectId() + "&includeACL=true"
            result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
                                                  self._cmisClient.username,
                                                  self._cmisClient.password)
            return BrowserACL(data=result['acl'])
        else:
            raise NotSupportedException

    def applyACL(self, acl):

        """
        Updates the object with the provided :class:`ACL`.
        Repository.getCapabilities['ACL'] must return manage to invoke this
        call.

        >>> acl = folder.getACL()
        >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true'))
        >>> acl.getEntries()
        {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
        """
        if self._repository.getCapabilities()['ACL'] == 'manage':
            # if the ACL capability is manage, this must be
            # supported
            # but it also depends on the canApplyACL allowable action
            # for this object
            if not isinstance(acl, ACL):
                raise CmisException('The ACL to apply must be an instance of the ACL class.')
            # get the root folder URL
            aclUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + "&cmisaction=applyACL"

            fields = {}
            for i, entry in enumerate(acl.getAddedAces()):
                fields['addACEPrincipal[%d]' % i] = entry.principalId
                for j, perm in enumerate(entry.permissions):
                    fields['addACEPermission[%d][%d]' % (i, j)] = perm
            for i, entry in enumerate(acl.getRemovedAces()):
                fields['removeACEPrincipal[%d]' % i] = entry.principalId
                for j, perm in enumerate(entry.permissions):
                    fields['removeACEPermission[%d][%d]' % (i, j)] = perm

            contentType, body = encode_multipart_formdata(fields, None, None)

            # invoke the URL
            result = self._cmisClient.binding.post(aclUrl.encode('utf-8'),
                                                   body,
                                                   contentType,
                                                   self._cmisClient.username,
                                                   self._cmisClient.password)

            # return the result set
            return BrowserACL(data=result)
        else:
            raise NotSupportedException

    def getRepository(self):

        """
        Returns the object's repository
        """

        return self._repository

    allowableActions = property(getAllowableActions)
    name = property(getName)
    id = property(getObjectId)
    properties = property(getProperties)
    ACL = property(getACL)
    repository = property(getRepository)


class BrowserRepository(object):
    """
    Represents a CMIS repository. Will lazily populate itself by
    calling the repository CMIS service URL.

    You must pass in an instance of a CmisClient when creating an
    instance of this class.
    """

    def __init__(self, cmisClient, data=None):
        """ Constructor """
        self._cmisClient = cmisClient
        self.data = data
        self._repositoryId = None
        self._repositoryName = None
        self._repositoryInfo = {}
        self._capabilities = {}
        self._permDefs = {}
        self._permMap = {}
        self._permissions = None
        self._propagation = None
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserRepository')
        self.logger.debug('Creating an instance of BrowserRepository')

    def __str__(self):
        """To string"""
        return self.getRepositoryId()

    def _initData(self):
        """
        This method clears out any local variables that would be out of sync
        when data is re-fetched from the server.
        """
        self._repositoryId = None
        self._repositoryName = None
        self._repositoryInfo = {}
        self._capabilities = {}
        self._permDefs = {}
        self._permMap = {}
        self._permissions = None
        self._propagation = None

    def reload(self):
        """
        This method will re-fetch the repository's XML data from the CMIS
        repository.
        """

        pass

    def getRepositoryId(self):

        """
        Returns this repository's unique identifier

        >>> repo = client.getDefaultRepository()
        >>> repo.getRepositoryId()
        u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
        """

        if self._repositoryId is None:
            if self.data is None:
                self.reload()
            self._repositoryId = self.data['repositoryId']
        return self._repositoryId

    def getRepositoryName(self):

        """
        Returns this repository's name

        >>> repo = client.getDefaultRepository()
        >>> repo.getRepositoryName()
        u'Main Repository'
        """

        if self._repositoryName is None:
            if self.data is None:
                self.reload()
            self._repositoryName = self.data['repositoryName']
        return self._repositoryName

    def getRepositoryInfo(self):

        """
        Returns a dict of repository information.

        >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName()
        u'Main Repository'
        >>> info = repo.getRepositoryInfo()
        >>> for k,v in info.items():
        ...     print "%s:%s" % (k,v)
        ...
        cmisSpecificationTitle:Version 1.0 Committee Draft 04
        cmisVersionSupported:1.0
        repositoryDescription:None
        productVersion:3.2.0 (r2 2440)
        rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348
        repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9
        repositoryName:Main Repository
        vendorName:Alfresco
        productName:Alfresco Repository (Community)
        """

        if not self._repositoryInfo:
            if self.data is None:
                self.reload()
            repoInfo = {'repositoryId': self.data['repositoryId'], 'repositoryName': self.data['repositoryName'],
                        'repositoryDescription': self.data['repositoryDescription'],
                        'vendorName': self.data['vendorName'], 'productName': self.data['productName'],
                        'productVersion': self.data['productVersion'], 'rootFolderId': self.data['rootFolderId'],
                        'latestChangeLogToken': self.data['latestChangeLogToken'],
                        'cmisVersionSupported': self.data['cmisVersionSupported'],
                        'changesIncomplete': self.data['changesIncomplete'],
                        'changesOnType': self.data['changesOnType'],
                        'principalIdAnonymous': self.data['principalIdAnonymous'],
                        'principalIdAnyone': self.data['principalIdAnyone']}
            if self.data.has_key('thinClientURI'):
                repoInfo['thinClientURI'] = self.data['thinClientURI']
            if self.data.has_key('extendedFeatures'):
                repoInfo['extendedFeatures'] = self.data['extendedFeatures']
            self._repositoryInfo = repoInfo
        return self._repositoryInfo

    def getRootFolderUrl(self):

        """ Gets the repository's root folder URL """

        if self.data is None:
            self.reload()
        return self.data['rootFolderUrl']

    def getRepositoryUrl(self):

        """ Gets the repository's URL """

        if self.data is None:
            self.reload()
        return self.data['repositoryUrl']

    def getObjectByPath(self, path, **kwargs):

        """
        Returns an object given the path to the object.

        >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf')
        >>> doc.getTitle()
        u'sample-b.pdf'

        The following optional arguments are not currently supported:
         - filter
         - includeAllowableActions
        """

        byPathUrl = self.getRootFolderUrl() + safe_quote(path) + "?cmisselector=object"
        result = self._cmisClient.binding.get(byPathUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, data=result, **kwargs), **kwargs)

    def getSupportedPermissions(self):
        """
        Returns the value of the cmis:supportedPermissions element. Valid
        values are:

         - basic: indicates that the CMIS Basic permissions are supported
         - repository: indicates that repository specific permissions are supported
         - both: indicates that both CMIS basic permissions and repository specific permissions are supported

        >>> repo.supportedPermissions
        u'both'
        """

        if not self.getCapabilities()['ACL']:
            raise NotSupportedException(messages.NO_ACL_SUPPORT)

        if not self._permissions:
            if self.data is None:
                self.reload()
            if self.data.has_key('aclCapabilities'):
                if self.data['aclCapabilities'].has_key('supportedPermissions'):
                    self._permissions = self.data['aclCapabilities']['supportedPermissions']
        return self._permissions

    def getPermissionDefinitions(self):

        """
        Returns a dictionary of permission definitions for this repository. The
        key is the permission string or technical name of the permission
        and the value is the permission description.

        >>> for permDef in repo.permissionDefinitions:
        ...     print permDef
        ...
        cmis:all
        {http://www.alfresco.org/model/system/1.0}base.LinkChildren
        {http://www.alfresco.org/model/content/1.0}folder.Consumer
        {http://www.alfresco.org/model/security/1.0}All.All
        {http://www.alfresco.org/model/system/1.0}base.CreateAssociations
        {http://www.alfresco.org/model/system/1.0}base.FullControl
        {http://www.alfresco.org/model/system/1.0}base.AddChildren
        {http://www.alfresco.org/model/system/1.0}base.ReadAssociations
        {http://www.alfresco.org/model/content/1.0}folder.Editor
        {http://www.alfresco.org/model/content/1.0}cmobject.Editor
        {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations
        cmis:read
        cmis:write
        """
        if not self.getCapabilities()['ACL']:
            raise NotSupportedException(messages.NO_ACL_SUPPORT)

        permData = self.data['aclCapabilities']['permissions']
        perms = {}
        for entry in permData:
            perms[entry['permission']] = entry['description']

        return perms

    def getPermissionMap(self):

        """
        Returns a dictionary representing the permission mapping table where
        each key is a permission key string and each value is a list of one or
        more permissions the principal must have to perform the operation.

        >>> for (k,v) in repo.permissionMap.items():
        ...     print 'To do this: %s, you must have these perms:' % k
        ...     for perm in v:
        ...             print perm
        ...
        To do this: canCreateFolder.Folder, you must have these perms:
        cmis:all
        {http://www.alfresco.org/model/system/1.0}base.CreateChildren
        To do this: canAddToFolder.Folder, you must have these perms:
        cmis:all
        {http://www.alfresco.org/model/system/1.0}base.CreateChildren
        To do this: canDelete.Object, you must have these perms:
        cmis:all
        {http://www.alfresco.org/model/system/1.0}base.DeleteNode
        To do this: canCheckin.Document, you must have these perms:
        cmis:all
        {http://www.alfresco.org/model/content/1.0}lockable.CheckIn
        """
        if not self.getCapabilities()['ACL']:
            raise NotSupportedException(messages.NO_ACL_SUPPORT)

        permData = self.data['aclCapabilities']['permissionMapping']
        permMap = {}
        for entry in permData:
            permMap[entry['key']] = entry['permission']

        return permMap

    def getPropagation(self):

        """
        Returns the value of the cmis:propagation element. Valid values are:
          - objectonly: indicates that the repository is able to apply ACEs
            without changing the ACLs of other objects
          - propagate: indicates that the repository is able to apply ACEs to a
            given object and propagate this change to all inheriting objects

        >>> repo.propagation
        u'propagate'
        """
        if not self.getCapabilities()['ACL']:
            raise NotSupportedException(messages.NO_ACL_SUPPORT)

        return self.data['aclCapabilities']['propagation']

    def getCapabilities(self):

        """
        Returns a dict of repository capabilities.

        >>> caps = repo.getCapabilities()
        >>> for k,v in caps.items():
        ...     print "%s:%s" % (k,v)
        ...
        PWCUpdatable:True
        VersionSpecificFiling:False
        Join:None
        ContentStreamUpdatability:anytime
        AllVersionsSearchable:False
        Renditions:None
        Multifiling:True
        GetFolderTree:True
        GetDescendants:True
        ACL:None
        PWCSearchable:True
        Query:bothcombined
        Unfiling:False
        Changes:None
        """

        if not self._capabilities:
            if self.data is None:
                self.reload()
            caps = {}
            if self.data.has_key('capabilities'):
                for cap in self.data['capabilities'].keys():
                    key = cap.replace('capability', '')
                    caps[key] = self.data['capabilities'][cap]
                self._capabilities = caps
        return self._capabilities

    def getRootFolder(self):
        """
        Returns the root folder of the repository

        >>> root = repo.getRootFolder()
        >>> root.getObjectId()
        u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
        """

        # get the root folder id
        rootFolderId = self.getRepositoryInfo()['rootFolderId']
        # instantiate a Folder object using the ID
        folder = BrowserFolder(self._cmisClient, self, rootFolderId)
        # return it
        return folder

    def getFolder(self, folderId):

        """
        Returns a :class:`Folder` object for a specified folderId

        >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348')
        >>> someFolder.getObjectId()
        u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
        """

        retObject = self.getObject(folderId)
        return BrowserFolder(self._cmisClient, self, data=retObject.data)

    def getTypeChildren(self,
                        typeId=None,
                        **kwargs):

        """
        Returns a list of :class:`ObjectType` objects corresponding to the
        child types of the type specified by the typeId.

        If no typeId is provided, the result will be the same as calling
        `self.getTypeDefinitions`

        These optional arguments are current unsupported:
         - includePropertyDefinitions
         - maxItems
         - skipCount

        >>> baseTypes = repo.getTypeChildren()
        >>> for baseType in baseTypes:
        ...     print baseType.getTypeId()
        ...
        cmis:folder
        cmis:relationship
        cmis:document
        cmis:policy
        """

        typesUrl = self.getRepositoryUrl() + "?cmisselector=typeChildren"

        if typeId is not None:
            typesUrl += "&typeId=%s" % (safe_quote(typeId))

        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        types = []
        for res in result['types']:
            objectType = BrowserObjectType(self._cmisClient,
                                           self,
                                           data=res)
            types.append(objectType)
        # return the result
        return types

    def getTypeDescendants(self, typeId=None, depth=None, **kwargs):
        """
        Returns a list of :class:`ObjectType` objects corresponding to the
        descendant types of the type specified by the typeId.

        If no typeId is provided, the repository's "typesdescendants" URL
        will be called to determine the list of descendant types.

        >>> allTypes = repo.getTypeDescendants()
        >>> for aType in allTypes:
        ...     print aType.getTypeId()
        ...
        cmis:folder
        F:cm:systemfolder
        F:act:savedactionfolder
        F:app:configurations
        F:fm:forums
        F:wcm:avmfolder
        F:wcm:avmplainfolder
        F:wca:webfolder
        F:wcm:avmlayeredfolder
        F:st:site
        F:app:glossary
        F:fm:topic

        This optional argument is supported:
         - depth

        These optional arguments are supported:
         - includePropertyDefinitions

        >>> types = alfRepo.getTypeDescendants('cmis:folder')
        >>> len(types)
        17
        >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1)
        >>> len(types)
        12
        >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2)
        >>> len(types)
        17
        """

        typesUrl = self.getRepositoryUrl() + "?cmisselector=typeDescendants"

        if typeId is not None:
            typesUrl += "&typeId=%s" % (safe_quote(typeId))
        if depth is not None:
            typesUrl += "&depth=%s" % (depth)
        print typesUrl

        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        serializer = TreeSerializer(treeType='type')
        types = serializer.getEntries(self._cmisClient, self, result)
        return types

    def getTypeDefinitions(self, **kwargs):

        """
        Returns a list of :class:`ObjectType` objects representing
        the base types in the repository.

        >>> baseTypes = repo.getTypeDefinitions()
        >>> for baseType in baseTypes:
        ...     print baseType.getTypeId()
        ...
        cmis:folder
        cmis:relationship
        cmis:document
        cmis:policy
        """

        typesUrl = self.getRepositoryUrl() + "?cmisselector=typeChildren"

        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        types = []
        for res in result['types']:
            objectType = BrowserObjectType(self._cmisClient,
                                           self,
                                           data=res)
            types.append(objectType)
        # return the result
        return types

    def getTypeDefinition(self, typeId):

        """
        Returns an :class:`ObjectType` object for the specified object type id.

        >>> folderType = repo.getTypeDefinition('cmis:folder')
        """
        # localhost:8080/chemistry/browser/A1?cmisselector=typeDefinition&typeId=cmis:folder
        typesUrl = self.getRepositoryUrl() + "?cmisselector=typeDefinition" + \
            "&typeId=" + typeId
        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password)

        return BrowserObjectType(self._cmisClient,
                                 self,
                                 data=result)

    def getCheckedOutDocs(self, **kwargs):

        """
        Returns a ResultSet of :class:`CmisObject` objects that
        are currently checked out.

        >>> rs = repo.getCheckedOutDocs()
        >>> len(rs.getResults())
        2
        >>> for doc in repo.getCheckedOutDocs().getResults():
        ...     doc.getTitle()
        ...
        u'sample-a (Working Copy).pdf'
        u'sample-b (Working Copy).pdf'

        These optional arguments are supported:
         - folderId
         - maxItems
         - skipCount
         - orderBy
         - filter
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
        """

        typesUrl = self.getRepositoryUrl() + "?cmisselector=checkedOut"

        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)

        return BrowserResultSet(self._cmisClient,
                                self,
                                data=result)

    def getObject(self,
                  objectId,
                  **kwargs):

        """
        Returns an object given the specified object ID.

        >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808')
        >>> doc.getTitle()
        u'sample-b.pdf'

        The following optional arguments are supported:
         - returnVersion
         - filter
         - includeRelationships
         - includePolicyIds
         - renditionFilter
         - includeACL
         - includeAllowableActions
        """

        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, CmisId(objectId), **kwargs), **kwargs)

    def query(self, statement, **kwargs):

        """
        Returns a list of :class:`CmisObject` objects based on the CMIS
        Query Language passed in as the statement. The actual objects
        returned will be instances of the appropriate child class based
        on the object's base type ID.

        In order for the results to be properly instantiated as objects,
        make sure you include 'cmis:objectId' as one of the fields in
        your select statement, or just use "SELECT \*".

        If you want the search results to automatically be instantiated with
        the appropriate sub-class of :class:`CmisObject` you must either
        include cmis:baseTypeId as one of the fields in your select statement
        or just use "SELECT \*".

        >>> q = "select * from cmis:document where cmis:name like '%test%'"
        >>> resultSet = repo.query(q)
        >>> len(resultSet.getResults())
        1
        >>> resultSet.hasNext()
        False

        The following optional arguments are supported:
         - searchAllVersions
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
         - maxItems
         - skipCount

        >>> q = 'select * from cmis:document'
        >>> rs = repo.query(q)
        >>> len(rs.getResults())
        148
        >>> rs = repo.query(q, maxItems='5')
        >>> len(rs.getResults())
        5
        >>> rs.hasNext()
        True
        """

        # build the CMIS query XML that we're going to POST
        queryUrl = self.getRepositoryUrl() + "?cmisaction=query&q=" + safe_quote(statement)

        # do the POST
        result = self._cmisClient.binding.post(queryUrl.encode('utf-8'),
                                               None,
                                               CMIS_FORM_TYPE,
                                               self._cmisClient.username,
                                               self._cmisClient.password,
                                               **kwargs)

        # return the result set
        return BrowserResultSet(self._cmisClient, self, result, serializer=ResultsSerializer())

    def getContentChanges(self, **kwargs):

        """
        Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects.

        >>> for changeEntry in rs:
        ...     changeEntry.objectId
        ...     changeEntry.id
        ...     changeEntry.changeType
        ...     changeEntry.changeTime
        ...
        'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b'
        u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b'
        u'created'
        datetime.datetime(2010, 2, 11, 12, 55, 14)
        'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923'
        u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923'
        u'updated'
        datetime.datetime(2010, 2, 11, 12, 55, 13)
        'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
        u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
        u'updated'

        The following optional arguments are supported:
         - changeLogToken
         - includeProperties
         - includePolicyIDs
         - includeACL
         - maxItems

        You can get the latest change log token by inspecting the repository
        info via :meth:`Repository.getRepositoryInfo`.

        >>> repo.info['latestChangeLogToken']
        u'2692'
        >>> rs = repo.getContentChanges(changeLogToken='2692')
        >>> len(rs)
        1
        >>> rs[0].id
        u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9'
        >>> rs[0].changeType
        u'updated'
        >>> rs[0].changeTime
        datetime.datetime(2010, 2, 16, 20, 6, 37)
        """
        if self.getCapabilities()['Changes'] is None:
            raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT)

        changesUrl = self.getRepositoryUrl() + "?cmisselector=contentChanges"

        result = self._cmisClient.binding.get(changesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)

        return BrowserResultSet(self._cmisClient,
                                self,
                                data=result,
                                serializer=ChangeEntrySerializer())

    def createDocumentFromString(self,
                                 name,
                                 properties={},
                                 parentFolder=None,
                                 contentString=None,
                                 contentType=None,
                                 contentEncoding=None):

        """
        Creates a new document setting the content to the string provided. If
        the repository supports unfiled objects, you do not have to pass in
        a parent :class:`Folder` otherwise it is required.

        This method is essentially a convenience method that wraps your string
        with a StringIO and then calls createDocument.

        >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain')
        <cmislib.model.Document object at 0x101352ed0>
        """

        # if you didn't pass in a parent folder
        if parentFolder is None:
            # if the repository doesn't require fileable objects to be filed
            if self.getCapabilities()['Unfiling']:
                # has not been implemented
                # postUrl = self.getCollectionLink(UNFILED_COLL)
                raise NotImplementedError
            else:
                # this repo requires fileable objects to be filed
                raise InvalidArgumentException

        return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString),
                                           contentType, contentEncoding)

    def createDocument(self,
                       name,
                       properties={},
                       parentFolder=None,
                       contentFile=None,
                       contentType=None,
                       contentEncoding=None):

        """
        Creates a new :class:`Document` object. If the repository
        supports unfiled objects, you do not have to pass in
        a parent :class:`Folder` otherwise it is required.

        To create a document with an associated contentFile, pass in a
        File object. The method will attempt to guess the appropriate content
        type and encoding based on the file. To specify it yourself, pass them
        in via the contentType and contentEncoding arguments.

        >>> f = open('sample-a.pdf', 'rb')
        >>> doc = folder.createDocument('sample-a.pdf', contentFile=f)
        <cmislib.model.Document object at 0x105be5e10>
        >>> f.close()
        >>> doc.getTitle()
        u'sample-a.pdf'

        The following optional arguments are not currently supported:
         - versioningState
         - policies
         - addACEs
         - removeACEs
        """

        # if you didn't pass in a parent folder
        if parentFolder is None:
            # if the repository doesn't require fileable objects to be filed
            if self.getCapabilities()['Unfiling']:
                # has not been implemented
                raise NotImplementedError
            else:
                # this repo requires fileable objects to be filed
                raise InvalidArgumentException

        # get the root folder URL
        createDocUrl = self.getRootFolderUrl()

        props = {"objectId": parentFolder.id,
                 "cmisaction": "createDocument",
                 "propertyId[0]": "cmis:name",
                 "propertyValue[0]": name}

        props["propertyId[1]"] = "cmis:objectTypeId"
        if properties.has_key('cmis:objectTypeId'):
            props["propertyValue[1]"] = properties['cmis:objectTypeId']
            del properties['cmis:objectTypeId']
        else:
            props["propertyValue[1]"] = "cmis:document"

        setProps(properties, props, initialIndex=2)

        contentType, body = encode_multipart_formdata(props, contentFile, contentType)

        # invoke the URL
        result = self._cmisClient.binding.post(createDocUrl.encode('utf-8'),
                                               body,
                                               contentType,
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        # return the result set
        return BrowserDocument(self._cmisClient, self, data=result)

    def createDocumentFromSource(self,
                                 sourceId,
                                 properties={},
                                 parentFolder=None):
        """
        This is not yet implemented.

        The following optional arguments are not yet supported:
         - versioningState
         - policies
         - addACEs
         - removeACEs
        """

        pass

    def createFolder(self,
                     parentFolder,
                     name,
                     properties={},
                     **kwargs):

        """
        Creates a new :class:`Folder` object in the specified parentFolder.

        >>> root = repo.getRootFolder()
        >>> folder = repo.createFolder(root, 'someFolder2')
        >>> folder.getTitle()
        u'someFolder2'
        >>> folder.getObjectId()
        u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f'

        The following optional arguments are not yet supported:
         - policies
         - addACEs
         - removeACEs
        """

        return parentFolder.createFolder(name, properties, **kwargs)

    def createRelationship(self, sourceObj, targetObj, relType):
        """
        Creates a relationship of the specific type between a source object
        and a target object and returns the new :class:`Relationship` object.

        The following optional arguments are not currently supported:
         - policies
         - addACEs
         - removeACEs
        """

        return sourceObj.createRelationship(targetObj, relType)

    def createPolicy(self, properties):
        """
        This has not yet been implemented.

        The following optional arguments are not currently supported:
         - folderId
         - policies
         - addACEs
         - removeACEs
        """

        pass

    capabilities = property(getCapabilities)
    id = property(getRepositoryId)
    info = property(getRepositoryInfo)
    name = property(getRepositoryName)
    rootFolder = property(getRootFolder)
    permissionDefinitions = property(getPermissionDefinitions)
    permissionMap = property(getPermissionMap)
    propagation = property(getPropagation)
    supportedPermissions = property(getSupportedPermissions)


class BrowserResultSet(object):

    """
    Represents a paged result set.
    """

    def __init__(self, cmisClient, repository, data, serializer=None):
        """ Constructor """
        self._cmisClient = cmisClient
        self._repository = repository
        self._data = data
        self._serializer = serializer
        self._results = []
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserResultSet')
        self.logger.debug('Creating an instance of BrowserResultSet')

    def __iter__(self):
        """ Iterator for the result set """
        return iter(self.getResults())

    def __getitem__(self, index):
        """ Getter for the result set """
        return self.getResults()[index]

    def __len__(self):
        """ Len method for the result set """
        return len(self.getResults())

    def reload(self):

        """
        Re-invokes the self link for the current set of results.

        >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
        >>> resultSet.reload()

        """

        pass

    def getResults(self):

        """
        Returns the results that were fetched and cached by the get*Page call.

        >>> resultSet = repo.getCheckedOutDocs()
        >>> resultSet.hasNext()
        False
        >>> for result in resultSet.getResults():
        ...     result
        ...
        <cmislib.model.Document object at 0x104851810>
        """

        if self._results:
            return self._results

        if self._data:
            self._results = self._serializer.fromJSON(self._cmisClient, self._repository, self._data)

        return self._results

    def hasObject(self, objectId):

        """
        Returns True if the specified objectId is found in the list of results,
        otherwise returns False.
        """

        for obj in self.getResults():
            if obj.id == objectId:
                return True
        return False

    def getFirst(self):

        """
        Returns the first page of results as a dictionary of
        :class:`CmisObject` objects or its appropriate sub-type. This only
        works when the server returns a "first" link. Not all of them do.

        >>> resultSet.hasFirst()
        True
        >>> results = resultSet.getFirst()
        >>> for result in results:
        ...     result
        ...
        <cmislib.model.Document object at 0x10480bc90>
        """

        pass

    def getPrev(self):

        """
        Returns the prev page of results as a dictionary of
        :class:`CmisObject` objects or its appropriate sub-type. This only
        works when the server returns a "prev" link. Not all of them do.
        >>> resultSet.hasPrev()
        True
        >>> results = resultSet.getPrev()
        >>> for result in results:
        ...     result
        ...
        <cmislib.model.Document object at 0x10480bc90>
        """

        pass

    def getNext(self):

        """
        Returns the next page of results as a dictionary of
        :class:`CmisObject` objects or its appropriate sub-type.
        >>> resultSet.hasNext()
        True
        >>> results = resultSet.getNext()
        >>> for result in results:
        ...     result
        ...
        <cmislib.model.Document object at 0x10480bc90>
        """

        pass

    def getLast(self):

        """
        Returns the last page of results as a dictionary of
        :class:`CmisObject` objects or its appropriate sub-type. This only
        works when the server is returning a "last" link. Not all of them do.

        >>> resultSet.hasLast()
        True
        >>> results = resultSet.getLast()
        >>> for result in results:
        ...     result
        ...
        <cmislib.model.Document object at 0x10480bc90>
        """

        pass

    def hasNext(self):

        """
        Returns True if this page contains a next link.

        >>> resultSet.hasNext()
        True
        """

        if self._data and 'hasMoreItems' in self._data:
            return self._data['hasMoreItems']

    def getNumItems(self):

        """
        Returns the number of items in the result set.
        >>> resultSet.getNumItems()
        3
        """

        if self._data:
            return self._data['numItems']

    def hasPrev(self):

        """
        Returns True if this page contains a prev link. Not all CMIS providers
        implement prev links consistently.

        >>> resultSet.hasPrev()
        True
        """

        pass

    def hasFirst(self):

        """
        Returns True if this page contains a first link. Not all CMIS providers
        implement first links consistently.

        >>> resultSet.hasFirst()
        True
        """

        pass

    def hasLast(self):

        """
        Returns True if this page contains a last link. Not all CMIS providers
        implement last links consistently.

        >>> resultSet.hasLast()
        True
        """

        pass


class BrowserDocument(BrowserCmisObject):

    """
    An object typically associated with file content.
    """

    def checkout(self):

        """
        Performs a checkout on the :class:`Document` and returns the
        Private Working Copy (PWC), which is also an instance of
        :class:`Document`

        >>> doc.getObjectId()
        u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0'
        >>> doc.isCheckedOut()
        False
        >>> pwc = doc.checkout()
        >>> doc.isCheckedOut()
        True
        """

        coUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "checkOut"}

        # invoke the URL
        result = self._cmisClient.binding.post(coUrl.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result))

    def cancelCheckout(self):
        """
        Cancels the checkout of this object by retrieving the Private Working
        Copy (PWC) and then deleting it. After the PWC is deleted, this object
        will be reloaded to update properties related to a checkout.

        >>> doc.isCheckedOut()
        True
        >>> doc.cancelCheckout()
        >>> doc.isCheckedOut()
        False
        """

        coUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "cancelCheckOut"}

        # invoke the URL
        self._cmisClient.binding.post(coUrl.encode('utf-8'),
                                      safe_urlencode(props),
                                      'application/x-www-form-urlencoded',
                                      self._cmisClient.username,
                                      self._cmisClient.password)

        return

    def getPrivateWorkingCopy(self):

        """
        Retrieves the object using the object ID in the property:
        cmis:versionSeriesCheckedOutId then uses getObject to instantiate
        the object.

        >>> doc.isCheckedOut()
        False
        >>> doc.checkout()
        <cmislib.model.Document object at 0x103a25ad0>
        >>> pwc = doc.getPrivateWorkingCopy()
        >>> pwc.getTitle()
        u'sample-b (Working Copy).pdf'
        """

        # reloading the document just to make sure we've got the latest
        # and greatest PWC ID
        self.reload()
        pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId']
        if pwcDocId:
            return self._repository.getObject(pwcDocId)

    def isCheckedOut(self):

        """
        Returns true if the document is checked out.

        >>> doc.isCheckedOut()
        True
        >>> doc.cancelCheckout()
        >>> doc.isCheckedOut()
        False
        """

        # reloading the document just to make sure we've got the latest
        # and greatest checked out prop
        self.reload()
        return self.getProperties()['cmis:isVersionSeriesCheckedOut']

    def getCheckedOutBy(self):

        """
        Returns the ID who currently has the document checked out.
        >>> pwc = doc.checkout()
        >>> pwc.getCheckedOutBy()
        u'admin'
        """

        # reloading the document just to make sure we've got the latest
        # and greatest checked out prop
        self.reload()
        return self.getProperties()['cmis:versionSeriesCheckedOutBy']

    def checkin(self, checkinComment=None, contentFile=None, contentType=None,
                properties=None, **kwargs):

        """
        Checks in this :class:`Document` which must be a private
        working copy (PWC).

        >>> doc.isCheckedOut()
        False
        >>> pwc = doc.checkout()
        >>> doc.isCheckedOut()
        True
        >>> pwc.checkin()
        <cmislib.model.Document object at 0x103a8ae90>
        >>> doc.isCheckedOut()
        False

        The following optional arguments are NOT supported:
         - policies
         - addACEs
         - removeACEs
        """
        # TODO implement optional arguments
        # major = true is supposed to be the default but inmemory 0.9 is throwing an error 500 without it
        if not kwargs.has_key('major'):
            kwargs['major'] = 'true'
        else:
            kwargs['major'] = 'false'

        props = {
            'checkinComment': checkinComment or "",
        }
        props.update(kwargs)
        propCount = 0
        properties = properties or {}
        for key, value in properties.iteritems():
            props["propertyId[%s]" % propCount] = key
            props["propertyValue[%s]" % propCount] = value
            propCount += 1

        ciUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + "&cmisaction=checkin"

        contentType, body = encode_multipart_formdata(props, contentFile, contentType)

        # invoke the URL
        result = self._cmisClient.binding.post(ciUrl.encode('utf-8'),
                                               body,
                                               contentType,
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result))

    def getLatestVersion(self, **kwargs):

        """
        Returns a :class:`Document` object representing the latest version in
        the version series.

        The following optional arguments are supported:
         - major
         - filter
         - includeRelationships
         - includePolicyIds
         - renditionFilter
         - includeACL
         - includeAllowableActions

        >>> latestDoc = doc.getLatestVersion()
        >>> latestDoc.getProperties()['cmis:versionLabel']
        u'2.1'
        >>> latestDoc = doc.getLatestVersion(major='false')
        >>> latestDoc.getProperties()['cmis:versionLabel']
        u'2.1'
        >>> latestDoc = doc.getLatestVersion(major='true')
        >>> latestDoc.getProperties()['cmis:versionLabel']
        u'2.0'
        """

        doc = None
        if kwargs.has_key('major') and kwargs['major'] == 'true':
            doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor')
        else:
            doc = self._repository.getObject(self.getObjectId(), returnVersion='latest')

        return doc

    def getPropertiesOfLatestVersion(self, **kwargs):

        """
        Like :class:`^CmisObject.getProperties`, returns a dict of properties
        from the latest version of this object in the version series.

        The optional major and filter arguments are supported.
        """

        latestDoc = self.getLatestVersion(**kwargs)

        return latestDoc.getProperties()

    def getAllVersions(self, **kwargs):

        """
        Returns a :class:`ResultSet` of document objects for the entire
        version history of this object, including any PWC's.

        The optional filter and includeAllowableActions are
        supported.
        """

        # get the version history link
        versionsUrl = self._repository.getRootFolderUrl() + '?cmisselector=versions' + '&objectId=' + self.getObjectId()

        # invoke the URL
        result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)

        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, data={'objects': result}, serializer=VersionsSerializer())

    def getContentStream(self):

        """
        Returns the CMIS service response from invoking the 'enclosure' link.

        >>> doc.getName()
        u'sample-b.pdf'
        >>> o = open('tmp.pdf', 'wb')
        >>> result = doc.getContentStream()
        >>> o.write(result.read())
        >>> result.close()
        >>> o.close()
        >>> import os.path
        >>> os.path.getsize('tmp.pdf')
        117248

        The optional streamId argument is not yet supported.
        """

        if not self.getAllowableActions()['canGetContentStream']:
            return None

        contentUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&selector=content"
        result, content = Rest().get(contentUrl.encode('utf-8'),
                                     self._cmisClient.username,
                                     self._cmisClient.password,
                                     **self._cmisClient.extArgs)
        if result['status'] != '200':
            raise CmisException(result['status'])
        return StringIO.StringIO(content)

    def setContentStream(self, contentFile, contentType=None):

        """
        Sets the content stream on this object.

        The following optional arguments are not yet supported:
         - overwriteFlag=None
        """

        # get the root folder URL
        createDocUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + "&cmisaction=setContent"

        contentType, body = encode_multipart_formdata(None, contentFile, contentType)

        # invoke the URL
        result = self._cmisClient.binding.post(createDocUrl.encode('utf-8'),
                                               body,
                                               contentType,
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        # return the result set
        return BrowserDocument(self._cmisClient, self, data=result)

    def deleteContentStream(self):

        """
        Delete's the content stream associated with this object.
        """

        if not self.allowableActions['canDeleteContentStream']:
            raise CmisException('Not allowed to delete the content stream')

        delUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "deleteContent"}

        if self.properties.has_key('cmis:changeToken'):
            props["changeToken"] = self.properties['cmis:changeToken']

        # invoke the URL
        self._cmisClient.binding.post(delUrl.encode('utf-8'),
                                      safe_urlencode(props),
                                      'application/x-www-form-urlencoded',
                                      self._cmisClient.username,
                                      self._cmisClient.password)

        return

    def getRenditions(self):

        """
        Returns an array of :class:`Rendition` objects. The repository
        must support the Renditions capability.

        The following optional arguments are not currently supported:
         - renditionFilter
         - maxItems
         - skipCount
        """

        # if Renditions capability is None, return notsupported
        if self._repository.getCapabilities()['Renditions']:
            pass
        else:
            raise NotSupportedException

        renditions = []

        contentUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=renditions&renditionFilter=*"
        result, content = Rest().get(contentUrl.encode('utf-8'),
                                     self._cmisClient.username,
                                     self._cmisClient.password,
                                     **self._cmisClient.extArgs)
        if result['status'] != '200':
            raise CmisException(result['status'])

        resultObj = json.loads(content)
        for rendObj in resultObj:
            renditions.append(BrowserRendition(rendObj))

        return renditions

    checkedOut = property(isCheckedOut)

    def getPaths(self):
        """
        Returns the Document's paths by asking for the parents with the
        includeRelativePathSegment flag set to true, then concats the value
        of cmis:path with the relativePathSegment.
        """

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=parents&includerelativepathsegment=true"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password)

        paths = []

        for res in result:
            path = res['object']['properties']['cmis:path']['value']
            logging.debug(path)
            relativePathSegment = res['relativePathSegment']

            # concat with a slash
            # add it to the list
            paths.append(path + '/' + relativePathSegment)

        return paths


class BrowserFolder(BrowserCmisObject):

    """
    A container object that can hold other :class:`CmisObject` objects
    """

    def createFolder(self, name, properties={}, **kwargs):

        """
        Creates a new :class:`Folder` using the properties provided.
        Right now I expect a property called 'cmis:name' but I don't
        complain if it isn't there (although the CMIS provider will). If a
        cmis:name property isn't provided, the value passed in to the name
        argument will be used.

        To specify a custom folder type, pass in a property called
        cmis:objectTypeId set to the :class:`CmisId` representing the type ID
        of the instance you want to create. If you do not pass in an object
        type ID, an instance of 'cmis:folder' will be created.

        >>> subFolder = folder.createFolder('someSubfolder')
        >>> subFolder.getName()
        u'someSubfolder'

        The following optional arguments are not supported:
         - policies
         - addACEs
         - removeACEs
        """

        # get the root folder URL
        createFolderUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "createFolder",
                 "propertyId[0]": "cmis:name",
                 "propertyValue[0]": name}

        props["propertyId[1]"] = "cmis:objectTypeId"
        if properties.has_key('cmis:objectTypeId'):
            props["propertyValue[1]"] = properties['cmis:objectTypeId']
            del properties['cmis:objectTypeId']
        else:
            props["propertyValue[1]"] = "cmis:folder"

        setProps(properties, props, initialIndex=2)

        # invoke the URL
        result = self._cmisClient.binding.post(createFolderUrl.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password,
                                               **kwargs)

        # return the result set
        return BrowserFolder(self._cmisClient, self._repository, data=result)

    def createDocumentFromString(self,
                                 name,
                                 properties={},
                                 contentString=None,
                                 contentType=None,
                                 contentEncoding=None):

        """
        Creates a new document setting the content to the string provided. If
        the repository supports unfiled objects, you do not have to pass in
        a parent :class:`Folder` otherwise it is required.

        This method is essentially a convenience method that wraps your string
        with a StringIO and then calls createDocument.

        >>> testFolder.createDocumentFromString('testdoc3', contentString='hello, world', contentType='text/plain')
        """

        return self._repository.createDocumentFromString(name,
                                                         properties,
                                                         self,
                                                         contentString,
                                                         contentType,
                                                         contentEncoding)

    def createDocument(self, name, properties={}, contentFile=None,
                       contentType=None, contentEncoding=None):

        """
        Creates a new Document object in the repository using
        the properties provided.

        Right now this is basically the same as createFolder,
        but this deals with contentStreams. The common logic should
        probably be moved to CmisObject.createObject.

        The method will attempt to guess the appropriate content
        type and encoding based on the file. To specify it yourself, pass them
        in via the contentType and contentEncoding arguments.

        >>> f = open('250px-Cmis_logo.png', 'rb')
        >>> subFolder.createDocument('logo.png', contentFile=f)
        <cmislib.model.Document object at 0x10410fa10>
        >>> f.close()

        If you wanted to set one or more properties when creating the doc, pass
        in a dict, like this:

        >>> props = {'cmis:someProp':'someVal'}
        >>> f = open('250px-Cmis_logo.png', 'rb')
        >>> subFolder.createDocument('logo.png', props, contentFile=f)
        <cmislib.model.Document object at 0x10410fa10>
        >>> f.close()

        To specify a custom object type, pass in a property called
        cmis:objectTypeId set to the :class:`CmisId` representing the type ID
        of the instance you want to create. If you do not pass in an object
        type ID, an instance of 'cmis:document' will be created.

        The following optional arguments are not yet supported:
         - versioningState
         - policies
         - addACEs
         - removeACEs
        """

        return self._repository.createDocument(name,
                                               properties,
                                               self,
                                               contentFile,
                                               contentType,
                                               contentEncoding)

    def getChildren(self, **kwargs):

        """
        Returns a paged :class:`ResultSet`. The result set contains a list of
        :class:`CmisObject` objects for each child of the Folder. The actual
        type of the object returned depends on the object's CMIS base type id.
        For example, the method might return a list that contains both
        :class:`Document` objects and :class:`Folder` objects.

        >>> childrenRS = subFolder.getChildren()
        >>> children = childrenRS.getResults()

        The following optional arguments are supported:
         - maxItems
         - skipCount
         - orderBy
         - filter
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
         - includePathSegment
        """

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=children"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, result, serializer=ChildrenSerializer())

    def getDescendants(self, **kwargs):

        """
        Gets the descendants of this folder. The descendants are returned as
        a paged :class:`ResultSet` object. The result set contains a list of
        :class:`CmisObject` objects where the actual type of each object
        returned will vary depending on the object's base type id. For example,
        the method might return a list that contains both :class:`Document`
        objects and :class:`Folder` objects.

        The following optional argument is supported:
         - depth. Use depth=-1 for all descendants, which is the default if no
           depth is specified.

        >>> resultSet = folder.getDescendants()
        >>> len(resultSet.getResults())
        105
        >>> resultSet = folder.getDescendants(depth=1)
        >>> len(resultSet.getResults())
        103

        The following optional arguments *may* also work but haven't been
        tested:

         - filter
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
         - includePathSegment

        """

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=descendants"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, result, serializer=TreeSerializer())

    def getTree(self, **kwargs):

        """
        Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`,
        this method returns only the descendant objects that are folders. The
        results do not include the current folder.

        The following optional arguments are supported:
         - depth
         - filter
         - includeRelationships
         - renditionFilter
         - includeAllowableActions
         - includePathSegment

         >>> rs = folder.getTree(depth='2')
         >>> len(rs.getResults())
         3
         >>> for folder in rs.getResults().values():
         ...     folder.getTitle()
         ...
         u'subfolder2'
         u'parent test folder'
         u'subfolder'
        """

        byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=foldertree"
        result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        # return the result set
        return BrowserResultSet(self._cmisClient, self._repository, result, serializer=TreeSerializer())

    def getParent(self):

        """
        The optional filter argument is not yet supported.
        """
        if self.properties.has_key('cmis:parentId') and self.properties['cmis:parentId'] is not None:
            return BrowserFolder(self._cmisClient, self._repository, objectId=self.properties['cmis:parentId'])

    def deleteTree(self, **kwargs):

        """
        Deletes the folder and all of its descendant objects.

        >>> resultSet = subFolder.getDescendants()
        >>> len(resultSet.getResults())
        2
        >>> subFolder.deleteTree()

        The following optional arguments are supported:
         - allVersions
         - unfileObjects
         - continueOnFailure
        """

        delUrl = self._repository.getRootFolderUrl()

        props = {"objectId": self.id,
                 "cmisaction": "deleteTree"}

        # invoke the URL
        self._cmisClient.binding.post(delUrl.encode('utf-8'),
                                      safe_urlencode(props),
                                      'application/x-www-form-urlencoded',
                                      self._cmisClient.username,
                                      self._cmisClient.password,
                                      **kwargs)

        return

    def addObject(self, cmisObject, **kwargs):

        """
        Adds the specified object as a child of this object. No new object is
        created. The repository must support multifiling for this to work.

        >>> sub1 = repo.getObjectByPath("/cmislib/sub1")
        >>> sub2 = repo.getObjectByPath("/cmislib/sub2")
        >>> doc = sub1.createDocument("testdoc1")
        >>> len(sub1.getChildren())
        1
        >>> len(sub2.getChildren())
        0
        >>> sub2.addObject(doc)
        >>> len(sub2.getChildren())
        1
        >>> sub2.getChildren()[0].name
        u'testdoc1'

        The following optional arguments are NOT supported:
         - allVersions
        """
        # TODO need to add support (and unit test) for allVersions

        addUrl = self._repository.getRootFolderUrl()

        props = {"folderId": self.id,
                 "cmisaction": "addObjectToFolder",
                 "objectId": cmisObject.id}

        # invoke the URL
        result = self._cmisClient.binding.post(addUrl.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password,
                                               **kwargs)

        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result))

    def removeObject(self, cmisObject):

        """
        Removes the specified object from this folder. The repository must
        support unfiling for this to work.
        """

        remUrl = self._repository.getRootFolderUrl()

        props = {"folderId": self.id,
                 "cmisaction": "removeObjectFromFolder",
                 "objectId": cmisObject.id}

        # invoke the URL
        result = self._cmisClient.binding.post(remUrl.encode('utf-8'),
                                               safe_urlencode(props),
                                               'application/x-www-form-urlencoded',
                                               self._cmisClient.username,
                                               self._cmisClient.password)

        return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result))

    def getPaths(self):
        """
        Returns the paths as a list of strings. The spec says folders cannot
        be multi-filed, so this should always be one value. We return a list
        to be symmetric with the same method in :class:`Document`.
        """

        return [self.properties['cmis:path']]


class BrowserRelationship(BrowserCmisObject):

    """
    Defines a relationship object between two :class:`CmisObjects` objects
    """

    def getSourceId(self):

        """
        Returns the :class:`CmisId` on the source side of the relationship.
        """

        return BrowserCmisId(self.properties['cmis:sourceId'])

    def getTargetId(self):

        """
        Returns the :class:`CmisId` on the target side of the relationship.
        """

        return BrowserCmisId(self.properties['cmis:targetId'])

    def getSource(self):

        """
        Returns an instance of the appropriate child-type of :class:`CmisObject`
        for the source side of the relationship.
        """

        sourceId = self.getSourceId()
        return getSpecializedObject(self._repository.getObject(sourceId))

    def getTarget(self):

        """
        Returns an instance of the appropriate child-type of :class:`CmisObject`
        for the target side of the relationship.
        """

        targetId = self.getTargetId()
        return getSpecializedObject(self._repository.getObject(targetId))

    sourceId = property(getSourceId)
    targetId = property(getTargetId)
    source = property(getSource)
    target = property(getTarget)


class BrowserPolicy(CmisObject):

    """
    An arbirary object that can 'applied' to objects that the
    repository identifies as being 'controllable'.
    """

    pass


class BrowserObjectType(ObjectType):

    """
    Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'.
    Contains metadata about the type.
    """

    def __init__(self, cmisClient, repository, typeId=None, data=None):
        """ Constructor """
        self._cmisClient = cmisClient
        self._repository = repository
        self._extArgs = None
        self._typeId = typeId
        self.data = data
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserObjectType')
        self.logger.debug('Creating an instance of BrowserObjectType')

    def __str__(self):
        """To string"""
        return self.getTypeId()

    def getTypeId(self):

        """
        Returns the type ID for this object.

        >>> docType = repo.getTypeDefinition('cmis:document')
        >>> docType.getTypeId()
        'cmis:document'
        """

        if self._typeId is None:
            if self.data is None:
                self.reload()
            self._typeId = CmisId(self.data['id'])

        return self._typeId

    def getLocalName(self):
        """Getter for cmis:localName"""
        if self.data is None:
            self.reload()
        return self.data['localName']

    def getLocalNamespace(self):
        """Getter for cmis:localNamespace"""
        if self.data is None:
            self.reload()
        return self.data['localNamespace']

    def getDisplayName(self):
        """Getter for cmis:displayName"""

        if self.data is None:
            self.reload()
        return self.data['displayName']

    def getQueryName(self):
        """Getter for cmis:queryName"""
        if self.data is None:
            self.reload()
        return self.data['queryName']

    def getDescription(self):
        """Getter for cmis:description"""
        if self.data is None:
            self.reload()
        return self.data['description']

    def getBaseId(self):
        """Getter for cmis:baseId"""
        if self.data is None:
            self.reload()
        return self.data['baseId']

    def isCreatable(self):
        """Getter for cmis:creatable"""
        if self.data is None:
            self.reload()
        return self.data['creatable']

    def isFileable(self):
        """Getter for cmis:fileable"""
        if self.data is None:
            self.reload()
        return self.data['fileable']

    def isQueryable(self):
        """Getter for cmis:queryable"""
        if self.data is None:
            self.reload()
        return self.data['queryable']

    def isFulltextIndexed(self):
        """Getter for cmis:fulltextIndexed"""

        if self.data is None:
            self.reload()
        return self.data['fulltextIndexed']

    def isIncludedInSupertypeQuery(self):
        """Getter for cmis:includedInSupertypeQuery"""

        if self.data is None:
            self.reload()
        return self.data['includedInSupertypeQuery']

    def isControllablePolicy(self):
        """Getter for cmis:controllablePolicy"""

        if self.data is None:
            self.reload()
        return self.data['controllablePolicy']

    def isControllableACL(self):
        """Getter for cmis:controllableACL"""

        if self.data is None:
            self.reload()
        return self.data['controllableACL']

    def getProperties(self):

        """
        Returns a list of :class:`Property` objects representing each property
        defined for this type.

        >>> objType = repo.getTypeDefinition('cmis:relationship')
        >>> for prop in objType.properties:
        ...    print 'Id:%s' % prop.id
        ...    print 'Cardinality:%s' % prop.cardinality
        ...    print 'Description:%s' % prop.description
        ...    print 'Display name:%s' % prop.displayName
        ...    print 'Local name:%s' % prop.localName
        ...    print 'Local namespace:%s' % prop.localNamespace
        ...    print 'Property type:%s' % prop.propertyType
        ...    print 'Query name:%s' % prop.queryName
        ...    print 'Updatability:%s' % prop.updatability
        ...    print 'Inherited:%s' % prop.inherited
        ...    print 'Orderable:%s' % prop.orderable
        ...    print 'Queryable:%s' % prop.queryable
        ...    print 'Required:%s' % prop.required
        ...    print 'Open choice:%s' % prop.openChoice
        """

        if self.data is None or not self.data.has_key('propertyDefinitions'):
            self.reload()
        props = {}
        for prop in self.data['propertyDefinitions'].keys():
            props[prop] = BrowserProperty(self.data['propertyDefinitions'][prop])
        return props

    def reload(self, **kwargs):
        """
        This method will reload the object's data from the CMIS service.
        """
        if kwargs:
            if self._extArgs:
                self._extArgs.update(kwargs)

        typesUrl = self._repository.getRepositoryUrl()
        kwargs['cmisselector'] = 'typeDefinition'
        kwargs['typeId'] = self.getTypeId()
        result = self._cmisClient.binding.get(typesUrl,
                                              self._cmisClient.username,
                                              self._cmisClient.password,
                                              **kwargs)
        self.data = result

    id = property(getTypeId)
    localName = property(getLocalName)
    localNamespace = property(getLocalNamespace)
    displayName = property(getDisplayName)
    queryName = property(getQueryName)
    description = property(getDescription)
    baseId = property(getBaseId)
    creatable = property(isCreatable)
    fileable = property(isFileable)
    queryable = property(isQueryable)
    fulltextIndexed = property(isFulltextIndexed)
    includedInSupertypeQuery = property(isIncludedInSupertypeQuery)
    controllablePolicy = property(isControllablePolicy)
    controllableACL = property(isControllableACL)
    properties = property(getProperties)


class BrowserProperty(object):

    """
    This class represents an attribute or property definition of an object
    type.
    """

    def __init__(self, data):
        """Constructor"""
        self.data = data
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserProperty')
        self.logger.debug('Creating an instance of BrowserProperty')

    def __str__(self):
        """To string"""
        return self.getId()

    def getId(self):
        """Getter for cmis:id"""
        return self.data['id']

    def getLocalName(self):
        """Getter for cmis:localName"""
        return self.data['localName']

    def getLocalNamespace(self):
        """Getter for cmis:localNamespace"""
        return self.data['localNamespace']

    def getDisplayName(self):
        """Getter for cmis:displayName"""
        return self.data['displayName']

    def getQueryName(self):
        """Getter for cmis:queryName"""
        return self.data['queryName']

    def getDescription(self):
        """Getter for cmis:description"""
        return self.data['description']

    def getPropertyType(self):
        """Getter for cmis:propertyType"""
        return self.data['propertyType']

    def getCardinality(self):
        """Getter for cmis:cardinality"""
        return self.data['cardinality']

    def getUpdatability(self):
        """Getter for cmis:updatability"""
        return self.data['updatability']

    def isInherited(self):
        """Getter for cmis:inherited"""
        return self.data['inherited']

    def isRequired(self):
        """Getter for cmis:required"""
        return self.data['required']

    def isQueryable(self):
        """Getter for cmis:queryable"""
        return self.data['queryable']

    def isOrderable(self):
        """Getter for cmis:orderable"""
        return self.data['orderable']

    def isOpenChoice(self):
        """Getter for cmis:openChoice"""
        return self.data['openChoice']

    id = property(getId)
    localName = property(getLocalName)
    localNamespace = property(getLocalNamespace)
    displayName = property(getDisplayName)
    queryName = property(getQueryName)
    description = property(getDescription)
    propertyType = property(getPropertyType)
    cardinality = property(getCardinality)
    updatability = property(getUpdatability)
    inherited = property(isInherited)
    required = property(isRequired)
    queryable = property(isQueryable)
    orderable = property(isOrderable)
    openChoice = property(isOpenChoice)


class BrowserACL(ACL):

    """
    Represents the Access Control List for an object.
    """

    def __init__(self, aceList=None, data=None):

        """
        Constructor. Pass in either a list of :class:`ACE` objects or the XML
        representation of the ACL. If you have only one ACE, don't worry about
        the list--the constructor will convert it to a list for you.
        """

        self._entries = {}
        if aceList:
            for ace in aceList:
                if not isinstance(ace, BrowserACE):
                    raise CmisException('Items into the aceList must be an instance of the BrowserACEclass.')
                self._entries[ace.principalId] = ace

        if data:
            self._data = data
            self._entries = self._getEntriesFromData()
        else:
            self._data = None

        self._originalEntries = self._copy_entries()

        self.logger = logging.getLogger('cmislib.browser.binding.BrowserACL')
        self.logger.debug('Creating an instance of BrowserACL')

    def _copy_entries(self):
        """
        Internal method used to keep a copy of the original entries of ACL
        :return:
        """

        result = {}
        for principalId, ace in self._entries.iteritems():
            result[principalId] = ace.copy()
        return result

    def _getEntriesFromData(self):

        """
        Internal method used to get the ACL entries from the fetched data.
        """

        if not self._data:
            return
        result = {}
        for entry in self._data['aces']:
            principalId = entry['principal']['principalId']
            direct = entry['isDirect']
            perms = entry['permissions']
            # create an ACE
            if len(perms) > 0:
                ace = BrowserACE(principalId, perms, direct)
                # append it to the dictionary
                result[principalId] = ace
        return result

    def addEntry(self, principalId, access, direct=True):

        """
        Adds an :class:`ACE` entry to the ACL.

        >>> acl = folder.getACL()
        >>> acl.addEntry(ACE('jpotts', 'cmis:read', 'true'))
        >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
        >>> acl.getEntries()
        {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
        """

        ace = BrowserACE(principalId, access, direct)
        self._entries[ace.principalId] = ace

    def removeEntry(self, principalId):

        """
        Removes the :class:`ACE` entry given a specific principalId.

        >>> acl.getEntries()
        {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
        >>> acl.removeEntry('jsmith')
        >>> acl.getEntries()
        {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>}
        """

        if self._entries.has_key(principalId):
            del self._entries[principalId]

    def clearEntries(self):

        """
        Clears all :class:`ACE` entries from the ACL and removes the internal
        XML representation of the ACL.

        >>> acl = ACL()
        >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
        >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true'))
        >>> acl.entries
        {'jpotts': <cmislib.model.ACE object at 0x1012c7310>, 'jsmith': <cmislib.model.ACE object at 0x100528490>}
        >>> acl.clearEntries()
        >>> acl.entries
        """

        self._entries.clear()
        self._data = None

    def getEntries(self):

        """
        Returns a dictionary of :class:`ACE` objects for each Access Control
        Entry in the ACL. The key value is the ACE principalid.

        >>> acl = ACL()
        >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
        >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true'))
        >>> for ace in acl.entries.values():
        ...     print 'principal:%s has the following permissions...' % ace.principalId
        ...     for perm in ace.permissions:
        ...             print perm
        ...
        principal:jpotts has the following permissions...
        cmis:write
        principal:jsmith has the following permissions...
        cmis:write
        """

        if self._entries:
            return self._entries
        else:
            if self._data:
                # parse data and build entry list
                self._entries = self._getEntriesFromData()
                # then return it
                return self._entries

    entries = property(getEntries)

    def getOriginalEntries(self):

        """
        Returns the original entries.
        """

        return self._originalEntries

    originalEntries = property(getOriginalEntries)

    def getRemovedAces(self):

        """
        Returns a list of removed ACE. The list is based on a difference
        between the original data and the current state

        """
        entries = self.entries
        originalEntries = self.originalEntries
        removedAces = []
        for principalId, original in originalEntries.iteritems():
            current = entries.get(principalId)
            if not current:
                removedAces.append(original.copy())
                continue
            if current.direct != original.direct:
                removedAces.append(original.copy())
                continue
            originalPerms = set(original.permissions)
            currentPerms = set(current.permissions)
            removedPerms = originalPerms - currentPerms
            if removedPerms:
                removedAces.append(BrowserACE(
                    principalId=principalId,
                    permissions=list(removedPerms),
                    direct=original.direct
                ))
        return removedAces

    def getAddedAces(self):

        """
        Returns the list of new ACE. The list is based on a difference
        between the original data and the current state

        """
        entries = self.entries
        originalEntries = self.originalEntries
        addedAces = []
        for principalId, current in entries.iteritems():
            original = originalEntries.get(principalId)
            if not original:
                addedAces.append(current.copy())
                continue
            if current.direct != original.direct:
                addedAces.append(current.copy())
                continue
            originalPerms = set(original.permissions)
            currentPerms = set(current.permissions)
            addedPerms = currentPerms - originalPerms
            if addedPerms:
                addedAces.append(BrowserACE(
                    principalId=principalId,
                    permissions=list(addedPerms),
                    direct=current.direct
                ))
        return addedAces


class BrowserACE(ACE):

    """
    Represents an ACE retrieved with the Browser Binding.
    """

    pass


class BrowserChangeEntry(ChangeEntry):

    """
    Represents a change log entry. Retrieve a list of change entries via
    :meth:`Repository.getContentChanges`.

    >>> for changeEntry in rs:
    ...     changeEntry.objectId
    ...     changeEntry.id
    ...     changeEntry.changeType
    ...     changeEntry.changeTime
    ...
    'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b'
    u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b'
    u'created'
    datetime.datetime(2010, 2, 11, 12, 55, 14)
    'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923'
    u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923'
    u'updated'
    datetime.datetime(2010, 2, 11, 12, 55, 13)
    'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
    u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
    u'updated'
    """

    def __init__(self, cmisClient, repository, data):
        """Constructor"""
        self._cmisClient = cmisClient
        self._repository = repository
        self._data = data
        self._properties = {}
        self._objectId = None
        self._changeEntryId = None
        self._changeType = None
        self._changeTime = None
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserChangeEntry')
        self.logger.debug('Creating an instance of BrowserChangeEntry')

    def getId(self):
        """
        Returns the unique ID of the change entry. This is not actually required
        by the spec and is absent in the browser binding for the Apache chemistry
        in-memory repository.
        """
        if self._changeEntryId is None:
            if 'id' in self._data.keys():
                self._changeEntryId = self._data.get('id')
        return self._changeEntryId

    def getObjectId(self):
        """
        Returns the object ID of the object that changed.
        """
        if self._objectId is None:
            if 'cmis:objectId' in self._data.get('properties').keys():
                self._objectId = self._data.get('properties').get('cmis:objectId').get('value')
        return self._objectId

    def getChangeType(self):

        """
        Returns the type of change that occurred. The resulting value must be
        one of:

         - created
         - updated
         - deleted
         - security
        """
        if self._changeType is None:
            self._changeType = self._data.get('changeEventInfo').get('changeType')

        return self._changeType

    def getACL(self):

        """
        Gets the :class:`ACL` object that is included with this Change Entry.
        """
        # TODO need to implement
        pass

    def getChangeTime(self):

        """
        Returns a datetime object representing the time the change occurred.
        """
        if self._changeTime is None:
            if 'changeTime' in self._data.get('changeEventInfo').keys():
                self._changeTime = self._data.get('changeEventInfo').get('changeTime')

        return parseDateTimeValue(self._changeTime)

    def getProperties(self):

        """
        Returns the properties of the change entry. Note that depending on the
        capabilities of the repository ("capabilityChanges") the list may not
        include the actual property values that changed.
        """
        if not self._properties:
            props = self._data.get('properties')
            for prop in props.itervalues():
                # property could be multi-valued
                if type(prop['value']) is list:
                    propVal = []
                    for val in prop['value']:
                        propVal.append(parsePropValueByType(val, prop['type']))
                    self._properties[prop['id']] = propVal
                else:
                    self._properties[prop['id']] = parsePropValueByType(prop['value'], prop['type'])

        return self._properties

    id = property(getId)
    objectId = property(getObjectId)
    changeTime = property(getChangeTime)
    changeType = property(getChangeType)
    properties = property(getProperties)


class BrowserChangeEntryResultSet(BrowserResultSet):

    """
    A specialized type of :class:`ResultSet` that knows how to instantiate
    :class:`ChangeEntry` objects. The parent class assumes children of
    :class:`CmisObject` which doesn't work for ChangeEntries.
    """

    def __iter__(self):

        """
        Overriding to make it work with a list instead of a dict.
        """

        return iter(self.getResults())

    def __getitem__(self, index):

        """
        Overriding to make it work with a list instead of a dict.
        """

        return self.getResults()[index]

    def __len__(self):

        """
        Overriding to make it work with a list instead of a dict.
        """

        return len(self.getResults())

    def getResults(self):

        """
        Overriding to make it work with a list instead of a dict.
        """
        if self._results:
            return self._results

        if self._data:
            entries = []
            for entry in self._data:
                changeEntry = BrowserChangeEntry(self._cmisClient, self._repository, entry)
                entries.append(changeEntry)

            self._results = entries

        return self._results


class BrowserRendition(object):

    """
    This class represents a Rendition.
    """

    def __init__(self, data):
        """Constructor"""
        self.data = data
        self.logger = logging.getLogger('cmislib.browser.binding.BrowserRendition')
        self.logger.debug('Creating an instance of BrowserRendition')

    def __str__(self):
        """To string"""
        return self.getStreamId()

    def getStreamId(self):
        """Getter for the rendition's stream ID"""
        return self.data['streamId']

    def getMimeType(self):
        """Getter for the rendition's mime type"""
        return self.data['mimeType']

    def getLength(self):
        """Getter for the renditions's length"""
        return self.data['length']

    def getTitle(self):
        """Getter for the renditions's title"""
        return self.data['title']

    def getKind(self):
        """Getter for the renditions's kind"""
        return self.data['kind']

    def getHeight(self):
        """Getter for the renditions's height"""
        return self.data['height']

    def getWidth(self):
        """Getter for the renditions's width"""
        return self.data['width']

    def getHref(self):
        """Getter for the renditions's href"""
        return self.data['href']

    def getRenditionDocumentId(self):
        """Getter for the renditions's width"""
        return self.data['renditionDocumentId']

    streamId = property(getStreamId)
    mimeType = property(getMimeType)
    length = property(getLength)
    title = property(getTitle)
    kind = property(getKind)
    height = property(getHeight)
    width = property(getWidth)
    href = property(getHref)
    renditionDocumentId = property(getRenditionDocumentId)


class BrowserCmisId(str):

    """
    This is a marker class to be used for Strings that are used as CMIS ID's.
    Making the objects instances of this class makes it easier to create the
    Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of
    cmis:propertyString.
    """

    pass


def setProps(properties, props, initialIndex=0):

    """
    Transform key, value from properties into props list items in the format
    expected by the HTTP POST request
    """

    i = initialIndex
    for key, val in properties.items():
        props["propertyId[%s]" % i] = key
        if hasattr(val, '__iter__'):
            j = 0
            for v in val:
                props["propertyValue[%s][%s]" % (i, j)] = v
                j += 1
        else:
            props["propertyValue[%s]" % i] = val
        i += 1


def getSpecializedObject(obj, **kwargs):

    """
    Returns an instance of the appropriate :class:`CmisObject` class or one
    of its child types depending on the specified baseType.
    """

    moduleLogger.debug('Inside getSpecializedObject')

    if 'cmis:baseTypeId' in obj.getProperties():
        baseType = obj.getProperties()['cmis:baseTypeId']
        if baseType == 'cmis:folder':
            return BrowserFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
        if baseType == 'cmis:document':
            return BrowserDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
        if baseType == 'cmis:relationship':
            return BrowserRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
        if baseType == 'cmis:policy':
            return BrowserPolicy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)

    # if the base type ID wasn't found in the props (this can happen when
    # someone runs a query that doesn't select * or doesn't individually
    # specify baseTypeId) or if the type isn't one of the known base
    # types, give the object back
    return obj


def encode_multipart_formdata(fields, contentFile, contentType):

    """
    fields is a sequence of (name, value) elements for regular form fields.
    files is a sequence of (name, filename, value) elements for data to be uploaded as files
    Return (content_type, body) ready for httplib.HTTP instance
    """

    boundary = 'aPacHeCheMIStrycMisLIb%s' % (int(time.time()))
    crlf = '\r\n'
    L = []
    fileName = None
    if fields:
        for (key, value) in fields.iteritems():
            if key == 'cmis:name':
                fileName = value
            L.append('--' + boundary)
            L.append('Content-Disposition: form-data; name="%s"' % key)
            L.append('Content-Type: text/plain; charset=utf-8')
            L.append('')
            L.append(value.encode('utf-8'))

    if contentFile:
        L.append('--' + boundary)
        L.append('Content-Disposition: form-data; name="%s"; filename=%s' % ('content', fileName))
        L.append('Content-Type: %s' % contentType)
        L.append('Content-Transfer-Encoding: binary')
        L.append('')
        L.append(contentFile.read())

    L.append('--' + boundary + '--')
    L.append('')
    body = crlf.join(L)
    content_type = 'multipart/form-data; boundary=%s' % boundary
    return content_type, body


class ResultsSerializer(object):

    """
    Responsible for serializing :class:`BrowserResultSet` objects.
    """

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        entries = []
        for obj in jsonObj['results']:
            cmisObject = getSpecializedObject(BrowserCmisObject(client,
                                                                repo,
                                                                data=obj))
            entries.append(cmisObject)

        return entries


class ChildrenSerializer(object):

    """
    Responsible for serializing lists of children.
    """

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        entries = []
        for obj in jsonObj['objects']:
            dataObj = obj['object']
            cmisObject = getSpecializedObject(BrowserCmisObject(client,
                                                                repo,
                                                                data=dataObj))
            entries.append(cmisObject)

        return entries


class RelationShipsSerializer(object):

    """
    Responsible for serializing lists of relationships.
    """

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        entries = []
        for obj in jsonObj['objects']:
            cmisObject = getSpecializedObject(BrowserCmisObject(client,
                                                                repo,
                                                                data=obj))
            entries.append(cmisObject)

        return entries


class VersionsSerializer(object):

    """
    Responsible for serializing a list of versions.
    """

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        entries = []
        for obj in jsonObj['objects']:
            cmisObject = getSpecializedObject(BrowserCmisObject(client,
                                                                repo,
                                                                data=obj))
            entries.append(cmisObject)

        return entries


# TODO Preserve tree hierarchy
class TreeSerializer(object):

    """
    The AtomPubBinding may be returning descendants and trees as a flat list of results.
    We should probably implement a Tree result set and return that here instead.
    """

    def __init__(self, treeType='object'):
        self.treeType = treeType

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        entries = self.getEntries(client, repo, jsonObj)

        return entries

    def getEntries(self, client, repo, jsonObj):
        '''obj is the list of items in the tree'''
        entries = []
        for obj in jsonObj:
            if self.treeType == 'object':
                dataObj = obj['object']['object']
                cmisThing = getSpecializedObject(BrowserCmisObject(client,
                                                                   repo,
                                                                   data=dataObj))
            elif self.treeType == 'type':
                dataObj = obj['type']
                cmisThing = BrowserObjectType(client, repo, data=dataObj)
            else:
                raise CmisException("Invalid tree type")

            entries.append(cmisThing)
            try:
                dataObj = obj['children']
                # if obj['object'].has_key('children'):
                #    for child in obj['object']['children']:
                childEntries = self.getEntries(client, repo, dataObj)
                entries = entries + childEntries
            except KeyError:
                pass

        return entries


class FolderSerializer(object):

    """
    Responsible for serializing :class:`Folder` objects.
    """

    def fromJSON(self, client, repo, jsonString):
        """Transforms the folder from JSON to an object."""
        obj = json.loads(jsonString)
        objectId = obj['succinctProperties']['cmis:objectId']
        folder = BrowserFolder(client, repo, objectId, properties=obj['succinctProperties'])
        return folder


class ACLSerializer(object):

    """
    Responsible for serializing :class:`BrowserACL` objects.
    """

    def toJSON(self, acl):
        """ Transforms the ACL to JSON. """
        entries = acl.getEntries()
        aces = []
        for key in entries:
            entryJSON = {}
            entryJSON['isDirect'] = entries[key].direct
            entryJSON['prinipcal'] = {'principalId': entries[key].principalId}
            entryJSON['permissions'] = entries[key].permissions
            aces.append(entryJSON)

        return json.dumps(aces)


class ChangeEntrySerializer(object):

    """
    Responsible for serializing lists of change entries.
    """

    def fromJSON(self, client, repo, jsonObj):
        """Transforms from JSON to the object."""
        self.logger = logging.getLogger('cmislib.browser.binding.ChangeEntrySerializer')
        entries = []
        for obj in jsonObj['objects']:
            self.logger.debug("Parsing a change entry object")
            cmisObject = BrowserChangeEntry(client,
                                            repo,
                                            data=obj)
            self.logger.debug("Parsed a change entry object, appending")
            entries.append(cmisObject)

        return entries
