text: #
  #      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 domain objects used to work with a CMIS provider.
  """
  from net import RESTService as Rest
  from exceptions import CmisException, RuntimeException, \
      ObjectNotFoundException, InvalidArgumentException, \
      PermissionDeniedException, NotSupportedException, \
      UpdateConflictException
  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
  
  # would kind of like to not have any parsing logic in this module,
  # but for now I'm going to put the serial/deserialization in methods
  # of the CMIS object classes
  from xml.dom import minidom
  
  # 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'
  
  moduleLogger = logging.getLogger('cmislib.model')
  
  
  class CmisClient(object):
  
      """
      Handles all communication with the CMIS provider.
      """
  
      def __init__(self, repositoryUrl, username, password, **kwargs):
  
          """
          This is the entry point to the API. You need to know the
          :param repositoryUrl: The service URL of the CMIS provider
          :param username: Username
          :param password: Password
  
          >>> client = CmisClient('http://localhost:8080/alfresco/s/cmis', 'admin', 'admin')
          """
  
          self.repositoryUrl = repositoryUrl
          self.username = username
          self.password = password
          self.extArgs = kwargs
          self.logger = logging.getLogger('cmislib.model.CmisClient')
          self.logger.info('Creating an instance of CmisClient')
  
      def __str__(self):
          """To string"""
          return 'CMIS client connection to %s' % self.repositoryUrl
  
      def getRepositories(self):
  
          """
          Returns a dict of high-level info about the repositories available at
          this service. The dict contains entries for 'repositoryId' and
          'repositoryName'.
  
          >>> client.getRepositories()
          [{'repositoryName': u'Main Repository', 'repositoryId': u'83beb297-a6fa-4ac5-844b-98c871c0eea9'}]
          """
  
          result = self.get(self.repositoryUrl, **self.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 = Repository(self, node)
              repositories.append({'repositoryId': repository.getRepositoryId(),
                                   'repositoryName': repository.getRepositoryInfo()['repositoryName']})
          return repositories
  
      def getRepository(self, repositoryId):
  
          """
          Returns the repository identified by the specified repositoryId.
  
          >>> repo = client.getRepository('83beb297-a6fa-4ac5-844b-98c871c0eea9')
          >>> repo.getRepositoryName()
          u'Main Repository'
          """
  
          doc = self.get(self.repositoryUrl, **self.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 Repository(self, workspaceElement)
  
          raise ObjectNotFoundException(url=self.repositoryUrl)
  
      def getDefaultRepository(self):
  
          """
          There does not appear to be anything in the spec that identifies
          a repository as being the default, so we'll define it to be the
          first one in the list.
  
          >>> repo = client.getDefaultRepository()
          >>> repo.getRepositoryId()
          u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
          """
  
          doc = self.get(self.repositoryUrl, **self.extArgs)
          workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace')
          # instantiate a Repository object with the first workspace
          # element we find
          repository = Repository(self, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0])
          return repository
  
      def get(self, url, **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)
  
          result = Rest().get(url,
                              username=self.username,
                              password=self.password,
                              **kwargs)
          if type(result) == HTTPError:
              self._processCommonErrors(result)
              return result
          else:
              try:
                  return minidom.parse(result)
              except ExpatError:
                  raise CmisException('Could not parse server response', url)
  
      def delete(self, url, **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)
  
          result = Rest().delete(url,
                                 username=self.username,
                                 password=self.password,
                                 **kwargs)
          if type(result) == HTTPError:
              self._processCommonErrors(result)
              return result
          else:
              pass
  
      def post(self, url, 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)
  
          result = Rest().post(url,
                               payload,
                               contentType,
                               username=self.username,
                               password=self.password,
                               **kwargs)
          if type(result) != HTTPError:
              try:
                  return minidom.parse(result)
              except ExpatError:
                  raise CmisException('Could not parse server response', url)
          elif result.code == 201:
              try:
                  return minidom.parse(result)
              except ExpatError:
                  raise CmisException('Could not parse server response', url)
          else:
              self._processCommonErrors(result)
              return result
  
      def put(self, url, 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)
  
          result = Rest().put(url,
                              payload,
                              contentType,
                              username=self.username,
                              password=self.password,
                              **kwargs)
          if type(result) == HTTPError:
              self._processCommonErrors(result)
              return result
          else:
              #if result.headers['content-length'] != '0':
              try:
                  return minidom.parse(result)
              except ExpatError:
                  # This may happen and is normal
                  return None
  
      def _processCommonErrors(self, error):
  
          """
          Maps HTTPErrors that are common to all to exceptions. Only errors
          that are truly global, like 401 not authorized, should be handled
          here. Callers should handle the rest.
          """
  
          if error.status == 401:
              raise PermissionDeniedException(error.status, error.url)
          elif error.status == 400:
              raise InvalidArgumentException(error.status, error.url)
          elif error.status == 404:
              raise ObjectNotFoundException(error.status, error.url)
          elif error.status == 403:
              raise PermissionDeniedException(error.status, error.url)
          elif error.status == 405:
              raise NotSupportedException(error.status, error.url)
          elif error.status == 409:
              raise UpdateConflictException(error.status, error.url)
          elif error.status == 500:
              raise RuntimeException(error.status, error.url)
  
      defaultRepository = property(getDefaultRepository)
      repositories = property(getRepositories)
  
  
  class Repository(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.get(self._cmisClient.repositoryUrl.encode('utf-8'))
          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 = Folder(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 Folder(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.get(childrenUrl.encode('utf-8'))
              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.get(descendUrl.encode('utf-8'), **kwargs)
          entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
          types = []
          for entryElement in entryElements:
              objectType = ObjectType(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.get(typesUrl, **kwargs)
          entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
          types = []
          for entryElement in entryElements:
              objectType = ObjectType(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 = ObjectType(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(CmisObject(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.get(byObjectPathUrl.encode('utf-8'), **addOptions)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # 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(CmisObject(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.post(queryUrl.encode('utf-8'),
                                         xmlDoc.toxml(encoding='utf-8'),
                                         CMIS_QUERY_TYPE)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(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.get(changesUrl.encode('utf-8'), **kwargs)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ChangeEntryResultSet(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.post(postUrl.encode('utf-8'), xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # what comes back is the XML for the new document,
          # so use it to instantiate a new document
          # then return it
          return Document(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.get(self.getCollectionLink(collectionType).encode('utf-8'), **kwargs)
          if (type(result) == HTTPError):
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(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 ResultSet(object):
  
      """
      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.get(link.encode('utf-8'))
              if (type(result) == HTTPError):
                  raise CmisException(result.code)
  
              # 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(CmisObject(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 CmisObject(object):
  
      """
      Common ancestor class for other CMIS domain objects such as
      :class:`Document` and :class:`Folder`.
      """
  
      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.get(byObjectIdUrl.encode('utf-8'), **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.get(parentUrl.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(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.put(selfUrl.encode('utf-8'),
                                               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.post(postUrl.encode('utf-8'), self.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      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.delete(url.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      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.post(url.encode('utf-8'),
                                         xmlDoc.toxml(encoding='utf-8'),
                                         ATOM_XML_TYPE)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # 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(CmisObject(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.get(url.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(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.get(aclUrl.encode('utf-8'))
              if type(result) == HTTPError:
                  raise CmisException(result.code)
              return ACL(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.put(aclUrl.encode('utf-8'), acl.getXmlDoc().toxml(encoding='utf-8'), CMIS_ACL_TYPE)
              if type(result) == HTTPError:
                  raise CmisException(result.code)
              return ACL(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 Document(CmisObject):
  
      """
      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.post(checkoutUrl.encode('utf-8'),
                                         entryXmlDoc.toxml(encoding='utf-8'),
                                         ATOM_XML_ENTRY_TYPE)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # 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 Document(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.put(url.encode('utf-8'), entryXmlDoc.toxml(encoding='utf-8'), ATOM_XML_TYPE, **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          return Document(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.get(versionsUrl.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(self._cmisClient, self._repository, result)
  
      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.
          """
  
          # TODO: Need to implement the streamId
  
          contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content')
  
          assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.'
  
          # if the src element exists, follow that
          if contentElements[0].attributes.has_key('src'):
              srcUrl = contentElements[0].attributes['src'].value
  
              # the cmis client class parses non-error responses
              result = Rest().get(srcUrl.encode('utf-8'),
                                  username=self._cmisClient.username,
                                  password=self._cmisClient.password,
                                  **self._cmisClient.extArgs)
              if result.code != 200:
                  raise CmisException(result.code)
              return result
          else:
              # otherwise, try to return the value of the content element
              if contentElements[0].childNodes:
                  return contentElements[0].childNodes[0].data
  
      def setContentStream(self, contentFile, contentType=None):
  
          """
          Sets the content stream on this object.
  
          The following optional arguments are not yet supported:
           - overwriteFlag=None
          """
  
          # get this object's content stream link
          contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content')
  
          assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.'
  
          # if the src element exists, follow that
          if contentElements[0].attributes.has_key('src'):
              srcUrl = contentElements[0].attributes['src'].value
  
          # there may be times when this URL is absent, but I'm not sure how to
          # set the content stream when that is the case
          assert(srcUrl), 'Unable to determine content stream URL.'
  
          # need to determine the mime type
          mimetype = contentType
          if not mimetype and hasattr(contentFile, 'name'):
              mimetype, encoding = mimetypes.guess_type(contentFile.name)
  
          if not mimetype:
              mimetype = 'application/binary'
  
          # 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']}
  
          # put the content file
          result = self._cmisClient.put(srcUrl.encode('utf-8'),
                                        contentFile.read(),
                                        mimetype,
                                        **args)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # what comes back is the XML for the updated document,
          # which is not required by the spec to be the same document
          # we just updated, so use it to instantiate a new document
          # then return it
          return Document(self._cmisClient, self._repository, xmlDoc=result)
  
      def deleteContentStream(self):
  
          """
          Delete's the content stream associated with this object.
          """
  
          # get this object's content stream link
          contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content')
  
          assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.'
  
          # if the src element exists, follow that
          if contentElements[0].attributes.has_key('src'):
              srcUrl = contentElements[0].attributes['src'].value
  
          # there may be times when this URL is absent, but I'm not sure how to
          # delete the content stream when that is the case
          assert(srcUrl), 'Unable to determine content stream URL.'
  
          # 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']}
  
          # delete the content stream
          result = self._cmisClient.delete(srcUrl.encode('utf-8'), **args)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      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
  
          if self.xmlDoc == None:
              self.reload()
  
          linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
  
          renditions = []
          for linkElement in linkElements:
  
              if linkElement.attributes.has_key('rel'):
                  relAttr = linkElement.attributes['rel'].value
  
                  if relAttr == RENDITION_REL:
                      renditions.append(Rendition(linkElement))
          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.
          """
          # 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.get(parentUrl.encode('utf-8'),
                                        filter='cmis:path',
                                        includeRelativePathSegment=True)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          paths = []
          rs = ResultSet(self._cmisClient, self._repository, result)
          for res in rs:
              path = res.properties['cmis:path']
              relativePathSegment = res.properties['cmisra:relativePathSegment']
  
              # concat with a slash
              # add it to the list
              paths.append(path + '/' + relativePathSegment)
  
          return paths
  
  
  class Folder(CmisObject):
  
      """
      A container object that can hold other :class:`CmisObject` objects
      """
  
      def createFolder(self, name, properties={}):
  
          """
          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 folder represented by folderId.
          # we'll use his 'children' link post the new child
          postUrl = self.getChildrenLink()
  
          # make sure the name property gets set
          properties['cmis:name'] = name
  
          # hardcoding to cmis:folder if it wasn't passed in via props
          if not properties.has_key('cmis:objectTypeId'):
              properties['cmis:objectTypeId'] = CmisId('cmis:folder')
          # and checking to make sure the object type ID is an instance of CmisId
          elif not isinstance(properties['cmis:objectTypeId'], CmisId):
              properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId'])
  
          # build the Atom entry
          entryXml = getEntryXmlDoc(self._repository, properties=properties)
  
          # post the Atom entry
          result = self._cmisClient.post(postUrl.encode('utf-8'),
                                         entryXml.toxml(encoding='utf-8'),
                                         ATOM_XML_ENTRY_TYPE)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # what comes back is the XML for the new folder,
          # so use it to instantiate a new folder then return it
          return Folder(self._cmisClient, self._repository, xmlDoc=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
          """
  
          # get the appropriate 'down' link
          childrenUrl = self.getChildrenLink()
          # invoke the URL
          result = self._cmisClient.get(childrenUrl.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(self._cmisClient, self._repository, result)
  
      def getChildrenLink(self):
  
          """
          Gets the Atom link that knows how to return this object's children.
          """
  
          url = self._getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P)
  
          assert len(url) > 0, "Could not find the children url"
  
          return url
  
      def getDescendantsLink(self):
  
          """
          Returns the 'down' link of type `CMIS_TREE_TYPE`
  
          >>> folder.getDescendantsLink()
          u'http://localhost:8080/alfresco/s/cmis/s/workspace:SpacesStore/i/86f6bf54-f0e8-4a72-8cb1-213599ba086c/descendants'
          """
  
          url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P)
  
          assert len(url) > 0, "Could not find the descendants url"
  
          # some servers return a depth arg as part of this URL
          # so strip it off but keep other args
          if url.find("?") >= 0:
              u = list(urlparse(url))
              u[4] = '&'.join([p for p in u[4].split('&') if not p.startswith('depth=')])
              url = urlunparse(u)
  
          return url
  
      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
  
          """
  
          if not self._repository.getCapabilities()['GetDescendants']:
              raise NotSupportedException('This repository does not support getDescendants')
  
          # default the depth to -1, which is all descendants
          if "depth" not in kwargs:
              kwargs['depth'] = -1
  
          # get the appropriate 'down' link
          descendantsUrl = self.getDescendantsLink()
  
          # invoke the URL
          result = self._cmisClient.get(descendantsUrl.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(self._cmisClient, self._repository, result)
  
      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'
          """
  
          # Get the descendants link and do a GET against it
          url = self._getLink(FOLDER_TREE_REL)
          assert url != None, 'Unable to determine folder tree link'
          result = self._cmisClient.get(url.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return ResultSet(self._cmisClient, self, result)
  
      def getParent(self):
  
          """
          This is not yet implemented.
  
          The optional filter argument is not yet supported.
          """
          # get the appropriate 'up' link
          parentUrl = self._getLink(UP_REL)
          # invoke the URL
          result = self._cmisClient.get(parentUrl.encode('utf-8'))
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # return the result set
          return Folder(self._cmisClient, self._repository, xmlDoc=result)
  
      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
          """
  
          # Per the spec, the repo must have the GetDescendants capability
          # to support deleteTree
          if not self._repository.getCapabilities()['GetDescendants']:
              raise NotSupportedException('This repository does not support deleteTree')
  
          # Get the descendants link and do a DELETE against it
          url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P)
          result = self._cmisClient.delete(url.encode('utf-8'), **kwargs)
  
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      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 supported:
           - allVersions
          """
  
          if not self._repository.getCapabilities()['Multifiling']:
              raise NotSupportedException('This repository does not support multifiling')
  
          postUrl = self.getChildrenLink()
  
          # post the Atom entry
          result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **kwargs)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      def removeObject(self, cmisObject):
  
          """
          Removes the specified object from this folder. The repository must
          support unfiling for this to work.
          """
  
          if not self._repository.getCapabilities()['Unfiling']:
              raise NotSupportedException('This repository does not support unfiling')
  
          postUrl = self._repository.getCollectionLink(UNFILED_COLL)
  
          args = {"removeFrom": self.getObjectId()}
  
          # post the Atom entry to the unfiled collection
          result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
      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 Relationship(CmisObject):
  
      """
      Defines a relationship object between two :class:`CmisObjects` objects
      """
  
      def getSourceId(self):
  
          """
          Returns the :class:`CmisId` on the source side of the relationship.
          """
  
          if self.xmlDoc == None:
              self.reload()
          props = self.getProperties()
          return CmisId(props['cmis:sourceId'])
  
      def getTargetId(self):
  
          """
          Returns the :class:`CmisId` on the target side of the relationship.
          """
  
          if self.xmlDoc == None:
              self.reload()
          props = self.getProperties()
          return CmisId(props['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 Policy(CmisObject):
  
      """
      An arbirary object that can 'applied' to objects that the
      repository identifies as being 'controllable'.
      """
  
      pass
  
  
  class ObjectType(object):
  
      """
      Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'.
      Contains metadata about the type.
      """
  
      def __init__(self, cmisClient, repository, typeId=None, xmlDoc=None):
          """ Constructor """
          self._cmisClient = cmisClient
          self._repository = repository
          self._kwargs = None
          self._typeId = typeId
          self.xmlDoc = xmlDoc
          self.logger = logging.getLogger('cmislib.model.ObjectType')
          self.logger.info('Creating an instance of ObjectType')
  
      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 == None:
              if self.xmlDoc == None:
                  self.reload()
              self._typeId = CmisId(self._getElementValue(CMIS_NS, 'id'))
  
          return self._typeId
  
      def _getElementValue(self, namespace, elementName):
  
          """
          Helper method to retrieve child element values from type XML.
          """
  
          if self.xmlDoc == None:
              self.reload()
          #typeEls = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'type')
          #assert len(typeEls) == 1, "Expected to find exactly one type element but instead found %d" % len(typeEls)
          #typeEl = typeEls[0]
          typeEl = None
          for e in self.xmlDoc.childNodes:
              if e.nodeType == e.ELEMENT_NODE and e.localName == "type":
                  typeEl = e
                  break
  
          assert typeEl, "Expected to find one child element named type"
          els = typeEl.getElementsByTagNameNS(namespace, elementName)
          if len(els) >= 1:
              el = els[0]
              if el and len(el.childNodes) >= 1:
                  return el.childNodes[0].data
  
      def getLocalName(self):
          """Getter for cmis:localName"""
          return self._getElementValue(CMIS_NS, 'localName')
  
      def getLocalNamespace(self):
          """Getter for cmis:localNamespace"""
          return self._getElementValue(CMIS_NS, 'localNamespace')
  
      def getDisplayName(self):
          """Getter for cmis:displayName"""
          return self._getElementValue(CMIS_NS, 'displayName')
  
      def getQueryName(self):
          """Getter for cmis:queryName"""
          return self._getElementValue(CMIS_NS, 'queryName')
  
      def getDescription(self):
          """Getter for cmis:description"""
          return self._getElementValue(CMIS_NS, 'description')
  
      def getBaseId(self):
          """Getter for cmis:baseId"""
          return CmisId(self._getElementValue(CMIS_NS, 'baseId'))
  
      def isCreatable(self):
          """Getter for cmis:creatable"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'creatable'))
  
      def isFileable(self):
          """Getter for cmis:fileable"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'fileable'))
  
      def isQueryable(self):
          """Getter for cmis:queryable"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable'))
  
      def isFulltextIndexed(self):
          """Getter for cmis:fulltextIndexed"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'fulltextIndexed'))
  
      def isIncludedInSupertypeQuery(self):
          """Getter for cmis:includedInSupertypeQuery"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'includedInSupertypeQuery'))
  
      def isControllablePolicy(self):
          """Getter for cmis:controllablePolicy"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'controllablePolicy'))
  
      def isControllableACL(self):
          """Getter for cmis:controllableACL"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'controllableACL'))
  
      def getLink(self, rel, linkType):
  
          """
          Gets the HREF for the link element with the specified rel and linkType.
  
          >>> from cmislib.model import ATOM_XML_FEED_TYPE
          >>> docType.getLink('down', ATOM_XML_FEED_TYPE)
          u'http://localhost:8080/alfresco/s/cmis/type/cmis:document/children'
          """
  
          linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
  
          for linkElement in linkElements:
  
              if linkElement.attributes.has_key('rel') and linkElement.attributes.has_key('type'):
                  relAttr = linkElement.attributes['rel'].value
                  typeAttr = linkElement.attributes['type'].value
  
                  if relAttr == rel and linkType.match(typeAttr):
                      return linkElement.attributes['href'].value
  
      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.xmlDoc == None:
              self.reload(includePropertyDefinitions='true')
          # Currently, property defs don't have an enclosing element. And, the
          # element name varies depending on type. Until that changes, I'm going
          # to find all elements unique to a prop, then grab its parent node.
          propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType')
          if len(propTypeElements) <= 0:
              self.reload(includePropertyDefinitions='true')
              propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType')
              assert len(propTypeElements) > 0, 'Could not retrieve object type property definitions'
          props = {}
          for typeEl in propTypeElements:
              prop = Property(typeEl.parentNode)
              props[prop.id] = prop
          return props
  
      def reload(self, **kwargs):
          """
          This method will reload the object's data from the CMIS service.
          """
          if kwargs:
              if self._kwargs:
                  self._kwargs.update(kwargs)
              else:
                  self._kwargs = kwargs
          templates = self._repository.getUriTemplates()
          template = templates['typebyid']['template']
          params = {'{id}': self._typeId}
          byTypeIdUrl = multiple_replace(params, template)
          result = self._cmisClient.get(byTypeIdUrl.encode('utf-8'), **kwargs)
          if type(result) == HTTPError:
              raise CmisException(result.code)
  
          # 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" % byTypeIdUrl
          self.xmlDoc = entryElements[0]
  
      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 Property(object):
  
      """
      This class represents an attribute or property definition of an object
      type.
      """
  
      def __init__(self, propNode):
          """Constructor"""
          self.xmlDoc = propNode
          self.logger = logging.getLogger('cmislib.model.Property')
          self.logger.info('Creating an instance of Property')
  
      def __str__(self):
          """To string"""
          return self.getId()
  
      def _getElementValue(self, namespace, elementName):
  
          """
          Utility method for retrieving element values from the object type XML.
          """
  
          els = self.xmlDoc.getElementsByTagNameNS(namespace, elementName)
          if len(els) >= 1:
              el = els[0]
              if el and len(el.childNodes) >= 1:
                  return el.childNodes[0].data
  
      def getId(self):
          """Getter for cmis:id"""
          return self._getElementValue(CMIS_NS, 'id')
  
      def getLocalName(self):
          """Getter for cmis:localName"""
          return self._getElementValue(CMIS_NS, 'localName')
  
      def getLocalNamespace(self):
          """Getter for cmis:localNamespace"""
          return self._getElementValue(CMIS_NS, 'localNamespace')
  
      def getDisplayName(self):
          """Getter for cmis:displayName"""
          return self._getElementValue(CMIS_NS, 'displayName')
  
      def getQueryName(self):
          """Getter for cmis:queryName"""
          return self._getElementValue(CMIS_NS, 'queryName')
  
      def getDescription(self):
          """Getter for cmis:description"""
          return self._getElementValue(CMIS_NS, 'description')
  
      def getPropertyType(self):
          """Getter for cmis:propertyType"""
          return self._getElementValue(CMIS_NS, 'propertyType')
  
      def getCardinality(self):
          """Getter for cmis:cardinality"""
          return self._getElementValue(CMIS_NS, 'cardinality')
  
      def getUpdatability(self):
          """Getter for cmis:updatability"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'updatability'))
  
      def isInherited(self):
          """Getter for cmis:inherited"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'inherited'))
  
      def isRequired(self):
          """Getter for cmis:required"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'required'))
  
      def isQueryable(self):
          """Getter for cmis:queryable"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable'))
  
      def isOrderable(self):
          """Getter for cmis:orderable"""
          return parseBoolValue(self._getElementValue(CMIS_NS, 'orderable'))
  
      def isOpenChoice(self):
          """Getter for cmis:openChoice"""
          return parseBoolValue(self._getElementValue(CMIS_NS, '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 ACL(object):
  
      """
      Represents the Access Control List for an object.
      """
  
      def __init__(self, aceList=None, xmlDoc=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.
          """
  
          if aceList:
              self._entries = aceList
          else:
              self._entries = {}
          if xmlDoc:
              self._xmlDoc = xmlDoc
              self._entries = self._getEntriesFromXml()
          else:
              self._xmlDoc = None
  
          self.logger = logging.getLogger('cmislib.model.ACL')
          self.logger.info('Creating an instance of ACL')
  
      def addEntry(self, ace):
  
          """
          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>}
          """
  
          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.getXmlDoc()
          <xml.dom.minidom.Document instance at 0x1012cbb90>
          >>> acl.clearEntries()
          >>> acl.entries
          >>> acl.getXmlDoc()
          """
  
          self._entries.clear()
          self._xmlDoc = 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._xmlDoc:
                  # parse XML doc and build entry list
                  self._entries = self._getEntriesFromXml()
                  # then return it
                  return self._entries
  
      def _getEntriesFromXml(self):
  
          """
          Helper method for getting the :class:`ACE` entries from an XML
          representation of the ACL.
          """
  
          if not self._xmlDoc:
              return
          result = {}
          # first child is the root node, cmis:acl
          for e in self._xmlDoc.childNodes[0].childNodes:
              if e.localName == 'permission':
                  # grab the principal/principalId element value
                  prinEl = e.getElementsByTagNameNS(CMIS_NS, 'principal')[0]
                  if prinEl and prinEl.childNodes:
                      prinIdEl = prinEl.getElementsByTagNameNS(CMIS_NS, 'principalId')[0]
                      if prinIdEl and prinIdEl.childNodes:
                          principalId = prinIdEl.childNodes[0].data
                  # grab the permission values
                  permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
                  perms = []
                  for permEl in permEls:
                      if permEl and permEl.childNodes:
                          perms.append(permEl.childNodes[0].data)
                  # grab the direct value
                  dirEl = e.getElementsByTagNameNS(CMIS_NS, 'direct')[0]
                  if dirEl and dirEl.childNodes:
                      direct = dirEl.childNodes[0].data
                  # create an ACE
                  if (len(perms) > 0):
                      ace = ACE(principalId, perms, direct)
                      # append it to the dictionary
                      result[principalId] = ace
          return result
  
      def getXmlDoc(self):
  
          """
          This method rebuilds the local XML representation of the ACL based on
          the :class:`ACE` objects in the entries list and returns the resulting
          XML Document.
          """
  
          if not self.getEntries():
              return
  
          xmlDoc = minidom.Document()
          aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl')
          aclEl.setAttribute('xmlns:cmis', CMIS_NS)
          for ace in self.getEntries().values():
              permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission')
              #principalId
              prinEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principal')
              prinIdEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principalId')
              prinIdElText = xmlDoc.createTextNode(ace.principalId)
              prinIdEl.appendChild(prinIdElText)
              prinEl.appendChild(prinIdEl)
              permEl.appendChild(prinEl)
              #direct
              directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct')
              directElText = xmlDoc.createTextNode(ace.direct)
              directEl.appendChild(directElText)
              permEl.appendChild(directEl)
              #permissions
              for perm in ace.permissions:
                  permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission')
                  permItemElText = xmlDoc.createTextNode(perm)
                  permItemEl.appendChild(permItemElText)
                  permEl.appendChild(permItemEl)
              aclEl.appendChild(permEl)
          xmlDoc.appendChild(aclEl)
          self._xmlDoc = xmlDoc
          return self._xmlDoc
  
      entries = property(getEntries)
  
  
  class ACE(object):
  
      """
      Represents an individual Access Control Entry.
      """
  
      def __init__(self, principalId=None, permissions=None, direct=None):
          """Constructor"""
          self._principalId = principalId
          if permissions:
              if isinstance(permissions, str):
                  self._permissions = [permissions]
              else:
                  self._permissions = permissions
          self._direct = direct
  
          self.logger = logging.getLogger('cmislib.model.ACE')
          self.logger.info('Creating an instance of ACE')
  
      @property
      def principalId(self):
          """Getter for principalId"""
          return self._principalId
  
      @property
      def direct(self):
          """Getter for direct"""
          return self._direct
  
      @property
      def permissions(self):
          """Getter for permissions"""
          return self._permissions
  
  
  class ChangeEntry(object):
  
      """
      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, xmlDoc):
          """Constructor"""
          self._cmisClient = cmisClient
          self._repository = repository
          self._xmlDoc = xmlDoc
          self._properties = {}
          self._objectId = None
          self._changeEntryId = None
          self._changeType = None
          self._changeTime = None
          self.logger = logging.getLogger('cmislib.model.ChangeEntry')
          self.logger.info('Creating an instance of ChangeEntry')
  
      def getId(self):
          """
          Returns the unique ID of the change entry.
          """
          if self._changeEntryId == None:
              self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data
          return self._changeEntryId
  
      def getObjectId(self):
          """
          Returns the object ID of the object that changed.
          """
          if self._objectId == None:
              props = self.getProperties()
              self._objectId = CmisId(props['cmis:objectId'])
          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 == None:
              self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data
          return self._changeType
  
      def getACL(self):
  
          """
          Gets the :class:`ACL` object that is included with this Change Entry.
          """
  
          # if you call getContentChanges with includeACL=true, you will get a
          # cmis:ACL entry. change entries don't appear to have a self URL so
          # instead of doing a reload with includeACL set to true, we'll either
          # see if the XML already has an ACL element and instantiate an ACL with
          # it, or we'll get the ACL_REL link, invoke that, and return the result
          if not self._repository.getCapabilities()['ACL']:
              return
          aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl')
          aclUrl = self._getLink(ACL_REL)
          if (len(aclEls) == 1):
              return ACL(self._cmisClient, self._repository, aclEls[0])
          elif aclUrl:
              result = self._cmisClient.get(aclUrl.encode('utf-8'))
              if type(result) == HTTPError:
                  raise CmisException(result.code)
              return ACL(xmlDoc=result)
  
      def getChangeTime(self):
  
          """
          Returns a datetime object representing the time the change occurred.
          """
  
          if self._changeTime == None:
              self._changeTime = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeTime')[0].firstChild.data
          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 self._properties == {}:
              propertiesElement = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0]
              for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
                  propertyName = node.attributes['propertyDefinitionId'].value
                  if node.childNodes and \
                     node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \
                     node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes:
                      propertyValue = parsePropValue(
                         node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data,
                         node.localName)
                  else:
                      propertyValue = None
                  self._properties[propertyName] = propertyValue
          return self._properties
  
      def _getLink(self, rel):
  
          """
          Returns the HREF attribute of an Atom link element 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
  
      id = property(getId)
      objectId = property(getObjectId)
      changeTime = property(getChangeTime)
      changeType = property(getChangeType)
      properties = property(getProperties)
  
  
  class ChangeEntryResultSet(ResultSet):
  
      """
      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._xmlDoc:
              entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
              entries = []
              for entryElement in entryElements:
                  changeEntry = ChangeEntry(self._cmisClient, self._repository, entryElement)
                  entries.append(changeEntry)
  
              self._results = entries
  
          return self._results
  
  
  class Rendition(object):
  
      """
      This class represents a Rendition.
      """
  
      def __init__(self, propNode):
          """Constructor"""
          self.xmlDoc = propNode
          self.logger = logging.getLogger('cmislib.model.Rendition')
          self.logger.info('Creating an instance of Rendition')
  
      def __str__(self):
          """To string"""
          return self.getStreamId()
  
      def getStreamId(self):
          """Getter for the rendition's stream ID"""
          if self.xmlDoc.attributes.has_key('streamId'):
              return self.xmlDoc.attributes['streamId'].value
  
      def getMimeType(self):
          """Getter for the rendition's mime type"""
          if self.xmlDoc.attributes.has_key('type'):
              return self.xmlDoc.attributes['type'].value
  
      def getLength(self):
          """Getter for the renditions's length"""
          if self.xmlDoc.attributes.has_key('length'):
              return self.xmlDoc.attributes['length'].value
  
      def getTitle(self):
          """Getter for the renditions's title"""
          if self.xmlDoc.attributes.has_key('title'):
              return self.xmlDoc.attributes['title'].value
  
      def getKind(self):
          """Getter for the renditions's kind"""
          if self.xmlDoc.hasAttributeNS(CMISRA_NS, 'renditionKind'):
              return self.xmlDoc.getAttributeNS(CMISRA_NS, 'renditionKind')
  
      def getHeight(self):
          """Getter for the renditions's height"""
          if self.xmlDoc.attributes.has_key('height'):
              return self.xmlDoc.attributes['height'].value
  
      def getWidth(self):
          """Getter for the renditions's width"""
          if self.xmlDoc.attributes.has_key('width'):
              return self.xmlDoc.attributes['width'].value
  
      def getHref(self):
          """Getter for the renditions's href"""
          if self.xmlDoc.attributes.has_key('href'):
              return self.xmlDoc.attributes['href'].value
  
      def getRenditionDocumentId(self):
          """Getter for the renditions's width"""
          if self.xmlDoc.attributes.has_key('renditionDocumentId'):
              return self.xmlDoc.attributes['renditionDocumentId'].value
  
      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 CmisId(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
  
  
  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
  
  
  def parsePropValue(value, nodeName):
  
      """
      Returns a properly-typed object based on the type as specified in the
      node's element name.
      """
  
      moduleLogger.debug('Inside parsePropValue')
  
      if nodeName == 'propertyId':
          return CmisId(value)
      elif nodeName == 'propertyString':
          return value
      elif nodeName == 'propertyBoolean':
          bDict = {'false': False, 'true': True}
          return bDict[value.lower()]
      elif nodeName == 'propertyInteger':
          return int(value)
      elif nodeName == 'propertyDecimal':
          return float(value)
      elif nodeName == 'propertyDateTime':
          #%z doesn't seem to work, so I'm going to trunc the offset
          #not all servers return microseconds, so those go too
          return parseDateTimeValue(value)
      else:
          return value
  
  
  def parseDateTimeValue(value):
  
      """
      Utility function to return a datetime from a string.
      """
      return iso8601.parse_date(value)
  
  
  def parseBoolValue(value):
  
      """
      Utility function to parse booleans and none from strings
      """
  
      if value == 'false':
          return False
      elif value == 'true':
          return True
      elif value == 'none':
          return None
      else:
          return value
  
  
  def toCMISValue(value):
  
      """
      Utility function to convert Python values to CMIS string values
      """
  
      if value == False:
          return 'false'
      elif value == True:
          return 'true'
      elif value == None:
          return 'none'
      else:
          return value
  
  
  def multiple_replace(aDict, text):
  
      """
      Replace in 'text' all occurences of any key in the given
      dictionary by its corresponding value.  Returns the new string.
  
      See http://code.activestate.com/recipes/81330/
      """
  
      # Create a regular expression  from the dictionary keys
      regex = re.compile("(%s)" % "|".join(map(re.escape, aDict.keys())))
  
      # For each match, look-up corresponding value in dictionary
      return regex.sub(lambda mo: aDict[mo.string[mo.start():mo.end()]], text)
  
  
  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 Folder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
          if baseType == 'cmis:document':
              return Document(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
          if baseType == 'cmis:relationship':
              return Relationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
          if baseType == 'cmis:policy':
              return Policy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **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 getEmptyXmlDoc():
  
      """
      Internal helper method that knows how to build an empty Atom entry.
      """
  
      moduleLogger.debug('Inside getEmptyXmlDoc')
  
      entryXmlDoc = minidom.Document()
      entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry")
      entryElement.setAttribute('xmlns', ATOM_NS)
      entryXmlDoc.appendChild(entryElement)
      return entryXmlDoc
  
  
  def getEntryXmlDoc(repo=None, objectTypeId=None, properties=None, contentFile=None,
                      contentType=None, contentEncoding=None):
  
      """
      Internal helper method that knows how to build an Atom entry based
      on the properties and, optionally, the contentFile provided.
      """
  
      moduleLogger.debug('Inside getEntryXmlDoc')
  
      entryXmlDoc = minidom.Document()
      entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry")
      entryElement.setAttribute('xmlns', ATOM_NS)
      entryElement.setAttribute('xmlns:app', APP_NS)
      entryElement.setAttribute('xmlns:cmisra', CMISRA_NS)
      entryXmlDoc.appendChild(entryElement)
  
      # if there is a File, encode it and add it to the XML
      if contentFile:
          mimetype = contentType
          encoding = contentEncoding
  
          # need to determine the mime type
          if not mimetype and hasattr(contentFile, 'name'):
              mimetype, encoding = mimetypes.guess_type(contentFile.name)
  
          if not mimetype:
              mimetype = 'application/binary'
  
          if not encoding:
              encoding = 'utf8'
  
          # This used to be ATOM_NS content but there is some debate among
          # vendors whether the ATOM_NS content must always be base64
          # encoded. The spec does mandate that CMISRA_NS content be encoded
          # and that element takes precedence over ATOM_NS content if it is
          # present, so it seems reasonable to use CMIS_RA content for now
          # and encode everything.
  
          fileData = contentFile.read().encode("base64")
          mediaElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:mediatype')
          mediaElementText = entryXmlDoc.createTextNode(mimetype)
          mediaElement.appendChild(mediaElementText)
          base64Element = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:base64')
          base64ElementText = entryXmlDoc.createTextNode(fileData)
          base64Element.appendChild(base64ElementText)
          contentElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:content')
          contentElement.appendChild(mediaElement)
          contentElement.appendChild(base64Element)
          entryElement.appendChild(contentElement)
  
      objectElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:object')
      objectElement.setAttribute('xmlns:cmis', CMIS_NS)
      entryElement.appendChild(objectElement)
  
      if properties:
          # a name is required for most things, but not for a checkout
          if properties.has_key('cmis:name'):
              titleElement = entryXmlDoc.createElementNS(ATOM_NS, "title")
              titleText = entryXmlDoc.createTextNode(properties['cmis:name'])
              titleElement.appendChild(titleText)
              entryElement.appendChild(titleElement)
  
          propsElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:properties')
          objectElement.appendChild(propsElement)
  
          typeDef = None
          for propName, propValue in properties.items():
              """
              the name of the element here is significant: it includes the
              data type. I should be able to figure out the right type based
              on the actual type of the object passed in.
  
              I could do a lookup to the type definition, but that doesn't
              seem worth the performance hit
              """
              if (propValue == None or (type(propValue) == list and propValue[0] == None)):
                  # grab the prop type from the typeDef
                  if (typeDef == None):
                      moduleLogger.debug('Looking up type def for: %s' % objectTypeId)
                      typeDef = repo.getTypeDefinition(objectTypeId)
                      #TODO what to do if type not found
                  propType = typeDef.properties[propName].propertyType
              elif type(propValue) == list:
                  propType = type(propValue[0])
              else:
                  propType = type(propValue)
  
              propElementName, propValueStrList = getElementNameAndValues(propType, propName, propValue, type(propValue) == list)
  
              propElement = entryXmlDoc.createElementNS(CMIS_NS, propElementName)
              propElement.setAttribute('propertyDefinitionId', propName)
              for val in propValueStrList:
                  if val == None:
                      continue
                  valElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:value')
                  valText = entryXmlDoc.createTextNode(val)
                  valElement.appendChild(valText)
                  propElement.appendChild(valElement)
              propsElement.appendChild(propElement)
  
      return entryXmlDoc
  
  
  def getElementNameAndValues(propType, propName, propValue, isList=False):
  
      """
      For a given property type, property name, and property value, this function
      returns the appropriate CMIS Atom entry element name and value list.
      """
  
      moduleLogger.debug('Inside getElementNameAndValues')
      moduleLogger.debug('propType:%s propName:%s isList:%s' % (propType, propName, isList))
      if (propType == 'id' or propType == CmisId):
          propElementName = 'cmis:propertyId'
          if isList:
              propValueStrList = []
              for val in propValue:
                  propValueStrList.append(val)
          else:
              propValueStrList = [propValue]
      elif (propType == 'string' or propType == str):
          propElementName = 'cmis:propertyString'
          if isList:
              propValueStrList = []
              for val in propValue:
                  propValueStrList.append(val)
          else:
              propValueStrList = [propValue]
      elif (propType == 'datetime' or propType == datetime.datetime):
          propElementName = 'cmis:propertyDateTime'
          if isList:
              propValueStrList = []
              for val in propValue:
                  if val != None:
                      propValueStrList.append(val.isoformat())
                  else:
                      propValueStrList.append(val)
          else:
              if propValue != None:
                  propValueStrList = [propValue.isoformat()]
              else:
                  propValueStrList = [propValue]
      elif (propType == 'boolean' or propType == bool):
          propElementName = 'cmis:propertyBoolean'
          if isList:
              propValueStrList = []
              for val in propValue:
                  if val != None:
                      propValueStrList.append(unicode(val).lower())
                  else:
                      propValueStrList.append(val)
          else:
              if propValue != None:
                  propValueStrList = [unicode(propValue).lower()]
              else:
                  propValueStrList = [propValue]
      elif (propType == 'integer' or propType == int):
          propElementName = 'cmis:propertyInteger'
          if isList:
              propValueStrList = []
              for val in propValue:
                  if val != None:
                      propValueStrList.append(unicode(val))
                  else:
                      propValueStrList.append(val)
          else:
              if propValue != None:
                  propValueStrList = [unicode(propValue)]
              else:
                  propValueStrList = [propValue]
      elif (propType == 'decimal' or propType == float):
          propElementName = 'cmis:propertyDecimal'
          if isList:
              propValueStrList = []
              for val in propValue:
                  if val != None:
                      propValueStrList.append(unicode(val))
                  else:
                      propValueStrList.append(val)
          else:
              if propValue != None:
                  propValueStrList = [unicode(propValue)]
              else:
                  propValueStrList = [propValue]
      else:
          propElementName = 'cmis:propertyString'
          if isList:
              propValueStrList = []
              for val in propValue:
                  if val != None:
                      propValueStrList.append(unicode(val))
                  else:
                      propValueStrList.append(val)
          else:
              if propValue != None:
                  propValueStrList = [unicode(propValue)]
              else:
                  propValueStrList = [propValue]
  
      return propElementName, propValueStrList
  