Merged cmislib trunk to cmislib-refactor branch
git-svn-id: https://svn.apache.org/repos/asf/chemistry/cmislib/branches/binding_refactor@1448892 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/cmislib/__init__.py b/src/cmislib/__init__.py
index 553b333..08ee007 100644
--- a/src/cmislib/__init__.py
+++ b/src/cmislib/__init__.py
@@ -20,6 +20,7 @@
Define package contents so that they are easy to import.
"""
-from model import CmisClient, Repository, Folder
+from model import CmisClient
+from domain import Repository, Folder
__all__ = ["CmisClient", "Repository", "Folder"]
diff --git a/src/cmislib/atompub_binding.py b/src/cmislib/atompub_binding.py
new file mode 100644
index 0000000..886d0ea
--- /dev/null
+++ b/src/cmislib/atompub_binding.py
@@ -0,0 +1,4348 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+Module containing the Atom Pub binding-specific objects used to work with a CMIS
+provider.
+"""
+from cmis_services import RepositoryServiceIfc
+from cmis_services import Binding
+from domain import CmisId, CmisObject, Repository, Relationship, Policy, ObjectType, Property, Folder, Document, ACL, ACE, ChangeEntry, ResultSet, ChangeEntryResultSet, Rendition
+from net import RESTService as Rest
+from exceptions import CmisException, RuntimeException, \
+ ObjectNotFoundException, InvalidArgumentException, \
+ PermissionDeniedException, NotSupportedException, \
+ UpdateConflictException
+from util import parseDateTimeValue
+import messages
+
+from urllib import quote
+from urllib2 import HTTPError
+from urlparse import urlparse, urlunparse
+import re
+import mimetypes
+from xml.parsers.expat import ExpatError
+import datetime
+import time
+import iso8601
+import StringIO
+import logging
+from xml.dom import minidom
+from util import multiple_replace, parsePropValue, parseBoolValue, toCMISValue
+
+moduleLogger = logging.getLogger('cmislib.atompubbinding')
+
+# Namespaces
+ATOM_NS = 'http://www.w3.org/2005/Atom'
+APP_NS = 'http://www.w3.org/2007/app'
+CMISRA_NS = 'http://docs.oasis-open.org/ns/cmis/restatom/200908/'
+CMIS_NS = 'http://docs.oasis-open.org/ns/cmis/core/200908/'
+
+# Content types
+# Not all of these patterns have variability, but some do. It seemed cleaner
+# just to treat them all like patterns to simplify the matching logic
+ATOM_XML_TYPE = 'application/atom+xml'
+ATOM_XML_ENTRY_TYPE = 'application/atom+xml;type=entry'
+ATOM_XML_ENTRY_TYPE_P = re.compile('^application/atom\+xml.*type.*entry')
+ATOM_XML_FEED_TYPE = 'application/atom+xml;type=feed'
+ATOM_XML_FEED_TYPE_P = re.compile('^application/atom\+xml.*type.*feed')
+CMIS_TREE_TYPE = 'application/cmistree+xml'
+CMIS_TREE_TYPE_P = re.compile('^application/cmistree\+xml')
+CMIS_QUERY_TYPE = 'application/cmisquery+xml'
+CMIS_ACL_TYPE = 'application/cmisacl+xml'
+
+# Standard rels
+DOWN_REL = 'down'
+FIRST_REL = 'first'
+LAST_REL = 'last'
+NEXT_REL = 'next'
+PREV_REL = 'prev'
+SELF_REL = 'self'
+UP_REL = 'up'
+TYPE_DESCENDANTS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/typedescendants'
+VERSION_HISTORY_REL = 'version-history'
+FOLDER_TREE_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/foldertree'
+RELATIONSHIPS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/relationships'
+ACL_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/acl'
+CHANGE_LOG_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/changes'
+POLICIES_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/policies'
+RENDITION_REL = 'alternate'
+
+# Collection types
+QUERY_COLL = 'query'
+TYPES_COLL = 'types'
+CHECKED_OUT_COLL = 'checkedout'
+UNFILED_COLL = 'unfiled'
+ROOT_COLL = 'root'
+
+class AtomPubBinding(Binding):
+ def __init__(self, **kwargs):
+ self.extArgs = kwargs
+
+ def getRepositoryService(self):
+ return RepositoryService()
+
+ def get(self, url, username, password, **kwargs):
+
+ """
+ Does a get against the CMIS service. More than likely, you will not
+ need to call this method. Instead, let the other objects do it for you.
+
+ For example, if you need to get a specific object by object id, try
+ :class:`Repository.getObject`. If you have a path instead of an object
+ id, use :class:`Repository.getObjectByPath`. Or, you could start with
+ the root folder (:class:`Repository.getRootFolder`) and drill down from
+ there.
+ """
+
+ # merge the cmis client extended args with the ones that got passed in
+ if (len(self.extArgs) > 0):
+ kwargs.update(self.extArgs)
+
+ result = Rest().get(url,
+ username=username,
+ password=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, username, password, **kwargs):
+
+ """
+ Does a delete against the CMIS service. More than likely, you will not
+ need to call this method. Instead, let the other objects do it for you.
+
+ For example, to delete a folder you'd call :class:`Folder.delete` and
+ to delete a document you'd call :class:`Document.delete`.
+ """
+
+ # merge the cmis client extended args with the ones that got passed in
+ if (len(self.extArgs) > 0):
+ kwargs.update(self.extArgs)
+
+ result = Rest().delete(url,
+ username=username,
+ password=password,
+ **kwargs)
+ if type(result) == HTTPError:
+ self._processCommonErrors(result)
+ return result
+ else:
+ pass
+
+ def post(self, url, username, password, payload, contentType, **kwargs):
+
+ """
+ Does a post against the CMIS service. More than likely, you will not
+ need to call this method. Instead, let the other objects do it for you.
+
+ For example, to update the properties on an object, you'd call
+ :class:`CmisObject.updateProperties`. Or, to check in a document that's
+ been checked out, you'd call :class:`Document.checkin` on the PWC.
+ """
+
+ # merge the cmis client extended args with the ones that got passed in
+ if (len(self.extArgs) > 0):
+ kwargs.update(self.extArgs)
+
+ result = Rest().post(url,
+ payload,
+ contentType,
+ username=username,
+ password=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, username, password, payload, contentType, **kwargs):
+
+ """
+ Does a put against the CMIS service. More than likely, you will not
+ need to call this method. Instead, let the other objects do it for you.
+
+ For example, to update the properties on an object, you'd call
+ :class:`CmisObject.updateProperties`. Or, to check in a document that's
+ been checked out, you'd call :class:`Document.checkin` on the PWC.
+ """
+
+ # merge the cmis client extended args with the ones that got passed in
+ if (len(self.extArgs) > 0):
+ kwargs.update(self.extArgs)
+
+ result = Rest().put(url,
+ payload,
+ contentType,
+ username=username,
+ password=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
+
+
+class RepositoryService(RepositoryServiceIfc):
+ def __init__(self):
+ self._uriTemplates = {}
+
+ def reload(self, obj):
+ self.logger.debug('Reload called on object')
+ obj.xmlDoc = obj._cmisClient.binding.get(obj._cmisClient.repositoryUrl.encode('utf-8'),
+ obj._cmisClient.username,
+ obj._cmisClient.password)
+ obj._initData()
+
+ def getRepository(self, client, repositoryId):
+ doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+ workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace')
+
+ for workspaceElement in workspaceElements:
+ idElement = workspaceElement.getElementsByTagNameNS(CMIS_NS, 'repositoryId')
+ if idElement[0].childNodes[0].data == repositoryId:
+ return AtomPubRepository(self, workspaceElement)
+
+ raise ObjectNotFoundException(url=client.repositoryUrl)
+
+ def getRepositories(self, client):
+ result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+ if (type(result) == HTTPError):
+ raise RuntimeException()
+
+ workspaceElements = result.getElementsByTagNameNS(APP_NS, 'workspace')
+ # instantiate a Repository object using every workspace element
+ # in the service URL then ask the repository object for its ID
+ # and name, and return that back
+
+ repositories = []
+ for node in [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE]:
+ repository = AtomPubRepository(client, node)
+ repositories.append({'repositoryId': repository.getRepositoryId(),
+ 'repositoryName': repository.getRepositoryInfo()['repositoryName']})
+ return repositories
+
+ def getDefaultRepository(self, client):
+ doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+ workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace')
+ # instantiate a Repository object with the first workspace
+ # element we find
+ repository = AtomPubRepository(client, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0])
+ return repository
+
+
+class UriTemplate(dict):
+
+ """
+ Simple dictionary to represent the data stored in
+ a URI template entry.
+ """
+
+ def __init__(self, template, templateType, mediaType):
+
+ """
+ Constructor
+ """
+
+ dict.__init__(self)
+ self['template'] = template
+ self['type'] = templateType
+ self['mediaType'] = mediaType
+
+
+class AtomPubCmisObject(CmisObject):
+
+ def __init__(self, cmisClient, repository, objectId=None, xmlDoc=None, **kwargs):
+ """ Constructor """
+ self._cmisClient = cmisClient
+ self._repository = repository
+ self._objectId = objectId
+ self._name = None
+ self._properties = {}
+ self._allowableActions = {}
+ self.xmlDoc = xmlDoc
+ self._kwargs = kwargs
+ self.logger = logging.getLogger('cmislib.model.CmisObject')
+ self.logger.info('Creating an instance of CmisObject')
+
+ def __str__(self):
+ """To string"""
+ return self.getObjectId()
+
+ def reload(self, **kwargs):
+
+ """
+ Fetches the latest representation of this object from the CMIS service.
+ Some methods, like :class:`^Document.checkout` do this for you.
+
+ If you call reload with a properties filter, the filter will be in
+ effect on subsequent calls until the filter argument is changed. To
+ reset to the full list of properties, call reload with filter set to
+ '*'.
+ """
+
+ self.logger.debug('Reload called on CmisObject')
+ if kwargs:
+ if self._kwargs:
+ self._kwargs.update(kwargs)
+ else:
+ self._kwargs = kwargs
+
+ templates = self._repository.getUriTemplates()
+ template = templates['objectbyid']['template']
+
+ # Doing some refactoring here. Originally, we snagged the template
+ # and then "filled in" the template based on the args passed in.
+ # However, some servers don't provide a full template which meant
+ # supported optional args wouldn't get passed in using the fill-the-
+ # template approach. What's going on now is that the template gets
+ # filled in where it can, but if additional, non-templated args are
+ # passed in, those will get tacked on to the query string as
+ # "additional" options.
+
+ params = {
+ '{id}': self.getObjectId(),
+ '{filter}': '',
+ '{includeAllowableActions}': 'false',
+ '{includePolicyIds}': 'false',
+ '{includeRelationships}': '',
+ '{includeACL}': 'false',
+ '{renditionFilter}': ''}
+
+ options = {}
+ addOptions = {} # args specified, but not in the template
+ for k, v in self._kwargs.items():
+ pKey = "{" + k + "}"
+ if template.find(pKey) >= 0:
+ options[pKey] = toCMISValue(v)
+ else:
+ addOptions[k] = toCMISValue(v)
+
+ # merge the templated args with the default params
+ params.update(options)
+
+ # fill in the template
+ byObjectIdUrl = multiple_replace(params, template)
+
+ self.xmlDoc = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **addOptions)
+ self._initData()
+
+ # if a returnVersion arg was passed in, it is possible we got back
+ # a different object ID than the value we started with, so it needs
+ # to be cleared out as well
+ if options.has_key('returnVersion') or addOptions.has_key('returnVersion'):
+ self._objectId = None
+
+ def _initData(self):
+
+ """
+ An internal method used to clear out any member variables that
+ might be out of sync if we were to fetch new XML from the
+ service.
+ """
+
+ self._properties = {}
+ self._name = None
+ self._allowableActions = {}
+
+ def getObjectId(self):
+
+ """
+ Returns the object ID for this object.
+
+ >>> doc = resultSet.getResults()[0]
+ >>> doc.getObjectId()
+ u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339'
+ """
+
+ if self._objectId == None:
+ if self.xmlDoc == None:
+ self.logger.debug('Both objectId and xmlDoc were None, reloading')
+ self.reload()
+ props = self.getProperties()
+ self._objectId = CmisId(props['cmis:objectId'])
+ return self._objectId
+
+ def getObjectParents(self, **kwargs):
+ """
+ Gets the parents of this object as a :class:`ResultSet`.
+
+ The following optional arguments are supported:
+ - filter
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ - includeRelativePathSegment
+ """
+ # get the appropriate 'up' link
+ parentUrl = self._getLink(UP_REL)
+
+ if parentUrl == None:
+ raise NotSupportedException('Root folder does not support getObjectParents')
+
+ # invoke the URL
+ result = self._cmisClient.binding.get(parentUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(self._cmisClient, self._repository, result)
+
+ def getPaths(self):
+ """
+ Returns the object's paths as a list of strings.
+ """
+ # see sub-classes for implementation
+ pass
+
+ def getAllowableActions(self):
+
+ """
+ Returns a dictionary of allowable actions, keyed off of the action name.
+
+ >>> actions = doc.getAllowableActions()
+ >>> for a in actions:
+ ... print "%s:%s" % (a,actions[a])
+ ...
+ canDeleteContentStream:True
+ canSetContentStream:True
+ canCreateRelationship:True
+ canCheckIn:False
+ canApplyACL:False
+ canDeleteObject:True
+ canGetAllVersions:True
+ canGetObjectParents:True
+ canGetProperties:True
+ """
+
+ if self._allowableActions == {}:
+ self.reload(includeAllowableActions=True)
+ allowElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'allowableActions')
+ assert len(allowElements) == 1, "Expected response to have exactly one allowableActions element"
+ allowElement = allowElements[0]
+ for node in [e for e in allowElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+ actionName = node.localName
+ actionValue = parseBoolValue(node.childNodes[0].data)
+ self._allowableActions[actionName] = actionValue
+
+ return self._allowableActions
+
+ def getTitle(self):
+
+ """
+ Returns the value of the object's cmis:title property.
+ """
+
+ if self.xmlDoc == None:
+ self.reload()
+
+ titleElement = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'title')[0]
+
+ if titleElement and titleElement.childNodes:
+ return titleElement.childNodes[0].data
+
+ def getProperties(self):
+
+ """
+ Returns a dict of the object's properties. If CMIS returns an
+ empty element for a property, the property will be in the
+ dict with a value of None.
+
+ >>> props = doc.getProperties()
+ >>> for p in props:
+ ... print "%s: %s" % (p, props[p])
+ ...
+ cmis:contentStreamMimeType: text/html
+ cmis:creationDate: 2009-12-15T09:45:35.369-06:00
+ cmis:baseTypeId: cmis:document
+ cmis:isLatestMajorVersion: false
+ cmis:isImmutable: false
+ cmis:isMajorVersion: false
+ cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339
+
+ The optional filter argument is not yet implemented.
+ """
+
+ #TODO implement filter
+ if self._properties == {}:
+ if self.xmlDoc == None:
+ self.reload()
+ propertiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0]
+ #cpattern = re.compile(r'^property([\w]*)')
+ for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMIS_NS]:
+ #propertyId, propertyString, propertyDateTime
+ #propertyType = cpattern.search(node.localName).groups()[0]
+ propertyName = node.attributes['propertyDefinitionId'].value
+ if node.childNodes and \
+ node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \
+ node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes:
+ valNodeList = node.getElementsByTagNameNS(CMIS_NS, 'value')
+ if (len(valNodeList) == 1):
+ propertyValue = parsePropValue(valNodeList[0].
+ childNodes[0].data,
+ node.localName)
+ else:
+ propertyValue = []
+ for valNode in valNodeList:
+ propertyValue.append(parsePropValue(valNode.
+ childNodes[0].data,
+ node.localName))
+ else:
+ propertyValue = None
+ self._properties[propertyName] = propertyValue
+
+ for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]:
+ propertyName = node.nodeName
+ if node.childNodes:
+ propertyValue = node.firstChild.nodeValue
+ else:
+ propertyValue = None
+ self._properties[propertyName] = propertyValue
+
+ return self._properties
+
+ def getName(self):
+
+ """
+ Returns the value of cmis:name from the getProperties() dictionary.
+ We don't need a getter for every standard CMIS property, but name
+ is a pretty common one so it seems to make sense.
+
+ >>> doc.getName()
+ u'system-overview.html'
+ """
+
+ if self._name == None:
+ self._name = self.getProperties()['cmis:name']
+ return self._name
+
+ def updateProperties(self, properties):
+
+ """
+ Updates the properties of an object with the properties provided.
+ Only provide the set of properties that need to be updated.
+
+ >>> folder = repo.getObjectByPath('/someFolder2')
+ >>> folder.getName()
+ u'someFolder2'
+ >>> props = {'cmis:name': 'someFolderFoo'}
+ >>> folder.updateProperties(props)
+ <cmislib.model.Folder object at 0x103ab1210>
+ >>> folder.getName()
+ u'someFolderFoo'
+
+ """
+
+ self.logger.debug('Inside updateProperties')
+
+ # get the self link
+ selfUrl = self._getSelfLink()
+
+ # if we have a change token, we must pass it back, per the spec
+ args = {}
+ if (self.properties.has_key('cmis:changeToken') and
+ self.properties['cmis:changeToken'] != None):
+ self.logger.debug('Change token present, adding it to args')
+ args = {"changeToken": self.properties['cmis:changeToken']}
+
+ # the getEntryXmlDoc function may need the object type
+ objectTypeId = None
+ if (self.properties.has_key('cmis:objectTypeId') and
+ not properties.has_key('cmis:objectTypeId')):
+ objectTypeId = self.properties['cmis:objectTypeId']
+ self.logger.debug('This object type is:%s' % objectTypeId)
+
+ # build the entry based on the properties provided
+ xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties)
+
+ self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml())
+
+ # do a PUT of the entry
+ updatedXmlDoc = self._cmisClient.binding.put(selfUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ xmlEntryDoc.toxml(encoding='utf-8'),
+ ATOM_XML_TYPE,
+ **args)
+
+ # reset the xmlDoc for this object with what we got back from
+ # the PUT, then call initData we dont' want to call
+ # self.reload because we've already got the parsed XML--
+ # there's no need to fetch it again
+ self.xmlDoc = updatedXmlDoc
+ self._initData()
+ return self
+
+ def move(self, sourceFolder, targetFolder):
+
+ """
+ Moves an object from the source folder to the target folder.
+
+ >>> sub1 = repo.getObjectByPath('/cmislib/sub1')
+ >>> sub2 = repo.getObjectByPath('/cmislib/sub2')
+ >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1')
+ >>> doc.move(sub1, sub2)
+ """
+
+ postUrl = targetFolder.getChildrenLink()
+
+ args = {"sourceFolderId": sourceFolder.id}
+
+ # post the Atom entry
+ result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ self.xmlDoc.toxml(encoding='utf-8'),
+ ATOM_XML_ENTRY_TYPE,
+ **args)
+ 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.binding.delete(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **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.binding.post(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ 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(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0]))
+
+ def getRelationships(self, **kwargs):
+
+ """
+ Returns a :class:`ResultSet` of :class:`Relationship` objects for each
+ relationship where the source is this object.
+
+ >>> rels = tstDoc1.getRelationships()
+ >>> len(rels.getResults())
+ 1
+ >>> rel = rels.getResults().values()[0]
+ >>> rel.getProperties()
+ {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None}
+
+ The following optional arguments are supported:
+ - includeSubRelationshipTypes
+ - relationshipDirection
+ - typeId
+ - maxItems
+ - skipCount
+ - filter
+ - includeAllowableActions
+ """
+
+ url = self._getLink(RELATIONSHIPS_REL)
+ assert url != None, 'Could not determine relationships URL'
+
+ result = self._cmisClient.binding.get(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(self._cmisClient, self._repository, result)
+
+ def removePolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+ """
+
+ # depends on this object's canRemovePolicy allowable action
+ if self.getAllowableActions()['canRemovePolicy']:
+ raise NotImplementedError
+ else:
+ raise CmisException('This object has canRemovePolicy set to false')
+
+ def getAppliedPolicies(self):
+
+ """
+ This is not yet implemented.
+ """
+
+ # depends on this object's canGetAppliedPolicies allowable action
+ if self.getAllowableActions()['canGetAppliedPolicies']:
+ raise NotImplementedError
+ else:
+ raise CmisException('This object has canGetAppliedPolicies set to false')
+
+ def getACL(self):
+
+ """
+ Repository.getCapabilities['ACL'] must return manage or discover.
+
+ >>> acl = folder.getACL()
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
+
+ The optional onlyBasicPermissions argument is currently not supported.
+ """
+
+ if self._repository.getCapabilities()['ACL']:
+ # if the ACL capability is discover or manage, this must be
+ # supported
+ aclUrl = self._getLink(ACL_REL)
+ result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+ return AtomPubACL(xmlDoc=result)
+ else:
+ raise NotSupportedException
+
+ def applyACL(self, acl):
+
+ """
+ Updates the object with the provided :class:`ACL`.
+ Repository.getCapabilities['ACL'] must return manage to invoke this
+ call.
+
+ >>> acl = folder.getACL()
+ >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true'))
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
+ """
+
+ if self._repository.getCapabilities()['ACL'] == 'manage':
+ # if the ACL capability is manage, this must be
+ # supported
+ # but it also depends on the canApplyACL allowable action
+ # for this object
+ if not isinstance(acl, ACL):
+ raise CmisException('The ACL to apply must be an instance of the ACL class.')
+ aclUrl = self._getLink(ACL_REL)
+ assert aclUrl, "Could not determine the object's ACL URL."
+ result = self._cmisClient.binding.put(aclUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ acl.getXmlDoc().toxml(encoding='utf-8'),
+ CMIS_ACL_TYPE)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+ return AtomPubACL(xmlDoc=result)
+ else:
+ raise NotSupportedException
+
+ def _getSelfLink(self):
+
+ """
+ Returns the URL used to retrieve this object.
+ """
+
+ url = self._getLink(SELF_REL)
+
+ assert len(url) > 0, "Could not determine the self link."
+
+ return url
+
+ def _getLink(self, rel, ltype=None):
+
+ """
+ Returns the HREF attribute of an Atom link element for the
+ specified rel.
+ """
+
+ if self.xmlDoc == None:
+ self.reload()
+ linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+ for linkElement in linkElements:
+
+ if ltype:
+ if linkElement.attributes.has_key('rel'):
+ relAttr = linkElement.attributes['rel'].value
+
+ if ltype and linkElement.attributes.has_key('type'):
+ typeAttr = linkElement.attributes['type'].value
+
+ if relAttr == rel and ltype.match(typeAttr):
+ return linkElement.attributes['href'].value
+ else:
+ if linkElement.attributes.has_key('rel'):
+ relAttr = linkElement.attributes['rel'].value
+
+ if relAttr == rel:
+ return linkElement.attributes['href'].value
+
+ allowableActions = property(getAllowableActions)
+ name = property(getName)
+ id = property(getObjectId)
+ properties = property(getProperties)
+ title = property(getTitle)
+ ACL = property(getACL)
+
+
+class AtomPubRepository(object):
+
+ """
+ Represents a CMIS repository. Will lazily populate itself by
+ calling the repository CMIS service URL.
+
+ You must pass in an instance of a CmisClient when creating an
+ instance of this class.
+ """
+
+ def __init__(self, cmisClient, xmlDoc=None):
+ """ Constructor """
+ self._cmisClient = cmisClient
+ self.xmlDoc = xmlDoc
+ self._repositoryId = None
+ self._repositoryName = None
+ self._repositoryInfo = {}
+ self._capabilities = {}
+ self._uriTemplates = {}
+ self._permDefs = {}
+ self._permMap = {}
+ self._permissions = None
+ self._propagation = None
+ self.logger = logging.getLogger('cmislib.model.Repository')
+ self.logger.info('Creating an instance of Repository')
+
+ def __str__(self):
+ """To string"""
+ return self.getRepositoryName()
+
+ def reload(self):
+ """
+ This method will re-fetch the repository's XML data from the CMIS
+ repository.
+ """
+ self.logger.debug('Reload called on object')
+ self.xmlDoc = self._cmisClient.binding.get(self._cmisClient.repositoryUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ self._initData()
+
+ def _initData(self):
+ """
+ This method clears out any local variables that would be out of sync
+ when data is re-fetched from the server.
+ """
+ self._repositoryId = None
+ self._repositoryName = None
+ self._repositoryInfo = {}
+ self._capabilities = {}
+ self._uriTemplates = {}
+ self._permDefs = {}
+ self._permMap = {}
+ self._permissions = None
+ self._propagation = None
+
+ def getSupportedPermissions(self):
+
+ """
+ Returns the value of the cmis:supportedPermissions element. Valid
+ values are:
+
+ - basic: indicates that the CMIS Basic permissions are supported
+ - repository: indicates that repository specific permissions are supported
+ - both: indicates that both CMIS basic permissions and repository specific permissions are supported
+
+ >>> repo.supportedPermissions
+ u'both'
+ """
+
+ if not self.getCapabilities()['ACL']:
+ raise NotSupportedException(messages.NO_ACL_SUPPORT)
+
+ if not self._permissions:
+ if self.xmlDoc == None:
+ self.reload()
+ suppEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'supportedPermissions')
+ assert len(suppEls) == 1, 'Expected the repository service document to have one element named supportedPermissions'
+ self._permissions = suppEls[0].childNodes[0].data
+
+ return self._permissions
+
+ def getPermissionDefinitions(self):
+
+ """
+ Returns a dictionary of permission definitions for this repository. The
+ key is the permission string or technical name of the permission
+ and the value is the permission description.
+
+ >>> for permDef in repo.permissionDefinitions:
+ ... print permDef
+ ...
+ cmis:all
+ {http://www.alfresco.org/model/system/1.0}base.LinkChildren
+ {http://www.alfresco.org/model/content/1.0}folder.Consumer
+ {http://www.alfresco.org/model/security/1.0}All.All
+ {http://www.alfresco.org/model/system/1.0}base.CreateAssociations
+ {http://www.alfresco.org/model/system/1.0}base.FullControl
+ {http://www.alfresco.org/model/system/1.0}base.AddChildren
+ {http://www.alfresco.org/model/system/1.0}base.ReadAssociations
+ {http://www.alfresco.org/model/content/1.0}folder.Editor
+ {http://www.alfresco.org/model/content/1.0}cmobject.Editor
+ {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations
+ cmis:read
+ cmis:write
+ """
+
+ if not self.getCapabilities()['ACL']:
+ raise NotSupportedException(messages.NO_ACL_SUPPORT)
+
+ if self._permDefs == {}:
+ if self.xmlDoc == None:
+ self.reload()
+ aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability')
+ assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability'
+ aclEl = aclEls[0]
+ perms = {}
+ for e in aclEl.childNodes:
+ if e.localName == 'permissions':
+ permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
+ assert len(permEls) == 1, 'Expected permissions element to have a child named permission'
+ descEls = e.getElementsByTagNameNS(CMIS_NS, 'description')
+ assert len(descEls) == 1, 'Expected permissions element to have a child named description'
+ perm = permEls[0].childNodes[0].data
+ desc = descEls[0].childNodes[0].data
+ perms[perm] = desc
+ self._permDefs = perms
+
+ return self._permDefs
+
+ def getPermissionMap(self):
+
+ """
+ Returns a dictionary representing the permission mapping table where
+ each key is a permission key string and each value is a list of one or
+ more permissions the principal must have to perform the operation.
+
+ >>> for (k,v) in repo.permissionMap.items():
+ ... print 'To do this: %s, you must have these perms:' % k
+ ... for perm in v:
+ ... print perm
+ ...
+ To do this: canCreateFolder.Folder, you must have these perms:
+ cmis:all
+ {http://www.alfresco.org/model/system/1.0}base.CreateChildren
+ To do this: canAddToFolder.Folder, you must have these perms:
+ cmis:all
+ {http://www.alfresco.org/model/system/1.0}base.CreateChildren
+ To do this: canDelete.Object, you must have these perms:
+ cmis:all
+ {http://www.alfresco.org/model/system/1.0}base.DeleteNode
+ To do this: canCheckin.Document, you must have these perms:
+ cmis:all
+ {http://www.alfresco.org/model/content/1.0}lockable.CheckIn
+ """
+
+ if not self.getCapabilities()['ACL']:
+ raise NotSupportedException(messages.NO_ACL_SUPPORT)
+
+ if self._permMap == {}:
+ if self.xmlDoc == None:
+ self.reload()
+ aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability')
+ assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability'
+ aclEl = aclEls[0]
+ permMap = {}
+ for e in aclEl.childNodes:
+ permList = []
+ if e.localName == 'mapping':
+ keyEls = e.getElementsByTagNameNS(CMIS_NS, 'key')
+ assert len(keyEls) == 1, 'Expected mapping element to have a child named key'
+ permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
+ assert len(permEls) >= 1, 'Expected mapping element to have at least one permission element'
+ key = keyEls[0].childNodes[0].data
+ for permEl in permEls:
+ permList.append(permEl.childNodes[0].data)
+ permMap[key] = permList
+ self._permMap = permMap
+
+ return self._permMap
+
+ def getPropagation(self):
+
+ """
+ Returns the value of the cmis:propagation element. Valid values are:
+ - objectonly: indicates that the repository is able to apply ACEs
+ without changing the ACLs of other objects
+ - propagate: indicates that the repository is able to apply ACEs to a
+ given object and propagate this change to all inheriting objects
+
+ >>> repo.propagation
+ u'propagate'
+ """
+
+ if not self.getCapabilities()['ACL']:
+ raise NotSupportedException(messages.NO_ACL_SUPPORT)
+
+ if not self._propagation:
+ if self.xmlDoc == None:
+ self.reload()
+ propEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propagation')
+ assert len(propEls) == 1, 'Expected the repository service document to have one element named propagation'
+ self._propagation = propEls[0].childNodes[0].data
+
+ return self._propagation
+
+ def getRepositoryId(self):
+
+ """
+ Returns this repository's unique identifier
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryId()
+ u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
+ """
+
+ if self._repositoryId == None:
+ if self.xmlDoc == None:
+ self.reload()
+ self._repositoryId = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryId')[0].firstChild.data
+ return self._repositoryId
+
+ def getRepositoryName(self):
+
+ """
+ Returns this repository's name
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryName()
+ u'Main Repository'
+ """
+
+ if self._repositoryName == None:
+ if self.xmlDoc == None:
+ self.reload()
+ self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data
+ return self._repositoryName
+
+ def getRepositoryInfo(self):
+
+ """
+ Returns a dict of repository information.
+
+ >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName()
+ u'Main Repository'
+ >>> info = repo.getRepositoryInfo()
+ >>> for k,v in info.items():
+ ... print "%s:%s" % (k,v)
+ ...
+ cmisSpecificationTitle:Version 1.0 Committee Draft 04
+ cmisVersionSupported:1.0
+ repositoryDescription:None
+ productVersion:3.2.0 (r2 2440)
+ rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348
+ repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9
+ repositoryName:Main Repository
+ vendorName:Alfresco
+ productName:Alfresco Repository (Community)
+ """
+
+ if not self._repositoryInfo:
+ if self.xmlDoc == None:
+ self.reload()
+ repoInfoElement = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'repositoryInfo')[0]
+ for node in repoInfoElement.childNodes:
+ if node.nodeType == node.ELEMENT_NODE and node.localName != 'capabilities':
+ try:
+ data = node.childNodes[0].data
+ except:
+ data = None
+ self._repositoryInfo[node.localName] = data
+ return self._repositoryInfo
+
+ def getCapabilities(self):
+
+ """
+ Returns a dict of repository capabilities.
+
+ >>> caps = repo.getCapabilities()
+ >>> for k,v in caps.items():
+ ... print "%s:%s" % (k,v)
+ ...
+ PWCUpdatable:True
+ VersionSpecificFiling:False
+ Join:None
+ ContentStreamUpdatability:anytime
+ AllVersionsSearchable:False
+ Renditions:None
+ Multifiling:True
+ GetFolderTree:True
+ GetDescendants:True
+ ACL:None
+ PWCSearchable:True
+ Query:bothcombined
+ Unfiling:False
+ Changes:None
+ """
+
+ if not self._capabilities:
+ if self.xmlDoc == None:
+ self.reload()
+ capabilitiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'capabilities')[0]
+ for node in [e for e in capabilitiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+ key = node.localName.replace('capability', '')
+ value = parseBoolValue(node.childNodes[0].data)
+ self._capabilities[key] = value
+ return self._capabilities
+
+ def getRootFolder(self):
+ """
+ Returns the root folder of the repository
+
+ >>> root = repo.getRootFolder()
+ >>> root.getObjectId()
+ u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
+ """
+ # get the root folder id
+ rootFolderId = self.getRepositoryInfo()['rootFolderId']
+ # instantiate a Folder object using the ID
+ folder = AtomPubFolder(self._cmisClient, self, rootFolderId)
+ # return it
+ return folder
+
+ def getFolder(self, folderId):
+
+ """
+ Returns a :class:`Folder` object for a specified folderId
+
+ >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348')
+ >>> someFolder.getObjectId()
+ u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
+ """
+
+ retObject = self.getObject(folderId)
+ return AtomPubFolder(self._cmisClient, self, xmlDoc=retObject.xmlDoc)
+
+ def getTypeChildren(self,
+ typeId=None):
+
+ """
+ Returns a list of :class:`ObjectType` objects corresponding to the
+ child types of the type specified by the typeId.
+
+ If no typeId is provided, the result will be the same as calling
+ `self.getTypeDefinitions`
+
+ These optional arguments are current unsupported:
+ - includePropertyDefinitions
+ - maxItems
+ - skipCount
+
+ >>> baseTypes = repo.getTypeChildren()
+ >>> for baseType in baseTypes:
+ ... print baseType.getTypeId()
+ ...
+ cmis:folder
+ cmis:relationship
+ cmis:document
+ cmis:policy
+ """
+
+ # Unfortunately, the spec does not appear to present a way to
+ # know how to get the children of a specific type without first
+ # retrieving the type, then asking it for one of its navigational
+ # links.
+
+ # if a typeId is specified, get it, then get its "down" link
+ if typeId:
+ targetType = self.getTypeDefinition(typeId)
+ childrenUrl = targetType.getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P)
+ typesXmlDoc = self._cmisClient.binding.get(childrenUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+ types = []
+ for entryElement in entryElements:
+ objectType = ObjectType(self._cmisClient,
+ self,
+ xmlDoc=entryElement)
+ types.append(objectType)
+ # otherwise, if a typeId is not specified, return
+ # the list of base types
+ else:
+ types = self.getTypeDefinitions()
+ return types
+
+ def getTypeDescendants(self, typeId=None, **kwargs):
+
+ """
+ Returns a list of :class:`ObjectType` objects corresponding to the
+ descendant types of the type specified by the typeId.
+
+ If no typeId is provided, the repository's "typesdescendants" URL
+ will be called to determine the list of descendant types.
+
+ >>> allTypes = repo.getTypeDescendants()
+ >>> for aType in allTypes:
+ ... print aType.getTypeId()
+ ...
+ cmis:folder
+ F:cm:systemfolder
+ F:act:savedactionfolder
+ F:app:configurations
+ F:fm:forums
+ F:wcm:avmfolder
+ F:wcm:avmplainfolder
+ F:wca:webfolder
+ F:wcm:avmlayeredfolder
+ F:st:site
+ F:app:glossary
+ F:fm:topic
+
+ These optional arguments are supported:
+ - depth
+ - includePropertyDefinitions
+
+ >>> types = alfRepo.getTypeDescendants('cmis:folder')
+ >>> len(types)
+ 17
+ >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1)
+ >>> len(types)
+ 12
+ >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2)
+ >>> len(types)
+ 17
+ """
+
+ # Unfortunately, the spec does not appear to present a way to
+ # know how to get the children of a specific type without first
+ # retrieving the type, then asking it for one of its navigational
+ # links.
+ if typeId:
+ targetType = self.getTypeDefinition(typeId)
+ descendUrl = targetType.getLink(DOWN_REL, CMIS_TREE_TYPE_P)
+
+ else:
+ descendUrl = self.getLink(TYPE_DESCENDANTS_REL)
+
+ if not descendUrl:
+ raise NotSupportedException("Could not determine the type descendants URL")
+
+ typesXmlDoc = self._cmisClient.binding.get(descendUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+ entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+ types = []
+ for entryElement in entryElements:
+ objectType = AtomPubObjectType(self._cmisClient,
+ self,
+ xmlDoc=entryElement)
+ types.append(objectType)
+ return types
+
+ def getTypeDefinitions(self, **kwargs):
+
+ """
+ Returns a list of :class:`ObjectType` objects representing
+ the base types in the repository.
+
+ >>> baseTypes = repo.getTypeDefinitions()
+ >>> for baseType in baseTypes:
+ ... print baseType.getTypeId()
+ ...
+ cmis:folder
+ cmis:relationship
+ cmis:document
+ cmis:policy
+ """
+
+ typesUrl = self.getCollectionLink(TYPES_COLL)
+ typesXmlDoc = self._cmisClient.binding.get(typesUrl,
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+ entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
+ types = []
+ for entryElement in entryElements:
+ objectType = AtomPubObjectType(self._cmisClient,
+ self,
+ xmlDoc=entryElement)
+ types.append(objectType)
+ # return the result
+ return types
+
+ def getTypeDefinition(self, typeId):
+
+ """
+ Returns an :class:`ObjectType` object for the specified object type id.
+
+ >>> folderType = repo.getTypeDefinition('cmis:folder')
+ """
+
+ objectType = AtomPubObjectType(self._cmisClient, self, typeId)
+ objectType.reload()
+ return objectType
+
+ def getLink(self, rel):
+ """
+ Returns the HREF attribute of an Atom link element for the
+ specified rel.
+ """
+ if self.xmlDoc == None:
+ self.reload()
+
+ linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+ for linkElement in linkElements:
+
+ if linkElement.attributes.has_key('rel'):
+ relAttr = linkElement.attributes['rel'].value
+
+ if relAttr == rel:
+ return linkElement.attributes['href'].value
+
+ def getCheckedOutDocs(self, **kwargs):
+
+ """
+ Returns a ResultSet of :class:`CmisObject` objects that
+ are currently checked out.
+
+ >>> rs = repo.getCheckedOutDocs()
+ >>> len(rs.getResults())
+ 2
+ >>> for doc in repo.getCheckedOutDocs().getResults():
+ ... doc.getTitle()
+ ...
+ u'sample-a (Working Copy).pdf'
+ u'sample-b (Working Copy).pdf'
+
+ These optional arguments are supported:
+ - folderId
+ - maxItems
+ - skipCount
+ - orderBy
+ - filter
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ """
+
+ return self.getCollection(CHECKED_OUT_COLL, **kwargs)
+
+ def getUnfiledDocs(self, **kwargs):
+
+ """
+ Returns a ResultSet of :class:`CmisObject` objects that
+ are currently unfiled.
+
+ >>> rs = repo.getUnfiledDocs()
+ >>> len(rs.getResults())
+ 2
+ >>> for doc in repo.getUnfiledDocs().getResults():
+ ... doc.getTitle()
+ ...
+ u'sample-a.pdf'
+ u'sample-b.pdf'
+
+ These optional arguments are supported:
+ - folderId
+ - maxItems
+ - skipCount
+ - orderBy
+ - filter
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ """
+
+ return self.getCollection(UNFILED_COLL, **kwargs)
+
+ def getObject(self,
+ objectId,
+ **kwargs):
+
+ """
+ Returns an object given the specified object ID.
+
+ >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808')
+ >>> doc.getTitle()
+ u'sample-b.pdf'
+
+ The following optional arguments are supported:
+ - returnVersion
+ - filter
+ - includeRelationships
+ - includePolicyIds
+ - renditionFilter
+ - includeACL
+ - includeAllowableActions
+ """
+
+ return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs)
+
+ def getObjectByPath(self, path, **kwargs):
+
+ """
+ Returns an object given the path to the object.
+
+ >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf')
+ >>> doc.getTitle()
+ u'sample-b.pdf'
+
+ The following optional arguments are not currently supported:
+ - filter
+ - includeAllowableActions
+ """
+
+ # get the uritemplate
+ template = self.getUriTemplates()['objectbypath']['template']
+
+ # fill in the template with the path provided
+ params = {
+ '{path}': quote(path, '/'),
+ '{filter}': '',
+ '{includeAllowableActions}': 'false',
+ '{includePolicyIds}': 'false',
+ '{includeRelationships}': '',
+ '{includeACL}': 'false',
+ '{renditionFilter}': ''}
+
+ options = {}
+ addOptions = {} # args specified, but not in the template
+ for k, v in kwargs.items():
+ pKey = "{" + k + "}"
+ if template.find(pKey) >= 0:
+ options[pKey] = toCMISValue(v)
+ else:
+ addOptions[k] = toCMISValue(v)
+
+ # merge the templated args with the default params
+ params.update(options)
+
+ byObjectPathUrl = multiple_replace(params, template)
+
+ # do a GET against the URL
+ result = self._cmisClient.binding.get(byObjectPathUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **addOptions)
+ 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(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **kwargs), **kwargs)
+
+ def query(self, statement, **kwargs):
+
+ """
+ Returns a list of :class:`CmisObject` objects based on the CMIS
+ Query Language passed in as the statement. The actual objects
+ returned will be instances of the appropriate child class based
+ on the object's base type ID.
+
+ In order for the results to be properly instantiated as objects,
+ make sure you include 'cmis:objectId' as one of the fields in
+ your select statement, or just use "SELECT \*".
+
+ If you want the search results to automatically be instantiated with
+ the appropriate sub-class of :class:`CmisObject` you must either
+ include cmis:baseTypeId as one of the fields in your select statement
+ or just use "SELECT \*".
+
+ >>> q = "select * from cmis:document where cmis:name like '%test%'"
+ >>> resultSet = repo.query(q)
+ >>> len(resultSet.getResults())
+ 1
+ >>> resultSet.hasNext()
+ False
+
+ The following optional arguments are supported:
+ - searchAllVersions
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ - maxItems
+ - skipCount
+
+ >>> q = 'select * from cmis:document'
+ >>> rs = repo.query(q)
+ >>> len(rs.getResults())
+ 148
+ >>> rs = repo.query(q, maxItems='5')
+ >>> len(rs.getResults())
+ 5
+ >>> rs.hasNext()
+ True
+ """
+
+ if self.xmlDoc == None:
+ self.reload()
+
+ # get the URL this repository uses to accept query POSTs
+ queryUrl = self.getCollectionLink(QUERY_COLL)
+
+ # build the CMIS query XML that we're going to POST
+ xmlDoc = self._getQueryXmlDoc(statement, **kwargs)
+
+ # do the POST
+ #print 'posting:%s' % xmlDoc.toxml(encoding='utf-8')
+ result = self._cmisClient.binding.post(queryUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ xmlDoc.toxml(encoding='utf-8'),
+ CMIS_QUERY_TYPE)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(self._cmisClient, self, result)
+
+ def getContentChanges(self, **kwargs):
+
+ """
+ Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects.
+
+ >>> for changeEntry in rs:
+ ... changeEntry.objectId
+ ... changeEntry.id
+ ... changeEntry.changeType
+ ... changeEntry.changeTime
+ ...
+ 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b'
+ u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b'
+ u'created'
+ datetime.datetime(2010, 2, 11, 12, 55, 14)
+ 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923'
+ u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923'
+ u'updated'
+ datetime.datetime(2010, 2, 11, 12, 55, 13)
+ 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
+ u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
+ u'updated'
+
+ The following optional arguments are supported:
+ - changeLogToken
+ - includeProperties
+ - includePolicyIDs
+ - includeACL
+ - maxItems
+
+ You can get the latest change log token by inspecting the repository
+ info via :meth:`Repository.getRepositoryInfo`.
+
+ >>> repo.info['latestChangeLogToken']
+ u'2692'
+ >>> rs = repo.getContentChanges(changeLogToken='2692')
+ >>> len(rs)
+ 1
+ >>> rs[0].id
+ u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9'
+ >>> rs[0].changeType
+ u'updated'
+ >>> rs[0].changeTime
+ datetime.datetime(2010, 2, 16, 20, 6, 37)
+ """
+
+ if self.getCapabilities()['Changes'] == None:
+ raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT)
+
+ changesUrl = self.getLink(CHANGE_LOG_REL)
+ result = self._cmisClient.binding.get(changesUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubChangeEntryResultSet(self._cmisClient, self, result)
+
+ def createDocumentFromString(self,
+ name,
+ properties={},
+ parentFolder=None,
+ contentString=None,
+ contentType=None,
+ contentEncoding=None):
+
+ """
+ Creates a new document setting the content to the string provided. If
+ the repository supports unfiled objects, you do not have to pass in
+ a parent :class:`Folder` otherwise it is required.
+
+ This method is essentially a convenience method that wraps your string
+ with a StringIO and then calls createDocument.
+
+ >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain')
+ <cmislib.model.Document object at 0x101352ed0>
+ """
+
+ # if you didn't pass in a parent folder
+ if parentFolder == None:
+ # if the repository doesn't require fileable objects to be filed
+ if self.getCapabilities()['Unfiling']:
+ # has not been implemented
+ #postUrl = self.getCollectionLink(UNFILED_COLL)
+ raise NotImplementedError
+ else:
+ # this repo requires fileable objects to be filed
+ raise InvalidArgumentException
+
+ return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString),
+ contentType, contentEncoding)
+
+ def createDocument(self,
+ name,
+ properties={},
+ parentFolder=None,
+ contentFile=None,
+ contentType=None,
+ contentEncoding=None):
+
+ """
+ Creates a new :class:`Document` object. If the repository
+ supports unfiled objects, you do not have to pass in
+ a parent :class:`Folder` otherwise it is required.
+
+ To create a document with an associated contentFile, pass in a
+ File object. The method will attempt to guess the appropriate content
+ type and encoding based on the file. To specify it yourself, pass them
+ in via the contentType and contentEncoding arguments.
+
+ >>> f = open('sample-a.pdf', 'rb')
+ >>> doc = folder.createDocument('sample-a.pdf', contentFile=f)
+ <cmislib.model.Document object at 0x105be5e10>
+ >>> f.close()
+ >>> doc.getTitle()
+ u'sample-a.pdf'
+
+ The following optional arguments are not currently supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ postUrl = ''
+ # if you didn't pass in a parent folder
+ if parentFolder == None:
+ # if the repository doesn't require fileable objects to be filed
+ if self.getCapabilities()['Unfiling']:
+ # has not been implemented
+ #postUrl = self.getCollectionLink(UNFILED_COLL)
+ raise NotImplementedError
+ else:
+ # this repo requires fileable objects to be filed
+ raise InvalidArgumentException
+ else:
+ postUrl = parentFolder.getChildrenLink()
+
+ # make sure a name is set
+ properties['cmis:name'] = name
+
+ # hardcoding to cmis:document if it wasn't
+ # passed in via props
+ if not properties.has_key('cmis:objectTypeId'):
+ properties['cmis:objectTypeId'] = CmisId('cmis:document')
+ # and if it was passed in, making sure it is a CmisId
+ elif not isinstance(properties['cmis:objectTypeId'], CmisId):
+ properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId'])
+
+ # build the Atom entry
+ xmlDoc = getEntryXmlDoc(self, None, properties, contentFile,
+ contentType, contentEncoding)
+
+ # post the Atom entry
+ result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ xmlDoc.toxml(encoding='utf-8'),
+ ATOM_XML_ENTRY_TYPE)
+ 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 AtomPubDocument(self._cmisClient, self, xmlDoc=result)
+
+ def createDocumentFromSource(self,
+ sourceId,
+ properties={},
+ parentFolder=None):
+ """
+ This is not yet implemented.
+
+ The following optional arguments are not yet supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+ # TODO: To be implemented
+ raise NotImplementedError
+
+ def createFolder(self,
+ parentFolder,
+ name,
+ properties={}):
+
+ """
+ Creates a new :class:`Folder` object in the specified parentFolder.
+
+ >>> root = repo.getRootFolder()
+ >>> folder = repo.createFolder(root, 'someFolder2')
+ >>> folder.getTitle()
+ u'someFolder2'
+ >>> folder.getObjectId()
+ u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f'
+
+ The following optional arguments are not yet supported:
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ return parentFolder.createFolder(name, properties)
+
+ def createRelationship(self, sourceObj, targetObj, relType):
+ """
+ Creates a relationship of the specific type between a source object
+ and a target object and returns the new :class:`Relationship` object.
+
+ The following optional arguments are not currently supported:
+ - policies
+ - addACEs
+ - removeACEs
+ """
+ return sourceObj.createRelationship(targetObj, relType)
+
+ def createPolicy(self, properties):
+ """
+ This has not yet been implemented.
+
+ The following optional arguments are not currently supported:
+ - folderId
+ - policies
+ - addACEs
+ - removeACEs
+ """
+ # TODO: To be implemented
+ raise NotImplementedError
+
+ def getUriTemplates(self):
+
+ """
+ Returns a list of the URI templates the repository service knows about.
+
+ >>> templates = repo.getUriTemplates()
+ >>> templates['typebyid']['mediaType']
+ u'application/atom+xml;type=entry'
+ >>> templates['typebyid']['template']
+ u'http://localhost:8080/alfresco/s/cmis/type/{id}'
+ """
+
+ if self._uriTemplates == {}:
+
+ if self.xmlDoc == None:
+ self.reload()
+
+ uriTemplateElements = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'uritemplate')
+
+ for uriTemplateElement in uriTemplateElements:
+ template = None
+ templType = None
+ mediatype = None
+
+ for node in [e for e in uriTemplateElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+ if node.localName == 'template':
+ template = node.childNodes[0].data
+ elif node.localName == 'type':
+ templType = node.childNodes[0].data
+ elif node.localName == 'mediatype':
+ mediatype = node.childNodes[0].data
+
+ self._uriTemplates[templType] = UriTemplate(template,
+ templType,
+ mediatype)
+
+ return self._uriTemplates
+
+ def getCollection(self, collectionType, **kwargs):
+
+ """
+ Returns a list of objects returned for the specified collection.
+
+ If the query collection is requested, an exception will be raised.
+ That collection isn't meant to be retrieved.
+
+ If the types collection is specified, the method returns the result of
+ `getTypeDefinitions` and ignores any optional params passed in.
+
+ >>> from cmislib.model import TYPES_COLL
+ >>> types = repo.getCollection(TYPES_COLL)
+ >>> len(types)
+ 4
+ >>> types[0].getTypeId()
+ u'cmis:folder'
+
+ Otherwise, the collection URL is invoked, and a :class:`ResultSet` is
+ returned.
+
+ >>> from cmislib.model import CHECKED_OUT_COLL
+ >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
+ >>> len(resultSet.getResults())
+ 1
+ """
+
+ if collectionType == QUERY_COLL:
+ raise NotSupportedException
+ elif collectionType == TYPES_COLL:
+ return self.getTypeDefinitions()
+
+ result = self._cmisClient.binding.get(self.getCollectionLink(collectionType).encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+ if (type(result) == HTTPError):
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(self._cmisClient, self, result)
+
+ def getCollectionLink(self, collectionType):
+
+ """
+ Returns the link HREF from the specified collectionType
+ ('checkedout', for example).
+
+ >>> from cmislib.model import CHECKED_OUT_COLL
+ >>> repo.getCollectionLink(CHECKED_OUT_COLL)
+ u'http://localhost:8080/alfresco/s/cmis/checkedout'
+
+ """
+
+ collectionElements = self.xmlDoc.getElementsByTagNameNS(APP_NS, 'collection')
+ for collectionElement in collectionElements:
+ link = collectionElement.attributes['href'].value
+ for node in [e for e in collectionElement.childNodes if e.nodeType == e.ELEMENT_NODE]:
+ if node.localName == 'collectionType':
+ if node.childNodes[0].data == collectionType:
+ return link
+
+ def _getQueryXmlDoc(self, query, **kwargs):
+
+ """
+ Utility method that knows how to build CMIS query xml around the
+ specified query statement.
+ """
+
+ cmisXmlDoc = minidom.Document()
+ queryElement = cmisXmlDoc.createElementNS(CMIS_NS, "query")
+ queryElement.setAttribute('xmlns', CMIS_NS)
+ cmisXmlDoc.appendChild(queryElement)
+
+ statementElement = cmisXmlDoc.createElementNS(CMIS_NS, "statement")
+ cdataSection = cmisXmlDoc.createCDATASection(query)
+ statementElement.appendChild(cdataSection)
+ queryElement.appendChild(statementElement)
+
+ for (k, v) in kwargs.items():
+ optionElement = cmisXmlDoc.createElementNS(CMIS_NS, k)
+ optionText = cmisXmlDoc.createTextNode(v)
+ optionElement.appendChild(optionText)
+ queryElement.appendChild(optionElement)
+
+ return cmisXmlDoc
+
+ capabilities = property(getCapabilities)
+ id = property(getRepositoryId)
+ info = property(getRepositoryInfo)
+ name = property(getRepositoryName)
+ rootFolder = property(getRootFolder)
+ permissionDefinitions = property(getPermissionDefinitions)
+ permissionMap = property(getPermissionMap)
+ propagation = property(getPropagation)
+ supportedPermissions = property(getSupportedPermissions)
+
+
+class AtomPubResultSet(ResultSet):
+
+ """
+ Represents a paged result set. In CMIS, this is most often an Atom feed.
+ """
+
+ def __init__(self, cmisClient, repository, xmlDoc):
+ ''' Constructor '''
+ self._cmisClient = cmisClient
+ self._repository = repository
+ self._xmlDoc = xmlDoc
+ self._results = []
+ self.logger = logging.getLogger('cmislib.model.ResultSet')
+ self.logger.info('Creating an instance of ResultSet')
+
+ def __iter__(self):
+ ''' Iterator for the result set '''
+ return iter(self.getResults())
+
+ def __getitem__(self, index):
+ ''' Getter for the result set '''
+ return self.getResults()[index]
+
+ def __len__(self):
+ ''' Len method for the result set '''
+ return len(self.getResults())
+
+ def _getLink(self, rel):
+ '''
+ Returns the link found in the feed's XML for the specified rel.
+ '''
+ linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link')
+
+ for linkElement in linkElements:
+
+ if linkElement.attributes.has_key('rel'):
+ relAttr = linkElement.attributes['rel'].value
+
+ if relAttr == rel:
+ return linkElement.attributes['href'].value
+
+ def _getPageResults(self, rel):
+ '''
+ Given a specified rel, does a get using that link (if one exists)
+ and then converts the resulting XML into a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type.
+
+ The results are kept around to facilitate repeated calls without moving
+ the cursor.
+ '''
+ link = self._getLink(rel)
+ if link:
+ result = self._cmisClient.binding.get(link.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ 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(AtomPubCmisObject(self._cmisClient,
+ self._repository,
+ xmlDoc=entryElement))
+ entries.append(cmisObject)
+
+ self._results = entries
+
+ return self._results
+
+ def hasObject(self, objectId):
+
+ '''
+ Returns True if the specified objectId is found in the list of results,
+ otherwise returns False.
+ '''
+
+ for obj in self.getResults():
+ if obj.id == objectId:
+ return True
+ return False
+
+ def getFirst(self):
+
+ '''
+ Returns the first page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "first" link. Not all of them do.
+
+ >>> resultSet.hasFirst()
+ True
+ >>> results = resultSet.getFirst()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ return self._getPageResults(FIRST_REL)
+
+ def getPrev(self):
+
+ '''
+ Returns the prev page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "prev" link. Not all of them do.
+ >>> resultSet.hasPrev()
+ True
+ >>> results = resultSet.getPrev()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ return self._getPageResults(PREV_REL)
+
+ def getNext(self):
+
+ '''
+ Returns the next page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type.
+ >>> resultSet.hasNext()
+ True
+ >>> results = resultSet.getNext()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ return self._getPageResults(NEXT_REL)
+
+ def getLast(self):
+
+ '''
+ Returns the last page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server is returning a "last" link. Not all of them do.
+
+ >>> resultSet.hasLast()
+ True
+ >>> results = resultSet.getLast()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ return self._getPageResults(LAST_REL)
+
+ def hasNext(self):
+
+ '''
+ Returns True if this page contains a next link.
+
+ >>> resultSet.hasNext()
+ True
+ '''
+
+ if self._getLink(NEXT_REL):
+ return True
+ else:
+ return False
+
+ def hasPrev(self):
+
+ '''
+ Returns True if this page contains a prev link. Not all CMIS providers
+ implement prev links consistently.
+
+ >>> resultSet.hasPrev()
+ True
+ '''
+
+ if self._getLink(PREV_REL):
+ return True
+ else:
+ return False
+
+ def hasFirst(self):
+
+ '''
+ Returns True if this page contains a first link. Not all CMIS providers
+ implement first links consistently.
+
+ >>> resultSet.hasFirst()
+ True
+ '''
+
+ if self._getLink(FIRST_REL):
+ return True
+ else:
+ return False
+
+ def hasLast(self):
+
+ '''
+ Returns True if this page contains a last link. Not all CMIS providers
+ implement last links consistently.
+
+ >>> resultSet.hasLast()
+ True
+ '''
+
+ if self._getLink(LAST_REL):
+ return True
+ else:
+ return False
+
+
+class AtomPubDocument(AtomPubCmisObject):
+
+ """
+ An object typically associated with file content.
+ """
+
+ def checkout(self):
+
+ """
+ Performs a checkout on the :class:`Document` and returns the
+ Private Working Copy (PWC), which is also an instance of
+ :class:`Document`
+
+ >>> doc.getObjectId()
+ u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0'
+ >>> doc.isCheckedOut()
+ False
+ >>> pwc = doc.checkout()
+ >>> doc.isCheckedOut()
+ True
+ """
+
+ # get the checkedout collection URL
+ checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL)
+ assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url."
+
+ # get this document's object ID
+ # build entry XML with it
+ properties = {'cmis:objectId': self.getObjectId()}
+ entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties)
+
+ # post it to to the checkedout collection URL
+ result = self._cmisClient.binding.post(checkoutUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ entryXmlDoc.toxml(encoding='utf-8'),
+ ATOM_XML_ENTRY_TYPE)
+
+ 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 AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result)
+
+ def cancelCheckout(self):
+ """
+ Cancels the checkout of this object by retrieving the Private Working
+ Copy (PWC) and then deleting it. After the PWC is deleted, this object
+ will be reloaded to update properties related to a checkout.
+
+ >>> doc.isCheckedOut()
+ True
+ >>> doc.cancelCheckout()
+ >>> doc.isCheckedOut()
+ False
+ """
+
+ pwcDoc = self.getPrivateWorkingCopy()
+ if pwcDoc:
+ pwcDoc.delete()
+ self.reload()
+
+ def getPrivateWorkingCopy(self):
+
+ """
+ Retrieves the object using the object ID in the property:
+ cmis:versionSeriesCheckedOutId then uses getObject to instantiate
+ the object.
+
+ >>> doc.isCheckedOut()
+ False
+ >>> doc.checkout()
+ <cmislib.model.Document object at 0x103a25ad0>
+ >>> pwc = doc.getPrivateWorkingCopy()
+ >>> pwc.getTitle()
+ u'sample-b (Working Copy).pdf'
+ """
+
+ # reloading the document just to make sure we've got the latest
+ # and greatest PWC ID
+ self.reload()
+ pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId']
+ if pwcDocId:
+ return self._repository.getObject(pwcDocId)
+
+ def isCheckedOut(self):
+
+ """
+ Returns true if the document is checked out.
+
+ >>> doc.isCheckedOut()
+ True
+ >>> doc.cancelCheckout()
+ >>> doc.isCheckedOut()
+ False
+ """
+
+ # reloading the document just to make sure we've got the latest
+ # and greatest checked out prop
+ self.reload()
+ return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut'])
+
+ def getCheckedOutBy(self):
+
+ """
+ Returns the ID who currently has the document checked out.
+ >>> pwc = doc.checkout()
+ >>> pwc.getCheckedOutBy()
+ u'admin'
+ """
+
+ # reloading the document just to make sure we've got the latest
+ # and greatest checked out prop
+ self.reload()
+ return self.getProperties()['cmis:versionSeriesCheckedOutBy']
+
+ def checkin(self, checkinComment=None, **kwargs):
+
+ """
+ Checks in this :class:`Document` which must be a private
+ working copy (PWC).
+
+ >>> doc.isCheckedOut()
+ False
+ >>> pwc = doc.checkout()
+ >>> doc.isCheckedOut()
+ True
+ >>> pwc.checkin()
+ <cmislib.model.Document object at 0x103a8ae90>
+ >>> doc.isCheckedOut()
+ False
+
+ The following optional arguments are supported:
+ - major
+ - properties
+ - contentStream
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ # Add checkin to kwargs and checkinComment, if it exists
+ kwargs['checkin'] = 'true'
+ kwargs['checkinComment'] = checkinComment
+
+ # Build an empty ATOM entry
+ entryXmlDoc = getEmptyXmlDoc()
+
+ # Get the self link
+ # Do a PUT of the empty ATOM to the self link
+ url = self._getSelfLink()
+ result = self._cmisClient.binding.put(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ entryXmlDoc.toxml(encoding='utf-8'),
+ ATOM_XML_TYPE,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result)
+
+ def getLatestVersion(self, **kwargs):
+
+ """
+ Returns a :class:`Document` object representing the latest version in
+ the version series.
+
+ The following optional arguments are supported:
+ - major
+ - filter
+ - includeRelationships
+ - includePolicyIds
+ - renditionFilter
+ - includeACL
+ - includeAllowableActions
+
+ >>> latestDoc = doc.getLatestVersion()
+ >>> latestDoc.getProperties()['cmis:versionLabel']
+ u'2.1'
+ >>> latestDoc = doc.getLatestVersion(major='false')
+ >>> latestDoc.getProperties()['cmis:versionLabel']
+ u'2.1'
+ >>> latestDoc = doc.getLatestVersion(major='true')
+ >>> latestDoc.getProperties()['cmis:versionLabel']
+ u'2.0'
+ """
+
+ doc = None
+ if kwargs.has_key('major') and kwargs['major'] == 'true':
+ doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor')
+ else:
+ doc = self._repository.getObject(self.getObjectId(), returnVersion='latest')
+
+ return doc
+
+ def getPropertiesOfLatestVersion(self, **kwargs):
+
+ """
+ Like :class:`^CmisObject.getProperties`, returns a dict of properties
+ from the latest version of this object in the version series.
+
+ The optional major and filter arguments are supported.
+ """
+
+ latestDoc = self.getLatestVersion(**kwargs)
+
+ return latestDoc.getProperties()
+
+ def getAllVersions(self, **kwargs):
+
+ """
+ Returns a :class:`ResultSet` of document objects for the entire
+ version history of this object, including any PWC's.
+
+ The optional filter and includeAllowableActions are
+ supported.
+ """
+
+ # get the version history link
+ versionsUrl = self._getLink(VERSION_HISTORY_REL)
+
+ # invoke the URL
+ result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(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.binding.put(srcUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ 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 AtomPubDocument(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.binding.delete(srcUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **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(AtomPubRendition(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.binding.get(parentUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ filter='cmis:path',
+ includeRelativePathSegment=True)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ paths = []
+ rs = AtomPubResultSet(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 AtomPubFolder(AtomPubCmisObject):
+
+ """
+ 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.binding.post(postUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ 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 AtomPubFolder(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.binding.get(childrenUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(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.binding.get(descendantsUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(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.binding.get(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **kwargs)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubResultSet(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.binding.get(parentUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+
+ # return the result set
+ return AtomPubFolder(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.binding.delete(url.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **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.binding.post(postUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ 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.binding.post(postUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ 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 AtomPubRelationship(AtomPubCmisObject):
+
+ """
+ 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 AtomPubCmisId(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 AtomPubCmisId(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 AtomPubPolicy(AtomPubCmisObject):
+
+ """
+ An arbirary object that can 'applied' to objects that the
+ repository identifies as being 'controllable'.
+ """
+
+ pass
+
+
+class AtomPubObjectType(ObjectType):
+
+ """
+ Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'.
+ Contains metadata about the type.
+ """
+
+ def __init__(self, cmisClient, repository, typeId=None, 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 AtomPubCmisId(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 = AtomPubProperty(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.binding.get(byTypeIdUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **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 AtomPubProperty(Property):
+
+ """
+ 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 AtomPubACL(ACL):
+
+ """
+ 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, principalId, access, direct):
+
+ """
+ Adds an :class:`ACE` entry to the ACL.
+
+ >>> acl = folder.getACL()
+ >>> acl.addEntry('jpotts', 'cmis:read', 'true')
+ >>> acl.addEntry('jsmith', 'cmis:write', 'true')
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
+ """
+ ace = AtomPubACE(principalId, access, direct)
+ self._entries[ace.principalId] = ace
+
+ def removeEntry(self, principalId):
+
+ """
+ Removes the :class:`ACE` entry given a specific principalId.
+
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
+ >>> acl.removeEntry('jsmith')
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>}
+ """
+
+ if self._entries.has_key(principalId):
+ del(self._entries[principalId])
+
+ def clearEntries(self):
+
+ """
+ Clears all :class:`ACE` entries from the ACL and removes the internal
+ XML representation of the ACL.
+
+ >>> acl = ACL()
+ >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
+ >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true'))
+ >>> acl.entries
+ {'jpotts': <cmislib.model.ACE object at 0x1012c7310>, 'jsmith': <cmislib.model.ACE object at 0x100528490>}
+ >>> acl.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 = AtomPubACE(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 AtomPubACE(ACE):
+
+ """
+ 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 AtomPubChangeEntry(ChangeEntry):
+
+ """
+ Represents a change log entry. Retrieve a list of change entries via
+ :meth:`Repository.getContentChanges`.
+
+ >>> for changeEntry in rs:
+ ... changeEntry.objectId
+ ... changeEntry.id
+ ... changeEntry.changeType
+ ... changeEntry.changeTime
+ ...
+ 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b'
+ u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b'
+ u'created'
+ datetime.datetime(2010, 2, 11, 12, 55, 14)
+ 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923'
+ u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923'
+ u'updated'
+ datetime.datetime(2010, 2, 11, 12, 55, 13)
+ 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
+ u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
+ u'updated'
+ """
+
+ def __init__(self, cmisClient, repository, 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 AtomPubACL(self._cmisClient, self._repository, aclEls[0])
+ elif aclUrl:
+ result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+ return AtomPubACL(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)
+
+
+ """
+ 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 AtomPubACL(self._cmisClient, self._repository, aclEls[0])
+ elif aclUrl:
+ result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password)
+ if type(result) == HTTPError:
+ raise CmisException(result.code)
+ return AtomPubACL(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 AtomPubChangeEntryResultSet(AtomPubResultSet):
+
+ """
+ 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 = AtomPubChangeEntry(self._cmisClient, self._repository, entryElement)
+ entries.append(changeEntry)
+
+ self._results = entries
+
+ return self._results
+
+
+class AtomPubRendition(Rendition):
+
+ """
+ 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 AtomPubCmisId(CmisId):
+
+ """
+ This is a marker class to be used for Strings that are used as CMIS ID's.
+ Making the objects instances of this class makes it easier to create the
+ Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of
+ cmis:propertyString.
+ """
+
+ pass
+
+def 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 AtomPubFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
+ if baseType == 'cmis:document':
+ return AtomPubDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
+ if baseType == 'cmis:relationship':
+ return AtomPubRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs)
+ if baseType == 'cmis:policy':
+ return AtomPubPolicy(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 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
+
+
+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
+
diff --git a/src/cmislib/browser_binding.py b/src/cmislib/browser_binding.py
new file mode 100644
index 0000000..62cbe54
--- /dev/null
+++ b/src/cmislib/browser_binding.py
@@ -0,0 +1,2378 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+Module containing the browser binding-specific objects used to work with a CMIS
+provider.
+"""
+from cmis_services import RepositoryServiceIfc
+from cmis_services import Binding
+from net import RESTService as Rest
+from urllib2 import HTTPError
+from exceptions import CmisException, RuntimeException
+from domain import CmisId, CmisObject, Repository, Relationship, Policy, ObjectType, Property, Folder, Document, ACL, ACE, ChangeEntry, ResultSet, ChangeEntryResultSet, Rendition
+import json
+import logging
+
+class BrowserBinding(Binding):
+ def __init__(self, **kwargs):
+ self.extArgs = kwargs
+
+ def getRepositoryService(self):
+ return RepositoryService()
+
+ def get(self, url, username, password, **kwargs):
+
+ """
+ Does a get against the CMIS service. More than likely, you will not
+ need to call this method. Instead, let the other objects do it for you.
+
+ For example, if you need to get a specific object by object id, try
+ :class:`Repository.getObject`. If you have a path instead of an object
+ id, use :class:`Repository.getObjectByPath`. Or, you could start with
+ the root folder (:class:`Repository.getRootFolder`) and drill down from
+ there.
+ """
+
+ # merge the cmis client extended args with the ones that got passed in
+ if (len(self.extArgs) > 0):
+ kwargs.update(self.extArgs)
+
+ result = Rest().get(url,
+ username=username,
+ password=password,
+ **kwargs)
+ if type(result) == HTTPError:
+ self._processCommonErrors(result)
+ else:
+ result = json.load(result)
+ return result
+
+class RepositoryService(RepositoryServiceIfc):
+ def getRepository(self, client, repositoryId):
+ #TODO
+ pass
+
+ def getRepositories(self, client):
+ result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+ if (type(result) == HTTPError):
+ raise RuntimeException()
+
+ repositories = []
+ for repo in result.itervalues():
+ repositories.append({'repositoryId': repo['repositoryId'],
+ 'repositoryName': repo['repositoryName']})
+ return repositories
+
+ def getDefaultRepository(self, client):
+ result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs)
+ # instantiate a Repository object with the first workspace
+ # element we find
+ for repo in result.itervalues():
+ repository = BrowserRepository(client, repo)
+ return repository
+
+ def getRepositoryInfo(self):
+ return "Here is your repository info using the browser binding"
+
+
+class BrowserCmisObject(object):
+
+ """
+ Common ancestor class for other CMIS domain objects such as
+ :class:`Document` and :class:`Folder`.
+ """
+
+ def __init__(self, cmisClient, repository, objectId=None, data=None, **kwargs):
+ """ Constructor """
+ self._cmisClient = cmisClient
+ self._repository = repository
+ self._objectId = objectId
+ self._name = None
+ self._properties = {}
+ self._allowableActions = {}
+ self.data = data
+ self._kwargs = kwargs
+ self.logger = logging.getLogger('cmislib.browser_binding.BrowserCmisObject')
+ 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
+ '*'.
+ """
+
+ if kwargs:
+ if self._kwargs:
+ self._kwargs.update(kwargs)
+ else:
+ self._kwargs = kwargs
+
+ byObjectIdUrl = self.getRootFolderUrl() + "?objectId=" + self.objectId
+ self.data = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'),
+ self._cmisClient.username,
+ self._cmisClient.password,
+ **addOptions)
+ self._initData()
+
+ # if a returnVersion arg was passed in, it is possible we got back
+ # a different object ID than the value we started with, so it needs
+ # to be cleared out as well
+ if options.has_key('returnVersion') or addOptions.has_key('returnVersion'):
+ self._objectId = None
+
+ def getObjectId(self):
+
+ """
+ Returns the object ID for this object.
+
+ >>> doc = resultSet.getResults()[0]
+ >>> doc.getObjectId()
+ u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getTitle(self):
+
+ """
+ Returns the value of the object's cmis:title property.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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'
+
+ """
+
+ pass
+
+ 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)
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ def applyPolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def createRelationship(self, targetObj, relTypeId):
+
+ """
+ Creates a relationship between this object and a specified target
+ object using the relationship type specified. Returns the new
+ :class:`Relationship` object.
+
+ >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc')
+ >>> rel.getProperties()
+ {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None}
+
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def removePolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def getAppliedPolicies(self):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def getACL(self):
+
+ """
+ Repository.getCapabilities['ACL'] must return manage or discover.
+
+ >>> acl = folder.getACL()
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
+
+ The optional onlyBasicPermissions argument is currently not supported.
+ """
+
+ pass
+
+ 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>}
+ """
+
+ pass
+
+ allowableActions = property(getAllowableActions)
+ name = property(getName)
+ id = property(getObjectId)
+ properties = property(getProperties)
+ title = property(getTitle)
+ ACL = property(getACL)
+
+
+class BrowserRepository(object):
+ """
+ Represents a CMIS repository. Will lazily populate itself by
+ calling the repository CMIS service URL.
+
+ You must pass in an instance of a CmisClient when creating an
+ instance of this class.
+ """
+
+ def __init__(self, cmisClient, data=None):
+ """ Constructor """
+ self._cmisClient = cmisClient
+ self.data = data
+ self._repositoryId = None
+ self._repositoryName = None
+ self._repositoryInfo = {}
+ self._capabilities = {}
+ self._permDefs = {}
+ self._permMap = {}
+ self._permissions = None
+ self._propagation = None
+ self.logger = logging.getLogger('cmislib.browser_binding.BrowserRepository')
+ self.logger.info('Creating an instance of Repository')
+
+ def __str__(self):
+ """To string"""
+ return self.getRepositoryName()
+
+ 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 reload(self):
+ """
+ This method will re-fetch the repository's XML data from the CMIS
+ repository.
+ """
+
+ pass
+
+ def getRepositoryId(self):
+
+ """
+ Returns this repository's unique identifier
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryId()
+ u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
+ """
+
+ if self._repositoryId == None:
+ if self.data == None:
+ self.reload()
+ self._repositoryId = self.data['repositoryId']
+ return self._repositoryId
+
+ def getRepositoryName(self):
+
+ """
+ Returns this repository's name
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryName()
+ u'Main Repository'
+ """
+
+ if self._repositoryName == None:
+ if self.data == None:
+ self.reload()
+ self._repositoryName = self.data['repositoryName']
+ return self._repositoryName
+
+ def getRepositoryInfo(self):
+
+ """
+ Returns a dict of repository information.
+
+ >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName()
+ u'Main Repository'
+ >>> info = repo.getRepositoryInfo()
+ >>> for k,v in info.items():
+ ... print "%s:%s" % (k,v)
+ ...
+ cmisSpecificationTitle:Version 1.0 Committee Draft 04
+ cmisVersionSupported:1.0
+ repositoryDescription:None
+ productVersion:3.2.0 (r2 2440)
+ rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348
+ repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9
+ repositoryName:Main Repository
+ vendorName:Alfresco
+ productName:Alfresco Repository (Community)
+ """
+
+ if not self._repositoryInfo:
+ if self.data == None:
+ self.reload()
+ repoInfo = {}
+ repoInfo['repositoryId'] = self.data['repositoryId']
+ repoInfo['repositoryName'] = self.data['repositoryName']
+ repoInfo['resositoryDescription'] = self.data['repositoryDescription']
+ repoInfo['vendorName'] = self.data['vendorName']
+ repoInfo['productName'] = self.data['productName']
+ repoInfo['productVersion'] = self.data['productVersion']
+ repoInfo['rootFolderId'] = self.data['rootFolderId']
+ repoInfo['latestChangeLogToken'] = self.data['latestChangeLogToken']
+ repoInfo['cmisVersionSupported'] = self.data['cmisVersionSupported']
+ repoInfo['thinClientURI'] = self.data['thinClientURI']
+ repoInfo['changesIncomplete'] = self.data['changesIncomplete']
+ repoInfo['changesOnType'] = self.data['changesOnType']
+ repoInfo['principalIdAnonymous'] = self.data['principalIdAnonymous']
+ repoInfo['principalIdAnyone'] = self.data['principalIdAnyone']
+ if self.data.has_key('extendedFeatures'):
+ repoInfo['extendedFeatures'] = self.data['extendedFeatures']
+ self._repositoryInfo = repoInfo
+ return self._repositoryInfo
+
+ def getRootFolderUrl(self):
+ if self.data == None:
+ self.reload()
+ return self.data['rootFolderUrl']
+
+ 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
+ """
+
+ pass
+
+ def getSupportedPermissions(self):
+ """
+ Returns the value of the cmis:supportedPermissions element. Valid
+ values are:
+
+ - basic: indicates that the CMIS Basic permissions are supported
+ - repository: indicates that repository specific permissions are supported
+ - both: indicates that both CMIS basic permissions and repository specific permissions are supported
+
+ >>> repo.supportedPermissions
+ u'both'
+ """
+
+ if not self.getCapabilities()['ACL']:
+ raise NotSupportedException(messages.NO_ACL_SUPPORT)
+
+ if not self._permissions:
+ if self.data == None:
+ self.reload()
+ if self.data.has_key('aclCapabilities'):
+ if self.data['aclCapabilities'].has_key('supportedPermissions'):
+ self._permissions = self.data['aclCapabilities']['supportedPermissions']
+ return self._permissions
+
+ def getPermissionDefinitions(self):
+
+ """
+ Returns a dictionary of permission definitions for this repository. The
+ key is the permission string or technical name of the permission
+ and the value is the permission description.
+
+ >>> for permDef in repo.permissionDefinitions:
+ ... print permDef
+ ...
+ cmis:all
+ {http://www.alfresco.org/model/system/1.0}base.LinkChildren
+ {http://www.alfresco.org/model/content/1.0}folder.Consumer
+ {http://www.alfresco.org/model/security/1.0}All.All
+ {http://www.alfresco.org/model/system/1.0}base.CreateAssociations
+ {http://www.alfresco.org/model/system/1.0}base.FullControl
+ {http://www.alfresco.org/model/system/1.0}base.AddChildren
+ {http://www.alfresco.org/model/system/1.0}base.ReadAssociations
+ {http://www.alfresco.org/model/content/1.0}folder.Editor
+ {http://www.alfresco.org/model/content/1.0}cmobject.Editor
+ {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations
+ cmis:read
+ cmis:write
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ def getCapabilities(self):
+
+ """
+ Returns a dict of repository capabilities.
+
+ >>> caps = repo.getCapabilities()
+ >>> for k,v in caps.items():
+ ... print "%s:%s" % (k,v)
+ ...
+ PWCUpdatable:True
+ VersionSpecificFiling:False
+ Join:None
+ ContentStreamUpdatability:anytime
+ AllVersionsSearchable:False
+ Renditions:None
+ Multifiling:True
+ GetFolderTree:True
+ GetDescendants:True
+ ACL:None
+ PWCSearchable:True
+ Query:bothcombined
+ Unfiling:False
+ Changes:None
+ """
+
+ if not self._capabilities:
+ if self.data == None:
+ self.reload()
+ caps = {}
+ if self.data.has_key('capabilities'):
+ for cap in self.data['capabilities'].keys():
+ key = cap.replace('capability', '')
+ caps[key] = self.data['capabilities'][cap]
+ self._capabilities = caps
+ return self._capabilities
+
+ def getRootFolder(self):
+ """
+ Returns the root folder of the repository
+
+ >>> root = repo.getRootFolder()
+ >>> root.getObjectId()
+ u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
+ """
+
+ # get the root folder id
+ rootFolderId = self.getRepositoryInfo()['rootFolderId']
+ # instantiate a Folder object using the ID
+ folder = BrowserFolder(self._cmisClient, self, rootFolderId)
+ # return it
+ return folder
+
+ def getFolder(self, folderId):
+
+ """
+ Returns a :class:`Folder` object for a specified folderId
+
+ >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348')
+ >>> someFolder.getObjectId()
+ u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
+ """
+
+ retObject = self.getObject(folderId)
+ return BrowserFolder(self._cmisClient, self, data=retObject.data)
+
+ def getTypeChildren(self,
+ typeId=None):
+
+ """
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getTypeDefinition(self, typeId):
+
+ """
+ Returns an :class:`ObjectType` object for the specified object type id.
+
+ >>> folderType = repo.getTypeDefinition('cmis:folder')
+ """
+
+ pass
+
+ def getLink(self, rel):
+ """
+ Returns the HREF attribute of an Atom link element for the
+ specified rel.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getObject(self,
+ objectId,
+ **kwargs):
+
+ """
+ Returns an object given the specified object ID.
+
+ >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808')
+ >>> doc.getTitle()
+ u'sample-b.pdf'
+
+ The following optional arguments are supported:
+ - returnVersion
+ - filter
+ - includeRelationships
+ - includePolicyIds
+ - renditionFilter
+ - includeACL
+ - includeAllowableActions
+ """
+
+ return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs)
+
+ def query(self, statement, **kwargs):
+
+ """
+ Returns a list of :class:`CmisObject` objects based on the CMIS
+ Query Language passed in as the statement. The actual objects
+ returned will be instances of the appropriate child class based
+ on the object's base type ID.
+
+ In order for the results to be properly instantiated as objects,
+ make sure you include 'cmis:objectId' as one of the fields in
+ your select statement, or just use "SELECT \*".
+
+ If you want the search results to automatically be instantiated with
+ the appropriate sub-class of :class:`CmisObject` you must either
+ include cmis:baseTypeId as one of the fields in your select statement
+ or just use "SELECT \*".
+
+ >>> q = "select * from cmis:document where cmis:name like '%test%'"
+ >>> resultSet = repo.query(q)
+ >>> len(resultSet.getResults())
+ 1
+ >>> resultSet.hasNext()
+ False
+
+ The following optional arguments are supported:
+ - searchAllVersions
+ - includeRelationships
+ - renditionFilter
+ - includeAllowableActions
+ - maxItems
+ - skipCount
+
+ >>> q = 'select * from cmis:document'
+ >>> rs = repo.query(q)
+ >>> len(rs.getResults())
+ 148
+ >>> rs = repo.query(q, maxItems='5')
+ >>> len(rs.getResults())
+ 5
+ >>> rs.hasNext()
+ True
+ """
+
+ pass
+
+ 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)
+ """
+
+ pass
+
+ 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>
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def createDocumentFromSource(self,
+ sourceId,
+ properties={},
+ parentFolder=None):
+ """
+ This is not yet implemented.
+
+ The following optional arguments are not yet supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ pass
+
+ def createFolder(self,
+ parentFolder,
+ name,
+ properties={}):
+
+ """
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def createPolicy(self, properties):
+ """
+ This has not yet been implemented.
+
+ The following optional arguments are not currently supported:
+ - folderId
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ capabilities = property(getCapabilities)
+ id = property(getRepositoryId)
+ info = property(getRepositoryInfo)
+ name = property(getRepositoryName)
+ rootFolder = property(getRootFolder)
+ permissionDefinitions = property(getPermissionDefinitions)
+ permissionMap = property(getPermissionMap)
+ propagation = property(getPropagation)
+ supportedPermissions = property(getSupportedPermissions)
+
+
+class BrowserResultSet(object):
+
+ """
+ Represents a paged result set. In CMIS, this is most often an Atom feed.
+ """
+
+ def __iter__(self):
+ ''' Iterator for the result set '''
+ return iter(self.getResults())
+
+ def __getitem__(self, index):
+ ''' Getter for the result set '''
+ return self.getResults()[index]
+
+ def __len__(self):
+ ''' Len method for the result set '''
+ return len(self.getResults())
+
+ def reload(self):
+
+ '''
+ Re-invokes the self link for the current set of results.
+
+ >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
+ >>> resultSet.reload()
+
+ '''
+
+ pass
+
+ def getResults(self):
+
+ '''
+ Returns the results that were fetched and cached by the get*Page call.
+
+ >>> resultSet = repo.getCheckedOutDocs()
+ >>> resultSet.hasNext()
+ False
+ >>> for result in resultSet.getResults():
+ ... result
+ ...
+ <cmislib.model.Document object at 0x104851810>
+ '''
+
+ pass
+
+ def hasObject(self, objectId):
+
+ '''
+ Returns True if the specified objectId is found in the list of results,
+ otherwise returns False.
+ '''
+
+ pass
+
+ def getFirst(self):
+
+ '''
+ Returns the first page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "first" link. Not all of them do.
+
+ >>> resultSet.hasFirst()
+ True
+ >>> results = resultSet.getFirst()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getPrev(self):
+
+ '''
+ Returns the prev page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "prev" link. Not all of them do.
+ >>> resultSet.hasPrev()
+ True
+ >>> results = resultSet.getPrev()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getNext(self):
+
+ '''
+ Returns the next page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type.
+ >>> resultSet.hasNext()
+ True
+ >>> results = resultSet.getNext()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getLast(self):
+
+ '''
+ Returns the last page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server is returning a "last" link. Not all of them do.
+
+ >>> resultSet.hasLast()
+ True
+ >>> results = resultSet.getLast()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def hasNext(self):
+
+ '''
+ Returns True if this page contains a next link.
+
+ >>> resultSet.hasNext()
+ True
+ '''
+
+ pass
+
+ def hasPrev(self):
+
+ '''
+ Returns True if this page contains a prev link. Not all CMIS providers
+ implement prev links consistently.
+
+ >>> resultSet.hasPrev()
+ True
+ '''
+
+ pass
+
+ def hasFirst(self):
+
+ '''
+ Returns True if this page contains a first link. Not all CMIS providers
+ implement first links consistently.
+
+ >>> resultSet.hasFirst()
+ True
+ '''
+
+ pass
+
+ def hasLast(self):
+
+ '''
+ Returns True if this page contains a last link. Not all CMIS providers
+ implement last links consistently.
+
+ >>> resultSet.hasLast()
+ True
+ '''
+
+ pass
+
+
+class BrowserDocument(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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ def isCheckedOut(self):
+
+ """
+ Returns true if the document is checked out.
+
+ >>> doc.isCheckedOut()
+ True
+ >>> doc.cancelCheckout()
+ >>> doc.isCheckedOut()
+ False
+ """
+
+ pass
+
+ def getCheckedOutBy(self):
+
+ """
+ Returns the ID who currently has the document checked out.
+ >>> pwc = doc.checkout()
+ >>> pwc.getCheckedOutBy()
+ u'admin'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ def setContentStream(self, contentFile, contentType=None):
+
+ """
+ Sets the content stream on this object.
+
+ The following optional arguments are not yet supported:
+ - overwriteFlag=None
+ """
+
+ pass
+
+ def deleteContentStream(self):
+
+ """
+ Delete's the content stream associated with this object.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+class BrowserFolder(BrowserCmisObject):
+
+ """
+ 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
+ """
+
+ pass
+
+ 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')
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ def getParent(self):
+
+ """
+ The optional filter argument is not yet supported.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def removeObject(self, cmisObject):
+
+ """
+ Removes the specified object from this folder. The repository must
+ support unfiling for this to work.
+ """
+
+ pass
+
+ 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`.
+ """
+
+ pass
+
+
+class BrowserRelationship(CmisObject):
+
+ """
+ Defines a relationship object between two :class:`CmisObjects` objects
+ """
+
+ def getSourceId(self):
+
+ """
+ Returns the :class:`CmisId` on the source side of the relationship.
+ """
+
+ pass
+
+ def getTargetId(self):
+
+ """
+ Returns the :class:`CmisId` on the target side of the relationship.
+ """
+
+ pass
+
+ def getSource(self):
+
+ """
+ Returns an instance of the appropriate child-type of :class:`CmisObject`
+ for the source side of the relationship.
+ """
+
+ pass
+
+ def getTarget(self):
+
+ """
+ Returns an instance of the appropriate child-type of :class:`CmisObject`
+ for the target side of the relationship.
+ """
+
+ pass
+
+ sourceId = property(getSourceId)
+ targetId = property(getTargetId)
+ source = property(getSource)
+ target = property(getTarget)
+
+
+class BrowserPolicy(CmisObject):
+
+ """
+ An arbirary object that can 'applied' to objects that the
+ repository identifies as being 'controllable'.
+ """
+
+ pass
+
+
+class BrowserObjectType(object):
+
+ """
+ Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'.
+ Contains metadata about the type.
+ """
+
+ def getTypeId(self):
+
+ """
+ Returns the type ID for this object.
+
+ >>> docType = repo.getTypeDefinition('cmis:document')
+ >>> docType.getTypeId()
+ 'cmis:document'
+ """
+
+ pass
+
+ def getLocalName(self):
+ """Getter for cmis:localName"""
+ pass
+
+ def getLocalNamespace(self):
+ """Getter for cmis:localNamespace"""
+ pass
+
+ def getDisplayName(self):
+ """Getter for cmis:displayName"""
+ pass
+
+ def getQueryName(self):
+ """Getter for cmis:queryName"""
+ pass
+
+ def getDescription(self):
+ """Getter for cmis:description"""
+ pass
+
+ def getBaseId(self):
+ """Getter for cmis:baseId"""
+ pass
+
+ def isCreatable(self):
+ """Getter for cmis:creatable"""
+ pass
+
+ def isFileable(self):
+ """Getter for cmis:fileable"""
+ pass
+
+ def isQueryable(self):
+ """Getter for cmis:queryable"""
+ pass
+
+ def isFulltextIndexed(self):
+ """Getter for cmis:fulltextIndexed"""
+ pass
+
+ def isIncludedInSupertypeQuery(self):
+ """Getter for cmis:includedInSupertypeQuery"""
+ pass
+
+ def isControllablePolicy(self):
+ """Getter for cmis:controllablePolicy"""
+ pass
+
+ def isControllableACL(self):
+ """Getter for cmis:controllableACL"""
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def reload(self, **kwargs):
+ """
+ This method will reload the object's data from the CMIS service.
+ """
+ pass
+
+ id = property(getTypeId)
+ localName = property(getLocalName)
+ localNamespace = property(getLocalNamespace)
+ displayName = property(getDisplayName)
+ queryName = property(getQueryName)
+ description = property(getDescription)
+ baseId = property(getBaseId)
+ creatable = property(isCreatable)
+ fileable = property(isFileable)
+ queryable = property(isQueryable)
+ fulltextIndexed = property(isFulltextIndexed)
+ includedInSupertypeQuery = property(isIncludedInSupertypeQuery)
+ controllablePolicy = property(isControllablePolicy)
+ controllableACL = property(isControllableACL)
+ properties = property(getProperties)
+
+
+class BrowserProperty(object):
+
+ """
+ This class represents an attribute or property definition of an object
+ type.
+ """
+
+ def getId(self):
+ """Getter for cmis:id"""
+ pass
+
+ def getLocalName(self):
+ """Getter for cmis:localName"""
+ pass
+
+ def getLocalNamespace(self):
+ """Getter for cmis:localNamespace"""
+ pass
+
+ def getDisplayName(self):
+ """Getter for cmis:displayName"""
+ pass
+
+ def getQueryName(self):
+ """Getter for cmis:queryName"""
+ pass
+
+ def getDescription(self):
+ """Getter for cmis:description"""
+ pass
+
+ def getPropertyType(self):
+ """Getter for cmis:propertyType"""
+ pass
+
+ def getCardinality(self):
+ """Getter for cmis:cardinality"""
+ pass
+
+ def getUpdatability(self):
+ """Getter for cmis:updatability"""
+ pass
+
+ def isInherited(self):
+ """Getter for cmis:inherited"""
+ pass
+
+ def isRequired(self):
+ """Getter for cmis:required"""
+ pass
+
+ def isQueryable(self):
+ """Getter for cmis:queryable"""
+ pass
+
+ def isOrderable(self):
+ """Getter for cmis:orderable"""
+ pass
+
+ def isOpenChoice(self):
+ """Getter for cmis:openChoice"""
+ pass
+
+ id = property(getId)
+ localName = property(getLocalName)
+ localNamespace = property(getLocalNamespace)
+ displayName = property(getDisplayName)
+ queryName = property(getQueryName)
+ description = property(getDescription)
+ propertyType = property(getPropertyType)
+ cardinality = property(getCardinality)
+ updatability = property(getUpdatability)
+ inherited = property(isInherited)
+ required = property(isRequired)
+ queryable = property(isQueryable)
+ orderable = property(isOrderable)
+ openChoice = property(isOpenChoice)
+
+
+class BrowserACL(object):
+
+ """
+ Represents the Access Control List for an object.
+ """
+
+ 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>}
+ """
+
+ pass
+
+ 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>}
+ """
+
+ pass
+
+ 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()
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ entries = property(getEntries)
+
+
+class BrowserACE(object):
+
+ """
+ Represents an individual Access Control Entry.
+ """
+
+ @property
+ def principalId(self):
+ """Getter for principalId"""
+ pass
+
+ @property
+ def direct(self):
+ """Getter for direct"""
+ pass
+
+ @property
+ def permissions(self):
+ """Getter for permissions"""
+ pass
+
+
+class BrowserChangeEntry(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 getId(self):
+ """
+ Returns the unique ID of the change entry.
+ """
+ pass
+
+ def getObjectId(self):
+ """
+ Returns the object ID of the object that changed.
+ """
+ pass
+
+ def getChangeType(self):
+
+ """
+ Returns the type of change that occurred. The resulting value must be
+ one of:
+
+ - created
+ - updated
+ - deleted
+ - security
+ """
+ pass
+
+ def getACL(self):
+
+ """
+ Gets the :class:`ACL` object that is included with this Change Entry.
+ """
+
+ pass
+
+ def getChangeTime(self):
+
+ """
+ Returns a datetime object representing the time the change occurred.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ id = property(getId)
+ objectId = property(getObjectId)
+ changeTime = property(getChangeTime)
+ changeType = property(getChangeType)
+ properties = property(getProperties)
+
+
+class BrowserChangeEntryResultSet(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.
+ """
+
+ pass
+
+
+class BrowserRendition(object):
+
+ """
+ This class represents a Rendition.
+ """
+
+ def __str__(self):
+ """To string"""
+ return self.getStreamId()
+
+ def getStreamId(self):
+ """Getter for the rendition's stream ID"""
+ pass
+
+ def getMimeType(self):
+ """Getter for the rendition's mime type"""
+ pass
+
+ def getLength(self):
+ """Getter for the renditions's length"""
+ pass
+
+ def getTitle(self):
+ """Getter for the renditions's title"""
+ pass
+
+ def getKind(self):
+ """Getter for the renditions's kind"""
+ pass
+
+ def getHeight(self):
+ """Getter for the renditions's height"""
+ pass
+
+ def getWidth(self):
+ """Getter for the renditions's width"""
+ pass
+
+ def getHref(self):
+ """Getter for the renditions's href"""
+ pass
+
+ def getRenditionDocumentId(self):
+ """Getter for the renditions's width"""
+ pass
+
+ streamId = property(getStreamId)
+ mimeType = property(getMimeType)
+ length = property(getLength)
+ title = property(getTitle)
+ kind = property(getKind)
+ height = property(getHeight)
+ width = property(getWidth)
+ href = property(getHref)
+ renditionDocumentId = property(getRenditionDocumentId)
+
+
+class BrowserCmisId(str):
+
+ """
+ This is a marker class to be used for Strings that are used as CMIS ID's.
+ Making the objects instances of this class makes it easier to create the
+ Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of
+ cmis:propertyString.
+ """
+
+ pass
+
+def getSpecializedObject(obj, **kwargs):
+
+ """
+ Returns an instance of the appropriate :class:`CmisObject` class or one
+ of its child types depending on the specified baseType.
+ """
+
+ moduleLogger.debug('Inside getSpecializedObject')
+
+ if 'cmis:baseTypeId' in obj.getProperties():
+ baseType = obj.getProperties()['cmis:baseTypeId']
+ if baseType == 'cmis:folder':
+ return BrowserFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
+ if baseType == 'cmis:document':
+ return BrowserDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
+ if baseType == 'cmis:relationship':
+ return BrowserRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
+ if baseType == 'cmis:policy':
+ return BrowserPolicy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs)
+
+ # if the base type ID wasn't found in the props (this can happen when
+ # someone runs a query that doesn't select * or doesn't individually
+ # specify baseTypeId) or if the type isn't one of the known base
+ # types, give the object back
+ return obj
diff --git a/src/cmislib/cmis_services.py b/src/cmislib/cmis_services.py
new file mode 100644
index 0000000..bea5212
--- /dev/null
+++ b/src/cmislib/cmis_services.py
@@ -0,0 +1,60 @@
+#
+# 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 base Binding class and other service objects.
+"""
+from exceptions import CmisException, RuntimeException, \
+ ObjectNotFoundException, InvalidArgumentException, \
+ PermissionDeniedException, NotSupportedException, \
+ UpdateConflictException
+
+class Binding(object):
+ def getRepositoryService():
+ pass
+
+ 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)
+
+
+class RepositoryServiceIfc(object):
+ def getRepositories():
+ pass
+
+ def getRepositoryInfo():
+ pass
diff --git a/src/cmislib/domain.py b/src/cmislib/domain.py
new file mode 100644
index 0000000..1b0cff5
--- /dev/null
+++ b/src/cmislib/domain.py
@@ -0,0 +1,2196 @@
+#
+# 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.
+"""
+import logging
+
+moduleLogger = logging.getLogger('cmislib.domain')
+
+class CmisObject(object):
+
+ """
+ Common ancestor class for other CMIS domain objects such as
+ :class:`Document` and :class:`Folder`.
+ """
+
+ 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
+ '*'.
+ """
+
+ pass
+
+ def getObjectId(self):
+
+ """
+ Returns the object ID for this object.
+
+ >>> doc = resultSet.getResults()[0]
+ >>> doc.getObjectId()
+ u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getTitle(self):
+
+ """
+ Returns the value of the object's cmis:title property.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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'
+
+ """
+
+ pass
+
+ 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)
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ def applyPolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def createRelationship(self, targetObj, relTypeId):
+
+ """
+ Creates a relationship between this object and a specified target
+ object using the relationship type specified. Returns the new
+ :class:`Relationship` object.
+
+ >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc')
+ >>> rel.getProperties()
+ {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None}
+
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def removePolicy(self, policyId):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def getAppliedPolicies(self):
+
+ """
+ This is not yet implemented.
+ """
+
+ pass
+
+ def getACL(self):
+
+ """
+ Repository.getCapabilities['ACL'] must return manage or discover.
+
+ >>> acl = folder.getACL()
+ >>> acl.getEntries()
+ {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
+
+ The optional onlyBasicPermissions argument is currently not supported.
+ """
+
+ pass
+
+ 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>}
+ """
+
+ pass
+
+ allowableActions = property(getAllowableActions)
+ name = property(getName)
+ id = property(getObjectId)
+ properties = property(getProperties)
+ title = property(getTitle)
+ ACL = property(getACL)
+
+
+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._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 _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 reload(self):
+ """
+ This method will re-fetch the repository's XML data from the CMIS
+ repository.
+ """
+
+ pass
+
+ def getRepositoryId(self):
+
+ """
+ Returns this repository's unique identifier
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryId()
+ u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
+ """
+
+ pass
+
+ def getRepositoryName(self):
+
+ """
+ Returns this repository's name
+
+ >>> repo = client.getDefaultRepository()
+ >>> repo.getRepositoryName()
+ u'Main Repository'
+ """
+
+ pass
+
+ 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)
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getRootFolder(self):
+ """
+ Returns the root folder of the repository
+
+ >>> root = repo.getRootFolder()
+ >>> root.getObjectId()
+ u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348'
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def getTypeDefinition(self, typeId):
+
+ """
+ Returns an :class:`ObjectType` object for the specified object type id.
+
+ >>> folderType = repo.getTypeDefinition('cmis:folder')
+ """
+
+ pass
+
+ def getLink(self, rel):
+ """
+ Returns the HREF attribute of an Atom link element for the
+ specified rel.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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)
+ """
+
+ pass
+
+ 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>
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def createDocumentFromSource(self,
+ sourceId,
+ properties={},
+ parentFolder=None):
+ """
+ This is not yet implemented.
+
+ The following optional arguments are not yet supported:
+ - versioningState
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ pass
+
+ def createFolder(self,
+ parentFolder,
+ name,
+ properties={}):
+
+ """
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def createPolicy(self, properties):
+ """
+ This has not yet been implemented.
+
+ The following optional arguments are not currently supported:
+ - folderId
+ - policies
+ - addACEs
+ - removeACEs
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ capabilities = property(getCapabilities)
+ id = property(getRepositoryId)
+ info = property(getRepositoryInfo)
+ name = property(getRepositoryName)
+ rootFolder = property(getRootFolder)
+ permissionDefinitions = property(getPermissionDefinitions)
+ permissionMap = property(getPermissionMap)
+ propagation = property(getPropagation)
+ supportedPermissions = property(getSupportedPermissions)
+
+
+class ResultSet(object):
+
+ """
+ Represents a paged result set. In CMIS, this is most often an Atom feed.
+ """
+
+ def __iter__(self):
+ ''' Iterator for the result set '''
+ return iter(self.getResults())
+
+ def __getitem__(self, index):
+ ''' Getter for the result set '''
+ return self.getResults()[index]
+
+ def __len__(self):
+ ''' Len method for the result set '''
+ return len(self.getResults())
+
+ def reload(self):
+
+ '''
+ Re-invokes the self link for the current set of results.
+
+ >>> resultSet = repo.getCollection(CHECKED_OUT_COLL)
+ >>> resultSet.reload()
+
+ '''
+
+ pass
+
+ def getResults(self):
+
+ '''
+ Returns the results that were fetched and cached by the get*Page call.
+
+ >>> resultSet = repo.getCheckedOutDocs()
+ >>> resultSet.hasNext()
+ False
+ >>> for result in resultSet.getResults():
+ ... result
+ ...
+ <cmislib.model.Document object at 0x104851810>
+ '''
+
+ pass
+
+ def hasObject(self, objectId):
+
+ '''
+ Returns True if the specified objectId is found in the list of results,
+ otherwise returns False.
+ '''
+
+ pass
+
+ def getFirst(self):
+
+ '''
+ Returns the first page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "first" link. Not all of them do.
+
+ >>> resultSet.hasFirst()
+ True
+ >>> results = resultSet.getFirst()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getPrev(self):
+
+ '''
+ Returns the prev page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server returns a "prev" link. Not all of them do.
+ >>> resultSet.hasPrev()
+ True
+ >>> results = resultSet.getPrev()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getNext(self):
+
+ '''
+ Returns the next page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type.
+ >>> resultSet.hasNext()
+ True
+ >>> results = resultSet.getNext()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def getLast(self):
+
+ '''
+ Returns the last page of results as a dictionary of
+ :class:`CmisObject` objects or its appropriate sub-type. This only
+ works when the server is returning a "last" link. Not all of them do.
+
+ >>> resultSet.hasLast()
+ True
+ >>> results = resultSet.getLast()
+ >>> for result in results:
+ ... result
+ ...
+ <cmislib.model.Document object at 0x10480bc90>
+ '''
+
+ pass
+
+ def hasNext(self):
+
+ '''
+ Returns True if this page contains a next link.
+
+ >>> resultSet.hasNext()
+ True
+ '''
+
+ pass
+
+ def hasPrev(self):
+
+ '''
+ Returns True if this page contains a prev link. Not all CMIS providers
+ implement prev links consistently.
+
+ >>> resultSet.hasPrev()
+ True
+ '''
+
+ pass
+
+ def hasFirst(self):
+
+ '''
+ Returns True if this page contains a first link. Not all CMIS providers
+ implement first links consistently.
+
+ >>> resultSet.hasFirst()
+ True
+ '''
+
+ pass
+
+ def hasLast(self):
+
+ '''
+ Returns True if this page contains a last link. Not all CMIS providers
+ implement last links consistently.
+
+ >>> resultSet.hasLast()
+ True
+ '''
+
+ pass
+
+
+class 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ def isCheckedOut(self):
+
+ """
+ Returns true if the document is checked out.
+
+ >>> doc.isCheckedOut()
+ True
+ >>> doc.cancelCheckout()
+ >>> doc.isCheckedOut()
+ False
+ """
+
+ pass
+
+ def getCheckedOutBy(self):
+
+ """
+ Returns the ID who currently has the document checked out.
+ >>> pwc = doc.checkout()
+ >>> pwc.getCheckedOutBy()
+ u'admin'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ def setContentStream(self, contentFile, contentType=None):
+
+ """
+ Sets the content stream on this object.
+
+ The following optional arguments are not yet supported:
+ - overwriteFlag=None
+ """
+
+ pass
+
+ def deleteContentStream(self):
+
+ """
+ Delete's the content stream associated with this object.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+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
+ """
+
+ pass
+
+ 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')
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+
+ """
+
+ pass
+
+ 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'
+ """
+
+ pass
+
+ def getParent(self):
+
+ """
+ The optional filter argument is not yet supported.
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def removeObject(self, cmisObject):
+
+ """
+ Removes the specified object from this folder. The repository must
+ support unfiling for this to work.
+ """
+
+ pass
+
+ 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`.
+ """
+
+ pass
+
+
+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.
+ """
+
+ pass
+
+ def getTargetId(self):
+
+ """
+ Returns the :class:`CmisId` on the target side of the relationship.
+ """
+
+ pass
+
+ def getSource(self):
+
+ """
+ Returns an instance of the appropriate child-type of :class:`CmisObject`
+ for the source side of the relationship.
+ """
+
+ pass
+
+ def getTarget(self):
+
+ """
+ Returns an instance of the appropriate child-type of :class:`CmisObject`
+ for the target side of the relationship.
+ """
+
+ pass
+
+ 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 getTypeId(self):
+
+ """
+ Returns the type ID for this object.
+
+ >>> docType = repo.getTypeDefinition('cmis:document')
+ >>> docType.getTypeId()
+ 'cmis:document'
+ """
+
+ pass
+
+ def getLocalName(self):
+ """Getter for cmis:localName"""
+ pass
+
+ def getLocalNamespace(self):
+ """Getter for cmis:localNamespace"""
+ pass
+
+ def getDisplayName(self):
+ """Getter for cmis:displayName"""
+ pass
+
+ def getQueryName(self):
+ """Getter for cmis:queryName"""
+ pass
+
+ def getDescription(self):
+ """Getter for cmis:description"""
+ pass
+
+ def getBaseId(self):
+ """Getter for cmis:baseId"""
+ pass
+
+ def isCreatable(self):
+ """Getter for cmis:creatable"""
+ pass
+
+ def isFileable(self):
+ """Getter for cmis:fileable"""
+ pass
+
+ def isQueryable(self):
+ """Getter for cmis:queryable"""
+ pass
+
+ def isFulltextIndexed(self):
+ """Getter for cmis:fulltextIndexed"""
+ pass
+
+ def isIncludedInSupertypeQuery(self):
+ """Getter for cmis:includedInSupertypeQuery"""
+ pass
+
+ def isControllablePolicy(self):
+ """Getter for cmis:controllablePolicy"""
+ pass
+
+ def isControllableACL(self):
+ """Getter for cmis:controllableACL"""
+ pass
+
+ 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'
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ def reload(self, **kwargs):
+ """
+ This method will reload the object's data from the CMIS service.
+ """
+ pass
+
+ 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 getId(self):
+ """Getter for cmis:id"""
+ pass
+
+ def getLocalName(self):
+ """Getter for cmis:localName"""
+ pass
+
+ def getLocalNamespace(self):
+ """Getter for cmis:localNamespace"""
+ pass
+
+ def getDisplayName(self):
+ """Getter for cmis:displayName"""
+ pass
+
+ def getQueryName(self):
+ """Getter for cmis:queryName"""
+ pass
+
+ def getDescription(self):
+ """Getter for cmis:description"""
+ pass
+
+ def getPropertyType(self):
+ """Getter for cmis:propertyType"""
+ pass
+
+ def getCardinality(self):
+ """Getter for cmis:cardinality"""
+ pass
+
+ def getUpdatability(self):
+ """Getter for cmis:updatability"""
+ pass
+
+ def isInherited(self):
+ """Getter for cmis:inherited"""
+ pass
+
+ def isRequired(self):
+ """Getter for cmis:required"""
+ pass
+
+ def isQueryable(self):
+ """Getter for cmis:queryable"""
+ pass
+
+ def isOrderable(self):
+ """Getter for cmis:orderable"""
+ pass
+
+ def isOpenChoice(self):
+ """Getter for cmis:openChoice"""
+ pass
+
+ 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 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>}
+ """
+
+ pass
+
+ 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>}
+ """
+
+ pass
+
+ 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()
+ """
+
+ pass
+
+ 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
+ """
+
+ pass
+
+ entries = property(getEntries)
+
+
+class ACE(object):
+
+ """
+ Represents an individual Access Control Entry.
+ """
+
+ @property
+ def principalId(self):
+ """Getter for principalId"""
+ pass
+
+ @property
+ def direct(self):
+ """Getter for direct"""
+ pass
+
+ @property
+ def permissions(self):
+ """Getter for permissions"""
+ pass
+
+
+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 getId(self):
+ """
+ Returns the unique ID of the change entry.
+ """
+ pass
+
+ def getObjectId(self):
+ """
+ Returns the object ID of the object that changed.
+ """
+ pass
+
+ def getChangeType(self):
+
+ """
+ Returns the type of change that occurred. The resulting value must be
+ one of:
+
+ - created
+ - updated
+ - deleted
+ - security
+ """
+ pass
+
+ def getACL(self):
+
+ """
+ Gets the :class:`ACL` object that is included with this Change Entry.
+ """
+
+ pass
+
+ def getChangeTime(self):
+
+ """
+ Returns a datetime object representing the time the change occurred.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+ 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.
+ """
+
+ pass
+
+
+class Rendition(object):
+
+ """
+ This class represents a Rendition.
+ """
+
+ def __str__(self):
+ """To string"""
+ return self.getStreamId()
+
+ def getStreamId(self):
+ """Getter for the rendition's stream ID"""
+ pass
+
+ def getMimeType(self):
+ """Getter for the rendition's mime type"""
+ pass
+
+ def getLength(self):
+ """Getter for the renditions's length"""
+ pass
+
+ def getTitle(self):
+ """Getter for the renditions's title"""
+ pass
+
+ def getKind(self):
+ """Getter for the renditions's kind"""
+ pass
+
+ def getHeight(self):
+ """Getter for the renditions's height"""
+ pass
+
+ def getWidth(self):
+ """Getter for the renditions's width"""
+ pass
+
+ def getHref(self):
+ """Getter for the renditions's href"""
+ pass
+
+ def getRenditionDocumentId(self):
+ """Getter for the renditions's width"""
+ pass
+
+ 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
diff --git a/src/cmislib/model.py b/src/cmislib/model.py
index 562f2ea..cc86368 100644
--- a/src/cmislib/model.py
+++ b/src/cmislib/model.py
@@ -17,78 +17,17 @@
# under the License.
#
"""
-Module containing the domain objects used to work with a CMIS provider.
+Module containing the CmisClient object, which is responsible for
+keeping track of connection information. The name 'model' is no longer
+really appropriate, but it is kept for backwards compatibility.
"""
-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
+from atompub_binding import AtomPubBinding
+from cmis_services import Binding
+from domain import CmisObject, Repository
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):
"""
@@ -110,6 +49,10 @@
self.username = username
self.password = password
self.extArgs = kwargs
+ if (kwargs.has_key('binding') and (isinstance(kwargs['binding'], Binding))):
+ self.binding = kwargs['binding']
+ else:
+ self.binding = AtomPubBinding(**kwargs)
self.logger = logging.getLogger('cmislib.model.CmisClient')
self.logger.info('Creating an instance of CmisClient')
@@ -128,21 +71,7 @@
[{'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
+ return self.binding.getRepositoryService().getRepositories(self)
def getRepository(self, repositoryId):
@@ -153,16 +82,7 @@
>>> 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)
+ return self.binding.getRepositoryService().getRepository(self, repositoryId)
def getDefaultRepository(self):
@@ -176,133 +96,7 @@
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
+ return self.binding.getRepositoryService().getDefaultRepository(self)
def _processCommonErrors(self, error):
@@ -329,3923 +123,3 @@
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
diff --git a/src/cmislib/util.py b/src/cmislib/util.py
new file mode 100644
index 0000000..b85e6f5
--- /dev/null
+++ b/src/cmislib/util.py
@@ -0,0 +1,111 @@
+#
+# 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 handy utility functions.
+"""
+import re
+import iso8601
+import logging
+from cmislib.domain import CmisId, Document, Folder
+
+moduleLogger = logging.getLogger('cmislib.util')
+
+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 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
+
+
diff --git a/src/tests/cmislibtest.py b/src/tests/cmislibtest.py
index 69a64e4..f99e5e4 100644
--- a/src/tests/cmislibtest.py
+++ b/src/tests/cmislibtest.py
@@ -23,7 +23,8 @@
'''
import unittest
from unittest import TestSuite, TestLoader
-from cmislib.model import CmisClient, ACE
+from cmislib.model import CmisClient
+from cmislib.domain import ACE
from cmislib.exceptions import \
ObjectNotFoundException, \
CmisException, \
@@ -1408,8 +1409,8 @@
if not self._repo.getSupportedPermissions() in ['both', 'basic']:
print 'Repository needs to support either both or basic permissions for this test'
return
- acl = self._testFolder.getACL()
- acl.addEntry(ACE(settings.TEST_PRINCIPAL_ID, 'cmis:write', 'true'))
+ acl = self._testFolder.getACL()
+ acl.addEntry(settings.TEST_PRINCIPAL_ID, 'cmis:write', 'true')
acl = self._testFolder.applyACL(acl)
# would be good to check that the permission we get back is what we set
# but at least one server (Alf) appears to map the basic perm to a
diff --git a/src/tests/settings.py b/src/tests/settings.py
index 423886e..c70d009 100644
--- a/src/tests/settings.py
+++ b/src/tests/settings.py
@@ -22,8 +22,8 @@
#
# CMIS repository's service URL
#REPOSITORY_URL = 'http://cmis.alfresco.com/s/cmis' # Alfresco demo
-#REPOSITORY_URL = 'http://localhost:8080/chemistry/atom' # Apache Chemistry
-REPOSITORY_URL = 'http://localhost:8080/alfresco/cmisatom' # Alfresco 4.0
+REPOSITORY_URL = 'http://localhost:8080/chemistry/atom' # Apache Chemistry
+#REPOSITORY_URL = 'http://localhost:8080/alfresco/cmisatom' # Alfresco 4.0
#REPOSITORY_URL = 'http://localhost:8080/alfresco/s/api/cmis' # Alfresco
#REPOSITORY_URL = 'http://cmis.demo.nuxeo.org/nuxeo/atom/cmis' # Nuxeo demo
#REPOSITORY_URL = 'http://localhost:8080/nuxeo/atom/cmis' # Nuxeo local