diff --git a/PKG-INFO b/PKG-INFO index 5696c19..55b1aad 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,12 +1,13 @@ Metadata-Version: 1.1 -Name: cmislib -Version: 0.5.1 +Name: cmislib-maykin +Version: 0.7.4 Summary: Apache Chemistry CMIS client library for Python Home-page: http://chemistry.apache.org/ Author: Apache Chemistry Project Author-email: dev@chemistry.apache.org License: Apache License (2.0) Description: ABOUT + ----- Thanks for using cmislib, the CMIS client library for Python. @@ -17,22 +18,32 @@ Description: ABOUT More info on CMIS can be found at: http://www.oasis-open.org/committees/cmis + This is a fork from https://github.com/apache/chemistry-cmislib and update to work with + Python 2.7 and 3.5. + SOURCE + ------ The source code for this project lives at http://chemistry.apache.org/ TESTS + ----- There are unit tests available in the tests directory. They require access to a CMIS provider. There are many freely-available CMIS repositories available to run locally or that are hosted. DOC + --- Documentation that tells you what this is all about can be found in the doc directory. Please see the doc for dependencies, required CMIS version level, required Python version, etc. + NOTES: + ------ + + This is a working version, and has been tested on Python 3.5 and Alfresco Community - 5.1.0 (r127059-b7) Platform: UNKNOWN Classifier: Development Status :: 4 - Beta diff --git a/README.txt b/README.md similarity index 76% rename from README.txt rename to README.md index abec83f..74894a9 100644 --- a/README.txt +++ b/README.md @@ -1,4 +1,5 @@ ABOUT +----- Thanks for using cmislib, the CMIS client library for Python. @@ -9,19 +10,29 @@ EMC Documentum, and any other content repository that is CMIS-compliant. More info on CMIS can be found at: http://www.oasis-open.org/committees/cmis +This is a fork from https://github.com/apache/chemistry-cmislib and update to work with +Python 2.7 and 3.5. + SOURCE +------ The source code for this project lives at http://chemistry.apache.org/ TESTS +----- There are unit tests available in the tests directory. They require access to a CMIS provider. There are many freely-available CMIS repositories available to run locally or that are hosted. DOC +--- Documentation that tells you what this is all about can be found in the doc directory. Please see the doc for dependencies, required CMIS version level, required Python version, etc. +NOTES: +------ + +This is a working version, and has been tested on Python 3.5 and Alfresco Community - 5.1.0 (r127059-b7) diff --git a/setup.cfg b/setup.cfg index 861a9f5..4a3e07f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,14 @@ +[isort] +combine_as_imports = true +default_section = THIRDPARTY +include_trailing_comma = false +line_length = 79 +multi_line_output = 5 +not_skip = __init__.py +known_first_party = cmslib +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER + [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff --git a/setup.py b/setup.py index 7d16d74..5fc7a81 100644 --- a/setup.py +++ b/setup.py @@ -19,33 +19,40 @@ import os from setuptools import setup, find_packages -version = '0.5.1' +version = '0.7.4' + +root_dir = os.path.dirname(os.path.abspath(__file__)) + def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() + with open(os.path.join(root_dir, fname)) as infile: + return infile.read() + setup( - name = "cmislib", - description = 'Apache Chemistry CMIS client library for Python', - version = version, - install_requires = [ - 'iso8601' - ], - author = 'Apache Chemistry Project', - author_email = 'dev@chemistry.apache.org', - license = 'Apache License (2.0)', - url = 'http://chemistry.apache.org/', - package_dir = {'':'src'}, - packages = find_packages('src', exclude=['tests']), - #include_package_data = True, - exclude_package_data = {'':['tests']}, - long_description = read('README.txt'), - classifiers = [ + name="cmislib-maykin", + description='Apache Chemistry CMIS client library for Python', + version=version, + install_requires=[ + 'iso8601', + 'httplib2', + 'six' + ], + author='Apache Chemistry Project', + author_email='dev@chemistry.apache.org', + license='Apache License (2.0)', + url='http://chemistry.apache.org/', + package_dir={'': 'src'}, + packages=find_packages('src', exclude=['tests']), + # include_package_data=True, + exclude_package_data={'': ['tests']}, + long_description=read('README.md'), + classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries", - ], + ], ) diff --git a/src/cmislib/__init__.py b/src/cmislib/__init__.py index 553b333..f865650 100644 --- a/src/cmislib/__init__.py +++ b/src/cmislib/__init__.py @@ -20,6 +20,8 @@ Define package contents so that they are easy to import. """ -from model import CmisClient, Repository, Folder +from cmislib.model import CmisClient +from cmislib.domain import Repository, Folder +from cmislib.cmis_services import Binding, RepositoryServiceIfc -__all__ = ["CmisClient", "Repository", "Folder"] +__all__ = ["Binding", "CmisClient", "RepositoryServiceIfc", "Repository", "Folder"] diff --git a/src/cmislib/atompub/__init__.py b/src/cmislib/atompub/__init__.py new file mode 100644 index 0000000..6e30c70 --- /dev/null +++ b/src/cmislib/atompub/__init__.py @@ -0,0 +1 @@ +__author__ = 'jpotts' diff --git a/src/cmislib/atompub/binding.py b/src/cmislib/atompub/binding.py new file mode 100644 index 0000000..b1412bf --- /dev/null +++ b/src/cmislib/atompub/binding.py @@ -0,0 +1,4201 @@ +# +# 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. +""" +import base64 +import datetime +import logging +import mimetypes +import re +from io import BytesIO +from xml.dom import minidom +from xml.parsers.expat import ExpatError + +import six +from six.moves.urllib.parse import quote, urlparse, urlunparse + +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 + +if six.PY3: + import io as StringIO +else: + import StringIO + +logger = logging.getLogger(__name__) +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' +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': + logger.error("Error content: %r", content) + 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': + logger.error("Error content: %r", content) + 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: + logger.error("Error payload: %r", payload) + logger.error("Error content: %r", content) + 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': + logger.error("Error payload: %r", payload) + logger.error("Error content: %r", content) + 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, + 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.info('Creating an instance of CmisObject') + + def __str__(self): + """To string""" + return self.getObjectId() + + def reload(self, **kwargs): + + """ + Fetches the latest representation of this object from the CMIS service. + Some methods, like :class:`^Document.checkout` do this for you. + + If you call reload with a properties filter, the filter will be in + effect on subsequent calls until the filter argument is changed. To + reset to the full list of properties, call reload with filter set to + '*'. + """ + + self.logger.debug('Reload called on CmisObject') + if kwargs: + if self._kwargs: + self._kwargs.update(kwargs) + else: + self._kwargs = kwargs + + templates = self._repository.getUriTemplates() + template = templates['objectbyid']['template'] + + # Doing some refactoring here. Originally, we snagged the template + # and then "filled in" the template based on the args passed in. + # However, some servers don't provide a full template which meant + # supported optional args wouldn't get passed in using the fill-the- + # template approach. What's going on now is that the template gets + # filled in where it can, but if additional, non-templated args are + # passed in, those will get tacked on to the query string as + # "additional" options. + + params = {'{id}': self.getObjectId(), + '{filter}': '', + '{includeAllowableActions}': 'false', + '{includePolicyIds}': 'false', + '{includeRelationships}': '', + '{includeACL}': 'false', + '{renditionFilter}': ''} + + options = {} + addOptions = {} # args specified, but not in the template + for k, v in self._kwargs.items(): + pKey = "{" + k + "}" + if template.find(pKey) >= 0: + options[pKey] = toCMISValue(v) + else: + addOptions[k] = toCMISValue(v) + + # merge the templated args with the default params + params.update(options) + + # fill in the template + byObjectIdUrl = multiple_replace(params, template) + + self.xmlDoc = self._cmisClient.binding.get(byObjectIdUrl, + 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 'returnVersion' in options or 'returnVersion' in addOptions: + 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, + 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 'rel' in linkElement.attributes: + 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: + propertyValue.append(parsePropValue(valNode. + childNodes[0].data, + node.localName)) + else: + propertyValue = None + self._properties[propertyName] = propertyValue + + for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]: + propertyName = node.nodeName + if node.childNodes: + propertyValue = node.firstChild.nodeValue + else: + propertyValue = None + self._properties[propertyName] = propertyValue + + return self._properties + + def getName(self): + + """ + Returns the value of cmis:name from the getProperties() dictionary. + We don't need a getter for every standard CMIS property, but name + is a pretty common one so it seems to make sense. + + >>> doc.getName() + u'system-overview.html' + """ + + if self._name 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) + + >>> 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 'cmis:changeToken' in self.properties 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 'cmis:objectTypeId' in self.properties and 'cmis:objectTypeId' not in properties: + 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, + 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, + 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, + 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, + 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, + 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': , 'jdoe': } + + 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, + 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': , 'jdoe': } + """ + + 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, + 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 'rel' in linkElement.attributes.keys(): + relAttr = linkElement.attributes['rel'].value + + if ltype and 'type' in linkElement.attributes.keys(): + typeAttr = linkElement.attributes['type'].value + + if relAttr == rel and ltype.match(typeAttr): + return linkElement.attributes['href'].value + else: + if 'rel' in linkElement.attributes.keys(): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + allowableActions = property(getAllowableActions) + name = property(getName) + id = property(getObjectId) + properties = property(getProperties) + title = property(getTitle) + ACL = property(getACL) + + +class AtomPubRepository(object): + + """ + Represents a CMIS repository. Will lazily populate itself by + calling the repository CMIS service URL. + + You must pass in an instance of a CmisClient when creating an + instance of this class. + """ + + def __init__(self, cmisClient, xmlDoc=None): + """ Constructor """ + self._cmisClient = cmisClient + self.xmlDoc = xmlDoc + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._uriTemplates = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + self.logger = logging.getLogger('cmislib.model.Repository') + self.logger.info('Creating an instance of Repository') + + def __str__(self): + """To string""" + return self.getRepositoryName() + + def reload(self): + """ + This method will re-fetch the repository's XML data from the CMIS + repository. + """ + self.logger.debug('Reload called on object') + self.xmlDoc = self._cmisClient.binding.get(self._cmisClient.repositoryUrl, + 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() + self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data + return self._repositoryName + + def getRepositoryInfo(self): + + """ + Returns a dict of repository information. + + >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() + u'Main Repository' + >>> info = repo.getRepositoryInfo() + >>> for k,v in info.items(): + ... print "%s:%s" % (k,v) + ... + cmisSpecificationTitle:Version 1.0 Committee Draft 04 + cmisVersionSupported:1.0 + repositoryDescription:None + productVersion:3.2.0 (r2 2440) + rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 + repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 + repositoryName:Main Repository + vendorName:Alfresco + productName:Alfresco Repository (Community) + """ + + if not self._repositoryInfo: + if self.xmlDoc 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, + 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, + 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 'rel' in linkElement.attributes.keys(): + 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}': 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, + 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, + 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, + 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') + + """ + + # 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) + + >>> 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 'cmis:objectTypeId' not in properties.keys(): + 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, + 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, + parentFolder, + properties={}): + """ + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + postUrl = parentFolder.getChildrenLink() + + # build the Atom entry + xmlDoc = getEntryXmlDoc(self, None, properties, sourceId=sourceId) + + # post the Atom entry + result = self._cmisClient.binding.post(postUrl, + 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 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), + 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.model.ResultSet') + self.logger.info('Creating an instance of ResultSet') + + def __iter__(self): + """ Iterator for the result set """ + return iter(self.getResults()) + + def __getitem__(self, index): + """ Getter for the result set """ + return self.getResults()[index] + + def __len__(self): + """ Len method for the result set """ + return len(self.getResults()) + + def _getLink(self, rel): + """ + Returns the link found in the feed's XML for the specified rel. + """ + linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + + if 'rel' in linkElement.attributes.keys(): + 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, + 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 + ... + + """ + 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 + ... + + """ + + 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 + ... + + """ + + 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 + ... + + """ + + 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 + ... + + """ + + 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, + 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() + + >>> pwc = doc.getPrivateWorkingCopy() + >>> pwc.getTitle() + u'sample-b (Working Copy).pdf' + """ + + # reloading the document just to make sure we've got the latest + # and greatest PWC ID + self.reload() + pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId'] + if pwcDocId: + return self._repository.getObject(pwcDocId) + + def isCheckedOut(self): + + """ + Returns true if the document is checked out. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + # reloading the document just to make sure we've got the latest + # and greatest checked out prop + self.reload() + return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut']) + + def getCheckedOutBy(self): + + """ + Returns the ID who currently has the document checked out. + >>> pwc = doc.checkout() + >>> pwc.getCheckedOutBy() + u'admin' + """ + + # reloading the document just to make sure we've got the latest + # and greatest checked out prop + self.reload() + return self.getProperties()['cmis:versionSeriesCheckedOutBy'] + + def checkin(self, checkinComment=None, **kwargs): + + """ + Checks in this :class:`Document` which must be a private + working copy (PWC). + + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + >>> pwc.checkin() + + >>> doc.isCheckedOut() + False + + The following optional arguments are supported: + - major + - properties + - contentStream + - policies + - addACEs + - removeACEs + """ + + # major = true is supposed to be the default but inmemory 0.9 is throwing an error 500 without it + if 'major' not in kwargs.keys(): + kwargs['major'] = 'true' + + # Add checkin to kwargs and checkinComment, if it exists + kwargs['checkin'] = 'true' + kwargs['checkinComment'] = checkinComment + + # Build an empty ATOM entry + entryXmlDoc = getEmptyXmlDoc() + + # Get the self link + # Do a PUT of the empty ATOM to the self link + url = self._getSelfLink() + result = self._cmisClient.binding.put(url, + 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 'major' in kwargs.keys() 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, + 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 'src' in contentElements[0].attributes.keys(): + srcUrl = contentElements[0].attributes['src'].value + + # the cmis client class parses non-error responses + result, content = Rest().get(srcUrl, + username=self._cmisClient.username, + password=self._cmisClient.password, + **self._cmisClient.extArgs) + if result['status'] != '200': + raise CmisException(result['status']) + + # StringIO return a in-memeory stream for text, not bytes + return BytesIO(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 'src' in contentElements[0].attributes.keys(): + 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 'cmis:changeToken' in self.properties.keys() 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, + 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 + # NOTE: this is a lie. Apprently, nothing is returned + 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 'src' in contentElements[0].attributes.keys(): + 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 'cmis:changeToken' in self.properties.keys() 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, + 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, + 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 + """ + name = name.split('/')[-1] + # 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 'cmis:objectTypeId' not in properties.keys(): + 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, + 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='UTF-8'): + + """ + 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) + + >>> 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) + + >>> 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 + """ + name = name.split('/')[-1] + 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, + 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, + 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, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return AtomPubResultSet(self._cmisClient, self, result) + + def getParent(self): + + """ + This is not yet implemented. + + The optional filter argument is not yet supported. + """ + # get the appropriate 'up' link + parentUrl = self._getLink(UP_REL) + # invoke the URL + result = self._cmisClient.binding.get(parentUrl, + 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, + 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, + 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, + 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.model.ObjectType') + self.logger.info('Creating an instance of ObjectType') + + def __str__(self): + """To string""" + return self.getTypeId() + + def getTypeId(self): + + """ + Returns the type ID for this object. + + >>> docType = repo.getTypeDefinition('cmis:document') + >>> docType.getTypeId() + 'cmis:document' + """ + + if self._typeId 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 'rel' in linkElement.attributes.keys() and 'type' in linkElement.attributes.keys(): + 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, + 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.model.Property') + self.logger.info('Creating an instance of Property') + + def __str__(self): + """To string""" + return self.getId() + + def _getElementValue(self, namespace, elementName): + + """ + Utility method for retrieving element values from the object type XML. + """ + + els = self.xmlDoc.getElementsByTagNameNS(namespace, elementName) + if len(els) >= 1: + el = els[0] + if el and len(el.childNodes) >= 1: + return el.childNodes[0].data + + def getId(self): + """Getter for cmis:id""" + return self._getElementValue(CMIS_NS, 'id') + + def getLocalName(self): + """Getter for cmis:localName""" + return self._getElementValue(CMIS_NS, 'localName') + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + return self._getElementValue(CMIS_NS, 'localNamespace') + + def getDisplayName(self): + """Getter for cmis:displayName""" + return self._getElementValue(CMIS_NS, 'displayName') + + def getQueryName(self): + """Getter for cmis:queryName""" + return self._getElementValue(CMIS_NS, 'queryName') + + def getDescription(self): + """Getter for cmis:description""" + return self._getElementValue(CMIS_NS, 'description') + + def getPropertyType(self): + """Getter for cmis:propertyType""" + return self._getElementValue(CMIS_NS, 'propertyType') + + def getCardinality(self): + """Getter for cmis:cardinality""" + return self._getElementValue(CMIS_NS, 'cardinality') + + def getUpdatability(self): + """Getter for cmis:updatability""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'updatability')) + + def isInherited(self): + """Getter for cmis:inherited""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'inherited')) + + def isRequired(self): + """Getter for cmis:required""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'required')) + + def isQueryable(self): + """Getter for cmis:queryable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) + + def isOrderable(self): + """Getter for cmis:orderable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'orderable')) + + def isOpenChoice(self): + """Getter for cmis:openChoice""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'openChoice')) + + id = property(getId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + propertyType = property(getPropertyType) + cardinality = property(getCardinality) + updatability = property(getUpdatability) + inherited = property(isInherited) + required = property(isRequired) + queryable = property(isQueryable) + orderable = property(isOrderable) + openChoice = property(isOpenChoice) + + +class AtomPubACL(ACL): + + """ + Represents the Access Control List for an object. + """ + + def __init__(self, aceList=None, xmlDoc=None): + + """ + Constructor. Pass in either a 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.model.ACL') + self.logger.info('Creating an instance of ACL') + + 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': , u'jdoe': , 'jpotts': , 'jsmith': } + """ + ace = AtomPubACE(principalId, access, direct) + if not self._entries: + self._entries = {ace.principalId : ace} + else: + if principalId in self._entries.keys(): + 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': , u'jdoe': , 'jpotts': , 'jsmith': } + >>> acl.removeEntry('jsmith') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } + """ + + if principalId in self._entries.keys(): + 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': , 'jsmith': } + >>> acl.getXmlDoc() + + >>> 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.model.ChangeEntry') + self.logger.info('Creating an instance of ChangeEntry') + + 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, + 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 'rel' in linkElement.attributes.keys(): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + +class AtomPubChangeEntryResultSet(AtomPubResultSet): + + """ + A specialized type of :class:`ResultSet` that knows how to instantiate + :class:`ChangeEntry` objects. The parent class assumes children of + :class:`CmisObject` which doesn't work for ChangeEntries. + """ + + def __iter__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return iter(self.getResults()) + + def __getitem__(self, index): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return self.getResults()[index] + + def __len__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return len(self.getResults()) + + def getResults(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + if self._results: + return self._results + + if self._xmlDoc: + entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') + entries = [] + for entryElement in entryElements: + changeEntry = AtomPubChangeEntry(self._cmisClient, self._repository, entryElement) + entries.append(changeEntry) + + self._results = entries + + return self._results + + +class AtomPubRendition(Rendition): + + """ + This class represents a Rendition. + """ + + def __init__(self, propNode): + """Constructor""" + self.xmlDoc = propNode + self.logger = logging.getLogger('cmislib.model.Rendition') + self.logger.info('Creating an instance of Rendition') + + def __str__(self): + """To string""" + return self.getStreamId() + + def getStreamId(self): + """Getter for the rendition's stream ID""" + if 'streamId' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['streamId'].value + + def getMimeType(self): + """Getter for the rendition's mime type""" + if 'type' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['type'].value + + def getLength(self): + """Getter for the renditions's length""" + if 'length' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['length'].value + + def getTitle(self): + """Getter for the renditions's title""" + if 'title' in self.xmlDoc.attributes.keys(): + 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 'height' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['height'].value + + def getWidth(self): + """Getter for the renditions's width""" + if 'width' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['width'].value + + def getHref(self): + """Getter for the renditions's href""" + if 'href' in self.xmlDoc.attributes.keys(): + return self.xmlDoc.attributes['href'].value + + def getRenditionDocumentId(self): + """Getter for the renditions's width""" + if 'renditionDocumentId' in self.xmlDoc.attributes.keys(): + 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, sourceId=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. + readingContent = contentFile.read() + + if not isinstance(readingContent, bytes): + readingContent = bytes(readingContent, encoding='utf-8') + + content = base64.b64encode(readingContent) + + fileData = content.decode('utf-8') + 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 sourceId: + sourceElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:sourceId') + sourceElement.setAttribute('propertyDefinitionId', sourceId) + objectElement.appendChild(sourceElement) + + if properties: + # a name is required for most things, but not for a checkout + if 'cmis:name' in properties.keys(): + 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: + if objectTypeId is None: + objectTypeId = properties.get('cmis:objectTypeId') + 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(six.text_type(val).lower()) + else: + propValueStrList.append(val) + else: + if propValue is not None: + propValueStrList = [six.text_type(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(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue is not None: + propValueStrList = [six.text_type(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(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue is not None: + propValueStrList = [six.text_type(propValue)] + else: + propValueStrList = [propValue] + else: + propElementName = 'cmis:propertyString' + if isList: + propValueStrList = [] + for val in propValue: + if val is not None: + propValueStrList.append(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue is not None: + propValueStrList = [six.text_type(propValue)] + else: + propValueStrList = [propValue] + + return propElementName, propValueStrList + + +def getEmptyXmlDoc(): + """ + Internal helper method that knows how to build an empty Atom entry. + """ + + moduleLogger.debug('Inside getEmptyXmlDoc') + + entryXmlDoc = minidom.Document() + entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") + entryElement.setAttribute('xmlns', ATOM_NS) + entryXmlDoc.appendChild(entryElement) + return entryXmlDoc diff --git a/src/cmislib/atompub_binding.py b/src/cmislib/atompub_binding.py new file mode 100644 index 0000000..f781f42 --- /dev/null +++ b/src/cmislib/atompub_binding.py @@ -0,0 +1,4280 @@ +# +# 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 __future__ import absolute_import + +import datetime +import logging +import mimetypes +import re +import StringIO +from urllib import quote +from urllib2 import HTTPError +from urlparse import urlparse, urlunparse +from xml.dom import minidom +from xml.parsers.expat import ExpatError + +import six + +from . import messages +from .cmis_services import RepositoryServiceIfc +from .cmis_services import Binding +from .domain import ( + CmisId, CmisObject, ObjectType, Property, ACL, ACE, + ChangeEntry, ResultSet, Rendition +) +from .net import RESTService as Rest +from .exceptions import ( + CmisException, RuntimeException, ObjectNotFoundException, + InvalidArgumentException, NotSupportedException +) +from .util import ( + parseDateTimeValue, multiple_replace, parsePropValue, parseBoolValue, toCMISValue +) + +moduleLogger = logging.getLogger('cmislib.atompubbinding') + +# Namespaces +ATOM_NS = 'http://www.w3.org/2005/Atom' +APP_NS = 'http://www.w3.org/2007/app' +CMISRA_NS = 'http://docs.oasis-open.org/ns/cmis/restatom/200908/' +CMIS_NS = 'http://docs.oasis-open.org/ns/cmis/core/200908/' + +# Content types +# Not all of these patterns have variability, but some do. It seemed cleaner +# just to treat them all like patterns to simplify the matching logic +ATOM_XML_TYPE = 'application/atom+xml' +ATOM_XML_ENTRY_TYPE = 'application/atom+xml;type=entry' +ATOM_XML_ENTRY_TYPE_P = re.compile('^application/atom\+xml.*type.*entry') +ATOM_XML_FEED_TYPE = 'application/atom+xml;type=feed' +ATOM_XML_FEED_TYPE_P = re.compile('^application/atom\+xml.*type.*feed') +CMIS_TREE_TYPE = 'application/cmistree+xml' +CMIS_TREE_TYPE_P = re.compile('^application/cmistree\+xml') +CMIS_QUERY_TYPE = 'application/cmisquery+xml' +CMIS_ACL_TYPE = 'application/cmisacl+xml' + +# Standard rels +DOWN_REL = 'down' +FIRST_REL = 'first' +LAST_REL = 'last' +NEXT_REL = 'next' +PREV_REL = 'prev' +SELF_REL = 'self' +UP_REL = 'up' +TYPE_DESCENDANTS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/typedescendants' +VERSION_HISTORY_REL = 'version-history' +FOLDER_TREE_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/foldertree' +RELATIONSHIPS_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/relationships' +ACL_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/acl' +CHANGE_LOG_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/changes' +POLICIES_REL = 'http://docs.oasis-open.org/ns/cmis/link/200908/policies' +RENDITION_REL = 'alternate' + +# Collection types +QUERY_COLL = 'query' +TYPES_COLL = 'types' +CHECKED_OUT_COLL = 'checkedout' +UNFILED_COLL = 'unfiled' +ROOT_COLL = 'root' + +class AtomPubBinding(Binding): + def __init__(self, **kwargs): + self.extArgs = kwargs + + def getRepositoryService(self): + return RepositoryService() + + def get(self, url, username, password, **kwargs): + + """ + Does a get against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, if you need to get a specific object by object id, try + :class:`Repository.getObject`. If you have a path instead of an object + id, use :class:`Repository.getObjectByPath`. Or, you could start with + the root folder (:class:`Repository.getRootFolder`) and drill down from + there. + """ + + # merge the cmis client extended args with the ones that got passed in + if (len(self.extArgs) > 0): + kwargs.update(self.extArgs) + + resp, content = Rest().get(url, + username=username, + password=password, + **kwargs) + if resp['status'] != '200': + self._processCommonErrors(resp, url) + return content + else: + try: + return minidom.parseString(content) + except ExpatError: + raise CmisException('Could not parse server response', url) + + def delete(self, url, username, password, **kwargs): + + """ + Does a delete against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, to delete a folder you'd call :class:`Folder.delete` and + to delete a document you'd call :class:`Document.delete`. + """ + + # merge the cmis client extended args with the ones that got passed in + if (len(self.extArgs) > 0): + kwargs.update(self.extArgs) + + resp, content = Rest().delete(url, + username=username, + password=password, + **kwargs) + if resp['status'] != '200': + self._processCommonErrors(resp, url) + return content + else: + pass + + def post(self, url, username, password, payload, contentType, **kwargs): + + """ + Does a post against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, to update the properties on an object, you'd call + :class:`CmisObject.updateProperties`. Or, to check in a document that's + been checked out, you'd call :class:`Document.checkin` on the PWC. + """ + + # merge the cmis client extended args with the ones that got passed in + if (len(self.extArgs) > 0): + kwargs.update(self.extArgs) + + resp, content = Rest().post(url, + payload, + contentType, + username=username, + password=password, + **kwargs) + if resp['status'] == '200': + try: + return minidom.parseString(content) + except ExpatError: + raise CmisException('Could not parse server response', url) + elif resp['status'] == '201': + try: + return minidom.parseString(content) + except ExpatError: + raise CmisException('Could not parse server response', url) + else: + self._processCommonErrors(resp, url) + return resp + + def put(self, url, username, password, payload, contentType, **kwargs): + + """ + Does a put against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, to update the properties on an object, you'd call + :class:`CmisObject.updateProperties`. Or, to check in a document that's + been checked out, you'd call :class:`Document.checkin` on the PWC. + """ + + # merge the cmis client extended args with the ones that got passed in + if (len(self.extArgs) > 0): + kwargs.update(self.extArgs) + + resp, content = Rest().put(url, + payload, + contentType, + username=username, + password=password, + **kwargs) + if resp['status'] != '200' and resp['status'] != '201': + self._processCommonErrors(resp, url) + return content + else: + #if result.headers['content-length'] != '0': + try: + return minidom.parseString(content) + except ExpatError: + # This may happen and is normal + return None + + +class RepositoryService(RepositoryServiceIfc): + def __init__(self): + self._uriTemplates = {} + + def reload(self, obj): + self.logger.debug('Reload called on object') + obj.xmlDoc = obj._cmisClient.binding.get(obj._cmisClient.repositoryUrl.encode('utf-8'), + obj._cmisClient.username, + obj._cmisClient.password) + obj._initData() + + def getRepository(self, client, repositoryId): + doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') + + for workspaceElement in workspaceElements: + idElement = workspaceElement.getElementsByTagNameNS(CMIS_NS, 'repositoryId') + if idElement[0].childNodes[0].data == repositoryId: + return AtomPubRepository(self, workspaceElement) + + raise ObjectNotFoundException(url=client.repositoryUrl) + + def getRepositories(self, client): + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + if (type(result) == HTTPError): + raise RuntimeException() + + workspaceElements = result.getElementsByTagNameNS(APP_NS, 'workspace') + # instantiate a Repository object using every workspace element + # in the service URL then ask the repository object for its ID + # and name, and return that back + + repositories = [] + for node in [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE]: + repository = AtomPubRepository(client, node) + repositories.append({'repositoryId': repository.getRepositoryId(), + 'repositoryName': repository.getRepositoryInfo()['repositoryName']}) + return repositories + + def getDefaultRepository(self, client): + doc = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') + # instantiate a Repository object with the first workspace + # element we find + repository = AtomPubRepository(client, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0]) + return repository + + +class UriTemplate(dict): + + """ + Simple dictionary to represent the data stored in + a URI template entry. + """ + + def __init__(self, template, templateType, mediaType): + + """ + Constructor + """ + + dict.__init__(self) + self['template'] = template + self['type'] = templateType + self['mediaType'] = mediaType + + +class AtomPubCmisObject(CmisObject): + + def __init__(self, cmisClient, repository, objectId=None, xmlDoc=None, **kwargs): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._objectId = objectId + self._name = None + self._properties = {} + self._allowableActions = {} + self.xmlDoc = xmlDoc + self._kwargs = kwargs + self.logger = logging.getLogger('cmislib.model.CmisObject') + self.logger.info('Creating an instance of CmisObject') + + def __str__(self): + """To string""" + return self.getObjectId() + + def reload(self, **kwargs): + + """ + Fetches the latest representation of this object from the CMIS service. + Some methods, like :class:`^Document.checkout` do this for you. + + If you call reload with a properties filter, the filter will be in + effect on subsequent calls until the filter argument is changed. To + reset to the full list of properties, call reload with filter set to + '*'. + """ + + self.logger.debug('Reload called on CmisObject') + if kwargs: + if self._kwargs: + self._kwargs.update(kwargs) + else: + self._kwargs = kwargs + + templates = self._repository.getUriTemplates() + template = templates['objectbyid']['template'] + + # Doing some refactoring here. Originally, we snagged the template + # and then "filled in" the template based on the args passed in. + # However, some servers don't provide a full template which meant + # supported optional args wouldn't get passed in using the fill-the- + # template approach. What's going on now is that the template gets + # filled in where it can, but if additional, non-templated args are + # passed in, those will get tacked on to the query string as + # "additional" options. + + params = { + '{id}': self.getObjectId(), + '{filter}': '', + '{includeAllowableActions}': 'false', + '{includePolicyIds}': 'false', + '{includeRelationships}': '', + '{includeACL}': 'false', + '{renditionFilter}': ''} + + options = {} + addOptions = {} # args specified, but not in the template + for k, v in self._kwargs.items(): + pKey = "{" + k + "}" + if template.find(pKey) >= 0: + options[pKey] = toCMISValue(v) + else: + addOptions[k] = toCMISValue(v) + + # merge the templated args with the default params + params.update(options) + + # fill in the template + byObjectIdUrl = multiple_replace(params, template) + + self.xmlDoc = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **addOptions) + self._initData() + + # if a returnVersion arg was passed in, it is possible we got back + # a different object ID than the value we started with, so it needs + # to be cleared out as well + if options.has_key('returnVersion') or addOptions.has_key('returnVersion'): + self._objectId = None + + def _initData(self): + + """ + An internal method used to clear out any member variables that + might be out of sync if we were to fetch new XML from the + service. + """ + + self._properties = {} + self._name = None + self._allowableActions = {} + + def getObjectId(self): + + """ + Returns the object ID for this object. + + >>> doc = resultSet.getResults()[0] + >>> doc.getObjectId() + u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' + """ + + if self._objectId == None: + if self.xmlDoc == None: + self.logger.debug('Both objectId and xmlDoc were None, reloading') + self.reload() + props = self.getProperties() + self._objectId = CmisId(props['cmis:objectId']) + return self._objectId + + def getObjectParents(self, **kwargs): + """ + Gets the parents of this object as a :class:`ResultSet`. + + The following optional arguments are supported: + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includeRelativePathSegment + """ + # get the appropriate 'up' link + parentUrl = self._getLink(UP_REL) + + if parentUrl == None: + raise NotSupportedException('Root folder does not support getObjectParents') + + # invoke the URL + result = self._cmisClient.binding.get(parentUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return AtomPubResultSet(self._cmisClient, self._repository, result) + + def getPaths(self): + """ + Returns the object's paths as a list of strings. + """ + # see sub-classes for implementation + pass + + def getAllowableActions(self): + + """ + Returns a dictionary of allowable actions, keyed off of the action name. + + >>> actions = doc.getAllowableActions() + >>> for a in actions: + ... print "%s:%s" % (a,actions[a]) + ... + canDeleteContentStream:True + canSetContentStream:True + canCreateRelationship:True + canCheckIn:False + canApplyACL:False + canDeleteObject:True + canGetAllVersions:True + canGetObjectParents:True + canGetProperties:True + """ + + if self._allowableActions == {}: + self.reload(includeAllowableActions=True) + allowElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'allowableActions') + assert len(allowElements) == 1, "Expected response to have exactly one allowableActions element" + allowElement = allowElements[0] + for node in [e for e in allowElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + actionName = node.localName + actionValue = parseBoolValue(node.childNodes[0].data) + self._allowableActions[actionName] = actionValue + + return self._allowableActions + + def getTitle(self): + + """ + Returns the value of the object's cmis:title property. + """ + + if self.xmlDoc == None: + self.reload() + + titleElement = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'title')[0] + + if titleElement and titleElement.childNodes: + return titleElement.childNodes[0].data + + def getProperties(self): + + """ + Returns a dict of the object's properties. If CMIS returns an + empty element for a property, the property will be in the + dict with a value of None. + + >>> props = doc.getProperties() + >>> for p in props: + ... print "%s: %s" % (p, props[p]) + ... + cmis:contentStreamMimeType: text/html + cmis:creationDate: 2009-12-15T09:45:35.369-06:00 + cmis:baseTypeId: cmis:document + cmis:isLatestMajorVersion: false + cmis:isImmutable: false + cmis:isMajorVersion: false + cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339 + + The optional filter argument is not yet implemented. + """ + + #TODO implement filter + if self._properties == {}: + if self.xmlDoc == None: + self.reload() + propertiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] + #cpattern = re.compile(r'^property([\w]*)') + for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMIS_NS]: + #propertyId, propertyString, propertyDateTime + #propertyType = cpattern.search(node.localName).groups()[0] + propertyName = node.attributes['propertyDefinitionId'].value + if node.childNodes and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: + valNodeList = node.getElementsByTagNameNS(CMIS_NS, 'value') + if (len(valNodeList) == 1): + propertyValue = parsePropValue(valNodeList[0]. + childNodes[0].data, + node.localName) + else: + propertyValue = [] + for valNode in valNodeList: + propertyValue.append(parsePropValue(valNode. + childNodes[0].data, + node.localName)) + else: + propertyValue = None + self._properties[propertyName] = propertyValue + + for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]: + propertyName = node.nodeName + if node.childNodes: + propertyValue = node.firstChild.nodeValue + else: + propertyValue = None + self._properties[propertyName] = propertyValue + + return self._properties + + def getName(self): + + """ + Returns the value of cmis:name from the getProperties() dictionary. + We don't need a getter for every standard CMIS property, but name + is a pretty common one so it seems to make sense. + + >>> doc.getName() + u'system-overview.html' + """ + + if self._name == None: + self._name = self.getProperties()['cmis:name'] + return self._name + + def updateProperties(self, properties): + + """ + Updates the properties of an object with the properties provided. + Only provide the set of properties that need to be updated. + + >>> folder = repo.getObjectByPath('/someFolder2') + >>> folder.getName() + u'someFolder2' + >>> props = {'cmis:name': 'someFolderFoo'} + >>> folder.updateProperties(props) + + >>> folder.getName() + u'someFolderFoo' + + """ + + self.logger.debug('Inside updateProperties') + + # get the self link + selfUrl = self._getSelfLink() + + # if we have a change token, we must pass it back, per the spec + args = {} + if (self.properties.has_key('cmis:changeToken') and + self.properties['cmis:changeToken'] != None): + self.logger.debug('Change token present, adding it to args') + args = {"changeToken": self.properties['cmis:changeToken']} + + # the getEntryXmlDoc function may need the object type + objectTypeId = None + if (self.properties.has_key('cmis:objectTypeId') and + not properties.has_key('cmis:objectTypeId')): + objectTypeId = self.properties['cmis:objectTypeId'] + self.logger.debug('This object type is:%s' % objectTypeId) + + # build the entry based on the properties provided + xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties) + + self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml()) + + # do a PUT of the entry + updatedXmlDoc = self._cmisClient.binding.put(selfUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + xmlEntryDoc.toxml(encoding='utf-8'), + ATOM_XML_TYPE, + **args) + + # reset the xmlDoc for this object with what we got back from + # the PUT, then call initData we dont' want to call + # self.reload because we've already got the parsed XML-- + # there's no need to fetch it again + self.xmlDoc = updatedXmlDoc + self._initData() + return self + + def move(self, sourceFolder, targetFolder): + + """ + Moves an object from the source folder to the target folder. + + >>> sub1 = repo.getObjectByPath('/cmislib/sub1') + >>> sub2 = repo.getObjectByPath('/cmislib/sub2') + >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1') + >>> doc.move(sub1, sub2) + """ + + postUrl = targetFolder.getChildrenLink() + + args = {"sourceFolderId": sourceFolder.id} + + # post the Atom entry + result = self._cmisClient.binding.post(postUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + self.xmlDoc.toxml(encoding='utf-8'), + ATOM_XML_ENTRY_TYPE, + **args) + + def delete(self, **kwargs): + + """ + Deletes this :class:`CmisObject` from the repository. Note that in the + case of a :class:`Folder` object, some repositories will refuse to + delete it if it contains children and some will delete it without + complaint. If what you really want to do is delete the folder and all + of its descendants, use :meth:`~Folder.deleteTree` instead. + + >>> folder.delete() + + The optional allVersions argument is supported. + """ + + url = self._getSelfLink() + result = self._cmisClient.binding.delete(url.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + def applyPolicy(self, policyId): + + """ + This is not yet implemented. + """ + + # depends on this object's canApplyPolicy allowable action + if self.getAllowableActions()['canApplyPolicy']: + raise NotImplementedError + else: + raise CmisException('This object has canApplyPolicy set to false') + + def createRelationship(self, targetObj, relTypeId): + + """ + Creates a relationship between this object and a specified target + object using the relationship type specified. Returns the new + :class:`Relationship` object. + + >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + """ + + if isinstance(relTypeId, str): + relTypeId = CmisId(relTypeId) + + props = {} + props['cmis:sourceId'] = self.getObjectId() + props['cmis:targetId'] = targetObj.getObjectId() + props['cmis:objectTypeId'] = relTypeId + xmlDoc = getEntryXmlDoc(self._repository, properties=props) + + url = self._getLink(RELATIONSHIPS_REL) + assert url != None, 'Could not determine relationships URL' + + result = self._cmisClient.binding.post(url.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + xmlDoc.toxml(encoding='utf-8'), + ATOM_XML_TYPE) + + # instantiate CmisObject objects with the results and return the list + entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') + assert(len(entryElements) == 1), "Expected entry element in result from relationship URL post" + return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0])) + + def getRelationships(self, **kwargs): + + """ + Returns a :class:`ResultSet` of :class:`Relationship` objects for each + relationship where the source is this object. + + >>> rels = tstDoc1.getRelationships() + >>> len(rels.getResults()) + 1 + >>> rel = rels.getResults().values()[0] + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + The following optional arguments are supported: + - includeSubRelationshipTypes + - relationshipDirection + - typeId + - maxItems + - skipCount + - filter + - includeAllowableActions + """ + + url = self._getLink(RELATIONSHIPS_REL) + assert url != None, 'Could not determine relationships URL' + + result = self._cmisClient.binding.get(url.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return AtomPubResultSet(self._cmisClient, self._repository, result) + + def removePolicy(self, policyId): + + """ + This is not yet implemented. + """ + + # depends on this object's canRemovePolicy allowable action + if self.getAllowableActions()['canRemovePolicy']: + raise NotImplementedError + else: + raise CmisException('This object has canRemovePolicy set to false') + + def getAppliedPolicies(self): + + """ + This is not yet implemented. + """ + + # depends on this object's canGetAppliedPolicies allowable action + if self.getAllowableActions()['canGetAppliedPolicies']: + raise NotImplementedError + else: + raise CmisException('This object has canGetAppliedPolicies set to false') + + def getACL(self): + + """ + Repository.getCapabilities['ACL'] must return manage or discover. + + >>> acl = folder.getACL() + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + + 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': , 'jdoe': } + """ + + if self._repository.getCapabilities()['ACL'] == 'manage': + # if the ACL capability is manage, this must be + # supported + # but it also depends on the canApplyACL allowable action + # for this object + if not isinstance(acl, ACL): + raise CmisException('The ACL to apply must be an instance of the ACL class.') + aclUrl = self._getLink(ACL_REL) + assert aclUrl, "Could not determine the object's ACL URL." + result = self._cmisClient.binding.put(aclUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + acl.getXmlDoc().toxml(encoding='utf-8'), + CMIS_ACL_TYPE) + return AtomPubACL(xmlDoc=result) + else: + raise NotSupportedException + + def _getSelfLink(self): + + """ + Returns the URL used to retrieve this object. + """ + + url = self._getLink(SELF_REL) + + assert len(url) > 0, "Could not determine the self link." + + return url + + def _getLink(self, rel, ltype=None): + + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + + if self.xmlDoc == None: + self.reload() + linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + + if ltype: + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if ltype and linkElement.attributes.has_key('type'): + typeAttr = linkElement.attributes['type'].value + + if relAttr == rel and ltype.match(typeAttr): + return linkElement.attributes['href'].value + else: + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + allowableActions = property(getAllowableActions) + name = property(getName) + id = property(getObjectId) + properties = property(getProperties) + title = property(getTitle) + ACL = property(getACL) + + +class AtomPubRepository(object): + + """ + Represents a CMIS repository. Will lazily populate itself by + calling the repository CMIS service URL. + + You must pass in an instance of a CmisClient when creating an + instance of this class. + """ + + def __init__(self, cmisClient, xmlDoc=None): + """ Constructor """ + self._cmisClient = cmisClient + self.xmlDoc = xmlDoc + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._uriTemplates = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + self.logger = logging.getLogger('cmislib.model.Repository') + self.logger.info('Creating an instance of Repository') + + def __str__(self): + """To string""" + return self.getRepositoryName() + + def reload(self): + """ + This method will re-fetch the repository's XML data from the CMIS + repository. + """ + self.logger.debug('Reload called on object') + self.xmlDoc = self._cmisClient.binding.get(self._cmisClient.repositoryUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + self._initData() + + def _initData(self): + """ + This method clears out any local variables that would be out of sync + when data is re-fetched from the server. + """ + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._uriTemplates = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + + def getSupportedPermissions(self): + + """ + Returns the value of the cmis:supportedPermissions element. Valid + values are: + + - basic: indicates that the CMIS Basic permissions are supported + - repository: indicates that repository specific permissions are supported + - both: indicates that both CMIS basic permissions and repository specific permissions are supported + + >>> repo.supportedPermissions + u'both' + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if not self._permissions: + if self.xmlDoc == None: + self.reload() + suppEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'supportedPermissions') + assert len(suppEls) == 1, 'Expected the repository service document to have one element named supportedPermissions' + self._permissions = suppEls[0].childNodes[0].data + + return self._permissions + + def getPermissionDefinitions(self): + + """ + Returns a dictionary of permission definitions for this repository. The + key is the permission string or technical name of the permission + and the value is the permission description. + + >>> for permDef in repo.permissionDefinitions: + ... print permDef + ... + cmis:all + {http://www.alfresco.org/model/system/1.0}base.LinkChildren + {http://www.alfresco.org/model/content/1.0}folder.Consumer + {http://www.alfresco.org/model/security/1.0}All.All + {http://www.alfresco.org/model/system/1.0}base.CreateAssociations + {http://www.alfresco.org/model/system/1.0}base.FullControl + {http://www.alfresco.org/model/system/1.0}base.AddChildren + {http://www.alfresco.org/model/system/1.0}base.ReadAssociations + {http://www.alfresco.org/model/content/1.0}folder.Editor + {http://www.alfresco.org/model/content/1.0}cmobject.Editor + {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations + cmis:read + cmis:write + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if self._permDefs == {}: + if self.xmlDoc == None: + self.reload() + aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') + assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' + aclEl = aclEls[0] + perms = {} + for e in aclEl.childNodes: + if e.localName == 'permissions': + permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') + assert len(permEls) == 1, 'Expected permissions element to have a child named permission' + descEls = e.getElementsByTagNameNS(CMIS_NS, 'description') + assert len(descEls) == 1, 'Expected permissions element to have a child named description' + perm = permEls[0].childNodes[0].data + desc = descEls[0].childNodes[0].data + perms[perm] = desc + self._permDefs = perms + + return self._permDefs + + def getPermissionMap(self): + + """ + Returns a dictionary representing the permission mapping table where + each key is a permission key string and each value is a list of one or + more permissions the principal must have to perform the operation. + + >>> for (k,v) in repo.permissionMap.items(): + ... print 'To do this: %s, you must have these perms:' % k + ... for perm in v: + ... print perm + ... + To do this: canCreateFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canAddToFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canDelete.Object, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.DeleteNode + To do this: canCheckin.Document, you must have these perms: + cmis:all + {http://www.alfresco.org/model/content/1.0}lockable.CheckIn + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if self._permMap == {}: + if self.xmlDoc == None: + self.reload() + aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') + assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' + aclEl = aclEls[0] + permMap = {} + for e in aclEl.childNodes: + permList = [] + if e.localName == 'mapping': + keyEls = e.getElementsByTagNameNS(CMIS_NS, 'key') + assert len(keyEls) == 1, 'Expected mapping element to have a child named key' + permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') + assert len(permEls) >= 1, 'Expected mapping element to have at least one permission element' + key = keyEls[0].childNodes[0].data + for permEl in permEls: + permList.append(permEl.childNodes[0].data) + permMap[key] = permList + self._permMap = permMap + + return self._permMap + + def getPropagation(self): + + """ + Returns the value of the cmis:propagation element. Valid values are: + - objectonly: indicates that the repository is able to apply ACEs + without changing the ACLs of other objects + - propagate: indicates that the repository is able to apply ACEs to a + given object and propagate this change to all inheriting objects + + >>> repo.propagation + u'propagate' + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if not self._propagation: + if self.xmlDoc == None: + self.reload() + propEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propagation') + assert len(propEls) == 1, 'Expected the repository service document to have one element named propagation' + self._propagation = propEls[0].childNodes[0].data + + return self._propagation + + def getRepositoryId(self): + + """ + Returns this repository's unique identifier + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryId() + u'83beb297-a6fa-4ac5-844b-98c871c0eea9' + """ + + if self._repositoryId == None: + if self.xmlDoc == None: + self.reload() + self._repositoryId = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryId')[0].firstChild.data + return self._repositoryId + + def getRepositoryName(self): + + """ + Returns this repository's name + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryName() + u'Main Repository' + """ + + if self._repositoryName == None: + if self.xmlDoc == None: + self.reload() + self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data + return self._repositoryName + + def getRepositoryInfo(self): + + """ + Returns a dict of repository information. + + >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() + u'Main Repository' + >>> info = repo.getRepositoryInfo() + >>> for k,v in info.items(): + ... print "%s:%s" % (k,v) + ... + cmisSpecificationTitle:Version 1.0 Committee Draft 04 + cmisVersionSupported:1.0 + repositoryDescription:None + productVersion:3.2.0 (r2 2440) + rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 + repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 + repositoryName:Main Repository + vendorName:Alfresco + productName:Alfresco Repository (Community) + """ + + if not self._repositoryInfo: + if self.xmlDoc == None: + self.reload() + repoInfoElement = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'repositoryInfo')[0] + for node in repoInfoElement.childNodes: + if node.nodeType == node.ELEMENT_NODE and node.localName != 'capabilities': + try: + data = node.childNodes[0].data + except: + data = None + self._repositoryInfo[node.localName] = data + return self._repositoryInfo + + def getCapabilities(self): + + """ + Returns a dict of repository capabilities. + + >>> caps = repo.getCapabilities() + >>> for k,v in caps.items(): + ... print "%s:%s" % (k,v) + ... + PWCUpdatable:True + VersionSpecificFiling:False + Join:None + ContentStreamUpdatability:anytime + AllVersionsSearchable:False + Renditions:None + Multifiling:True + GetFolderTree:True + GetDescendants:True + ACL:None + PWCSearchable:True + Query:bothcombined + Unfiling:False + Changes:None + """ + + if not self._capabilities: + if self.xmlDoc == None: + self.reload() + capabilitiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'capabilities')[0] + for node in [e for e in capabilitiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + key = node.localName.replace('capability', '') + value = parseBoolValue(node.childNodes[0].data) + self._capabilities[key] = value + return self._capabilities + + def getRootFolder(self): + """ + Returns the root folder of the repository + + >>> root = repo.getRootFolder() + >>> root.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + # get the root folder id + rootFolderId = self.getRepositoryInfo()['rootFolderId'] + # instantiate a Folder object using the ID + folder = AtomPubFolder(self._cmisClient, self, rootFolderId) + # return it + return folder + + def getFolder(self, folderId): + + """ + Returns a :class:`Folder` object for a specified folderId + + >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') + >>> someFolder.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + retObject = self.getObject(folderId) + return AtomPubFolder(self._cmisClient, self, xmlDoc=retObject.xmlDoc) + + def getTypeChildren(self, + typeId=None): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + child types of the type specified by the typeId. + + If no typeId is provided, the result will be the same as calling + `self.getTypeDefinitions` + + These optional arguments are current unsupported: + - includePropertyDefinitions + - maxItems + - skipCount + + >>> baseTypes = repo.getTypeChildren() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + # Unfortunately, the spec does not appear to present a way to + # know how to get the children of a specific type without first + # retrieving the type, then asking it for one of its navigational + # links. + + # if a typeId is specified, get it, then get its "down" link + if typeId: + targetType = self.getTypeDefinition(typeId) + childrenUrl = targetType.getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P) + typesXmlDoc = self._cmisClient.binding.get(childrenUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') + types = [] + for entryElement in entryElements: + objectType = ObjectType(self._cmisClient, + self, + xmlDoc=entryElement) + types.append(objectType) + # otherwise, if a typeId is not specified, return + # the list of base types + else: + types = self.getTypeDefinitions() + return types + + def getTypeDescendants(self, typeId=None, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + descendant types of the type specified by the typeId. + + If no typeId is provided, the repository's "typesdescendants" URL + will be called to determine the list of descendant types. + + >>> allTypes = repo.getTypeDescendants() + >>> for aType in allTypes: + ... print aType.getTypeId() + ... + cmis:folder + F:cm:systemfolder + F:act:savedactionfolder + F:app:configurations + F:fm:forums + F:wcm:avmfolder + F:wcm:avmplainfolder + F:wca:webfolder + F:wcm:avmlayeredfolder + F:st:site + F:app:glossary + F:fm:topic + + These optional arguments are supported: + - depth + - includePropertyDefinitions + + >>> types = alfRepo.getTypeDescendants('cmis:folder') + >>> len(types) + 17 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1) + >>> len(types) + 12 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2) + >>> len(types) + 17 + """ + + # Unfortunately, the spec does not appear to present a way to + # know how to get the children of a specific type without first + # retrieving the type, then asking it for one of its navigational + # links. + if typeId: + targetType = self.getTypeDefinition(typeId) + descendUrl = targetType.getLink(DOWN_REL, CMIS_TREE_TYPE_P) + + else: + descendUrl = self.getLink(TYPE_DESCENDANTS_REL) + + if not descendUrl: + raise NotSupportedException("Could not determine the type descendants URL") + + typesXmlDoc = self._cmisClient.binding.get(descendUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') + types = [] + for entryElement in entryElements: + objectType = AtomPubObjectType(self._cmisClient, + self, + xmlDoc=entryElement) + types.append(objectType) + return types + + def getTypeDefinitions(self, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects representing + the base types in the repository. + + >>> baseTypes = repo.getTypeDefinitions() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + typesUrl = self.getCollectionLink(TYPES_COLL) + typesXmlDoc = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') + types = [] + for entryElement in entryElements: + objectType = AtomPubObjectType(self._cmisClient, + self, + xmlDoc=entryElement) + types.append(objectType) + # return the result + return types + + def getTypeDefinition(self, typeId): + + """ + Returns an :class:`ObjectType` object for the specified object type id. + + >>> folderType = repo.getTypeDefinition('cmis:folder') + """ + + objectType = AtomPubObjectType(self._cmisClient, self, typeId) + objectType.reload() + return objectType + + def getLink(self, rel): + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + if self.xmlDoc == None: + self.reload() + + linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + def getCheckedOutDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently checked out. + + >>> rs = repo.getCheckedOutDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getCheckedOutDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a (Working Copy).pdf' + u'sample-b (Working Copy).pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + return self.getCollection(CHECKED_OUT_COLL, **kwargs) + + def getUnfiledDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently unfiled. + + >>> rs = repo.getUnfiledDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getUnfiledDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a.pdf' + u'sample-b.pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + return self.getCollection(UNFILED_COLL, **kwargs) + + def getObject(self, + objectId, + **kwargs): + + """ + Returns an object given the specified object ID. + + >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are supported: + - returnVersion + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + """ + + return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs) + + def getObjectByPath(self, path, **kwargs): + + """ + Returns an object given the path to the object. + + >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are not currently supported: + - filter + - includeAllowableActions + """ + + # get the uritemplate + template = self.getUriTemplates()['objectbypath']['template'] + + # fill in the template with the path provided + params = { + '{path}': quote(path, '/'), + '{filter}': '', + '{includeAllowableActions}': 'false', + '{includePolicyIds}': 'false', + '{includeRelationships}': '', + '{includeACL}': 'false', + '{renditionFilter}': ''} + + options = {} + addOptions = {} # args specified, but not in the template + for k, v in kwargs.items(): + pKey = "{" + k + "}" + if template.find(pKey) >= 0: + options[pKey] = toCMISValue(v) + else: + addOptions[k] = toCMISValue(v) + + # merge the templated args with the default params + params.update(options) + + byObjectPathUrl = multiple_replace(params, template) + + # do a GET against the URL + result = self._cmisClient.binding.get(byObjectPathUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **addOptions) + + # instantiate CmisObject objects with the results and return the list + entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') + assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byObjectPathUrl + return getSpecializedObject(AtomPubCmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **kwargs), **kwargs) + + def query(self, statement, **kwargs): + + """ + Returns a list of :class:`CmisObject` objects based on the CMIS + Query Language passed in as the statement. The actual objects + returned will be instances of the appropriate child class based + on the object's base type ID. + + In order for the results to be properly instantiated as objects, + make sure you include 'cmis:objectId' as one of the fields in + your select statement, or just use "SELECT \*". + + If you want the search results to automatically be instantiated with + the appropriate sub-class of :class:`CmisObject` you must either + include cmis:baseTypeId as one of the fields in your select statement + or just use "SELECT \*". + + >>> q = "select * from cmis:document where cmis:name like '%test%'" + >>> resultSet = repo.query(q) + >>> len(resultSet.getResults()) + 1 + >>> resultSet.hasNext() + False + + The following optional arguments are supported: + - searchAllVersions + - includeRelationships + - renditionFilter + - includeAllowableActions + - maxItems + - skipCount + + >>> q = 'select * from cmis:document' + >>> rs = repo.query(q) + >>> len(rs.getResults()) + 148 + >>> rs = repo.query(q, maxItems='5') + >>> len(rs.getResults()) + 5 + >>> rs.hasNext() + True + """ + + if self.xmlDoc == None: + self.reload() + + # get the URL this repository uses to accept query POSTs + queryUrl = self.getCollectionLink(QUERY_COLL) + + # build the CMIS query XML that we're going to POST + xmlDoc = self._getQueryXmlDoc(statement, **kwargs) + + # do the POST + #print 'posting:%s' % xmlDoc.toxml(encoding='utf-8') + result = self._cmisClient.binding.post(queryUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + xmlDoc.toxml(encoding='utf-8'), + CMIS_QUERY_TYPE) + + # return the result set + return AtomPubResultSet(self._cmisClient, self, result) + + def getContentChanges(self, **kwargs): + + """ + Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + + The following optional arguments are supported: + - changeLogToken + - includeProperties + - includePolicyIDs + - includeACL + - maxItems + + You can get the latest change log token by inspecting the repository + info via :meth:`Repository.getRepositoryInfo`. + + >>> repo.info['latestChangeLogToken'] + u'2692' + >>> rs = repo.getContentChanges(changeLogToken='2692') + >>> len(rs) + 1 + >>> rs[0].id + u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' + >>> rs[0].changeType + u'updated' + >>> rs[0].changeTime + datetime.datetime(2010, 2, 16, 20, 6, 37) + """ + + if self.getCapabilities()['Changes'] == None: + raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT) + + changesUrl = self.getLink(CHANGE_LOG_REL) + result = self._cmisClient.binding.get(changesUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return AtomPubChangeEntryResultSet(self._cmisClient, self, result) + + def createDocumentFromString(self, + name, + properties={}, + parentFolder=None, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') + + """ + + # if you didn't pass in a parent folder + if parentFolder == None: + # if the repository doesn't require fileable objects to be filed + if self.getCapabilities()['Unfiling']: + # has not been implemented + #postUrl = self.getCollectionLink(UNFILED_COLL) + raise NotImplementedError + else: + # this repo requires fileable objects to be filed + raise InvalidArgumentException + + return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString), + contentType, contentEncoding) + + def createDocument(self, + name, + properties={}, + parentFolder=None, + contentFile=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new :class:`Document` object. If the repository + supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + To create a document with an associated contentFile, pass in a + File object. The method will attempt to guess the appropriate content + type and encoding based on the file. To specify it yourself, pass them + in via the contentType and contentEncoding arguments. + + >>> f = open('sample-a.pdf', 'rb') + >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) + + >>> f.close() + >>> doc.getTitle() + u'sample-a.pdf' + + The following optional arguments are not currently supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + postUrl = '' + # if you didn't pass in a parent folder + if parentFolder == None: + # if the repository doesn't require fileable objects to be filed + if self.getCapabilities()['Unfiling']: + # has not been implemented + #postUrl = self.getCollectionLink(UNFILED_COLL) + raise NotImplementedError + else: + # this repo requires fileable objects to be filed + raise InvalidArgumentException + else: + postUrl = parentFolder.getChildrenLink() + + # make sure a name is set + properties['cmis:name'] = name + + # hardcoding to cmis:document if it wasn't + # passed in via props + if not properties.has_key('cmis:objectTypeId'): + properties['cmis:objectTypeId'] = CmisId('cmis:document') + # and if it was passed in, making sure it is a CmisId + elif not isinstance(properties['cmis:objectTypeId'], CmisId): + properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) + + # build the Atom entry + xmlDoc = getEntryXmlDoc(self, None, properties, contentFile, + contentType, contentEncoding) + + # post the Atom entry + result = self._cmisClient.binding.post(postUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + xmlDoc.toxml(encoding='utf-8'), + ATOM_XML_ENTRY_TYPE) + + # what comes back is the XML for the new document, + # so use it to instantiate a new document + # then return it + return AtomPubDocument(self._cmisClient, self, xmlDoc=result) + + def createDocumentFromSource(self, + sourceId, + properties={}, + parentFolder=None): + """ + This is not yet implemented. + + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + # TODO: To be implemented + raise NotImplementedError + + def createFolder(self, + parentFolder, + name, + properties={}): + + """ + Creates a new :class:`Folder` object in the specified parentFolder. + + >>> root = repo.getRootFolder() + >>> folder = repo.createFolder(root, 'someFolder2') + >>> folder.getTitle() + u'someFolder2' + >>> folder.getObjectId() + u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' + + The following optional arguments are not yet supported: + - policies + - addACEs + - removeACEs + """ + + return parentFolder.createFolder(name, properties) + + def createRelationship(self, sourceObj, targetObj, relType): + """ + Creates a relationship of the specific type between a source object + and a target object and returns the new :class:`Relationship` object. + + The following optional arguments are not currently supported: + - policies + - addACEs + - removeACEs + """ + return sourceObj.createRelationship(targetObj, relType) + + def createPolicy(self, properties): + """ + This has not yet been implemented. + + The following optional arguments are not currently supported: + - folderId + - policies + - addACEs + - removeACEs + """ + # TODO: To be implemented + raise NotImplementedError + + def getUriTemplates(self): + + """ + Returns a list of the URI templates the repository service knows about. + + >>> templates = repo.getUriTemplates() + >>> templates['typebyid']['mediaType'] + u'application/atom+xml;type=entry' + >>> templates['typebyid']['template'] + u'http://localhost:8080/alfresco/s/cmis/type/{id}' + """ + + if self._uriTemplates == {}: + + if self.xmlDoc == None: + self.reload() + + uriTemplateElements = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'uritemplate') + + for uriTemplateElement in uriTemplateElements: + template = None + templType = None + mediatype = None + + for node in [e for e in uriTemplateElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + if node.localName == 'template': + template = node.childNodes[0].data + elif node.localName == 'type': + templType = node.childNodes[0].data + elif node.localName == 'mediatype': + mediatype = node.childNodes[0].data + + self._uriTemplates[templType] = UriTemplate(template, + templType, + mediatype) + + return self._uriTemplates + + def getCollection(self, collectionType, **kwargs): + + """ + Returns a list of objects returned for the specified collection. + + If the query collection is requested, an exception will be raised. + That collection isn't meant to be retrieved. + + If the types collection is specified, the method returns the result of + `getTypeDefinitions` and ignores any optional params passed in. + + >>> from cmislib.model import TYPES_COLL + >>> types = repo.getCollection(TYPES_COLL) + >>> len(types) + 4 + >>> types[0].getTypeId() + u'cmis:folder' + + Otherwise, the collection URL is invoked, and a :class:`ResultSet` is + returned. + + >>> from cmislib.model import CHECKED_OUT_COLL + >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) + >>> len(resultSet.getResults()) + 1 + """ + + if collectionType == QUERY_COLL: + raise NotSupportedException + elif collectionType == TYPES_COLL: + return self.getTypeDefinitions() + + result = self._cmisClient.binding.get(self.getCollectionLink(collectionType).encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return AtomPubResultSet(self._cmisClient, self, result) + + def getCollectionLink(self, collectionType): + + """ + Returns the link HREF from the specified collectionType + ('checkedout', for example). + + >>> from cmislib.model import CHECKED_OUT_COLL + >>> repo.getCollectionLink(CHECKED_OUT_COLL) + u'http://localhost:8080/alfresco/s/cmis/checkedout' + + """ + + collectionElements = self.xmlDoc.getElementsByTagNameNS(APP_NS, 'collection') + for collectionElement in collectionElements: + link = collectionElement.attributes['href'].value + for node in [e for e in collectionElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + if node.localName == 'collectionType': + if node.childNodes[0].data == collectionType: + return link + + def _getQueryXmlDoc(self, query, **kwargs): + + """ + Utility method that knows how to build CMIS query xml around the + specified query statement. + """ + + cmisXmlDoc = minidom.Document() + queryElement = cmisXmlDoc.createElementNS(CMIS_NS, "query") + queryElement.setAttribute('xmlns', CMIS_NS) + cmisXmlDoc.appendChild(queryElement) + + statementElement = cmisXmlDoc.createElementNS(CMIS_NS, "statement") + cdataSection = cmisXmlDoc.createCDATASection(query) + statementElement.appendChild(cdataSection) + queryElement.appendChild(statementElement) + + for (k, v) in kwargs.items(): + optionElement = cmisXmlDoc.createElementNS(CMIS_NS, k) + optionText = cmisXmlDoc.createTextNode(v) + optionElement.appendChild(optionText) + queryElement.appendChild(optionElement) + + return cmisXmlDoc + + capabilities = property(getCapabilities) + id = property(getRepositoryId) + info = property(getRepositoryInfo) + name = property(getRepositoryName) + rootFolder = property(getRootFolder) + permissionDefinitions = property(getPermissionDefinitions) + permissionMap = property(getPermissionMap) + propagation = property(getPropagation) + supportedPermissions = property(getSupportedPermissions) + + +class AtomPubResultSet(ResultSet): + + """ + Represents a paged result set. In CMIS, this is most often an Atom feed. + """ + + def __init__(self, cmisClient, repository, xmlDoc): + ''' Constructor ''' + self._cmisClient = cmisClient + self._repository = repository + self._xmlDoc = xmlDoc + self._results = [] + self.logger = logging.getLogger('cmislib.model.ResultSet') + self.logger.info('Creating an instance of ResultSet') + + def __iter__(self): + ''' Iterator for the result set ''' + return iter(self.getResults()) + + def __getitem__(self, index): + ''' Getter for the result set ''' + return self.getResults()[index] + + def __len__(self): + ''' Len method for the result set ''' + return len(self.getResults()) + + def _getLink(self, rel): + ''' + Returns the link found in the feed's XML for the specified rel. + ''' + linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + def _getPageResults(self, rel): + ''' + Given a specified rel, does a get using that link (if one exists) + and then converts the resulting XML into a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. + + The results are kept around to facilitate repeated calls without moving + the cursor. + ''' + link = self._getLink(rel) + if link: + result = self._cmisClient.binding.get(link.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + + # return the result + self._xmlDoc = result + self._results = [] + return self.getResults() + + def reload(self): + + ''' + Re-invokes the self link for the current set of results. + + >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) + >>> resultSet.reload() + + ''' + + self.logger.debug('Reload called on result set') + self._getPageResults(SELF_REL) + + def getResults(self): + + ''' + Returns the results that were fetched and cached by the get*Page call. + + >>> resultSet = repo.getCheckedOutDocs() + >>> resultSet.hasNext() + False + >>> for result in resultSet.getResults(): + ... result + ... + + ''' + 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 + ... + + ''' + + 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 + ... + + ''' + + 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 + ... + + ''' + + 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 + ... + + ''' + + 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() + + >>> pwc = doc.getPrivateWorkingCopy() + >>> pwc.getTitle() + u'sample-b (Working Copy).pdf' + """ + + # reloading the document just to make sure we've got the latest + # and greatest PWC ID + self.reload() + pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId'] + if pwcDocId: + return self._repository.getObject(pwcDocId) + + def isCheckedOut(self): + + """ + Returns true if the document is checked out. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + # reloading the document just to make sure we've got the latest + # and greatest checked out prop + self.reload() + return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut']) + + def getCheckedOutBy(self): + + """ + Returns the ID who currently has the document checked out. + >>> pwc = doc.checkout() + >>> pwc.getCheckedOutBy() + u'admin' + """ + + # reloading the document just to make sure we've got the latest + # and greatest checked out prop + self.reload() + return self.getProperties()['cmis:versionSeriesCheckedOutBy'] + + def checkin(self, checkinComment=None, **kwargs): + + """ + Checks in this :class:`Document` which must be a private + working copy (PWC). + + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + >>> pwc.checkin() + + >>> doc.isCheckedOut() + False + + The following optional arguments are supported: + - major + - properties + - contentStream + - policies + - addACEs + - removeACEs + """ + + # Add checkin to kwargs and checkinComment, if it exists + kwargs['checkin'] = 'true' + kwargs['checkinComment'] = checkinComment + + # Build an empty ATOM entry + entryXmlDoc = getEmptyXmlDoc() + + # Get the self link + # Do a PUT of the empty ATOM to the self link + url = self._getSelfLink() + result = self._cmisClient.binding.put(url.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + entryXmlDoc.toxml(encoding='utf-8'), + ATOM_XML_TYPE, + **kwargs) + + return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result) + + def getLatestVersion(self, **kwargs): + + """ + Returns a :class:`Document` object representing the latest version in + the version series. + + The following optional arguments are supported: + - major + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + + >>> latestDoc = doc.getLatestVersion() + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='false') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='true') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.0' + """ + + doc = None + if kwargs.has_key('major') and kwargs['major'] == 'true': + doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor') + else: + doc = self._repository.getObject(self.getObjectId(), returnVersion='latest') + + return doc + + def getPropertiesOfLatestVersion(self, **kwargs): + + """ + Like :class:`^CmisObject.getProperties`, returns a dict of properties + from the latest version of this object in the version series. + + The optional major and filter arguments are supported. + """ + + latestDoc = self.getLatestVersion(**kwargs) + + return latestDoc.getProperties() + + def getAllVersions(self, **kwargs): + + """ + Returns a :class:`ResultSet` of document objects for the entire + version history of this object, including any PWC's. + + The optional filter and includeAllowableActions are + supported. + """ + + # get the version history link + versionsUrl = self._getLink(VERSION_HISTORY_REL) + + # invoke the URL + result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'), + 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') + + 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'] != 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'] != None): + self.logger.debug('Change token present, adding it to args') + args = {"changeToken": self.properties['cmis:changeToken']} + + # delete the content stream + result = self._cmisClient.binding.delete(srcUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **args) + + def getRenditions(self): + + """ + Returns an array of :class:`Rendition` objects. The repository + must support the Renditions capability. + + The following optional arguments are not currently supported: + - renditionFilter + - maxItems + - skipCount + """ + + # if Renditions capability is None, return notsupported + if self._repository.getCapabilities()['Renditions']: + pass + else: + raise NotSupportedException + + if self.xmlDoc == None: + self.reload() + + linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + renditions = [] + for linkElement in linkElements: + + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == RENDITION_REL: + renditions.append(AtomPubRendition(linkElement)) + return renditions + + checkedOut = property(isCheckedOut) + + def getPaths(self): + """ + Returns the Document's paths by asking for the parents with the + includeRelativePathSegment flag set to true, then concats the value + of cmis:path with the relativePathSegment. + """ + # get the appropriate 'up' link + parentUrl = self._getLink(UP_REL) + + if parentUrl == None: + raise NotSupportedException('Root folder does not support getObjectParents') + + # invoke the URL + result = self._cmisClient.binding.get(parentUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + filter='cmis:path', + includeRelativePathSegment=True) + + 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) + + >>> 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) + + >>> 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 != 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, 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 + result = self._cmisClient.binding.post(postUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + cmisObject.xmlDoc.toxml(encoding='utf-8'), + ATOM_XML_ENTRY_TYPE, + **kwargs) + + def removeObject(self, cmisObject): + + """ + Removes the specified object from this folder. The repository must + support unfiling for this to work. + """ + + if not self._repository.getCapabilities()['Unfiling']: + raise NotSupportedException('This repository does not support unfiling') + + postUrl = self._repository.getCollectionLink(UNFILED_COLL) + + args = {"removeFrom": self.getObjectId()} + + # post the Atom entry to the unfiled collection + result = self._cmisClient.binding.post(postUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + cmisObject.xmlDoc.toxml(encoding='utf-8'), + ATOM_XML_ENTRY_TYPE, + **args) + + def getPaths(self): + """ + Returns the paths as a list of strings. The spec says folders cannot + be multi-filed, so this should always be one value. We return a list + to be symmetric with the same method in :class:`Document`. + """ + return [self.properties['cmis:path']] + + +class AtomPubRelationship(AtomPubCmisObject): + + """ + Defines a relationship object between two :class:`CmisObjects` objects + """ + + def getSourceId(self): + + """ + Returns the :class:`CmisId` on the source side of the relationship. + """ + + if self.xmlDoc == None: + self.reload() + props = self.getProperties() + return AtomPubCmisId(props['cmis:sourceId']) + + def getTargetId(self): + + """ + Returns the :class:`CmisId` on the target side of the relationship. + """ + + if self.xmlDoc == None: + self.reload() + props = self.getProperties() + return AtomPubCmisId(props['cmis:targetId']) + + def getSource(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the source side of the relationship. + """ + + sourceId = self.getSourceId() + return getSpecializedObject(self._repository.getObject(sourceId)) + + def getTarget(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the target side of the relationship. + """ + + targetId = self.getTargetId() + return getSpecializedObject(self._repository.getObject(targetId)) + + sourceId = property(getSourceId) + targetId = property(getTargetId) + source = property(getSource) + target = property(getTarget) + + +class AtomPubPolicy(AtomPubCmisObject): + + """ + An arbirary object that can 'applied' to objects that the + repository identifies as being 'controllable'. + """ + + pass + + +class AtomPubObjectType(ObjectType): + + """ + Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'. + Contains metadata about the type. + """ + + def __init__(self, cmisClient, repository, typeId=None, xmlDoc=None): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._kwargs = None + self._typeId = typeId + self.xmlDoc = xmlDoc + self.logger = logging.getLogger('cmislib.model.ObjectType') + self.logger.info('Creating an instance of ObjectType') + + def __str__(self): + """To string""" + return self.getTypeId() + + def getTypeId(self): + + """ + Returns the type ID for this object. + + >>> docType = repo.getTypeDefinition('cmis:document') + >>> docType.getTypeId() + 'cmis:document' + """ + + if self._typeId == None: + if self.xmlDoc == None: + self.reload() + self._typeId = CmisId(self._getElementValue(CMIS_NS, 'id')) + + return self._typeId + + def _getElementValue(self, namespace, elementName): + + """ + Helper method to retrieve child element values from type XML. + """ + + if self.xmlDoc == None: + self.reload() + #typeEls = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'type') + #assert len(typeEls) == 1, "Expected to find exactly one type element but instead found %d" % len(typeEls) + #typeEl = typeEls[0] + typeEl = None + for e in self.xmlDoc.childNodes: + if e.nodeType == e.ELEMENT_NODE and e.localName == "type": + typeEl = e + break + + assert typeEl, "Expected to find one child element named type" + els = typeEl.getElementsByTagNameNS(namespace, elementName) + if len(els) >= 1: + el = els[0] + if el and len(el.childNodes) >= 1: + return el.childNodes[0].data + + def getLocalName(self): + """Getter for cmis:localName""" + return self._getElementValue(CMIS_NS, 'localName') + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + return self._getElementValue(CMIS_NS, 'localNamespace') + + def getDisplayName(self): + """Getter for cmis:displayName""" + return self._getElementValue(CMIS_NS, 'displayName') + + def getQueryName(self): + """Getter for cmis:queryName""" + return self._getElementValue(CMIS_NS, 'queryName') + + def getDescription(self): + """Getter for cmis:description""" + return self._getElementValue(CMIS_NS, 'description') + + def getBaseId(self): + """Getter for cmis:baseId""" + return AtomPubCmisId(self._getElementValue(CMIS_NS, 'baseId')) + + def isCreatable(self): + """Getter for cmis:creatable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'creatable')) + + def isFileable(self): + """Getter for cmis:fileable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'fileable')) + + def isQueryable(self): + """Getter for cmis:queryable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) + + def isFulltextIndexed(self): + """Getter for cmis:fulltextIndexed""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'fulltextIndexed')) + + def isIncludedInSupertypeQuery(self): + """Getter for cmis:includedInSupertypeQuery""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'includedInSupertypeQuery')) + + def isControllablePolicy(self): + """Getter for cmis:controllablePolicy""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'controllablePolicy')) + + def isControllableACL(self): + """Getter for cmis:controllableACL""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'controllableACL')) + + def getLink(self, rel, linkType): + + """ + Gets the HREF for the link element with the specified rel and linkType. + + >>> from cmislib.model import ATOM_XML_FEED_TYPE + >>> docType.getLink('down', ATOM_XML_FEED_TYPE) + u'http://localhost:8080/alfresco/s/cmis/type/cmis:document/children' + """ + + linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + + if linkElement.attributes.has_key('rel') and linkElement.attributes.has_key('type'): + relAttr = linkElement.attributes['rel'].value + typeAttr = linkElement.attributes['type'].value + + if relAttr == rel and linkType.match(typeAttr): + return linkElement.attributes['href'].value + + def getProperties(self): + + """ + Returns a list of :class:`Property` objects representing each property + defined for this type. + + >>> objType = repo.getTypeDefinition('cmis:relationship') + >>> for prop in objType.properties: + ... print 'Id:%s' % prop.id + ... print 'Cardinality:%s' % prop.cardinality + ... print 'Description:%s' % prop.description + ... print 'Display name:%s' % prop.displayName + ... print 'Local name:%s' % prop.localName + ... print 'Local namespace:%s' % prop.localNamespace + ... print 'Property type:%s' % prop.propertyType + ... print 'Query name:%s' % prop.queryName + ... print 'Updatability:%s' % prop.updatability + ... print 'Inherited:%s' % prop.inherited + ... print 'Orderable:%s' % prop.orderable + ... print 'Queryable:%s' % prop.queryable + ... print 'Required:%s' % prop.required + ... print 'Open choice:%s' % prop.openChoice + """ + + if self.xmlDoc == None: + self.reload(includePropertyDefinitions='true') + # Currently, property defs don't have an enclosing element. And, the + # element name varies depending on type. Until that changes, I'm going + # to find all elements unique to a prop, then grab its parent node. + propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') + if len(propTypeElements) <= 0: + self.reload(includePropertyDefinitions='true') + propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') + assert len(propTypeElements) > 0, 'Could not retrieve object type property definitions' + props = {} + for typeEl in propTypeElements: + prop = AtomPubProperty(typeEl.parentNode) + props[prop.id] = prop + return props + + def reload(self, **kwargs): + """ + This method will reload the object's data from the CMIS service. + """ + if kwargs: + if self._kwargs: + self._kwargs.update(kwargs) + else: + self._kwargs = kwargs + templates = self._repository.getUriTemplates() + template = templates['typebyid']['template'] + params = {'{id}': self._typeId} + byTypeIdUrl = multiple_replace(params, template) + result = self._cmisClient.binding.get(byTypeIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # instantiate CmisObject objects with the results and return the list + entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') + assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byTypeIdUrl + self.xmlDoc = entryElements[0] + + id = property(getTypeId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + baseId = property(getBaseId) + creatable = property(isCreatable) + fileable = property(isFileable) + queryable = property(isQueryable) + fulltextIndexed = property(isFulltextIndexed) + includedInSupertypeQuery = property(isIncludedInSupertypeQuery) + controllablePolicy = property(isControllablePolicy) + controllableACL = property(isControllableACL) + properties = property(getProperties) + + +class AtomPubProperty(Property): + + """ + This class represents an attribute or property definition of an object + type. + """ + + def __init__(self, propNode): + """Constructor""" + self.xmlDoc = propNode + self.logger = logging.getLogger('cmislib.model.Property') + self.logger.info('Creating an instance of Property') + + def __str__(self): + """To string""" + return self.getId() + + def _getElementValue(self, namespace, elementName): + + """ + Utility method for retrieving element values from the object type XML. + """ + + els = self.xmlDoc.getElementsByTagNameNS(namespace, elementName) + if len(els) >= 1: + el = els[0] + if el and len(el.childNodes) >= 1: + return el.childNodes[0].data + + def getId(self): + """Getter for cmis:id""" + return self._getElementValue(CMIS_NS, 'id') + + def getLocalName(self): + """Getter for cmis:localName""" + return self._getElementValue(CMIS_NS, 'localName') + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + return self._getElementValue(CMIS_NS, 'localNamespace') + + def getDisplayName(self): + """Getter for cmis:displayName""" + return self._getElementValue(CMIS_NS, 'displayName') + + def getQueryName(self): + """Getter for cmis:queryName""" + return self._getElementValue(CMIS_NS, 'queryName') + + def getDescription(self): + """Getter for cmis:description""" + return self._getElementValue(CMIS_NS, 'description') + + def getPropertyType(self): + """Getter for cmis:propertyType""" + return self._getElementValue(CMIS_NS, 'propertyType') + + def getCardinality(self): + """Getter for cmis:cardinality""" + return self._getElementValue(CMIS_NS, 'cardinality') + + def getUpdatability(self): + """Getter for cmis:updatability""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'updatability')) + + def isInherited(self): + """Getter for cmis:inherited""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'inherited')) + + def isRequired(self): + """Getter for cmis:required""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'required')) + + def isQueryable(self): + """Getter for cmis:queryable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) + + def isOrderable(self): + """Getter for cmis:orderable""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'orderable')) + + def isOpenChoice(self): + """Getter for cmis:openChoice""" + return parseBoolValue(self._getElementValue(CMIS_NS, 'openChoice')) + + id = property(getId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + propertyType = property(getPropertyType) + cardinality = property(getCardinality) + updatability = property(getUpdatability) + inherited = property(isInherited) + required = property(isRequired) + queryable = property(isQueryable) + orderable = property(isOrderable) + openChoice = property(isOpenChoice) + + +class AtomPubACL(ACL): + + """ + Represents the Access Control List for an object. + """ + + def __init__(self, aceList=None, xmlDoc=None): + + """ + Constructor. Pass in either a list of :class:`ACE` objects or the XML + representation of the ACL. If you have only one ACE, don't worry about + the list--the constructor will convert it to a list for you. + """ + + if aceList: + self._entries = aceList + else: + self._entries = {} + if xmlDoc: + self._xmlDoc = xmlDoc + self._entries = self._getEntriesFromXml() + else: + self._xmlDoc = None + + self.logger = logging.getLogger('cmislib.model.ACL') + self.logger.info('Creating an instance of ACL') + + def addEntry(self, principalId, access, direct): + + """ + Adds an :class:`ACE` entry to the ACL. + + >>> acl = folder.getACL() + >>> acl.addEntry('jpotts', 'cmis:read', 'true') + >>> acl.addEntry('jsmith', 'cmis:write', 'true') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + """ + ace = AtomPubACE(principalId, access, direct) + self._entries[ace.principalId] = ace + + def removeEntry(self, principalId): + + """ + Removes the :class:`ACE` entry given a specific principalId. + + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + >>> acl.removeEntry('jsmith') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } + """ + + if self._entries.has_key(principalId): + del(self._entries[principalId]) + + def clearEntries(self): + + """ + Clears all :class:`ACE` entries from the ACL and removes the internal + XML representation of the ACL. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> acl.entries + {'jpotts': , 'jsmith': } + >>> acl.getXmlDoc() + + >>> acl.clearEntries() + >>> acl.entries + >>> acl.getXmlDoc() + """ + + self._entries.clear() + self._xmlDoc = None + + def getEntries(self): + + """ + Returns a dictionary of :class:`ACE` objects for each Access Control + Entry in the ACL. The key value is the ACE principalid. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> for ace in acl.entries.values(): + ... print 'principal:%s has the following permissions...' % ace.principalId + ... for perm in ace.permissions: + ... print perm + ... + principal:jpotts has the following permissions... + cmis:write + principal:jsmith has the following permissions... + cmis:write + """ + + if self._entries: + return self._entries + else: + if self._xmlDoc: + # parse XML doc and build entry list + self._entries = self._getEntriesFromXml() + # then return it + return self._entries + + def _getEntriesFromXml(self): + + """ + Helper method for getting the :class:`ACE` entries from an XML + representation of the ACL. + """ + + if not self._xmlDoc: + return + result = {} + # first child is the root node, cmis:acl + for e in self._xmlDoc.childNodes[0].childNodes: + if e.localName == 'permission': + # grab the principal/principalId element value + prinEl = e.getElementsByTagNameNS(CMIS_NS, 'principal')[0] + if prinEl and prinEl.childNodes: + prinIdEl = prinEl.getElementsByTagNameNS(CMIS_NS, 'principalId')[0] + if prinIdEl and prinIdEl.childNodes: + principalId = prinIdEl.childNodes[0].data + # grab the permission values + permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') + perms = [] + for permEl in permEls: + if permEl and permEl.childNodes: + perms.append(permEl.childNodes[0].data) + # grab the direct value + dirEl = e.getElementsByTagNameNS(CMIS_NS, 'direct')[0] + if dirEl and dirEl.childNodes: + direct = dirEl.childNodes[0].data + # create an ACE + if (len(perms) > 0): + ace = AtomPubACE(principalId, perms, direct) + # append it to the dictionary + result[principalId] = ace + return result + + def getXmlDoc(self): + + """ + This method rebuilds the local XML representation of the ACL based on + the :class:`ACE` objects in the entries list and returns the resulting + XML Document. + """ + + if not self.getEntries(): + return + + xmlDoc = minidom.Document() + aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl') + aclEl.setAttribute('xmlns:cmis', CMIS_NS) + for ace in self.getEntries().values(): + permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') + #principalId + prinEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principal') + prinIdEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principalId') + prinIdElText = xmlDoc.createTextNode(ace.principalId) + prinIdEl.appendChild(prinIdElText) + prinEl.appendChild(prinIdEl) + permEl.appendChild(prinEl) + #direct + directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct') + directElText = xmlDoc.createTextNode(ace.direct) + directEl.appendChild(directElText) + permEl.appendChild(directEl) + #permissions + for perm in ace.permissions: + permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') + permItemElText = xmlDoc.createTextNode(perm) + permItemEl.appendChild(permItemElText) + permEl.appendChild(permItemEl) + aclEl.appendChild(permEl) + xmlDoc.appendChild(aclEl) + self._xmlDoc = xmlDoc + return self._xmlDoc + + entries = property(getEntries) + + +class AtomPubACE(ACE): + + """ + Represents an individual Access Control Entry. + """ + + def __init__(self, principalId=None, permissions=None, direct=None): + """Constructor""" + self._principalId = principalId + if permissions: + if isinstance(permissions, str): + self._permissions = [permissions] + else: + self._permissions = permissions + self._direct = direct + + self.logger = logging.getLogger('cmislib.model.ACE') + self.logger.info('Creating an instance of ACE') + + @property + def principalId(self): + """Getter for principalId""" + return self._principalId + + @property + def direct(self): + """Getter for direct""" + return self._direct + + @property + def permissions(self): + """Getter for permissions""" + return self._permissions + + +class AtomPubChangeEntry(ChangeEntry): + + """ + Represents a change log entry. Retrieve a list of change entries via + :meth:`Repository.getContentChanges`. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + """ + + def __init__(self, cmisClient, repository, xmlDoc): + """Constructor""" + self._cmisClient = cmisClient + self._repository = repository + self._xmlDoc = xmlDoc + self._properties = {} + self._objectId = None + self._changeEntryId = None + self._changeType = None + self._changeTime = None + self.logger = logging.getLogger('cmislib.model.ChangeEntry') + self.logger.info('Creating an instance of ChangeEntry') + + def getId(self): + """ + Returns the unique ID of the change entry. + """ + if self._changeEntryId == None: + self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data + return self._changeEntryId + + def getObjectId(self): + """ + Returns the object ID of the object that changed. + """ + if self._objectId == None: + props = self.getProperties() + self._objectId = CmisId(props['cmis:objectId']) + return self._objectId + + def getChangeType(self): + + """ + Returns the type of change that occurred. The resulting value must be + one of: + + - created + - updated + - deleted + - security + """ + + if self._changeType == None: + self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data + return self._changeType + + def getACL(self): + + """ + Gets the :class:`ACL` object that is included with this Change Entry. + """ + + # if you call getContentChanges with includeACL=true, you will get a + # cmis:ACL entry. change entries don't appear to have a self URL so + # instead of doing a reload with includeACL set to true, we'll either + # see if the XML already has an ACL element and instantiate an ACL with + # it, or we'll get the ACL_REL link, invoke that, and return the result + if not self._repository.getCapabilities()['ACL']: + return + aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl') + aclUrl = self._getLink(ACL_REL) + if (len(aclEls) == 1): + return AtomPubACL(self._cmisClient, self._repository, aclEls[0]) + elif aclUrl: + result = self._cmisClient.binding.get(aclUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + return AtomPubACL(xmlDoc=result) + + def getChangeTime(self): + + """ + Returns a datetime object representing the time the change occurred. + """ + + if self._changeTime == None: + self._changeTime = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeTime')[0].firstChild.data + return parseDateTimeValue(self._changeTime) + + def getProperties(self): + + """ + Returns the properties of the change entry. Note that depending on the + capabilities of the repository ("capabilityChanges") the list may not + include the actual property values that changed. + """ + + if self._properties == {}: + propertiesElement = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] + for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + propertyName = node.attributes['propertyDefinitionId'].value + if node.childNodes and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: + propertyValue = parsePropValue( + node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data, + node.localName) + else: + propertyValue = None + self._properties[propertyName] = propertyValue + return self._properties + + def _getLink(self, rel): + + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + + linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + + """ + Represents a change log entry. Retrieve a list of change entries via + :meth:`Repository.getContentChanges`. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + """ + + def __init__(self, cmisClient, repository, xmlDoc): + """Constructor""" + self._cmisClient = cmisClient + self._repository = repository + self._xmlDoc = xmlDoc + self._properties = {} + self._objectId = None + self._changeEntryId = None + self._changeType = None + self._changeTime = None + self.logger = logging.getLogger('cmislib.model.ChangeEntry') + self.logger.info('Creating an instance of ChangeEntry') + + def getId(self): + """ + Returns the unique ID of the change entry. + """ + if self._changeEntryId == None: + self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data + return self._changeEntryId + + def getObjectId(self): + """ + Returns the object ID of the object that changed. + """ + if self._objectId == None: + props = self.getProperties() + self._objectId = CmisId(props['cmis:objectId']) + return self._objectId + + def getChangeType(self): + + """ + Returns the type of change that occurred. The resulting value must be + one of: + + - created + - updated + - deleted + - security + """ + + if self._changeType == None: + self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data + return self._changeType + + def getACL(self): + + """ + Gets the :class:`ACL` object that is included with this Change Entry. + """ + + # if you call getContentChanges with includeACL=true, you will get a + # cmis:ACL entry. change entries don't appear to have a self URL so + # instead of doing a reload with includeACL set to true, we'll either + # see if the XML already has an ACL element and instantiate an ACL with + # it, or we'll get the ACL_REL link, invoke that, and return the result + if not self._repository.getCapabilities()['ACL']: + return + aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl') + aclUrl = self._getLink(ACL_REL) + if (len(aclEls) == 1): + return AtomPubACL(self._cmisClient, self._repository, aclEls[0]) + elif aclUrl: + result = self._cmisClient.binding.get(aclUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + return AtomPubACL(xmlDoc=result) + + def getChangeTime(self): + + """ + Returns a datetime object representing the time the change occurred. + """ + + if self._changeTime == None: + self._changeTime = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeTime')[0].firstChild.data + return parseDateTimeValue(self._changeTime) + + def getProperties(self): + + """ + Returns the properties of the change entry. Note that depending on the + capabilities of the repository ("capabilityChanges") the list may not + include the actual property values that changed. + """ + + if self._properties == {}: + propertiesElement = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] + for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: + propertyName = node.attributes['propertyDefinitionId'].value + if node.childNodes and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ + node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: + propertyValue = parsePropValue( + node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data, + node.localName) + else: + propertyValue = None + self._properties[propertyName] = propertyValue + return self._properties + + def _getLink(self, rel): + + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + + linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') + + for linkElement in linkElements: + if linkElement.attributes.has_key('rel'): + relAttr = linkElement.attributes['rel'].value + + if relAttr == rel: + return linkElement.attributes['href'].value + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + +class AtomPubChangeEntryResultSet(AtomPubResultSet): + + """ + A specialized type of :class:`ResultSet` that knows how to instantiate + :class:`ChangeEntry` objects. The parent class assumes children of + :class:`CmisObject` which doesn't work for ChangeEntries. + """ + + def __iter__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return iter(self.getResults()) + + def __getitem__(self, index): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return self.getResults()[index] + + def __len__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return len(self.getResults()) + + def getResults(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + if self._results: + return self._results + + if self._xmlDoc: + entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') + entries = [] + for entryElement in entryElements: + changeEntry = AtomPubChangeEntry(self._cmisClient, self._repository, entryElement) + entries.append(changeEntry) + + self._results = entries + + return self._results + + +class AtomPubRendition(Rendition): + + """ + This class represents a Rendition. + """ + + def __init__(self, propNode): + """Constructor""" + self.xmlDoc = propNode + self.logger = logging.getLogger('cmislib.model.Rendition') + self.logger.info('Creating an instance of Rendition') + + def __str__(self): + """To string""" + return self.getStreamId() + + def getStreamId(self): + """Getter for the rendition's stream ID""" + if self.xmlDoc.attributes.has_key('streamId'): + return self.xmlDoc.attributes['streamId'].value + + def getMimeType(self): + """Getter for the rendition's mime type""" + if self.xmlDoc.attributes.has_key('type'): + return self.xmlDoc.attributes['type'].value + + def getLength(self): + """Getter for the renditions's length""" + if self.xmlDoc.attributes.has_key('length'): + return self.xmlDoc.attributes['length'].value + + def getTitle(self): + """Getter for the renditions's title""" + if self.xmlDoc.attributes.has_key('title'): + return self.xmlDoc.attributes['title'].value + + def getKind(self): + """Getter for the renditions's kind""" + if self.xmlDoc.hasAttributeNS(CMISRA_NS, 'renditionKind'): + return self.xmlDoc.getAttributeNS(CMISRA_NS, 'renditionKind') + + def getHeight(self): + """Getter for the renditions's height""" + if self.xmlDoc.attributes.has_key('height'): + return self.xmlDoc.attributes['height'].value + + def getWidth(self): + """Getter for the renditions's width""" + if self.xmlDoc.attributes.has_key('width'): + return self.xmlDoc.attributes['width'].value + + def getHref(self): + """Getter for the renditions's href""" + if self.xmlDoc.attributes.has_key('href'): + return self.xmlDoc.attributes['href'].value + + def getRenditionDocumentId(self): + """Getter for the renditions's width""" + if self.xmlDoc.attributes.has_key('renditionDocumentId'): + return self.xmlDoc.attributes['renditionDocumentId'].value + + streamId = property(getStreamId) + mimeType = property(getMimeType) + length = property(getLength) + title = property(getTitle) + kind = property(getKind) + height = property(getHeight) + width = property(getWidth) + href = property(getHref) + renditionDocumentId = property(getRenditionDocumentId) + + +class AtomPubCmisId(CmisId): + + """ + This is a marker class to be used for Strings that are used as CMIS ID's. + Making the objects instances of this class makes it easier to create the + Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of + cmis:propertyString. + """ + + pass + +def getSpecializedObject(obj, **kwargs): + + """ + Returns an instance of the appropriate :class:`CmisObject` class or one + of its child types depending on the specified baseType. + """ + + moduleLogger.debug('Inside getSpecializedObject') + + if 'cmis:baseTypeId' in obj.getProperties(): + baseType = obj.getProperties()['cmis:baseTypeId'] + if baseType == 'cmis:folder': + return AtomPubFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) + if baseType == 'cmis:document': + return AtomPubDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) + if baseType == 'cmis:relationship': + return AtomPubRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) + if baseType == 'cmis:policy': + return AtomPubPolicy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) + + # if the base type ID wasn't found in the props (this can happen when + # someone runs a query that doesn't select * or doesn't individually + # specify baseTypeId) or if the type isn't one of the known base + # types, give the object back + return obj + +def getEntryXmlDoc(repo=None, objectTypeId=None, properties=None, contentFile=None, + contentType=None, contentEncoding=None): + + """ + Internal helper method that knows how to build an Atom entry based + on the properties and, optionally, the contentFile provided. + """ + + moduleLogger.debug('Inside getEntryXmlDoc') + + entryXmlDoc = minidom.Document() + entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") + entryElement.setAttribute('xmlns', ATOM_NS) + entryElement.setAttribute('xmlns:app', APP_NS) + entryElement.setAttribute('xmlns:cmisra', CMISRA_NS) + entryXmlDoc.appendChild(entryElement) + + # if there is a File, encode it and add it to the XML + if contentFile: + mimetype = contentType + encoding = contentEncoding + + # need to determine the mime type + if not mimetype and hasattr(contentFile, 'name'): + mimetype, encoding = mimetypes.guess_type(contentFile.name) + + if not mimetype: + mimetype = 'application/binary' + + if not encoding: + encoding = 'utf8' + + # This used to be ATOM_NS content but there is some debate among + # vendors whether the ATOM_NS content must always be base64 + # encoded. The spec does mandate that CMISRA_NS content be encoded + # and that element takes precedence over ATOM_NS content if it is + # present, so it seems reasonable to use CMIS_RA content for now + # and encode everything. + + fileData = contentFile.read().encode("base64") + mediaElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:mediatype') + mediaElementText = entryXmlDoc.createTextNode(mimetype) + mediaElement.appendChild(mediaElementText) + base64Element = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:base64') + base64ElementText = entryXmlDoc.createTextNode(fileData) + base64Element.appendChild(base64ElementText) + contentElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:content') + contentElement.appendChild(mediaElement) + contentElement.appendChild(base64Element) + entryElement.appendChild(contentElement) + + objectElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:object') + objectElement.setAttribute('xmlns:cmis', CMIS_NS) + entryElement.appendChild(objectElement) + + if properties: + # a name is required for most things, but not for a checkout + if properties.has_key('cmis:name'): + titleElement = entryXmlDoc.createElementNS(ATOM_NS, "title") + titleText = entryXmlDoc.createTextNode(properties['cmis:name']) + titleElement.appendChild(titleText) + entryElement.appendChild(titleElement) + + propsElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:properties') + objectElement.appendChild(propsElement) + + typeDef = None + for propName, propValue in properties.items(): + """ + the name of the element here is significant: it includes the + data type. I should be able to figure out the right type based + on the actual type of the object passed in. + + I could do a lookup to the type definition, but that doesn't + seem worth the performance hit + """ + if (propValue == None or (type(propValue) == list and propValue[0] == None)): + # grab the prop type from the typeDef + if (typeDef == None): + moduleLogger.debug('Looking up type def for: %s' % objectTypeId) + typeDef = repo.getTypeDefinition(objectTypeId) + #TODO what to do if type not found + propType = typeDef.properties[propName].propertyType + elif type(propValue) == list: + propType = type(propValue[0]) + else: + propType = type(propValue) + + propElementName, propValueStrList = getElementNameAndValues(propType, propName, propValue, type(propValue) == list) + + propElement = entryXmlDoc.createElementNS(CMIS_NS, propElementName) + propElement.setAttribute('propertyDefinitionId', propName) + for val in propValueStrList: + if val == None: + continue + valElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:value') + valText = entryXmlDoc.createTextNode(val) + valElement.appendChild(valText) + propElement.appendChild(valElement) + propsElement.appendChild(propElement) + + return entryXmlDoc + +def getElementNameAndValues(propType, propName, propValue, isList=False): + + """ + For a given property type, property name, and property value, this function + returns the appropriate CMIS Atom entry element name and value list. + """ + + moduleLogger.debug('Inside getElementNameAndValues') + moduleLogger.debug('propType:%s propName:%s isList:%s' % (propType, propName, isList)) + if (propType == 'id' or propType == CmisId): + propElementName = 'cmis:propertyId' + if isList: + propValueStrList = [] + for val in propValue: + propValueStrList.append(val) + else: + propValueStrList = [propValue] + elif (propType == 'string' or propType == str): + propElementName = 'cmis:propertyString' + if isList: + propValueStrList = [] + for val in propValue: + propValueStrList.append(val) + else: + propValueStrList = [propValue] + elif (propType == 'datetime' or propType == datetime.datetime): + propElementName = 'cmis:propertyDateTime' + if isList: + propValueStrList = [] + for val in propValue: + if val != None: + propValueStrList.append(val.isoformat()) + else: + propValueStrList.append(val) + else: + if propValue != None: + propValueStrList = [propValue.isoformat()] + else: + propValueStrList = [propValue] + elif (propType == 'boolean' or propType == bool): + propElementName = 'cmis:propertyBoolean' + if isList: + propValueStrList = [] + for val in propValue: + if val != None: + propValueStrList.append(six.text_type(val).lower()) + else: + propValueStrList.append(val) + else: + if propValue != None: + propValueStrList = [six.text_type(propValue).lower()] + else: + propValueStrList = [propValue] + elif (propType == 'integer' or propType == int): + propElementName = 'cmis:propertyInteger' + if isList: + propValueStrList = [] + for val in propValue: + if val != None: + propValueStrList.append(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue != None: + propValueStrList = [six.text_type(propValue)] + else: + propValueStrList = [propValue] + elif (propType == 'decimal' or propType == float): + propElementName = 'cmis:propertyDecimal' + if isList: + propValueStrList = [] + for val in propValue: + if val != None: + propValueStrList.append(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue != None: + propValueStrList = [six.text_type(propValue)] + else: + propValueStrList = [propValue] + else: + propElementName = 'cmis:propertyString' + if isList: + propValueStrList = [] + for val in propValue: + if val != None: + propValueStrList.append(six.text_type(val)) + else: + propValueStrList.append(val) + else: + if propValue != None: + propValueStrList = [six.text_type(propValue)] + else: + propValueStrList = [propValue] + + return propElementName, propValueStrList + + +def getEmptyXmlDoc(): + + """ + Internal helper method that knows how to build an empty Atom entry. + """ + + moduleLogger.debug('Inside getEmptyXmlDoc') + + entryXmlDoc = minidom.Document() + entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") + entryElement.setAttribute('xmlns', ATOM_NS) + entryXmlDoc.appendChild(entryElement) + return entryXmlDoc + diff --git a/src/cmislib/browser/__init__.py b/src/cmislib/browser/__init__.py new file mode 100644 index 0000000..6e30c70 --- /dev/null +++ b/src/cmislib/browser/__init__.py @@ -0,0 +1 @@ +__author__ = 'jpotts' diff --git a/src/cmislib/browser/binding.py b/src/cmislib/browser/binding.py new file mode 100644 index 0000000..67db838 --- /dev/null +++ b/src/cmislib/browser/binding.py @@ -0,0 +1,3219 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +""" +Module containing the browser binding-specific objects used to work with a CMIS +provider. +""" +from cmislib.cmis_services import Binding, RepositoryServiceIfc +from cmislib.domain import CmisId, CmisObject, ObjectType, ACL, ACE, ChangeEntry +from cmislib.exceptions import CmisException, InvalidArgumentException,\ + NotSupportedException, ObjectNotFoundException +from cmislib.net import RESTService as Rest +from cmislib.util import parsePropValueByType +import json +import logging +from io import StringIO +import time +from urllib.parse import urlencode, quote + +CMIS_FORM_TYPE = 'application/x-www-form-urlencoded;charset=utf-8' + +moduleLogger = logging.getLogger('cmislib.browser.binding') + + +class BrowserBinding(Binding): + + """ + Implements the Browser Binding to communicate with the CMIS server. The + Browser Binding uses only GET and POST. It sends JSON and HTML forms and + gets back JSON. + """ + + 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) + result = None + if resp['status'] != '200': + self._processCommonErrors(resp, url) + else: + result = json.loads(content) + return result + + def post(self, url, payload, contentType, username, password, **kwargs): + + """ + Does a post against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, to update the properties on an object, you'd call + :class:`CmisObject.updateProperties`. Or, to check in a document that's + been checked out, you'd call :class:`Document.checkin` on the PWC. + """ + + # merge the cmis client extended args with the ones that got passed in + if len(self.extArgs) > 0: + kwargs.update(self.extArgs) + + result = None + resp, content = Rest().post(url, + payload, + contentType, + username=username, + password=password, + **kwargs) + if resp['status'] != '200' and resp['status'] != '201': + self._processCommonErrors(resp, url) + elif content is not None and content != "": + result = json.loads(content) + return result + + +class RepositoryService(RepositoryServiceIfc): + + """ + Defines the repository service for the Browser Binding. + """ + + def getRepository(self, client, repositoryId): + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + + if repositoryId in result: + return BrowserRepository(client, result[repositoryId]) + + raise ObjectNotFoundException(url=client.repositoryUrl) + + def getRepositories(self, client): + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + + repositories = [] + for repo in result.itervalues(): + repositories.append({'repositoryId': repo['repositoryId'], + 'repositoryName': repo['repositoryName']}) + return repositories + + def getDefaultRepository(self, client): + + """ + Gets the default repository for this server. The spec doesn't include + the notion of a default, so this just returns the first one in the + list. + """ + + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + # instantiate a Repository object with the first workspace + # element we find + repository = None + for repo in result.itervalues(): + repository = BrowserRepository(client, repo) + return repository + + +class BrowserCmisObject(object): + + """ + Common ancestor class for other CMIS domain objects such as + :class:`Document` and :class:`Folder`. + """ + + def __init__(self, cmisClient, repository, objectId=None, data=None, **kwargs): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._objectId = objectId + self._properties = {} + self._allowableActions = {} + self.data = data + self._extArgs = kwargs + self.logger = logging.getLogger('cmislib.browser.binding.BrowserCmisObject') + self.logger.info('Creating an instance of CmisObject') + + def __str__(self): + """To string""" + return self.getObjectId() + + def _initData(self): + + """ + An internal method used to clear out any member variables that + might be out of sync if we were to fetch new data from the + service. + """ + + self._properties = {} + self._allowableActions = {} + + def reload(self, **kwargs): + + """ + Fetches the latest representation of this object from the CMIS service. + Some methods, like :class:`^Document.checkout` do this for you. + + If you call reload with a properties filter, the filter will be in + effect on subsequent calls until the filter argument is changed. To + reset to the full list of properties, call reload with filter set to + '*'. + """ + + if self._extArgs: + self._extArgs.update(kwargs) + else: + self._extArgs = kwargs + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=object" + self.data = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **self._extArgs) + 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 'returnVersion' in self._extArgs.keys(): + self._objectId = None + + def getObjectId(self): + + """ + Returns the object ID for this object. + + >>> doc = resultSet.getResults()[0] + >>> doc.getObjectId() + u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' + """ + + if self._objectId is None: + if self.data is None: + self.logger.debug('Both objectId and data 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 + """ + + if not self.getAllowableActions()['canGetObjectParents']: + raise NotSupportedException('Object does not support getObjectParents') + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=parents" + result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + # return the result set + return BrowserResultSet(self._cmisClient, self._repository, {'objects': result}, serializer=ChildrenSerializer()) + + def getPaths(self): + """ + Returns the object's paths as a list of strings. + """ + # see sub-classes for implementation + pass + + def getAllowableActions(self): + + """ + Returns a dictionary of allowable actions, keyed off of the action name. + + >>> actions = doc.getAllowableActions() + >>> for a in actions: + ... print "%s:%s" % (a,actions[a]) + ... + canDeleteContentStream:True + canSetContentStream:True + canCreateRelationship:True + canCheckIn:False + canApplyACL:False + canDeleteObject:True + canGetAllVersions:True + canGetObjectParents:True + canGetProperties:True + """ + + if self._allowableActions == {}: + self.reload(includeAllowableActions=True) + assert 'allowableActions' in self.data.keys(), "Expected object data to have an allowableActions key" + allowableActions = self.data['allowableActions'] + self._allowableActions = allowableActions + + return self._allowableActions + + 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. + """ + + if self._properties == {}: + if self.data is None: + self.reload() + for prop in self.data['properties'].itervalues(): + self._properties[prop['id']] = parsePropValueByType(prop['value'], prop['type']) + + 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' + """ + + return self.getProperties()['cmis: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) + + >>> folder.getName() + u'someFolderFoo' + + """ + + # get the root folder URL + updateUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + + props = {"cmisaction": "update"} + + propCount = 0 + for prop in properties: + props["propertyId[%s]" % propCount] = prop + props["propertyValue[%s]" % propCount] = properties[prop] + propCount += 1 + + # invoke the URL + result = self._cmisClient.binding.post(updateUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + self.data = result + 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) + """ + + moveUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "move", + "sourceFolderId": sourceFolder.id, + "targetFolderId": targetFolder.id} + + # invoke the URL + self._cmisClient.binding.post(moveUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + return + + 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. + """ + + delUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "delete"} + + # invoke the URL + self._cmisClient.binding.post(delUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + return + + def applyPolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def createRelationship(self, targetObj, relTypeId): + + """ + Creates a relationship between this object and a specified target + object using the relationship type specified. Returns the new + :class:`Relationship` object. + + >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + """ + + pass + + def getRelationships(self, **kwargs): + + """ + Returns a :class:`ResultSet` of :class:`Relationship` objects for each + relationship where the source is this object. + + >>> rels = tstDoc1.getRelationships() + >>> len(rels.getResults()) + 1 + >>> rel = rels.getResults().values()[0] + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + The following optional arguments are supported: + - includeSubRelationshipTypes + - relationshipDirection + - typeId + - maxItems + - skipCount + - filter + - includeAllowableActions + """ + + pass + + def removePolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def getAppliedPolicies(self): + + """ + This is not yet implemented. + """ + + pass + + def getACL(self): + + """ + Repository.getCapabilities['ACL'] must return manage or discover. + + >>> acl = folder.getACL() + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + + 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._repository.getRootFolderUrl() + "?cmisselector=object&objectId=" + self.getObjectId() + "&includeACL=true" + result = self._cmisClient.binding.get(aclUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + return BrowserACL(data=result['acl']) + 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': , 'jdoe': } + """ + 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.') + # get the root folder URL + aclUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + "&cmisaction=applyACL" + + aclJSON = ACLSerializer().toJSON(acl) + + contentType, body = encode_multipart_formdata(None, StringIO.StringIO(aclJSON), 'application/json') + + # invoke the URL + result = self._cmisClient.binding.post(aclUrl.encode('utf-8'), + body, + contentType, + self._cmisClient.username, + self._cmisClient.password) + + # return the result set + return BrowserACL(data=result) + else: + raise NotSupportedException + + allowableActions = property(getAllowableActions) + name = property(getName) + id = property(getObjectId) + properties = property(getProperties) + ACL = property(getACL) + + +class BrowserRepository(object): + """ + Represents a CMIS repository. Will lazily populate itself by + calling the repository CMIS service URL. + + You must pass in an instance of a CmisClient when creating an + instance of this class. + """ + + def __init__(self, cmisClient, data=None): + """ Constructor """ + self._cmisClient = cmisClient + self.data = data + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + self.logger = logging.getLogger('cmislib.browser.binding.BrowserRepository') + self.logger.info('Creating an instance of Repository') + + def __str__(self): + """To string""" + return self.getRepositoryName() + + def _initData(self): + """ + This method clears out any local variables that would be out of sync + when data is re-fetched from the server. + """ + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + + def reload(self): + """ + This method will re-fetch the repository's XML data from the CMIS + repository. + """ + + pass + + def getRepositoryId(self): + + """ + Returns this repository's unique identifier + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryId() + u'83beb297-a6fa-4ac5-844b-98c871c0eea9' + """ + + if self._repositoryId is None: + if self.data is None: + self.reload() + self._repositoryId = self.data['repositoryId'] + return self._repositoryId + + def getRepositoryName(self): + + """ + Returns this repository's name + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryName() + u'Main Repository' + """ + + if self._repositoryName is None: + if self.data is None: + self.reload() + self._repositoryName = self.data['repositoryName'] + return self._repositoryName + + def getRepositoryInfo(self): + + """ + Returns a dict of repository information. + + >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() + u'Main Repository' + >>> info = repo.getRepositoryInfo() + >>> for k,v in info.items(): + ... print "%s:%s" % (k,v) + ... + cmisSpecificationTitle:Version 1.0 Committee Draft 04 + cmisVersionSupported:1.0 + repositoryDescription:None + productVersion:3.2.0 (r2 2440) + rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 + repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 + repositoryName:Main Repository + vendorName:Alfresco + productName:Alfresco Repository (Community) + """ + + if not self._repositoryInfo: + if self.data is None: + self.reload() + repoInfo = {'repositoryId': self.data['repositoryId'], 'repositoryName': self.data['repositoryName'], + 'repositoryDescription': self.data['repositoryDescription'], + 'vendorName': self.data['vendorName'], 'productName': self.data['productName'], + 'productVersion': self.data['productVersion'], 'rootFolderId': self.data['rootFolderId'], + 'latestChangeLogToken': self.data['latestChangeLogToken'], + 'cmisVersionSupported': self.data['cmisVersionSupported'], + 'changesIncomplete': self.data['changesIncomplete'], + 'changesOnType': self.data['changesOnType'], + 'principalIdAnonymous': self.data['principalIdAnonymous'], + 'principalIdAnyone': self.data['principalIdAnyone']} + if 'thinClientURI' in self.data.keys(): + repoInfo['thinClientURI'] = self.data['thinClientURI'] + if 'extendedFeatures' in self.data.keys(): + repoInfo['extendedFeatures'] = self.data['extendedFeatures'] + self._repositoryInfo = repoInfo + return self._repositoryInfo + + def getRootFolderUrl(self): + + """ Gets the repository's root folder URL """ + + if self.data is None: + self.reload() + return self.data['rootFolderUrl'] + + def getRepositoryUrl(self): + + """ Gets the repository's URL """ + + if self.data is None: + self.reload() + return self.data['repositoryUrl'] + + 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 + """ + + byPathUrl = self.getRootFolderUrl() + quote(path) + "?cmisselector=object" + result = self._cmisClient.binding.get(byPathUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, data=result, **kwargs), **kwargs) + + def getSupportedPermissions(self): + """ + Returns the value of the cmis:supportedPermissions element. Valid + values are: + + - basic: indicates that the CMIS Basic permissions are supported + - repository: indicates that repository specific permissions are supported + - both: indicates that both CMIS basic permissions and repository specific permissions are supported + + >>> repo.supportedPermissions + u'both' + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if not self._permissions: + if self.data is None: + self.reload() + if 'aclCapabilities' in self.data.keys(): + if 'supportedPermissions' in self.data['aclCapabilities'].keys(): + self._permissions = self.data['aclCapabilities']['supportedPermissions'] + return self._permissions + + def getPermissionDefinitions(self): + + """ + Returns a dictionary of permission definitions for this repository. The + key is the permission string or technical name of the permission + and the value is the permission description. + + >>> for permDef in repo.permissionDefinitions: + ... print permDef + ... + cmis:all + {http://www.alfresco.org/model/system/1.0}base.LinkChildren + {http://www.alfresco.org/model/content/1.0}folder.Consumer + {http://www.alfresco.org/model/security/1.0}All.All + {http://www.alfresco.org/model/system/1.0}base.CreateAssociations + {http://www.alfresco.org/model/system/1.0}base.FullControl + {http://www.alfresco.org/model/system/1.0}base.AddChildren + {http://www.alfresco.org/model/system/1.0}base.ReadAssociations + {http://www.alfresco.org/model/content/1.0}folder.Editor + {http://www.alfresco.org/model/content/1.0}cmobject.Editor + {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations + cmis:read + cmis:write + """ + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + permData = self.data['aclCapabilities']['permissions'] + perms = {} + for entry in permData: + perms[entry['permission']] = entry['description'] + + return perms + + 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) + + permData = self.data['aclCapabilities']['permissionMapping'] + permMap = {} + for entry in permData: + permMap[entry['key']] = entry['permission'] + + return 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) + + return self.data['aclCapabilities']['propagation'] + + def getCapabilities(self): + + """ + Returns a dict of repository capabilities. + + >>> caps = repo.getCapabilities() + >>> for k,v in caps.items(): + ... print "%s:%s" % (k,v) + ... + PWCUpdatable:True + VersionSpecificFiling:False + Join:None + ContentStreamUpdatability:anytime + AllVersionsSearchable:False + Renditions:None + Multifiling:True + GetFolderTree:True + GetDescendants:True + ACL:None + PWCSearchable:True + Query:bothcombined + Unfiling:False + Changes:None + """ + + if not self._capabilities: + if self.data is None: + self.reload() + caps = {} + if 'capabilities' in self.data.keys(): + for cap in self.data['capabilities'].keys(): + key = cap.replace('capability', '') + caps[key] = self.data['capabilities'][cap] + self._capabilities = caps + return self._capabilities + + def getRootFolder(self): + """ + Returns the root folder of the repository + + >>> root = repo.getRootFolder() + >>> root.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + # get the root folder id + rootFolderId = self.getRepositoryInfo()['rootFolderId'] + # instantiate a Folder object using the ID + folder = BrowserFolder(self._cmisClient, self, rootFolderId) + # return it + return folder + + def getFolder(self, folderId): + + """ + Returns a :class:`Folder` object for a specified folderId + + >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') + >>> someFolder.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + retObject = self.getObject(folderId) + return BrowserFolder(self._cmisClient, self, data=retObject.data) + + def getTypeChildren(self, + typeId=None, + **kwargs): + + """ + 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 + """ + + typesUrl = self.getRepositoryUrl() + "?cmisselector=typeChildren" + + if typeId is not None: + typesUrl += "&typeId=%s" % (quote(typeId)) + + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + types = [] + for res in result['types']: + objectType = BrowserObjectType(self._cmisClient, + self, + data=res) + types.append(objectType) + # return the result + return types + + def getTypeDescendants(self, typeId=None, depth=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 + + This optional argument is supported: + - depth + + These optional arguments are supported: + - includePropertyDefinitions + + >>> types = alfRepo.getTypeDescendants('cmis:folder') + >>> len(types) + 17 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1) + >>> len(types) + 12 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2) + >>> len(types) + 17 + """ + + typesUrl = self.getRepositoryUrl() + "?cmisselector=typeDescendants" + + if typeId is not None: + typesUrl += "&typeId=%s" % (quote(typeId)) + if depth is not None: + typesUrl += "&depth=%s" % (depth) + + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + serializer = TreeSerializer(treeType='type') + types = serializer.getEntries(self._cmisClient, self, result) + 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.getRepositoryUrl() + "?cmisselector=typeChildren" + + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + types = [] + for res in result['types']: + objectType = BrowserObjectType(self._cmisClient, + self, + data=res) + 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') + """ + # localhost:8080/chemistry/browser/A1?cmisselector=typeDefinition&typeId=cmis:folder + typesUrl = self.getRepositoryUrl() + "?cmisselector=typeDefinition" + \ + "&typeId=" + typeId + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password) + + return BrowserObjectType(self._cmisClient, + self, + data=result) + + 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 + """ + + typesUrl = self.getRepositoryUrl() + "?cmisselector=checkedOut" + + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + return BrowserResultSet(self._cmisClient, + self, + data=result) + + def getUnfiledDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently unfiled. + + >>> rs = repo.getUnfiledDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getUnfiledDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a.pdf' + u'sample-b.pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + pass + + def getObject(self, + objectId, + **kwargs): + + """ + Returns an object given the specified object ID. + + >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are supported: + - returnVersion + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + """ + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, CmisId(objectId), **kwargs), **kwargs) + + def query(self, statement, **kwargs): + + """ + Returns a list of :class:`CmisObject` objects based on the CMIS + Query Language passed in as the statement. The actual objects + returned will be instances of the appropriate child class based + on the object's base type ID. + + In order for the results to be properly instantiated as objects, + make sure you include 'cmis:objectId' as one of the fields in + your select statement, or just use "SELECT \*". + + If you want the search results to automatically be instantiated with + the appropriate sub-class of :class:`CmisObject` you must either + include cmis:baseTypeId as one of the fields in your select statement + or just use "SELECT \*". + + >>> q = "select * from cmis:document where cmis:name like '%test%'" + >>> resultSet = repo.query(q) + >>> len(resultSet.getResults()) + 1 + >>> resultSet.hasNext() + False + + The following optional arguments are supported: + - searchAllVersions + - includeRelationships + - renditionFilter + - includeAllowableActions + - maxItems + - skipCount + + >>> q = 'select * from cmis:document' + >>> rs = repo.query(q) + >>> len(rs.getResults()) + 148 + >>> rs = repo.query(q, maxItems='5') + >>> len(rs.getResults()) + 5 + >>> rs.hasNext() + True + """ + + # build the CMIS query XML that we're going to POST + queryUrl = self.getRepositoryUrl() + "?cmisaction=query&q=" + quote(statement) + + # do the POST + result = self._cmisClient.binding.post(queryUrl.encode('utf-8'), + None, + CMIS_FORM_TYPE, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return BrowserResultSet(self._cmisClient, self, result, serializer=ResultsSerializer()) + + def getContentChanges(self, **kwargs): + + """ + Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + + The following optional arguments are supported: + - changeLogToken + - includeProperties + - includePolicyIDs + - includeACL + - maxItems + + You can get the latest change log token by inspecting the repository + info via :meth:`Repository.getRepositoryInfo`. + + >>> repo.info['latestChangeLogToken'] + u'2692' + >>> rs = repo.getContentChanges(changeLogToken='2692') + >>> len(rs) + 1 + >>> rs[0].id + u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' + >>> rs[0].changeType + u'updated' + >>> rs[0].changeTime + datetime.datetime(2010, 2, 16, 20, 6, 37) + """ + + pass + + def createDocumentFromString(self, + name, + properties={}, + parentFolder=None, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') + + """ + + # 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) + + >>> f.close() + >>> doc.getTitle() + u'sample-a.pdf' + + The following optional arguments are not currently supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + # if you didn't pass in a parent folder + if parentFolder is None: + # if the repository doesn't require fileable objects to be filed + if self.getCapabilities()['Unfiling']: + # has not been implemented + raise NotImplementedError + else: + # this repo requires fileable objects to be filed + raise InvalidArgumentException + + # get the root folder URL + createDocUrl = self.getRootFolderUrl() + + props = {"objectId": parentFolder.id, + "cmisaction": "createDocument", + "propertyId[0]": "cmis:name", + "propertyValue[0]": name} + + props["propertyId[1]"] = "cmis:objectTypeId" + if 'cmis:objectTypeId' in properties.keys(): + props["propertyValue[1]"] = properties['cmis:objectTypeId'] + else: + props["propertyValue[1]"] = "cmis:document" + + propCount = 2 + for prop in properties: + props["propertyId[%s]" % propCount] = prop + props["propertyValue[%s]" % propCount] = properties[prop] + propCount += 1 + + contentType, body = encode_multipart_formdata(props, contentFile, contentType) + + # invoke the URL + result = self._cmisClient.binding.post(createDocUrl.encode('utf-8'), + body, + contentType, + self._cmisClient.username, + self._cmisClient.password) + + # return the result set + return BrowserDocument(self._cmisClient, self, data=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 + """ + + pass + + def createFolder(self, + parentFolder, + name, + properties={}, + **kwargs): + + """ + 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, **kwargs) + + def createRelationship(self, sourceObj, targetObj, relType): + """ + Creates a relationship of the specific type between a source object + and a target object and returns the new :class:`Relationship` object. + + The following optional arguments are not currently supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createPolicy(self, properties): + """ + This has not yet been implemented. + + The following optional arguments are not currently supported: + - folderId + - policies + - addACEs + - removeACEs + """ + + pass + + capabilities = property(getCapabilities) + id = property(getRepositoryId) + info = property(getRepositoryInfo) + name = property(getRepositoryName) + rootFolder = property(getRootFolder) + permissionDefinitions = property(getPermissionDefinitions) + permissionMap = property(getPermissionMap) + propagation = property(getPropagation) + supportedPermissions = property(getSupportedPermissions) + + +class BrowserResultSet(object): + + """ + Represents a paged result set. + """ + + def __init__(self, cmisClient, repository, data, serializer=None): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._data = data + self._serializer = serializer + self._results = [] + self.logger = logging.getLogger('cmislib.browser.binding.BrowserResultSet') + self.logger.info('Creating an instance of ResultSet') + + def __iter__(self): + """ Iterator for the result set """ + return iter(self.getResults()) + + def __getitem__(self, index): + """ Getter for the result set """ + return self.getResults()[index] + + def __len__(self): + """ Len method for the result set """ + return len(self.getResults()) + + def reload(self): + + """ + Re-invokes the self link for the current set of results. + + >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) + >>> resultSet.reload() + + """ + + pass + + def getResults(self): + + """ + Returns the results that were fetched and cached by the get*Page call. + + >>> resultSet = repo.getCheckedOutDocs() + >>> resultSet.hasNext() + False + >>> for result in resultSet.getResults(): + ... result + ... + + """ + + if self._results: + return self._results + + if self._data: + self._results = self._serializer.fromJSON(self._cmisClient, self._repository, self._data) + + 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 + ... + + """ + + pass + + def getPrev(self): + + """ + Returns the prev page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server returns a "prev" link. Not all of them do. + >>> resultSet.hasPrev() + True + >>> results = resultSet.getPrev() + >>> for result in results: + ... result + ... + + """ + + pass + + def getNext(self): + + """ + Returns the next page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. + >>> resultSet.hasNext() + True + >>> results = resultSet.getNext() + >>> for result in results: + ... result + ... + + """ + + pass + + def getLast(self): + + """ + Returns the last page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server is returning a "last" link. Not all of them do. + + >>> resultSet.hasLast() + True + >>> results = resultSet.getLast() + >>> for result in results: + ... result + ... + + """ + + pass + + def hasNext(self): + + """ + Returns True if this page contains a next link. + + >>> resultSet.hasNext() + True + """ + + if self._data and 'hasMoreItems' in self._data: + return self._data['hasMoreItems'] + + def getNumItems(self): + + """ + Returns the number of items in the result set. + >>> resultSet.getNumItems() + 3 + """ + + if self._data: + return self._data['numItems'] + + def hasPrev(self): + + """ + Returns True if this page contains a prev link. Not all CMIS providers + implement prev links consistently. + + >>> resultSet.hasPrev() + True + """ + + pass + + def hasFirst(self): + + """ + Returns True if this page contains a first link. Not all CMIS providers + implement first links consistently. + + >>> resultSet.hasFirst() + True + """ + + pass + + def hasLast(self): + + """ + Returns True if this page contains a last link. Not all CMIS providers + implement last links consistently. + + >>> resultSet.hasLast() + True + """ + + pass + + +class BrowserDocument(BrowserCmisObject): + + """ + 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 + """ + + coUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "checkOut"} + + # invoke the URL + result = self._cmisClient.binding.post(coUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=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 + """ + + coUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "cancelCheckOut"} + + # invoke the URL + self._cmisClient.binding.post(coUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + return + + 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() + + >>> 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 self.getProperties()['cmis:isVersionSeriesCheckedOut'] + + def getCheckedOutBy(self): + + """ + Returns the ID who currently has the document checked out. + >>> pwc = doc.checkout() + >>> pwc.getCheckedOutBy() + u'admin' + """ + + # reloading the document just to make sure we've got the latest + # and greatest checked out prop + self.reload() + return self.getProperties()['cmis:versionSeriesCheckedOutBy'] + + def checkin(self, checkinComment=None, **kwargs): + + """ + Checks in this :class:`Document` which must be a private + working copy (PWC). + + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + >>> pwc.checkin() + + >>> doc.isCheckedOut() + False + + The following optional arguments are NOT supported: + - major + - properties + - contentStream + - policies + - addACEs + - removeACEs + """ + # TODO implement optional arguments + # major = true is supposed to be the default but inmemory 0.9 is throwing an error 500 without it + if 'major' not in kwargs.keys(): + kwargs['major'] = 'true' + + kwargs['checkinComment'] = checkinComment + + ciUrl = self._repository.getRootFolderUrl() + + # TODO don't hardcode major flag + props = {"objectId": self.id, + "cmisaction": "checkIn"} + + # invoke the URL + result = self._cmisClient.binding.post(ciUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=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 'makir' in kwargs.keys() 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._repository.getRootFolderUrl() + '?cmisselector=versions' + '&objectId=' + self.getObjectId() + + # invoke the URL + result = self._cmisClient.binding.get(versionsUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return BrowserResultSet(self._cmisClient, self._repository, data={'objects': result}, serializer=VersionsSerializer()) + + 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. + """ + + if not self.getAllowableActions()['canGetContentStream']: + return None + + contentUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&selector=content" + result, content = Rest().get(contentUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **self._cmisClient.extArgs) + if result['status'] != '200': + raise CmisException(result['status']) + return StringIO.StringIO(content) + + def setContentStream(self, contentFile, contentType=None): + + """ + Sets the content stream on this object. + + The following optional arguments are not yet supported: + - overwriteFlag=None + """ + + # get the root folder URL + createDocUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.id + "&cmisaction=setContent" + + contentType, body = encode_multipart_formdata(None, contentFile, contentType) + + # invoke the URL + result = self._cmisClient.binding.post(createDocUrl.encode('utf-8'), + body, + contentType, + self._cmisClient.username, + self._cmisClient.password) + + # return the result set + return BrowserDocument(self._cmisClient, self, data=result) + + def deleteContentStream(self): + + """ + Delete's the content stream associated with this object. + """ + + if not self.allowableActions['canDeleteContentStream']: + raise CmisException('Not allowed to delete the content stream') + + delUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "deleteContent"} + + if 'cmis:changeToken' in self.properties.keys(): + props["changeToken"] = self.properties['cmis:changeToken'] + + # invoke the URL + self._cmisClient.binding.post(delUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + return + + 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 + + renditions = [] + + contentUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=renditions&renditionFilter=*" + result, content = Rest().get(contentUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **self._cmisClient.extArgs) + if result['status'] != '200': + raise CmisException(result['status']) + + resultObj = json.loads(content) + for rendObj in resultObj: + renditions.append(BrowserRendition(rendObj)) + + return renditions + + checkedOut = property(isCheckedOut) + + def getPaths(self): + """ + Returns the Document's paths by asking for the parents with the + includeRelativePathSegment flag set to true, then concats the value + of cmis:path with the relativePathSegment. + """ + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=parents&includerelativepathsegment=true" + result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password) + + paths = [] + + for res in result: + path = res['object']['properties']['cmis:path']['value'] + logging.debug(path) + relativePathSegment = res['relativePathSegment'] + + # concat with a slash + # add it to the list + paths.append(path + '/' + relativePathSegment) + + return paths + + +class BrowserFolder(BrowserCmisObject): + + """ + A container object that can hold other :class:`CmisObject` objects + """ + + def createFolder(self, name, properties={}, **kwargs): + + """ + 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 root folder URL + createFolderUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "createFolder", + "propertyId[0]": "cmis:name", + "propertyValue[0]": name} + + props["propertyId[1]"] = "cmis:objectTypeId" + if 'cmis:objectTypeId' in properties.keys(): + props["propertyValue[1]"] = properties['cmis:objectTypeId'] + else: + props["propertyValue[1]"] = "cmis:folder" + + propCount = 2 + for prop in properties: + props["propertyId[%s]" % propCount] = prop.key + props["propertyValue[%s]" % propCount] = prop + propCount += 1 + + # invoke the URL + result = self._cmisClient.binding.post(createFolderUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + # return the result set + return BrowserFolder(self._cmisClient, self._repository, data=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) + + >>> 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) + + >>> 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 + """ + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=children" + result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + # return the result set + return BrowserResultSet(self._cmisClient, self._repository, result, serializer=ChildrenSerializer()) + + 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 + + """ + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=descendants" + result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + # return the result set + return BrowserResultSet(self._cmisClient, self._repository, result, serializer=TreeSerializer()) + + 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' + """ + + byObjectIdUrl = self._repository.getRootFolderUrl() + "?objectId=" + self.getObjectId() + "&cmisselector=foldertree" + result = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + # return the result set + return BrowserResultSet(self._cmisClient, self._repository, result, serializer=TreeSerializer()) + + def getParent(self): + + """ + The optional filter argument is not yet supported. + """ + if 'cmis:parentId' in self.properties.keys() and self.properties['cmis:parentId'] is not None: + return BrowserFolder(self._cmisClient, self._repository, objectId=self.properties['cmis:parentId']) + + 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 + """ + + delUrl = self._repository.getRootFolderUrl() + + props = {"objectId": self.id, + "cmisaction": "deleteTree"} + + # invoke the URL + self._cmisClient.binding.post(delUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + return + + 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 NOT supported: + - allVersions + """ + # TODO need to add support (and unit test) for allVersions + + addUrl = self._repository.getRootFolderUrl() + + props = {"folderId": self.id, + "cmisaction": "addObjectToFolder", + "objectId": cmisObject.id} + + # invoke the URL + result = self._cmisClient.binding.post(addUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result)) + + def removeObject(self, cmisObject): + + """ + Removes the specified object from this folder. The repository must + support unfiling for this to work. + """ + + remUrl = self._repository.getRootFolderUrl() + + props = {"folderId": self.id, + "cmisaction": "removeObjectFromFolder", + "objectId": cmisObject.id} + + # invoke the URL + result = self._cmisClient.binding.post(remUrl.encode('utf-8'), + urlencode(props), + 'application/x-www-form-urlencoded', + self._cmisClient.username, + self._cmisClient.password) + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self._repository, data=result)) + + 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 BrowserRelationship(CmisObject): + + """ + Defines a relationship object between two :class:`CmisObjects` objects + """ + + def getSourceId(self): + + """ + Returns the :class:`CmisId` on the source side of the relationship. + """ + # TODO need to implement + pass + + def getTargetId(self): + + """ + Returns the :class:`CmisId` on the target side of the relationship. + """ + # TODO need to implement + pass + + def getSource(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the source side of the relationship. + """ + # TODO need to implement + pass + + def getTarget(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the target side of the relationship. + """ + # TODO need to implement + pass + + sourceId = property(getSourceId) + targetId = property(getTargetId) + source = property(getSource) + target = property(getTarget) + + +class BrowserPolicy(CmisObject): + + """ + An arbirary object that can 'applied' to objects that the + repository identifies as being 'controllable'. + """ + + pass + + +class BrowserObjectType(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, data=None): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._extArgs = None + self._typeId = typeId + self.data = data + self.logger = logging.getLogger('cmislib.browser.binding.BrowserObjectType') + self.logger.info('Creating an instance of BrowserObjectType') + + 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.data is None: + self.reload() + self._typeId = CmisId(self.data['id']) + + return self._typeId + + def getLocalName(self): + """Getter for cmis:localName""" + if self.data is None: + self.reload() + return self.data['localName'] + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + if self.data is None: + self.reload() + return self.data['localNamespace'] + + def getDisplayName(self): + """Getter for cmis:displayName""" + + if self.data is None: + self.reload() + return self.data['displayName'] + + def getQueryName(self): + """Getter for cmis:queryName""" + if self.data is None: + self.reload() + return self.data['queryName'] + + def getDescription(self): + """Getter for cmis:description""" + if self.data is None: + self.reload() + return self.data['description'] + + def getBaseId(self): + """Getter for cmis:baseId""" + if self.data is None: + self.reload() + return self.data['baseId'] + + def isCreatable(self): + """Getter for cmis:creatable""" + if self.data is None: + self.reload() + return self.data['creatable'] + + def isFileable(self): + """Getter for cmis:fileable""" + if self.data is None: + self.reload() + return self.data['fileable'] + + def isQueryable(self): + """Getter for cmis:queryable""" + if self.data is None: + self.reload() + return self.data['queryable'] + + def isFulltextIndexed(self): + """Getter for cmis:fulltextIndexed""" + + if self.data is None: + self.reload() + return self.data['fulltextIndexed'] + + def isIncludedInSupertypeQuery(self): + """Getter for cmis:includedInSupertypeQuery""" + + if self.data is None: + self.reload() + return self.data['includedInSupertypeQuery'] + + def isControllablePolicy(self): + """Getter for cmis:controllablePolicy""" + + if self.data is None: + self.reload() + return self.data['controllablePolicy'] + + def isControllableACL(self): + """Getter for cmis:controllableACL""" + + if self.data is None: + self.reload() + return self.data['controllableACL'] + + 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.data is None or 'propertyDefinitions' not in self.data.keys(): + self.reload() + props = {} + for prop in self.data['propertyDefinitions'].keys(): + props[prop] = BrowserProperty(self.data['propertyDefinitions'][prop]) + return props + + def reload(self, **kwargs): + """ + This method will reload the object's data from the CMIS service. + """ + if kwargs: + if self._extArgs: + self._extArgs.update(kwargs) + + typesUrl = self._repository.getRepositoryUrl() + kwargs['cmisselector'] = 'typeDefinition' + kwargs['typeId'] = self.getTypeId() + result = self._cmisClient.binding.get(typesUrl, + self._cmisClient.username, + self._cmisClient.password, + **kwargs) + self.data = result + + id = property(getTypeId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + baseId = property(getBaseId) + creatable = property(isCreatable) + fileable = property(isFileable) + queryable = property(isQueryable) + fulltextIndexed = property(isFulltextIndexed) + includedInSupertypeQuery = property(isIncludedInSupertypeQuery) + controllablePolicy = property(isControllablePolicy) + controllableACL = property(isControllableACL) + properties = property(getProperties) + + +class BrowserProperty(object): + + """ + This class represents an attribute or property definition of an object + type. + """ + + def __init__(self, data): + """Constructor""" + self.data = data + self.logger = logging.getLogger('cmislib.browser.binding.BrowserProperty') + self.logger.info('Creating an instance of BrowserProperty') + + def __str__(self): + """To string""" + return self.getId() + + def getId(self): + """Getter for cmis:id""" + return self.data['id'] + + def getLocalName(self): + """Getter for cmis:localName""" + return self.data['localName'] + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + return self.data['localNamespace'] + + def getDisplayName(self): + """Getter for cmis:displayName""" + return self.data['displayName'] + + def getQueryName(self): + """Getter for cmis:queryName""" + return self.data['queryName'] + + def getDescription(self): + """Getter for cmis:description""" + return self.data['description'] + + def getPropertyType(self): + """Getter for cmis:propertyType""" + return self.data['propertyType'] + + def getCardinality(self): + """Getter for cmis:cardinality""" + return self.data['cardinality'] + + def getUpdatability(self): + """Getter for cmis:updatability""" + return self.data['updatability'] + + def isInherited(self): + """Getter for cmis:inherited""" + return self.data['inherited'] + + def isRequired(self): + """Getter for cmis:required""" + return self.data['required'] + + def isQueryable(self): + """Getter for cmis:queryable""" + return self.data['queryable'] + + def isOrderable(self): + """Getter for cmis:orderable""" + return self.data['orderable'] + + def isOpenChoice(self): + """Getter for cmis:openChoice""" + return self.data['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 BrowserACL(ACL): + + """ + Represents the Access Control List for an object. + """ + + def __init__(self, aceList=None, data=None): + + """ + Constructor. Pass in either a list of :class:`ACE` objects or the XML + representation of the ACL. If you have only one ACE, don't worry about + the list--the constructor will convert it to a list for you. + """ + + if aceList: + self._entries = aceList + else: + self._entries = {} + if data: + self._data = data + self._entries = self._getEntriesFromData() + else: + self._data = None + + self.logger = logging.getLogger('cmislib.browser.binding.BrowserACL') + self.logger.info('Creating an instance of ACL') + + def _getEntriesFromData(self): + + """ + Internal method used to get the ACL entries from the fetched data. + """ + + if not self._data: + return + result = {} + for entry in self._data['aces']: + principalId = entry['principal']['principalId'] + direct = entry['isDirect'] + perms = entry['permissions'] + # create an ACE + if len(perms) > 0: + ace = BrowserACE(principalId, perms, direct) + # append it to the dictionary + result[principalId] = ace + return result + + def addEntry(self, principalId, access, direct): + + """ + Adds an :class:`ACE` entry to the ACL. + + >>> acl = folder.getACL() + >>> acl.addEntry(ACE('jpotts', 'cmis:read', 'true')) + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + """ + + ace = BrowserACE(principalId, access, direct) + self._entries[ace.principalId] = ace + + def removeEntry(self, principalId): + + """ + Removes the :class:`ACE` entry given a specific principalId. + + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + >>> acl.removeEntry('jsmith') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } + """ + + if principalId in self._entries.keys(): + del self._entries[principalId] + + def clearEntries(self): + + """ + Clears all :class:`ACE` entries from the ACL and removes the internal + XML representation of the ACL. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> acl.entries + {'jpotts': , 'jsmith': } + >>> acl.getXmlDoc() + + >>> acl.clearEntries() + >>> acl.entries + >>> acl.getXmlDoc() + """ + + self._entries.clear() + self._data = None + + def getEntries(self): + + """ + Returns a dictionary of :class:`ACE` objects for each Access Control + Entry in the ACL. The key value is the ACE principalid. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> for ace in acl.entries.values(): + ... print 'principal:%s has the following permissions...' % ace.principalId + ... for perm in ace.permissions: + ... print perm + ... + principal:jpotts has the following permissions... + cmis:write + principal:jsmith has the following permissions... + cmis:write + """ + + if self._entries: + return self._entries + else: + if self._data: + # parse data and build entry list + self._entries = self._getEntriesFromData() + # then return it + return self._entries + + entries = property(getEntries) + + +class BrowserACE(ACE): + + """ + Represents an ACE retrieved with the Browser Binding. + """ + + pass + + +class BrowserChangeEntry(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 getId(self): + """ + Returns the unique ID of the change entry. + """ + # TODO need to implement + pass + + def getObjectId(self): + """ + Returns the object ID of the object that changed. + """ + # TODO need to implement + pass + + def getChangeType(self): + + """ + Returns the type of change that occurred. The resulting value must be + one of: + + - created + - updated + - deleted + - security + """ + # TODO need to implement + pass + + def getACL(self): + + """ + Gets the :class:`ACL` object that is included with this Change Entry. + """ + # TODO need to implement + pass + + def getChangeTime(self): + + """ + Returns a datetime object representing the time the change occurred. + """ + # TODO need to implement + pass + + def getProperties(self): + + """ + Returns the properties of the change entry. Note that depending on the + capabilities of the repository ("capabilityChanges") the list may not + include the actual property values that changed. + """ + # TODO need to implement + pass + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + +class BrowserChangeEntryResultSet(BrowserResultSet): + + """ + 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. + """ + # TODO need to implement + pass + + +class BrowserRendition(object): + + """ + This class represents a Rendition. + """ + + def __init__(self, data): + """Constructor""" + self.data = data + self.logger = logging.getLogger('cmislib.browser.binding.BrowserRendition') + self.logger.info('Creating an instance of Rendition') + + def __str__(self): + """To string""" + return self.getStreamId() + + def getStreamId(self): + """Getter for the rendition's stream ID""" + return self.data['streamId'] + + def getMimeType(self): + """Getter for the rendition's mime type""" + return self.data['mimeType'] + + def getLength(self): + """Getter for the renditions's length""" + return self.data['length'] + + def getTitle(self): + """Getter for the renditions's title""" + return self.data['title'] + + def getKind(self): + """Getter for the renditions's kind""" + return self.data['kind'] + + def getHeight(self): + """Getter for the renditions's height""" + return self.data['height'] + + def getWidth(self): + """Getter for the renditions's width""" + return self.data['width'] + + def getHref(self): + """Getter for the renditions's href""" + return self.data['href'] + + def getRenditionDocumentId(self): + """Getter for the renditions's width""" + return self.data['renditionDocumentId'] + + streamId = property(getStreamId) + mimeType = property(getMimeType) + length = property(getLength) + title = property(getTitle) + kind = property(getKind) + height = property(getHeight) + width = property(getWidth) + href = property(getHref) + renditionDocumentId = property(getRenditionDocumentId) + + +class BrowserCmisId(str): + + """ + This is a marker class to be used for Strings that are used as CMIS ID's. + Making the objects instances of this class makes it easier to create the + Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of + cmis:propertyString. + """ + + pass + + +def getSpecializedObject(obj, **kwargs): + + """ + Returns an instance of the appropriate :class:`CmisObject` class or one + of its child types depending on the specified baseType. + """ + + moduleLogger.debug('Inside getSpecializedObject') + + if 'cmis:baseTypeId' in obj.getProperties(): + baseType = obj.getProperties()['cmis:baseTypeId'] + if baseType == 'cmis:folder': + return BrowserFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:document': + return BrowserDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:relationship': + return BrowserRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:policy': + return BrowserPolicy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + + # if the base type ID wasn't found in the props (this can happen when + # someone runs a query that doesn't select * or doesn't individually + # specify baseTypeId) or if the type isn't one of the known base + # types, give the object back + return obj + + +def encode_multipart_formdata(fields, contentFile, contentType): + """ + fields is a sequence of (name, value) elements for regular form fields. + files is a sequence of (name, filename, value) elements for data to be uploaded as files + Return (content_type, body) ready for httplib.HTTP instance + """ + boundary = 'aPacHeCheMIStrycMisLIb%s' % (int(time.time())) + crlf = '\r\n' + L = [] + fileName = None + if fields: + for (key, value) in fields.iteritems(): + if key == 'cmis:name': + fileName = value + L.append('--' + boundary) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('Content-Type: text/plain; charset=utf-8') + L.append('') + L.append(value.encode('utf-8')) + + if contentFile: + L.append('--' + boundary) + L.append('Content-Disposition: form-data; name="%s"; filename=%s' % ('content', fileName)) + L.append('Content-Type: %s' % contentType) + L.append('Content-Transfer-Encoding: binary') + L.append('') + L.append(contentFile.read()) + + L.append('--' + boundary + '--') + L.append('') + body = crlf.join(L) + content_type = 'multipart/form-data; boundary=%s' % boundary + return content_type, body + + +class ResultsSerializer(object): + + """ + Responsible for serializing :class:`BrowserResultSet` objects. + """ + + def fromJSON(self, client, repo, jsonObj): + """Transforms from JSON to the object.""" + entries = [] + for obj in jsonObj['results']: + cmisObject = getSpecializedObject(BrowserCmisObject(client, + repo, + data=obj)) + entries.append(cmisObject) + + return entries + + +class ChildrenSerializer(object): + + """ + Responsible for serializing lists of children. + """ + + def fromJSON(self, client, repo, jsonObj): + """Transforms from JSON to the object.""" + entries = [] + for obj in jsonObj['objects']: + dataObj = obj['object'] + cmisObject = getSpecializedObject(BrowserCmisObject(client, + repo, + data=dataObj)) + entries.append(cmisObject) + + return entries + + +class VersionsSerializer(object): + + """ + Responsible for serializing a list of versions. + """ + + def fromJSON(self, client, repo, jsonObj): + """Transforms from JSON to the object.""" + entries = [] + for obj in jsonObj['objects']: + cmisObject = getSpecializedObject(BrowserCmisObject(client, + repo, + data=obj)) + entries.append(cmisObject) + + return entries + + +# TODO Preserve tree hierarchy +class TreeSerializer(object): + + """ + The AtomPubBinding may be returning descendants and trees as a flat list of results. + We should probably implement a Tree result set and return that here instead. + """ + + def __init__(self, treeType='object'): + self.treeType = treeType + + def fromJSON(self, client, repo, jsonObj): + """Transforms from JSON to the object.""" + entries = self.getEntries(client, repo, jsonObj) + + return entries + + def getEntries(self, client, repo, jsonObj): + '''obj is the list of items in the tree''' + entries = [] + for obj in jsonObj: + if self.treeType == 'object': + dataObj = obj['object']['object'] + cmisThing = getSpecializedObject(BrowserCmisObject(client, + repo, + data=dataObj)) + elif self.treeType == 'type': + dataObj = obj['type'] + cmisThing = BrowserObjectType(client, repo, data=dataObj) + else: + raise CmisException("Invalid tree type") + + entries.append(cmisThing) + try: + dataObj = obj['children'] + # if obj['object'].has_key('children'): + # for child in obj['object']['children']: + childEntries = self.getEntries(client, repo, dataObj) + entries = entries + childEntries + except KeyError: + pass + + return entries + + +class FolderSerializer(object): + + """ + Responsible for serializing :class:`Folder` objects. + """ + + def fromJSON(self, client, repo, jsonString): + """Transforms the folder from JSON to an object.""" + obj = json.loads(jsonString) + objectId = obj['succinctProperties']['cmis:objectId'] + folder = BrowserFolder(client, repo, objectId, properties=obj['succinctProperties']) + return folder + + +class ACLSerializer(object): + + """ + Responsible for serializing :class:`BrowserACL` objects. + """ + + def toJSON(self, acl): + """ Transforms the ACL to JSON. """ + entries = acl.getEntries() + aces = [] + for key in entries: + entryJSON = {} + entryJSON['isDirect'] = entries[key].direct + entryJSON['prinipcal'] = {'principalId': entries[key].principalId} + entryJSON['permissions'] = entries[key].permissions + aces.append(entryJSON) + + return json.dumps(aces) diff --git a/src/cmislib/browser_binding.py b/src/cmislib/browser_binding.py new file mode 100644 index 0000000..62cbe54 --- /dev/null +++ b/src/cmislib/browser_binding.py @@ -0,0 +1,2378 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +""" +Module containing the browser binding-specific objects used to work with a CMIS +provider. +""" +from cmis_services import RepositoryServiceIfc +from cmis_services import Binding +from net import RESTService as Rest +from urllib2 import HTTPError +from exceptions import CmisException, RuntimeException +from domain import CmisId, CmisObject, Repository, Relationship, Policy, ObjectType, Property, Folder, Document, ACL, ACE, ChangeEntry, ResultSet, ChangeEntryResultSet, Rendition +import json +import logging + +class BrowserBinding(Binding): + def __init__(self, **kwargs): + self.extArgs = kwargs + + def getRepositoryService(self): + return RepositoryService() + + def get(self, url, username, password, **kwargs): + + """ + Does a get against the CMIS service. More than likely, you will not + need to call this method. Instead, let the other objects do it for you. + + For example, if you need to get a specific object by object id, try + :class:`Repository.getObject`. If you have a path instead of an object + id, use :class:`Repository.getObjectByPath`. Or, you could start with + the root folder (:class:`Repository.getRootFolder`) and drill down from + there. + """ + + # merge the cmis client extended args with the ones that got passed in + if (len(self.extArgs) > 0): + kwargs.update(self.extArgs) + + result = Rest().get(url, + username=username, + password=password, + **kwargs) + if type(result) == HTTPError: + self._processCommonErrors(result) + else: + result = json.load(result) + return result + +class RepositoryService(RepositoryServiceIfc): + def getRepository(self, client, repositoryId): + #TODO + pass + + def getRepositories(self, client): + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + if (type(result) == HTTPError): + raise RuntimeException() + + repositories = [] + for repo in result.itervalues(): + repositories.append({'repositoryId': repo['repositoryId'], + 'repositoryName': repo['repositoryName']}) + return repositories + + def getDefaultRepository(self, client): + result = client.binding.get(client.repositoryUrl, client.username, client.password, **client.extArgs) + # instantiate a Repository object with the first workspace + # element we find + for repo in result.itervalues(): + repository = BrowserRepository(client, repo) + return repository + + def getRepositoryInfo(self): + return "Here is your repository info using the browser binding" + + +class BrowserCmisObject(object): + + """ + Common ancestor class for other CMIS domain objects such as + :class:`Document` and :class:`Folder`. + """ + + def __init__(self, cmisClient, repository, objectId=None, data=None, **kwargs): + """ Constructor """ + self._cmisClient = cmisClient + self._repository = repository + self._objectId = objectId + self._name = None + self._properties = {} + self._allowableActions = {} + self.data = data + self._kwargs = kwargs + self.logger = logging.getLogger('cmislib.browser_binding.BrowserCmisObject') + self.logger.info('Creating an instance of CmisObject') + + def __str__(self): + """To string""" + return self.getObjectId() + + def reload(self, **kwargs): + + """ + Fetches the latest representation of this object from the CMIS service. + Some methods, like :class:`^Document.checkout` do this for you. + + If you call reload with a properties filter, the filter will be in + effect on subsequent calls until the filter argument is changed. To + reset to the full list of properties, call reload with filter set to + '*'. + """ + + if kwargs: + if self._kwargs: + self._kwargs.update(kwargs) + else: + self._kwargs = kwargs + + byObjectIdUrl = self.getRootFolderUrl() + "?objectId=" + self.objectId + self.data = self._cmisClient.binding.get(byObjectIdUrl.encode('utf-8'), + self._cmisClient.username, + self._cmisClient.password, + **addOptions) + self._initData() + + # if a returnVersion arg was passed in, it is possible we got back + # a different object ID than the value we started with, so it needs + # to be cleared out as well + if options.has_key('returnVersion') or addOptions.has_key('returnVersion'): + self._objectId = None + + def getObjectId(self): + + """ + Returns the object ID for this object. + + >>> doc = resultSet.getResults()[0] + >>> doc.getObjectId() + u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' + """ + + pass + + def getObjectParents(self, **kwargs): + """ + Gets the parents of this object as a :class:`ResultSet`. + + The following optional arguments are supported: + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includeRelativePathSegment + """ + + pass + + def getPaths(self): + """ + Returns the object's paths as a list of strings. + """ + # see sub-classes for implementation + pass + + def getAllowableActions(self): + + """ + Returns a dictionary of allowable actions, keyed off of the action name. + + >>> actions = doc.getAllowableActions() + >>> for a in actions: + ... print "%s:%s" % (a,actions[a]) + ... + canDeleteContentStream:True + canSetContentStream:True + canCreateRelationship:True + canCheckIn:False + canApplyACL:False + canDeleteObject:True + canGetAllVersions:True + canGetObjectParents:True + canGetProperties:True + """ + + pass + + def getTitle(self): + + """ + Returns the value of the object's cmis:title property. + """ + + pass + + def getProperties(self): + + """ + Returns a dict of the object's properties. If CMIS returns an + empty element for a property, the property will be in the + dict with a value of None. + + >>> props = doc.getProperties() + >>> for p in props: + ... print "%s: %s" % (p, props[p]) + ... + cmis:contentStreamMimeType: text/html + cmis:creationDate: 2009-12-15T09:45:35.369-06:00 + cmis:baseTypeId: cmis:document + cmis:isLatestMajorVersion: false + cmis:isImmutable: false + cmis:isMajorVersion: false + cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339 + + The optional filter argument is not yet implemented. + """ + + pass + + def getName(self): + + """ + Returns the value of cmis:name from the getProperties() dictionary. + We don't need a getter for every standard CMIS property, but name + is a pretty common one so it seems to make sense. + + >>> doc.getName() + u'system-overview.html' + """ + + pass + + def updateProperties(self, properties): + + """ + Updates the properties of an object with the properties provided. + Only provide the set of properties that need to be updated. + + >>> folder = repo.getObjectByPath('/someFolder2') + >>> folder.getName() + u'someFolder2' + >>> props = {'cmis:name': 'someFolderFoo'} + >>> folder.updateProperties(props) + + >>> folder.getName() + u'someFolderFoo' + + """ + + pass + + def move(self, sourceFolder, targetFolder): + + """ + Moves an object from the source folder to the target folder. + + >>> sub1 = repo.getObjectByPath('/cmislib/sub1') + >>> sub2 = repo.getObjectByPath('/cmislib/sub2') + >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1') + >>> doc.move(sub1, sub2) + """ + + pass + + def delete(self, **kwargs): + + """ + Deletes this :class:`CmisObject` from the repository. Note that in the + case of a :class:`Folder` object, some repositories will refuse to + delete it if it contains children and some will delete it without + complaint. If what you really want to do is delete the folder and all + of its descendants, use :meth:`~Folder.deleteTree` instead. + + >>> folder.delete() + + The optional allVersions argument is supported. + """ + + pass + + def applyPolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def createRelationship(self, targetObj, relTypeId): + + """ + Creates a relationship between this object and a specified target + object using the relationship type specified. Returns the new + :class:`Relationship` object. + + >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + """ + + pass + + def getRelationships(self, **kwargs): + + """ + Returns a :class:`ResultSet` of :class:`Relationship` objects for each + relationship where the source is this object. + + >>> rels = tstDoc1.getRelationships() + >>> len(rels.getResults()) + 1 + >>> rel = rels.getResults().values()[0] + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + The following optional arguments are supported: + - includeSubRelationshipTypes + - relationshipDirection + - typeId + - maxItems + - skipCount + - filter + - includeAllowableActions + """ + + pass + + def removePolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def getAppliedPolicies(self): + + """ + This is not yet implemented. + """ + + pass + + def getACL(self): + + """ + Repository.getCapabilities['ACL'] must return manage or discover. + + >>> acl = folder.getACL() + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + + The optional onlyBasicPermissions argument is currently not supported. + """ + + pass + + def applyACL(self, acl): + + """ + Updates the object with the provided :class:`ACL`. + Repository.getCapabilities['ACL'] must return manage to invoke this + call. + + >>> acl = folder.getACL() + >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true')) + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + """ + + pass + + allowableActions = property(getAllowableActions) + name = property(getName) + id = property(getObjectId) + properties = property(getProperties) + title = property(getTitle) + ACL = property(getACL) + + +class BrowserRepository(object): + """ + Represents a CMIS repository. Will lazily populate itself by + calling the repository CMIS service URL. + + You must pass in an instance of a CmisClient when creating an + instance of this class. + """ + + def __init__(self, cmisClient, data=None): + """ Constructor """ + self._cmisClient = cmisClient + self.data = data + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + self.logger = logging.getLogger('cmislib.browser_binding.BrowserRepository') + self.logger.info('Creating an instance of Repository') + + def __str__(self): + """To string""" + return self.getRepositoryName() + + def _initData(self): + """ + This method clears out any local variables that would be out of sync + when data is re-fetched from the server. + """ + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._uriTemplates = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + + def reload(self): + """ + This method will re-fetch the repository's XML data from the CMIS + repository. + """ + + pass + + def getRepositoryId(self): + + """ + Returns this repository's unique identifier + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryId() + u'83beb297-a6fa-4ac5-844b-98c871c0eea9' + """ + + if self._repositoryId == None: + if self.data == None: + self.reload() + self._repositoryId = self.data['repositoryId'] + return self._repositoryId + + def getRepositoryName(self): + + """ + Returns this repository's name + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryName() + u'Main Repository' + """ + + if self._repositoryName == None: + if self.data == None: + self.reload() + self._repositoryName = self.data['repositoryName'] + return self._repositoryName + + def getRepositoryInfo(self): + + """ + Returns a dict of repository information. + + >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() + u'Main Repository' + >>> info = repo.getRepositoryInfo() + >>> for k,v in info.items(): + ... print "%s:%s" % (k,v) + ... + cmisSpecificationTitle:Version 1.0 Committee Draft 04 + cmisVersionSupported:1.0 + repositoryDescription:None + productVersion:3.2.0 (r2 2440) + rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 + repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 + repositoryName:Main Repository + vendorName:Alfresco + productName:Alfresco Repository (Community) + """ + + if not self._repositoryInfo: + if self.data == None: + self.reload() + repoInfo = {} + repoInfo['repositoryId'] = self.data['repositoryId'] + repoInfo['repositoryName'] = self.data['repositoryName'] + repoInfo['resositoryDescription'] = self.data['repositoryDescription'] + repoInfo['vendorName'] = self.data['vendorName'] + repoInfo['productName'] = self.data['productName'] + repoInfo['productVersion'] = self.data['productVersion'] + repoInfo['rootFolderId'] = self.data['rootFolderId'] + repoInfo['latestChangeLogToken'] = self.data['latestChangeLogToken'] + repoInfo['cmisVersionSupported'] = self.data['cmisVersionSupported'] + repoInfo['thinClientURI'] = self.data['thinClientURI'] + repoInfo['changesIncomplete'] = self.data['changesIncomplete'] + repoInfo['changesOnType'] = self.data['changesOnType'] + repoInfo['principalIdAnonymous'] = self.data['principalIdAnonymous'] + repoInfo['principalIdAnyone'] = self.data['principalIdAnyone'] + if self.data.has_key('extendedFeatures'): + repoInfo['extendedFeatures'] = self.data['extendedFeatures'] + self._repositoryInfo = repoInfo + return self._repositoryInfo + + def getRootFolderUrl(self): + if self.data == None: + self.reload() + return self.data['rootFolderUrl'] + + def getObjectByPath(self, path, **kwargs): + + """ + Returns an object given the path to the object. + + >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are not currently supported: + - filter + - includeAllowableActions + """ + + pass + + def getSupportedPermissions(self): + """ + Returns the value of the cmis:supportedPermissions element. Valid + values are: + + - basic: indicates that the CMIS Basic permissions are supported + - repository: indicates that repository specific permissions are supported + - both: indicates that both CMIS basic permissions and repository specific permissions are supported + + >>> repo.supportedPermissions + u'both' + """ + + if not self.getCapabilities()['ACL']: + raise NotSupportedException(messages.NO_ACL_SUPPORT) + + if not self._permissions: + if self.data == None: + self.reload() + if self.data.has_key('aclCapabilities'): + if self.data['aclCapabilities'].has_key('supportedPermissions'): + self._permissions = self.data['aclCapabilities']['supportedPermissions'] + return self._permissions + + def getPermissionDefinitions(self): + + """ + Returns a dictionary of permission definitions for this repository. The + key is the permission string or technical name of the permission + and the value is the permission description. + + >>> for permDef in repo.permissionDefinitions: + ... print permDef + ... + cmis:all + {http://www.alfresco.org/model/system/1.0}base.LinkChildren + {http://www.alfresco.org/model/content/1.0}folder.Consumer + {http://www.alfresco.org/model/security/1.0}All.All + {http://www.alfresco.org/model/system/1.0}base.CreateAssociations + {http://www.alfresco.org/model/system/1.0}base.FullControl + {http://www.alfresco.org/model/system/1.0}base.AddChildren + {http://www.alfresco.org/model/system/1.0}base.ReadAssociations + {http://www.alfresco.org/model/content/1.0}folder.Editor + {http://www.alfresco.org/model/content/1.0}cmobject.Editor + {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations + cmis:read + cmis:write + """ + + pass + + def getPermissionMap(self): + + """ + Returns a dictionary representing the permission mapping table where + each key is a permission key string and each value is a list of one or + more permissions the principal must have to perform the operation. + + >>> for (k,v) in repo.permissionMap.items(): + ... print 'To do this: %s, you must have these perms:' % k + ... for perm in v: + ... print perm + ... + To do this: canCreateFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canAddToFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canDelete.Object, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.DeleteNode + To do this: canCheckin.Document, you must have these perms: + cmis:all + {http://www.alfresco.org/model/content/1.0}lockable.CheckIn + """ + + pass + + def getPropagation(self): + + """ + Returns the value of the cmis:propagation element. Valid values are: + - objectonly: indicates that the repository is able to apply ACEs + without changing the ACLs of other objects + - propagate: indicates that the repository is able to apply ACEs to a + given object and propagate this change to all inheriting objects + + >>> repo.propagation + u'propagate' + """ + + pass + + def getCapabilities(self): + + """ + Returns a dict of repository capabilities. + + >>> caps = repo.getCapabilities() + >>> for k,v in caps.items(): + ... print "%s:%s" % (k,v) + ... + PWCUpdatable:True + VersionSpecificFiling:False + Join:None + ContentStreamUpdatability:anytime + AllVersionsSearchable:False + Renditions:None + Multifiling:True + GetFolderTree:True + GetDescendants:True + ACL:None + PWCSearchable:True + Query:bothcombined + Unfiling:False + Changes:None + """ + + if not self._capabilities: + if self.data == None: + self.reload() + caps = {} + if self.data.has_key('capabilities'): + for cap in self.data['capabilities'].keys(): + key = cap.replace('capability', '') + caps[key] = self.data['capabilities'][cap] + self._capabilities = caps + return self._capabilities + + def getRootFolder(self): + """ + Returns the root folder of the repository + + >>> root = repo.getRootFolder() + >>> root.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + # get the root folder id + rootFolderId = self.getRepositoryInfo()['rootFolderId'] + # instantiate a Folder object using the ID + folder = BrowserFolder(self._cmisClient, self, rootFolderId) + # return it + return folder + + def getFolder(self, folderId): + + """ + Returns a :class:`Folder` object for a specified folderId + + >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') + >>> someFolder.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + retObject = self.getObject(folderId) + return BrowserFolder(self._cmisClient, self, data=retObject.data) + + def getTypeChildren(self, + typeId=None): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + child types of the type specified by the typeId. + + If no typeId is provided, the result will be the same as calling + `self.getTypeDefinitions` + + These optional arguments are current unsupported: + - includePropertyDefinitions + - maxItems + - skipCount + + >>> baseTypes = repo.getTypeChildren() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + pass + + def getTypeDescendants(self, typeId=None, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + descendant types of the type specified by the typeId. + + If no typeId is provided, the repository's "typesdescendants" URL + will be called to determine the list of descendant types. + + >>> allTypes = repo.getTypeDescendants() + >>> for aType in allTypes: + ... print aType.getTypeId() + ... + cmis:folder + F:cm:systemfolder + F:act:savedactionfolder + F:app:configurations + F:fm:forums + F:wcm:avmfolder + F:wcm:avmplainfolder + F:wca:webfolder + F:wcm:avmlayeredfolder + F:st:site + F:app:glossary + F:fm:topic + + These optional arguments are supported: + - depth + - includePropertyDefinitions + + >>> types = alfRepo.getTypeDescendants('cmis:folder') + >>> len(types) + 17 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1) + >>> len(types) + 12 + >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2) + >>> len(types) + 17 + """ + + pass + + def getTypeDefinitions(self, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects representing + the base types in the repository. + + >>> baseTypes = repo.getTypeDefinitions() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + pass + + def getTypeDefinition(self, typeId): + + """ + Returns an :class:`ObjectType` object for the specified object type id. + + >>> folderType = repo.getTypeDefinition('cmis:folder') + """ + + pass + + def getLink(self, rel): + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + + pass + + def getCheckedOutDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently checked out. + + >>> rs = repo.getCheckedOutDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getCheckedOutDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a (Working Copy).pdf' + u'sample-b (Working Copy).pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + pass + + def getUnfiledDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently unfiled. + + >>> rs = repo.getUnfiledDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getUnfiledDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a.pdf' + u'sample-b.pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + pass + + def getObject(self, + objectId, + **kwargs): + + """ + Returns an object given the specified object ID. + + >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are supported: + - returnVersion + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + """ + + return getSpecializedObject(BrowserCmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs) + + def query(self, statement, **kwargs): + + """ + Returns a list of :class:`CmisObject` objects based on the CMIS + Query Language passed in as the statement. The actual objects + returned will be instances of the appropriate child class based + on the object's base type ID. + + In order for the results to be properly instantiated as objects, + make sure you include 'cmis:objectId' as one of the fields in + your select statement, or just use "SELECT \*". + + If you want the search results to automatically be instantiated with + the appropriate sub-class of :class:`CmisObject` you must either + include cmis:baseTypeId as one of the fields in your select statement + or just use "SELECT \*". + + >>> q = "select * from cmis:document where cmis:name like '%test%'" + >>> resultSet = repo.query(q) + >>> len(resultSet.getResults()) + 1 + >>> resultSet.hasNext() + False + + The following optional arguments are supported: + - searchAllVersions + - includeRelationships + - renditionFilter + - includeAllowableActions + - maxItems + - skipCount + + >>> q = 'select * from cmis:document' + >>> rs = repo.query(q) + >>> len(rs.getResults()) + 148 + >>> rs = repo.query(q, maxItems='5') + >>> len(rs.getResults()) + 5 + >>> rs.hasNext() + True + """ + + pass + + def getContentChanges(self, **kwargs): + + """ + Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + + The following optional arguments are supported: + - changeLogToken + - includeProperties + - includePolicyIDs + - includeACL + - maxItems + + You can get the latest change log token by inspecting the repository + info via :meth:`Repository.getRepositoryInfo`. + + >>> repo.info['latestChangeLogToken'] + u'2692' + >>> rs = repo.getContentChanges(changeLogToken='2692') + >>> len(rs) + 1 + >>> rs[0].id + u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' + >>> rs[0].changeType + u'updated' + >>> rs[0].changeTime + datetime.datetime(2010, 2, 16, 20, 6, 37) + """ + + pass + + def createDocumentFromString(self, + name, + properties={}, + parentFolder=None, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') + + """ + + pass + + def createDocument(self, + name, + properties={}, + parentFolder=None, + contentFile=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new :class:`Document` object. If the repository + supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + To create a document with an associated contentFile, pass in a + File object. The method will attempt to guess the appropriate content + type and encoding based on the file. To specify it yourself, pass them + in via the contentType and contentEncoding arguments. + + >>> f = open('sample-a.pdf', 'rb') + >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) + + >>> f.close() + >>> doc.getTitle() + u'sample-a.pdf' + + The following optional arguments are not currently supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def createDocumentFromSource(self, + sourceId, + properties={}, + parentFolder=None): + """ + This is not yet implemented. + + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def createFolder(self, + parentFolder, + name, + properties={}): + + """ + Creates a new :class:`Folder` object in the specified parentFolder. + + >>> root = repo.getRootFolder() + >>> folder = repo.createFolder(root, 'someFolder2') + >>> folder.getTitle() + u'someFolder2' + >>> folder.getObjectId() + u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' + + The following optional arguments are not yet supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createRelationship(self, sourceObj, targetObj, relType): + """ + Creates a relationship of the specific type between a source object + and a target object and returns the new :class:`Relationship` object. + + The following optional arguments are not currently supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createPolicy(self, properties): + """ + This has not yet been implemented. + + The following optional arguments are not currently supported: + - folderId + - policies + - addACEs + - removeACEs + """ + + pass + + def getCollection(self, collectionType, **kwargs): + + """ + Returns a list of objects returned for the specified collection. + + If the query collection is requested, an exception will be raised. + That collection isn't meant to be retrieved. + + If the types collection is specified, the method returns the result of + `getTypeDefinitions` and ignores any optional params passed in. + + >>> from cmislib.model import TYPES_COLL + >>> types = repo.getCollection(TYPES_COLL) + >>> len(types) + 4 + >>> types[0].getTypeId() + u'cmis:folder' + + Otherwise, the collection URL is invoked, and a :class:`ResultSet` is + returned. + + >>> from cmislib.model import CHECKED_OUT_COLL + >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) + >>> len(resultSet.getResults()) + 1 + """ + + pass + + capabilities = property(getCapabilities) + id = property(getRepositoryId) + info = property(getRepositoryInfo) + name = property(getRepositoryName) + rootFolder = property(getRootFolder) + permissionDefinitions = property(getPermissionDefinitions) + permissionMap = property(getPermissionMap) + propagation = property(getPropagation) + supportedPermissions = property(getSupportedPermissions) + + +class BrowserResultSet(object): + + """ + Represents a paged result set. In CMIS, this is most often an Atom feed. + """ + + def __iter__(self): + ''' Iterator for the result set ''' + return iter(self.getResults()) + + def __getitem__(self, index): + ''' Getter for the result set ''' + return self.getResults()[index] + + def __len__(self): + ''' Len method for the result set ''' + return len(self.getResults()) + + def reload(self): + + ''' + Re-invokes the self link for the current set of results. + + >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) + >>> resultSet.reload() + + ''' + + pass + + def getResults(self): + + ''' + Returns the results that were fetched and cached by the get*Page call. + + >>> resultSet = repo.getCheckedOutDocs() + >>> resultSet.hasNext() + False + >>> for result in resultSet.getResults(): + ... result + ... + + ''' + + pass + + def hasObject(self, objectId): + + ''' + Returns True if the specified objectId is found in the list of results, + otherwise returns False. + ''' + + pass + + def getFirst(self): + + ''' + Returns the first page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server returns a "first" link. Not all of them do. + + >>> resultSet.hasFirst() + True + >>> results = resultSet.getFirst() + >>> for result in results: + ... result + ... + + ''' + + pass + + def getPrev(self): + + ''' + Returns the prev page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server returns a "prev" link. Not all of them do. + >>> resultSet.hasPrev() + True + >>> results = resultSet.getPrev() + >>> for result in results: + ... result + ... + + ''' + + pass + + def getNext(self): + + ''' + Returns the next page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. + >>> resultSet.hasNext() + True + >>> results = resultSet.getNext() + >>> for result in results: + ... result + ... + + ''' + + pass + + def getLast(self): + + ''' + Returns the last page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server is returning a "last" link. Not all of them do. + + >>> resultSet.hasLast() + True + >>> results = resultSet.getLast() + >>> for result in results: + ... result + ... + + ''' + + pass + + def hasNext(self): + + ''' + Returns True if this page contains a next link. + + >>> resultSet.hasNext() + True + ''' + + pass + + def hasPrev(self): + + ''' + Returns True if this page contains a prev link. Not all CMIS providers + implement prev links consistently. + + >>> resultSet.hasPrev() + True + ''' + + pass + + def hasFirst(self): + + ''' + Returns True if this page contains a first link. Not all CMIS providers + implement first links consistently. + + >>> resultSet.hasFirst() + True + ''' + + pass + + def hasLast(self): + + ''' + Returns True if this page contains a last link. Not all CMIS providers + implement last links consistently. + + >>> resultSet.hasLast() + True + ''' + + pass + + +class BrowserDocument(CmisObject): + + """ + An object typically associated with file content. + """ + + def checkout(self): + + """ + Performs a checkout on the :class:`Document` and returns the + Private Working Copy (PWC), which is also an instance of + :class:`Document` + + >>> doc.getObjectId() + u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0' + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + """ + + pass + + def cancelCheckout(self): + """ + Cancels the checkout of this object by retrieving the Private Working + Copy (PWC) and then deleting it. After the PWC is deleted, this object + will be reloaded to update properties related to a checkout. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + pass + + def getPrivateWorkingCopy(self): + + """ + Retrieves the object using the object ID in the property: + cmis:versionSeriesCheckedOutId then uses getObject to instantiate + the object. + + >>> doc.isCheckedOut() + False + >>> doc.checkout() + + >>> pwc = doc.getPrivateWorkingCopy() + >>> pwc.getTitle() + u'sample-b (Working Copy).pdf' + """ + + pass + + def isCheckedOut(self): + + """ + Returns true if the document is checked out. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + pass + + def getCheckedOutBy(self): + + """ + Returns the ID who currently has the document checked out. + >>> pwc = doc.checkout() + >>> pwc.getCheckedOutBy() + u'admin' + """ + + pass + + def checkin(self, checkinComment=None, **kwargs): + + """ + Checks in this :class:`Document` which must be a private + working copy (PWC). + + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + >>> pwc.checkin() + + >>> doc.isCheckedOut() + False + + The following optional arguments are supported: + - major + - properties + - contentStream + - policies + - addACEs + - removeACEs + """ + + pass + + def getLatestVersion(self, **kwargs): + + """ + Returns a :class:`Document` object representing the latest version in + the version series. + + The following optional arguments are supported: + - major + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + + >>> latestDoc = doc.getLatestVersion() + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='false') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='true') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.0' + """ + + pass + + def getPropertiesOfLatestVersion(self, **kwargs): + + """ + Like :class:`^CmisObject.getProperties`, returns a dict of properties + from the latest version of this object in the version series. + + The optional major and filter arguments are supported. + """ + + pass + + def getAllVersions(self, **kwargs): + + """ + Returns a :class:`ResultSet` of document objects for the entire + version history of this object, including any PWC's. + + The optional filter and includeAllowableActions are + supported. + """ + + pass + + def getContentStream(self): + + """ + Returns the CMIS service response from invoking the 'enclosure' link. + + >>> doc.getName() + u'sample-b.pdf' + >>> o = open('tmp.pdf', 'wb') + >>> result = doc.getContentStream() + >>> o.write(result.read()) + >>> result.close() + >>> o.close() + >>> import os.path + >>> os.path.getsize('tmp.pdf') + 117248 + + The optional streamId argument is not yet supported. + """ + + pass + + def setContentStream(self, contentFile, contentType=None): + + """ + Sets the content stream on this object. + + The following optional arguments are not yet supported: + - overwriteFlag=None + """ + + pass + + def deleteContentStream(self): + + """ + Delete's the content stream associated with this object. + """ + + pass + + def getRenditions(self): + + """ + Returns an array of :class:`Rendition` objects. The repository + must support the Renditions capability. + + The following optional arguments are not currently supported: + - renditionFilter + - maxItems + - skipCount + """ + + pass + + checkedOut = property(isCheckedOut) + + def getPaths(self): + """ + Returns the Document's paths by asking for the parents with the + includeRelativePathSegment flag set to true, then concats the value + of cmis:path with the relativePathSegment. + """ + + pass + +class BrowserFolder(BrowserCmisObject): + + """ + A container object that can hold other :class:`CmisObject` objects + """ + + def createFolder(self, name, properties={}): + + """ + Creates a new :class:`Folder` using the properties provided. + Right now I expect a property called 'cmis:name' but I don't + complain if it isn't there (although the CMIS provider will). If a + cmis:name property isn't provided, the value passed in to the name + argument will be used. + + To specify a custom folder type, pass in a property called + cmis:objectTypeId set to the :class:`CmisId` representing the type ID + of the instance you want to create. If you do not pass in an object + type ID, an instance of 'cmis:folder' will be created. + + >>> subFolder = folder.createFolder('someSubfolder') + >>> subFolder.getName() + u'someSubfolder' + + The following optional arguments are not supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createDocumentFromString(self, + name, + properties={}, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> testFolder.createDocumentFromString('testdoc3', contentString='hello, world', contentType='text/plain') + """ + + pass + + def createDocument(self, name, properties={}, contentFile=None, + contentType=None, contentEncoding=None): + + """ + Creates a new Document object in the repository using + the properties provided. + + Right now this is basically the same as createFolder, + but this deals with contentStreams. The common logic should + probably be moved to CmisObject.createObject. + + The method will attempt to guess the appropriate content + type and encoding based on the file. To specify it yourself, pass them + in via the contentType and contentEncoding arguments. + + >>> f = open('250px-Cmis_logo.png', 'rb') + >>> subFolder.createDocument('logo.png', contentFile=f) + + >>> 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) + + >>> f.close() + + To specify a custom object type, pass in a property called + cmis:objectTypeId set to the :class:`CmisId` representing the type ID + of the instance you want to create. If you do not pass in an object + type ID, an instance of 'cmis:document' will be created. + + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def getChildren(self, **kwargs): + + """ + Returns a paged :class:`ResultSet`. The result set contains a list of + :class:`CmisObject` objects for each child of the Folder. The actual + type of the object returned depends on the object's CMIS base type id. + For example, the method might return a list that contains both + :class:`Document` objects and :class:`Folder` objects. + + >>> childrenRS = subFolder.getChildren() + >>> children = childrenRS.getResults() + + The following optional arguments are supported: + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + """ + + pass + + def getDescendants(self, **kwargs): + + """ + Gets the descendants of this folder. The descendants are returned as + a paged :class:`ResultSet` object. The result set contains a list of + :class:`CmisObject` objects where the actual type of each object + returned will vary depending on the object's base type id. For example, + the method might return a list that contains both :class:`Document` + objects and :class:`Folder` objects. + + The following optional argument is supported: + - depth. Use depth=-1 for all descendants, which is the default if no + depth is specified. + + >>> resultSet = folder.getDescendants() + >>> len(resultSet.getResults()) + 105 + >>> resultSet = folder.getDescendants(depth=1) + >>> len(resultSet.getResults()) + 103 + + The following optional arguments *may* also work but haven't been + tested: + + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + + """ + + pass + + def getTree(self, **kwargs): + + """ + Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`, + this method returns only the descendant objects that are folders. The + results do not include the current folder. + + The following optional arguments are supported: + - depth + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + + >>> rs = folder.getTree(depth='2') + >>> len(rs.getResults()) + 3 + >>> for folder in rs.getResults().values(): + ... folder.getTitle() + ... + u'subfolder2' + u'parent test folder' + u'subfolder' + """ + + pass + + def getParent(self): + + """ + The optional filter argument is not yet supported. + """ + + pass + + def deleteTree(self, **kwargs): + + """ + Deletes the folder and all of its descendant objects. + + >>> resultSet = subFolder.getDescendants() + >>> len(resultSet.getResults()) + 2 + >>> subFolder.deleteTree() + + The following optional arguments are supported: + - allVersions + - unfileObjects + - continueOnFailure + """ + + pass + + def addObject(self, cmisObject, **kwargs): + + """ + Adds the specified object as a child of this object. No new object is + created. The repository must support multifiling for this to work. + + >>> sub1 = repo.getObjectByPath("/cmislib/sub1") + >>> sub2 = repo.getObjectByPath("/cmislib/sub2") + >>> doc = sub1.createDocument("testdoc1") + >>> len(sub1.getChildren()) + 1 + >>> len(sub2.getChildren()) + 0 + >>> sub2.addObject(doc) + >>> len(sub2.getChildren()) + 1 + >>> sub2.getChildren()[0].name + u'testdoc1' + + The following optional arguments are supported: + - allVersions + """ + + pass + + def removeObject(self, cmisObject): + + """ + Removes the specified object from this folder. The repository must + support unfiling for this to work. + """ + + pass + + def getPaths(self): + """ + Returns the paths as a list of strings. The spec says folders cannot + be multi-filed, so this should always be one value. We return a list + to be symmetric with the same method in :class:`Document`. + """ + + pass + + +class BrowserRelationship(CmisObject): + + """ + Defines a relationship object between two :class:`CmisObjects` objects + """ + + def getSourceId(self): + + """ + Returns the :class:`CmisId` on the source side of the relationship. + """ + + pass + + def getTargetId(self): + + """ + Returns the :class:`CmisId` on the target side of the relationship. + """ + + pass + + def getSource(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the source side of the relationship. + """ + + pass + + def getTarget(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the target side of the relationship. + """ + + pass + + sourceId = property(getSourceId) + targetId = property(getTargetId) + source = property(getSource) + target = property(getTarget) + + +class BrowserPolicy(CmisObject): + + """ + An arbirary object that can 'applied' to objects that the + repository identifies as being 'controllable'. + """ + + pass + + +class BrowserObjectType(object): + + """ + Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'. + Contains metadata about the type. + """ + + def getTypeId(self): + + """ + Returns the type ID for this object. + + >>> docType = repo.getTypeDefinition('cmis:document') + >>> docType.getTypeId() + 'cmis:document' + """ + + pass + + def getLocalName(self): + """Getter for cmis:localName""" + pass + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + pass + + def getDisplayName(self): + """Getter for cmis:displayName""" + pass + + def getQueryName(self): + """Getter for cmis:queryName""" + pass + + def getDescription(self): + """Getter for cmis:description""" + pass + + def getBaseId(self): + """Getter for cmis:baseId""" + pass + + def isCreatable(self): + """Getter for cmis:creatable""" + pass + + def isFileable(self): + """Getter for cmis:fileable""" + pass + + def isQueryable(self): + """Getter for cmis:queryable""" + pass + + def isFulltextIndexed(self): + """Getter for cmis:fulltextIndexed""" + pass + + def isIncludedInSupertypeQuery(self): + """Getter for cmis:includedInSupertypeQuery""" + pass + + def isControllablePolicy(self): + """Getter for cmis:controllablePolicy""" + pass + + def isControllableACL(self): + """Getter for cmis:controllableACL""" + pass + + def getLink(self, rel, linkType): + + """ + Gets the HREF for the link element with the specified rel and linkType. + + >>> from cmislib.model import ATOM_XML_FEED_TYPE + >>> docType.getLink('down', ATOM_XML_FEED_TYPE) + u'http://localhost:8080/alfresco/s/cmis/type/cmis:document/children' + """ + + pass + + def getProperties(self): + + """ + Returns a list of :class:`Property` objects representing each property + defined for this type. + + >>> objType = repo.getTypeDefinition('cmis:relationship') + >>> for prop in objType.properties: + ... print 'Id:%s' % prop.id + ... print 'Cardinality:%s' % prop.cardinality + ... print 'Description:%s' % prop.description + ... print 'Display name:%s' % prop.displayName + ... print 'Local name:%s' % prop.localName + ... print 'Local namespace:%s' % prop.localNamespace + ... print 'Property type:%s' % prop.propertyType + ... print 'Query name:%s' % prop.queryName + ... print 'Updatability:%s' % prop.updatability + ... print 'Inherited:%s' % prop.inherited + ... print 'Orderable:%s' % prop.orderable + ... print 'Queryable:%s' % prop.queryable + ... print 'Required:%s' % prop.required + ... print 'Open choice:%s' % prop.openChoice + """ + + pass + + def reload(self, **kwargs): + """ + This method will reload the object's data from the CMIS service. + """ + pass + + id = property(getTypeId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + baseId = property(getBaseId) + creatable = property(isCreatable) + fileable = property(isFileable) + queryable = property(isQueryable) + fulltextIndexed = property(isFulltextIndexed) + includedInSupertypeQuery = property(isIncludedInSupertypeQuery) + controllablePolicy = property(isControllablePolicy) + controllableACL = property(isControllableACL) + properties = property(getProperties) + + +class BrowserProperty(object): + + """ + This class represents an attribute or property definition of an object + type. + """ + + def getId(self): + """Getter for cmis:id""" + pass + + def getLocalName(self): + """Getter for cmis:localName""" + pass + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + pass + + def getDisplayName(self): + """Getter for cmis:displayName""" + pass + + def getQueryName(self): + """Getter for cmis:queryName""" + pass + + def getDescription(self): + """Getter for cmis:description""" + pass + + def getPropertyType(self): + """Getter for cmis:propertyType""" + pass + + def getCardinality(self): + """Getter for cmis:cardinality""" + pass + + def getUpdatability(self): + """Getter for cmis:updatability""" + pass + + def isInherited(self): + """Getter for cmis:inherited""" + pass + + def isRequired(self): + """Getter for cmis:required""" + pass + + def isQueryable(self): + """Getter for cmis:queryable""" + pass + + def isOrderable(self): + """Getter for cmis:orderable""" + pass + + def isOpenChoice(self): + """Getter for cmis:openChoice""" + pass + + id = property(getId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + propertyType = property(getPropertyType) + cardinality = property(getCardinality) + updatability = property(getUpdatability) + inherited = property(isInherited) + required = property(isRequired) + queryable = property(isQueryable) + orderable = property(isOrderable) + openChoice = property(isOpenChoice) + + +class BrowserACL(object): + + """ + Represents the Access Control List for an object. + """ + + def addEntry(self, ace): + + """ + Adds an :class:`ACE` entry to the ACL. + + >>> acl = folder.getACL() + >>> acl.addEntry(ACE('jpotts', 'cmis:read', 'true')) + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + """ + + pass + + def removeEntry(self, principalId): + + """ + Removes the :class:`ACE` entry given a specific principalId. + + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + >>> acl.removeEntry('jsmith') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } + """ + + pass + + def clearEntries(self): + + """ + Clears all :class:`ACE` entries from the ACL and removes the internal + XML representation of the ACL. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> acl.entries + {'jpotts': , 'jsmith': } + >>> acl.getXmlDoc() + + >>> acl.clearEntries() + >>> acl.entries + >>> acl.getXmlDoc() + """ + + pass + + def getEntries(self): + + """ + Returns a dictionary of :class:`ACE` objects for each Access Control + Entry in the ACL. The key value is the ACE principalid. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> for ace in acl.entries.values(): + ... print 'principal:%s has the following permissions...' % ace.principalId + ... for perm in ace.permissions: + ... print perm + ... + principal:jpotts has the following permissions... + cmis:write + principal:jsmith has the following permissions... + cmis:write + """ + + pass + + entries = property(getEntries) + + +class BrowserACE(object): + + """ + Represents an individual Access Control Entry. + """ + + @property + def principalId(self): + """Getter for principalId""" + pass + + @property + def direct(self): + """Getter for direct""" + pass + + @property + def permissions(self): + """Getter for permissions""" + pass + + +class BrowserChangeEntry(object): + + """ + Represents a change log entry. Retrieve a list of change entries via + :meth:`Repository.getContentChanges`. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + """ + + def getId(self): + """ + Returns the unique ID of the change entry. + """ + pass + + def getObjectId(self): + """ + Returns the object ID of the object that changed. + """ + pass + + def getChangeType(self): + + """ + Returns the type of change that occurred. The resulting value must be + one of: + + - created + - updated + - deleted + - security + """ + pass + + def getACL(self): + + """ + Gets the :class:`ACL` object that is included with this Change Entry. + """ + + pass + + def getChangeTime(self): + + """ + Returns a datetime object representing the time the change occurred. + """ + + pass + + def getProperties(self): + + """ + Returns the properties of the change entry. Note that depending on the + capabilities of the repository ("capabilityChanges") the list may not + include the actual property values that changed. + """ + + pass + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + +class BrowserChangeEntryResultSet(ResultSet): + + """ + A specialized type of :class:`ResultSet` that knows how to instantiate + :class:`ChangeEntry` objects. The parent class assumes children of + :class:`CmisObject` which doesn't work for ChangeEntries. + """ + + def __iter__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return iter(self.getResults()) + + def __getitem__(self, index): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return self.getResults()[index] + + def __len__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return len(self.getResults()) + + def getResults(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + pass + + +class BrowserRendition(object): + + """ + This class represents a Rendition. + """ + + def __str__(self): + """To string""" + return self.getStreamId() + + def getStreamId(self): + """Getter for the rendition's stream ID""" + pass + + def getMimeType(self): + """Getter for the rendition's mime type""" + pass + + def getLength(self): + """Getter for the renditions's length""" + pass + + def getTitle(self): + """Getter for the renditions's title""" + pass + + def getKind(self): + """Getter for the renditions's kind""" + pass + + def getHeight(self): + """Getter for the renditions's height""" + pass + + def getWidth(self): + """Getter for the renditions's width""" + pass + + def getHref(self): + """Getter for the renditions's href""" + pass + + def getRenditionDocumentId(self): + """Getter for the renditions's width""" + pass + + streamId = property(getStreamId) + mimeType = property(getMimeType) + length = property(getLength) + title = property(getTitle) + kind = property(getKind) + height = property(getHeight) + width = property(getWidth) + href = property(getHref) + renditionDocumentId = property(getRenditionDocumentId) + + +class BrowserCmisId(str): + + """ + This is a marker class to be used for Strings that are used as CMIS ID's. + Making the objects instances of this class makes it easier to create the + Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of + cmis:propertyString. + """ + + pass + +def getSpecializedObject(obj, **kwargs): + + """ + Returns an instance of the appropriate :class:`CmisObject` class or one + of its child types depending on the specified baseType. + """ + + moduleLogger.debug('Inside getSpecializedObject') + + if 'cmis:baseTypeId' in obj.getProperties(): + baseType = obj.getProperties()['cmis:baseTypeId'] + if baseType == 'cmis:folder': + return BrowserFolder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:document': + return BrowserDocument(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:relationship': + return BrowserRelationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + if baseType == 'cmis:policy': + return BrowserPolicy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.data, **kwargs) + + # if the base type ID wasn't found in the props (this can happen when + # someone runs a query that doesn't select * or doesn't individually + # specify baseTypeId) or if the type isn't one of the known base + # types, give the object back + return obj diff --git a/src/cmislib/cmis_services.py b/src/cmislib/cmis_services.py new file mode 100644 index 0000000..a9726a7 --- /dev/null +++ b/src/cmislib/cmis_services.py @@ -0,0 +1,93 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +""" +Module containing the base Binding class and other service objects. +""" +import logging + +from cmislib.exceptions import CmisException, RuntimeException, \ + ObjectNotFoundException, InvalidArgumentException, \ + PermissionDeniedException, NotSupportedException, \ + UpdateConflictException + +logger = logging.getLogger(__name__) + + +class Binding(object): + + """ + Represents the binding used to communicate with the CMIS server. + """ + + def getRepositoryService(self): + + """ + Returns the repository service specific to this binding. + """ + + pass + + def _processCommonErrors(self, error, url): + + """ + Maps HTTPErrors that are common to all to exceptions. Only errors + that are truly global, like 401 not authorized, should be handled + here. Callers should handle the rest. + """ + + if error['status'] == '401': + raise PermissionDeniedException(error['status'], url) + elif error['status'] == '400': + raise InvalidArgumentException(error['status'], url) + elif error['status'] == '404': + raise ObjectNotFoundException(error['status'], url) + elif error['status'] == '403': + raise PermissionDeniedException(error['status'], url) + elif error['status'] == '405': + raise NotSupportedException(error['status'], url) + elif error['status'] == '409': + raise UpdateConflictException(error['status'], url) + elif error['status'] == '500': + raise RuntimeException(error['status'], url) + else: + raise CmisException(error['status'], url) + + +class RepositoryServiceIfc(object): + + """ + Defines the interface for the repository service. + """ + + def getRepositories(self, client): + + """ + Returns a list of repositories for this server. + """ + + pass + + def getRepositoryInfo(self): + + """ + Returns the repository information for this server. + """ + + pass diff --git a/src/cmislib/domain.py b/src/cmislib/domain.py new file mode 100644 index 0000000..a2f7a25 --- /dev/null +++ b/src/cmislib/domain.py @@ -0,0 +1,2210 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +""" +Module containing the domain objects used to work with a CMIS provider. +""" +import logging + +moduleLogger = logging.getLogger('cmislib.domain') + + +class CmisObject(object): + + """ + Common ancestor class for other CMIS domain objects such as + :class:`Document` and :class:`Folder`. + """ + + def __str__(self): + """To string""" + return self.getObjectId() + + def reload(self, **kwargs): + + """ + Fetches the latest representation of this object from the CMIS service. + Some methods, like :class:`^Document.checkout` do this for you. + + If you call reload with a properties filter, the filter will be in + effect on subsequent calls until the filter argument is changed. To + reset to the full list of properties, call reload with filter set to + '*'. + """ + + pass + + def getObjectId(self): + + """ + Returns the object ID for this object. + + >>> doc = resultSet.getResults()[0] + >>> doc.getObjectId() + u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' + """ + + pass + + def getObjectParents(self, **kwargs): + """ + Gets the parents of this object as a :class:`ResultSet`. + + The following optional arguments are supported: + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includeRelativePathSegment + """ + + pass + + def getPaths(self): + """ + Returns the object's paths as a list of strings. + """ + # see sub-classes for implementation + pass + + def getAllowableActions(self): + + """ + Returns a dictionary of allowable actions, keyed off of the action name. + + >>> actions = doc.getAllowableActions() + >>> for a in actions: + ... print "%s:%s" % (a,actions[a]) + ... + canDeleteContentStream:True + canSetContentStream:True + canCreateRelationship:True + canCheckIn:False + canApplyACL:False + canDeleteObject:True + canGetAllVersions:True + canGetObjectParents:True + canGetProperties:True + """ + + pass + + def getTitle(self): + + """ + Returns the value of the object's cmis:title property. + """ + + pass + + def getProperties(self): + + """ + Returns a dict of the object's properties. If CMIS returns an + empty element for a property, the property will be in the + dict with a value of None. + + >>> props = doc.getProperties() + >>> for p in props: + ... print "%s: %s" % (p, props[p]) + ... + cmis:contentStreamMimeType: text/html + cmis:creationDate: 2009-12-15T09:45:35.369-06:00 + cmis:baseTypeId: cmis:document + cmis:isLatestMajorVersion: false + cmis:isImmutable: false + cmis:isMajorVersion: false + cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339 + + The optional filter argument is not yet implemented. + """ + + pass + + def getName(self): + + """ + Returns the value of cmis:name from the getProperties() dictionary. + We don't need a getter for every standard CMIS property, but name + is a pretty common one so it seems to make sense. + + >>> doc.getName() + u'system-overview.html' + """ + + pass + + def updateProperties(self, properties): + + """ + Updates the properties of an object with the properties provided. + Only provide the set of properties that need to be updated. + + >>> folder = repo.getObjectByPath('/someFolder2') + >>> folder.getName() + u'someFolder2' + >>> props = {'cmis:name': 'someFolderFoo'} + >>> folder.updateProperties(props) + + >>> folder.getName() + u'someFolderFoo' + + """ + + pass + + def move(self, sourceFolder, targetFolder): + + """ + Moves an object from the source folder to the target folder. + + >>> sub1 = repo.getObjectByPath('/cmislib/sub1') + >>> sub2 = repo.getObjectByPath('/cmislib/sub2') + >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1') + >>> doc.move(sub1, sub2) + """ + + pass + + def delete(self, **kwargs): + + """ + Deletes this :class:`CmisObject` from the repository. Note that in the + case of a :class:`Folder` object, some repositories will refuse to + delete it if it contains children and some will delete it without + complaint. If what you really want to do is delete the folder and all + of its descendants, use :meth:`~Folder.deleteTree` instead. + + >>> folder.delete() + + The optional allVersions argument is supported. + """ + + pass + + def applyPolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def createRelationship(self, targetObj, relTypeId): + + """ + Creates a relationship between this object and a specified target + object using the relationship type specified. Returns the new + :class:`Relationship` object. + + >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + """ + + pass + + def getRelationships(self, **kwargs): + + """ + Returns a :class:`ResultSet` of :class:`Relationship` objects for each + relationship where the source is this object. + + >>> rels = tstDoc1.getRelationships() + >>> len(rels.getResults()) + 1 + >>> rel = rels.getResults().values()[0] + >>> rel.getProperties() + {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} + + The following optional arguments are supported: + - includeSubRelationshipTypes + - relationshipDirection + - typeId + - maxItems + - skipCount + - filter + - includeAllowableActions + """ + + pass + + def removePolicy(self, policyId): + + """ + This is not yet implemented. + """ + + pass + + def getAppliedPolicies(self): + + """ + This is not yet implemented. + """ + + pass + + def getACL(self): + + """ + Repository.getCapabilities['ACL'] must return manage or discover. + + >>> acl = folder.getACL() + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + + The optional onlyBasicPermissions argument is currently not supported. + """ + + pass + + def applyACL(self, acl): + + """ + Updates the object with the provided :class:`ACL`. + Repository.getCapabilities['ACL'] must return manage to invoke this + call. + + >>> acl = folder.getACL() + >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true')) + >>> acl.getEntries() + {u'GROUP_EVERYONE': , 'jdoe': } + """ + + pass + + allowableActions = property(getAllowableActions) + name = property(getName) + id = property(getObjectId) + properties = property(getProperties) + title = property(getTitle) + ACL = property(getACL) + + +class Repository(object): + """ + Represents a CMIS repository. Will lazily populate itself by + calling the repository CMIS service URL. + + You must pass in an instance of a CmisClient when creating an + instance of this class. + """ + + def __init__(self, cmisClient, xmlDoc=None): + """ Constructor """ + self._cmisClient = cmisClient + self.xmlDoc = xmlDoc + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + self.logger = logging.getLogger('cmislib.model.Repository') + self.logger.info('Creating an instance of Repository') + + def __str__(self): + """To string""" + return self.getRepositoryName() + + def _initData(self): + """ + This method clears out any local variables that would be out of sync + when data is re-fetched from the server. + """ + self._repositoryId = None + self._repositoryName = None + self._repositoryInfo = {} + self._capabilities = {} + self._uriTemplates = {} + self._permDefs = {} + self._permMap = {} + self._permissions = None + self._propagation = None + + def reload(self): + """ + This method will re-fetch the repository's XML data from the CMIS + repository. + """ + + pass + + def getRepositoryId(self): + + """ + Returns this repository's unique identifier + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryId() + u'83beb297-a6fa-4ac5-844b-98c871c0eea9' + """ + + pass + + def getRepositoryName(self): + + """ + Returns this repository's name + + >>> repo = client.getDefaultRepository() + >>> repo.getRepositoryName() + u'Main Repository' + """ + + pass + + def getRepositoryInfo(self): + + """ + Returns a dict of repository information. + + >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() + u'Main Repository' + >>> info = repo.getRepositoryInfo() + >>> for k,v in info.items(): + ... print "%s:%s" % (k,v) + ... + cmisSpecificationTitle:Version 1.0 Committee Draft 04 + cmisVersionSupported:1.0 + repositoryDescription:None + productVersion:3.2.0 (r2 2440) + rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 + repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 + repositoryName:Main Repository + vendorName:Alfresco + productName:Alfresco Repository (Community) + """ + + pass + + def getObjectByPath(self, path, **kwargs): + + """ + Returns an object given the path to the object. + + >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are not currently supported: + - filter + - includeAllowableActions + """ + + pass + + def getSupportedPermissions(self): + """ + Returns the value of the cmis:supportedPermissions element. Valid + values are: + + - basic: indicates that the CMIS Basic permissions are supported + - repository: indicates that repository specific permissions are supported + - both: indicates that both CMIS basic permissions and repository specific permissions are supported + + >>> repo.supportedPermissions + u'both' + """ + + pass + + def getPermissionDefinitions(self): + + """ + Returns a dictionary of permission definitions for this repository. The + key is the permission string or technical name of the permission + and the value is the permission description. + + >>> for permDef in repo.permissionDefinitions: + ... print permDef + ... + cmis:all + {http://www.alfresco.org/model/system/1.0}base.LinkChildren + {http://www.alfresco.org/model/content/1.0}folder.Consumer + {http://www.alfresco.org/model/security/1.0}All.All + {http://www.alfresco.org/model/system/1.0}base.CreateAssociations + {http://www.alfresco.org/model/system/1.0}base.FullControl + {http://www.alfresco.org/model/system/1.0}base.AddChildren + {http://www.alfresco.org/model/system/1.0}base.ReadAssociations + {http://www.alfresco.org/model/content/1.0}folder.Editor + {http://www.alfresco.org/model/content/1.0}cmobject.Editor + {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations + cmis:read + cmis:write + """ + + pass + + def getPermissionMap(self): + + """ + Returns a dictionary representing the permission mapping table where + each key is a permission key string and each value is a list of one or + more permissions the principal must have to perform the operation. + + >>> for (k,v) in repo.permissionMap.items(): + ... print 'To do this: %s, you must have these perms:' % k + ... for perm in v: + ... print perm + ... + To do this: canCreateFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canAddToFolder.Folder, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.CreateChildren + To do this: canDelete.Object, you must have these perms: + cmis:all + {http://www.alfresco.org/model/system/1.0}base.DeleteNode + To do this: canCheckin.Document, you must have these perms: + cmis:all + {http://www.alfresco.org/model/content/1.0}lockable.CheckIn + """ + + pass + + def getPropagation(self): + + """ + Returns the value of the cmis:propagation element. Valid values are: + - objectonly: indicates that the repository is able to apply ACEs + without changing the ACLs of other objects + - propagate: indicates that the repository is able to apply ACEs to a + given object and propagate this change to all inheriting objects + + >>> repo.propagation + u'propagate' + """ + + pass + + def getCapabilities(self): + + """ + Returns a dict of repository capabilities. + + >>> caps = repo.getCapabilities() + >>> for k,v in caps.items(): + ... print "%s:%s" % (k,v) + ... + PWCUpdatable:True + VersionSpecificFiling:False + Join:None + ContentStreamUpdatability:anytime + AllVersionsSearchable:False + Renditions:None + Multifiling:True + GetFolderTree:True + GetDescendants:True + ACL:None + PWCSearchable:True + Query:bothcombined + Unfiling:False + Changes:None + """ + + pass + + def getRootFolder(self): + """ + Returns the root folder of the repository + + >>> root = repo.getRootFolder() + >>> root.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + pass + + def getFolder(self, folderId): + + """ + Returns a :class:`Folder` object for a specified folderId + + >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') + >>> someFolder.getObjectId() + u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' + """ + + pass + + def getTypeChildren(self, + typeId=None): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + child types of the type specified by the typeId. + + If no typeId is provided, the result will be the same as calling + `self.getTypeDefinitions` + + These optional arguments are current unsupported: + - includePropertyDefinitions + - maxItems + - skipCount + + >>> baseTypes = repo.getTypeChildren() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + pass + + def getTypeDescendants(self, typeId=None, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects corresponding to the + descendant types of the type specified by the typeId. + + If no typeId is provided, the repository's "typesdescendants" URL + will be called to determine the list of descendant types. + + >>> allTypes = repo.getTypeDescendants() + >>> for aType in allTypes: + ... print aType.getTypeId() + ... + cmis:folder + F:cm:systemfolder + F:act:savedactionfolder + F:app:configurations + F:fm:forums + F:wcm:avmfolder + F:wcm:avmplainfolder + F:wca:webfolder + F:wcm:avmlayeredfolder + F:st:site + F:app:glossary + F:fm:topic + + These optional arguments are supported: + - depth + - includePropertyDefinitions + + >>> types = 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 + """ + + pass + + def getTypeDefinitions(self, **kwargs): + + """ + Returns a list of :class:`ObjectType` objects representing + the base types in the repository. + + >>> baseTypes = repo.getTypeDefinitions() + >>> for baseType in baseTypes: + ... print baseType.getTypeId() + ... + cmis:folder + cmis:relationship + cmis:document + cmis:policy + """ + + pass + + def getTypeDefinition(self, typeId): + + """ + Returns an :class:`ObjectType` object for the specified object type id. + + >>> folderType = repo.getTypeDefinition('cmis:folder') + """ + + pass + + def getLink(self, rel): + """ + Returns the HREF attribute of an Atom link element for the + specified rel. + """ + + pass + + def getCheckedOutDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently checked out. + + >>> rs = repo.getCheckedOutDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getCheckedOutDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a (Working Copy).pdf' + u'sample-b (Working Copy).pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + pass + + def getUnfiledDocs(self, **kwargs): + + """ + Returns a ResultSet of :class:`CmisObject` objects that + are currently unfiled. + + >>> rs = repo.getUnfiledDocs() + >>> len(rs.getResults()) + 2 + >>> for doc in repo.getUnfiledDocs().getResults(): + ... doc.getTitle() + ... + u'sample-a.pdf' + u'sample-b.pdf' + + These optional arguments are supported: + - folderId + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + """ + + pass + + def getObject(self, + objectId, + **kwargs): + + """ + Returns an object given the specified object ID. + + >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') + >>> doc.getTitle() + u'sample-b.pdf' + + The following optional arguments are supported: + - returnVersion + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + """ + + pass + + def query(self, statement, **kwargs): + + """ + Returns a list of :class:`CmisObject` objects based on the CMIS + Query Language passed in as the statement. The actual objects + returned will be instances of the appropriate child class based + on the object's base type ID. + + In order for the results to be properly instantiated as objects, + make sure you include 'cmis:objectId' as one of the fields in + your select statement, or just use "SELECT \*". + + If you want the search results to automatically be instantiated with + the appropriate sub-class of :class:`CmisObject` you must either + include cmis:baseTypeId as one of the fields in your select statement + or just use "SELECT \*". + + >>> q = "select * from cmis:document where cmis:name like '%test%'" + >>> resultSet = repo.query(q) + >>> len(resultSet.getResults()) + 1 + >>> resultSet.hasNext() + False + + The following optional arguments are supported: + - searchAllVersions + - includeRelationships + - renditionFilter + - includeAllowableActions + - maxItems + - skipCount + + >>> q = 'select * from cmis:document' + >>> rs = repo.query(q) + >>> len(rs.getResults()) + 148 + >>> rs = repo.query(q, maxItems='5') + >>> len(rs.getResults()) + 5 + >>> rs.hasNext() + True + """ + + pass + + def getContentChanges(self, **kwargs): + + """ + Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + + The following optional arguments are supported: + - changeLogToken + - includeProperties + - includePolicyIDs + - includeACL + - maxItems + + You can get the latest change log token by inspecting the repository + info via :meth:`Repository.getRepositoryInfo`. + + >>> repo.info['latestChangeLogToken'] + u'2692' + >>> rs = repo.getContentChanges(changeLogToken='2692') + >>> len(rs) + 1 + >>> rs[0].id + u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' + >>> rs[0].changeType + u'updated' + >>> rs[0].changeTime + datetime.datetime(2010, 2, 16, 20, 6, 37) + """ + + pass + + def createDocumentFromString(self, + name, + properties={}, + parentFolder=None, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') + + """ + + pass + + def createDocument(self, + name, + properties={}, + parentFolder=None, + contentFile=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new :class:`Document` object. If the repository + supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + To create a document with an associated contentFile, pass in a + File object. The method will attempt to guess the appropriate content + type and encoding based on the file. To specify it yourself, pass them + in via the contentType and contentEncoding arguments. + + >>> f = open('sample-a.pdf', 'rb') + >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) + + >>> f.close() + >>> doc.getTitle() + u'sample-a.pdf' + + The following optional arguments are not currently supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def createDocumentFromSource(self, + sourceId, + properties={}, + parentFolder=None): + """ + This is not yet implemented. + + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def createFolder(self, + parentFolder, + name, + properties={}): + + """ + Creates a new :class:`Folder` object in the specified parentFolder. + + >>> root = repo.getRootFolder() + >>> folder = repo.createFolder(root, 'someFolder2') + >>> folder.getTitle() + u'someFolder2' + >>> folder.getObjectId() + u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' + + The following optional arguments are not yet supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createRelationship(self, sourceObj, targetObj, relType): + """ + Creates a relationship of the specific type between a source object + and a target object and returns the new :class:`Relationship` object. + + The following optional arguments are not currently supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createPolicy(self, properties): + """ + This has not yet been implemented. + + The following optional arguments are not currently supported: + - folderId + - policies + - addACEs + - removeACEs + """ + + pass + + def getCollection(self, collectionType, **kwargs): + + """ + Returns a list of objects returned for the specified collection. + + If the query collection is requested, an exception will be raised. + That collection isn't meant to be retrieved. + + If the types collection is specified, the method returns the result of + `getTypeDefinitions` and ignores any optional params passed in. + + >>> from cmislib.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 + """ + + pass + + capabilities = property(getCapabilities) + id = property(getRepositoryId) + info = property(getRepositoryInfo) + name = property(getRepositoryName) + rootFolder = property(getRootFolder) + permissionDefinitions = property(getPermissionDefinitions) + permissionMap = property(getPermissionMap) + propagation = property(getPropagation) + supportedPermissions = property(getSupportedPermissions) + + +class ResultSet(object): + + """ + Represents a paged result set. In CMIS, this is most often an Atom feed. + """ + + def __iter__(self): + """ Iterator for the result set """ + return iter(self.getResults()) + + def __getitem__(self, index): + """ Getter for the result set """ + return self.getResults()[index] + + def __len__(self): + """ Len method for the result set """ + return len(self.getResults()) + + def reload(self): + + """ + Re-invokes the self link for the current set of results. + + >>> resultSet.reload() + + """ + + pass + + def getResults(self): + + """ + Returns the results that were fetched and cached by the get*Page call. + + >>> resultSet = repo.getCheckedOutDocs() + >>> resultSet.hasNext() + False + >>> for result in resultSet.getResults(): + ... result + ... + + """ + + pass + + def hasObject(self, objectId): + + """ + Returns True if the specified objectId is found in the list of results, + otherwise returns False. + """ + + pass + + def getFirst(self): + + """ + Returns the first page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server returns a "first" link. Not all of them do. + + >>> resultSet.hasFirst() + True + >>> results = resultSet.getFirst() + >>> for result in results: + ... result + ... + + """ + + pass + + def getPrev(self): + + """ + Returns the prev page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server returns a "prev" link. Not all of them do. + >>> resultSet.hasPrev() + True + >>> results = resultSet.getPrev() + >>> for result in results: + ... result + ... + + """ + + pass + + def getNext(self): + + """ + Returns the next page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. + >>> resultSet.hasNext() + True + >>> results = resultSet.getNext() + >>> for result in results: + ... result + ... + + """ + + pass + + def getLast(self): + + """ + Returns the last page of results as a dictionary of + :class:`CmisObject` objects or its appropriate sub-type. This only + works when the server is returning a "last" link. Not all of them do. + + >>> resultSet.hasLast() + True + >>> results = resultSet.getLast() + >>> for result in results: + ... result + ... + + """ + + pass + + def hasNext(self): + + """ + Returns True if this page contains a next link. + + >>> resultSet.hasNext() + True + """ + + pass + + def hasPrev(self): + + """ + Returns True if this page contains a prev link. Not all CMIS providers + implement prev links consistently. + + >>> resultSet.hasPrev() + True + """ + + pass + + def hasFirst(self): + + """ + Returns True if this page contains a first link. Not all CMIS providers + implement first links consistently. + + >>> resultSet.hasFirst() + True + """ + + pass + + def hasLast(self): + + """ + Returns True if this page contains a last link. Not all CMIS providers + implement last links consistently. + + >>> resultSet.hasLast() + True + """ + + pass + + +class Document(CmisObject): + + """ + An object typically associated with file content. + """ + + def checkout(self): + + """ + Performs a checkout on the :class:`Document` and returns the + Private Working Copy (PWC), which is also an instance of + :class:`Document` + + >>> doc.getObjectId() + u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0' + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + """ + + pass + + def cancelCheckout(self): + """ + Cancels the checkout of this object by retrieving the Private Working + Copy (PWC) and then deleting it. After the PWC is deleted, this object + will be reloaded to update properties related to a checkout. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + pass + + def getPrivateWorkingCopy(self): + + """ + Retrieves the object using the object ID in the property: + cmis:versionSeriesCheckedOutId then uses getObject to instantiate + the object. + + >>> doc.isCheckedOut() + False + >>> doc.checkout() + + >>> pwc = doc.getPrivateWorkingCopy() + >>> pwc.getTitle() + u'sample-b (Working Copy).pdf' + """ + + pass + + def isCheckedOut(self): + + """ + Returns true if the document is checked out. + + >>> doc.isCheckedOut() + True + >>> doc.cancelCheckout() + >>> doc.isCheckedOut() + False + """ + + pass + + def getCheckedOutBy(self): + + """ + Returns the ID who currently has the document checked out. + >>> pwc = doc.checkout() + >>> pwc.getCheckedOutBy() + u'admin' + """ + + pass + + def checkin(self, checkinComment=None, **kwargs): + + """ + Checks in this :class:`Document` which must be a private + working copy (PWC). + + >>> doc.isCheckedOut() + False + >>> pwc = doc.checkout() + >>> doc.isCheckedOut() + True + >>> pwc.checkin() + + >>> doc.isCheckedOut() + False + + The following optional arguments are supported: + - major + - properties + - contentStream + - policies + - addACEs + - removeACEs + """ + + pass + + def getLatestVersion(self, **kwargs): + + """ + Returns a :class:`Document` object representing the latest version in + the version series. + + The following optional arguments are supported: + - major + - filter + - includeRelationships + - includePolicyIds + - renditionFilter + - includeACL + - includeAllowableActions + + >>> latestDoc = doc.getLatestVersion() + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='false') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.1' + >>> latestDoc = doc.getLatestVersion(major='true') + >>> latestDoc.getProperties()['cmis:versionLabel'] + u'2.0' + """ + + pass + + def getPropertiesOfLatestVersion(self, **kwargs): + + """ + Like :class:`^CmisObject.getProperties`, returns a dict of properties + from the latest version of this object in the version series. + + The optional major and filter arguments are supported. + """ + + pass + + def getAllVersions(self, **kwargs): + + """ + Returns a :class:`ResultSet` of document objects for the entire + version history of this object, including any PWC's. + + The optional filter and includeAllowableActions are + supported. + """ + + pass + + def getContentStream(self): + + """ + Returns the CMIS service response from invoking the 'enclosure' link. + + >>> doc.getName() + u'sample-b.pdf' + >>> o = open('tmp.pdf', 'wb') + >>> result = doc.getContentStream() + >>> o.write(result.read()) + >>> result.close() + >>> o.close() + >>> import os.path + >>> os.path.getsize('tmp.pdf') + 117248 + + The optional streamId argument is not yet supported. + """ + + pass + + def setContentStream(self, contentFile, contentType=None): + + """ + Sets the content stream on this object. + + The following optional arguments are not yet supported: + - overwriteFlag=None + """ + + pass + + def deleteContentStream(self): + + """ + Delete's the content stream associated with this object. + """ + + pass + + def getRenditions(self): + + """ + Returns an array of :class:`Rendition` objects. The repository + must support the Renditions capability. + + The following optional arguments are not currently supported: + - renditionFilter + - maxItems + - skipCount + """ + + pass + + checkedOut = property(isCheckedOut) + + def getPaths(self): + """ + Returns the Document's paths by asking for the parents with the + includeRelativePathSegment flag set to true, then concats the value + of cmis:path with the relativePathSegment. + """ + + pass + + +class Folder(CmisObject): + + """ + A container object that can hold other :class:`CmisObject` objects + """ + + def createFolder(self, name, properties={}): + + """ + Creates a new :class:`Folder` using the properties provided. + Right now I expect a property called 'cmis:name' but I don't + complain if it isn't there (although the CMIS provider will). If a + cmis:name property isn't provided, the value passed in to the name + argument will be used. + + To specify a custom folder type, pass in a property called + cmis:objectTypeId set to the :class:`CmisId` representing the type ID + of the instance you want to create. If you do not pass in an object + type ID, an instance of 'cmis:folder' will be created. + + >>> subFolder = folder.createFolder('someSubfolder') + >>> subFolder.getName() + u'someSubfolder' + + The following optional arguments are not supported: + - policies + - addACEs + - removeACEs + """ + + pass + + def createDocumentFromString(self, + name, + properties={}, + contentString=None, + contentType=None, + contentEncoding=None): + + """ + Creates a new document setting the content to the string provided. If + the repository supports unfiled objects, you do not have to pass in + a parent :class:`Folder` otherwise it is required. + + This method is essentially a convenience method that wraps your string + with a StringIO and then calls createDocument. + + >>> testFolder.createDocumentFromString('testdoc3', contentString='hello, world', contentType='text/plain') + """ + + pass + + def createDocument(self, name, properties={}, contentFile=None, + contentType=None, contentEncoding=None): + + """ + Creates a new Document object in the repository using + the properties provided. + + Right now this is basically the same as createFolder, + but this deals with contentStreams. The common logic should + probably be moved to CmisObject.createObject. + + The method will attempt to guess the appropriate content + type and encoding based on the file. To specify it yourself, pass them + in via the contentType and contentEncoding arguments. + + >>> f = open('250px-Cmis_logo.png', 'rb') + >>> subFolder.createDocument('logo.png', contentFile=f) + + >>> 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) + + >>> f.close() + + To specify a custom object type, pass in a property called + cmis:objectTypeId set to the :class:`CmisId` representing the type ID + of the instance you want to create. If you do not pass in an object + type ID, an instance of 'cmis:document' will be created. + + The following optional arguments are not yet supported: + - versioningState + - policies + - addACEs + - removeACEs + """ + + pass + + def getChildren(self, **kwargs): + + """ + Returns a paged :class:`ResultSet`. The result set contains a list of + :class:`CmisObject` objects for each child of the Folder. The actual + type of the object returned depends on the object's CMIS base type id. + For example, the method might return a list that contains both + :class:`Document` objects and :class:`Folder` objects. + + >>> childrenRS = subFolder.getChildren() + >>> children = childrenRS.getResults() + + The following optional arguments are supported: + - maxItems + - skipCount + - orderBy + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + """ + + pass + + def getDescendants(self, **kwargs): + + """ + Gets the descendants of this folder. The descendants are returned as + a paged :class:`ResultSet` object. The result set contains a list of + :class:`CmisObject` objects where the actual type of each object + returned will vary depending on the object's base type id. For example, + the method might return a list that contains both :class:`Document` + objects and :class:`Folder` objects. + + The following optional argument is supported: + - depth. Use depth=-1 for all descendants, which is the default if no + depth is specified. + + >>> resultSet = folder.getDescendants() + >>> len(resultSet.getResults()) + 105 + >>> resultSet = folder.getDescendants(depth=1) + >>> len(resultSet.getResults()) + 103 + + The following optional arguments *may* also work but haven't been + tested: + + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + + """ + + pass + + def getTree(self, **kwargs): + + """ + Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`, + this method returns only the descendant objects that are folders. The + results do not include the current folder. + + The following optional arguments are supported: + - depth + - filter + - includeRelationships + - renditionFilter + - includeAllowableActions + - includePathSegment + + >>> rs = folder.getTree(depth='2') + >>> len(rs.getResults()) + 3 + >>> for folder in rs.getResults().values(): + ... folder.getTitle() + ... + u'subfolder2' + u'parent test folder' + u'subfolder' + """ + + pass + + def getParent(self): + + """ + The optional filter argument is not yet supported. + """ + + pass + + def deleteTree(self, **kwargs): + + """ + Deletes the folder and all of its descendant objects. + + >>> resultSet = subFolder.getDescendants() + >>> len(resultSet.getResults()) + 2 + >>> subFolder.deleteTree() + + The following optional arguments are supported: + - allVersions + - unfileObjects + - continueOnFailure + """ + + pass + + def addObject(self, cmisObject, **kwargs): + + """ + Adds the specified object as a child of this object. No new object is + created. The repository must support multifiling for this to work. + + >>> sub1 = repo.getObjectByPath("/cmislib/sub1") + >>> sub2 = repo.getObjectByPath("/cmislib/sub2") + >>> doc = sub1.createDocument("testdoc1") + >>> len(sub1.getChildren()) + 1 + >>> len(sub2.getChildren()) + 0 + >>> sub2.addObject(doc) + >>> len(sub2.getChildren()) + 1 + >>> sub2.getChildren()[0].name + u'testdoc1' + + The following optional arguments are supported: + - allVersions + """ + + pass + + def removeObject(self, cmisObject): + + """ + Removes the specified object from this folder. The repository must + support unfiling for this to work. + """ + + pass + + def getPaths(self): + """ + Returns the paths as a list of strings. The spec says folders cannot + be multi-filed, so this should always be one value. We return a list + to be symmetric with the same method in :class:`Document`. + """ + + pass + + +class Relationship(CmisObject): + + """ + Defines a relationship object between two :class:`CmisObjects` objects + """ + + def getSourceId(self): + + """ + Returns the :class:`CmisId` on the source side of the relationship. + """ + + pass + + def getTargetId(self): + + """ + Returns the :class:`CmisId` on the target side of the relationship. + """ + + pass + + def getSource(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the source side of the relationship. + """ + + pass + + def getTarget(self): + + """ + Returns an instance of the appropriate child-type of :class:`CmisObject` + for the target side of the relationship. + """ + + pass + + sourceId = property(getSourceId) + targetId = property(getTargetId) + source = property(getSource) + target = property(getTarget) + + +class Policy(CmisObject): + + """ + An arbirary object that can 'applied' to objects that the + repository identifies as being 'controllable'. + """ + + pass + + +class ObjectType(object): + + """ + Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'. + Contains metadata about the type. + """ + + def getTypeId(self): + + """ + Returns the type ID for this object. + + >>> docType = repo.getTypeDefinition('cmis:document') + >>> docType.getTypeId() + 'cmis:document' + """ + + pass + + def getLocalName(self): + """Getter for cmis:localName""" + pass + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + pass + + def getDisplayName(self): + """Getter for cmis:displayName""" + pass + + def getQueryName(self): + """Getter for cmis:queryName""" + pass + + def getDescription(self): + """Getter for cmis:description""" + pass + + def getBaseId(self): + """Getter for cmis:baseId""" + pass + + def isCreatable(self): + """Getter for cmis:creatable""" + pass + + def isFileable(self): + """Getter for cmis:fileable""" + pass + + def isQueryable(self): + """Getter for cmis:queryable""" + pass + + def isFulltextIndexed(self): + """Getter for cmis:fulltextIndexed""" + pass + + def isIncludedInSupertypeQuery(self): + """Getter for cmis:includedInSupertypeQuery""" + pass + + def isControllablePolicy(self): + """Getter for cmis:controllablePolicy""" + pass + + def isControllableACL(self): + """Getter for cmis:controllableACL""" + pass + + def getLink(self, rel, linkType): + + """ + Gets the HREF for the link element with the specified rel and linkType. + + >>> from cmislib.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' + """ + + pass + + def getProperties(self): + + """ + Returns a list of :class:`Property` objects representing each property + defined for this type. + + >>> objType = repo.getTypeDefinition('cmis:relationship') + >>> for prop in objType.properties: + ... print 'Id:%s' % prop.id + ... print 'Cardinality:%s' % prop.cardinality + ... print 'Description:%s' % prop.description + ... print 'Display name:%s' % prop.displayName + ... print 'Local name:%s' % prop.localName + ... print 'Local namespace:%s' % prop.localNamespace + ... print 'Property type:%s' % prop.propertyType + ... print 'Query name:%s' % prop.queryName + ... print 'Updatability:%s' % prop.updatability + ... print 'Inherited:%s' % prop.inherited + ... print 'Orderable:%s' % prop.orderable + ... print 'Queryable:%s' % prop.queryable + ... print 'Required:%s' % prop.required + ... print 'Open choice:%s' % prop.openChoice + """ + + pass + + def reload(self, **kwargs): + """ + This method will reload the object's data from the CMIS service. + """ + pass + + id = property(getTypeId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + baseId = property(getBaseId) + creatable = property(isCreatable) + fileable = property(isFileable) + queryable = property(isQueryable) + fulltextIndexed = property(isFulltextIndexed) + includedInSupertypeQuery = property(isIncludedInSupertypeQuery) + controllablePolicy = property(isControllablePolicy) + controllableACL = property(isControllableACL) + properties = property(getProperties) + + +class Property(object): + + """ + This class represents an attribute or property definition of an object + type. + """ + + def getId(self): + """Getter for cmis:id""" + pass + + def getLocalName(self): + """Getter for cmis:localName""" + pass + + def getLocalNamespace(self): + """Getter for cmis:localNamespace""" + pass + + def getDisplayName(self): + """Getter for cmis:displayName""" + pass + + def getQueryName(self): + """Getter for cmis:queryName""" + pass + + def getDescription(self): + """Getter for cmis:description""" + pass + + def getPropertyType(self): + """Getter for cmis:propertyType""" + pass + + def getCardinality(self): + """Getter for cmis:cardinality""" + pass + + def getUpdatability(self): + """Getter for cmis:updatability""" + pass + + def isInherited(self): + """Getter for cmis:inherited""" + pass + + def isRequired(self): + """Getter for cmis:required""" + pass + + def isQueryable(self): + """Getter for cmis:queryable""" + pass + + def isOrderable(self): + """Getter for cmis:orderable""" + pass + + def isOpenChoice(self): + """Getter for cmis:openChoice""" + pass + + id = property(getId) + localName = property(getLocalName) + localNamespace = property(getLocalNamespace) + displayName = property(getDisplayName) + queryName = property(getQueryName) + description = property(getDescription) + propertyType = property(getPropertyType) + cardinality = property(getCardinality) + updatability = property(getUpdatability) + inherited = property(isInherited) + required = property(isRequired) + queryable = property(isQueryable) + orderable = property(isOrderable) + openChoice = property(isOpenChoice) + + +class ACL(object): + + """ + Represents the Access Control List for an object. + """ + + def addEntry(self, principalId, access, direct): + + """ + Adds an :class:`ACE` entry to the ACL. + + >>> acl = folder.getACL() + >>> acl.addEntry('jpotts', 'cmis:read', 'true') + >>> acl.addEntry('jsmith', 'cmis:write', 'true') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + """ + + pass + + def removeEntry(self, principalId): + + """ + Removes the :class:`ACE` entry given a specific principalId. + + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } + >>> acl.removeEntry('jsmith') + >>> acl.getEntries() + {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } + """ + + pass + + def clearEntries(self): + + """ + Clears all :class:`ACE` entries from the ACL and removes the internal + XML representation of the ACL. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> acl.entries + {'jpotts': , 'jsmith': } + >>> acl.getXmlDoc() + + >>> acl.clearEntries() + >>> acl.entries + >>> acl.getXmlDoc() + """ + + pass + + def getEntries(self): + + """ + Returns a dictionary of :class:`ACE` objects for each Access Control + Entry in the ACL. The key value is the ACE principalid. + + >>> acl = ACL() + >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) + >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) + >>> for ace in acl.entries.values(): + ... print 'principal:%s has the following permissions...' % ace.principalId + ... for perm in ace.permissions: + ... print perm + ... + principal:jpotts has the following permissions... + cmis:write + principal:jsmith has the following permissions... + cmis:write + """ + + pass + + entries = property(getEntries) + + +class ACE(object): + + """ + Represents an individual Access Control Entry. + """ + + def __init__(self, principalId=None, permissions=None, direct=None): + """Constructor""" + self._principalId = principalId + if permissions: + if isinstance(permissions, str): + self._permissions = [permissions] + else: + self._permissions = permissions + self._direct = direct + + self.logger = logging.getLogger('cmislib.model.ACE') + self.logger.info('Creating an instance of ACE for %s' % principalId) + + @property + def principalId(self): + """Getter for principalId""" + return self._principalId + + @property + def direct(self): + """Getter for direct""" + return self._direct + + @property + def permissions(self): + """Getter for permissions""" + return self._permissions + + +class ChangeEntry(object): + + """ + Represents a change log entry. Retrieve a list of change entries via + :meth:`Repository.getContentChanges`. + + >>> for changeEntry in rs: + ... changeEntry.objectId + ... changeEntry.id + ... changeEntry.changeType + ... changeEntry.changeTime + ... + 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' + u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' + u'created' + datetime.datetime(2010, 2, 11, 12, 55, 14) + 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' + u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' + u'updated' + datetime.datetime(2010, 2, 11, 12, 55, 13) + 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' + u'updated' + """ + + def getId(self): + """ + Returns the unique ID of the change entry. + """ + pass + + def getObjectId(self): + """ + Returns the object ID of the object that changed. + """ + pass + + def getChangeType(self): + + """ + Returns the type of change that occurred. The resulting value must be + one of: + + - created + - updated + - deleted + - security + """ + pass + + def getACL(self): + + """ + Gets the :class:`ACL` object that is included with this Change Entry. + """ + + pass + + def getChangeTime(self): + + """ + Returns a datetime object representing the time the change occurred. + """ + + pass + + def getProperties(self): + + """ + Returns the properties of the change entry. Note that depending on the + capabilities of the repository ("capabilityChanges") the list may not + include the actual property values that changed. + """ + + pass + + id = property(getId) + objectId = property(getObjectId) + changeTime = property(getChangeTime) + changeType = property(getChangeType) + properties = property(getProperties) + + +class ChangeEntryResultSet(ResultSet): + + """ + A specialized type of :class:`ResultSet` that knows how to instantiate + :class:`ChangeEntry` objects. The parent class assumes children of + :class:`CmisObject` which doesn't work for ChangeEntries. + """ + + def __iter__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return iter(self.getResults()) + + def __getitem__(self, index): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return self.getResults()[index] + + def __len__(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + return len(self.getResults()) + + def getResults(self): + + """ + Overriding to make it work with a list instead of a dict. + """ + + pass + + +class Rendition(object): + + """ + This class represents a Rendition. + """ + + def __str__(self): + """To string""" + return self.getStreamId() + + def getStreamId(self): + """Getter for the rendition's stream ID""" + pass + + def getMimeType(self): + """Getter for the rendition's mime type""" + pass + + def getLength(self): + """Getter for the renditions's length""" + pass + + def getTitle(self): + """Getter for the renditions's title""" + pass + + def getKind(self): + """Getter for the renditions's kind""" + pass + + def getHeight(self): + """Getter for the renditions's height""" + pass + + def getWidth(self): + """Getter for the renditions's width""" + pass + + def getHref(self): + """Getter for the renditions's href""" + pass + + def getRenditionDocumentId(self): + """Getter for the renditions's width""" + pass + + streamId = property(getStreamId) + mimeType = property(getMimeType) + length = property(getLength) + title = property(getTitle) + kind = property(getKind) + height = property(getHeight) + width = property(getWidth) + href = property(getHref) + renditionDocumentId = property(getRenditionDocumentId) + + +class CmisId(str): + + """ + This is a marker class to be used for Strings that are used as CMIS ID's. + Making the objects instances of this class makes it easier to create the + Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of + cmis:propertyString. + """ + + pass diff --git a/src/cmislib/exceptions.py b/src/cmislib/exceptions.py index 84a988c..207280d 100644 --- a/src/cmislib/exceptions.py +++ b/src/cmislib/exceptions.py @@ -17,7 +17,9 @@ # under the License. # - +""" +Module containing exceptions. +""" class CmisException(Exception): """ diff --git a/src/cmislib/messages.py b/src/cmislib/messages.py index 8103c72..b0ff6cc 100644 --- a/src/cmislib/messages.py +++ b/src/cmislib/messages.py @@ -17,5 +17,8 @@ # under the License. # +""" +Common strings. +""" NO_ACL_SUPPORT = 'This repository does not support ACLs' NO_CHANGE_LOG_SUPPORT = 'This repository does not support change logs' diff --git a/src/cmislib/model.py b/src/cmislib/model.py index 562f2ea..3346942 100644 --- a/src/cmislib/model.py +++ b/src/cmislib/model.py @@ -17,78 +17,18 @@ # under the License. # """ -Module containing the domain objects used to work with a CMIS provider. +Module containing the CmisClient object, which is responsible for +keeping track of connection information. The name 'model' is no longer +really appropriate, but it is kept for backwards compatibility. """ -from net import RESTService as Rest -from exceptions import CmisException, RuntimeException, \ - ObjectNotFoundException, InvalidArgumentException, \ - PermissionDeniedException, NotSupportedException, \ - UpdateConflictException -import messages - -from urllib import quote -from urllib2 import HTTPError -from urlparse import urlparse, urlunparse -import re -import mimetypes -from xml.parsers.expat import ExpatError -import datetime -import time -import iso8601 -import StringIO import logging -# would kind of like to not have any parsing logic in this module, -# but for now I'm going to put the serial/deserialization in methods -# of the CMIS object classes -from xml.dom import minidom +from cmislib.atompub.binding import AtomPubBinding +from cmislib.cmis_services import 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' moduleLogger = logging.getLogger('cmislib.model') - class CmisClient(object): """ @@ -110,6 +50,10 @@ class CmisClient(object): self.username = username self.password = password self.extArgs = kwargs + if 'binding' in kwargs and (isinstance(kwargs['binding'], Binding)): + self.binding = kwargs['binding'] + else: + self.binding = AtomPubBinding(**kwargs) self.logger = logging.getLogger('cmislib.model.CmisClient') self.logger.info('Creating an instance of CmisClient') @@ -128,21 +72,7 @@ class CmisClient(object): [{'repositoryName': u'Main Repository', 'repositoryId': u'83beb297-a6fa-4ac5-844b-98c871c0eea9'}] """ - result = self.get(self.repositoryUrl, **self.extArgs) - if (type(result) == HTTPError): - raise RuntimeException() - - workspaceElements = result.getElementsByTagNameNS(APP_NS, 'workspace') - # instantiate a Repository object using every workspace element - # in the service URL then ask the repository object for its ID - # and name, and return that back - - repositories = [] - for node in [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE]: - repository = Repository(self, node) - repositories.append({'repositoryId': repository.getRepositoryId(), - 'repositoryName': repository.getRepositoryInfo()['repositoryName']}) - return repositories + return self.binding.getRepositoryService().getRepositories(self) def getRepository(self, repositoryId): @@ -153,16 +83,7 @@ class CmisClient(object): >>> repo.getRepositoryName() u'Main Repository' """ - - doc = self.get(self.repositoryUrl, **self.extArgs) - workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') - - for workspaceElement in workspaceElements: - idElement = workspaceElement.getElementsByTagNameNS(CMIS_NS, 'repositoryId') - if idElement[0].childNodes[0].data == repositoryId: - return Repository(self, workspaceElement) - - raise ObjectNotFoundException(url=self.repositoryUrl) + return self.binding.getRepositoryService().getRepository(self, repositoryId) def getDefaultRepository(self): @@ -176,4076 +97,7 @@ class CmisClient(object): u'83beb297-a6fa-4ac5-844b-98c871c0eea9' """ - doc = self.get(self.repositoryUrl, **self.extArgs) - workspaceElements = doc.getElementsByTagNameNS(APP_NS, 'workspace') - # instantiate a Repository object with the first workspace - # element we find - repository = Repository(self, [e for e in workspaceElements if e.nodeType == e.ELEMENT_NODE][0]) - return repository - - def get(self, url, **kwargs): - - """ - Does a get against the CMIS service. More than likely, you will not - need to call this method. Instead, let the other objects do it for you. - - For example, if you need to get a specific object by object id, try - :class:`Repository.getObject`. If you have a path instead of an object - id, use :class:`Repository.getObjectByPath`. Or, you could start with - the root folder (:class:`Repository.getRootFolder`) and drill down from - there. - """ - - # merge the cmis client extended args with the ones that got passed in - if (len(self.extArgs) > 0): - kwargs.update(self.extArgs) - - result = Rest().get(url, - username=self.username, - password=self.password, - **kwargs) - if type(result) == HTTPError: - self._processCommonErrors(result) - return result - else: - try: - return minidom.parse(result) - except ExpatError: - raise CmisException('Could not parse server response', url) - - def delete(self, url, **kwargs): - - """ - Does a delete against the CMIS service. More than likely, you will not - need to call this method. Instead, let the other objects do it for you. - - For example, to delete a folder you'd call :class:`Folder.delete` and - to delete a document you'd call :class:`Document.delete`. - """ - - # merge the cmis client extended args with the ones that got passed in - if (len(self.extArgs) > 0): - kwargs.update(self.extArgs) - - result = Rest().delete(url, - username=self.username, - password=self.password, - **kwargs) - if type(result) == HTTPError: - self._processCommonErrors(result) - return result - else: - pass - - def post(self, url, payload, contentType, **kwargs): - - """ - Does a post against the CMIS service. More than likely, you will not - need to call this method. Instead, let the other objects do it for you. - - For example, to update the properties on an object, you'd call - :class:`CmisObject.updateProperties`. Or, to check in a document that's - been checked out, you'd call :class:`Document.checkin` on the PWC. - """ - - # merge the cmis client extended args with the ones that got passed in - if (len(self.extArgs) > 0): - kwargs.update(self.extArgs) - - result = Rest().post(url, - payload, - contentType, - username=self.username, - password=self.password, - **kwargs) - if type(result) != HTTPError: - try: - return minidom.parse(result) - except ExpatError: - raise CmisException('Could not parse server response', url) - elif result.code == 201: - try: - return minidom.parse(result) - except ExpatError: - raise CmisException('Could not parse server response', url) - else: - self._processCommonErrors(result) - return result - - def put(self, url, payload, contentType, **kwargs): - - """ - Does a put against the CMIS service. More than likely, you will not - need to call this method. Instead, let the other objects do it for you. - - For example, to update the properties on an object, you'd call - :class:`CmisObject.updateProperties`. Or, to check in a document that's - been checked out, you'd call :class:`Document.checkin` on the PWC. - """ - - # merge the cmis client extended args with the ones that got passed in - if (len(self.extArgs) > 0): - kwargs.update(self.extArgs) - - result = Rest().put(url, - payload, - contentType, - username=self.username, - password=self.password, - **kwargs) - if type(result) == HTTPError: - self._processCommonErrors(result) - return result - else: - #if result.headers['content-length'] != '0': - try: - return minidom.parse(result) - except ExpatError: - # This may happen and is normal - return None - - def _processCommonErrors(self, error): - - """ - Maps HTTPErrors that are common to all to exceptions. Only errors - that are truly global, like 401 not authorized, should be handled - here. Callers should handle the rest. - """ - - if error.status == 401: - raise PermissionDeniedException(error.status, error.url) - elif error.status == 400: - raise InvalidArgumentException(error.status, error.url) - elif error.status == 404: - raise ObjectNotFoundException(error.status, error.url) - elif error.status == 403: - raise PermissionDeniedException(error.status, error.url) - elif error.status == 405: - raise NotSupportedException(error.status, error.url) - elif error.status == 409: - raise UpdateConflictException(error.status, error.url) - elif error.status == 500: - raise RuntimeException(error.status, error.url) + return self.binding.getRepositoryService().getDefaultRepository(self) defaultRepository = property(getDefaultRepository) repositories = property(getRepositories) - - -class Repository(object): - - """ - Represents a CMIS repository. Will lazily populate itself by - calling the repository CMIS service URL. - - You must pass in an instance of a CmisClient when creating an - instance of this class. - """ - - def __init__(self, cmisClient, xmlDoc=None): - """ Constructor """ - self._cmisClient = cmisClient - self.xmlDoc = xmlDoc - self._repositoryId = None - self._repositoryName = None - self._repositoryInfo = {} - self._capabilities = {} - self._uriTemplates = {} - self._permDefs = {} - self._permMap = {} - self._permissions = None - self._propagation = None - self.logger = logging.getLogger('cmislib.model.Repository') - self.logger.info('Creating an instance of Repository') - - def __str__(self): - """To string""" - return self.getRepositoryName() - - def reload(self): - """ - This method will re-fetch the repository's XML data from the CMIS - repository. - """ - self.logger.debug('Reload called on object') - self.xmlDoc = self._cmisClient.get(self._cmisClient.repositoryUrl.encode('utf-8')) - self._initData() - - def _initData(self): - """ - This method clears out any local variables that would be out of sync - when data is re-fetched from the server. - """ - self._repositoryId = None - self._repositoryName = None - self._repositoryInfo = {} - self._capabilities = {} - self._uriTemplates = {} - self._permDefs = {} - self._permMap = {} - self._permissions = None - self._propagation = None - - def getSupportedPermissions(self): - - """ - Returns the value of the cmis:supportedPermissions element. Valid - values are: - - - basic: indicates that the CMIS Basic permissions are supported - - repository: indicates that repository specific permissions are supported - - both: indicates that both CMIS basic permissions and repository specific permissions are supported - - >>> repo.supportedPermissions - u'both' - """ - - if not self.getCapabilities()['ACL']: - raise NotSupportedException(messages.NO_ACL_SUPPORT) - - if not self._permissions: - if self.xmlDoc == None: - self.reload() - suppEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'supportedPermissions') - assert len(suppEls) == 1, 'Expected the repository service document to have one element named supportedPermissions' - self._permissions = suppEls[0].childNodes[0].data - - return self._permissions - - def getPermissionDefinitions(self): - - """ - Returns a dictionary of permission definitions for this repository. The - key is the permission string or technical name of the permission - and the value is the permission description. - - >>> for permDef in repo.permissionDefinitions: - ... print permDef - ... - cmis:all - {http://www.alfresco.org/model/system/1.0}base.LinkChildren - {http://www.alfresco.org/model/content/1.0}folder.Consumer - {http://www.alfresco.org/model/security/1.0}All.All - {http://www.alfresco.org/model/system/1.0}base.CreateAssociations - {http://www.alfresco.org/model/system/1.0}base.FullControl - {http://www.alfresco.org/model/system/1.0}base.AddChildren - {http://www.alfresco.org/model/system/1.0}base.ReadAssociations - {http://www.alfresco.org/model/content/1.0}folder.Editor - {http://www.alfresco.org/model/content/1.0}cmobject.Editor - {http://www.alfresco.org/model/system/1.0}base.DeleteAssociations - cmis:read - cmis:write - """ - - if not self.getCapabilities()['ACL']: - raise NotSupportedException(messages.NO_ACL_SUPPORT) - - if self._permDefs == {}: - if self.xmlDoc == None: - self.reload() - aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') - assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' - aclEl = aclEls[0] - perms = {} - for e in aclEl.childNodes: - if e.localName == 'permissions': - permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') - assert len(permEls) == 1, 'Expected permissions element to have a child named permission' - descEls = e.getElementsByTagNameNS(CMIS_NS, 'description') - assert len(descEls) == 1, 'Expected permissions element to have a child named description' - perm = permEls[0].childNodes[0].data - desc = descEls[0].childNodes[0].data - perms[perm] = desc - self._permDefs = perms - - return self._permDefs - - def getPermissionMap(self): - - """ - Returns a dictionary representing the permission mapping table where - each key is a permission key string and each value is a list of one or - more permissions the principal must have to perform the operation. - - >>> for (k,v) in repo.permissionMap.items(): - ... print 'To do this: %s, you must have these perms:' % k - ... for perm in v: - ... print perm - ... - To do this: canCreateFolder.Folder, you must have these perms: - cmis:all - {http://www.alfresco.org/model/system/1.0}base.CreateChildren - To do this: canAddToFolder.Folder, you must have these perms: - cmis:all - {http://www.alfresco.org/model/system/1.0}base.CreateChildren - To do this: canDelete.Object, you must have these perms: - cmis:all - {http://www.alfresco.org/model/system/1.0}base.DeleteNode - To do this: canCheckin.Document, you must have these perms: - cmis:all - {http://www.alfresco.org/model/content/1.0}lockable.CheckIn - """ - - if not self.getCapabilities()['ACL']: - raise NotSupportedException(messages.NO_ACL_SUPPORT) - - if self._permMap == {}: - if self.xmlDoc == None: - self.reload() - aclEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'aclCapability') - assert len(aclEls) == 1, 'Expected the repository service document to have one element named aclCapability' - aclEl = aclEls[0] - permMap = {} - for e in aclEl.childNodes: - permList = [] - if e.localName == 'mapping': - keyEls = e.getElementsByTagNameNS(CMIS_NS, 'key') - assert len(keyEls) == 1, 'Expected mapping element to have a child named key' - permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') - assert len(permEls) >= 1, 'Expected mapping element to have at least one permission element' - key = keyEls[0].childNodes[0].data - for permEl in permEls: - permList.append(permEl.childNodes[0].data) - permMap[key] = permList - self._permMap = permMap - - return self._permMap - - def getPropagation(self): - - """ - Returns the value of the cmis:propagation element. Valid values are: - - objectonly: indicates that the repository is able to apply ACEs - without changing the ACLs of other objects - - propagate: indicates that the repository is able to apply ACEs to a - given object and propagate this change to all inheriting objects - - >>> repo.propagation - u'propagate' - """ - - if not self.getCapabilities()['ACL']: - raise NotSupportedException(messages.NO_ACL_SUPPORT) - - if not self._propagation: - if self.xmlDoc == None: - self.reload() - propEls = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propagation') - assert len(propEls) == 1, 'Expected the repository service document to have one element named propagation' - self._propagation = propEls[0].childNodes[0].data - - return self._propagation - - def getRepositoryId(self): - - """ - Returns this repository's unique identifier - - >>> repo = client.getDefaultRepository() - >>> repo.getRepositoryId() - u'83beb297-a6fa-4ac5-844b-98c871c0eea9' - """ - - if self._repositoryId == None: - if self.xmlDoc == None: - self.reload() - self._repositoryId = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryId')[0].firstChild.data - return self._repositoryId - - def getRepositoryName(self): - - """ - Returns this repository's name - - >>> repo = client.getDefaultRepository() - >>> repo.getRepositoryName() - u'Main Repository' - """ - - if self._repositoryName == None: - if self.xmlDoc == None: - self.reload() - self._repositoryName = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'repositoryName')[0].firstChild.data - return self._repositoryName - - def getRepositoryInfo(self): - - """ - Returns a dict of repository information. - - >>> repo = client.getDefaultRepository()>>> repo.getRepositoryName() - u'Main Repository' - >>> info = repo.getRepositoryInfo() - >>> for k,v in info.items(): - ... print "%s:%s" % (k,v) - ... - cmisSpecificationTitle:Version 1.0 Committee Draft 04 - cmisVersionSupported:1.0 - repositoryDescription:None - productVersion:3.2.0 (r2 2440) - rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348 - repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9 - repositoryName:Main Repository - vendorName:Alfresco - productName:Alfresco Repository (Community) - """ - - if not self._repositoryInfo: - if self.xmlDoc == None: - self.reload() - repoInfoElement = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'repositoryInfo')[0] - for node in repoInfoElement.childNodes: - if node.nodeType == node.ELEMENT_NODE and node.localName != 'capabilities': - try: - data = node.childNodes[0].data - except: - data = None - self._repositoryInfo[node.localName] = data - return self._repositoryInfo - - def getCapabilities(self): - - """ - Returns a dict of repository capabilities. - - >>> caps = repo.getCapabilities() - >>> for k,v in caps.items(): - ... print "%s:%s" % (k,v) - ... - PWCUpdatable:True - VersionSpecificFiling:False - Join:None - ContentStreamUpdatability:anytime - AllVersionsSearchable:False - Renditions:None - Multifiling:True - GetFolderTree:True - GetDescendants:True - ACL:None - PWCSearchable:True - Query:bothcombined - Unfiling:False - Changes:None - """ - - if not self._capabilities: - if self.xmlDoc == None: - self.reload() - capabilitiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'capabilities')[0] - for node in [e for e in capabilitiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: - key = node.localName.replace('capability', '') - value = parseBoolValue(node.childNodes[0].data) - self._capabilities[key] = value - return self._capabilities - - def getRootFolder(self): - """ - Returns the root folder of the repository - - >>> root = repo.getRootFolder() - >>> root.getObjectId() - u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' - """ - # get the root folder id - rootFolderId = self.getRepositoryInfo()['rootFolderId'] - # instantiate a Folder object using the ID - folder = Folder(self._cmisClient, self, rootFolderId) - # return it - return folder - - def getFolder(self, folderId): - - """ - Returns a :class:`Folder` object for a specified folderId - - >>> someFolder = repo.getFolder('workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348') - >>> someFolder.getObjectId() - u'workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348' - """ - - retObject = self.getObject(folderId) - return Folder(self._cmisClient, self, xmlDoc=retObject.xmlDoc) - - def getTypeChildren(self, - typeId=None): - - """ - Returns a list of :class:`ObjectType` objects corresponding to the - child types of the type specified by the typeId. - - If no typeId is provided, the result will be the same as calling - `self.getTypeDefinitions` - - These optional arguments are current unsupported: - - includePropertyDefinitions - - maxItems - - skipCount - - >>> baseTypes = repo.getTypeChildren() - >>> for baseType in baseTypes: - ... print baseType.getTypeId() - ... - cmis:folder - cmis:relationship - cmis:document - cmis:policy - """ - - # Unfortunately, the spec does not appear to present a way to - # know how to get the children of a specific type without first - # retrieving the type, then asking it for one of its navigational - # links. - - # if a typeId is specified, get it, then get its "down" link - if typeId: - targetType = self.getTypeDefinition(typeId) - childrenUrl = targetType.getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P) - typesXmlDoc = self._cmisClient.get(childrenUrl.encode('utf-8')) - entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') - types = [] - for entryElement in entryElements: - objectType = ObjectType(self._cmisClient, - self, - xmlDoc=entryElement) - types.append(objectType) - # otherwise, if a typeId is not specified, return - # the list of base types - else: - types = self.getTypeDefinitions() - return types - - def getTypeDescendants(self, typeId=None, **kwargs): - - """ - Returns a list of :class:`ObjectType` objects corresponding to the - descendant types of the type specified by the typeId. - - If no typeId is provided, the repository's "typesdescendants" URL - will be called to determine the list of descendant types. - - >>> allTypes = repo.getTypeDescendants() - >>> for aType in allTypes: - ... print aType.getTypeId() - ... - cmis:folder - F:cm:systemfolder - F:act:savedactionfolder - F:app:configurations - F:fm:forums - F:wcm:avmfolder - F:wcm:avmplainfolder - F:wca:webfolder - F:wcm:avmlayeredfolder - F:st:site - F:app:glossary - F:fm:topic - - These optional arguments are supported: - - depth - - includePropertyDefinitions - - >>> types = alfRepo.getTypeDescendants('cmis:folder') - >>> len(types) - 17 - >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=1) - >>> len(types) - 12 - >>> types = alfRepo.getTypeDescendants('cmis:folder', depth=2) - >>> len(types) - 17 - """ - - # Unfortunately, the spec does not appear to present a way to - # know how to get the children of a specific type without first - # retrieving the type, then asking it for one of its navigational - # links. - if typeId: - targetType = self.getTypeDefinition(typeId) - descendUrl = targetType.getLink(DOWN_REL, CMIS_TREE_TYPE_P) - - else: - descendUrl = self.getLink(TYPE_DESCENDANTS_REL) - - if not descendUrl: - raise NotSupportedException("Could not determine the type descendants URL") - - typesXmlDoc = self._cmisClient.get(descendUrl.encode('utf-8'), **kwargs) - entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') - types = [] - for entryElement in entryElements: - objectType = ObjectType(self._cmisClient, - self, - xmlDoc=entryElement) - types.append(objectType) - return types - - def getTypeDefinitions(self, **kwargs): - - """ - Returns a list of :class:`ObjectType` objects representing - the base types in the repository. - - >>> baseTypes = repo.getTypeDefinitions() - >>> for baseType in baseTypes: - ... print baseType.getTypeId() - ... - cmis:folder - cmis:relationship - cmis:document - cmis:policy - """ - - typesUrl = self.getCollectionLink(TYPES_COLL) - typesXmlDoc = self._cmisClient.get(typesUrl, **kwargs) - entryElements = typesXmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') - types = [] - for entryElement in entryElements: - objectType = ObjectType(self._cmisClient, - self, - xmlDoc=entryElement) - types.append(objectType) - # return the result - return types - - def getTypeDefinition(self, typeId): - - """ - Returns an :class:`ObjectType` object for the specified object type id. - - >>> folderType = repo.getTypeDefinition('cmis:folder') - """ - - objectType = ObjectType(self._cmisClient, self, typeId) - objectType.reload() - return objectType - - def getLink(self, rel): - """ - Returns the HREF attribute of an Atom link element for the - specified rel. - """ - if self.xmlDoc == None: - self.reload() - - linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - for linkElement in linkElements: - - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if relAttr == rel: - return linkElement.attributes['href'].value - - def getCheckedOutDocs(self, **kwargs): - - """ - Returns a ResultSet of :class:`CmisObject` objects that - are currently checked out. - - >>> rs = repo.getCheckedOutDocs() - >>> len(rs.getResults()) - 2 - >>> for doc in repo.getCheckedOutDocs().getResults(): - ... doc.getTitle() - ... - u'sample-a (Working Copy).pdf' - u'sample-b (Working Copy).pdf' - - These optional arguments are supported: - - folderId - - maxItems - - skipCount - - orderBy - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - """ - - return self.getCollection(CHECKED_OUT_COLL, **kwargs) - - def getUnfiledDocs(self, **kwargs): - - """ - Returns a ResultSet of :class:`CmisObject` objects that - are currently unfiled. - - >>> rs = repo.getUnfiledDocs() - >>> len(rs.getResults()) - 2 - >>> for doc in repo.getUnfiledDocs().getResults(): - ... doc.getTitle() - ... - u'sample-a.pdf' - u'sample-b.pdf' - - These optional arguments are supported: - - folderId - - maxItems - - skipCount - - orderBy - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - """ - - return self.getCollection(UNFILED_COLL, **kwargs) - - def getObject(self, - objectId, - **kwargs): - - """ - Returns an object given the specified object ID. - - >>> doc = repo.getObject('workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808') - >>> doc.getTitle() - u'sample-b.pdf' - - The following optional arguments are supported: - - returnVersion - - filter - - includeRelationships - - includePolicyIds - - renditionFilter - - includeACL - - includeAllowableActions - """ - - return getSpecializedObject(CmisObject(self._cmisClient, self, objectId, **kwargs), **kwargs) - - def getObjectByPath(self, path, **kwargs): - - """ - Returns an object given the path to the object. - - >>> doc = repo.getObjectByPath('/jeff test/sample-b.pdf') - >>> doc.getTitle() - u'sample-b.pdf' - - The following optional arguments are not currently supported: - - filter - - includeAllowableActions - """ - - # get the uritemplate - template = self.getUriTemplates()['objectbypath']['template'] - - # fill in the template with the path provided - params = { - '{path}': quote(path, '/'), - '{filter}': '', - '{includeAllowableActions}': 'false', - '{includePolicyIds}': 'false', - '{includeRelationships}': '', - '{includeACL}': 'false', - '{renditionFilter}': ''} - - options = {} - addOptions = {} # args specified, but not in the template - for k, v in kwargs.items(): - pKey = "{" + k + "}" - if template.find(pKey) >= 0: - options[pKey] = toCMISValue(v) - else: - addOptions[k] = toCMISValue(v) - - # merge the templated args with the default params - params.update(options) - - byObjectPathUrl = multiple_replace(params, template) - - # do a GET against the URL - result = self._cmisClient.get(byObjectPathUrl.encode('utf-8'), **addOptions) - if type(result) == HTTPError: - raise CmisException(result.code) - - # instantiate CmisObject objects with the results and return the list - entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') - assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byObjectPathUrl - return getSpecializedObject(CmisObject(self._cmisClient, self, xmlDoc=entryElements[0], **kwargs), **kwargs) - - def query(self, statement, **kwargs): - - """ - Returns a list of :class:`CmisObject` objects based on the CMIS - Query Language passed in as the statement. The actual objects - returned will be instances of the appropriate child class based - on the object's base type ID. - - In order for the results to be properly instantiated as objects, - make sure you include 'cmis:objectId' as one of the fields in - your select statement, or just use "SELECT \*". - - If you want the search results to automatically be instantiated with - the appropriate sub-class of :class:`CmisObject` you must either - include cmis:baseTypeId as one of the fields in your select statement - or just use "SELECT \*". - - >>> q = "select * from cmis:document where cmis:name like '%test%'" - >>> resultSet = repo.query(q) - >>> len(resultSet.getResults()) - 1 - >>> resultSet.hasNext() - False - - The following optional arguments are supported: - - searchAllVersions - - includeRelationships - - renditionFilter - - includeAllowableActions - - maxItems - - skipCount - - >>> q = 'select * from cmis:document' - >>> rs = repo.query(q) - >>> len(rs.getResults()) - 148 - >>> rs = repo.query(q, maxItems='5') - >>> len(rs.getResults()) - 5 - >>> rs.hasNext() - True - """ - - if self.xmlDoc == None: - self.reload() - - # get the URL this repository uses to accept query POSTs - queryUrl = self.getCollectionLink(QUERY_COLL) - - # build the CMIS query XML that we're going to POST - xmlDoc = self._getQueryXmlDoc(statement, **kwargs) - - # do the POST - #print 'posting:%s' % xmlDoc.toxml(encoding='utf-8') - result = self._cmisClient.post(queryUrl.encode('utf-8'), - xmlDoc.toxml(encoding='utf-8'), - CMIS_QUERY_TYPE) - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self, result) - - def getContentChanges(self, **kwargs): - - """ - Returns a :class:`ResultSet` containing :class:`ChangeEntry` objects. - - >>> for changeEntry in rs: - ... changeEntry.objectId - ... changeEntry.id - ... changeEntry.changeType - ... changeEntry.changeTime - ... - 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' - u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' - u'created' - datetime.datetime(2010, 2, 11, 12, 55, 14) - 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' - u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' - u'updated' - datetime.datetime(2010, 2, 11, 12, 55, 13) - 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' - u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' - u'updated' - - The following optional arguments are supported: - - changeLogToken - - includeProperties - - includePolicyIDs - - includeACL - - maxItems - - You can get the latest change log token by inspecting the repository - info via :meth:`Repository.getRepositoryInfo`. - - >>> repo.info['latestChangeLogToken'] - u'2692' - >>> rs = repo.getContentChanges(changeLogToken='2692') - >>> len(rs) - 1 - >>> rs[0].id - u'urn:uuid:8e88f694-93ef-44c5-9f70-f12fff824be9' - >>> rs[0].changeType - u'updated' - >>> rs[0].changeTime - datetime.datetime(2010, 2, 16, 20, 6, 37) - """ - - if self.getCapabilities()['Changes'] == None: - raise NotSupportedException(messages.NO_CHANGE_LOG_SUPPORT) - - changesUrl = self.getLink(CHANGE_LOG_REL) - result = self._cmisClient.get(changesUrl.encode('utf-8'), **kwargs) - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ChangeEntryResultSet(self._cmisClient, self, result) - - def createDocumentFromString(self, - name, - properties={}, - parentFolder=None, - contentString=None, - contentType=None, - contentEncoding=None): - - """ - Creates a new document setting the content to the string provided. If - the repository supports unfiled objects, you do not have to pass in - a parent :class:`Folder` otherwise it is required. - - This method is essentially a convenience method that wraps your string - with a StringIO and then calls createDocument. - - >>> repo.createDocumentFromString('testdoc5', parentFolder=testFolder, contentString='Hello, World!', contentType='text/plain') - - """ - - # if you didn't pass in a parent folder - if parentFolder == None: - # if the repository doesn't require fileable objects to be filed - if self.getCapabilities()['Unfiling']: - # has not been implemented - #postUrl = self.getCollectionLink(UNFILED_COLL) - raise NotImplementedError - else: - # this repo requires fileable objects to be filed - raise InvalidArgumentException - - return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString), - contentType, contentEncoding) - - def createDocument(self, - name, - properties={}, - parentFolder=None, - contentFile=None, - contentType=None, - contentEncoding=None): - - """ - Creates a new :class:`Document` object. If the repository - supports unfiled objects, you do not have to pass in - a parent :class:`Folder` otherwise it is required. - - To create a document with an associated contentFile, pass in a - File object. The method will attempt to guess the appropriate content - type and encoding based on the file. To specify it yourself, pass them - in via the contentType and contentEncoding arguments. - - >>> f = open('sample-a.pdf', 'rb') - >>> doc = folder.createDocument('sample-a.pdf', contentFile=f) - - >>> f.close() - >>> doc.getTitle() - u'sample-a.pdf' - - The following optional arguments are not currently supported: - - versioningState - - policies - - addACEs - - removeACEs - """ - - postUrl = '' - # if you didn't pass in a parent folder - if parentFolder == None: - # if the repository doesn't require fileable objects to be filed - if self.getCapabilities()['Unfiling']: - # has not been implemented - #postUrl = self.getCollectionLink(UNFILED_COLL) - raise NotImplementedError - else: - # this repo requires fileable objects to be filed - raise InvalidArgumentException - else: - postUrl = parentFolder.getChildrenLink() - - # make sure a name is set - properties['cmis:name'] = name - - # hardcoding to cmis:document if it wasn't - # passed in via props - if not properties.has_key('cmis:objectTypeId'): - properties['cmis:objectTypeId'] = CmisId('cmis:document') - # and if it was passed in, making sure it is a CmisId - elif not isinstance(properties['cmis:objectTypeId'], CmisId): - properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) - - # build the Atom entry - xmlDoc = getEntryXmlDoc(self, None, properties, contentFile, - contentType, contentEncoding) - - # post the Atom entry - result = self._cmisClient.post(postUrl.encode('utf-8'), xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE) - if type(result) == HTTPError: - raise CmisException(result.code) - - # what comes back is the XML for the new document, - # so use it to instantiate a new document - # then return it - return Document(self._cmisClient, self, xmlDoc=result) - - def createDocumentFromSource(self, - sourceId, - properties={}, - parentFolder=None): - """ - This is not yet implemented. - - The following optional arguments are not yet supported: - - versioningState - - policies - - addACEs - - removeACEs - """ - # TODO: To be implemented - raise NotImplementedError - - def createFolder(self, - parentFolder, - name, - properties={}): - - """ - Creates a new :class:`Folder` object in the specified parentFolder. - - >>> root = repo.getRootFolder() - >>> folder = repo.createFolder(root, 'someFolder2') - >>> folder.getTitle() - u'someFolder2' - >>> folder.getObjectId() - u'workspace://SpacesStore/2224a63c-350b-438c-be72-8f425e79ce1f' - - The following optional arguments are not yet supported: - - policies - - addACEs - - removeACEs - """ - - return parentFolder.createFolder(name, properties) - - def createRelationship(self, sourceObj, targetObj, relType): - """ - Creates a relationship of the specific type between a source object - and a target object and returns the new :class:`Relationship` object. - - The following optional arguments are not currently supported: - - policies - - addACEs - - removeACEs - """ - return sourceObj.createRelationship(targetObj, relType) - - def createPolicy(self, properties): - """ - This has not yet been implemented. - - The following optional arguments are not currently supported: - - folderId - - policies - - addACEs - - removeACEs - """ - # TODO: To be implemented - raise NotImplementedError - - def getUriTemplates(self): - - """ - Returns a list of the URI templates the repository service knows about. - - >>> templates = repo.getUriTemplates() - >>> templates['typebyid']['mediaType'] - u'application/atom+xml;type=entry' - >>> templates['typebyid']['template'] - u'http://localhost:8080/alfresco/s/cmis/type/{id}' - """ - - if self._uriTemplates == {}: - - if self.xmlDoc == None: - self.reload() - - uriTemplateElements = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'uritemplate') - - for uriTemplateElement in uriTemplateElements: - template = None - templType = None - mediatype = None - - for node in [e for e in uriTemplateElement.childNodes if e.nodeType == e.ELEMENT_NODE]: - if node.localName == 'template': - template = node.childNodes[0].data - elif node.localName == 'type': - templType = node.childNodes[0].data - elif node.localName == 'mediatype': - mediatype = node.childNodes[0].data - - self._uriTemplates[templType] = UriTemplate(template, - templType, - mediatype) - - return self._uriTemplates - - def getCollection(self, collectionType, **kwargs): - - """ - Returns a list of objects returned for the specified collection. - - If the query collection is requested, an exception will be raised. - That collection isn't meant to be retrieved. - - If the types collection is specified, the method returns the result of - `getTypeDefinitions` and ignores any optional params passed in. - - >>> from cmislib.model import TYPES_COLL - >>> types = repo.getCollection(TYPES_COLL) - >>> len(types) - 4 - >>> types[0].getTypeId() - u'cmis:folder' - - Otherwise, the collection URL is invoked, and a :class:`ResultSet` is - returned. - - >>> from cmislib.model import CHECKED_OUT_COLL - >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) - >>> len(resultSet.getResults()) - 1 - """ - - if collectionType == QUERY_COLL: - raise NotSupportedException - elif collectionType == TYPES_COLL: - return self.getTypeDefinitions() - - result = self._cmisClient.get(self.getCollectionLink(collectionType).encode('utf-8'), **kwargs) - if (type(result) == HTTPError): - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self, result) - - def getCollectionLink(self, collectionType): - - """ - Returns the link HREF from the specified collectionType - ('checkedout', for example). - - >>> from cmislib.model import CHECKED_OUT_COLL - >>> repo.getCollectionLink(CHECKED_OUT_COLL) - u'http://localhost:8080/alfresco/s/cmis/checkedout' - - """ - - collectionElements = self.xmlDoc.getElementsByTagNameNS(APP_NS, 'collection') - for collectionElement in collectionElements: - link = collectionElement.attributes['href'].value - for node in [e for e in collectionElement.childNodes if e.nodeType == e.ELEMENT_NODE]: - if node.localName == 'collectionType': - if node.childNodes[0].data == collectionType: - return link - - def _getQueryXmlDoc(self, query, **kwargs): - - """ - Utility method that knows how to build CMIS query xml around the - specified query statement. - """ - - cmisXmlDoc = minidom.Document() - queryElement = cmisXmlDoc.createElementNS(CMIS_NS, "query") - queryElement.setAttribute('xmlns', CMIS_NS) - cmisXmlDoc.appendChild(queryElement) - - statementElement = cmisXmlDoc.createElementNS(CMIS_NS, "statement") - cdataSection = cmisXmlDoc.createCDATASection(query) - statementElement.appendChild(cdataSection) - queryElement.appendChild(statementElement) - - for (k, v) in kwargs.items(): - optionElement = cmisXmlDoc.createElementNS(CMIS_NS, k) - optionText = cmisXmlDoc.createTextNode(v) - optionElement.appendChild(optionText) - queryElement.appendChild(optionElement) - - return cmisXmlDoc - - capabilities = property(getCapabilities) - id = property(getRepositoryId) - info = property(getRepositoryInfo) - name = property(getRepositoryName) - rootFolder = property(getRootFolder) - permissionDefinitions = property(getPermissionDefinitions) - permissionMap = property(getPermissionMap) - propagation = property(getPropagation) - supportedPermissions = property(getSupportedPermissions) - - -class ResultSet(object): - - """ - Represents a paged result set. In CMIS, this is most often an Atom feed. - """ - - def __init__(self, cmisClient, repository, xmlDoc): - ''' Constructor ''' - self._cmisClient = cmisClient - self._repository = repository - self._xmlDoc = xmlDoc - self._results = [] - self.logger = logging.getLogger('cmislib.model.ResultSet') - self.logger.info('Creating an instance of ResultSet') - - def __iter__(self): - ''' Iterator for the result set ''' - return iter(self.getResults()) - - def __getitem__(self, index): - ''' Getter for the result set ''' - return self.getResults()[index] - - def __len__(self): - ''' Len method for the result set ''' - return len(self.getResults()) - - def _getLink(self, rel): - ''' - Returns the link found in the feed's XML for the specified rel. - ''' - linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - for linkElement in linkElements: - - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if relAttr == rel: - return linkElement.attributes['href'].value - - def _getPageResults(self, rel): - ''' - Given a specified rel, does a get using that link (if one exists) - and then converts the resulting XML into a dictionary of - :class:`CmisObject` objects or its appropriate sub-type. - - The results are kept around to facilitate repeated calls without moving - the cursor. - ''' - link = self._getLink(rel) - if link: - result = self._cmisClient.get(link.encode('utf-8')) - if (type(result) == HTTPError): - raise CmisException(result.code) - - # return the result - self._xmlDoc = result - self._results = [] - return self.getResults() - - def reload(self): - - ''' - Re-invokes the self link for the current set of results. - - >>> resultSet = repo.getCollection(CHECKED_OUT_COLL) - >>> resultSet.reload() - - ''' - - self.logger.debug('Reload called on result set') - self._getPageResults(SELF_REL) - - def getResults(self): - - ''' - Returns the results that were fetched and cached by the get*Page call. - - >>> resultSet = repo.getCheckedOutDocs() - >>> resultSet.hasNext() - False - >>> for result in resultSet.getResults(): - ... result - ... - - ''' - if self._results: - return self._results - - if self._xmlDoc: - entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') - entries = [] - for entryElement in entryElements: - cmisObject = getSpecializedObject(CmisObject(self._cmisClient, - self._repository, - xmlDoc=entryElement)) - entries.append(cmisObject) - - self._results = entries - - return self._results - - def hasObject(self, objectId): - - ''' - Returns True if the specified objectId is found in the list of results, - otherwise returns False. - ''' - - for obj in self.getResults(): - if obj.id == objectId: - return True - return False - - def getFirst(self): - - ''' - Returns the first page of results as a dictionary of - :class:`CmisObject` objects or its appropriate sub-type. This only - works when the server returns a "first" link. Not all of them do. - - >>> resultSet.hasFirst() - True - >>> results = resultSet.getFirst() - >>> for result in results: - ... result - ... - - ''' - - 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 - ... - - ''' - - 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 - ... - - ''' - - 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 - ... - - ''' - - return self._getPageResults(LAST_REL) - - def hasNext(self): - - ''' - Returns True if this page contains a next link. - - >>> resultSet.hasNext() - True - ''' - - if self._getLink(NEXT_REL): - return True - else: - return False - - def hasPrev(self): - - ''' - Returns True if this page contains a prev link. Not all CMIS providers - implement prev links consistently. - - >>> resultSet.hasPrev() - True - ''' - - if self._getLink(PREV_REL): - return True - else: - return False - - def hasFirst(self): - - ''' - Returns True if this page contains a first link. Not all CMIS providers - implement first links consistently. - - >>> resultSet.hasFirst() - True - ''' - - if self._getLink(FIRST_REL): - return True - else: - return False - - def hasLast(self): - - ''' - Returns True if this page contains a last link. Not all CMIS providers - implement last links consistently. - - >>> resultSet.hasLast() - True - ''' - - if self._getLink(LAST_REL): - return True - else: - return False - - -class CmisObject(object): - - """ - Common ancestor class for other CMIS domain objects such as - :class:`Document` and :class:`Folder`. - """ - - def __init__(self, cmisClient, repository, objectId=None, xmlDoc=None, **kwargs): - """ Constructor """ - self._cmisClient = cmisClient - self._repository = repository - self._objectId = objectId - self._name = None - self._properties = {} - self._allowableActions = {} - self.xmlDoc = xmlDoc - self._kwargs = kwargs - self.logger = logging.getLogger('cmislib.model.CmisObject') - self.logger.info('Creating an instance of CmisObject') - - def __str__(self): - """To string""" - return self.getObjectId() - - def reload(self, **kwargs): - - """ - Fetches the latest representation of this object from the CMIS service. - Some methods, like :class:`^Document.checkout` do this for you. - - If you call reload with a properties filter, the filter will be in - effect on subsequent calls until the filter argument is changed. To - reset to the full list of properties, call reload with filter set to - '*'. - """ - - self.logger.debug('Reload called on CmisObject') - if kwargs: - if self._kwargs: - self._kwargs.update(kwargs) - else: - self._kwargs = kwargs - - templates = self._repository.getUriTemplates() - template = templates['objectbyid']['template'] - - # Doing some refactoring here. Originally, we snagged the template - # and then "filled in" the template based on the args passed in. - # However, some servers don't provide a full template which meant - # supported optional args wouldn't get passed in using the fill-the- - # template approach. What's going on now is that the template gets - # filled in where it can, but if additional, non-templated args are - # passed in, those will get tacked on to the query string as - # "additional" options. - - params = { - '{id}': self.getObjectId(), - '{filter}': '', - '{includeAllowableActions}': 'false', - '{includePolicyIds}': 'false', - '{includeRelationships}': '', - '{includeACL}': 'false', - '{renditionFilter}': ''} - - options = {} - addOptions = {} # args specified, but not in the template - for k, v in self._kwargs.items(): - pKey = "{" + k + "}" - if template.find(pKey) >= 0: - options[pKey] = toCMISValue(v) - else: - addOptions[k] = toCMISValue(v) - - # merge the templated args with the default params - params.update(options) - - # fill in the template - byObjectIdUrl = multiple_replace(params, template) - - self.xmlDoc = self._cmisClient.get(byObjectIdUrl.encode('utf-8'), **addOptions) - self._initData() - - # if a returnVersion arg was passed in, it is possible we got back - # a different object ID than the value we started with, so it needs - # to be cleared out as well - if options.has_key('returnVersion') or addOptions.has_key('returnVersion'): - self._objectId = None - - def _initData(self): - - """ - An internal method used to clear out any member variables that - might be out of sync if we were to fetch new XML from the - service. - """ - - self._properties = {} - self._name = None - self._allowableActions = {} - - def getObjectId(self): - - """ - Returns the object ID for this object. - - >>> doc = resultSet.getResults()[0] - >>> doc.getObjectId() - u'workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339' - """ - - if self._objectId == None: - if self.xmlDoc == None: - self.logger.debug('Both objectId and xmlDoc were None, reloading') - self.reload() - props = self.getProperties() - self._objectId = CmisId(props['cmis:objectId']) - return self._objectId - - def getObjectParents(self, **kwargs): - """ - Gets the parents of this object as a :class:`ResultSet`. - - The following optional arguments are supported: - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - - includeRelativePathSegment - """ - # get the appropriate 'up' link - parentUrl = self._getLink(UP_REL) - - if parentUrl == None: - raise NotSupportedException('Root folder does not support getObjectParents') - - # invoke the URL - result = self._cmisClient.get(parentUrl.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self._repository, result) - - def getPaths(self): - """ - Returns the object's paths as a list of strings. - """ - # see sub-classes for implementation - pass - - def getAllowableActions(self): - - """ - Returns a dictionary of allowable actions, keyed off of the action name. - - >>> actions = doc.getAllowableActions() - >>> for a in actions: - ... print "%s:%s" % (a,actions[a]) - ... - canDeleteContentStream:True - canSetContentStream:True - canCreateRelationship:True - canCheckIn:False - canApplyACL:False - canDeleteObject:True - canGetAllVersions:True - canGetObjectParents:True - canGetProperties:True - """ - - if self._allowableActions == {}: - self.reload(includeAllowableActions=True) - allowElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'allowableActions') - assert len(allowElements) == 1, "Expected response to have exactly one allowableActions element" - allowElement = allowElements[0] - for node in [e for e in allowElement.childNodes if e.nodeType == e.ELEMENT_NODE]: - actionName = node.localName - actionValue = parseBoolValue(node.childNodes[0].data) - self._allowableActions[actionName] = actionValue - - return self._allowableActions - - def getTitle(self): - - """ - Returns the value of the object's cmis:title property. - """ - - if self.xmlDoc == None: - self.reload() - - titleElement = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'title')[0] - - if titleElement and titleElement.childNodes: - return titleElement.childNodes[0].data - - def getProperties(self): - - """ - Returns a dict of the object's properties. If CMIS returns an - empty element for a property, the property will be in the - dict with a value of None. - - >>> props = doc.getProperties() - >>> for p in props: - ... print "%s: %s" % (p, props[p]) - ... - cmis:contentStreamMimeType: text/html - cmis:creationDate: 2009-12-15T09:45:35.369-06:00 - cmis:baseTypeId: cmis:document - cmis:isLatestMajorVersion: false - cmis:isImmutable: false - cmis:isMajorVersion: false - cmis:objectId: workspace://SpacesStore/dc26102b-e312-471b-b2af-91bfb0225339 - - The optional filter argument is not yet implemented. - """ - - #TODO implement filter - if self._properties == {}: - if self.xmlDoc == None: - self.reload() - propertiesElement = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] - #cpattern = re.compile(r'^property([\w]*)') - for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMIS_NS]: - #propertyId, propertyString, propertyDateTime - #propertyType = cpattern.search(node.localName).groups()[0] - propertyName = node.attributes['propertyDefinitionId'].value - if node.childNodes and \ - node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ - node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: - valNodeList = node.getElementsByTagNameNS(CMIS_NS, 'value') - if (len(valNodeList) == 1): - propertyValue = parsePropValue(valNodeList[0]. - childNodes[0].data, - node.localName) - else: - propertyValue = [] - for valNode in valNodeList: - propertyValue.append(parsePropValue(valNode. - childNodes[0].data, - node.localName)) - else: - propertyValue = None - self._properties[propertyName] = propertyValue - - for node in [e for e in self.xmlDoc.childNodes if e.nodeType == e.ELEMENT_NODE and e.namespaceURI == CMISRA_NS]: - propertyName = node.nodeName - if node.childNodes: - propertyValue = node.firstChild.nodeValue - else: - propertyValue = None - self._properties[propertyName] = propertyValue - - return self._properties - - def getName(self): - - """ - Returns the value of cmis:name from the getProperties() dictionary. - We don't need a getter for every standard CMIS property, but name - is a pretty common one so it seems to make sense. - - >>> doc.getName() - u'system-overview.html' - """ - - if self._name == None: - self._name = self.getProperties()['cmis:name'] - return self._name - - def updateProperties(self, properties): - - """ - Updates the properties of an object with the properties provided. - Only provide the set of properties that need to be updated. - - >>> folder = repo.getObjectByPath('/someFolder2') - >>> folder.getName() - u'someFolder2' - >>> props = {'cmis:name': 'someFolderFoo'} - >>> folder.updateProperties(props) - - >>> folder.getName() - u'someFolderFoo' - - """ - - self.logger.debug('Inside updateProperties') - - # get the self link - selfUrl = self._getSelfLink() - - # if we have a change token, we must pass it back, per the spec - args = {} - if (self.properties.has_key('cmis:changeToken') and - self.properties['cmis:changeToken'] != None): - self.logger.debug('Change token present, adding it to args') - args = {"changeToken": self.properties['cmis:changeToken']} - - # the getEntryXmlDoc function may need the object type - objectTypeId = None - if (self.properties.has_key('cmis:objectTypeId') and - not properties.has_key('cmis:objectTypeId')): - objectTypeId = self.properties['cmis:objectTypeId'] - self.logger.debug('This object type is:%s' % objectTypeId) - - # build the entry based on the properties provided - xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties) - - self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml()) - - # do a PUT of the entry - updatedXmlDoc = self._cmisClient.put(selfUrl.encode('utf-8'), - xmlEntryDoc.toxml(encoding='utf-8'), - ATOM_XML_TYPE, - **args) - - # reset the xmlDoc for this object with what we got back from - # the PUT, then call initData we dont' want to call - # self.reload because we've already got the parsed XML-- - # there's no need to fetch it again - self.xmlDoc = updatedXmlDoc - self._initData() - return self - - def move(self, sourceFolder, targetFolder): - - """ - Moves an object from the source folder to the target folder. - - >>> sub1 = repo.getObjectByPath('/cmislib/sub1') - >>> sub2 = repo.getObjectByPath('/cmislib/sub2') - >>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1') - >>> doc.move(sub1, sub2) - """ - - postUrl = targetFolder.getChildrenLink() - - args = {"sourceFolderId": sourceFolder.id} - - # post the Atom entry - result = self._cmisClient.post(postUrl.encode('utf-8'), self.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args) - if type(result) == HTTPError: - raise CmisException(result.code) - - def delete(self, **kwargs): - - """ - Deletes this :class:`CmisObject` from the repository. Note that in the - case of a :class:`Folder` object, some repositories will refuse to - delete it if it contains children and some will delete it without - complaint. If what you really want to do is delete the folder and all - of its descendants, use :meth:`~Folder.deleteTree` instead. - - >>> folder.delete() - - The optional allVersions argument is supported. - """ - - url = self._getSelfLink() - result = self._cmisClient.delete(url.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - def applyPolicy(self, policyId): - - """ - This is not yet implemented. - """ - - # depends on this object's canApplyPolicy allowable action - if self.getAllowableActions()['canApplyPolicy']: - raise NotImplementedError - else: - raise CmisException('This object has canApplyPolicy set to false') - - def createRelationship(self, targetObj, relTypeId): - - """ - Creates a relationship between this object and a specified target - object using the relationship type specified. Returns the new - :class:`Relationship` object. - - >>> rel = tstDoc1.createRelationship(tstDoc2, 'R:cmiscustom:assoc') - >>> rel.getProperties() - {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} - - """ - - if isinstance(relTypeId, str): - relTypeId = CmisId(relTypeId) - - props = {} - props['cmis:sourceId'] = self.getObjectId() - props['cmis:targetId'] = targetObj.getObjectId() - props['cmis:objectTypeId'] = relTypeId - xmlDoc = getEntryXmlDoc(self._repository, properties=props) - - url = self._getLink(RELATIONSHIPS_REL) - assert url != None, 'Could not determine relationships URL' - - result = self._cmisClient.post(url.encode('utf-8'), - xmlDoc.toxml(encoding='utf-8'), - ATOM_XML_TYPE) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # instantiate CmisObject objects with the results and return the list - entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') - assert(len(entryElements) == 1), "Expected entry element in result from relationship URL post" - return getSpecializedObject(CmisObject(self._cmisClient, self, xmlDoc=entryElements[0])) - - def getRelationships(self, **kwargs): - - """ - Returns a :class:`ResultSet` of :class:`Relationship` objects for each - relationship where the source is this object. - - >>> rels = tstDoc1.getRelationships() - >>> len(rels.getResults()) - 1 - >>> rel = rels.getResults().values()[0] - >>> rel.getProperties() - {u'cmis:objectId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:creationDate': None, u'cmis:objectTypeId': u'R:cmiscustom:assoc', u'cmis:lastModificationDate': None, u'cmis:targetId': u'workspace://SpacesStore/0ca1aa08-cb49-42e2-8881-53aa8496a1c1', u'cmis:lastModifiedBy': None, u'cmis:baseTypeId': u'cmis:relationship', u'cmis:sourceId': u'workspace://SpacesStore/271c48dd-6548-4771-a8f5-0de69b7cdc25', u'cmis:changeToken': None, u'cmis:createdBy': None} - - The following optional arguments are supported: - - includeSubRelationshipTypes - - relationshipDirection - - typeId - - maxItems - - skipCount - - filter - - includeAllowableActions - """ - - url = self._getLink(RELATIONSHIPS_REL) - assert url != None, 'Could not determine relationships URL' - - result = self._cmisClient.get(url.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self._repository, result) - - def removePolicy(self, policyId): - - """ - This is not yet implemented. - """ - - # depends on this object's canRemovePolicy allowable action - if self.getAllowableActions()['canRemovePolicy']: - raise NotImplementedError - else: - raise CmisException('This object has canRemovePolicy set to false') - - def getAppliedPolicies(self): - - """ - This is not yet implemented. - """ - - # depends on this object's canGetAppliedPolicies allowable action - if self.getAllowableActions()['canGetAppliedPolicies']: - raise NotImplementedError - else: - raise CmisException('This object has canGetAppliedPolicies set to false') - - def getACL(self): - - """ - Repository.getCapabilities['ACL'] must return manage or discover. - - >>> acl = folder.getACL() - >>> acl.getEntries() - {u'GROUP_EVERYONE': , 'jdoe': } - - The optional onlyBasicPermissions argument is currently not supported. - """ - - if self._repository.getCapabilities()['ACL']: - # if the ACL capability is discover or manage, this must be - # supported - aclUrl = self._getLink(ACL_REL) - result = self._cmisClient.get(aclUrl.encode('utf-8')) - if type(result) == HTTPError: - raise CmisException(result.code) - return ACL(xmlDoc=result) - else: - raise NotSupportedException - - def applyACL(self, acl): - - """ - Updates the object with the provided :class:`ACL`. - Repository.getCapabilities['ACL'] must return manage to invoke this - call. - - >>> acl = folder.getACL() - >>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true')) - >>> acl.getEntries() - {u'GROUP_EVERYONE': , 'jdoe': } - """ - - if self._repository.getCapabilities()['ACL'] == 'manage': - # if the ACL capability is manage, this must be - # supported - # but it also depends on the canApplyACL allowable action - # for this object - if not isinstance(acl, ACL): - raise CmisException('The ACL to apply must be an instance of the ACL class.') - aclUrl = self._getLink(ACL_REL) - assert aclUrl, "Could not determine the object's ACL URL." - result = self._cmisClient.put(aclUrl.encode('utf-8'), acl.getXmlDoc().toxml(encoding='utf-8'), CMIS_ACL_TYPE) - if type(result) == HTTPError: - raise CmisException(result.code) - return ACL(xmlDoc=result) - else: - raise NotSupportedException - - def _getSelfLink(self): - - """ - Returns the URL used to retrieve this object. - """ - - url = self._getLink(SELF_REL) - - assert len(url) > 0, "Could not determine the self link." - - return url - - def _getLink(self, rel, ltype=None): - - """ - Returns the HREF attribute of an Atom link element for the - specified rel. - """ - - if self.xmlDoc == None: - self.reload() - linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - for linkElement in linkElements: - - if ltype: - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if ltype and linkElement.attributes.has_key('type'): - typeAttr = linkElement.attributes['type'].value - - if relAttr == rel and ltype.match(typeAttr): - return linkElement.attributes['href'].value - else: - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if relAttr == rel: - return linkElement.attributes['href'].value - - allowableActions = property(getAllowableActions) - name = property(getName) - id = property(getObjectId) - properties = property(getProperties) - title = property(getTitle) - ACL = property(getACL) - - -class Document(CmisObject): - - """ - An object typically associated with file content. - """ - - def checkout(self): - - """ - Performs a checkout on the :class:`Document` and returns the - Private Working Copy (PWC), which is also an instance of - :class:`Document` - - >>> doc.getObjectId() - u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0' - >>> doc.isCheckedOut() - False - >>> pwc = doc.checkout() - >>> doc.isCheckedOut() - True - """ - - # get the checkedout collection URL - checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL) - assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url." - - # get this document's object ID - # build entry XML with it - properties = {'cmis:objectId': self.getObjectId()} - entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties) - - # post it to to the checkedout collection URL - result = self._cmisClient.post(checkoutUrl.encode('utf-8'), - entryXmlDoc.toxml(encoding='utf-8'), - ATOM_XML_ENTRY_TYPE) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # now that the doc is checked out, we need to refresh the XML - # to pick up the prop updates related to a checkout - self.reload() - - return Document(self._cmisClient, self._repository, xmlDoc=result) - - def cancelCheckout(self): - """ - Cancels the checkout of this object by retrieving the Private Working - Copy (PWC) and then deleting it. After the PWC is deleted, this object - will be reloaded to update properties related to a checkout. - - >>> doc.isCheckedOut() - True - >>> doc.cancelCheckout() - >>> doc.isCheckedOut() - False - """ - - pwcDoc = self.getPrivateWorkingCopy() - if pwcDoc: - pwcDoc.delete() - self.reload() - - def getPrivateWorkingCopy(self): - - """ - Retrieves the object using the object ID in the property: - cmis:versionSeriesCheckedOutId then uses getObject to instantiate - the object. - - >>> doc.isCheckedOut() - False - >>> doc.checkout() - - >>> pwc = doc.getPrivateWorkingCopy() - >>> pwc.getTitle() - u'sample-b (Working Copy).pdf' - """ - - # reloading the document just to make sure we've got the latest - # and greatest PWC ID - self.reload() - pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId'] - if pwcDocId: - return self._repository.getObject(pwcDocId) - - def isCheckedOut(self): - - """ - Returns true if the document is checked out. - - >>> doc.isCheckedOut() - True - >>> doc.cancelCheckout() - >>> doc.isCheckedOut() - False - """ - - # reloading the document just to make sure we've got the latest - # and greatest checked out prop - self.reload() - return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut']) - - def getCheckedOutBy(self): - - """ - Returns the ID who currently has the document checked out. - >>> pwc = doc.checkout() - >>> pwc.getCheckedOutBy() - u'admin' - """ - - # reloading the document just to make sure we've got the latest - # and greatest checked out prop - self.reload() - return self.getProperties()['cmis:versionSeriesCheckedOutBy'] - - def checkin(self, checkinComment=None, **kwargs): - - """ - Checks in this :class:`Document` which must be a private - working copy (PWC). - - >>> doc.isCheckedOut() - False - >>> pwc = doc.checkout() - >>> doc.isCheckedOut() - True - >>> pwc.checkin() - - >>> doc.isCheckedOut() - False - - The following optional arguments are supported: - - major - - properties - - contentStream - - policies - - addACEs - - removeACEs - """ - - # Add checkin to kwargs and checkinComment, if it exists - kwargs['checkin'] = 'true' - kwargs['checkinComment'] = checkinComment - - # Build an empty ATOM entry - entryXmlDoc = getEmptyXmlDoc() - - # Get the self link - # Do a PUT of the empty ATOM to the self link - url = self._getSelfLink() - result = self._cmisClient.put(url.encode('utf-8'), entryXmlDoc.toxml(encoding='utf-8'), ATOM_XML_TYPE, **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - return Document(self._cmisClient, self._repository, xmlDoc=result) - - def getLatestVersion(self, **kwargs): - - """ - Returns a :class:`Document` object representing the latest version in - the version series. - - The following optional arguments are supported: - - major - - filter - - includeRelationships - - includePolicyIds - - renditionFilter - - includeACL - - includeAllowableActions - - >>> latestDoc = doc.getLatestVersion() - >>> latestDoc.getProperties()['cmis:versionLabel'] - u'2.1' - >>> latestDoc = doc.getLatestVersion(major='false') - >>> latestDoc.getProperties()['cmis:versionLabel'] - u'2.1' - >>> latestDoc = doc.getLatestVersion(major='true') - >>> latestDoc.getProperties()['cmis:versionLabel'] - u'2.0' - """ - - doc = None - if kwargs.has_key('major') and kwargs['major'] == 'true': - doc = self._repository.getObject(self.getObjectId(), returnVersion='latestmajor') - else: - doc = self._repository.getObject(self.getObjectId(), returnVersion='latest') - - return doc - - def getPropertiesOfLatestVersion(self, **kwargs): - - """ - Like :class:`^CmisObject.getProperties`, returns a dict of properties - from the latest version of this object in the version series. - - The optional major and filter arguments are supported. - """ - - latestDoc = self.getLatestVersion(**kwargs) - - return latestDoc.getProperties() - - def getAllVersions(self, **kwargs): - - """ - Returns a :class:`ResultSet` of document objects for the entire - version history of this object, including any PWC's. - - The optional filter and includeAllowableActions are - supported. - """ - - # get the version history link - versionsUrl = self._getLink(VERSION_HISTORY_REL) - - # invoke the URL - result = self._cmisClient.get(versionsUrl.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self._repository, result) - - def getContentStream(self): - - """ - Returns the CMIS service response from invoking the 'enclosure' link. - - >>> doc.getName() - u'sample-b.pdf' - >>> o = open('tmp.pdf', 'wb') - >>> result = doc.getContentStream() - >>> o.write(result.read()) - >>> result.close() - >>> o.close() - >>> import os.path - >>> os.path.getsize('tmp.pdf') - 117248 - - The optional streamId argument is not yet supported. - """ - - # TODO: Need to implement the streamId - - contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') - - assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' - - # if the src element exists, follow that - if contentElements[0].attributes.has_key('src'): - srcUrl = contentElements[0].attributes['src'].value - - # the cmis client class parses non-error responses - result = Rest().get(srcUrl.encode('utf-8'), - username=self._cmisClient.username, - password=self._cmisClient.password, - **self._cmisClient.extArgs) - if result.code != 200: - raise CmisException(result.code) - return result - else: - # otherwise, try to return the value of the content element - if contentElements[0].childNodes: - return contentElements[0].childNodes[0].data - - def setContentStream(self, contentFile, contentType=None): - - """ - Sets the content stream on this object. - - The following optional arguments are not yet supported: - - overwriteFlag=None - """ - - # get this object's content stream link - contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') - - assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' - - # if the src element exists, follow that - if contentElements[0].attributes.has_key('src'): - srcUrl = contentElements[0].attributes['src'].value - - # there may be times when this URL is absent, but I'm not sure how to - # set the content stream when that is the case - assert(srcUrl), 'Unable to determine content stream URL.' - - # need to determine the mime type - mimetype = contentType - if not mimetype and hasattr(contentFile, 'name'): - mimetype, encoding = mimetypes.guess_type(contentFile.name) - - if not mimetype: - mimetype = 'application/binary' - - # if we have a change token, we must pass it back, per the spec - args = {} - if (self.properties.has_key('cmis:changeToken') and - self.properties['cmis:changeToken'] != None): - self.logger.debug('Change token present, adding it to args') - args = {"changeToken": self.properties['cmis:changeToken']} - - # put the content file - result = self._cmisClient.put(srcUrl.encode('utf-8'), - contentFile.read(), - mimetype, - **args) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # what comes back is the XML for the updated document, - # which is not required by the spec to be the same document - # we just updated, so use it to instantiate a new document - # then return it - return Document(self._cmisClient, self._repository, xmlDoc=result) - - def deleteContentStream(self): - - """ - Delete's the content stream associated with this object. - """ - - # get this object's content stream link - contentElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'content') - - assert(len(contentElements) == 1), 'Expected to find exactly one atom:content element.' - - # if the src element exists, follow that - if contentElements[0].attributes.has_key('src'): - srcUrl = contentElements[0].attributes['src'].value - - # there may be times when this URL is absent, but I'm not sure how to - # delete the content stream when that is the case - assert(srcUrl), 'Unable to determine content stream URL.' - - # if we have a change token, we must pass it back, per the spec - args = {} - if (self.properties.has_key('cmis:changeToken') and - self.properties['cmis:changeToken'] != None): - self.logger.debug('Change token present, adding it to args') - args = {"changeToken": self.properties['cmis:changeToken']} - - # delete the content stream - result = self._cmisClient.delete(srcUrl.encode('utf-8'), **args) - if type(result) == HTTPError: - raise CmisException(result.code) - - def getRenditions(self): - - """ - Returns an array of :class:`Rendition` objects. The repository - must support the Renditions capability. - - The following optional arguments are not currently supported: - - renditionFilter - - maxItems - - skipCount - """ - - # if Renditions capability is None, return notsupported - if self._repository.getCapabilities()['Renditions']: - pass - else: - raise NotSupportedException - - if self.xmlDoc == None: - self.reload() - - linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - renditions = [] - for linkElement in linkElements: - - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if relAttr == RENDITION_REL: - renditions.append(Rendition(linkElement)) - return renditions - - checkedOut = property(isCheckedOut) - - def getPaths(self): - """ - Returns the Document's paths by asking for the parents with the - includeRelativePathSegment flag set to true, then concats the value - of cmis:path with the relativePathSegment. - """ - # get the appropriate 'up' link - parentUrl = self._getLink(UP_REL) - - if parentUrl == None: - raise NotSupportedException('Root folder does not support getObjectParents') - - # invoke the URL - result = self._cmisClient.get(parentUrl.encode('utf-8'), - filter='cmis:path', - includeRelativePathSegment=True) - - if type(result) == HTTPError: - raise CmisException(result.code) - - paths = [] - rs = ResultSet(self._cmisClient, self._repository, result) - for res in rs: - path = res.properties['cmis:path'] - relativePathSegment = res.properties['cmisra:relativePathSegment'] - - # concat with a slash - # add it to the list - paths.append(path + '/' + relativePathSegment) - - return paths - - -class Folder(CmisObject): - - """ - A container object that can hold other :class:`CmisObject` objects - """ - - def createFolder(self, name, properties={}): - - """ - Creates a new :class:`Folder` using the properties provided. - Right now I expect a property called 'cmis:name' but I don't - complain if it isn't there (although the CMIS provider will). If a - cmis:name property isn't provided, the value passed in to the name - argument will be used. - - To specify a custom folder type, pass in a property called - cmis:objectTypeId set to the :class:`CmisId` representing the type ID - of the instance you want to create. If you do not pass in an object - type ID, an instance of 'cmis:folder' will be created. - - >>> subFolder = folder.createFolder('someSubfolder') - >>> subFolder.getName() - u'someSubfolder' - - The following optional arguments are not supported: - - policies - - addACEs - - removeACEs - """ - - # get the folder represented by folderId. - # we'll use his 'children' link post the new child - postUrl = self.getChildrenLink() - - # make sure the name property gets set - properties['cmis:name'] = name - - # hardcoding to cmis:folder if it wasn't passed in via props - if not properties.has_key('cmis:objectTypeId'): - properties['cmis:objectTypeId'] = CmisId('cmis:folder') - # and checking to make sure the object type ID is an instance of CmisId - elif not isinstance(properties['cmis:objectTypeId'], CmisId): - properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId']) - - # build the Atom entry - entryXml = getEntryXmlDoc(self._repository, properties=properties) - - # post the Atom entry - result = self._cmisClient.post(postUrl.encode('utf-8'), - entryXml.toxml(encoding='utf-8'), - ATOM_XML_ENTRY_TYPE) - if type(result) == HTTPError: - raise CmisException(result.code) - - # what comes back is the XML for the new folder, - # so use it to instantiate a new folder then return it - return Folder(self._cmisClient, self._repository, xmlDoc=result) - - def createDocumentFromString(self, - name, - properties={}, - contentString=None, - contentType=None, - contentEncoding=None): - - """ - Creates a new document setting the content to the string provided. If - the repository supports unfiled objects, you do not have to pass in - a parent :class:`Folder` otherwise it is required. - - This method is essentially a convenience method that wraps your string - with a StringIO and then calls createDocument. - - >>> testFolder.createDocumentFromString('testdoc3', contentString='hello, world', contentType='text/plain') - """ - - return self._repository.createDocumentFromString(name, properties, - self, contentString, contentType, contentEncoding) - - def createDocument(self, name, properties={}, contentFile=None, - contentType=None, contentEncoding=None): - - """ - Creates a new Document object in the repository using - the properties provided. - - Right now this is basically the same as createFolder, - but this deals with contentStreams. The common logic should - probably be moved to CmisObject.createObject. - - The method will attempt to guess the appropriate content - type and encoding based on the file. To specify it yourself, pass them - in via the contentType and contentEncoding arguments. - - >>> f = open('250px-Cmis_logo.png', 'rb') - >>> subFolder.createDocument('logo.png', contentFile=f) - - >>> 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) - - >>> f.close() - - To specify a custom object type, pass in a property called - cmis:objectTypeId set to the :class:`CmisId` representing the type ID - of the instance you want to create. If you do not pass in an object - type ID, an instance of 'cmis:document' will be created. - - The following optional arguments are not yet supported: - - versioningState - - policies - - addACEs - - removeACEs - """ - - return self._repository.createDocument(name, - properties, - self, - contentFile, - contentType, - contentEncoding) - - def getChildren(self, **kwargs): - - """ - Returns a paged :class:`ResultSet`. The result set contains a list of - :class:`CmisObject` objects for each child of the Folder. The actual - type of the object returned depends on the object's CMIS base type id. - For example, the method might return a list that contains both - :class:`Document` objects and :class:`Folder` objects. - - >>> childrenRS = subFolder.getChildren() - >>> children = childrenRS.getResults() - - The following optional arguments are supported: - - maxItems - - skipCount - - orderBy - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - - includePathSegment - """ - - # get the appropriate 'down' link - childrenUrl = self.getChildrenLink() - # invoke the URL - result = self._cmisClient.get(childrenUrl.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self._repository, result) - - def getChildrenLink(self): - - """ - Gets the Atom link that knows how to return this object's children. - """ - - url = self._getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P) - - assert len(url) > 0, "Could not find the children url" - - return url - - def getDescendantsLink(self): - - """ - Returns the 'down' link of type `CMIS_TREE_TYPE` - - >>> folder.getDescendantsLink() - u'http://localhost:8080/alfresco/s/cmis/s/workspace:SpacesStore/i/86f6bf54-f0e8-4a72-8cb1-213599ba086c/descendants' - """ - - url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P) - - assert len(url) > 0, "Could not find the descendants url" - - # some servers return a depth arg as part of this URL - # so strip it off but keep other args - if url.find("?") >= 0: - u = list(urlparse(url)) - u[4] = '&'.join([p for p in u[4].split('&') if not p.startswith('depth=')]) - url = urlunparse(u) - - return url - - def getDescendants(self, **kwargs): - - """ - Gets the descendants of this folder. The descendants are returned as - a paged :class:`ResultSet` object. The result set contains a list of - :class:`CmisObject` objects where the actual type of each object - returned will vary depending on the object's base type id. For example, - the method might return a list that contains both :class:`Document` - objects and :class:`Folder` objects. - - The following optional argument is supported: - - depth. Use depth=-1 for all descendants, which is the default if no - depth is specified. - - >>> resultSet = folder.getDescendants() - >>> len(resultSet.getResults()) - 105 - >>> resultSet = folder.getDescendants(depth=1) - >>> len(resultSet.getResults()) - 103 - - The following optional arguments *may* also work but haven't been - tested: - - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - - includePathSegment - - """ - - if not self._repository.getCapabilities()['GetDescendants']: - raise NotSupportedException('This repository does not support getDescendants') - - # default the depth to -1, which is all descendants - if "depth" not in kwargs: - kwargs['depth'] = -1 - - # get the appropriate 'down' link - descendantsUrl = self.getDescendantsLink() - - # invoke the URL - result = self._cmisClient.get(descendantsUrl.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self._repository, result) - - def getTree(self, **kwargs): - - """ - Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`, - this method returns only the descendant objects that are folders. The - results do not include the current folder. - - The following optional arguments are supported: - - depth - - filter - - includeRelationships - - renditionFilter - - includeAllowableActions - - includePathSegment - - >>> rs = folder.getTree(depth='2') - >>> len(rs.getResults()) - 3 - >>> for folder in rs.getResults().values(): - ... folder.getTitle() - ... - u'subfolder2' - u'parent test folder' - u'subfolder' - """ - - # Get the descendants link and do a GET against it - url = self._getLink(FOLDER_TREE_REL) - assert url != None, 'Unable to determine folder tree link' - result = self._cmisClient.get(url.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return ResultSet(self._cmisClient, self, result) - - def getParent(self): - - """ - This is not yet implemented. - - The optional filter argument is not yet supported. - """ - # get the appropriate 'up' link - parentUrl = self._getLink(UP_REL) - # invoke the URL - result = self._cmisClient.get(parentUrl.encode('utf-8')) - - if type(result) == HTTPError: - raise CmisException(result.code) - - # return the result set - return Folder(self._cmisClient, self._repository, xmlDoc=result) - - def deleteTree(self, **kwargs): - - """ - Deletes the folder and all of its descendant objects. - - >>> resultSet = subFolder.getDescendants() - >>> len(resultSet.getResults()) - 2 - >>> subFolder.deleteTree() - - The following optional arguments are supported: - - allVersions - - unfileObjects - - continueOnFailure - """ - - # Per the spec, the repo must have the GetDescendants capability - # to support deleteTree - if not self._repository.getCapabilities()['GetDescendants']: - raise NotSupportedException('This repository does not support deleteTree') - - # Get the descendants link and do a DELETE against it - url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P) - result = self._cmisClient.delete(url.encode('utf-8'), **kwargs) - - if type(result) == HTTPError: - raise CmisException(result.code) - - def addObject(self, cmisObject, **kwargs): - - """ - Adds the specified object as a child of this object. No new object is - created. The repository must support multifiling for this to work. - - >>> sub1 = repo.getObjectByPath("/cmislib/sub1") - >>> sub2 = repo.getObjectByPath("/cmislib/sub2") - >>> doc = sub1.createDocument("testdoc1") - >>> len(sub1.getChildren()) - 1 - >>> len(sub2.getChildren()) - 0 - >>> sub2.addObject(doc) - >>> len(sub2.getChildren()) - 1 - >>> sub2.getChildren()[0].name - u'testdoc1' - - The following optional arguments are supported: - - allVersions - """ - - if not self._repository.getCapabilities()['Multifiling']: - raise NotSupportedException('This repository does not support multifiling') - - postUrl = self.getChildrenLink() - - # post the Atom entry - result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **kwargs) - if type(result) == HTTPError: - raise CmisException(result.code) - - def removeObject(self, cmisObject): - - """ - Removes the specified object from this folder. The repository must - support unfiling for this to work. - """ - - if not self._repository.getCapabilities()['Unfiling']: - raise NotSupportedException('This repository does not support unfiling') - - postUrl = self._repository.getCollectionLink(UNFILED_COLL) - - args = {"removeFrom": self.getObjectId()} - - # post the Atom entry to the unfiled collection - result = self._cmisClient.post(postUrl.encode('utf-8'), cmisObject.xmlDoc.toxml(encoding='utf-8'), ATOM_XML_ENTRY_TYPE, **args) - if type(result) == HTTPError: - raise CmisException(result.code) - - def getPaths(self): - """ - Returns the paths as a list of strings. The spec says folders cannot - be multi-filed, so this should always be one value. We return a list - to be symmetric with the same method in :class:`Document`. - """ - return [self.properties['cmis:path']] - - -class Relationship(CmisObject): - - """ - Defines a relationship object between two :class:`CmisObjects` objects - """ - - def getSourceId(self): - - """ - Returns the :class:`CmisId` on the source side of the relationship. - """ - - if self.xmlDoc == None: - self.reload() - props = self.getProperties() - return CmisId(props['cmis:sourceId']) - - def getTargetId(self): - - """ - Returns the :class:`CmisId` on the target side of the relationship. - """ - - if self.xmlDoc == None: - self.reload() - props = self.getProperties() - return CmisId(props['cmis:targetId']) - - def getSource(self): - - """ - Returns an instance of the appropriate child-type of :class:`CmisObject` - for the source side of the relationship. - """ - - sourceId = self.getSourceId() - return getSpecializedObject(self._repository.getObject(sourceId)) - - def getTarget(self): - - """ - Returns an instance of the appropriate child-type of :class:`CmisObject` - for the target side of the relationship. - """ - - targetId = self.getTargetId() - return getSpecializedObject(self._repository.getObject(targetId)) - - sourceId = property(getSourceId) - targetId = property(getTargetId) - source = property(getSource) - target = property(getTarget) - - -class Policy(CmisObject): - - """ - An arbirary object that can 'applied' to objects that the - repository identifies as being 'controllable'. - """ - - pass - - -class ObjectType(object): - - """ - Represents the CMIS object type such as 'cmis:document' or 'cmis:folder'. - Contains metadata about the type. - """ - - def __init__(self, cmisClient, repository, typeId=None, xmlDoc=None): - """ Constructor """ - self._cmisClient = cmisClient - self._repository = repository - self._kwargs = None - self._typeId = typeId - self.xmlDoc = xmlDoc - self.logger = logging.getLogger('cmislib.model.ObjectType') - self.logger.info('Creating an instance of ObjectType') - - def __str__(self): - """To string""" - return self.getTypeId() - - def getTypeId(self): - - """ - Returns the type ID for this object. - - >>> docType = repo.getTypeDefinition('cmis:document') - >>> docType.getTypeId() - 'cmis:document' - """ - - if self._typeId == None: - if self.xmlDoc == None: - self.reload() - self._typeId = CmisId(self._getElementValue(CMIS_NS, 'id')) - - return self._typeId - - def _getElementValue(self, namespace, elementName): - - """ - Helper method to retrieve child element values from type XML. - """ - - if self.xmlDoc == None: - self.reload() - #typeEls = self.xmlDoc.getElementsByTagNameNS(CMISRA_NS, 'type') - #assert len(typeEls) == 1, "Expected to find exactly one type element but instead found %d" % len(typeEls) - #typeEl = typeEls[0] - typeEl = None - for e in self.xmlDoc.childNodes: - if e.nodeType == e.ELEMENT_NODE and e.localName == "type": - typeEl = e - break - - assert typeEl, "Expected to find one child element named type" - els = typeEl.getElementsByTagNameNS(namespace, elementName) - if len(els) >= 1: - el = els[0] - if el and len(el.childNodes) >= 1: - return el.childNodes[0].data - - def getLocalName(self): - """Getter for cmis:localName""" - return self._getElementValue(CMIS_NS, 'localName') - - def getLocalNamespace(self): - """Getter for cmis:localNamespace""" - return self._getElementValue(CMIS_NS, 'localNamespace') - - def getDisplayName(self): - """Getter for cmis:displayName""" - return self._getElementValue(CMIS_NS, 'displayName') - - def getQueryName(self): - """Getter for cmis:queryName""" - return self._getElementValue(CMIS_NS, 'queryName') - - def getDescription(self): - """Getter for cmis:description""" - return self._getElementValue(CMIS_NS, 'description') - - def getBaseId(self): - """Getter for cmis:baseId""" - return CmisId(self._getElementValue(CMIS_NS, 'baseId')) - - def isCreatable(self): - """Getter for cmis:creatable""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'creatable')) - - def isFileable(self): - """Getter for cmis:fileable""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'fileable')) - - def isQueryable(self): - """Getter for cmis:queryable""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) - - def isFulltextIndexed(self): - """Getter for cmis:fulltextIndexed""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'fulltextIndexed')) - - def isIncludedInSupertypeQuery(self): - """Getter for cmis:includedInSupertypeQuery""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'includedInSupertypeQuery')) - - def isControllablePolicy(self): - """Getter for cmis:controllablePolicy""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'controllablePolicy')) - - def isControllableACL(self): - """Getter for cmis:controllableACL""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'controllableACL')) - - def getLink(self, rel, linkType): - - """ - Gets the HREF for the link element with the specified rel and linkType. - - >>> from cmislib.model import ATOM_XML_FEED_TYPE - >>> docType.getLink('down', ATOM_XML_FEED_TYPE) - u'http://localhost:8080/alfresco/s/cmis/type/cmis:document/children' - """ - - linkElements = self.xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - for linkElement in linkElements: - - if linkElement.attributes.has_key('rel') and linkElement.attributes.has_key('type'): - relAttr = linkElement.attributes['rel'].value - typeAttr = linkElement.attributes['type'].value - - if relAttr == rel and linkType.match(typeAttr): - return linkElement.attributes['href'].value - - def getProperties(self): - - """ - Returns a list of :class:`Property` objects representing each property - defined for this type. - - >>> objType = repo.getTypeDefinition('cmis:relationship') - >>> for prop in objType.properties: - ... print 'Id:%s' % prop.id - ... print 'Cardinality:%s' % prop.cardinality - ... print 'Description:%s' % prop.description - ... print 'Display name:%s' % prop.displayName - ... print 'Local name:%s' % prop.localName - ... print 'Local namespace:%s' % prop.localNamespace - ... print 'Property type:%s' % prop.propertyType - ... print 'Query name:%s' % prop.queryName - ... print 'Updatability:%s' % prop.updatability - ... print 'Inherited:%s' % prop.inherited - ... print 'Orderable:%s' % prop.orderable - ... print 'Queryable:%s' % prop.queryable - ... print 'Required:%s' % prop.required - ... print 'Open choice:%s' % prop.openChoice - """ - - if self.xmlDoc == None: - self.reload(includePropertyDefinitions='true') - # Currently, property defs don't have an enclosing element. And, the - # element name varies depending on type. Until that changes, I'm going - # to find all elements unique to a prop, then grab its parent node. - propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') - if len(propTypeElements) <= 0: - self.reload(includePropertyDefinitions='true') - propTypeElements = self.xmlDoc.getElementsByTagNameNS(CMIS_NS, 'propertyType') - assert len(propTypeElements) > 0, 'Could not retrieve object type property definitions' - props = {} - for typeEl in propTypeElements: - prop = Property(typeEl.parentNode) - props[prop.id] = prop - return props - - def reload(self, **kwargs): - """ - This method will reload the object's data from the CMIS service. - """ - if kwargs: - if self._kwargs: - self._kwargs.update(kwargs) - else: - self._kwargs = kwargs - templates = self._repository.getUriTemplates() - template = templates['typebyid']['template'] - params = {'{id}': self._typeId} - byTypeIdUrl = multiple_replace(params, template) - result = self._cmisClient.get(byTypeIdUrl.encode('utf-8'), **kwargs) - if type(result) == HTTPError: - raise CmisException(result.code) - - # instantiate CmisObject objects with the results and return the list - entryElements = result.getElementsByTagNameNS(ATOM_NS, 'entry') - assert(len(entryElements) == 1), "Expected entry element in result from calling %s" % byTypeIdUrl - self.xmlDoc = entryElements[0] - - id = property(getTypeId) - localName = property(getLocalName) - localNamespace = property(getLocalNamespace) - displayName = property(getDisplayName) - queryName = property(getQueryName) - description = property(getDescription) - baseId = property(getBaseId) - creatable = property(isCreatable) - fileable = property(isFileable) - queryable = property(isQueryable) - fulltextIndexed = property(isFulltextIndexed) - includedInSupertypeQuery = property(isIncludedInSupertypeQuery) - controllablePolicy = property(isControllablePolicy) - controllableACL = property(isControllableACL) - properties = property(getProperties) - - -class Property(object): - - """ - This class represents an attribute or property definition of an object - type. - """ - - def __init__(self, propNode): - """Constructor""" - self.xmlDoc = propNode - self.logger = logging.getLogger('cmislib.model.Property') - self.logger.info('Creating an instance of Property') - - def __str__(self): - """To string""" - return self.getId() - - def _getElementValue(self, namespace, elementName): - - """ - Utility method for retrieving element values from the object type XML. - """ - - els = self.xmlDoc.getElementsByTagNameNS(namespace, elementName) - if len(els) >= 1: - el = els[0] - if el and len(el.childNodes) >= 1: - return el.childNodes[0].data - - def getId(self): - """Getter for cmis:id""" - return self._getElementValue(CMIS_NS, 'id') - - def getLocalName(self): - """Getter for cmis:localName""" - return self._getElementValue(CMIS_NS, 'localName') - - def getLocalNamespace(self): - """Getter for cmis:localNamespace""" - return self._getElementValue(CMIS_NS, 'localNamespace') - - def getDisplayName(self): - """Getter for cmis:displayName""" - return self._getElementValue(CMIS_NS, 'displayName') - - def getQueryName(self): - """Getter for cmis:queryName""" - return self._getElementValue(CMIS_NS, 'queryName') - - def getDescription(self): - """Getter for cmis:description""" - return self._getElementValue(CMIS_NS, 'description') - - def getPropertyType(self): - """Getter for cmis:propertyType""" - return self._getElementValue(CMIS_NS, 'propertyType') - - def getCardinality(self): - """Getter for cmis:cardinality""" - return self._getElementValue(CMIS_NS, 'cardinality') - - def getUpdatability(self): - """Getter for cmis:updatability""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'updatability')) - - def isInherited(self): - """Getter for cmis:inherited""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'inherited')) - - def isRequired(self): - """Getter for cmis:required""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'required')) - - def isQueryable(self): - """Getter for cmis:queryable""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'queryable')) - - def isOrderable(self): - """Getter for cmis:orderable""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'orderable')) - - def isOpenChoice(self): - """Getter for cmis:openChoice""" - return parseBoolValue(self._getElementValue(CMIS_NS, 'openChoice')) - - id = property(getId) - localName = property(getLocalName) - localNamespace = property(getLocalNamespace) - displayName = property(getDisplayName) - queryName = property(getQueryName) - description = property(getDescription) - propertyType = property(getPropertyType) - cardinality = property(getCardinality) - updatability = property(getUpdatability) - inherited = property(isInherited) - required = property(isRequired) - queryable = property(isQueryable) - orderable = property(isOrderable) - openChoice = property(isOpenChoice) - - -class ACL(object): - - """ - Represents the Access Control List for an object. - """ - - def __init__(self, aceList=None, xmlDoc=None): - - """ - Constructor. Pass in either a list of :class:`ACE` objects or the XML - representation of the ACL. If you have only one ACE, don't worry about - the list--the constructor will convert it to a list for you. - """ - - if aceList: - self._entries = aceList - else: - self._entries = {} - if xmlDoc: - self._xmlDoc = xmlDoc - self._entries = self._getEntriesFromXml() - else: - self._xmlDoc = None - - self.logger = logging.getLogger('cmislib.model.ACL') - self.logger.info('Creating an instance of ACL') - - def addEntry(self, ace): - - """ - Adds an :class:`ACE` entry to the ACL. - - >>> acl = folder.getACL() - >>> acl.addEntry(ACE('jpotts', 'cmis:read', 'true')) - >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) - >>> acl.getEntries() - {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } - """ - - self._entries[ace.principalId] = ace - - def removeEntry(self, principalId): - - """ - Removes the :class:`ACE` entry given a specific principalId. - - >>> acl.getEntries() - {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': , 'jsmith': } - >>> acl.removeEntry('jsmith') - >>> acl.getEntries() - {u'GROUP_EVERYONE': , u'jdoe': , 'jpotts': } - """ - - if self._entries.has_key(principalId): - del(self._entries[principalId]) - - def clearEntries(self): - - """ - Clears all :class:`ACE` entries from the ACL and removes the internal - XML representation of the ACL. - - >>> acl = ACL() - >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) - >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) - >>> acl.entries - {'jpotts': , 'jsmith': } - >>> acl.getXmlDoc() - - >>> acl.clearEntries() - >>> acl.entries - >>> acl.getXmlDoc() - """ - - self._entries.clear() - self._xmlDoc = None - - def getEntries(self): - - """ - Returns a dictionary of :class:`ACE` objects for each Access Control - Entry in the ACL. The key value is the ACE principalid. - - >>> acl = ACL() - >>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true')) - >>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true')) - >>> for ace in acl.entries.values(): - ... print 'principal:%s has the following permissions...' % ace.principalId - ... for perm in ace.permissions: - ... print perm - ... - principal:jpotts has the following permissions... - cmis:write - principal:jsmith has the following permissions... - cmis:write - """ - - if self._entries: - return self._entries - else: - if self._xmlDoc: - # parse XML doc and build entry list - self._entries = self._getEntriesFromXml() - # then return it - return self._entries - - def _getEntriesFromXml(self): - - """ - Helper method for getting the :class:`ACE` entries from an XML - representation of the ACL. - """ - - if not self._xmlDoc: - return - result = {} - # first child is the root node, cmis:acl - for e in self._xmlDoc.childNodes[0].childNodes: - if e.localName == 'permission': - # grab the principal/principalId element value - prinEl = e.getElementsByTagNameNS(CMIS_NS, 'principal')[0] - if prinEl and prinEl.childNodes: - prinIdEl = prinEl.getElementsByTagNameNS(CMIS_NS, 'principalId')[0] - if prinIdEl and prinIdEl.childNodes: - principalId = prinIdEl.childNodes[0].data - # grab the permission values - permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission') - perms = [] - for permEl in permEls: - if permEl and permEl.childNodes: - perms.append(permEl.childNodes[0].data) - # grab the direct value - dirEl = e.getElementsByTagNameNS(CMIS_NS, 'direct')[0] - if dirEl and dirEl.childNodes: - direct = dirEl.childNodes[0].data - # create an ACE - if (len(perms) > 0): - ace = ACE(principalId, perms, direct) - # append it to the dictionary - result[principalId] = ace - return result - - def getXmlDoc(self): - - """ - This method rebuilds the local XML representation of the ACL based on - the :class:`ACE` objects in the entries list and returns the resulting - XML Document. - """ - - if not self.getEntries(): - return - - xmlDoc = minidom.Document() - aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl') - aclEl.setAttribute('xmlns:cmis', CMIS_NS) - for ace in self.getEntries().values(): - permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') - #principalId - prinEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principal') - prinIdEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principalId') - prinIdElText = xmlDoc.createTextNode(ace.principalId) - prinIdEl.appendChild(prinIdElText) - prinEl.appendChild(prinIdEl) - permEl.appendChild(prinEl) - #direct - directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct') - directElText = xmlDoc.createTextNode(ace.direct) - directEl.appendChild(directElText) - permEl.appendChild(directEl) - #permissions - for perm in ace.permissions: - permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission') - permItemElText = xmlDoc.createTextNode(perm) - permItemEl.appendChild(permItemElText) - permEl.appendChild(permItemEl) - aclEl.appendChild(permEl) - xmlDoc.appendChild(aclEl) - self._xmlDoc = xmlDoc - return self._xmlDoc - - entries = property(getEntries) - - -class ACE(object): - - """ - Represents an individual Access Control Entry. - """ - - def __init__(self, principalId=None, permissions=None, direct=None): - """Constructor""" - self._principalId = principalId - if permissions: - if isinstance(permissions, str): - self._permissions = [permissions] - else: - self._permissions = permissions - self._direct = direct - - self.logger = logging.getLogger('cmislib.model.ACE') - self.logger.info('Creating an instance of ACE') - - @property - def principalId(self): - """Getter for principalId""" - return self._principalId - - @property - def direct(self): - """Getter for direct""" - return self._direct - - @property - def permissions(self): - """Getter for permissions""" - return self._permissions - - -class ChangeEntry(object): - - """ - Represents a change log entry. Retrieve a list of change entries via - :meth:`Repository.getContentChanges`. - - >>> for changeEntry in rs: - ... changeEntry.objectId - ... changeEntry.id - ... changeEntry.changeType - ... changeEntry.changeTime - ... - 'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b' - u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b' - u'created' - datetime.datetime(2010, 2, 11, 12, 55, 14) - 'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923' - u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923' - u'updated' - datetime.datetime(2010, 2, 11, 12, 55, 13) - 'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' - u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb' - u'updated' - """ - - def __init__(self, cmisClient, repository, xmlDoc): - """Constructor""" - self._cmisClient = cmisClient - self._repository = repository - self._xmlDoc = xmlDoc - self._properties = {} - self._objectId = None - self._changeEntryId = None - self._changeType = None - self._changeTime = None - self.logger = logging.getLogger('cmislib.model.ChangeEntry') - self.logger.info('Creating an instance of ChangeEntry') - - def getId(self): - """ - Returns the unique ID of the change entry. - """ - if self._changeEntryId == None: - self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data - return self._changeEntryId - - def getObjectId(self): - """ - Returns the object ID of the object that changed. - """ - if self._objectId == None: - props = self.getProperties() - self._objectId = CmisId(props['cmis:objectId']) - return self._objectId - - def getChangeType(self): - - """ - Returns the type of change that occurred. The resulting value must be - one of: - - - created - - updated - - deleted - - security - """ - - if self._changeType == None: - self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data - return self._changeType - - def getACL(self): - - """ - Gets the :class:`ACL` object that is included with this Change Entry. - """ - - # if you call getContentChanges with includeACL=true, you will get a - # cmis:ACL entry. change entries don't appear to have a self URL so - # instead of doing a reload with includeACL set to true, we'll either - # see if the XML already has an ACL element and instantiate an ACL with - # it, or we'll get the ACL_REL link, invoke that, and return the result - if not self._repository.getCapabilities()['ACL']: - return - aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl') - aclUrl = self._getLink(ACL_REL) - if (len(aclEls) == 1): - return ACL(self._cmisClient, self._repository, aclEls[0]) - elif aclUrl: - result = self._cmisClient.get(aclUrl.encode('utf-8')) - if type(result) == HTTPError: - raise CmisException(result.code) - return ACL(xmlDoc=result) - - def getChangeTime(self): - - """ - Returns a datetime object representing the time the change occurred. - """ - - if self._changeTime == None: - self._changeTime = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeTime')[0].firstChild.data - return parseDateTimeValue(self._changeTime) - - def getProperties(self): - - """ - Returns the properties of the change entry. Note that depending on the - capabilities of the repository ("capabilityChanges") the list may not - include the actual property values that changed. - """ - - if self._properties == {}: - propertiesElement = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'properties')[0] - for node in [e for e in propertiesElement.childNodes if e.nodeType == e.ELEMENT_NODE]: - propertyName = node.attributes['propertyDefinitionId'].value - if node.childNodes and \ - node.getElementsByTagNameNS(CMIS_NS, 'value')[0] and \ - node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes: - propertyValue = parsePropValue( - node.getElementsByTagNameNS(CMIS_NS, 'value')[0].childNodes[0].data, - node.localName) - else: - propertyValue = None - self._properties[propertyName] = propertyValue - return self._properties - - def _getLink(self, rel): - - """ - Returns the HREF attribute of an Atom link element for the - specified rel. - """ - - linkElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'link') - - for linkElement in linkElements: - if linkElement.attributes.has_key('rel'): - relAttr = linkElement.attributes['rel'].value - - if relAttr == rel: - return linkElement.attributes['href'].value - - id = property(getId) - objectId = property(getObjectId) - changeTime = property(getChangeTime) - changeType = property(getChangeType) - properties = property(getProperties) - - -class ChangeEntryResultSet(ResultSet): - - """ - A specialized type of :class:`ResultSet` that knows how to instantiate - :class:`ChangeEntry` objects. The parent class assumes children of - :class:`CmisObject` which doesn't work for ChangeEntries. - """ - - def __iter__(self): - - """ - Overriding to make it work with a list instead of a dict. - """ - - return iter(self.getResults()) - - def __getitem__(self, index): - - """ - Overriding to make it work with a list instead of a dict. - """ - - return self.getResults()[index] - - def __len__(self): - - """ - Overriding to make it work with a list instead of a dict. - """ - - return len(self.getResults()) - - def getResults(self): - - """ - Overriding to make it work with a list instead of a dict. - """ - - if self._results: - return self._results - - if self._xmlDoc: - entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry') - entries = [] - for entryElement in entryElements: - changeEntry = ChangeEntry(self._cmisClient, self._repository, entryElement) - entries.append(changeEntry) - - self._results = entries - - return self._results - - -class Rendition(object): - - """ - This class represents a Rendition. - """ - - def __init__(self, propNode): - """Constructor""" - self.xmlDoc = propNode - self.logger = logging.getLogger('cmislib.model.Rendition') - self.logger.info('Creating an instance of Rendition') - - def __str__(self): - """To string""" - return self.getStreamId() - - def getStreamId(self): - """Getter for the rendition's stream ID""" - if self.xmlDoc.attributes.has_key('streamId'): - return self.xmlDoc.attributes['streamId'].value - - def getMimeType(self): - """Getter for the rendition's mime type""" - if self.xmlDoc.attributes.has_key('type'): - return self.xmlDoc.attributes['type'].value - - def getLength(self): - """Getter for the renditions's length""" - if self.xmlDoc.attributes.has_key('length'): - return self.xmlDoc.attributes['length'].value - - def getTitle(self): - """Getter for the renditions's title""" - if self.xmlDoc.attributes.has_key('title'): - return self.xmlDoc.attributes['title'].value - - def getKind(self): - """Getter for the renditions's kind""" - if self.xmlDoc.hasAttributeNS(CMISRA_NS, 'renditionKind'): - return self.xmlDoc.getAttributeNS(CMISRA_NS, 'renditionKind') - - def getHeight(self): - """Getter for the renditions's height""" - if self.xmlDoc.attributes.has_key('height'): - return self.xmlDoc.attributes['height'].value - - def getWidth(self): - """Getter for the renditions's width""" - if self.xmlDoc.attributes.has_key('width'): - return self.xmlDoc.attributes['width'].value - - def getHref(self): - """Getter for the renditions's href""" - if self.xmlDoc.attributes.has_key('href'): - return self.xmlDoc.attributes['href'].value - - def getRenditionDocumentId(self): - """Getter for the renditions's width""" - if self.xmlDoc.attributes.has_key('renditionDocumentId'): - return self.xmlDoc.attributes['renditionDocumentId'].value - - streamId = property(getStreamId) - mimeType = property(getMimeType) - length = property(getLength) - title = property(getTitle) - kind = property(getKind) - height = property(getHeight) - width = property(getWidth) - href = property(getHref) - renditionDocumentId = property(getRenditionDocumentId) - - -class CmisId(str): - - """ - This is a marker class to be used for Strings that are used as CMIS ID's. - Making the objects instances of this class makes it easier to create the - Atom entry XML with the appropriate type, ie, cmis:propertyId, instead of - cmis:propertyString. - """ - - pass - - -class UriTemplate(dict): - - """ - Simple dictionary to represent the data stored in - a URI template entry. - """ - - def __init__(self, template, templateType, mediaType): - - """ - Constructor - """ - - dict.__init__(self) - self['template'] = template - self['type'] = templateType - self['mediaType'] = mediaType - - -def parsePropValue(value, nodeName): - - """ - Returns a properly-typed object based on the type as specified in the - node's element name. - """ - - moduleLogger.debug('Inside parsePropValue') - - if nodeName == 'propertyId': - return CmisId(value) - elif nodeName == 'propertyString': - return value - elif nodeName == 'propertyBoolean': - bDict = {'false': False, 'true': True} - return bDict[value.lower()] - elif nodeName == 'propertyInteger': - return int(value) - elif nodeName == 'propertyDecimal': - return float(value) - elif nodeName == 'propertyDateTime': - #%z doesn't seem to work, so I'm going to trunc the offset - #not all servers return microseconds, so those go too - return parseDateTimeValue(value) - else: - return value - - -def parseDateTimeValue(value): - - """ - Utility function to return a datetime from a string. - """ - return iso8601.parse_date(value) - - -def parseBoolValue(value): - - """ - Utility function to parse booleans and none from strings - """ - - if value == 'false': - return False - elif value == 'true': - return True - elif value == 'none': - return None - else: - return value - - -def toCMISValue(value): - - """ - Utility function to convert Python values to CMIS string values - """ - - if value == False: - return 'false' - elif value == True: - return 'true' - elif value == None: - return 'none' - else: - return value - - -def multiple_replace(aDict, text): - - """ - Replace in 'text' all occurences of any key in the given - dictionary by its corresponding value. Returns the new string. - - See http://code.activestate.com/recipes/81330/ - """ - - # Create a regular expression from the dictionary keys - regex = re.compile("(%s)" % "|".join(map(re.escape, aDict.keys()))) - - # For each match, look-up corresponding value in dictionary - return regex.sub(lambda mo: aDict[mo.string[mo.start():mo.end()]], text) - - -def getSpecializedObject(obj, **kwargs): - - """ - Returns an instance of the appropriate :class:`CmisObject` class or one - of its child types depending on the specified baseType. - """ - - moduleLogger.debug('Inside getSpecializedObject') - - if 'cmis:baseTypeId' in obj.getProperties(): - baseType = obj.getProperties()['cmis:baseTypeId'] - if baseType == 'cmis:folder': - return Folder(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) - if baseType == 'cmis:document': - return Document(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) - if baseType == 'cmis:relationship': - return Relationship(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) - if baseType == 'cmis:policy': - return Policy(obj._cmisClient, obj._repository, obj.getObjectId(), obj.xmlDoc, **kwargs) - - # if the base type ID wasn't found in the props (this can happen when - # someone runs a query that doesn't select * or doesn't individually - # specify baseTypeId) or if the type isn't one of the known base - # types, give the object back - return obj - - -def getEmptyXmlDoc(): - - """ - Internal helper method that knows how to build an empty Atom entry. - """ - - moduleLogger.debug('Inside getEmptyXmlDoc') - - entryXmlDoc = minidom.Document() - entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") - entryElement.setAttribute('xmlns', ATOM_NS) - entryXmlDoc.appendChild(entryElement) - return entryXmlDoc - - -def getEntryXmlDoc(repo=None, objectTypeId=None, properties=None, contentFile=None, - contentType=None, contentEncoding=None): - - """ - Internal helper method that knows how to build an Atom entry based - on the properties and, optionally, the contentFile provided. - """ - - moduleLogger.debug('Inside getEntryXmlDoc') - - entryXmlDoc = minidom.Document() - entryElement = entryXmlDoc.createElementNS(ATOM_NS, "entry") - entryElement.setAttribute('xmlns', ATOM_NS) - entryElement.setAttribute('xmlns:app', APP_NS) - entryElement.setAttribute('xmlns:cmisra', CMISRA_NS) - entryXmlDoc.appendChild(entryElement) - - # if there is a File, encode it and add it to the XML - if contentFile: - mimetype = contentType - encoding = contentEncoding - - # need to determine the mime type - if not mimetype and hasattr(contentFile, 'name'): - mimetype, encoding = mimetypes.guess_type(contentFile.name) - - if not mimetype: - mimetype = 'application/binary' - - if not encoding: - encoding = 'utf8' - - # This used to be ATOM_NS content but there is some debate among - # vendors whether the ATOM_NS content must always be base64 - # encoded. The spec does mandate that CMISRA_NS content be encoded - # and that element takes precedence over ATOM_NS content if it is - # present, so it seems reasonable to use CMIS_RA content for now - # and encode everything. - - fileData = contentFile.read().encode("base64") - mediaElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:mediatype') - mediaElementText = entryXmlDoc.createTextNode(mimetype) - mediaElement.appendChild(mediaElementText) - base64Element = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:base64') - base64ElementText = entryXmlDoc.createTextNode(fileData) - base64Element.appendChild(base64ElementText) - contentElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:content') - contentElement.appendChild(mediaElement) - contentElement.appendChild(base64Element) - entryElement.appendChild(contentElement) - - objectElement = entryXmlDoc.createElementNS(CMISRA_NS, 'cmisra:object') - objectElement.setAttribute('xmlns:cmis', CMIS_NS) - entryElement.appendChild(objectElement) - - if properties: - # a name is required for most things, but not for a checkout - if properties.has_key('cmis:name'): - titleElement = entryXmlDoc.createElementNS(ATOM_NS, "title") - titleText = entryXmlDoc.createTextNode(properties['cmis:name']) - titleElement.appendChild(titleText) - entryElement.appendChild(titleElement) - - propsElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:properties') - objectElement.appendChild(propsElement) - - typeDef = None - for propName, propValue in properties.items(): - """ - the name of the element here is significant: it includes the - data type. I should be able to figure out the right type based - on the actual type of the object passed in. - - I could do a lookup to the type definition, but that doesn't - seem worth the performance hit - """ - if (propValue == None or (type(propValue) == list and propValue[0] == None)): - # grab the prop type from the typeDef - if (typeDef == None): - moduleLogger.debug('Looking up type def for: %s' % objectTypeId) - typeDef = repo.getTypeDefinition(objectTypeId) - #TODO what to do if type not found - propType = typeDef.properties[propName].propertyType - elif type(propValue) == list: - propType = type(propValue[0]) - else: - propType = type(propValue) - - propElementName, propValueStrList = getElementNameAndValues(propType, propName, propValue, type(propValue) == list) - - propElement = entryXmlDoc.createElementNS(CMIS_NS, propElementName) - propElement.setAttribute('propertyDefinitionId', propName) - for val in propValueStrList: - if val == None: - continue - valElement = entryXmlDoc.createElementNS(CMIS_NS, 'cmis:value') - valText = entryXmlDoc.createTextNode(val) - valElement.appendChild(valText) - propElement.appendChild(valElement) - propsElement.appendChild(propElement) - - return entryXmlDoc - - -def getElementNameAndValues(propType, propName, propValue, isList=False): - - """ - For a given property type, property name, and property value, this function - returns the appropriate CMIS Atom entry element name and value list. - """ - - moduleLogger.debug('Inside getElementNameAndValues') - moduleLogger.debug('propType:%s propName:%s isList:%s' % (propType, propName, isList)) - if (propType == 'id' or propType == CmisId): - propElementName = 'cmis:propertyId' - if isList: - propValueStrList = [] - for val in propValue: - propValueStrList.append(val) - else: - propValueStrList = [propValue] - elif (propType == 'string' or propType == str): - propElementName = 'cmis:propertyString' - if isList: - propValueStrList = [] - for val in propValue: - propValueStrList.append(val) - else: - propValueStrList = [propValue] - elif (propType == 'datetime' or propType == datetime.datetime): - propElementName = 'cmis:propertyDateTime' - if isList: - propValueStrList = [] - for val in propValue: - if val != None: - propValueStrList.append(val.isoformat()) - else: - propValueStrList.append(val) - else: - if propValue != None: - propValueStrList = [propValue.isoformat()] - else: - propValueStrList = [propValue] - elif (propType == 'boolean' or propType == bool): - propElementName = 'cmis:propertyBoolean' - if isList: - propValueStrList = [] - for val in propValue: - if val != None: - propValueStrList.append(unicode(val).lower()) - else: - propValueStrList.append(val) - else: - if propValue != None: - propValueStrList = [unicode(propValue).lower()] - else: - propValueStrList = [propValue] - elif (propType == 'integer' or propType == int): - propElementName = 'cmis:propertyInteger' - if isList: - propValueStrList = [] - for val in propValue: - if val != None: - propValueStrList.append(unicode(val)) - else: - propValueStrList.append(val) - else: - if propValue != None: - propValueStrList = [unicode(propValue)] - else: - propValueStrList = [propValue] - elif (propType == 'decimal' or propType == float): - propElementName = 'cmis:propertyDecimal' - if isList: - propValueStrList = [] - for val in propValue: - if val != None: - propValueStrList.append(unicode(val)) - else: - propValueStrList.append(val) - else: - if propValue != None: - propValueStrList = [unicode(propValue)] - else: - propValueStrList = [propValue] - else: - propElementName = 'cmis:propertyString' - if isList: - propValueStrList = [] - for val in propValue: - if val != None: - propValueStrList.append(unicode(val)) - else: - propValueStrList.append(val) - else: - if propValue != None: - propValueStrList = [unicode(propValue)] - else: - propValueStrList = [propValue] - - return propElementName, propValueStrList diff --git a/src/cmislib/net.py b/src/cmislib/net.py index 11a757a..af469a4 100644 --- a/src/cmislib/net.py +++ b/src/cmislib/net.py @@ -16,81 +16,20 @@ # specific language governing permissions and limitations # under the License. # -''' -Module that knows how to connect to the AtomPub Binding of a CMIS repo -''' +""" +Module that takes care of network communications for cmislib. It does +not know anything about CMIS or do anything special with regard to the +response it receives. +""" -from urllib import urlencode -from urllib2 import HTTPBasicAuthHandler, \ - HTTPPasswordMgrWithDefaultRealm, \ - HTTPRedirectHandler, \ - HTTPDefaultErrorHandler, \ - HTTPError, \ - Request, \ - build_opener, \ - AbstractBasicAuthHandler +import sys + +if sys.version_info >= (3,): + from urllib.parse import urlencode +else: + from urllib import urlencode import logging - - -class SmartRedirectHandler(HTTPRedirectHandler): - - """ Handles 301 and 302 redirects """ - - def http_error_301(self, req, fp, code, msg, headers): - """ Handle a 301 error """ - result = HTTPRedirectHandler.http_error_301( - self, req, fp, code, msg, headers) - result.status = code - return result - - def http_error_302(self, req, fp, code, msg, headers): - """ Handle a 302 error """ - result = HTTPRedirectHandler.http_error_302( - self, req, fp, code, msg, headers) - result.status = code - return result - - -class DefaultErrorHandler(HTTPDefaultErrorHandler): - - """ Default error handler """ - - def http_error_default(self, req, fp, code, msg, headers): - """Provide an implementation for the default handler""" - result = HTTPError( - req.get_full_url(), code, msg, headers, fp) - result.status = code - return result - - -class ContextualBasicAuthHandler(HTTPBasicAuthHandler): - - """ - Handles 401 errors without recursing indefinitely. The recursing - behaviour has been introduced in Python 2.6.5 to handle 401 redirects - used by some architectures of authentication. - """ - - def __init__(self, password_mgr): - HTTPBasicAuthHandler.__init__(self, password_mgr) - self.authContext = set([]) - - def http_error_401(self, req, fp, code, msg, headers): - """Override the default autoretry behaviour""" - url = req.get_full_url() - hdrs = req.header_items() - hdrs = ', '.join(['%s: %s' % (key, value) - for key, value in sorted(hdrs)]) - context = (url, hdrs) - if context in self.authContext: - self.authContext.clear() - result = HTTPError( - req.get_full_url(), code, msg, headers, fp) - result.status = code - return result - self.authContext.add(context) - return self.http_error_auth_reqed('www-authenticate', - url, req, headers) +import httplib2 class RESTService(object): @@ -112,12 +51,12 @@ class RESTService(object): """ Makes a get request to the URL specified.""" - headers = None + headers = {} if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] - del(kwargs['headers']) - self.logger.debug('Headers passed in:%s' % headers) + del kwargs['headers'] + self.logger.debug('Headers passed in:' + headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: @@ -125,35 +64,22 @@ class RESTService(object): self.logger.debug('About to do a GET on:' + url) - request = RESTRequest(url, method='GET') + h = httplib2.Http() + h.add_credentials(username, password) + headers['User-Agent'] = self.user_agent - # add a user-agent - request.add_header('User-Agent', self.user_agent) - if headers: - for k, v in headers.items(): - self.logger.debug('Adding header:%s:%s' % (k, v)) - request.add_header(k, v) - - # create a password manager - passwordManager = HTTPPasswordMgrWithDefaultRealm() - passwordManager.add_password(None, url, username, password) - - opener = build_opener(SmartRedirectHandler(), - DefaultErrorHandler(), - ContextualBasicAuthHandler(passwordManager)) - - return opener.open(request) + return h.request(url, method='GET', headers=headers) def delete(self, url, username=None, password=None, **kwargs): """ Makes a delete request to the URL specified. """ - headers = None + headers = {} if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] - del(kwargs['headers']) - self.logger.debug('Headers passed in:%s' % headers) + del kwargs['headers'] + self.logger.debug('Headers passed in:' + headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: @@ -161,30 +87,11 @@ class RESTService(object): self.logger.debug('About to do a DELETE on:' + url) - request = RESTRequest(url, method='DELETE') + h = httplib2.Http() + h.add_credentials(username, password) + headers['User-Agent'] = self.user_agent - # add a user-agent - request.add_header('User-Agent', self.user_agent) - if headers: - for k, v in headers.items(): - self.logger.debug('Adding header:%s:%s' % (k, v)) - request.add_header(k, v) - - # create a password manager - passwordManager = HTTPPasswordMgrWithDefaultRealm() - passwordManager.add_password(None, url, username, password) - - opener = build_opener(SmartRedirectHandler(), - DefaultErrorHandler(), - ContextualBasicAuthHandler(passwordManager)) - - #try: - # opener.open(request) - #except urllib2.HTTPError, e: - # if e.code is not 204: - # raise e - #return None - return opener.open(request) + return h.request(url, method='DELETE', headers=headers) def put(self, url, @@ -200,12 +107,12 @@ class RESTService(object): specified content type. """ - headers = None + headers = {} if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] - del(kwargs['headers']) - self.logger.debug('Headers passed in:%s' % headers) + del kwargs['headers'] + self.logger.debug('Headers passed in:' + headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: @@ -213,27 +120,12 @@ class RESTService(object): self.logger.debug('About to do a PUT on:' + url) - request = RESTRequest(url, payload, method='PUT') - - # set the content type header - request.add_header('Content-Type', contentType) - - # add a user-agent - request.add_header('User-Agent', self.user_agent) - if headers: - for k, v in headers.items(): - self.logger.debug('Adding header:%s:%s' % (k, v)) - request.add_header(k, v) - - # create a password manager - passwordManager = HTTPPasswordMgrWithDefaultRealm() - passwordManager.add_password(None, url, username, password) - - opener = build_opener(SmartRedirectHandler(), - DefaultErrorHandler(), - ContextualBasicAuthHandler(passwordManager)) - - return opener.open(request) + h = httplib2.Http() + h.add_credentials(username, password) + headers['User-Agent'] = self.user_agent + if contentType is not None: + headers['Content-Type'] = contentType + return h.request(url, body=payload, method='PUT', headers=headers) def post(self, url, @@ -249,12 +141,12 @@ class RESTService(object): specified content type. """ - headers = None + headers = {} if kwargs: if 'headers' in kwargs: headers = kwargs['headers'] - del(kwargs['headers']) - self.logger.debug('Headers passed in:%s' % headers) + del kwargs['headers'] + self.logger.debug('Headers passed in:' + headers) if url.find('?') >= 0: url = url + '&' + urlencode(kwargs) else: @@ -262,47 +154,9 @@ class RESTService(object): self.logger.debug('About to do a POST on:' + url) - request = RESTRequest(url, payload, method='POST') - - # set the content type header - request.add_header('Content-Type', contentType) - - # add a user-agent - request.add_header('User-Agent', self.user_agent) - if headers: - for k, v in headers.items(): - self.logger.debug('Adding header:%s:%s' % (k, v)) - request.add_header(k, v) - - # create a password manager - passwordManager = HTTPPasswordMgrWithDefaultRealm() - passwordManager.add_password(None, url, username, password) - - opener = build_opener(SmartRedirectHandler(), - DefaultErrorHandler(), - ContextualBasicAuthHandler(passwordManager)) - - try: - return opener.open(request) - except HTTPError, e: - if e.code is not 201: - return e - else: - return e.read() - - -class RESTRequest(Request): - - """ - Overrides urllib's request default behavior - """ - - def __init__(self, *args, **kwargs): - """ Constructor """ - self._method = kwargs.pop('method', 'GET') - assert self._method in ['GET', 'POST', 'PUT', 'DELETE'] - Request.__init__(self, *args, **kwargs) - - def get_method(self): - """ Override the get method """ - return self._method + h = httplib2.Http() + h.add_credentials(username, password) + headers['User-Agent'] = self.user_agent + if contentType is not None: + headers['Content-Type'] = contentType + return h.request(url, body=payload, method='POST', headers=headers) diff --git a/src/cmislib/util.py b/src/cmislib/util.py new file mode 100644 index 0000000..a38264c --- /dev/null +++ b/src/cmislib/util.py @@ -0,0 +1,164 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +""" +Module containing handy utility functions. +""" +import re +import iso8601 +import logging +import datetime +from cmislib.domain import CmisId + +moduleLogger = logging.getLogger('cmislib.util') + + +def multiple_replace(aDict, text): + + """ + Replace in 'text' all occurences of any key in the given + dictionary by its corresponding value. Returns the new string. + + See http://code.activestate.com/recipes/81330/ + """ + + # Create a regular expression from the dictionary keys + regex = re.compile("(%s)" % "|".join(map(re.escape, aDict.keys()))) + + # For each match, look-up corresponding value in dictionary + return regex.sub(lambda mo: aDict[mo.string[mo.start():mo.end()]], text) + + +def parsePropValue(value, nodeName): + + """ + Returns a properly-typed object based on the type as specified in the + node's element name. + """ + + moduleLogger.debug('Inside parsePropValue') + + if nodeName == 'propertyId': + return CmisId(value) + elif nodeName == 'propertyString': + return value + elif nodeName == 'propertyBoolean': + bDict = {'false': False, 'true': True} + return bDict[value.lower()] + elif nodeName == 'propertyInteger': + return int(value) + elif nodeName == 'propertyDecimal': + return float(value) + elif nodeName == 'propertyDateTime': + # %z doesn't seem to work, so I'm going to trunc the offset + # not all servers return microseconds, so those go too + return parseDateTimeValue(value) + else: + return value + + +def parsePropValueByType(value, typeName): + + """ + Returns a properly-typed object based on the type as specified in the + node's property definition. + """ + + moduleLogger.debug('Inside parsePropValueByType: %s: %s', typeName, value) + + if typeName == 'id': + if value: + return CmisId(value) + else: + return None + elif typeName == 'string': + return value + elif typeName == 'boolean': + if not value: + return False + if type(value) == bool: + return value + else: + bDict = {'false': False, 'true': True} + return bDict[value.lower()] + elif typeName == 'integer': + if value: + return int(value) + else: + return 0 + elif typeName == 'decimal': + if value: + # search result relevance is returning as an arrary of decimals + # in the browser binding for some reason + if isinstance(value, list): + return float(value[0]) + else: + return float(value) + else: + return 0.0 + elif typeName == 'datetime': + # %z doesn't seem to work, so I'm going to trunc the offset + # not all servers return microseconds, so those go too + return parseDateTimeValue(value) + else: + return value + + +def parseDateTimeValue(value): + + """ + Utility function to return a datetime from a string. + """ + if type(value) == str: + return iso8601.parse_date(value) + elif type(value) == int: + return datetime.datetime.fromtimestamp(value / 1000) + else: + return + + +def parseBoolValue(value): + + """ + Utility function to parse booleans and none from strings + """ + + if value == 'false': + return False + elif value == 'true': + return True + elif value == 'none': + return None + else: + return value + + +def toCMISValue(value): + + """ + Utility function to convert Python values to CMIS string values + """ + + if value is False: + return 'false' + elif value is True: + return 'true' + elif value is None: + return 'none' + else: + return value diff --git a/src/doc/src/.doctrees/about.doctree b/src/doc/src/.doctrees/about.doctree deleted file mode 100644 index a78eb64..0000000 Binary files a/src/doc/src/.doctrees/about.doctree and /dev/null differ diff --git a/src/doc/src/.doctrees/environment.pickle b/src/doc/src/.doctrees/environment.pickle deleted file mode 100644 index 661e3a4..0000000 Binary files a/src/doc/src/.doctrees/environment.pickle and /dev/null differ diff --git a/src/doc/src/.doctrees/index.doctree b/src/doc/src/.doctrees/index.doctree deleted file mode 100644 index 35f3e39..0000000 Binary files a/src/doc/src/.doctrees/index.doctree and /dev/null differ diff --git a/src/doc/src/Makefile b/src/doc/src/Makefile deleted file mode 100644 index 0139530..0000000 --- a/src/doc/src/Makefile +++ /dev/null @@ -1,174 +0,0 @@ -# -# 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. -# - -# -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = ../build -SOURCEDIR = . - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MyTestProject.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MyTestProject.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/MyTestProject" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MyTestProject" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/src/doc/src/about.rst b/src/doc/src/about.rst deleted file mode 100644 index 00697ad..0000000 --- a/src/doc/src/about.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. - 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. - -About Apache Chemistry cmislib -============================== -The goal of this project is to create a CMIS client for Python that can be used to work with any CMIS-compliant repository. - -The library is being developed with the following guidelines: - * Developers using this API should be able to work with CMIS domain objects without having to worry about the underlying implementation details. - * The library will use the Resftul AtomPub Binding. - * The library will conform to the `CMIS spec `_ as closely as possible. Several public CMIS repositories are being used to test the API. - * The library should have no hard-coded URL's. It should be able to get everything it needs regarding how to work with the CMIS service from the CMIS service URL response and subsequent calls. - * There shouldn't have to be a vendor-specific version of this library. The goal is for it to be interoperable with CMIS-compliant providers. - -Quick Example -------------- -This should give you an idea of how easy and natural it is to work with the API: - >>> cmisClient = cmislib.CmisClient('http://localhost:8080/alfresco/cmisatom', 'admin', 'admin') - >>> repo = cmisClient.defaultRepository - >>> rootFolder = repo.rootFolder - >>> children = rootFolder.getChildren() - >>> newFolder = rootFolder.createFolder('testDeleteFolder folder') - >>> props = newFolder.properties - >>> newFolder.delete() - -To-Do's -------- -Miscellaneous - * createDocumentFromSource - * getProperties filter - * getContentStream stream id - -Unfiling/multifiling support - * createDocument without a parent folder (unfiled) - - * The spec does not yet support this. Although the spec does say that a folder ID is optional, it does not specify which URL to post the unfiled document to. - -Policies - * Policy object - * createPolicy - * applyPolicy - * removePolicy - * getAppliedPolicies diff --git a/src/doc/src/code.rst b/src/doc/src/code.rst deleted file mode 100644 index 04db5b6..0000000 --- a/src/doc/src/code.rst +++ /dev/null @@ -1,62 +0,0 @@ -.. - 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. - -Code -==== - -The :mod:`cmislib.model` Module -------------------------------- - -The :mod:`cmislib.model` Module contains all the CMIS domain objects. The ones you will work with are listed as top level package elements. When working with the repository, the first thing you need to do is grab an instance of :class:`cmislib.CmisClient`, passing it the repository endpoint URL, username, and password. - -For example, in Alfresco 4 and higher, the repository endpoint is -'http://localhost:8080/alfresco/cmisatom'. In earlier versions of -Alfresco, the endpoint is -'http://localhost:8080/alfresco/s/api/cmis'. In both cases, the -default username and password are 'admin' and 'admin'. - ->>> cmisClient = cmislib.CmisClient('http://localhost:8080/alfresco/s/cmis', 'admin', 'admin') - -From there you can get the default repository... - ->>> repo = cmisClient.defaultRepository - -or a specific repository if you know the repository ID. - ->>> repo = cmisClient.getRepository('83beb297-a6fa-4ac5-844b-98c871c0eea9') - -Once you have that, you're off to the races. Use the :class:`cmislib.Repository` class to create new :class:`cmislib.Folder` and :class:`cmislib.Document` objects, perform searches, etc. - -.. automodule:: cmislib.model - :members: - -The :mod:`cmislib.net` Module ------------------------------ - -The :mod:`cmislib.net` Module contains the classes used by :mod:`cmislib.model.CmisClient` to communicate with the CMIS repository. The most important of which is :class:`cmislib.net.RESTService`. - -.. automodule:: cmislib.net - :members: RESTService - -The :mod:`tests` Module -------------------------------- - -The :mod:`tests` Module contains unit tests for all classes and methods in :mod:`cmislib.model`. See :ref:`tests` for more information on running tests. - -.. automodule:: tests - :members: diff --git a/src/doc/src/conf.py b/src/doc/src/conf.py deleted file mode 100644 index ea6d54e..0000000 --- a/src/doc/src/conf.py +++ /dev/null @@ -1,303 +0,0 @@ -# -*- 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. -# -# CMIS Library documentation build configuration file, created by -# sphinx-quickstart on Fri Dec 14 16:13:15 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) -sys.path.append(os.path.abspath('../..')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Apache Chemistry cmislib' -copyright = u'2013, Apache Software Foundation' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.5' -# The full version, including alpha/beta/rc tags. -release = '0.5.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'cmislibdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'cmislib.tex', u'Apache Chemistry cmislib Documentation', - u'Jeff Potts', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'cmislib', u'Apache Chemistry cmislib Documentation', - [u'Jeff Potts'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'cmislib', u'Apache Chemistry cmislib Documentation', - u'Jeff Potts', 'cmislib', 'Python client library for CMIS', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'Apache Chemistry cmislib Documentation' -epub_author = u'Jeff Potts' -epub_publisher = u'Jeff Potts' -epub_copyright = u'2013, Apache Software Foundation' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True diff --git a/src/doc/src/devguide.rst b/src/doc/src/devguide.rst deleted file mode 100644 index b959e7f..0000000 --- a/src/doc/src/devguide.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. - 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. - -=============== -Developer Guide -=============== - -This page is for people who wish to contribute code to this project. - -Developer Setup ---------------- -Check out the source from head, switch to the source code's root directory, then run: - python setup.py develop - -That will set up this project's src directory in the easy-install.pth file in site-packages. - -Release Process ---------------- - -Checklist: - #. All newly-added code has a unit test - #. All tests pass cleanly (or have good reasons for not passing) - #. Change setup.cfg to have the appropriate tag ('dev', for example, or '' for a stable release) - #. Change setup.py to have the appropriate version number - #. Inline comments updated with changes - #. Sphinx doc updated with changes - #. Docs build cleanly - .. code-block:: bash - - cd src/doc/src/ - make html - - #. pep8 runs without much complaint - .. code-block:: bash - - pep8 --ignore=E501,W601 --repeat model.py - - #. pylint runs without much complaint - .. code-block:: bash - - pylint --disable=C0103,R0904,R0913,C0301,W0511 cmislibtest.py - - #. All changes checked in - #. Tag the release using 'cmislib-[release num]-RC[x]' - #. Use the release script to build the release artifacts - .. code-block:: bash - - cd dist - ./release.sh -u jpotts@apache.org - - This will do a 'setup.py bdist sdist' and will then sign all artifacts. - - Note that the artifacts will be named without 'RC[x]'. These are the same artifacts that will be distributed if the vote passes. - - #. Copy files to the Apache server under ~/public_html/chemistry/cmislib/[release num] - #. Start vote. Send an email to dev@chemistry.apache.org announcing the vote, highlighting the changes, pointing to the tagged source, and referencing the artifacts that have been copied to the Apache server. - #. After 72 hours, if the vote passes, continue, otherwise address issues and start over - #. Copy the files to the appropriate Apache dist directory, which is /www/www.apache.org/dist/chemistry/cmislib/[release num] - #. Rename the RC tag in source code control - #. Update the cmislib home page with download links to the new release - #. Upload files to Pypi - #. Check the `cheesecake `_ score - .. code-block:: bash - - python cheesecake_index --name=cmislib - diff --git a/src/doc/src/docs.rst b/src/doc/src/docs.rst deleted file mode 100644 index 340bfdb..0000000 --- a/src/doc/src/docs.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. - 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. - -Documentation -============= - -This documentation was generated with `Sphinx `_. To install Sphinx on Mac OS X using Macports: - -MAC OS X:: - - sudo port install py26-sphinx - -Once you've got Sphinx installed, if you need to regenerate the documentation:: - - cd /path/to/cmislib/src/doc/src - Run either: - sphinx-build -b html -d ../build/.doctrees . ../build - make html - -The generated HTML will be placed in doc/build:: - - firefox file:///path/to/cmislib/src/doc/build/index.html diff --git a/src/doc/src/examples.rst b/src/doc/src/examples.rst deleted file mode 100644 index fd6bb79..0000000 --- a/src/doc/src/examples.rst +++ /dev/null @@ -1,137 +0,0 @@ -.. - 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. - -.. _examples: - -======== -Examples -======== -There's nothing in cmislib that is specific to any particular vendor. Once you give it your CMIS provider's service URL and some credentials, it figures out where to go from there. - -Let's look at some examples using Alfresco's public CMIS repository. - ------------------------ -Get a Repository object ------------------------ - - #. From the command-line, start the Python shell by typing `python` then hit enter. - #. Import the CmisClient: - - >>> from cmislib import CmisClient - - #. Point the CmisClient at the repository's service URL - - >>> client = CmisClient('http://cmis.alfresco.com/cmisatom', 'admin', 'admin') - - #. Get the default repository for the service - - >>> repo = client.defaultRepository - >>> repo.id - u'83beb297-a6fa-4ac5-844b-98c871c0eea9' - - #. Get the repository's properties. This for-loop spits out everything cmislib knows about the repo. - - >>> repo.name - u'Main Repository' - >>> info = repo.info - >>> 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) - -------------------- -Folders & Documents -------------------- - -Once you've got the Repository object you can start working with folders. - - #. Create a new folder in the root. You should name yours something unique. - - >>> root = repo.rootFolder - >>> someFolder = root.createFolder('someFolder') - >>> someFolder.id - u'workspace://SpacesStore/91f344ef-84e7-43d8-b379-959c0be7e8fc' - - #. Then, you can create some content: - - >>> someFile = open('test.txt', 'r') - >>> someDoc = someFolder.createDocument('Test Document', contentFile=someFile) - - #. And, if you want, you can dump the properties of the newly-created document (this is a partial list): - - >>> props = someDoc.properties - >>> for k,v in props.items(): - ... print '%s:%s' % (k,v) - ... - cmis:contentStreamMimeType:text/plain - cmis:creationDate:2009-12-18T10:59:26.667-06:00 - cmis:baseTypeId:cmis:document - cmis:isLatestMajorVersion:false - cmis:isImmutable:false - cmis:isMajorVersion:false - cmis:objectId:workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479 - ----------------------------------- -Searching For & Retrieving Objects ----------------------------------- - -There are several different ways to grab an object: - * You can run a CMIS query - * You can ask the repository to give you one for a specific path or object ID - * You can traverse the repository using a folder's children and/or descendants - - #. Let's find the doc we just created with a full-text search. - - .. note:: - Note that I'm currently seeing a problem with Alfresco in which the CMIS service returns one less result than what's really there): - - >>> results = repo.query("select * from cmis:document where contains('test')") - >>> for result in results: - ... print result.name - ... - Test Document2 - example test script.js - - #. Alternatively, you can also get objects by their path, like this: - - >>> someDoc = repo.getObjectByPath('/someFolder/Test Document') - >>> someDoc.id - u'workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479' - - #. Or their object ID, like this: - - >>> someDoc = repo.getObject('workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479') - >>> someDoc.name - u'Test Document' - - #. Folder objects have getChildren() and getDescendants() methods that will return a list of :class:`CmisObject` objects: - - >>> children= someFolder.getChildren() - >>> for child in children: - ... print child.name - ... - Test Document - Test Document2 diff --git a/src/doc/src/index.rst b/src/doc/src/index.rst deleted file mode 100644 index 4656ebc..0000000 --- a/src/doc/src/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. - 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. - -.. cmislib documentation master file, created by - sphinx-quickstart on Thu Dec 10 10:12:43 2009. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to the Apache Chemistry cmislib documentation! -================================================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - about.rst - install.rst - examples.rst - code.rst - devguide.rst - tests.rst - docs.rst - sample-data.rst - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/src/doc/src/install.rst b/src/doc/src/install.rst deleted file mode 100644 index d535d7b..0000000 --- a/src/doc/src/install.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. - 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. - -Installation -============ - -Requirements ------------- -These requirements must be met: - - Python 2.6.x - - CMIS provider compliant with CMIS 1.0 - -Steps ------ - #. If you don't have `Python `_ installed already, do so. - #. If you don't have `setuptools `_ installed already, do so. - #. Once setuptools is installed, type `easy_install cmislib` - #. That's it! - -Once you do that, you should be able to fire up Python on the command-line and import cmislib successfully. - - >>> from cmislib import CmisClient, Repository, Folder - -To validate everything is working, run some :ref:`tests` or walk through some :ref:`examples`. diff --git a/src/doc/src/make.bat b/src/doc/src/make.bat deleted file mode 100644 index 323d373..0000000 --- a/src/doc/src/make.bat +++ /dev/null @@ -1,132 +0,0 @@ -@ECHO OFF - -REM -REM Licensed to the Apache Software Foundation (ASF) under one -REM or more contributor license agreements. See the NOTICE file -REM distributed with this work for additional information -REM regarding copyright ownership. The ASF licenses this file -REM to you under the Apache License, Version 2.0 (the -REM "License"); you may not use this file except in compliance -REM with the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, -REM software distributed under the License is distributed on an -REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -REM KIND, either express or implied. See the License for the -REM specific language governing permissions and limitations -REM under the License. -REM - -REM Command file for Sphinx documentation - -set SPHINXBUILD=sphinx-build -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\CMISLibrary.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\CMISLibrary.ghc - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/src/doc/src/run-sphinx.py b/src/doc/src/run-sphinx.py deleted file mode 100755 index 619439a..0000000 --- a/src/doc/src/run-sphinx.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python -# -# 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. -# -import os -import sys -os.environ['SPHINXBUILD']="sphinx-build" -build_dir = sys.path[0] + "/../build" -os.environ['BUILDDIR']=build_dir -os.environ['SOURCEDIR']=sys.path[0] -# force a clean every time -print "Removing build dir: %s" % build_dir -os.system("rm -rf " + build_dir) -os.system("make -e --makefile=" + sys.path[0] + "/Makefile html") -zip_file = sys.path[0] + "/../docs.zip" -os.system("rm " + zip_file) -os.chdir(build_dir) -os.system("zip -r " + zip_file + " *") diff --git a/src/doc/src/sample-data.rst b/src/doc/src/sample-data.rst deleted file mode 100644 index 6a345d7..0000000 --- a/src/doc/src/sample-data.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. - 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. - -Sample Data -=========== - -The /path/to/cmislib/src/data directory contains some sample XML responses from a CMIS service. These are for sample and development purposes and can safely be ignored if you are an end-user of the library. - -In some cases there are two files for the same response. For example, 'types.xml' came from Alfresco while 'types.chemistry.xml' came from the simple Apache Chemistry test server. diff --git a/src/doc/src/tests.rst b/src/doc/src/tests.rst deleted file mode 100644 index da1431a..0000000 --- a/src/doc/src/tests.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. - 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. - -.. _tests: - -===== -Tests -===== - -This code includes unit tests. To run the tests:: - - cd /path/to/cmislib/tests - Edit settings.py - Set REPOSITORY_URL, USERNAME, PASSWORD - Optionally, set TEST_ROOT_PATH and other settings to meet your needs - python cmislibtest.py - -.. note:: - http://cmis.alfresco.com is a freely-available, hosted CMIS service. If you want to use that for testing, the URL is http://cmis.alfresco.com/s/cmis and the username and password are admin/admin. See the wiki for other known CMIS test servers. - -If everything goes well, you should see:: - - Ran X tests in 3.607s - - OK - -.. note:: - Depending on the implementation of the CMIS provider, you may see errors or failures instead of 'OK'. diff --git a/src/tests/250px-Cmis_logo.png b/src/tests/250px-Cmis_logo.png deleted file mode 100644 index 20006a3..0000000 Binary files a/src/tests/250px-Cmis_logo.png and /dev/null differ diff --git a/src/tests/__init__.py b/src/tests/__init__.py deleted file mode 100644 index 2e04fa4..0000000 --- a/src/tests/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -# 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. -# -from cmislibtest import * diff --git a/src/tests/cmislibtest.py b/src/tests/cmislibtest.py deleted file mode 100644 index a45e5fb..0000000 --- a/src/tests/cmislibtest.py +++ /dev/null @@ -1,1479 +0,0 @@ -# -*- 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. -# - -''' -Unit tests for cmislib -''' -import unittest -from unittest import TestSuite, TestLoader -from cmislib.model import CmisClient, ACE -from cmislib.exceptions import \ - ObjectNotFoundException, \ - CmisException, \ - NotSupportedException -from cmislib import messages -import os -from time import sleep, time -import settings - -## Fix test file paths in case test is launched using nosetests -my_dir = os.path.dirname(os.path.abspath(__file__)) -try: - os.stat(settings.TEST_BINARY_1) -except: - settings.TEST_BINARY_1 = os.path.join(my_dir, settings.TEST_BINARY_1) -try: - os.stat(settings.TEST_BINARY_2) -except: - settings.TEST_BINARY_2 = os.path.join(my_dir, settings.TEST_BINARY_2) - - -class CmisTestBase(unittest.TestCase): - - """ Common ancestor class for most cmislib unit test classes. """ - - def setUp(self): - """ Create a root test folder for the test. """ - self._cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - self._repo = self._cmisClient.getDefaultRepository() - self._rootFolder = self._repo.getObjectByPath(settings.TEST_ROOT_PATH) - self._folderName = " ".join(['cmislib', self.__class__.__name__, str(time())]) - self._testFolder = self._rootFolder.createFolder(self._folderName) - - def tearDown(self): - """ Clean up after the test. """ - try: - self._testFolder.deleteTree() - except NotSupportedException: - print "Couldn't delete test folder because deleteTree is not supported" - - -class CmisClientTest(unittest.TestCase): - - """ Tests for the :class:`CmisClient` class. """ - - def testCmisClient(self): - '''Instantiate a CmisClient object''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - self.assert_(cmisClient != None) - - def testGetRepositories(self): - '''Call getRepositories and make sure at least one comes back with - an ID and a name - ''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repoInfo = cmisClient.getRepositories() - self.assert_(len(repoInfo) >= 1) - self.assert_('repositoryId' in repoInfo[0]) - self.assert_('repositoryName' in repoInfo[0]) - - def testDefaultRepository(self): - '''Get the default repository by calling the repo's service URL''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - self.assert_(repo != None) - self.assert_(repo.getRepositoryId() != None) - - def testGetRepository(self): - '''Get a repository by repository ID''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - defaultRepoId = repo.getRepositoryId() - defaultRepoName = repo.getRepositoryName() - repo = cmisClient.getRepository(defaultRepoId) - self.assertEquals(defaultRepoId, repo.getRepositoryId()) - self.assertEquals(defaultRepoName, repo.getRepositoryName()) - - # Error conditions - def testCmisClientBadUrl(self): - '''Try to instantiate a CmisClient object with a known bad URL''' - cmisClient = CmisClient(settings.REPOSITORY_URL + 'foobar', settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - self.assertRaises(CmisException, cmisClient.getRepositories) - - def testGetRepositoryBadId(self): - '''Try to get a repository with a bad repo ID''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - self.assertRaises(ObjectNotFoundException, - cmisClient.getRepository, - '123FOO') - - -class QueryTest(CmisTestBase): - - """ Tests related to running CMIS queries. """ - - # TODO: Test the rest of these queries - # queryDateRange = "SELECT cmis:name from cmis:document " \ - # "where cmis:creationDate >= TIMESTAMP'2009-11-10T00:00:00.000-06:00' and " \ - # "cmis:creationDate < TIMESTAMP'2009-11-18T00:00:00.000-06:00'" - # queryFolderFullText = "SELECT cmis:name from cmis:document " \ - # "where in_folder('workspace://SpacesStore/3935ce21-9f6f-4d46-9e22-4f97e1d5d9d8') " \ - # "and contains('contract')" - # queryCombined = "SELECT cmis:name from cmis:document " \ - # "where in_tree('workspace://SpacesStore/3935ce21-9f6f-4d46-9e22-4f97e1d5d9d8') and " \ - # "contains('contract') and cm:description like \"%sign%\"" - - def setUp(self): - """ - Override the base setUp to include creating a couple - of test docs. - """ - CmisTestBase.setUp(self) - # I think this may be an Alfresco bug. The CMIS query results contain - # 1 less entry element than the number of search results. So this test - # will create two documents and search for the second one which should - # work in all repositories. - testFile = open(settings.TEST_BINARY_2, 'rb') - self._testContent = self._testFolder.createDocument(testFile.name, contentFile=testFile) - testFile.close() - testFile = open(settings.TEST_BINARY_2, 'rb') - self._testContent2 = self._testFolder.createDocument(settings.TEST_BINARY_2.replace('.', '2.'), contentFile=testFile) - testFile.close() - self._maxFullTextTries = settings.MAX_FULL_TEXT_TRIES - - def testSimpleSelect(self): - '''Execute simple select star from cmis:document''' - querySimpleSelect = "SELECT * FROM cmis:document" - resultSet = self._repo.query(querySimpleSelect) - self.assertTrue(isInResultSet(resultSet, self._testContent)) - - def testWildcardPropertyMatch(self): - '''Find content w/wildcard match on cmis:name property''' - name = self._testContent.getProperties()['cmis:name'] - querySimpleSelect = "SELECT * FROM cmis:document where cmis:name like '" + name[:7] + "%'" - resultSet = self._repo.query(querySimpleSelect) - self.assertTrue(isInResultSet(resultSet, self._testContent)) - - def testPropertyMatch(self): - '''Find content matching cmis:name property''' - name = self._testContent2.getProperties()['cmis:name'] - querySimpleSelect = "SELECT * FROM cmis:document where cmis:name = '" + name + "'" - resultSet = self._repo.query(querySimpleSelect) - self.assertTrue(isInResultSet(resultSet, self._testContent2)) - - def testFullText(self): - '''Find content using a full-text query''' - queryFullText = "SELECT cmis:objectId, cmis:name FROM cmis:document " \ - "WHERE contains('whitepaper')" - # on the first full text search the indexer may need a chance to - # do its thing - found = False - maxTries = self._maxFullTextTries - while not found and (maxTries > 0): - resultSet = self._repo.query(queryFullText) - found = isInResultSet(resultSet, self._testContent2) - if not found: - maxTries -= 1 - print 'Not found...sleeping for 10 secs. Remaining tries:%d' % maxTries - sleep(settings.FULL_TEXT_WAIT) - self.assertTrue(found) - - def testScore(self): - '''Find content using FT, sorted by relevance score''' - queryScore = "SELECT cmis:objectId, cmis:name, Score() as relevance " \ - "FROM cmis:document WHERE contains('sample') " \ - "order by relevance DESC" - - # on the first full text search the indexer may need a chance to - # do its thing - found = False - maxTries = self._maxFullTextTries - while not found and (maxTries > 0): - resultSet = self._repo.query(queryScore) - found = isInResultSet(resultSet, self._testContent2) - if not found: - maxTries -= 1 - print 'Not found...sleeping for 10 secs. Remaining tries:%d' % maxTries - sleep(10) - self.assertTrue(found) - - -class RepositoryTest(CmisTestBase): - - """ Tests for the :class:`Repository` class. """ - - def testRepositoryInfo(self): - '''Retrieve repository info''' - repoInfo = self._repo.getRepositoryInfo() - self.assertTrue('repositoryId' in repoInfo) - self.assertTrue('repositoryName' in repoInfo) - self.assertTrue('repositoryDescription' in repoInfo) - self.assertTrue('vendorName' in repoInfo) - self.assertTrue('productName' in repoInfo) - self.assertTrue('productVersion' in repoInfo) - self.assertTrue('rootFolderId' in repoInfo) - self.assertTrue('cmisVersionSupported' in repoInfo) - - def testRepositoryCapabilities(self): - '''Retrieve repository capabilities''' - caps = self._repo.getCapabilities() - self.assertTrue('ACL' in caps) - self.assertTrue('AllVersionsSearchable' in caps) - self.assertTrue('Changes' in caps) - self.assertTrue('ContentStreamUpdatability' in caps) - self.assertTrue('GetDescendants' in caps) - self.assertTrue('GetFolderTree' in caps) - self.assertTrue('Multifiling' in caps) - self.assertTrue('PWCSearchable' in caps) - self.assertTrue('PWCUpdatable' in caps) - self.assertTrue('Query' in caps) - self.assertTrue('Renditions' in caps) - self.assertTrue('Unfiling' in caps) - self.assertTrue('VersionSpecificFiling' in caps) - self.assertTrue('Join' in caps) - - def testGetRootFolder(self): - '''Get the root folder of the repository''' - rootFolder = self._repo.getRootFolder() - self.assert_(rootFolder != None) - self.assert_(rootFolder.getObjectId() != None) - - def testCreateFolder(self): - '''Create a new folder in the root folder''' - folderName = 'testCreateFolder folder' - newFolder = self._repo.createFolder(self._rootFolder, folderName) - self.assertEquals(folderName, newFolder.getName()) - newFolder.delete() - - def testCreateDocument(self): - '''Create a new 'content-less' document''' - documentName = 'testDocument' - newDoc = self._repo.createDocument(documentName, parentFolder=self._testFolder) - self.assertEquals(documentName, newDoc.getName()) - - def testCreateDocumentFromString(self): - '''Create a new document from a string''' - documentName = 'testDocument' - contentString = 'Test content string' - newDoc = self._repo.createDocumentFromString(documentName, - parentFolder=self._testFolder, - contentString=contentString, - contentType='text/plain') - self.assertEquals(documentName, newDoc.getName()) - self.assertEquals(newDoc.getContentStream().read(), contentString) - - # CMIS-279 - def testCreateDocumentUnicode(self): - '''Create a new doc with unicode characters in the name''' - documentName = u'abc cdeöäüß%§-_caféè.txt' - newDoc = self._repo.createDocument(documentName, parentFolder=self._testFolder) - self.assertEquals(documentName, newDoc.getName()) - - def testGetObject(self): - '''Create a test folder then attempt to retrieve it as a - :class:`CmisObject` object using its object ID''' - folderName = 'testGetObject folder' - newFolder = self._repo.createFolder(self._testFolder, folderName) - objectId = newFolder.getObjectId() - someObject = self._repo.getObject(objectId) - self.assertEquals(folderName, someObject.getName()) - newFolder.delete() - - def testReturnVersion(self): - '''Get latest and latestmajor versions of an object''' - f = open(settings.TEST_BINARY_1, 'rb') - doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) - doc10Id = doc10.getObjectId() - if (not doc10.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc10.checkout() - doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 - if (not doc11.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc11.checkout() - doc20 = pwc.checkin() # checkin a major version, 2.0 - doc20Id = doc20.getObjectId() - if (not doc20.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc20.checkout() - doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 - doc21Id = doc21.getObjectId() - - docLatest = self._repo.getObject(doc10Id, returnVersion='latest') - self.assertEquals(doc21Id, docLatest.getObjectId()) - - docLatestMajor = self._repo.getObject(doc10Id, returnVersion='latestmajor') - self.assertEquals(doc20Id, docLatestMajor.getObjectId()) - - def testGetFolder(self): - '''Create a test folder then attempt to retrieve the Folder object - using its object ID''' - folderName = 'testGetFolder folder' - newFolder = self._repo.createFolder(self._testFolder, folderName) - objectId = newFolder.getObjectId() - someFolder = self._repo.getFolder(objectId) - self.assertEquals(folderName, someFolder.getName()) - newFolder.delete() - - def testGetObjectByPath(self): - '''Create test objects (one folder, one document) then try to get - them by path''' - # names of folders and test docs - parentFolderName = 'testGetObjectByPath folder' - subFolderName = 'subfolder' - docName = 'testdoc' - - # create the folder structure - parentFolder = self._testFolder.createFolder(parentFolderName) - subFolder = parentFolder.createFolder(subFolderName) - # use the subfolder path to get the folder by path - subFolderPath = subFolder.getProperties().get("cmis:path") - searchFolder = self._repo.getObjectByPath(subFolderPath) - self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) - - # create a test doc - doc = subFolder.createDocument(docName) - # ask the doc for its paths - searchDocPaths = doc.getPaths() - # for each path in the list, try to get the object by path - # this is better than building a path with the doc's name b/c the name - # isn't guaranteed to be used as the path segment (see CMIS-232) - for path in searchDocPaths: - searchDoc = self._repo.getObjectByPath(path) - self.assertEquals(doc.getObjectId(), searchDoc.getObjectId()) - - # get the subfolder by path, then ask for its children - subFolder = self._repo.getObjectByPath(subFolderPath) - self.assertEquals(len(subFolder.getChildren().getResults()), 1) - - def testGetUnfiledDocs(self): - '''Tests the repository's unfiled collection''' - - if not self._repo.getCapabilities()['Unfiling']: - print 'Repo does not support unfiling, skipping' - return - - # create a test folder and test doc - testFolder = self._testFolder.createFolder('unfile test') - newDoc = testFolder.createDocument('testdoc') - - # make sure the new doc isn't in the unfiled collection - try: - rs = self._repo.getUnfiledDocs() - self.assertFalse(isInResultSet(rs, newDoc)) - except NotSupportedException: - print 'This repository does not support read access to the unfiled collection...skipping' - return - - # delete the test folder and tell it to unfile the testdoc - objId = newDoc.getObjectId() - testFolder.deleteTree(unfileObjects='unfile') - - # grab the document by object ID - newDoc = self._repo.getObject(objId) - - # the doc should now be in the unfiled collection - self.assertTrue(isInResultSet(self._repo.getUnfiledDocs(), newDoc)) - self.assertEquals('testdoc', newDoc.getTitle()) - -# def testCreateUnfiledDocument(self): -# '''Create a new unfiled document''' -# if self._repo.getCapabilities()['Unfiling'] != True: -# print 'Repo does not support unfiling, skipping' -# return -# documentName = 'testDocument' -# newDoc = self._repo.createDocument(documentName) -# self.assertEquals(documentName, newDoc.getName()) - - def testMoveDocument(self): - '''Move a Document from one folder to another folder''' - subFolder1 = self._testFolder.createFolder('sub1') - doc = subFolder1.createDocument('testdoc1') - self.assertEquals(len(subFolder1.getChildren()), 1) - subFolder2 = self._testFolder.createFolder('sub2') - self.assertEquals(len(subFolder2.getChildren()), 0) - doc.move(subFolder1, subFolder2) - self.assertEquals(len(subFolder1.getChildren()), 0) - self.assertEquals(len(subFolder2.getChildren()), 1) - self.assertEquals(doc.name, subFolder2.getChildren()[0].name) - - #Exceptions - - def testGetObjectBadId(self): - '''Attempt to get an object using a known bad ID''' - # this object ID is implementation specific (Alfresco) but is universally - # bad so it should work for all repositories - self.assertRaises(ObjectNotFoundException, - self._repo.getObject, - self._testFolder.getObjectId()[:-5] + 'BADID') - - def testGetObjectBadPath(self): - '''Attempt to get an object using a known bad path''' - self.assertRaises(ObjectNotFoundException, - self._repo.getObjectByPath, - '/123foo/BAR.jtp') - - -class FolderTest(CmisTestBase): - - """ Tests for the :class:`Folder` class """ - - def testGetChildren(self): - '''Get the children of the test folder''' - childFolderName1 = 'testchild1' - childFolderName2 = 'testchild2' - grandChildFolderName = 'testgrandchild' - childFolder1 = self._testFolder.createFolder(childFolderName1) - childFolder2 = self._testFolder.createFolder(childFolderName2) - grandChild = childFolder2.createFolder(grandChildFolderName) - resultSet = self._testFolder.getChildren() - self.assert_(resultSet != None) - self.assertEquals(2, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertFalse(isInResultSet(resultSet, grandChild)) - - def testGetDescendants(self): - '''Get the descendants of the root folder''' - childFolderName1 = 'testchild1' - childFolderName2 = 'testchild2' - grandChildFolderName1 = 'testgrandchild' - childFolder1 = self._testFolder.createFolder(childFolderName1) - childFolder2 = self._testFolder.createFolder(childFolderName2) - grandChild = childFolder1.createFolder(grandChildFolderName1) - - # test getting descendants with depth=1 - resultSet = self._testFolder.getDescendants(depth=1) - self.assert_(resultSet != None) - self.assertEquals(2, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertFalse(isInResultSet(resultSet, grandChild)) - - # test getting descendants with depth=2 - resultSet = self._testFolder.getDescendants(depth=2) - self.assert_(resultSet != None) - self.assertEquals(3, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertTrue(isInResultSet(resultSet, grandChild)) - - # test getting descendants with depth=-1 - resultSet = self._testFolder.getDescendants() # -1 is the default depth - self.assert_(resultSet != None) - self.assertEquals(3, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertTrue(isInResultSet(resultSet, grandChild)) - - def testGetTree(self): - '''Get the folder tree of the test folder''' - childFolderName1 = 'testchild1' - childFolderName2 = 'testchild2' - grandChildFolderName1 = 'testgrandchild' - childFolder1 = self._testFolder.createFolder(childFolderName1) - childFolder1.createDocument('testdoc1') - childFolder2 = self._testFolder.createFolder(childFolderName2) - childFolder2.createDocument('testdoc2') - grandChild = childFolder1.createFolder(grandChildFolderName1) - grandChild.createDocument('testdoc3') - - # test getting tree with depth=1 - resultSet = self._testFolder.getTree(depth=1) - self.assert_(resultSet != None) - self.assertEquals(2, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertFalse(isInResultSet(resultSet, grandChild)) - - # test getting tree with depth=2 - resultSet = self._testFolder.getTree(depth=2) - self.assert_(resultSet != None) - self.assertEquals(3, len(resultSet.getResults())) - self.assertTrue(isInResultSet(resultSet, childFolder1)) - self.assertTrue(isInResultSet(resultSet, childFolder2)) - self.assertTrue(isInResultSet(resultSet, grandChild)) - - def testDeleteEmptyFolder(self): - '''Create a test folder, then delete it''' - folderName = 'testDeleteEmptyFolder folder' - testFolder = self._testFolder.createFolder(folderName) - self.assertEquals(folderName, testFolder.getName()) - newFolder = testFolder.createFolder('testFolder') - testFolderChildren = testFolder.getChildren() - self.assertEquals(1, len(testFolderChildren.getResults())) - newFolder.delete() - testFolderChildren = testFolder.getChildren() - self.assertEquals(0, len(testFolderChildren.getResults())) - - def testDeleteNonEmptyFolder(self): - '''Create a test folder with something in it, then delete it''' - folderName = 'testDeleteNonEmptyFolder folder' - testFolder = self._testFolder.createFolder(folderName) - self.assertEquals(folderName, testFolder.getName()) - newFolder = testFolder.createFolder('testFolder') - testFolderChildren = testFolder.getChildren() - self.assertEquals(1, len(testFolderChildren.getResults())) - newFolder.createDocument('testDoc') - self.assertEquals(1, len(newFolder.getChildren().getResults())) - newFolder.deleteTree() - testFolderChildren = testFolder.getChildren() - self.assertEquals(0, len(testFolderChildren.getResults())) - - def testGetProperties(self): - '''Get the root folder, then get its properties''' - props = self._testFolder.getProperties() - self.assert_(props != None) - self.assert_('cmis:objectId' in props) - self.assert_(props['cmis:objectId'] != None) - self.assert_('cmis:objectTypeId' in props) - self.assert_(props['cmis:objectTypeId'] != None) - self.assert_('cmis:name' in props) - self.assert_(props['cmis:name'] != None) - - def testPropertyFilter(self): - '''Test the properties filter''' - # names of folders and test docs - parentFolderName = 'testGetObjectByPath folder' - subFolderName = 'subfolder' - - # create the folder structure - parentFolder = self._testFolder.createFolder(parentFolderName) - subFolder = parentFolder.createFolder(subFolderName) - subFolderPath = subFolder.getProperties().get("cmis:path") - - # Per CMIS-170, CMIS providers are not required to filter the - # properties returned. So these tests will check only for the presence - # of the properties asked for, not the absence of properties that - # should be filtered if the server chooses to do so. - - # test when used with getObjectByPath - searchFolder = self._repo.getObjectByPath(subFolderPath, - filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') - self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) - - # test when used with getObjectByPath + reload - searchFolder = self._repo.getObjectByPath(subFolderPath, - filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') - searchFolder.reload() - self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) - - # test when used with getObject - searchFolder = self._repo.getObject(subFolder.getObjectId(), - filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') - self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) - - # test when used with getObject + reload - searchFolder = self._repo.getObject(subFolder.getObjectId(), - filter='cmis:objectId,cmis:objectTypeId,cmis:baseTypeId') - searchFolder.reload() - self.assertEquals(subFolder.getObjectId(), searchFolder.getObjectId()) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) - - # test that you can do a reload with a reset filter - searchFolder.reload(filter='*') - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:objectTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:baseTypeId')) - self.assertTrue(searchFolder.getProperties().has_key('cmis:name')) - - def testUpdateProperties(self): - '''Create a test folder, then update its properties''' - folderName = 'testUpdateProperties folder' - newFolder = self._testFolder.createFolder(folderName) - self.assertEquals(folderName, newFolder.getName()) - folderName2 = 'testUpdateProperties folder2' - props = {'cmis:name': folderName2} - newFolder.updateProperties(props) - self.assertEquals(folderName2, newFolder.getName()) - - def testSubFolder(self): - '''Create a test folder, then create a test folder within that.''' - parentFolder = self._testFolder.createFolder('testSubFolder folder') - self.assert_('cmis:objectId' in parentFolder.getProperties()) - childFolder = parentFolder.createFolder('child folder') - self.assert_('cmis:objectId' in childFolder.getProperties()) - self.assert_(childFolder.getProperties()['cmis:objectId'] != None) - - def testAllowableActions(self): - '''Create a test folder, then get its allowable actions''' - actions = self._testFolder.getAllowableActions() - self.assert_(len(actions) > 0) - - def testGetParent(self): - '''Get a folder's parent using the getParent call''' - childFolder = self._testFolder.createFolder('parentTest') - parentFolder = childFolder.getParent() - self.assertEquals(self._testFolder.getObjectId(), parentFolder.getObjectId()) - - def testAddObject(self): - '''Add an existing object to another folder''' - if not self._repo.getCapabilities()['Multifiling']: - print 'This repository does not allow multifiling, skipping' - return - - subFolder1 = self._testFolder.createFolder('sub1') - doc = subFolder1.createDocument('testdoc1') - self.assertEquals(len(subFolder1.getChildren()), 1) - subFolder2 = self._testFolder.createFolder('sub2') - self.assertEquals(len(subFolder2.getChildren()), 0) - subFolder2.addObject(doc) - self.assertEquals(len(subFolder2.getChildren()), 1) - self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) - - def testRemoveObject(self): - '''Remove an existing object from a secondary folder''' - if not self._repo.getCapabilities()['Unfiling']: - print 'This repository does not allow unfiling, skipping' - return - - subFolder1 = self._testFolder.createFolder('sub1') - doc = subFolder1.createDocument('testdoc1') - self.assertEquals(len(subFolder1.getChildren()), 1) - subFolder2 = self._testFolder.createFolder('sub2') - self.assertEquals(len(subFolder2.getChildren()), 0) - subFolder2.addObject(doc) - self.assertEquals(len(subFolder2.getChildren()), 1) - self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) - subFolder2.removeObject(doc) - self.assertEquals(len(subFolder2.getChildren()), 0) - self.assertEquals(len(subFolder1.getChildren()), 1) - self.assertEquals(doc.name, subFolder1.getChildren()[0].name) - - def testGetPaths(self): - '''Get a folder's paths''' - # ask the root for its path - root = self._repo.getRootFolder() - paths = root.getPaths() - self.assertTrue(len(paths) == 1) - self.assertTrue(paths[0] == '/') - # ask the test folder for its paths - paths = self._testFolder.getPaths() - self.assertTrue(len(paths) == 1) - - # Exceptions - - def testBadParentFolder(self): - '''Try to create a folder on a bad/bogus/deleted parent - folder object''' - firstFolder = self._testFolder.createFolder('testBadParentFolder folder') - self.assert_('cmis:objectId' in firstFolder.getProperties()) - firstFolder.delete() - # folder isn't in the repo anymore, but I still have the object - # really, this seems like it ought to be an ObjectNotFoundException but - # not all CMIS providers report it as such - self.assertRaises(CmisException, - firstFolder.createFolder, - 'bad parent') - -# Per CMIS-169, nothing in the spec says that an exception should be thrown -# when a duplicate folder is created, so this test is really not necessary. -# def testDuplicateFolder(self): -# '''Try to create a folder that already exists''' -# folderName = 'testDupFolder folder' -# firstFolder = self._testFolder.createFolder(folderName) -# self.assert_('cmis:objectId' in firstFolder.getProperties()) -# # really, this needs to be ContentAlreadyExistsException but -# # not all CMIS providers report it as such -# self.assertRaises(CmisException, -# self._testFolder.createFolder, -# folderName) - - -class ChangeEntryTest(CmisTestBase): - - """ Tests for the :class:`ChangeEntry` class """ - - def testGetContentChanges(self): - - """Get the content changes and inspect Change Entry props""" - - # need to check changes capability - if not self._repo.capabilities['Changes']: - print messages.NO_CHANGE_LOG_SUPPORT - return - - # at least one change should have been made due to the creation of the - # test documents - rs = self._repo.getContentChanges() - self.assertTrue(len(rs) > 0) - changeEntry = rs[0] - self.assertTrue(changeEntry.id) - self.assertTrue(changeEntry.changeType in ['created', 'updated', 'deleted', 'security']) - self.assertTrue(changeEntry.changeTime) - - def testGetACL(self): - - """Gets the ACL that is included with a Change Entry.""" - - # need to check changes capability - if not self._repo.capabilities['Changes']: - print messages.NO_CHANGE_LOG_SUPPORT - return - - # need to check ACL capability - if not self._repo.capabilities['ACL']: - print messages.NO_ACL_SUPPORT - return - - # need to test once with includeACL set to true - rs = self._repo.getContentChanges(includeACL='true') - self.assertTrue(len(rs) > 0) - changeEntry = rs[0] - acl = changeEntry.getACL() - self.assertTrue(acl) - for entry in acl.getEntries().values(): - self.assertTrue(entry.principalId) - self.assertTrue(entry.permissions) - - # need to test once without includeACL set - rs = self._repo.getContentChanges() - self.assertTrue(len(rs) > 0) - changeEntry = rs[0] - acl = changeEntry.getACL() - self.assertTrue(acl) - for entry in acl.getEntries().values(): - self.assertTrue(entry.principalId) - self.assertTrue(entry.permissions) - - def testGetProperties(self): - - """Gets the properties of an object included with a Change Entry.""" - - # need to check changes capability - changeCap = self._repo.capabilities['Changes'] - if not changeCap: - print messages.NO_CHANGE_LOG_SUPPORT - return - - # need to test once without includeProperties set. the objectID should be there - rs = self._repo.getContentChanges() - self.assertTrue(len(rs) > 0) - changeEntry = rs[0] - self.assertTrue(changeEntry.properties['cmis:objectId']) - - # need to test once with includeProperties set. the objectID should be there plus object props - if changeCap in ['properties', 'all']: - rs = self._repo.getContentChanges(includeProperties='true') - self.assertTrue(len(rs) > 0) - changeEntry = rs[0] - self.assertTrue(changeEntry.properties['cmis:objectId']) - self.assertTrue(changeEntry.properties['cmis:name']) - - -class DocumentTest(CmisTestBase): - - """ Tests for the :class:`Document` class """ - - def testCheckout(self): - '''Create a document in a test folder, then check it out''' - newDoc = self._testFolder.createDocument('testDocument') - if (not newDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwcDoc = newDoc.checkout() - try: - self.assertTrue(newDoc.isCheckedOut()) - self.assert_('cmis:objectId' in newDoc.getProperties()) - self.assert_('cmis:objectId' in pwcDoc.getProperties()) - checkedOutDocs = self._repo.getCollection('checkedout') - self.assertTrue(isInResultSet(checkedOutDocs, pwcDoc)) - finally: - pwcDoc.delete() - - def testCheckin(self): - '''Create a document in a test folder, check it out, then in''' - testFilename = settings.TEST_BINARY_1 - contentFile = open(testFilename, 'rb') - testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) - contentFile.close() - self.assertEquals(testFilename, testDoc.getName()) - if (not testDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwcDoc = testDoc.checkout() - - try: - self.assertTrue(testDoc.isCheckedOut()) - self.assert_('cmis:objectId' in testDoc.getProperties()) - self.assert_('cmis:objectId' in pwcDoc.getProperties()) - testDoc = pwcDoc.checkin() - self.assertFalse(testDoc.isCheckedOut()) - finally: - if testDoc.isCheckedOut(): - pwcDoc.delete() - - def testCheckinComment(self): - '''Checkin a document with a comment''' - testFilename = settings.TEST_BINARY_1 - contentFile = open(testFilename, 'rb') - testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) - contentFile.close() - self.assertEquals(testFilename, testDoc.getName()) - if (not testDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwcDoc = testDoc.checkout() - - try: - self.assertTrue(testDoc.isCheckedOut()) - testDoc = pwcDoc.checkin(checkinComment='Just a few changes') - self.assertFalse(testDoc.isCheckedOut()) - self.assertEquals('Just a few changes', - testDoc.getProperties()['cmis:checkinComment']) - finally: - if testDoc.isCheckedOut(): - pwcDoc.delete() - - def testCheckinAfterGetPWC(self): - '''Create a document in a test folder, check it out, call getPWC, then checkin''' - if not self._repo.getCapabilities()['PWCUpdatable'] == True: - print 'Repository does not support PWCUpdatable, skipping' - return - - testFilename = settings.TEST_BINARY_1 - contentFile = open(testFilename, 'rb') - testDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) - contentFile.close() - self.assertEquals(testFilename, testDoc.getName()) - # Alfresco has a bug where if you get the PWC this way - # the checkin will not be successful - if (not testDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - testDoc.checkout() - pwcDoc = testDoc.getPrivateWorkingCopy() - try: - self.assertTrue(testDoc.isCheckedOut()) - self.assert_('cmis:objectId' in testDoc.getProperties()) - self.assert_('cmis:objectId' in pwcDoc.getProperties()) - testDoc = pwcDoc.checkin() - self.assertFalse(testDoc.isCheckedOut()) - finally: - if testDoc.isCheckedOut(): - pwcDoc.delete() - - def testCancelCheckout(self): - '''Create a document in a test folder, check it out, then cancel - checkout''' - newDoc = self._testFolder.createDocument('testDocument') - if (not newDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwcDoc = newDoc.checkout() - try: - self.assertTrue(newDoc.isCheckedOut()) - self.assert_('cmis:objectId' in newDoc.getProperties()) - self.assert_('cmis:objectId' in pwcDoc.getProperties()) - checkedOutDocs = self._repo.getCollection('checkedout') - self.assertTrue(isInResultSet(checkedOutDocs, pwcDoc)) - finally: - pwcDoc.delete() - self.assertFalse(newDoc.isCheckedOut()) - checkedOutDocs = self._repo.getCollection('checkedout') - self.assertFalse(isInResultSet(checkedOutDocs, pwcDoc)) - - def testDeleteDocument(self): - '''Create a document in a test folder, then delete it''' - newDoc = self._testFolder.createDocument('testDocument') - children = self._testFolder.getChildren() - self.assertEquals(1, len(children.getResults())) - newDoc.delete() - children = self._testFolder.getChildren() - self.assertEquals(0, len(children.getResults())) - - def testGetLatestVersion(self): - '''Get latest version of an object''' - f = open(settings.TEST_BINARY_1, 'rb') - doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) - if (not doc10.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc10.checkout() - doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 - if (not doc11.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc11.checkout() - doc20 = pwc.checkin() # checkin a major version, 2.0 - doc20Id = doc20.getObjectId() - if (not doc20.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc20.checkout() - doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 - doc21Id = doc21.getObjectId() - - docLatest = doc10.getLatestVersion() - self.assertEquals(doc21Id, docLatest.getObjectId()) - - docLatestMajor = doc10.getLatestVersion(major='true') - self.assertEquals(doc20Id, docLatestMajor.getObjectId()) - - def testGetPropertiesOfLatestVersion(self): - '''Get properties of latest version of an object''' - f = open(settings.TEST_BINARY_1, 'rb') - doc10 = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=f) - if (not doc10.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc10.checkout() - doc11 = pwc.checkin(major='false') # checkin a minor version, 1.1 - if (not doc11.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc11.checkout() - doc20 = pwc.checkin() # checkin a major version, 2.0 - # what comes back from a checkin may not include all props, so reload - doc20.reload() - doc20Label = doc20.getProperties()['cmis:versionLabel'] - if (not doc20.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc20.checkout() - doc21 = pwc.checkin(major='false') # checkin a minor version, 2.1 - # what comes back from a checkin may not include all props, so reload - doc21.reload() - doc21Label = doc21.getProperties()['cmis:versionLabel'] - - propsLatest = doc10.getPropertiesOfLatestVersion() - self.assertEquals(doc21Label, propsLatest['cmis:versionLabel']) - - propsLatestMajor = doc10.getPropertiesOfLatestVersion(major='true') - self.assertEquals(doc20Label, propsLatestMajor['cmis:versionLabel']) - - def testGetProperties(self): - '''Create a document in a test folder, then get its properties''' - newDoc = self._testFolder.createDocument('testDocument') - self.assertEquals('testDocument', newDoc.getName()) - self.assertTrue('cmis:objectTypeId' in newDoc.getProperties()) - self.assertTrue('cmis:objectId' in newDoc.getProperties()) - - def testAllowableActions(self): - '''Create document in a test folder, then get its allowable actions''' - newDoc = self._testFolder.createDocument('testDocument') - actions = newDoc.getAllowableActions() - self.assert_(len(actions) > 0) - - def testUpdateProperties(self): - '''Create a document in a test folder, then update its properties''' - newDoc = self._testFolder.createDocument('testDocument') - self.assertEquals('testDocument', newDoc.getName()) - props = {'cmis:name': 'testDocument2'} - newDoc.updateProperties(props) - self.assertEquals('testDocument2', newDoc.getName()) - - def testSetContentStreamPWC(self): - '''Set the content stream on the PWC''' - if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': - print 'This repository does not allow content stream updates, skipping' - return - - testFile1 = settings.TEST_BINARY_1 - testFile1Size = os.path.getsize(testFile1) - exportFile1 = testFile1.replace('.', 'export.') - testFile2 = settings.TEST_BINARY_2 - testFile2Size = os.path.getsize(testFile2) - exportFile2 = testFile1.replace('.', 'export.') - - # create a test document - contentFile = open(testFile1, 'rb') - newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) - contentFile.close() - - # export the test document - result = newDoc.getContentStream() - outfile = open(exportFile1, 'wb') - outfile.write(result.read()) - result.close() - outfile.close() - - # the file we exported should be the same size as the file we - # originally created - self.assertEquals(testFile1Size, os.path.getsize(exportFile1)) - - # checkout the file - if (not newDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = newDoc.checkout() - - # update the PWC with a new file - f = open(testFile2, 'rb') - pwc.setContentStream(f) - f.close() - - # checkin the PWC - newDoc = pwc.checkin() - - # export the checked in document - result = newDoc.getContentStream() - outfile = open(exportFile2, 'wb') - outfile.write(result.read()) - result.close() - outfile.close() - - # the file we exported should be the same size as the file we - # checked in after updating the PWC - self.assertEquals(testFile2Size, os.path.getsize(exportFile2)) - os.remove(exportFile2) - - def testSetContentStreamPWCMimeType(self): - '''Check the mimetype after the PWC checkin''' - if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': - print 'This repository does not allow content stream updates, skipping' - return - - testFile1 = settings.TEST_BINARY_1 - - # create a test document - contentFile = open(testFile1, 'rb') - newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) - origMimeType = newDoc.properties['cmis:contentStreamMimeType'] - contentFile.close() - - # checkout the file - if (not newDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = newDoc.checkout() - - # update the PWC with a new file - f = open(testFile1, 'rb') - pwc.setContentStream(f) - f.close() - - # checkin the PWC - newDoc = pwc.checkin() - - # CMIS-231 the checked in doc should have the same mime type as - # the original document - self.assertEquals(origMimeType, - newDoc.properties['cmis:contentStreamMimeType']) - - def testSetContentStreamDoc(self): - '''Set the content stream on a doc that's not checked out''' - if self._repo.getCapabilities()['ContentStreamUpdatability'] != 'anytime': - print 'This repository does not allow content stream updates on the doc, skipping' - return - - testFile1 = settings.TEST_BINARY_1 - testFile1Size = os.path.getsize(testFile1) - exportFile1 = testFile1.replace('.', 'export.') - testFile2 = settings.TEST_BINARY_2 - testFile2Size = os.path.getsize(testFile2) - exportFile2 = testFile1.replace('.', 'export.') - - # create a test document - contentFile = open(testFile1, 'rb') - newDoc = self._testFolder.createDocument(testFile1, contentFile=contentFile) - contentFile.close() - - # export the test document - result = newDoc.getContentStream() - outfile = open(exportFile1, 'wb') - outfile.write(result.read()) - result.close() - outfile.close() - - # the file we exported should be the same size as the file we - # originally created - self.assertEquals(testFile1Size, os.path.getsize(exportFile1)) - - # update the PWC with a new file - f = open(testFile2, 'rb') - newDoc.setContentStream(f) - f.close() - - # export the checked in document - result = newDoc.getContentStream() - outfile = open(exportFile2, 'wb') - outfile.write(result.read()) - result.close() - outfile.close() - - # the file we exported should be the same size as the file we - # checked in after updating the PWC - self.assertEquals(testFile2Size, os.path.getsize(exportFile2)) - os.remove(exportFile2) - - def testDeleteContentStreamPWC(self): - '''Delete the content stream of a PWC''' - if self._repo.getCapabilities()['ContentStreamUpdatability'] == 'none': - print 'This repository does not allow content stream updates, skipping' - return - if not self._repo.getCapabilities()['PWCUpdatable'] == True: - print 'Repository does not support PWCUpdatable, skipping' - return - - # create a test document - contentFile = open(settings.TEST_BINARY_1, 'rb') - newDoc = self._testFolder.createDocument(settings.TEST_BINARY_1, contentFile=contentFile) - contentFile.close() - if (not newDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = newDoc.checkout() - pwc.deleteContentStream() - self.assertRaises(CmisException, pwc.getContentStream) - pwc.delete() - - def testCreateDocumentBinary(self): - '''Create a binary document using a file from the file system''' - testFilename = settings.TEST_BINARY_1 - contentFile = open(testFilename, 'rb') - newDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) - contentFile.close() - self.assertEquals(testFilename, newDoc.getName()) - - # test to make sure the file we get back is the same length - # as the file we sent - result = newDoc.getContentStream() - exportFilename = testFilename.replace('.', 'export.') - outfile = open(exportFilename, 'wb') - outfile.write(result.read()) - result.close() - outfile.close() - self.assertEquals(os.path.getsize(testFilename), - os.path.getsize(exportFilename)) - - # cleanup - os.remove(exportFilename) - - def testCreateDocumentFromString(self): - '''Create a new document from a string''' - documentName = 'testDocument' - contentString = 'Test content string' - newDoc = self._testFolder.createDocumentFromString(documentName, - contentString=contentString, contentType='text/plain') - self.assertEquals(documentName, newDoc.getName()) - self.assertEquals(newDoc.getContentStream().read(), contentString) - - def testCreateDocumentPlain(self): - '''Create a plain document using a file from the file system''' - testFilename = 'plain.txt' - testFile = open(testFilename, 'w') - testFile.write('This is a sample text file line 1.\n') - testFile.write('This is a sample text file line 2.\n') - testFile.write('This is a sample text file line 3.\n') - testFile.close() - contentFile = open(testFilename, 'r') - newDoc = self._testFolder.createDocument(testFilename, contentFile=contentFile) - contentFile.close() - self.assertEquals(testFilename, newDoc.getName()) - - # test to make sure the file we get back is the same length as the - # file we sent - result = newDoc.getContentStream() - exportFilename = testFilename.replace('txt', 'export.txt') - outfile = open(exportFilename, 'w') - outfile.write(result.read()) - result.close() - outfile.close() - self.assertEquals(os.path.getsize(testFilename), - os.path.getsize(exportFilename)) - - # export - os.remove(exportFilename) - os.remove(testFilename) - - def testGetAllVersions(self): - '''Get all versions of an object''' - testDoc = self._testFolder.createDocument('testdoc') - if (not testDoc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = testDoc.checkout() - doc = pwc.checkin() # 2.0 - if (not doc.allowableActions['canCheckOut']): - print 'The test doc cannot be checked out...skipping' - return - pwc = doc.checkout() - doc = pwc.checkin() # 3.0 - # what comes back from a checkin may not include all props, so reload - doc.reload() - self.assertEquals('3.0', doc.getProperties()['cmis:versionLabel']) - rs = doc.getAllVersions() - self.assertEquals(3, len(rs.getResults())) -# for count in range(0, 3): -# if count == 0: -# self.assertEquals('true', -# rs.getResults().values()[count].getProperties()['cmis:isLatestVersion']) -# else: -# self.assertEquals('false', -# rs.getResults().values()[count].getProperties()['cmis:isLatestVersion']) - - def testGetObjectParents(self): - '''Gets all object parents of an CmisObject''' - childFolder = self._testFolder.createFolder('parentTest') - parentFolder = childFolder.getObjectParents().getResults()[0] - self.assertEquals(self._testFolder.getObjectId(), parentFolder.getObjectId()) - - def testGetObjectParentsWithinRootFolder(self): - '''Gets all object parents of a root folder''' - rootFolder = self._repo.getRootFolder() - self.assertRaises(NotSupportedException, rootFolder.getObjectParents) - - def testGetObjectParentsMultiple(self): - '''Gets all parents of a multi-filed object''' - if not self._repo.getCapabilities()['Multifiling']: - print 'This repository does not allow multifiling, skipping' - return - - subFolder1 = self._testFolder.createFolder('sub1') - doc = subFolder1.createDocument('testdoc1') - self.assertEquals(len(subFolder1.getChildren()), 1) - subFolder2 = self._testFolder.createFolder('sub2') - self.assertEquals(len(subFolder2.getChildren()), 0) - subFolder2.addObject(doc) - self.assertEquals(len(subFolder2.getChildren()), 1) - self.assertEquals(subFolder1.getChildren()[0].name, subFolder2.getChildren()[0].name) - parentNames = ['sub1', 'sub2'] - for parent in doc.getObjectParents(): - parentNames.remove(parent.name) - self.assertEquals(len(parentNames), 0) - - def testGetPaths(self): - '''Get the paths of a document''' - testDoc = self._testFolder.createDocument('testdoc') - # ask the test doc for its paths - paths = testDoc.getPaths() - self.assertTrue(len(paths) >= 1) - - def testRenditions(self): - '''Get the renditions for a document''' - if not self._repo.getCapabilities().has_key('Renditions'): - print 'Repo does not support unfiling, skipping' - return - - testDoc = self._testFolder.createDocumentFromString('testdoc.txt', contentString='test', contentType='text/plain') - sleep(settings.FULL_TEXT_WAIT) - if (testDoc.getAllowableActions().has_key('canGetRenditions') and - testDoc.getAllowableActions()['canGetRenditions'] == True): - rends = testDoc.getRenditions() - self.assertTrue(len(rends) >= 1) - else: - print 'Test doc does not have rendition, skipping' - return - - -class TypeTest(unittest.TestCase): - - """ - Tests for the :class:`ObjectType` class (and related methods in the - :class:`Repository` class. - """ - - def testTypeDescendants(self): - '''Get the descendant types of the repository.''' - - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - typeDefs = repo.getTypeDescendants() - folderDef = None - for typeDef in typeDefs: - if typeDef.getTypeId() == 'cmis:folder': - folderDef = typeDef - break - self.assertTrue(folderDef) - self.assertTrue(folderDef.baseId) - - def testTypeChildren(self): - '''Get the child types for this repository and make sure cmis:folder - is in the list.''' - - #This test would be more interesting if there was a standard way to - #deploy a custom model. Then we could look for custom types. - - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - typeDefs = repo.getTypeChildren() - folderDef = None - for typeDef in typeDefs: - if typeDef.getTypeId() == 'cmis:folder': - folderDef = typeDef - break - self.assertTrue(folderDef) - self.assertTrue(folderDef.baseId) - - def testTypeDefinition(self): - '''Get the cmis:document type and test a few props of the type.''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - docTypeDef = repo.getTypeDefinition('cmis:document') - self.assertEquals('cmis:document', docTypeDef.getTypeId()) - self.assertTrue(docTypeDef.baseId) - - def testTypeProperties(self): - '''Get the properties for a type.''' - cmisClient = CmisClient(settings.REPOSITORY_URL, settings.USERNAME, settings.PASSWORD, **settings.EXT_ARGS) - repo = cmisClient.getDefaultRepository() - docTypeDef = repo.getTypeDefinition('cmis:document') - self.assertEquals('cmis:document', docTypeDef.getTypeId()) - props = docTypeDef.getProperties().values() - self.assertTrue(len(props) > 0) - for prop in props: - if prop.queryable: - self.assertTrue(prop.queryName) - self.assertTrue(prop.propertyType) - - -class ACLTest(CmisTestBase): - - """ - Tests related to :class:`ACL` and :class:`ACE` - """ - - def testSupportedPermissions(self): - '''Test the value of supported permissions enum''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - self.assertTrue(self._repo.getSupportedPermissions() in ['basic', 'repository', 'both']) - - def testPermissionDefinitions(self): - '''Test the list of permission definitions''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - supportedPerms = self._repo.getPermissionDefinitions() - self.assertTrue(supportedPerms.has_key('cmis:write')) - - def testPermissionMap(self): - '''Test the permission mapping''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - permMap = self._repo.getPermissionMap() - self.assertTrue(permMap.has_key('canGetProperties.Object')) - self.assertTrue(len(permMap['canGetProperties.Object']) > 0) - - def testPropagation(self): - '''Test the propagation setting''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - self.assertTrue(self._repo.getPropagation() in ['objectonly', 'propagate', 'repositorydetermined']) - - def testGetObjectACL(self): - '''Test getting an object's ACL''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - acl = self._testFolder.getACL() - for entry in acl.getEntries().values(): - self.assertTrue(entry.principalId) - self.assertTrue(entry.permissions) - - def testApplyACL(self): - '''Test updating an object's ACL''' - if not self._repo.getCapabilities()['ACL']: - print messages.NO_ACL_SUPPORT - return - if not self._repo.getCapabilities()['ACL'] == 'manage': - print 'Repository does not support manage ACL' - return - if not self._repo.getSupportedPermissions() in ['both', 'basic']: - print 'Repository needs to support either both or basic permissions for this test' - return - acl = self._testFolder.getACL() - acl.addEntry(ACE(settings.TEST_PRINCIPAL_ID, 'cmis:write', 'true')) - acl = self._testFolder.applyACL(acl) - # would be good to check that the permission we get back is what we set - # but at least one server (Alf) appears to map the basic perm to a - # repository-specific perm - self.assertTrue(acl.getEntries().has_key(settings.TEST_PRINCIPAL_ID)) - - -def isInCollection(collection, targetDoc): - ''' - Util function that searches a list of objects for a matching target - object. - ''' - for doc in collection: - # hacking around a bizarre thing in Alfresco which is that when the - # PWC comes back it has an object ID of say 123ABC but when you look - # in the checked out collection the object ID of the PWC is now - # 123ABC;1.0. What is that ;1.0? I don't know, but object IDs are - # supposed to be immutable so I'm not sure what's going on there. - if doc.getObjectId().startswith(targetDoc.getObjectId()): - return True - return False - - -def isInResultSet(resultSet, targetDoc): - """ - Util function that searches a :class:`ResultSet` for a specified target - object. Note that this function will do a getNext on every page of the - result set until it finds what it is looking for or reaches the end of - the result set. For every item in the result set, the properties - are retrieved. Long story short: this could be an expensive call. - """ - done = False - while not done: - if resultSet.hasObject(targetDoc.getObjectId()): - return True - if resultSet.hasNext(): - resultSet.getNext() - else: - done = True - -if __name__ == "__main__": - #unittest.main() - tts = TestSuite() - #tts.addTests(TestLoader().loadTestsFromName('testGetObjectByPath', RepositoryTest)) - #unittest.TextTestRunner().run(tts) - #import sys; sys.exit(0) - - tts.addTests(TestLoader().loadTestsFromTestCase(CmisClientTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(RepositoryTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(FolderTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(DocumentTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(TypeTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(ACLTest)) - tts.addTests(TestLoader().loadTestsFromTestCase(ChangeEntryTest)) - -# tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentFromString', RepositoryTest)) -# tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentFromString', DocumentTest)) -# tts.addTests(TestLoader().loadTestsFromName('testMoveDocument', RepositoryTest)) -# tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentBinary', DocumentTest)) -# tts.addTests(TestLoader().loadTestsFromName('testCreateDocumentPlain', DocumentTest)) -# tts.addTests(TestLoader().loadTestsFromName('testAddObject', FolderTest)) -# tts.addTests(TestLoader().loadTestsFromName('testRemoveObject', FolderTest)) -# tts.addTests(TestLoader().loadTestsFromName('testFolderLeadingDot', FolderTest)) -# tts.addTests(TestLoader().loadTestsFromName('testGetObjectParents', DocumentTest)) -# tts.addTests(TestLoader().loadTestsFromName('testGetObjectParentsMultiple', DocumentTest)) -# tts.addTests(TestLoader().loadTestsFromName('testRenditions', DocumentTest)) - - # WARNING: Potentially long-running tests - - # Query tests - #tts.addTests(TestLoader().loadTestsFromTestCase(QueryTest)) - #tts.addTest(QueryTest('testPropertyMatch')) - #tts.addTest(QueryTest('testFullText')) - #tts.addTest(QueryTest('testScore')) - #tts.addTest(QueryTest('testWildcardPropertyMatch')) - #tts.addTest(QueryTest('testSimpleSelect')) - - unittest.TextTestRunner().run(tts) diff --git a/src/tests/sample-a.pdf b/src/tests/sample-a.pdf deleted file mode 100755 index 727cb1f..0000000 Binary files a/src/tests/sample-a.pdf and /dev/null differ diff --git a/src/tests/settings.py b/src/tests/settings.py deleted file mode 100644 index 7e87525..0000000 --- a/src/tests/settings.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# 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. -# - -# -# Override these settings with values to match your environment. -# -# CMIS repository's service URL -#REPOSITORY_URL = 'http://cmis.alfresco.com/s/cmis' # Alfresco demo -#REPOSITORY_URL = 'http://localhost:8080/chemistry/atom' # Apache Chemistry -REPOSITORY_URL = 'http://localhost:8080/alfresco/cmisatom' # Alfresco 4.0 -#REPOSITORY_URL = 'http://localhost:8080/alfresco/s/api/cmis' # Alfresco -#REPOSITORY_URL = 'http://cmis.demo.nuxeo.org/nuxeo/atom/cmis' # Nuxeo demo -#REPOSITORY_URL = 'http://localhost:8080/nuxeo/atom/cmis' # Nuxeo local - -# CMIS repository credentials -USERNAME = 'admin' # Alfresco -PASSWORD = 'admin' # Alfresco -#USERNAME = '' -#PASSWORD = '' -#USERNAME = 'Administrator' # Nuxeo -#PASSWORD = 'Administrator' # Nuxeo -EXT_ARGS = {} -#EXT_ARGS = {'alf_ticket': 'TICKET_cef29079d8d5341338bf372b08278bc30ec89380'} -# Absolute path to a directory where test folders can be created, including -# the trailing slash. -#TEST_ROOT_PATH = '/default-domain/workspaces/cmislib' # No trailing slash -TEST_ROOT_PATH = '/cmislib' # No trailing slash -#TEST_ROOT_PATH = '/' -# Binary test files. Assumed to exist in the same dir as this python script -TEST_BINARY_1 = '250px-Cmis_logo.png' -TEST_BINARY_2 = 'sample-a.pdf' -# For repositories that support setting an ACL, the name of an existing -# principal ID to add to the ACL of a test object. Some repositories care -# if this ID doesn't exist. Some repositories don't. -TEST_PRINCIPAL_ID = 'tuser1' -# For repositories that may index test content asynchronously, the number of -# times a query is retried before giving up. -MAX_FULL_TEXT_TRIES = 10 -# The number of seconds the test should sleep between tries. -FULL_TEXT_WAIT = 10