You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@chemistry.apache.org by jp...@apache.org on 2013/02/27 22:39:05 UTC

svn commit: r1450978 [2/6] - in /chemistry/cmislib/trunk/src: cmislib/__init__.py cmislib/atompub_binding.py cmislib/browser_binding.py cmislib/cmis_services.py cmislib/domain.py cmislib/model.py cmislib/net.py cmislib/util.py tests/cmislibtest.py

Added: chemistry/cmislib/trunk/src/cmislib/atompub_binding.py
URL: http://svn.apache.org/viewvc/chemistry/cmislib/trunk/src/cmislib/atompub_binding.py?rev=1450978&view=auto
==============================================================================
--- chemistry/cmislib/trunk/src/cmislib/atompub_binding.py (added)
+++ chemistry/cmislib/trunk/src/cmislib/atompub_binding.py Wed Feb 27 21:39:05 2013
@@ -0,0 +1,4274 @@
+#
+#      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 Atom Pub binding-specific objects used to work with a CMIS
+provider.
+"""
+from cmis_services import RepositoryServiceIfc
+from cmis_services import Binding
+from domain import CmisId, CmisObject, Repository, Relationship, Policy, ObjectType, Property, Folder, Document, ACL, ACE, ChangeEntry, ResultSet, ChangeEntryResultSet, Rendition
+from net import RESTService as Rest
+from exceptions import CmisException, RuntimeException, \
+    ObjectNotFoundException, InvalidArgumentException, \
+    PermissionDeniedException, NotSupportedException, \
+    UpdateConflictException
+from util import parseDateTimeValue
+import messages
+
+from urllib import quote
+from urllib2 import HTTPError
+from urlparse import urlparse, urlunparse
+import re
+import mimetypes
+from xml.parsers.expat import ExpatError
+import datetime
+import time
+import iso8601
+import StringIO
+import logging
+from xml.dom import minidom
+from util import multiple_replace, parsePropValue, parseBoolValue, toCMISValue
+
+moduleLogger = logging.getLogger('cmislib.atompubbinding')
+
+# Namespaces
+ATOM_NS = 'http://www.w3.org/2005/Atom'
+APP_NS = 'http://www.w3.org/2007/app'
+CMISRA_NS = 'http://docs.oasis-open.org/ns/cmis/restatom/200908/'
+CMIS_NS = 'http://docs.oasis-open.org/ns/cmis/core/200908/'
+
+# Content types
+# Not all of these patterns have variability, but some do. It seemed cleaner
+# just to treat them all like patterns to simplify the matching logic
+ATOM_XML_TYPE = 'application/atom+xml'
+ATOM_XML_ENTRY_TYPE = 'application/atom+xml;type=entry'
+ATOM_XML_ENTRY_TYPE_P = re.compile('^application/atom\+xml.*type.*entry')
+ATOM_XML_FEED_TYPE = 'application/atom+xml;type=feed'
+ATOM_XML_FEED_TYPE_P = re.compile('^application/atom\+xml.*type.*feed')
+CMIS_TREE_TYPE = 'application/cmistree+xml'
+CMIS_TREE_TYPE_P = re.compile('^application/cmistree\+xml')
+CMIS_QUERY_TYPE = 'application/cmisquery+xml'
+CMIS_ACL_TYPE = 'application/cmisacl+xml'
+
+# Standard rels
+DOWN_REL = 'down'
+FIRST_REL = 'first'
+LAST_REL = 'last'
+NEXT_REL = 'next'
+PREV_REL = 'prev'
+SELF_REL = 'self'
+UP_REL = 'up'
+TYPE_DESCENDANTS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/typedescendants'
+VERSION_HISTORY_REL = 'version-history'
+FOLDER_TREE_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/foldertree'
+RELATIONSHIPS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/relationships'
+ACL_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/acl'
+CHANGE_LOG_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/changes'
+POLICIES_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/policies'
+RENDITION_REL = 'alternate'
+
+# Collection types
+QUERY_COLL = 'query'
+TYPES_COLL = 'types'
+CHECKED_OUT_COLL = 'checkedout'
+UNFILED_COLL = 'unfiled'
+ROOT_COLL = 'root'
+
+class AtomPubBinding(Binding):
+    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)
+        if resp['status'] != '200':
+            self._processCommonErrors(resp, url)
+            return content
+        else:
+            try:
+                return minidom.parseString(content)
+            except ExpatError:
+                raise CmisException('Could not parse server response', url)
+
+    def delete(self, url, username, password, **kwargs):
+
+        """
+        Does a delete 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 delete a folder you'd call :class:`Folder.delete` and
+        to delete a document you'd call :class:`Document.delete`.
+        """
+
+        # 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().delete(url,
+                               username=username,
+                               password=password,
+                               **kwargs)
+        if resp['status'] != '200':
+            self._processCommonErrors(resp, url)
+            return content
+        else:
+            pass
+
+    def post(self, url, username, password, payload, contentType, **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)
+
+        resp, content = Rest().post(url,
+                             payload,
+                             contentType,
+                             username=username,
+                             password=password,
+                             **kwargs)
+        if resp['status'] == '200':
+            try:
+                return minidom.parseString(content)
+            except ExpatError:
+                raise CmisException('Could not parse server response', url)
+        elif resp['status'] == '201':
+            try:
+                return minidom.parseString(content)
+            except ExpatError:
+                raise CmisException('Could not parse server response', url)
+        else:
+            self._processCommonErrors(resp, url)
+            return resp
+
+    def put(self, url, username, password, payload, contentType, **kwargs):
+
+        """
+        Does a put 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)
+
+        resp, content = Rest().put(url,
+                            payload,
+                            contentType,
+                            username=username,
+                            password=password,
+                            **kwargs)
+        if resp['status'] != '200' and resp['status'] != '201':
+            self._processCommonErrors(resp, url)
+            return content
+        else:
+            #if result.headers['content-length'] != '0':
+            try:
+                return minidom.parseString(content)
+            except ExpatError:
+                # This may happen and is normal
+                return None
+
+
+class RepositoryService(RepositoryServiceIfc):
+    def __init__(self):
+        self._uriTemplates = {}
+
+    def reload(self, obj):
+        self.logger.debug('Reload called on object')
+        obj.xmlDoc = obj._cmisClient.binding.get(obj._cmisClient.repositoryUrl.encode('utf-8'),
+                                         obj._cmisClient.username,
+                                         obj._cmisClient.password)
+        obj._initData()
+        
+    def getRepository(self, client, repositoryId):
+        doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+        workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace')
+
+        for workspaceElement in workspaceElements:
+            idElement = workspaceElement.getElementsByTagNameNS(CMIS_NS, 'repositoryId')
+            if idElement[0].childNodes[0].data == repositoryId:
+                return AtomPubRepository(self, workspaceElement)
+
+        raise ObjectNotFoundException(url=client.repositoryUrl)
+
+    def getRepositories(self, client):
+        result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+        if (type(result) == HTTPError):
+            raise RuntimeException()
+
+        workspaceElements = result.getElementsByTagNameNS(APP_NS, 'workspace')
+        # instantiate a Repository object using every workspace element
+        # in the service URL then ask the repository object for its ID
+        # and name, and return that back
+
+        repositories = []
+        for node in [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE]:
+            repository = AtomPubRepository(client, node)
+            repositories.append({'repositoryId': repository.getRepositoryId(),
+                                 'repositoryName': repository.getRepositoryInfo()['repositoryName']})
+        return repositories
+
+    def getDefaultRepository(self, client):
+        doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+        workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace')
+        # instantiate a Repository object with the first workspace
+        # element we find
+        repository = AtomPubRepository(client, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0])
+        return repository
+
+
+class UriTemplate(dict):
+
+    """
+    Simple dictionary to represent the data stored in
+    a URI template entry.
+    """
+
+    def __init__(self, template, templateType, mediaType):
+
+        """
+        Constructor
+        """
+
+        dict.__init__(self)
+        self['template'] = template
+        self['type'] = templateType
+        self['mediaType'] = mediaType
+
+
+class AtomPubCmisObject(CmisObject):
+
+    def __init__(self, cmisClient, repository, objectId=None, xmlDoc=None, **kwargs):
+        """ Constructor """
+        self._cmisClient = cmisClient
+        self._repository = repository
+        self._objectId = objectId
+        self._name = None
+        self._properties = {}
+        self._allowableActions = {}
+        self.xmlDoc = xmlDoc
+        self._kwargs = kwargs
+        self.logger = logging.getLogger('cmislib.model.CmisObject')
+        self.logger.info('Creating an instance of CmisObject')
+
+    def __str__(self):
+        """To string"""
+        return self.getObjectId()
+
+    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
+        '*'.
+        """
+
+        self.logger.debug('Reload called on CmisObject')
+        if kwargs:
+            if self._kwargs:
+                self._kwargs.update(kwargs)
+            else:
+                self._kwargs = kwargs
+
+        templates = self._repository.getUriTemplates()
+        template = templates['objectbyid']['template']
+
+        # Doing some refactoring here. Originally, we snagged the template
+        # and then "filled in" the template based on the args passed in.
+        # However, some servers don't provide a full template which meant
+        # supported optional args wouldn't get passed in using the fill-the-
+        # template approach. What's going on now is that the template gets
+        # filled in where it can, but if additional, non-templated args are
+        # passed in, those will get tacked on to the query string as
+        # "additional" options.
+
+        params = {
+              '{id}': self.getObjectId(),
+              '{filter}': '',
+              '{includeAllowableActions}': 'false',
+              '{includePolicyIds}': 'false',
+              '{includeRelationships}': '',
+              '{includeACL}': 'false',
+              '{renditionFilter}': ''}
+
+        options = {}
+        addOptions = {}  # args specified, but not in the template
+        for k, v in self._kwargs.items():
+            pKey = "{" + k + "}"
+            if template.find(pKey) >= 0:
+                options[pKey] = toCMISValue(v)
+            else:
+                addOptions[k] = toCMISValue(v)
+
+        # merge the templated args with the default params
+        params.update(options)
+
+        # fill in the template
+        byObjectIdUrl = multiple_replace(params, template)
+
+        self.xmlDoc = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
+                                                   self._cmisClient.username,
+                                                   self._cmisClient.password,
+                                                   **addOptions)
+        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 options.has_key('returnVersion') or addOptions.has_key('returnVersion'):
+            self._objectId = None
+
+    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 XML from the
+        service.
+        """
+
+        self._properties = {}
+        self._name = None
+        self._allowableActions = {}
+
+    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 == None:
+            if self.xmlDoc == None:
+                self.logger.debug('Both objectId and xmlDoc 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
+        """
+        # get the appropriate 'up' link
+        parentUrl = self._getLink(UP_REL)
+
+        if parentUrl == None:
+            raise NotSupportedException('Root folder does not support getObjectParents')
+
+        # invoke the URL
+        result = self._cmisClient.binding.get(parentUrl.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **kwargs)
+
+        # return the result set
+        return AtomPubResultSet(self._cmisClient, self._repository, result)
+
+    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)
+            allowElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'allowableActions')
+            assert len(allowElements) == 1, "Expected response to have exactly one allowableActions element"
+            allowElement = allowElements[0]
+            for node in [e for e in allowElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+                actionName = node.localName
+                actionValue = parseBoolValue(node.childNodes[0].data)
+                self._allowableActions[actionName] = actionValue
+
+        return self._allowableActions
+
+    def getTitle(self):
+
+        """
+        Returns the value of the object's cmis:title property.
+        """
+
+        if self.xmlDoc == None:
+            self.reload()
+
+        titleElement = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'title')[0]
+
+        if titleElement and titleElement.childNodes:
+            return titleElement.childNodes[0].data
+
+    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.
+        """
+
+        #TODO implement filter
+        if self._properties == {}:
+            if self.xmlDoc == None:
+                self.reload()
+            propertiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0]
+            #cpattern = re.compile(r'^property([\w]*)')
+            for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMIS_NS]:
+                #propertyId, propertyString, propertyDateTime
+                #propertyType = cpattern.search(node.localName).groups()[0]
+                propertyName = node.attributes['propertyDefinitionId'].value
+                if node.childNodes and \
+                   node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \
+                   node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes:
+                    valNodeList = node.getElementsByTagNameNS(CMIS_NS, 'value')
+                    if (len(valNodeList) == 1):
+                        propertyValue = parsePropValue(valNodeList[0].
+                                                       childNodes[0].data,
+                                                       node.localName)
+                    else:
+                        propertyValue = []
+                        for valNode in valNodeList:
+                            propertyValue.append(parsePropValue(valNode.
+                                                       childNodes[0].data,
+                                                       node.localName))
+                else:
+                    propertyValue = None
+                self._properties[propertyName] = propertyValue
+
+            for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]:
+                propertyName = node.nodeName
+                if node.childNodes:
+                    propertyValue = node.firstChild.nodeValue
+                else:
+                    propertyValue = None
+                self._properties[propertyName] = propertyValue
+
+        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'
+        """
+
+        if self._name == None:
+            self._name = self.getProperties()['cmis:name']
+        return self._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'
+
+        """
+
+        self.logger.debug('Inside updateProperties')
+
+        # get the self link
+        selfUrl = self._getSelfLink()
+
+        # if we have a change token, we must pass it back, per the spec
+        args = {}
+        if (self.properties.has_key('cmis:changeToken') and
+            self.properties['cmis:changeToken'] != None):
+            self.logger.debug('Change token present, adding it to args')
+            args = {"changeToken": self.properties['cmis:changeToken']}
+
+        # the getEntryXmlDoc function may need the object type
+        objectTypeId = None
+        if (self.properties.has_key('cmis:objectTypeId') and
+            not properties.has_key('cmis:objectTypeId')):
+            objectTypeId = self.properties['cmis:objectTypeId']
+            self.logger.debug('This object type is:%s' % objectTypeId)
+
+        # build the entry based on the properties provided
+        xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties)
+
+        self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml())
+
+        # do a PUT of the entry
+        updatedXmlDoc = self._cmisClient.binding.put(selfUrl.encode('utf-8'),
+                                             self._cmisClient.username,
+                                             self._cmisClient.password,
+                                             xmlEntryDoc.toxml(encoding='utf-8'),
+                                             ATOM_XML_TYPE,
+                                             **args)
+
+        # reset the xmlDoc for this object with what we got back from
+        # the PUT, then call initData we dont' want to call
+        # self.reload because we've already got the parsed XML--
+        # there's no need to fetch it again
+        self.xmlDoc = updatedXmlDoc
+        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)
+        """
+
+        postUrl = targetFolder.getChildrenLink()
+
+        args = {"sourceFolderId": sourceFolder.id}
+
+        # post the Atom entry
+        result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
+                                               self._cmisClient.username,
+                                               self._cmisClient.password,
+                                               self.xmlDoc.toxml(encoding='utf-8'),
+                                               ATOM_XML_ENTRY_TYPE,
+                                               **args)
+
+    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.
+        """
+
+        url = self._getSelfLink()
+        result = self._cmisClient.binding.delete(url.encode('utf-8'),
+                                         self._cmisClient.username,
+                                         self._cmisClient.password,
+                                         **kwargs)
+
+    def applyPolicy(self, policyId):
+
+        """
+        This is not yet implemented.
+        """
+
+        # depends on this object's canApplyPolicy allowable action
+        if self.getAllowableActions()['canApplyPolicy']:
+            raise NotImplementedError
+        else:
+            raise CmisException('This object has canApplyPolicy set to false')
+
+    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 isinstance(relTypeId, str):
+            relTypeId = CmisId(relTypeId)
+
+        props = {}
+        props['cmis:sourceId'] = self.getObjectId()
+        props['cmis:targetId'] = targetObj.getObjectId()
+        props['cmis:objectTypeId'] = relTypeId
+        xmlDoc = getEntryXmlDoc(self._repository, properties=props)
+
+        url = self._getLink(RELATIONSHIPS_REL)
+        assert url != None, 'Could not determine relationships URL'
+
+        result = self._cmisClient.binding.post(url.encode('utf-8'),
+                                               self._cmisClient.username,
+                                               self._cmisClient.password,
+                                               xmlDoc.toxml(encoding='utf-8'),
+                                               ATOM_XML_TYPE)
+
+        # instantiate CmisObject objects with the results and return the list
+        entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry')
+        assert(len(entryElements) == 1), "Expected entry element in result from relationship URL post"
+        return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0]))
+
+    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
+        """
+
+        url = self._getLink(RELATIONSHIPS_REL)
+        assert url != None, 'Could not determine relationships URL'
+
+        result = self._cmisClient.binding.get(url.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **kwargs)
+
+        # return the result set
+        return AtomPubResultSet(self._cmisClient, self._repository, result)
+
+    def removePolicy(self, policyId):
+
+        """
+        This is not yet implemented.
+        """
+
+        # depends on this object's canRemovePolicy allowable action
+        if self.getAllowableActions()['canRemovePolicy']:
+            raise NotImplementedError
+        else:
+            raise CmisException('This object has canRemovePolicy set to false')
+
+    def getAppliedPolicies(self):
+
+        """
+        This is not yet implemented.
+        """
+
+        # depends on this object's canGetAppliedPolicies allowable action
+        if self.getAllowableActions()['canGetAppliedPolicies']:
+            raise NotImplementedError
+        else:
+            raise CmisException('This object has canGetAppliedPolicies set to false')
+
+    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._getLink(ACL_REL)
+            result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password)
+            return AtomPubACL(xmlDoc=result)
+        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.')
+            aclUrl = self._getLink(ACL_REL)
+            assert aclUrl, "Could not determine the object's ACL URL."
+            result = self._cmisClient.binding.put(aclUrl.encode('utf-8'),
+                                          self._cmisClient.username,
+                                          self._cmisClient.password,
+                                          acl.getXmlDoc().toxml(encoding='utf-8'),
+                                          CMIS_ACL_TYPE)
+            return AtomPubACL(xmlDoc=result)
+        else:
+            raise NotSupportedException
+
+    def _getSelfLink(self):
+
+        """
+        Returns the URL used to retrieve this object.
+        """
+
+        url = self._getLink(SELF_REL)
+
+        assert len(url) > 0, "Could not determine the self link."
+
+        return url
+
+    def _getLink(self, rel, ltype=None):
+
+        """
+        Returns the HREF attribute of an Atom link element for the
+        specified rel.
+        """
+
+        if self.xmlDoc == None:
+            self.reload()
+        linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+        for linkElement in linkElements:
+
+            if ltype:
+                if linkElement.attributes.has_key('rel'):
+                    relAttr = linkElement.attributes['rel'].value
+
+                    if ltype and linkElement.attributes.has_key('type'):
+                        typeAttr = linkElement.attributes['type'].value
+
+                        if relAttr == rel and ltype.match(typeAttr):
+                            return linkElement.attributes['href'].value
+            else:
+                if linkElement.attributes.has_key('rel'):
+                    relAttr = linkElement.attributes['rel'].value
+
+                    if relAttr == rel:
+                        return linkElement.attributes['href'].value
+
+    allowableActions = property(getAllowableActions)
+    name = property(getName)
+    id = property(getObjectId)
+    properties = property(getProperties)
+    title = property(getTitle)
+    ACL = property(getACL)
+
+
+class AtomPubRepository(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, xmlDoc=None):
+        """ Constructor """
+        self._cmisClient = cmisClient
+        self.xmlDoc = xmlDoc
+        self._repositoryId = None
+        self._repositoryName = None
+        self._repositoryInfo = {}
+        self._capabilities = {}
+        self._uriTemplates = {}
+        self._permDefs = {}
+        self._permMap = {}
+        self._permissions = None
+        self._propagation = None
+        self.logger = logging.getLogger('cmislib.model.Repository')
+        self.logger.info('Creating an instance of Repository')
+
+    def __str__(self):
+        """To string"""
+        return self.getRepositoryName()
+
+    def reload(self):
+        """
+        This method will re-fetch the repository's XML data from the CMIS
+        repository.
+        """
+        self.logger.debug('Reload called on object')
+        self.xmlDoc = self._cmisClient.binding.get(self._cmisClient.repositoryUrl.encode('utf-8'),
+                                                   self._cmisClient.username,
+                                                   self._cmisClient.password)
+        self._initData()
+
+    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._uriTemplates = {}
+        self._permDefs = {}
+        self._permMap = {}
+        self._permissions = None
+        self._propagation = None
+
+    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.xmlDoc == None:
+                self.reload()
+            suppEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'supportedPermissions')
+            assert len(suppEls) == 1, 'Expected the repository service document to have one element named supportedPermissions'
+            self._permissions = suppEls[0].childNodes[0].data
+
+        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)
+
+        if self._permDefs == {}:
+            if self.xmlDoc == None:
+                self.reload()
+            aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability')
+            assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability'
+            aclEl = aclEls[0]
+            perms = {}
+            for e in aclEl.childNodes:
+                if e.localName == 'permissions':
+                    permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
+                    assert len(permEls) == 1, 'Expected permissions element to have a child named permission'
+                    descEls = e.getElementsByTagNameNS(CMIS_NS, 'description')
+                    assert len(descEls) == 1, 'Expected permissions element to have a child named description'
+                    perm = permEls[0].childNodes[0].data
+                    desc = descEls[0].childNodes[0].data
+                    perms[perm] = desc
+            self._permDefs = perms
+
+        return self._permDefs
+
+    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)
+
+        if self._permMap == {}:
+            if self.xmlDoc == None:
+                self.reload()
+            aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability')
+            assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability'
+            aclEl = aclEls[0]
+            permMap = {}
+            for e in aclEl.childNodes:
+                permList = []
+                if e.localName == 'mapping':
+                    keyEls = e.getElementsByTagNameNS(CMIS_NS, 'key')
+                    assert len(keyEls) == 1, 'Expected mapping element to have a child named key'
+                    permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
+                    assert len(permEls) >= 1, 'Expected mapping element to have at least one permission element'
+                    key = keyEls[0].childNodes[0].data
+                    for permEl in permEls:
+                        permList.append(permEl.childNodes[0].data)
+                    permMap[key] = permList
+            self._permMap = permMap
+
+        return self._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)
+
+        if not self._propagation:
+            if self.xmlDoc == None:
+                self.reload()
+            propEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propagation')
+            assert len(propEls) == 1, 'Expected the repository service document to have one element named propagation'
+            self._propagation = propEls[0].childNodes[0].data
+
+        return self._propagation
+
+    def getRepositoryId(self):
+
+        """
+        Returns this repository's unique identifier
+
+        >>> repo = client.getDefaultRepository()
+        >>> repo.getRepositoryId()
+        u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
+        """
+
+        if self._repositoryId == None:
+            if self.xmlDoc == None:
+                self.reload()
+            self._repositoryId = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryId')[0].firstChild.data
+        return self._repositoryId
+
+    def getRepositoryName(self):
+
+        """
+        Returns this repository's name
+
+        >>> repo = client.getDefaultRepository()
+        >>> repo.getRepositoryName()
+        u'Main Repository'
+        """
+
+        if self._repositoryName == None:
+            if self.xmlDoc == None:
+                self.reload()
+            self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data
+        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.xmlDoc == None:
+                self.reload()
+            repoInfoElement = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'repositoryInfo')[0]
+            for node in repoInfoElement.childNodes:
+                if node.nodeType == node.ELEMENT_NODE and node.localName != 'capabilities':
+                    try:
+                        data = node.childNodes[0].data
+                    except:
+                        data = None
+                    self._repositoryInfo[node.localName] = data
+        return self._repositoryInfo
+
+    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.xmlDoc == None:
+                self.reload()
+            capabilitiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'capabilities')[0]
+            for node in [e for e in capabilitiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+                key = node.localName.replace('capability', '')
+                value = parseBoolValue(node.childNodes[0].data)
+                self._capabilities[key] = value
+        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 = AtomPubFolder(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 AtomPubFolder(self._cmisClient, self, xmlDoc=retObject.xmlDoc)
+
+    def getTypeChildren(self,
+                        typeId=None):
+
+        """
+        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
+        """
+
+        # Unfortunately, the spec does not appear to present a way to
+        # know how to get the children of a specific type without first
+        # retrieving the type, then asking it for one of its navigational
+        # links.
+
+        # if a typeId is specified, get it, then get its "down" link
+        if typeId:
+            targetType = self.getTypeDefinition(typeId)
+            childrenUrl = targetType.getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P)
+            typesXmlDoc = self._cmisClient.binding.get(childrenUrl.encode('utf-8'),
+                                                       self._cmisClient.username,
+                                                       self._cmisClient.password)
+            entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+            types = []
+            for entryElement in entryElements:
+                objectType = ObjectType(self._cmisClient,
+                                        self,
+                                        xmlDoc=entryElement)
+                types.append(objectType)
+        # otherwise, if a typeId is not specified, return
+        # the list of base types
+        else:
+            types = self.getTypeDefinitions()
+        return types
+
+    def getTypeDescendants(self, typeId=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
+
+        These optional arguments are supported:
+         - depth
+         - 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
+        """
+
+        # Unfortunately, the spec does not appear to present a way to
+        # know how to get the children of a specific type without first
+        # retrieving the type, then asking it for one of its navigational
+        # links.
+        if typeId:
+            targetType = self.getTypeDefinition(typeId)
+            descendUrl = targetType.getLink(DOWN_REL, CMIS_TREE_TYPE_P)
+
+        else:
+            descendUrl = self.getLink(TYPE_DESCENDANTS_REL)
+
+        if not descendUrl:
+            raise NotSupportedException("Could not determine the type descendants URL")
+
+        typesXmlDoc = self._cmisClient.binding.get(descendUrl.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **kwargs)
+        entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+        types = []
+        for entryElement in entryElements:
+            objectType = AtomPubObjectType(self._cmisClient,
+                                    self,
+                                    xmlDoc=entryElement)
+            types.append(objectType)
+        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.getCollectionLink(TYPES_COLL)
+        typesXmlDoc = self._cmisClient.binding.get(typesUrl,
+                                                   self._cmisClient.username,
+                                                   self._cmisClient.password,
+                                                   **kwargs)
+        entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+        types = []
+        for entryElement in entryElements:
+            objectType = AtomPubObjectType(self._cmisClient,
+                                    self,
+                                    xmlDoc=entryElement)
+            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')
+        """
+
+        objectType = AtomPubObjectType(self._cmisClient, self, typeId)
+        objectType.reload()
+        return objectType
+
+    def getLink(self, rel):
+        """
+        Returns the HREF attribute of an Atom link element for the
+        specified rel.
+        """
+        if self.xmlDoc == None:
+            self.reload()
+
+        linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+        for linkElement in linkElements:
+
+            if linkElement.attributes.has_key('rel'):
+                relAttr = linkElement.attributes['rel'].value
+
+                if relAttr == rel:
+                    return linkElement.attributes['href'].value
+
+    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
+        """
+
+        return self.getCollection(CHECKED_OUT_COLL, **kwargs)
+
+    def getUnfiledDocs(self, **kwargs):
+
+        """
+        Returns a ResultSet of :class:`CmisObject` objects that
+        are currently unfiled.
+
+        >>> rs = repo.getUnfiledDocs()
+        >>> len(rs.getResults())
+        2
+        >>> for doc in repo.getUnfiledDocs().getResults():
+        ...     doc.getTitle()
+        ...
+        u'sample-a.pdf'
+        u'sample-b.pdf'
+
+        These optional arguments are supported:
+         - folderId
+         - maxItems
+         - skipCount
+         - orderBy
+         - filter
+         - includeRelationships
+         - renditionFilter
+         - includeAllowableActions
+        """
+
+        return self.getCollection(UNFILED_COLL, **kwargs)
+
+    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(AtomPubCmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs)
+
+    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
+        """
+
+        # get the uritemplate
+        template = self.getUriTemplates()['objectbypath']['template']
+
+        # fill in the template with the path provided
+        params = {
+              '{path}': quote(path, '/'),
+              '{filter}': '',
+              '{includeAllowableActions}': 'false',
+              '{includePolicyIds}': 'false',
+              '{includeRelationships}': '',
+              '{includeACL}': 'false',
+              '{renditionFilter}': ''}
+
+        options = {}
+        addOptions = {}  # args specified, but not in the template
+        for k, v in kwargs.items():
+            pKey = "{" + k + "}"
+            if template.find(pKey) >= 0:
+                options[pKey] = toCMISValue(v)
+            else:
+                addOptions[k] = toCMISValue(v)
+
+        # merge the templated args with the default params
+        params.update(options)
+
+        byObjectPathUrl = multiple_replace(params, template)
+
+        # do a GET against the URL
+        result = self._cmisClient.binding.get(byObjectPathUrl.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **addOptions)
+
+        # instantiate CmisObject objects with the results and return the list
+        entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry')
+        assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byObjectPathUrl
+        return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **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
+        """
+
+        if self.xmlDoc == None:
+            self.reload()
+
+        # get the URL this repository uses to accept query POSTs
+        queryUrl = self.getCollectionLink(QUERY_COLL)
+
+        # build the CMIS query XML that we're going to POST
+        xmlDoc = self._getQueryXmlDoc(statement, **kwargs)
+
+        # do the POST
+        #print 'posting:%s' % xmlDoc.toxml(encoding='utf-8')
+        result = self._cmisClient.binding.post(queryUrl.encode('utf-8'),
+                                               self._cmisClient.username,
+                                               self._cmisClient.password,
+                                               xmlDoc.toxml(encoding='utf-8'),
+                                               CMIS_QUERY_TYPE)
+
+        # return the result set
+        return AtomPubResultSet(self._cmisClient, self, result)
+
+    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'] == None:
+            raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT)
+
+        changesUrl = self.getLink(CHANGE_LOG_REL)
+        result = self._cmisClient.binding.get(changesUrl.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **kwargs)
+
+        # return the result set
+        return AtomPubChangeEntryResultSet(self._cmisClient, self, result)
+
+    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 == 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
+        """
+
+        postUrl = ''
+        # if you didn't pass in a parent folder
+        if parentFolder == 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
+        else:
+            postUrl = parentFolder.getChildrenLink()
+
+        # make sure a name is set
+        properties['cmis:name'] = name
+
+        # hardcoding to cmis:document if it wasn't
+        # passed in via props
+        if not properties.has_key('cmis:objectTypeId'):
+            properties['cmis:objectTypeId'] = CmisId('cmis:document')
+        # and if it was passed in, making sure it is a CmisId
+        elif not isinstance(properties['cmis:objectTypeId'], CmisId):
+            properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId'])
+
+        # build the Atom entry
+        xmlDoc = getEntryXmlDoc(self, None, properties, contentFile,
+                                contentType, contentEncoding)
+
+        # post the Atom entry
+        result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
+                                               self._cmisClient.username,
+                                               self._cmisClient.password,
+                                               xmlDoc.toxml(encoding='utf-8'),
+                                               ATOM_XML_ENTRY_TYPE)
+
+        # what comes back is the XML for the new document,
+        # so use it to instantiate a new document
+        # then return it
+        return AtomPubDocument(self._cmisClient, self, xmlDoc=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
+        """
+        # TODO: To be implemented
+        raise NotImplementedError
+
+    def createFolder(self,
+                     parentFolder,
+                     name,
+                     properties={}):
+
+        """
+        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)
+
+    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
+        """
+        # TODO: To be implemented
+        raise NotImplementedError
+
+    def getUriTemplates(self):
+
+        """
+        Returns a list of the URI templates the repository service knows about.
+
+        >>> templates = repo.getUriTemplates()
+        >>> templates['typebyid']['mediaType']
+        u'application/atom+xml;type=entry'
+        >>> templates['typebyid']['template']
+        u'http://localhost:8080/alfresco/s/cmis/type/{id}'
+        """
+
+        if self._uriTemplates == {}:
+
+            if self.xmlDoc == None:
+                self.reload()
+
+            uriTemplateElements = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'uritemplate')
+
+            for uriTemplateElement in uriTemplateElements:
+                template = None
+                templType = None
+                mediatype = None
+
+                for node in [e for e in uriTemplateElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+                    if node.localName == 'template':
+                        template = node.childNodes[0].data
+                    elif node.localName == 'type':
+                        templType = node.childNodes[0].data
+                    elif node.localName == 'mediatype':
+                        mediatype = node.childNodes[0].data
+
+                self._uriTemplates[templType] = UriTemplate(template,
+                                                       templType,
+                                                       mediatype)
+
+        return self._uriTemplates
+
+    def getCollection(self, collectionType, **kwargs):
+
+        """
+        Returns a list of objects returned for the specified collection.
+
+        If the query collection is requested, an exception will be raised.
+        That collection isn't meant to be retrieved.
+
+        If the types collection is specified, the method returns the result of
+        `getTypeDefinitions` and ignores any optional params passed in.
+
+        >>> from cmislib.model import TYPES_COLL
+        >>> types = repo.getCollection(TYPES_COLL)
+        >>> len(types)
+        4
+        >>> types[0].getTypeId()
+        u'cmis:folder'
+
+        Otherwise, the collection URL is invoked, and a :class:`ResultSet` is
+        returned.
+
+        >>> from cmislib.model import CHECKED_OUT_COLL
+        >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
+        >>> len(resultSet.getResults())
+        1
+        """
+
+        if collectionType == QUERY_COLL:
+            raise NotSupportedException
+        elif collectionType == TYPES_COLL:
+            return self.getTypeDefinitions()
+
+        result = self._cmisClient.binding.get(self.getCollectionLink(collectionType).encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password,
+                                              **kwargs)
+
+        # return the result set
+        return AtomPubResultSet(self._cmisClient, self, result)
+
+    def getCollectionLink(self, collectionType):
+
+        """
+        Returns the link HREF from the specified collectionType
+        ('checkedout', for example).
+
+        >>> from cmislib.model import CHECKED_OUT_COLL
+        >>> repo.getCollectionLink(CHECKED_OUT_COLL)
+        u'http://localhost:8080/alfresco/s/cmis/checkedout'
+
+        """
+
+        collectionElements = self.xmlDoc.getElementsByTagNameNS(APP_NS, 'collection')
+        for collectionElement in collectionElements:
+            link = collectionElement.attributes['href'].value
+            for node in [e for e in collectionElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+                if node.localName == 'collectionType':
+                    if node.childNodes[0].data == collectionType:
+                        return link
+
+    def _getQueryXmlDoc(self, query, **kwargs):
+
+        """
+        Utility method that knows how to build CMIS query xml around the
+        specified query statement.
+        """
+
+        cmisXmlDoc = minidom.Document()
+        queryElement = cmisXmlDoc.createElementNS(CMIS_NS, "query")
+        queryElement.setAttribute('xmlns', CMIS_NS)
+        cmisXmlDoc.appendChild(queryElement)
+
+        statementElement = cmisXmlDoc.createElementNS(CMIS_NS, "statement")
+        cdataSection = cmisXmlDoc.createCDATASection(query)
+        statementElement.appendChild(cdataSection)
+        queryElement.appendChild(statementElement)
+
+        for (k, v) in kwargs.items():
+            optionElement = cmisXmlDoc.createElementNS(CMIS_NS, k)
+            optionText = cmisXmlDoc.createTextNode(v)
+            optionElement.appendChild(optionText)
+            queryElement.appendChild(optionElement)
+
+        return cmisXmlDoc
+
+    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 AtomPubResultSet(ResultSet):
+
+    """
+    Represents a paged result set. In CMIS, this is most often an Atom feed.
+    """
+
+    def __init__(self, cmisClient, repository, xmlDoc):
+        ''' Constructor '''
+        self._cmisClient = cmisClient
+        self._repository = repository
+        self._xmlDoc = xmlDoc
+        self._results = []
+        self.logger = logging.getLogger('cmislib.model.ResultSet')
+        self.logger.info('Creating an instance of ResultSet')
+
+    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 _getLink(self, rel):
+        '''
+        Returns the link found in the feed's XML for the specified rel.
+        '''
+        linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+        for linkElement in linkElements:
+
+            if linkElement.attributes.has_key('rel'):
+                relAttr = linkElement.attributes['rel'].value
+
+                if relAttr == rel:
+                    return linkElement.attributes['href'].value
+
+    def _getPageResults(self, rel):
+        '''
+        Given a specified rel, does a get using that link (if one exists)
+        and then converts the resulting XML into a dictionary of
+        :class:`CmisObject` objects or its appropriate sub-type.
+
+        The results are kept around to facilitate repeated calls without moving
+        the cursor.
+        '''
+        link = self._getLink(rel)
+        if link:
+            result = self._cmisClient.binding.get(link.encode('utf-8'),
+                                              self._cmisClient.username,
+                                              self._cmisClient.password)
+
+            # return the result
+            self._xmlDoc = result
+            self._results = []
+            return self.getResults()
+
+    def reload(self):
+
+        '''
+        Re-invokes the self link for the current set of results.
+
+        >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
+        >>> resultSet.reload()
+
+        '''
+
+        self.logger.debug('Reload called on result set')
+        self._getPageResults(SELF_REL)
+
+    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._xmlDoc:
+            entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+            entries = []
+            for entryElement in entryElements:
+                cmisObject = getSpecializedObject(AtomPubCmisObject(self._cmisClient,
+                                                                    self._repository,
+                                                                    xmlDoc=entryElement))
+                entries.append(cmisObject)
+
+            self._results = entries
+
+        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>
+        '''
+
+        return self._getPageResults(FIRST_REL)
+
+    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>
+        '''
+
+        return self._getPageResults(PREV_REL)
+
+    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>
+        '''
+
+        return self._getPageResults(NEXT_REL)
+
+    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>
+        '''
+
+        return self._getPageResults(LAST_REL)
+
+    def hasNext(self):
+
+        '''
+        Returns True if this page contains a next link.
+
+        >>> resultSet.hasNext()
+        True
+        '''
+
+        if self._getLink(NEXT_REL):
+            return True
+        else:
+            return False
+
+    def hasPrev(self):
+
+        '''
+        Returns True if this page contains a prev link. Not all CMIS providers
+        implement prev links consistently.
+
+        >>> resultSet.hasPrev()
+        True
+        '''
+
+        if self._getLink(PREV_REL):
+            return True
+        else:
+            return False
+
+    def hasFirst(self):
+
+        '''
+        Returns True if this page contains a first link. Not all CMIS providers
+        implement first links consistently.
+
+        >>> resultSet.hasFirst()
+        True
+        '''
+
+        if self._getLink(FIRST_REL):
+            return True
+        else:
+            return False
+
+    def hasLast(self):
+
+        '''
+        Returns True if this page contains a last link. Not all CMIS providers
+        implement last links consistently.
+
+        >>> resultSet.hasLast()
+        True
+        '''
+
+        if self._getLink(LAST_REL):
+            return True
+        else:
+            return False
+
+
+class AtomPubDocument(AtomPubCmisObject):
+
+    """
+    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
+        """
+
+        # get the checkedout collection URL
+        checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL)
+        assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url."
+
+        # get this document's object ID
+        # build entry XML with it
+        properties = {'cmis:objectId': self.getObjectId()}
+        entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties)
+
+        # post it to to the checkedout collection URL
+        result = self._cmisClient.binding.post(checkoutUrl.encode('utf-8'),
+                                               self._cmisClient.username,
+                                               self._cmisClient.password,
+                                               entryXmlDoc.toxml(encoding='utf-8'),
+                                               ATOM_XML_ENTRY_TYPE)
+
+        # now that the doc is checked out, we need to refresh the XML
+        # to pick up the prop updates related to a checkout
+        self.reload()
+
+        return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=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
+        """
+
+        pwcDoc = self.getPrivateWorkingCopy()
+        if pwcDoc:
+            pwcDoc.delete()
+            self.reload()
+
+    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 parseBoolValue(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, **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 supported:
+         - major
+         - properties
+         - contentStream
+         - policies
+         - addACEs
+         - removeACEs
+        """
+
+        # Add checkin to kwargs and checkinComment, if it exists
+        kwargs['checkin'] = 'true'
+        kwargs['checkinComment'] = checkinComment
+
+        # Build an empty ATOM entry
+        entryXmlDoc = getEmptyXmlDoc()
+
+        # Get the self link
+        # Do a PUT of the empty ATOM to the self link
+        url = self._getSelfLink()
+        result = self._cmisClient.binding.put(url.encode('utf-8'),
+                                      self._cmisClient.username,
+                                      self._cmisClient.password,
+                                      entryXmlDoc.toxml(encoding='utf-8'),
+                                      ATOM_XML_TYPE,
+                                      **kwargs)
+
+        return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=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._getLink(VERSION_HISTORY_REL)
+
+        # invoke the URL
+        result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'),

[... 1870 lines stripped ...]