| # -*- coding: utf-8 -*- |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| """ |
| Module containing the Atom Pub binding-specific objects used to work with a CMIS |
| provider. |
| """ |
| from cmislib.cmis_services import Binding, RepositoryServiceIfc |
| from cmislib.domain import CmisId, CmisObject, ObjectType, Property, ACL, ACE, ChangeEntry, ResultSet, Rendition |
| from cmislib import messages |
| from cmislib.net import RESTService as Rest |
| from cmislib.exceptions import CmisException, \ |
| ObjectNotFoundException, InvalidArgumentException, \ |
| NotSupportedException |
| from cmislib.util import multiple_replace, parsePropValue, parseBoolValue, toCMISValue, parseDateTimeValue, safe_quote |
| |
| from urllib import quote |
| from urlparse import urlparse, urlunparse |
| import re |
| import mimetypes |
| from xml.parsers.expat import ExpatError |
| import datetime |
| import StringIO |
| import logging |
| from xml.dom import minidom |
| |
| moduleLogger = logging.getLogger('cmislib.atompub.binding') |
| |
| # 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): |
| |
| """ |
| The binding responsible for talking to the CMIS server via the AtomPub |
| Publishing Protocol. |
| """ |
| |
| def __init__(self, **kwargs): |
| self.extArgs = kwargs |
| |
| def getRepositoryService(self): |
| return RepositoryService() |
| |
| def get(self, url, username, password, **kwargs): |
| |
| """ |
| Does a get against the CMIS service. More than likely, you will not |
| need to call this method. Instead, let the other objects do it for you. |
| |
| For example, if you need to get a specific object by object id, try |
| :class:`Repository.getObject`. If you have a path instead of an object |
| id, use :class:`Repository.getObjectByPath`. Or, you could start with |
| the root folder (:class:`Repository.getRootFolder`) and drill down from |
| there. |
| """ |
| |
| # merge the cmis client extended args with the ones that got passed in |
| if len(self.extArgs) > 0: |
| kwargs.update(self.extArgs) |
| |
| resp, content = Rest().get(url, |
| username=username, |
| password=password, |
| **kwargs) |
| if resp['status'] != '200': |
| self._processCommonErrors(resp, url) |
| return content |
| else: |
| try: |
| return minidom.parseString(content) |
| except ExpatError: |
| raise CmisException('Could not parse server response', url) |
| |
| def delete(self, url, username, password, **kwargs): |
| |
| """ |
| Does a delete against the CMIS service. More than likely, you will not |
| need to call this method. Instead, let the other objects do it for you. |
| |
| For example, to delete a folder you'd call :class:`Folder.delete` and |
| to delete a document you'd call :class:`Document.delete`. |
| """ |
| |
| # merge the cmis client extended args with the ones that got passed in |
| if len(self.extArgs) > 0: |
| kwargs.update(self.extArgs) |
| |
| resp, content = Rest().delete(url, |
| username=username, |
| password=password, |
| **kwargs) |
| if resp['status'] != '200' and resp['status'] != '204': |
| self._processCommonErrors(resp, url) |
| return content |
| else: |
| pass |
| |
| def post(self, url, username, password, payload, contentType, **kwargs): |
| |
| """ |
| Does a post against the CMIS service. More than likely, you will not |
| need to call this method. Instead, let the other objects do it for you. |
| |
| For example, to update the properties on an object, you'd call |
| :class:`CmisObject.updateProperties`. Or, to check in a document that's |
| been checked out, you'd call :class:`Document.checkin` on the PWC. |
| """ |
| |
| # merge the cmis client extended args with the ones that got passed in |
| if len(self.extArgs) > 0: |
| kwargs.update(self.extArgs) |
| |
| resp, content = Rest().post(url, |
| payload, |
| contentType, |
| username=username, |
| password=password, |
| **kwargs) |
| if resp['status'] == '200': |
| try: |
| return minidom.parseString(content) |
| except ExpatError: |
| raise CmisException('Could not parse server response', url) |
| elif resp['status'] == '201': |
| try: |
| return minidom.parseString(content) |
| except ExpatError: |
| raise CmisException('Could not parse server response', url) |
| else: |
| self._processCommonErrors(resp, url) |
| return resp |
| |
| def put(self, url, username, password, payload, contentType, **kwargs): |
| |
| """ |
| Does a put against the CMIS service. More than likely, you will not |
| need to call this method. Instead, let the other objects do it for you. |
| |
| For example, to update the properties on an object, you'd call |
| :class:`CmisObject.updateProperties`. Or, to check in a document that's |
| been checked out, you'd call :class:`Document.checkin` on the PWC. |
| """ |
| |
| # merge the cmis client extended args with the ones that got passed in |
| if len(self.extArgs) > 0: |
| kwargs.update(self.extArgs) |
| |
| resp, content = Rest().put(url, |
| payload, |
| contentType, |
| username=username, |
| password=password, |
| **kwargs) |
| if resp['status'] != '200' and resp['status'] != '201': |
| self._processCommonErrors(resp, url) |
| return content |
| else: |
| try: |
| return minidom.parseString(content) |
| except ExpatError: |
| # This may happen and is normal |
| return None |
| |
| |
| class RepositoryService(RepositoryServiceIfc): |
| |
| """ |
| The repository service for the AtomPub binding. |
| """ |
| |
| def __init__(self): |
| self._uriTemplates = {} |
| self.logger = logging.getLogger('cmislib.atompub.binding.RepositoryService') |
| |
| def reload(self, obj): |
| |
| """ Reloads the state of the repository object.""" |
| |
| 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): |
| |
| """ |
| Get the repository for the specified 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): |
| |
| """ |
| Get all of the repositories provided by the server. |
| """ |
| |
| result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) |
| |
| 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): |
| |
| """ |
| Returns the default repository for the server via the AtomPub binding. |
| """ |
| |
| 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.atompub.binding.AtomPubCmisObject') |
| self.logger.debug('Creating an instance of AtomPubCmisObject') |
| |
| 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 is None: |
| if self.xmlDoc is 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 is None: |
| raise NotSupportedException('Root folder does not support getObjectParents') |
| |
| # invoke the URL |
| result = self._cmisClient.binding.get(parentUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # return the result set |
| return AtomPubResultSet(self._cmisClient, self._repository, result) |
| |
| def getPaths(self): |
| """ |
| Returns the object's paths as a list of strings. |
| """ |
| # see sub-classes for implementation |
| pass |
| |
| def 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 is 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 |
| |
| 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 atom:title property. |
| """ |
| |
| if self.xmlDoc is 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 is 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: |
| try: |
| propertyValue.append(parsePropValue(valNode. |
| childNodes[0].data, |
| node.localName)) |
| except IndexError: |
| pass |
| 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 is 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'] is not 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 |
| self._cmisClient.binding.post(postUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| self.xmlDoc.toxml(encoding='utf-8'), |
| ATOM_XML_ENTRY_TYPE, |
| **args) |
| |
| def delete(self, **kwargs): |
| |
| """ |
| Deletes this :class:`CmisObject` from the repository. Note that in the |
| case of a :class:`Folder` object, some repositories will refuse to |
| delete it if it contains children and some will delete it without |
| complaint. If what you really want to do is delete the folder and all |
| of its descendants, use :meth:`~Folder.deleteTree` instead. |
| |
| >>> folder.delete() |
| |
| The optional allVersions argument is supported. |
| """ |
| |
| url = self._getSelfLink() |
| self._cmisClient.binding.delete(url.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| def applyPolicy(self, policyId): |
| |
| """ |
| This is not yet implemented. |
| """ |
| |
| # depends on this object's canApplyPolicy allowable action |
| if self.getAllowableActions()['canApplyPolicy']: |
| raise NotImplementedError |
| else: |
| raise CmisException('This object has canApplyPolicy set to false') |
| |
| def createRelationship(self, targetObj, relTypeId): |
| |
| """ |
| Creates a relationship between this object and a specified target |
| object using the relationship type specified. Returns the new |
| :class:`Relationship` object. |
| |
| >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') |
| >>> rel.getProperties() |
| {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} |
| |
| """ |
| |
| if isinstance(relTypeId, str): |
| relTypeId = CmisId(relTypeId) |
| |
| props = {} |
| props['cmis:sourceId'] = self.getObjectId() |
| props['cmis:targetId'] = targetObj.getObjectId() |
| props['cmis:objectTypeId'] = relTypeId |
| xmlDoc = getEntryXmlDoc(self._repository, properties=props) |
| |
| url = self._getLink(RELATIONSHIPS_REL) |
| assert url is not None, 'Could not determine relationships URL' |
| |
| result = self._cmisClient.binding.post(url.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| xmlDoc.toxml(encoding='utf-8'), |
| ATOM_XML_TYPE) |
| |
| # instantiate CmisObject objects with the results and return the list |
| entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') |
| assert(len(entryElements) == 1), "Expected entry element in result from relationship URL post" |
| return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0])) |
| |
| def getRelationships(self, **kwargs): |
| |
| """ |
| Returns a :class:`ResultSet` of :class:`Relationship` objects for each |
| relationship where the source is this object. |
| |
| >>> rels = tstDoc1.getRelationships() |
| >>> len(rels.getResults()) |
| 1 |
| >>> rel = rels.getResults().values()[0] |
| >>> rel.getProperties() |
| {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} |
| |
| The following optional arguments are supported: |
| - includeSubRelationshipTypes |
| - relationshipDirection |
| - typeId |
| - maxItems |
| - skipCount |
| - filter |
| - includeAllowableActions |
| """ |
| |
| url = self._getLink(RELATIONSHIPS_REL) |
| assert url is not None, 'Could not determine relationships URL' |
| |
| result = self._cmisClient.binding.get(url.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # return the result set |
| return AtomPubResultSet(self._cmisClient, self._repository, result) |
| |
| def removePolicy(self, policyId): |
| |
| """ |
| This is not yet implemented. |
| """ |
| |
| # depends on this object's canRemovePolicy allowable action |
| if self.getAllowableActions()['canRemovePolicy']: |
| raise NotImplementedError |
| else: |
| raise CmisException('This object has canRemovePolicy set to false') |
| |
| def getAppliedPolicies(self): |
| |
| """ |
| This is not yet implemented. |
| """ |
| |
| # depends on this object's canGetAppliedPolicies allowable action |
| if self.getAllowableActions()['canGetAppliedPolicies']: |
| raise NotImplementedError |
| else: |
| raise CmisException('This object has canGetAppliedPolicies set to false') |
| |
| def getACL(self): |
| |
| """ |
| Repository.getCapabilities['ACL'] must return manage or discover. |
| |
| >>> acl = folder.getACL() |
| >>> acl.getEntries() |
| {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>} |
| |
| The optional onlyBasicPermissions argument is currently not supported. |
| """ |
| |
| if self._repository.getCapabilities()['ACL']: |
| # if the ACL capability is discover or manage, this must be |
| # supported |
| aclUrl = self._getLink(ACL_REL) |
| result = self._cmisClient.binding.get(aclUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password) |
| return AtomPubACL(xmlDoc=result) |
| else: |
| raise NotSupportedException |
| |
| def applyACL(self, acl): |
| |
| """ |
| Updates the object with the provided :class:`ACL`. |
| Repository.getCapabilities['ACL'] must return manage to invoke this |
| call. |
| |
| >>> acl = folder.getACL() |
| >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true')) |
| >>> acl.getEntries() |
| {u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>} |
| """ |
| |
| if self._repository.getCapabilities()['ACL'] == 'manage': |
| # if the ACL capability is manage, this must be |
| # supported |
| # but it also depends on the canApplyACL allowable action |
| # for this object |
| if not isinstance(acl, ACL): |
| raise CmisException('The ACL to apply must be an instance of the ACL class.') |
| aclUrl = self._getLink(ACL_REL) |
| assert aclUrl, "Could not determine the object's ACL URL." |
| result = self._cmisClient.binding.put(aclUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| acl.getXmlDoc().toxml(encoding='utf-8'), |
| CMIS_ACL_TYPE) |
| return AtomPubACL(xmlDoc=result) |
| else: |
| raise NotSupportedException |
| |
| def _getSelfLink(self): |
| |
| """ |
| Returns the URL used to retrieve this object. |
| """ |
| |
| url = self._getLink(SELF_REL) |
| |
| assert len(url) > 0, "Could not determine the self link." |
| |
| return url |
| |
| def _getLink(self, rel, ltype=None): |
| |
| """ |
| Returns the HREF attribute of an Atom link element for the |
| specified rel. |
| """ |
| |
| if self.xmlDoc is None: |
| self.reload() |
| linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') |
| |
| for linkElement in linkElements: |
| |
| if ltype: |
| if linkElement.attributes.has_key('rel'): |
| relAttr = linkElement.attributes['rel'].value |
| |
| if ltype and linkElement.attributes.has_key('type'): |
| typeAttr = linkElement.attributes['type'].value |
| |
| if relAttr == rel and ltype.match(typeAttr): |
| return linkElement.attributes['href'].value |
| else: |
| if linkElement.attributes.has_key('rel'): |
| relAttr = linkElement.attributes['rel'].value |
| |
| if relAttr == rel: |
| return linkElement.attributes['href'].value |
| |
| def getRepository(self): |
| |
| """ |
| Returns the object's repository |
| """ |
| |
| return self._repository |
| |
| allowableActions = property(getAllowableActions) |
| name = property(getName) |
| id = property(getObjectId) |
| properties = property(getProperties) |
| title = property(getTitle) |
| ACL = property(getACL) |
| repository = property(getRepository) |
| |
| |
| 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.atompub.binding.AtomPubRepository') |
| self.logger.debug('Creating an instance of AtomPubRepository') |
| |
| def __str__(self): |
| """To string""" |
| return self.getRepositoryId() |
| |
| 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 is 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 is 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 is 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 is 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 is None: |
| if self.xmlDoc is 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 is None: |
| if self.xmlDoc is None: |
| self.reload() |
| if self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild: |
| self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data |
| else: |
| self._repositoryName = u'' |
| |
| 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 is 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' and \ |
| node.localName != 'aclCapability': |
| try: |
| data = node.childNodes[0].data |
| except IndexError: |
| data = None |
| except AttributeError: |
| 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 is 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 = AtomPubObjectType(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 = repo.getTypeDescendants('cmis:folder') |
| >>> len(types) |
| 17 |
| >>> types = repo.getTypeDescendants('cmis:folder', depth=1) |
| >>> len(types) |
| 12 |
| >>> types = repo.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 is 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, CmisId(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}': safe_quote(path), |
| '{filter}': '', |
| '{includeAllowableActions}': 'false', |
| '{includePolicyIds}': 'false', |
| '{includeRelationships}': '', |
| '{includeACL}': 'false', |
| '{renditionFilter}': ''} |
| |
| options = {} |
| addOptions = {} # args specified, but not in the template |
| for k, v in kwargs.items(): |
| pKey = "{" + k + "}" |
| if template.find(pKey) >= 0: |
| options[pKey] = toCMISValue(v) |
| else: |
| addOptions[k] = toCMISValue(v) |
| |
| # merge the templated args with the default params |
| params.update(options) |
| |
| byObjectPathUrl = multiple_replace(params, template) |
| |
| # do a GET against the URL |
| result = self._cmisClient.binding.get(byObjectPathUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **addOptions) |
| |
| # instantiate CmisObject objects with the results and return the list |
| entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') |
| assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byObjectPathUrl |
| return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **kwargs), **kwargs) |
| |
| def query(self, statement, **kwargs): |
| |
| """ |
| Returns a list of :class:`CmisObject` objects based on the CMIS |
| Query Language passed in as the statement. The actual objects |
| returned will be instances of the appropriate child class based |
| on the object's base type ID. |
| |
| In order for the results to be properly instantiated as objects, |
| make sure you include 'cmis:objectId' as one of the fields in |
| your select statement, or just use "SELECT \*". |
| |
| If you want the search results to automatically be instantiated with |
| the appropriate sub-class of :class:`CmisObject` you must either |
| include cmis:baseTypeId as one of the fields in your select statement |
| or just use "SELECT \*". |
| |
| >>> q = "select * from cmis:document where cmis:name like '%test%'" |
| >>> resultSet = repo.query(q) |
| >>> len(resultSet.getResults()) |
| 1 |
| >>> resultSet.hasNext() |
| False |
| |
| The following optional arguments are supported: |
| - searchAllVersions |
| - includeRelationships |
| - renditionFilter |
| - includeAllowableActions |
| - maxItems |
| - skipCount |
| |
| >>> q = 'select * from cmis:document' |
| >>> rs = repo.query(q) |
| >>> len(rs.getResults()) |
| 148 |
| >>> rs = repo.query(q, maxItems='5') |
| >>> len(rs.getResults()) |
| 5 |
| >>> rs.hasNext() |
| True |
| """ |
| |
| if self.xmlDoc is None: |
| self.reload() |
| |
| # get the URL this repository uses to accept query POSTs |
| queryUrl = self.getCollectionLink(QUERY_COLL) |
| |
| # build the CMIS query XML that we're going to POST |
| xmlDoc = self._getQueryXmlDoc(statement, **kwargs) |
| |
| # do the POST |
| # print 'posting:%s' % xmlDoc.toxml(encoding='utf-8') |
| result = self._cmisClient.binding.post(queryUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| xmlDoc.toxml(encoding='utf-8'), |
| CMIS_QUERY_TYPE) |
| |
| # return the result set |
| return AtomPubResultSet(self._cmisClient, self, result) |
| |
| def getContentChanges(self, **kwargs): |
| |
| """ |
| Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. |
| |
| >>> for changeEntry in rs: |
| ... changeEntry.objectId |
| ... changeEntry.id |
| ... changeEntry.changeType |
| ... changeEntry.changeTime |
| ... |
| 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' |
| u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' |
| u'created' |
| datetime.datetime(2010, 2, 11, 12, 55, 14) |
| 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' |
| u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' |
| u'updated' |
| datetime.datetime(2010, 2, 11, 12, 55, 13) |
| 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' |
| u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' |
| u'updated' |
| |
| The following optional arguments are supported: |
| - changeLogToken |
| - includeProperties |
| - includePolicyIDs |
| - includeACL |
| - maxItems |
| |
| You can get the latest change log token by inspecting the repository |
| info via :meth:`Repository.getRepositoryInfo`. |
| |
| >>> repo.info['latestChangeLogToken'] |
| u'2692' |
| >>> rs = repo.getContentChanges(changeLogToken='2692') |
| >>> len(rs) |
| 1 |
| >>> rs[0].id |
| u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' |
| >>> rs[0].changeType |
| u'updated' |
| >>> rs[0].changeTime |
| datetime.datetime(2010, 2, 16, 20, 6, 37) |
| """ |
| |
| if self.getCapabilities()['Changes'] is None: |
| raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT) |
| |
| changesUrl = self.getLink(CHANGE_LOG_REL) |
| result = self._cmisClient.binding.get(changesUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # return the result set |
| return AtomPubChangeEntryResultSet(self._cmisClient, self, result) |
| |
| def createDocumentFromString(self, |
| name, |
| properties={}, |
| parentFolder=None, |
| contentString=None, |
| contentType=None, |
| contentEncoding=None): |
| |
| """ |
| Creates a new document setting the content to the string provided. If |
| the repository supports unfiled objects, you do not have to pass in |
| a parent :class:`Folder` otherwise it is required. |
| |
| This method is essentially a convenience method that wraps your string |
| with a StringIO and then calls createDocument. |
| |
| >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') |
| <cmislib.model.Document object at 0x101352ed0> |
| """ |
| |
| # if you didn't pass in a parent folder |
| if parentFolder is None: |
| # if the repository doesn't require fileable objects to be filed |
| if self.getCapabilities()['Unfiling']: |
| # has not been implemented |
| # postUrl = self.getCollectionLink(UNFILED_COLL) |
| raise NotImplementedError |
| else: |
| # this repo requires fileable objects to be filed |
| raise InvalidArgumentException |
| |
| return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString), |
| contentType, contentEncoding) |
| |
| def createDocument(self, |
| name, |
| properties={}, |
| parentFolder=None, |
| contentFile=None, |
| contentType=None, |
| contentEncoding=None): |
| |
| """ |
| Creates a new :class:`Document` object. If the repository |
| supports unfiled objects, you do not have to pass in |
| a parent :class:`Folder` otherwise it is required. |
| |
| To create a document with an associated contentFile, pass in a |
| File object. The method will attempt to guess the appropriate content |
| type and encoding based on the file. To specify it yourself, pass them |
| in via the contentType and contentEncoding arguments. |
| |
| >>> f = open('sample-a.pdf', 'rb') |
| >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) |
| <cmislib.model.Document object at 0x105be5e10> |
| >>> f.close() |
| >>> doc.getTitle() |
| u'sample-a.pdf' |
| |
| The following optional arguments are not currently supported: |
| - versioningState |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| |
| postUrl = '' |
| # if you didn't pass in a parent folder |
| if parentFolder is None: |
| # if the repository doesn't require fileable objects to be filed |
| if self.getCapabilities()['Unfiling']: |
| # has not been implemented |
| # postUrl = self.getCollectionLink(UNFILED_COLL) |
| raise NotImplementedError |
| else: |
| # this repo requires fileable objects to be filed |
| raise InvalidArgumentException |
| else: |
| postUrl = parentFolder.getChildrenLink() |
| |
| # make sure a name is set |
| properties['cmis:name'] = name |
| |
| # hardcoding to cmis:document if it wasn't |
| # passed in via props |
| if not properties.has_key('cmis:objectTypeId'): |
| properties['cmis:objectTypeId'] = CmisId('cmis:document') |
| # and if it was passed in, making sure it is a CmisId |
| elif not isinstance(properties['cmis:objectTypeId'], CmisId): |
| properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) |
| |
| # build the Atom entry |
| xmlDoc = getEntryXmlDoc(self, None, properties, contentFile, |
| contentType, contentEncoding) |
| |
| # post the Atom entry |
| result = self._cmisClient.binding.post(postUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| xmlDoc.toxml(encoding='utf-8'), |
| ATOM_XML_ENTRY_TYPE) |
| |
| # what comes back is the XML for the new document, |
| # so use it to instantiate a new document |
| # then return it |
| return AtomPubDocument(self._cmisClient, self, xmlDoc=result) |
| |
| def createDocumentFromSource(self, |
| sourceId, |
| properties={}, |
| parentFolder=None): |
| """ |
| This is not yet implemented. |
| |
| The following optional arguments are not yet supported: |
| - versioningState |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| # TODO: To be implemented |
| raise NotImplementedError |
| |
| def createFolder(self, |
| parentFolder, |
| name, |
| properties={}): |
| |
| """ |
| Creates a new :class:`Folder` object in the specified parentFolder. |
| |
| >>> root = repo.getRootFolder() |
| >>> folder = repo.createFolder(root, 'someFolder2') |
| >>> folder.getTitle() |
| u'someFolder2' |
| >>> folder.getObjectId() |
| u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' |
| |
| The following optional arguments are not yet supported: |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| |
| return parentFolder.createFolder(name, properties) |
| |
| def createRelationship(self, sourceObj, targetObj, relType): |
| """ |
| Creates a relationship of the specific type between a source object |
| and a target object and returns the new :class:`Relationship` object. |
| |
| The following optional arguments are not currently supported: |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| return sourceObj.createRelationship(targetObj, relType) |
| |
| def createPolicy(self, properties): |
| """ |
| This has not yet been implemented. |
| |
| The following optional arguments are not currently supported: |
| - folderId |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| # TODO: To be implemented |
| raise NotImplementedError |
| |
| def getUriTemplates(self): |
| |
| """ |
| Returns a list of the URI templates the repository service knows about. |
| |
| >>> templates = repo.getUriTemplates() |
| >>> templates['typebyid']['mediaType'] |
| u'application/atom+xml;type=entry' |
| >>> templates['typebyid']['template'] |
| u'http://localhost:8080/alfresco/s/cmis/type/{id}' |
| """ |
| |
| if self._uriTemplates == {}: |
| |
| if self.xmlDoc is 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.atompub.atompub_binding 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.atompub.atompub_binding import CHECKED_OUT_COLL |
| >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) |
| >>> len(resultSet.getResults()) |
| 1 |
| """ |
| |
| if collectionType == QUERY_COLL: |
| raise NotSupportedException |
| elif collectionType == TYPES_COLL: |
| return self.getTypeDefinitions() |
| |
| result = self._cmisClient.binding.get(self.getCollectionLink(collectionType).encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # return the result set |
| return AtomPubResultSet(self._cmisClient, self, result) |
| |
| def getCollectionLink(self, collectionType): |
| |
| """ |
| Returns the link HREF from the specified collectionType |
| ('checkedout', for example). |
| |
| >>> from cmislib.atompub.atompub_binding 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") |
| # CMIS-703 |
| # cdataSection = cmisXmlDoc.createCDATASection(query) |
| # statementElement.appendChild(cdataSection) |
| textNode = cmisXmlDoc.createTextNode(query) |
| statementElement.appendChild(textNode) |
| 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.atompub.binding.AtomPubResultSet') |
| self.logger.debug('Creating an instance of AtomPubResultSet') |
| |
| def __iter__(self): |
| """ Iterator for the result set """ |
| return iter(self.getResults()) |
| |
| def __getitem__(self, index): |
| """ Getter for the result set """ |
| return self.getResults()[index] |
| |
| def __len__(self): |
| """ Len method for the result set """ |
| return len(self.getResults()) |
| |
| def _getLink(self, rel): |
| """ |
| Returns the link found in the feed's XML for the specified rel. |
| """ |
| linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') |
| |
| for linkElement in linkElements: |
| |
| if linkElement.attributes.has_key('rel'): |
| relAttr = linkElement.attributes['rel'].value |
| |
| if relAttr == rel: |
| return linkElement.attributes['href'].value |
| |
| def _getPageResults(self, rel): |
| """ |
| Given a specified rel, does a get using that link (if one exists) |
| and then converts the resulting XML into a dictionary of |
| :class:`CmisObject` objects or its appropriate sub-type. |
| |
| The results are kept around to facilitate repeated calls without moving |
| the cursor. |
| """ |
| link = self._getLink(rel) |
| if link: |
| result = self._cmisClient.binding.get(link.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password) |
| |
| # return the result |
| self._xmlDoc = result |
| self._results = [] |
| return self.getResults() |
| |
| def reload(self): |
| |
| """ |
| Re-invokes the self link for the current set of results. |
| |
| >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) |
| >>> resultSet.reload() |
| |
| """ |
| |
| self.logger.debug('Reload called on result set') |
| self._getPageResults(SELF_REL) |
| |
| def getResults(self): |
| |
| """ |
| Returns the results that were fetched and cached by the get*Page call. |
| |
| >>> resultSet = repo.getCheckedOutDocs() |
| >>> resultSet.hasNext() |
| False |
| >>> for result in resultSet.getResults(): |
| ... result |
| ... |
| <cmislib.model.Document object at 0x104851810> |
| """ |
| if self._results: |
| return self._results |
| |
| if self._xmlDoc: |
| entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') |
| entries = [] |
| for entryElement in entryElements: |
| cmisObject = getSpecializedObject(AtomPubCmisObject(self._cmisClient, |
| self._repository, |
| xmlDoc=entryElement)) |
| entries.append(cmisObject) |
| |
| self._results = entries |
| |
| return self._results |
| |
| def hasObject(self, objectId): |
| |
| """ |
| Returns True if the specified objectId is found in the list of results, |
| otherwise returns False. |
| """ |
| |
| for obj in self.getResults(): |
| if obj.id == objectId: |
| return True |
| return False |
| |
| def getFirst(self): |
| |
| """ |
| Returns the first page of results as a dictionary of |
| :class:`CmisObject` objects or its appropriate sub-type. This only |
| works when the server returns a "first" link. Not all of them do. |
| |
| >>> resultSet.hasFirst() |
| True |
| >>> results = resultSet.getFirst() |
| >>> for result in results: |
| ... result |
| ... |
| <cmislib.model.Document object at 0x10480bc90> |
| """ |
| |
| return self._getPageResults(FIRST_REL) |
| |
| def getPrev(self): |
| |
| """ |
| Returns the prev page of results as a dictionary of |
| :class:`CmisObject` objects or its appropriate sub-type. This only |
| works when the server returns a "prev" link. Not all of them do. |
| >>> resultSet.hasPrev() |
| True |
| >>> results = resultSet.getPrev() |
| >>> for result in results: |
| ... result |
| ... |
| <cmislib.model.Document object at 0x10480bc90> |
| """ |
| |
| return self._getPageResults(PREV_REL) |
| |
| def getNext(self): |
| |
| """ |
| Returns the next page of results as a dictionary of |
| :class:`CmisObject` objects or its appropriate sub-type. |
| >>> resultSet.hasNext() |
| True |
| >>> results = resultSet.getNext() |
| >>> for result in results: |
| ... result |
| ... |
| <cmislib.model.Document object at 0x10480bc90> |
| """ |
| |
| return self._getPageResults(NEXT_REL) |
| |
| def getLast(self): |
| |
| """ |
| Returns the last page of results as a dictionary of |
| :class:`CmisObject` objects or its appropriate sub-type. This only |
| works when the server is returning a "last" link. Not all of them do. |
| |
| >>> resultSet.hasLast() |
| True |
| >>> results = resultSet.getLast() |
| >>> for result in results: |
| ... result |
| ... |
| <cmislib.model.Document object at 0x10480bc90> |
| """ |
| |
| return self._getPageResults(LAST_REL) |
| |
| def hasNext(self): |
| |
| """ |
| Returns True if this page contains a next link. |
| |
| >>> resultSet.hasNext() |
| True |
| """ |
| |
| if self._getLink(NEXT_REL): |
| return True |
| else: |
| return False |
| |
| def hasPrev(self): |
| |
| """ |
| Returns True if this page contains a prev link. Not all CMIS providers |
| implement prev links consistently. |
| |
| >>> resultSet.hasPrev() |
| True |
| """ |
| |
| if self._getLink(PREV_REL): |
| return True |
| else: |
| return False |
| |
| def hasFirst(self): |
| |
| """ |
| Returns True if this page contains a first link. Not all CMIS providers |
| implement first links consistently. |
| |
| >>> resultSet.hasFirst() |
| True |
| """ |
| |
| if self._getLink(FIRST_REL): |
| return True |
| else: |
| return False |
| |
| def hasLast(self): |
| |
| """ |
| Returns True if this page contains a last link. Not all CMIS providers |
| implement last links consistently. |
| |
| >>> resultSet.hasLast() |
| True |
| """ |
| |
| if self._getLink(LAST_REL): |
| return True |
| else: |
| return False |
| |
| |
| class AtomPubDocument(AtomPubCmisObject): |
| |
| """ |
| An object typically associated with file content. |
| """ |
| |
| def checkout(self): |
| |
| """ |
| Performs a checkout on the :class:`Document` and returns the |
| Private Working Copy (PWC), which is also an instance of |
| :class:`Document` |
| |
| >>> doc.getObjectId() |
| u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0' |
| >>> doc.isCheckedOut() |
| False |
| >>> pwc = doc.checkout() |
| >>> doc.isCheckedOut() |
| True |
| """ |
| |
| # get the checkedout collection URL |
| checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL) |
| assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url." |
| |
| # get this document's object ID |
| # build entry XML with it |
| properties = {'cmis:objectId': self.getObjectId()} |
| entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties) |
| |
| # post it to to the checkedout collection URL |
| result = self._cmisClient.binding.post(checkoutUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| entryXmlDoc.toxml(encoding='utf-8'), |
| ATOM_XML_ENTRY_TYPE) |
| |
| # now that the doc is checked out, we need to refresh the XML |
| # to pick up the prop updates related to a checkout |
| self.reload() |
| |
| return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result) |
| |
| def cancelCheckout(self): |
| """ |
| Cancels the checkout of this object by retrieving the Private Working |
| Copy (PWC) and then deleting it. After the PWC is deleted, this object |
| will be reloaded to update properties related to a checkout. |
| |
| >>> doc.isCheckedOut() |
| True |
| >>> doc.cancelCheckout() |
| >>> doc.isCheckedOut() |
| False |
| """ |
| |
| pwcDoc = self.getPrivateWorkingCopy() |
| if pwcDoc: |
| pwcDoc.delete() |
| self.reload() |
| |
| def getPrivateWorkingCopy(self): |
| |
| """ |
| Retrieves the object using the object ID in the property: |
| cmis:versionSeriesCheckedOutId then uses getObject to instantiate |
| the object. |
| |
| >>> doc.isCheckedOut() |
| False |
| >>> doc.checkout() |
| <cmislib.model.Document object at 0x103a25ad0> |
| >>> pwc = doc.getPrivateWorkingCopy() |
| >>> pwc.getTitle() |
| u'sample-b (Working Copy).pdf' |
| """ |
| |
| # reloading the document just to make sure we've got the latest |
| # and greatest PWC ID |
| self.reload() |
| pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId'] |
| if pwcDocId: |
| return self._repository.getObject(pwcDocId) |
| |
| def isCheckedOut(self): |
| |
| """ |
| Returns true if the document is checked out. |
| |
| >>> doc.isCheckedOut() |
| True |
| >>> doc.cancelCheckout() |
| >>> doc.isCheckedOut() |
| False |
| """ |
| |
| # reloading the document just to make sure we've got the latest |
| # and greatest checked out prop |
| self.reload() |
| return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut']) |
| |
| def getCheckedOutBy(self): |
| |
| """ |
| Returns the ID who currently has the document checked out. |
| >>> pwc = doc.checkout() |
| >>> pwc.getCheckedOutBy() |
| u'admin' |
| """ |
| |
| # reloading the document just to make sure we've got the latest |
| # and greatest checked out prop |
| self.reload() |
| return self.getProperties()['cmis:versionSeriesCheckedOutBy'] |
| |
| def checkin(self, checkinComment=None, contentFile=None, contentType=None, |
| properties=None, **kwargs): |
| |
| """ |
| Checks in this :class:`Document` which must be a private |
| working copy (PWC). |
| |
| >>> doc.isCheckedOut() |
| False |
| >>> pwc = doc.checkout() |
| >>> doc.isCheckedOut() |
| True |
| >>> pwc.checkin() |
| <cmislib.model.Document object at 0x103a8ae90> |
| >>> doc.isCheckedOut() |
| False |
| |
| The following optional arguments are NOT supported: |
| - policies |
| - addACEs |
| - removeACEs |
| """ |
| |
| # major = true is supposed to be the default but inmemory 0.9 is throwing an error 500 without it |
| if not kwargs.has_key('major'): |
| kwargs['major'] = 'true' |
| |
| # Add checkin to kwargs and checkinComment, if it exists |
| kwargs['checkin'] = 'true' |
| kwargs['checkinComment'] = checkinComment |
| |
| if not properties and not contentFile: |
| # Build an empty ATOM entry |
| entryXmlDoc = getEmptyXmlDoc() |
| else: |
| # 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 |
| entryXmlDoc = getEntryXmlDoc( |
| self._repository, objectTypeId, properties, contentFile, contentType) |
| |
| # Get the self link |
| # Do a PUT of the empty ATOM to the self link |
| url = self._getSelfLink() |
| result = self._cmisClient.binding.put(url.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| entryXmlDoc.toxml(encoding='utf-8'), |
| ATOM_XML_TYPE, |
| **kwargs) |
| |
| return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result) |
| |
| def getLatestVersion(self, **kwargs): |
| |
| """ |
| Returns a :class:`Document` object representing the latest version in |
| the version series. |
| |
| The following optional arguments are supported: |
| - major |
| - filter |
| - includeRelationships |
| - includePolicyIds |
| - renditionFilter |
| - includeACL |
| - includeAllowableActions |
| |
| >>> latestDoc = doc.getLatestVersion() |
| >>> latestDoc.getProperties()['cmis:versionLabel'] |
| u'2.1' |
| >>> latestDoc = doc.getLatestVersion(major='false') |
| >>> latestDoc.getProperties()['cmis:versionLabel'] |
| u'2.1' |
| >>> latestDoc = doc.getLatestVersion(major='true') |
| >>> latestDoc.getProperties()['cmis:versionLabel'] |
| u'2.0' |
| """ |
| |
| doc = None |
| if kwargs.has_key('major') and kwargs['major'] == 'true': |
| doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor') |
| else: |
| doc = self._repository.getObject(self.getObjectId(), returnVersion='latest') |
| |
| return doc |
| |
| def getPropertiesOfLatestVersion(self, **kwargs): |
| |
| """ |
| Like :class:`^CmisObject.getProperties`, returns a dict of properties |
| from the latest version of this object in the version series. |
| |
| The optional major and filter arguments are supported. |
| """ |
| |
| latestDoc = self.getLatestVersion(**kwargs) |
| |
| return latestDoc.getProperties() |
| |
| def getAllVersions(self, **kwargs): |
| |
| """ |
| Returns a :class:`ResultSet` of document objects for the entire |
| version history of this object, including any PWC's. |
| |
| The optional filter and includeAllowableActions are |
| supported. |
| """ |
| |
| # get the version history link |
| versionsUrl = self._getLink(VERSION_HISTORY_REL) |
| |
| # invoke the URL |
| result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # 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') |
| |
| # CMIS-701 |
| if len(contentElements) != 1: |
| self.reload() |
| 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, content = Rest().get(srcUrl.encode('utf-8'), |
| username=self._cmisClient.username, |
| password=self._cmisClient.password, |
| **self._cmisClient.extArgs) |
| if result['status'] != '200': |
| raise CmisException(result['status']) |
| return StringIO.StringIO(content) |
| 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'] is not 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) |
| |
| # 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'] is not None: |
| self.logger.debug('Change token present, adding it to args') |
| args = {"changeToken": self.properties['cmis:changeToken']} |
| |
| # delete the content stream |
| self._cmisClient.binding.delete(srcUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **args) |
| |
| 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 is 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) |
| |
| 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) |
| |
| # 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) |
| |
| # 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) |
| |
| # 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 is not None, 'Unable to determine folder tree link' |
| result = self._cmisClient.binding.get(url.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password, |
| **kwargs) |
| |
| # return the result set |
| return AtomPubResultSet(self._cmisClient, self._repository, result) |
| |
| def 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) |
| |
| # 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) |
| |
| 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 |
| 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) |
| |
| 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 |
| 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) |
| |
| 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 is 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 is 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.atompub.binding.AtomPubObjectType') |
| self.logger.debug('Creating an instance of AtomPubObjectType') |
| |
| def __str__(self): |
| """To string""" |
| return self.getTypeId() |
| |
| def getTypeId(self): |
| |
| """ |
| Returns the type ID for this object. |
| |
| >>> docType = repo.getTypeDefinition('cmis:document') |
| >>> docType.getTypeId() |
| 'cmis:document' |
| """ |
| |
| if self._typeId is None: |
| if self.xmlDoc is 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 is 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.atompub.atompub_binding 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 is 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) |
| |
| # 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.atompub.binding.AtomPubProperty') |
| self.logger.debug('Creating an instance of AtomPubProperty') |
| |
| 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 dict of :class:`ACE` objects keyed to the |
| principalId or the XML representation of the ACL. |
| """ |
| |
| if aceList: |
| self._entries = aceList |
| else: |
| self._entries = None |
| if xmlDoc: |
| self._xmlDoc = xmlDoc |
| self._entries = self._getEntriesFromXml() |
| else: |
| self._xmlDoc = None |
| |
| self.logger = logging.getLogger('cmislib.atompub.binding.AtomPubACL') |
| self.logger.debug('Creating an instance of AtomPubACL') |
| |
| def addEntry(self, principalId, access, direct=True): |
| |
| """ |
| Adds an :class:`ACE` entry to the ACL. |
| |
| The default for direct is True but you can override it if needed. |
| |
| >>> acl = folder.getACL() |
| >>> acl.addEntry('jpotts', 'cmis:read') |
| >>> acl.addEntry('jsmith', 'cmis:write') |
| >>> 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) |
| if not self._entries: |
| self._entries = {ace.principalId: ace} |
| else: |
| if self._entries.has_key(principalId): |
| if access not in self._entries[principalId].permissions: |
| perms = self._entries[principalId].permissions |
| perms.append(access) |
| self.removeEntry(principalId) |
| if not self._entries: |
| self._entries = {principalId: AtomPubACE(principalId, perms, direct)} |
| else: |
| self._entries[principalId] = AtomPubACE(principalId, perms, direct) |
| else: |
| self._entries[ace.principalId] = ace |
| |
| def removeEntry(self, principalId): |
| |
| """ |
| Removes the :class:`ACE` entry given a specific principalId. If a given |
| principalId has more than one permission, calling removeEntry will |
| remove the entry completely. |
| |
| >>> 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] |
| if len(self._entries) == 0: |
| self.clearEntries() |
| |
| 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')) |
| >>> acl.addEntry(ACE('jpotts', 'cmis:write')) |
| >>> 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 = None |
| 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')) |
| >>> acl.addEntry(ACE('jpotts', 'cmis:write')) |
| >>> 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] |
| direct = None |
| if dirEl and dirEl.childNodes: |
| direct = parseBoolValue(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. |
| """ |
| |
| xmlDoc = minidom.Document() |
| aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl') |
| aclEl.setAttribute('xmlns:cmis', CMIS_NS) |
| if self.getEntries(): |
| for ace in self.getEntries().values(): |
| # only want direct permissions |
| if ace.direct: |
| 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) |
| # permissions |
| for perm in ace.permissions: |
| permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') |
| permItemElText = xmlDoc.createTextNode(perm) |
| permItemEl.appendChild(permItemElText) |
| permEl.appendChild(permItemEl) |
| directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct') |
| directElText = xmlDoc.createTextNode(toCMISValue(ace.direct)) |
| directEl.appendChild(directElText) |
| permEl.appendChild(directEl) |
| aclEl.appendChild(permEl) |
| else: |
| permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') |
| aclEl.appendChild(permEl) |
| xmlDoc.appendChild(aclEl) |
| return xmlDoc |
| |
| entries = property(getEntries) |
| |
| |
| class AtomPubACE(ACE): |
| |
| """ |
| Represents an ACE for the AtomPub binding. |
| """ |
| |
| pass |
| |
| |
| 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.atompub.binding.AtomPubChangeEntry') |
| self.logger.debug('Creating an instance of AtomPubChangeEntry') |
| |
| def getId(self): |
| """ |
| Returns the unique ID of the change entry. |
| """ |
| if self._changeEntryId is 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 is 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 is 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(aceList=aclEls[0]) |
| elif aclUrl: |
| result = self._cmisClient.binding.get(aclUrl.encode('utf-8'), |
| self._cmisClient.username, |
| self._cmisClient.password) |
| return AtomPubACL(xmlDoc=result) |
| |
| def getChangeTime(self): |
| |
| """ |
| Returns a datetime object representing the time the change occurred. |
| """ |
| |
| if self._changeTime is 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.atompub.binding.AtomPubRendition') |
| self.logger.debug('Creating an instance of AtomPubRendition') |
| |
| 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 is None or (type(propValue) == list and propValue[0] is None): |
| # grab the prop type from the typeDef |
| if typeDef is 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 is 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 is not None: |
| propValueStrList.append(val.isoformat()) |
| else: |
| propValueStrList.append(val) |
| else: |
| if propValue is not 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 is not None: |
| propValueStrList.append(unicode(val).lower()) |
| else: |
| propValueStrList.append(val) |
| else: |
| if propValue is not 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 is not None: |
| propValueStrList.append(unicode(val)) |
| else: |
| propValueStrList.append(val) |
| else: |
| if propValue is not 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 is not None: |
| propValueStrList.append(unicode(val)) |
| else: |
| propValueStrList.append(val) |
| else: |
| if propValue is not None: |
| propValueStrList = [unicode(propValue)] |
| else: |
| propValueStrList = [propValue] |
| else: |
| propElementName = 'cmis:propertyString' |
| if isList: |
| propValueStrList = [] |
| for val in propValue: |
| if val is not None: |
| propValueStrList.append(unicode(val)) |
| else: |
| propValueStrList.append(val) |
| else: |
| if propValue is not 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 |