You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@chemistry.apache.org by jp...@apache.org on 2010/02/20 21:33:00 UTC
svn commit: r912218 [2/11] - in /incubator/chemistry/trunk/cmislib: ./ src/
src/cmislib.egg-info/ src/cmislib/ src/data/ src/doc/ src/doc/src/
src/doc/src/.doctrees/ src/doc/src/_static/ src/tests/
Added: incubator/chemistry/trunk/cmislib/src/cmislib/model.py
URL: http://svn.apache.org/viewvc/incubator/chemistry/trunk/cmislib/src/cmislib/model.py?rev=912218&view=auto
==============================================================================
--- incubator/chemistry/trunk/cmislib/src/cmislib/model.py (added)
+++ incubator/chemistry/trunk/cmislib/src/cmislib/model.py Sat Feb 20 20:32:57 2010
@@ -0,0 +1,3057 @@
+#
+# Licensed 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.
+#
+# Authors:
+# Jeff Potts, Optaros
+#
+"""
+Module containing the domain objects used to work with a CMIS provider.
+"""
+from cmislib.net import RESTService as Rest
+from cmislib.exceptions import CmisException, RuntimeException, \
+ ObjectNotFoundException, InvalidArgumentException, \
+ PermissionDeniedException, NotSupportedException, \
+ UpdateConflictException
+from urllib import quote_plus
+from urllib2 import HTTPError
+import re
+import mimetypes
+from xml.parsers.expat import ExpatError
+
+# 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'
+
+# 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'
+
+# Collection types
+QUERY_COLL = 'query'
+TYPES_COLL = 'types'
+CHECKED_OUT_COLL = 'checkedout'
+UNFILED_COLL = 'unfiled'
+ROOT_COLL = 'root'
+
+
+class CmisClient(object):
+
+ """
+ Handles all communication with the CMIS provider.
+ """
+
+ def __init__(self, repositoryUrl, username, password):
+
+ """
+ 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
+
+ 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'.
+
+ See CMIS specification document 2.2.2.1 getRepositories
+
+ >>> client.getRepositories()
+ [{'repositoryName': u'Main Repository', 'repositoryId': u'83beb297-a6fa-4ac5-844b-98c871c0eea9'}]
+ """
+
+ result = self.get(self.repositoryUrl)
+ 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)
+ 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
+
+ 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)
+ 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 to 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.
+ """
+
+ result = Rest().get(url,
+ username=self.username,
+ password=self.password,
+ **kwargs)
+ if type(result) == HTTPError:
+ self._processCommonErrors(result)
+ return result
+ else:
+ return minidom.parse(result)
+
+ 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 to 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`.
+ """
+
+ 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 to 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.
+ """
+
+ result = Rest().post(url,
+ payload,
+ contentType,
+ username=self.username,
+ password=self.password,
+ **kwargs)
+ if type(result) != HTTPError:
+ return minidom.parse(result)
+ elif result.code == 201:
+ return minidom.parse(result)
+ 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 to 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.
+ """
+
+ 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:
+ 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.
+
+ See CMIS specification document 3.2.4.1 Common CMIS Exceptions
+ """
+
+ if error.status == 401:
+ raise PermissionDeniedException(error.status)
+ elif error.status == 400:
+ raise InvalidArgumentException(error.status)
+ elif error.status == 404:
+ raise ObjectNotFoundException(error.status)
+ elif error.status == 403:
+ raise PermissionDeniedException(error.status)
+ elif error.status == 405:
+ raise NotSupportedException(error.status)
+ elif error.status == 409:
+ raise UpdateConflictException(error.status)
+ elif error.status == 500:
+ raise RuntimeException(error.status)
+
+ 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.
+
+ See CMIS specification document 2.1.1 Repository
+ """
+
+ 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 = {}
+
+ 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.xmlDoc = self._cmisClient.get(self._cmisClient.repositoryUrl)
+ 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 = {}
+
+ 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.
+
+ See CMIS specification document 2.2.2.2 getRepositoryInfo
+
+ >>> 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 = parseValue(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`
+
+ See CMIS specification document 2.2.2.3 getTypeChildren
+
+ 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', ATOM_XML_FEED_TYPE_P)
+ typesXmlDoc = self._cmisClient.get(childrenUrl)
+ 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.
+
+ See CMIS specification document 2.2.2.4 getTypeDescendants
+
+ >>> 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', 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, **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.
+
+ See CMIS specification document 2.2.2.5 getTypeDefinition
+
+ >>> 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.
+
+ See CMIS specification document 2.2.3.6 getCheckedOutDocs
+
+ >>> 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.
+
+ See CMIS specification document 2.2.4.7 getObject
+
+ >>> 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))
+
+ def getObjectByPath(self, path):
+
+ """
+ Returns an object given the path to the object.
+
+ See CMIS specification document 2.2.4.9 getObjectByPath
+
+ >>> 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_plus(path, '/'),
+ '{filter}': '',
+ '{includeAllowableActions}': 'false',
+ '{includePolicyIds}': 'false',
+ '{includeRelationships}': 'false',
+ '{includeACL}': 'false',
+ '{renditionFilter}': ''}
+ byObjectPathUrl = multiple_replace(params, template)
+
+ # do a GET against the URL
+ result = self._cmisClient.get(byObjectPathUrl)
+ 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]))
+
+ 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 \*".
+
+ See CMIS specification document 2.2.6.1 query
+
+ >>> 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()
+ result = self._cmisClient.post(queryUrl,
+ xmlDoc.toxml(),
+ CMIS_QUERY_TYPE)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return ResultSet(self._cmisClient, self, result)
+
+ def getContentChanges(self):
+
+ """
+ See CMIS specification document 2.2.6.2 getContentChanges
+
+ The following optional arguments are not yet supported:
+ - changeLogToken
+ - includeProperties
+ - includePolicyIDs
+ - includeACL
+ - maxItems
+ """
+
+ if self.getCapabilities()['Changes'] == None:
+ raise NotSupportedException
+ else:
+ raise NotImplementedError
+
+ 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.
+
+ See CMIS specification document 2.2.4.1 createDocument
+
+ >>> f = open('sample-a.pdf', 'rb')
+ >>> doc = folder.createDocument('sample-a.pdf', contentFile=f)
+ <cmislib.model.Document object at 0x105be5e10>
+ >>> f.close()
+ >>> doc.getTitle()
+ u'sample-a.pdf'
+
+ The following optional arguments are not currently supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ # if you didn't pass in a parent folder
+ if parentFolder == None:
+ # if the repository doesn't require fileable objects to be filed
+ if self.getCapabilities()['Unfiling']:
+ # has not been implemented
+ raise NotImplementedError
+ else:
+ # this repo requires fileable objects to be filed
+ raise InvalidArgumentException
+
+ return parentFolder.createDocument(name, properties, contentFile,
+ contentType, contentEncoding)
+
+ def createDocumentFromSource(self,
+ sourceId,
+ properties={},
+ parentFolder=None):
+ """
+ This is not yet implemented.
+
+ See CMIS specification document 2.2.4.2 createDocumentFromSource
+
+ 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.
+
+ See CMIS specification document 2.2.4.3 createFolder
+
+ >>> 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.
+
+ See CMIS specification document 2.2.4.4 createRelationship
+
+ 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.
+
+ See CMIS specification document 2.2.4.5 createPolicy
+
+ 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), **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)
+
+
+class ResultSet():
+
+ """
+ Represents a paged result set. In CMIS, this is most often an Atom feed.
+ """
+
+ def __init__(self, cmisClient, repository, xmlDoc):
+ ''' Constructor '''
+ self._cmisClient = cmisClient
+ self._repository = repository
+ self._xmlDoc = xmlDoc
+ self._results = []
+
+ def __iter__(self):
+ return self.getResults().itervalues()
+
+ def __getitem__(self, index):
+ return self.getResults().values()[index]
+
+ def __len__(self):
+ 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)
+ 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._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[cmisObject.getObjectId()] = cmisObject
+
+ self._results = entries
+
+ return self._results
+
+ 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
+
+ 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 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}': 'false',
+ '{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, **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.reload()
+ props = self.getProperties()
+ self._objectId = props['cmis:objectId']
+ return self._objectId
+
+ def getObjectParents(self):
+ """
+ This has not yet been implemented.
+
+ See CMIS specification document 2.2.3.5 getObjectParents
+
+ The following optional arguments are not supported:
+ - filter
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ - includeRelativePathSegment
+ """
+
+ # TODO To be implemented
+ raise NotImplementedError
+
+ 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
+
+ See CMIS specification document 2.2.4.6 getAllowableActions
+ """
+
+ 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 = parseValue(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.
+
+ See CMIS specification document 2.2.4.8 getProperties
+
+ >>> 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]:
+ #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:
+ propertyValue = node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data
+ 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.
+
+ See CMIS specification document 2.2.4.12 updateProperties
+
+ >>> 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'
+
+ The optional changeToken is not yet supported.
+ """
+
+ # TODO need to support the changeToken
+
+ # get the self link
+ selfUrl = self._getSelfLink()
+
+ # build the entry based on the properties provided
+ xmlEntryDoc = self._getEntryXmlDoc(properties)
+
+ # do a PUT of the entry
+ updatedXmlDoc = self._cmisClient.put(selfUrl,
+ xmlEntryDoc.toxml(),
+ ATOM_XML_TYPE)
+
+ # 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, targetFolderId, sourceFolderId):
+
+ """
+ This is not yet implemented.
+
+ See CMIS specification document 2.2.4.13 move
+ """
+
+ raise NotImplementedError
+
+ 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.
+
+ See CMIS specification document 2.2.4.14 delete
+
+ >>> folder.delete()
+
+ The optional allVersions argument is supported.
+ """
+
+ url = self._getSelfLink()
+ result = self._cmisClient.delete(url, **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ def applyPolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+
+ See CMIS specification document 2.2.9.1 applyPolicy
+ """
+
+ # 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, relType):
+
+ """
+ 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}
+
+ """
+
+ props = {}
+ props['cmis:sourceId'] = self.getObjectId()
+ props['cmis:targetId'] = targetObj.getObjectId()
+ props['cmis:objectTypeId'] = relType
+ xmlDoc = self._getEntryXmlDoc(props)
+
+ url = self._getLink(RELATIONSHIPS_REL)
+ assert url != None, 'Could not determine relationships URL'
+
+ result = self._cmisClient.post(url,
+ xmlDoc.toxml(),
+ 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.
+
+ See CMIS specification document 2.2.8.1 getObjectRelationships
+
+ >>> 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, **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.
+
+ See CMIS specification document 2.2.9.2 removePolicy
+ """
+
+ # 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.
+
+ See CMIS specification document 2.2.9.3 getAppliedPolicies
+ """
+
+ # 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.
+
+ See CMIS specification document 2.2.10.1 getACL
+
+ 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
+ raise NotImplementedError
+ else:
+ raise NotSupportedException
+
+ def applyACL(self):
+
+ """
+ Repository.getCapabilities['ACL'] must return manage.
+
+ See CMIS specification document 2.2.10.2 applyACL
+
+ The following optional arguments are currently not supported:
+ - addACEs
+ - removeACEs
+ - ACLPropagation
+ """
+
+ 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
+ raise NotImplementedError
+ else:
+ raise NotSupportedException
+
+ def _getSelfLink(self):
+
+ """
+ Returns the URL used to retrieve this object.
+ """
+
+ url = self._getLink('self')
+
+ 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
+
+ def _getEmptyXmlDoc(self):
+
+ """
+ Internal helper method that knows how to build an empty Atom entry.
+ """
+
+ entryXmlDoc = minidom.Document()
+ entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry")
+ entryElement.setAttribute('xmlns', ATOM_NS)
+ entryXmlDoc.appendChild(entryElement)
+ return entryXmlDoc
+
+ def _getEntryXmlDoc(self, 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.
+ """
+
+ 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")
+ contentElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:content')
+ 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.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)
+
+ for propName, propValue in properties.items():
+ """
+ the name of the element here is significant. maybe rather
+ than a simple string, I should be passing around property
+ objects because I kind of need to know the type.
+ It may be possible to guess a date time from a string,
+ but an ID will be harder.
+
+ for now I'll just guess the type based on the property name.
+ """
+ # TODO: Need to support property types other than String, Id,
+ # and DateTime see 2.1.2.1 Property
+ # TODO: Need a less hackish way to determine property type
+ if propName.endswith('String'):
+ propElementName = 'cmis:propertyString'
+ elif propName.endswith('Id'):
+ propElementName = 'cmis:propertyId'
+ elif propName.endswith('Date') or propName.endswith('DateTime'):
+ propElementName = 'cmis:propertyDateTime'
+ else:
+ propElementName = 'cmis:propertyString'
+
+ propElement = entryXmlDoc.createElementNS(CMIS_NS, propElementName)
+ propElement.setAttribute('propertyDefinitionId', propName)
+ valElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:value')
+ val = entryXmlDoc.createTextNode(propValue)
+ valElement.appendChild(val)
+ propElement.appendChild(valElement)
+ propsElement.appendChild(propElement)
+
+ return entryXmlDoc
+
+ allowableActions = property(getAllowableActions)
+ name = property(getName)
+ id = property(getObjectId)
+ properties = property(getProperties)
+ title = property(getTitle)
+
+
+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`
+
+ See CMIS specification document 2.2.7.1 checkout
+
+ >>> 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 = self._getEntryXmlDoc(properties)
+
+ # post it to to the checkedout collection URL
+ result = self._cmisClient.post(checkoutUrl,
+ entryXmlDoc.toxml(),
+ 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.
+
+ See CMIS specification document 2.2.7.2 cancelCheckOut
+
+ >>> 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 parseValue(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).
+
+ See CMIS specification document 2.2.7.3 checkIn
+
+ >>> 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 = self._getEmptyXmlDoc()
+
+ # Get the self link
+ # Do a PUT of the empty ATOM to the self link
+ url = self._getSelfLink()
+ result = self._cmisClient.put(url, entryXmlDoc.toxml(), 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. This is retrieved by
+ See CMIS specification document 2.2.7.4 getObjectOfLatestVersion
+
+ 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.
+
+ See CMIS specification document 2.2.7.4 getPropertiesOfLatestVersion
+
+ 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.
+
+ See CMIS specification document 2.2.7.5 getAllVersions
+
+ 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, **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.
+
+ See CMIS specification document 2.2.4.10 getContentStream
+
+ >>> 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,
+ username=self._cmisClient.username,
+ password=self._cmisClient.password)
+ 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):
+
+ """
+ See CMIS specification document 2.2.4.16 setContentStream
+
+ The following optional arguments are not yet supported:
+ - overwriteFlag=None,
+ - changeToken=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.'
+
+ # build the Atom entry
+ #xmlDoc = self._getEntryXmlDoc(contentFile=contentFile)
+
+ # post the Atom entry
+ result = self._cmisClient.put(srcUrl, contentFile.read(), ATOM_XML_TYPE)
+ 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, changeToken=None):
+
+ """
+ See CMIS specification document 2.2.4.17 deleteContentStream
+ """
+
+ # 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.'
+
+ # delete the content stream
+ result = self._cmisClient.delete(srcUrl)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ def getRenditions(self):
+
+ """
+ This is not yet supported.
+
+ See CMIS specification document 2.2.4.11 getRenditions
+
+ The following optional arguments are not currently supported:
+ - renditionFilter
+ - maxItems
+ - skipCount
+ """
+
+ # if Renditions capability is None, return notsupported
+ if self._repository.getCapabilities()['Renditions']:
+ raise NotImplementedError
+ else:
+ raise NotSupportedException
+
+ checkedOut = property(isCheckedOut)
+
+
+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)
+
+ See CMIS specification document 2.2.4.3 createFolder
+
+ >>> subFolder = folder.createFolder('someSubfolder')
+ >>> subFolder.getName()
+ u'someSubfolder'
+
+ The following optional arguments are not yet 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'] = 'cmis:folder'
+
+ # build the Atom entry
+ entryXml = self._getEntryXmlDoc(properties)
+
+ # post the Atom entry
+ result = self._cmisClient.post(postUrl,
+ entryXml.toxml(),
+ 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 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()
+
+ The following optional arguments are not yet supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ # get the folder represented by folderId.
+ # we'll use his 'children' link post the new child
+ postUrl = self.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'] = 'cmis:document'
+
+ # build the Atom entry
+ xmlDoc = self._getEntryXmlDoc(properties, contentFile,
+ contentType, contentEncoding)
+
+ # post the Atom entry
+ result = self._cmisClient.post(postUrl, xmlDoc.toxml(), 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._repository, xmlDoc=result)
+
+ 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.
+
+ See CMIS specification document 2.2.3.1 getChildren
+
+ >>> 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, **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
+ if url.find("?") >= 0:
+ url = url[:url.find("?")]
+
+ 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.
+
+ See CMIS specification document 2.2.3.2 getDescendants
+
+ 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, **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.
+
+ See CMIS specification document 2.2.3.3 getFolderTree
+
+ 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, **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.
+
+ See CMIS specification document 2.2.3.4 getFolderParent
+
[... 477 lines stripped ...]