4281 lines
152 KiB
Python
4281 lines
152 KiB
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.
|
|
#
|
|
"""
|
|
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)
|
|
<cmislib.model.Folder object at 0x103ab1210>
|
|
>>> folder.getName()
|
|
u'someFolderFoo'
|
|
|
|
"""
|
|
|
|
self.logger.debug('Inside updateProperties')
|
|
|
|
# get the self link
|
|
selfUrl = self._getSelfLink()
|
|
|
|
# if we have a change token, we must pass it back, per the spec
|
|
args = {}
|
|
if (self.properties.has_key('cmis:changeToken') and
|
|
self.properties['cmis:changeToken'] != None):
|
|
self.logger.debug('Change token present, adding it to args')
|
|
args = {"changeToken": self.properties['cmis:changeToken']}
|
|
|
|
# the getEntryXmlDoc function may need the object type
|
|
objectTypeId = None
|
|
if (self.properties.has_key('cmis:objectTypeId') and
|
|
not properties.has_key('cmis:objectTypeId')):
|
|
objectTypeId = self.properties['cmis:objectTypeId']
|
|
self.logger.debug('This object type is:%s' % objectTypeId)
|
|
|
|
# build the entry based on the properties provided
|
|
xmlEntryDoc = getEntryXmlDoc(self._repository, objectTypeId, properties)
|
|
|
|
self.logger.debug('xmlEntryDoc:' + xmlEntryDoc.toxml())
|
|
|
|
# do a PUT of the entry
|
|
updatedXmlDoc = self._cmisClient.binding.put(selfUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
xmlEntryDoc.toxml(encoding='utf-8'),
|
|
ATOM_XML_TYPE,
|
|
**args)
|
|
|
|
# reset the xmlDoc for this object with what we got back from
|
|
# the PUT, then call initData we dont' want to call
|
|
# self.reload because we've already got the parsed XML--
|
|
# there's no need to fetch it again
|
|
self.xmlDoc = updatedXmlDoc
|
|
self._initData()
|
|
return self
|
|
|
|
def move(self, sourceFolder, targetFolder):
|
|
|
|
"""
|
|
Moves an object from the source folder to the target folder.
|
|
|
|
>>> sub1 = repo.getObjectByPath('/cmislib/sub1')
|
|
>>> sub2 = repo.getObjectByPath('/cmislib/sub2')
|
|
>>> doc = repo.getObjectByPath('/cmislib/sub1/testdoc1')
|
|
>>> doc.move(sub1, sub2)
|
|
"""
|
|
|
|
postUrl = targetFolder.getChildrenLink()
|
|
|
|
args = {"sourceFolderId": sourceFolder.id}
|
|
|
|
# post the Atom entry
|
|
result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
self.xmlDoc.toxml(encoding='utf-8'),
|
|
ATOM_XML_ENTRY_TYPE,
|
|
**args)
|
|
|
|
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': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
|
|
|
|
The optional onlyBasicPermissions argument is currently not supported.
|
|
"""
|
|
|
|
if self._repository.getCapabilities()['ACL']:
|
|
# if the ACL capability is discover or manage, this must be
|
|
# supported
|
|
aclUrl = self._getLink(ACL_REL)
|
|
result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password)
|
|
return AtomPubACL(xmlDoc=result)
|
|
else:
|
|
raise NotSupportedException
|
|
|
|
def applyACL(self, acl):
|
|
|
|
"""
|
|
Updates the object with the provided :class:`ACL`.
|
|
Repository.getCapabilities['ACL'] must return manage to invoke this
|
|
call.
|
|
|
|
>>> acl = folder.getACL()
|
|
>>> acl.addEntry(ACE('jdoe', 'cmis:write', 'true'))
|
|
>>> acl.getEntries()
|
|
{u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x10071a8d0>, 'jdoe': <cmislib.model.ACE object at 0x10071a590>}
|
|
"""
|
|
|
|
if self._repository.getCapabilities()['ACL'] == 'manage':
|
|
# if the ACL capability is manage, this must be
|
|
# supported
|
|
# but it also depends on the canApplyACL allowable action
|
|
# for this object
|
|
if not isinstance(acl, ACL):
|
|
raise CmisException('The ACL to apply must be an instance of the ACL class.')
|
|
aclUrl = self._getLink(ACL_REL)
|
|
assert aclUrl, "Could not determine the object's ACL URL."
|
|
result = self._cmisClient.binding.put(aclUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
acl.getXmlDoc().toxml(encoding='utf-8'),
|
|
CMIS_ACL_TYPE)
|
|
return AtomPubACL(xmlDoc=result)
|
|
else:
|
|
raise NotSupportedException
|
|
|
|
def _getSelfLink(self):
|
|
|
|
"""
|
|
Returns the URL used to retrieve this object.
|
|
"""
|
|
|
|
url = self._getLink(SELF_REL)
|
|
|
|
assert len(url) > 0, "Could not determine the self link."
|
|
|
|
return url
|
|
|
|
def _getLink(self, rel, ltype=None):
|
|
|
|
"""
|
|
Returns the HREF attribute of an Atom link element for the
|
|
specified rel.
|
|
"""
|
|
|
|
if self.xmlDoc == 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')
|
|
<cmislib.model.Document object at 0x101352ed0>
|
|
"""
|
|
|
|
# if you didn't pass in a parent folder
|
|
if parentFolder == None:
|
|
# if the repository doesn't require fileable objects to be filed
|
|
if self.getCapabilities()['Unfiling']:
|
|
# has not been implemented
|
|
#postUrl = self.getCollectionLink(UNFILED_COLL)
|
|
raise NotImplementedError
|
|
else:
|
|
# this repo requires fileable objects to be filed
|
|
raise InvalidArgumentException
|
|
|
|
return parentFolder.createDocument(name, properties, StringIO.StringIO(contentString),
|
|
contentType, contentEncoding)
|
|
|
|
def createDocument(self,
|
|
name,
|
|
properties={},
|
|
parentFolder=None,
|
|
contentFile=None,
|
|
contentType=None,
|
|
contentEncoding=None):
|
|
|
|
"""
|
|
Creates a new :class:`Document` object. If the repository
|
|
supports unfiled objects, you do not have to pass in
|
|
a parent :class:`Folder` otherwise it is required.
|
|
|
|
To create a document with an associated contentFile, pass in a
|
|
File object. The method will attempt to guess the appropriate content
|
|
type and encoding based on the file. To specify it yourself, pass them
|
|
in via the contentType and contentEncoding arguments.
|
|
|
|
>>> f = open('sample-a.pdf', 'rb')
|
|
>>> doc = folder.createDocument('sample-a.pdf', contentFile=f)
|
|
<cmislib.model.Document object at 0x105be5e10>
|
|
>>> f.close()
|
|
>>> doc.getTitle()
|
|
u'sample-a.pdf'
|
|
|
|
The following optional arguments are not currently supported:
|
|
- versioningState
|
|
- policies
|
|
- addACEs
|
|
- removeACEs
|
|
"""
|
|
|
|
postUrl = ''
|
|
# if you didn't pass in a parent folder
|
|
if parentFolder == None:
|
|
# if the repository doesn't require fileable objects to be filed
|
|
if self.getCapabilities()['Unfiling']:
|
|
# has not been implemented
|
|
#postUrl = self.getCollectionLink(UNFILED_COLL)
|
|
raise NotImplementedError
|
|
else:
|
|
# this repo requires fileable objects to be filed
|
|
raise InvalidArgumentException
|
|
else:
|
|
postUrl = parentFolder.getChildrenLink()
|
|
|
|
# make sure a name is set
|
|
properties['cmis:name'] = name
|
|
|
|
# hardcoding to cmis:document if it wasn't
|
|
# passed in via props
|
|
if not properties.has_key('cmis:objectTypeId'):
|
|
properties['cmis:objectTypeId'] = CmisId('cmis:document')
|
|
# and if it was passed in, making sure it is a CmisId
|
|
elif not isinstance(properties['cmis:objectTypeId'], CmisId):
|
|
properties['cmis:objectTypeId'] = CmisId(properties['cmis:objectTypeId'])
|
|
|
|
# build the Atom entry
|
|
xmlDoc = getEntryXmlDoc(self, None, properties, contentFile,
|
|
contentType, contentEncoding)
|
|
|
|
# post the Atom entry
|
|
result = self._cmisClient.binding.post(postUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
xmlDoc.toxml(encoding='utf-8'),
|
|
ATOM_XML_ENTRY_TYPE)
|
|
|
|
# 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
|
|
...
|
|
<cmislib.model.Document object at 0x104851810>
|
|
'''
|
|
if self._results:
|
|
return self._results
|
|
|
|
if self._xmlDoc:
|
|
entryElements = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'entry')
|
|
entries = []
|
|
for entryElement in entryElements:
|
|
cmisObject = getSpecializedObject(AtomPubCmisObject(self._cmisClient,
|
|
self._repository,
|
|
xmlDoc=entryElement))
|
|
entries.append(cmisObject)
|
|
|
|
self._results = entries
|
|
|
|
return self._results
|
|
|
|
def hasObject(self, objectId):
|
|
|
|
'''
|
|
Returns True if the specified objectId is found in the list of results,
|
|
otherwise returns False.
|
|
'''
|
|
|
|
for obj in self.getResults():
|
|
if obj.id == objectId:
|
|
return True
|
|
return False
|
|
|
|
def getFirst(self):
|
|
|
|
'''
|
|
Returns the first page of results as a dictionary of
|
|
:class:`CmisObject` objects or its appropriate sub-type. This only
|
|
works when the server returns a "first" link. Not all of them do.
|
|
|
|
>>> resultSet.hasFirst()
|
|
True
|
|
>>> results = resultSet.getFirst()
|
|
>>> for result in results:
|
|
... result
|
|
...
|
|
<cmislib.model.Document object at 0x10480bc90>
|
|
'''
|
|
|
|
return self._getPageResults(FIRST_REL)
|
|
|
|
def getPrev(self):
|
|
|
|
'''
|
|
Returns the prev page of results as a dictionary of
|
|
:class:`CmisObject` objects or its appropriate sub-type. This only
|
|
works when the server returns a "prev" link. Not all of them do.
|
|
>>> resultSet.hasPrev()
|
|
True
|
|
>>> results = resultSet.getPrev()
|
|
>>> for result in results:
|
|
... result
|
|
...
|
|
<cmislib.model.Document object at 0x10480bc90>
|
|
'''
|
|
|
|
return self._getPageResults(PREV_REL)
|
|
|
|
def getNext(self):
|
|
|
|
'''
|
|
Returns the next page of results as a dictionary of
|
|
:class:`CmisObject` objects or its appropriate sub-type.
|
|
>>> resultSet.hasNext()
|
|
True
|
|
>>> results = resultSet.getNext()
|
|
>>> for result in results:
|
|
... result
|
|
...
|
|
<cmislib.model.Document object at 0x10480bc90>
|
|
'''
|
|
|
|
return self._getPageResults(NEXT_REL)
|
|
|
|
def getLast(self):
|
|
|
|
'''
|
|
Returns the last page of results as a dictionary of
|
|
:class:`CmisObject` objects or its appropriate sub-type. This only
|
|
works when the server is returning a "last" link. Not all of them do.
|
|
|
|
>>> resultSet.hasLast()
|
|
True
|
|
>>> results = resultSet.getLast()
|
|
>>> for result in results:
|
|
... result
|
|
...
|
|
<cmislib.model.Document object at 0x10480bc90>
|
|
'''
|
|
|
|
return self._getPageResults(LAST_REL)
|
|
|
|
def hasNext(self):
|
|
|
|
'''
|
|
Returns True if this page contains a next link.
|
|
|
|
>>> resultSet.hasNext()
|
|
True
|
|
'''
|
|
|
|
if self._getLink(NEXT_REL):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def hasPrev(self):
|
|
|
|
'''
|
|
Returns True if this page contains a prev link. Not all CMIS providers
|
|
implement prev links consistently.
|
|
|
|
>>> resultSet.hasPrev()
|
|
True
|
|
'''
|
|
|
|
if self._getLink(PREV_REL):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def hasFirst(self):
|
|
|
|
'''
|
|
Returns True if this page contains a first link. Not all CMIS providers
|
|
implement first links consistently.
|
|
|
|
>>> resultSet.hasFirst()
|
|
True
|
|
'''
|
|
|
|
if self._getLink(FIRST_REL):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def hasLast(self):
|
|
|
|
'''
|
|
Returns True if this page contains a last link. Not all CMIS providers
|
|
implement last links consistently.
|
|
|
|
>>> resultSet.hasLast()
|
|
True
|
|
'''
|
|
|
|
if self._getLink(LAST_REL):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
class AtomPubDocument(AtomPubCmisObject):
|
|
|
|
"""
|
|
An object typically associated with file content.
|
|
"""
|
|
|
|
def checkout(self):
|
|
|
|
"""
|
|
Performs a checkout on the :class:`Document` and returns the
|
|
Private Working Copy (PWC), which is also an instance of
|
|
:class:`Document`
|
|
|
|
>>> doc.getObjectId()
|
|
u'workspace://SpacesStore/f0c8b90f-bec0-4405-8b9c-2ab570589808;1.0'
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
>>> pwc = doc.checkout()
|
|
>>> doc.isCheckedOut()
|
|
True
|
|
"""
|
|
|
|
# get the checkedout collection URL
|
|
checkoutUrl = self._repository.getCollectionLink(CHECKED_OUT_COLL)
|
|
assert len(checkoutUrl) > 0, "Could not determine the checkedout collection url."
|
|
|
|
# get this document's object ID
|
|
# build entry XML with it
|
|
properties = {'cmis:objectId': self.getObjectId()}
|
|
entryXmlDoc = getEntryXmlDoc(self._repository, properties=properties)
|
|
|
|
# post it to to the checkedout collection URL
|
|
result = self._cmisClient.binding.post(checkoutUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
entryXmlDoc.toxml(encoding='utf-8'),
|
|
ATOM_XML_ENTRY_TYPE)
|
|
|
|
# now that the doc is checked out, we need to refresh the XML
|
|
# to pick up the prop updates related to a checkout
|
|
self.reload()
|
|
|
|
return AtomPubDocument(self._cmisClient, self._repository, xmlDoc=result)
|
|
|
|
def cancelCheckout(self):
|
|
"""
|
|
Cancels the checkout of this object by retrieving the Private Working
|
|
Copy (PWC) and then deleting it. After the PWC is deleted, this object
|
|
will be reloaded to update properties related to a checkout.
|
|
|
|
>>> doc.isCheckedOut()
|
|
True
|
|
>>> doc.cancelCheckout()
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
"""
|
|
|
|
pwcDoc = self.getPrivateWorkingCopy()
|
|
if pwcDoc:
|
|
pwcDoc.delete()
|
|
self.reload()
|
|
|
|
def getPrivateWorkingCopy(self):
|
|
|
|
"""
|
|
Retrieves the object using the object ID in the property:
|
|
cmis:versionSeriesCheckedOutId then uses getObject to instantiate
|
|
the object.
|
|
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
>>> doc.checkout()
|
|
<cmislib.model.Document object at 0x103a25ad0>
|
|
>>> pwc = doc.getPrivateWorkingCopy()
|
|
>>> pwc.getTitle()
|
|
u'sample-b (Working Copy).pdf'
|
|
"""
|
|
|
|
# reloading the document just to make sure we've got the latest
|
|
# and greatest PWC ID
|
|
self.reload()
|
|
pwcDocId = self.getProperties()['cmis:versionSeriesCheckedOutId']
|
|
if pwcDocId:
|
|
return self._repository.getObject(pwcDocId)
|
|
|
|
def isCheckedOut(self):
|
|
|
|
"""
|
|
Returns true if the document is checked out.
|
|
|
|
>>> doc.isCheckedOut()
|
|
True
|
|
>>> doc.cancelCheckout()
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
"""
|
|
|
|
# reloading the document just to make sure we've got the latest
|
|
# and greatest checked out prop
|
|
self.reload()
|
|
return parseBoolValue(self.getProperties()['cmis:isVersionSeriesCheckedOut'])
|
|
|
|
def getCheckedOutBy(self):
|
|
|
|
"""
|
|
Returns the ID who currently has the document checked out.
|
|
>>> pwc = doc.checkout()
|
|
>>> pwc.getCheckedOutBy()
|
|
u'admin'
|
|
"""
|
|
|
|
# reloading the document just to make sure we've got the latest
|
|
# and greatest checked out prop
|
|
self.reload()
|
|
return self.getProperties()['cmis:versionSeriesCheckedOutBy']
|
|
|
|
def checkin(self, checkinComment=None, **kwargs):
|
|
|
|
"""
|
|
Checks in this :class:`Document` which must be a private
|
|
working copy (PWC).
|
|
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
>>> pwc = doc.checkout()
|
|
>>> doc.isCheckedOut()
|
|
True
|
|
>>> pwc.checkin()
|
|
<cmislib.model.Document object at 0x103a8ae90>
|
|
>>> doc.isCheckedOut()
|
|
False
|
|
|
|
The following optional arguments are supported:
|
|
- major
|
|
- properties
|
|
- contentStream
|
|
- policies
|
|
- addACEs
|
|
- removeACEs
|
|
"""
|
|
|
|
# Add checkin to kwargs and checkinComment, if it exists
|
|
kwargs['checkin'] = 'true'
|
|
kwargs['checkinComment'] = checkinComment
|
|
|
|
# Build an empty ATOM entry
|
|
entryXmlDoc = getEmptyXmlDoc()
|
|
|
|
# Get the self link
|
|
# Do a PUT of the empty ATOM to the self link
|
|
url = self._getSelfLink()
|
|
result = self._cmisClient.binding.put(url.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
entryXmlDoc.toxml(encoding='utf-8'),
|
|
ATOM_XML_TYPE,
|
|
**kwargs)
|
|
|
|
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)
|
|
<cmislib.model.Document object at 0x10410fa10>
|
|
>>> f.close()
|
|
|
|
If you wanted to set one or more properties when creating the doc, pass
|
|
in a dict, like this:
|
|
|
|
>>> props = {'cmis:someProp':'someVal'}
|
|
>>> f = open('250px-Cmis_logo.png', 'rb')
|
|
>>> subFolder.createDocument('logo.png', props, contentFile=f)
|
|
<cmislib.model.Document object at 0x10410fa10>
|
|
>>> f.close()
|
|
|
|
To specify a custom object type, pass in a property called
|
|
cmis:objectTypeId set to the :class:`CmisId` representing the type ID
|
|
of the instance you want to create. If you do not pass in an object
|
|
type ID, an instance of 'cmis:document' will be created.
|
|
|
|
The following optional arguments are not yet supported:
|
|
- versioningState
|
|
- policies
|
|
- addACEs
|
|
- removeACEs
|
|
"""
|
|
|
|
return self._repository.createDocument(name,
|
|
properties,
|
|
self,
|
|
contentFile,
|
|
contentType,
|
|
contentEncoding)
|
|
|
|
def getChildren(self, **kwargs):
|
|
|
|
"""
|
|
Returns a paged :class:`ResultSet`. The result set contains a list of
|
|
:class:`CmisObject` objects for each child of the Folder. The actual
|
|
type of the object returned depends on the object's CMIS base type id.
|
|
For example, the method might return a list that contains both
|
|
:class:`Document` objects and :class:`Folder` objects.
|
|
|
|
>>> childrenRS = subFolder.getChildren()
|
|
>>> children = childrenRS.getResults()
|
|
|
|
The following optional arguments are supported:
|
|
- maxItems
|
|
- skipCount
|
|
- orderBy
|
|
- filter
|
|
- includeRelationships
|
|
- renditionFilter
|
|
- includeAllowableActions
|
|
- includePathSegment
|
|
"""
|
|
|
|
# get the appropriate 'down' link
|
|
childrenUrl = self.getChildrenLink()
|
|
# invoke the URL
|
|
result = self._cmisClient.binding.get(childrenUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
**kwargs)
|
|
|
|
# return the result set
|
|
return AtomPubResultSet(self._cmisClient, self._repository, result)
|
|
|
|
def getChildrenLink(self):
|
|
|
|
"""
|
|
Gets the Atom link that knows how to return this object's children.
|
|
"""
|
|
|
|
url = self._getLink(DOWN_REL, ATOM_XML_FEED_TYPE_P)
|
|
|
|
assert len(url) > 0, "Could not find the children url"
|
|
|
|
return url
|
|
|
|
def getDescendantsLink(self):
|
|
|
|
"""
|
|
Returns the 'down' link of type `CMIS_TREE_TYPE`
|
|
|
|
>>> folder.getDescendantsLink()
|
|
u'http://localhost:8080/alfresco/s/cmis/s/workspace:SpacesStore/i/86f6bf54-f0e8-4a72-8cb1-213599ba086c/descendants'
|
|
"""
|
|
|
|
url = self._getLink(DOWN_REL, CMIS_TREE_TYPE_P)
|
|
|
|
assert len(url) > 0, "Could not find the descendants url"
|
|
|
|
# some servers return a depth arg as part of this URL
|
|
# so strip it off but keep other args
|
|
if url.find("?") >= 0:
|
|
u = list(urlparse(url))
|
|
u[4] = '&'.join([p for p in u[4].split('&') if not p.startswith('depth=')])
|
|
url = urlunparse(u)
|
|
|
|
return url
|
|
|
|
def getDescendants(self, **kwargs):
|
|
|
|
"""
|
|
Gets the descendants of this folder. The descendants are returned as
|
|
a paged :class:`ResultSet` object. The result set contains a list of
|
|
:class:`CmisObject` objects where the actual type of each object
|
|
returned will vary depending on the object's base type id. For example,
|
|
the method might return a list that contains both :class:`Document`
|
|
objects and :class:`Folder` objects.
|
|
|
|
The following optional argument is supported:
|
|
- depth. Use depth=-1 for all descendants, which is the default if no
|
|
depth is specified.
|
|
|
|
>>> resultSet = folder.getDescendants()
|
|
>>> len(resultSet.getResults())
|
|
105
|
|
>>> resultSet = folder.getDescendants(depth=1)
|
|
>>> len(resultSet.getResults())
|
|
103
|
|
|
|
The following optional arguments *may* also work but haven't been
|
|
tested:
|
|
|
|
- filter
|
|
- includeRelationships
|
|
- renditionFilter
|
|
- includeAllowableActions
|
|
- includePathSegment
|
|
|
|
"""
|
|
|
|
if not self._repository.getCapabilities()['GetDescendants']:
|
|
raise NotSupportedException('This repository does not support getDescendants')
|
|
|
|
# default the depth to -1, which is all descendants
|
|
if "depth" not in kwargs:
|
|
kwargs['depth'] = -1
|
|
|
|
# get the appropriate 'down' link
|
|
descendantsUrl = self.getDescendantsLink()
|
|
|
|
# invoke the URL
|
|
result = self._cmisClient.binding.get(descendantsUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password,
|
|
**kwargs)
|
|
|
|
# return the result set
|
|
return AtomPubResultSet(self._cmisClient, self._repository, result)
|
|
|
|
def getTree(self, **kwargs):
|
|
|
|
"""
|
|
Unlike :class:`Folder.getChildren` or :class:`Folder.getDescendants`,
|
|
this method returns only the descendant objects that are folders. The
|
|
results do not include the current folder.
|
|
|
|
The following optional arguments are supported:
|
|
- depth
|
|
- filter
|
|
- includeRelationships
|
|
- renditionFilter
|
|
- includeAllowableActions
|
|
- includePathSegment
|
|
|
|
>>> rs = folder.getTree(depth='2')
|
|
>>> len(rs.getResults())
|
|
3
|
|
>>> for folder in rs.getResults().values():
|
|
... folder.getTitle()
|
|
...
|
|
u'subfolder2'
|
|
u'parent test folder'
|
|
u'subfolder'
|
|
"""
|
|
|
|
# Get the descendants link and do a GET against it
|
|
url = self._getLink(FOLDER_TREE_REL)
|
|
assert url != 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': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
|
|
"""
|
|
ace = AtomPubACE(principalId, access, direct)
|
|
self._entries[ace.principalId] = ace
|
|
|
|
def removeEntry(self, principalId):
|
|
|
|
"""
|
|
Removes the :class:`ACE` entry given a specific principalId.
|
|
|
|
>>> acl.getEntries()
|
|
{u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>, 'jsmith': <cmislib.model.ACE object at 0x1005a2210>}
|
|
>>> acl.removeEntry('jsmith')
|
|
>>> acl.getEntries()
|
|
{u'GROUP_EVERYONE': <cmislib.model.ACE object at 0x100731410>, u'jdoe': <cmislib.model.ACE object at 0x100731150>, 'jpotts': <cmislib.model.ACE object at 0x1005a22d0>}
|
|
"""
|
|
|
|
if self._entries.has_key(principalId):
|
|
del(self._entries[principalId])
|
|
|
|
def clearEntries(self):
|
|
|
|
"""
|
|
Clears all :class:`ACE` entries from the ACL and removes the internal
|
|
XML representation of the ACL.
|
|
|
|
>>> acl = ACL()
|
|
>>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
|
|
>>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true'))
|
|
>>> acl.entries
|
|
{'jpotts': <cmislib.model.ACE object at 0x1012c7310>, 'jsmith': <cmislib.model.ACE object at 0x100528490>}
|
|
>>> acl.getXmlDoc()
|
|
<xml.dom.minidom.Document instance at 0x1012cbb90>
|
|
>>> acl.clearEntries()
|
|
>>> acl.entries
|
|
>>> acl.getXmlDoc()
|
|
"""
|
|
|
|
self._entries.clear()
|
|
self._xmlDoc = None
|
|
|
|
def getEntries(self):
|
|
|
|
"""
|
|
Returns a dictionary of :class:`ACE` objects for each Access Control
|
|
Entry in the ACL. The key value is the ACE principalid.
|
|
|
|
>>> acl = ACL()
|
|
>>> acl.addEntry(ACE('jsmith', 'cmis:write', 'true'))
|
|
>>> acl.addEntry(ACE('jpotts', 'cmis:write', 'true'))
|
|
>>> for ace in acl.entries.values():
|
|
... print 'principal:%s has the following permissions...' % ace.principalId
|
|
... for perm in ace.permissions:
|
|
... print perm
|
|
...
|
|
principal:jpotts has the following permissions...
|
|
cmis:write
|
|
principal:jsmith has the following permissions...
|
|
cmis:write
|
|
"""
|
|
|
|
if self._entries:
|
|
return self._entries
|
|
else:
|
|
if self._xmlDoc:
|
|
# parse XML doc and build entry list
|
|
self._entries = self._getEntriesFromXml()
|
|
# then return it
|
|
return self._entries
|
|
|
|
def _getEntriesFromXml(self):
|
|
|
|
"""
|
|
Helper method for getting the :class:`ACE` entries from an XML
|
|
representation of the ACL.
|
|
"""
|
|
|
|
if not self._xmlDoc:
|
|
return
|
|
result = {}
|
|
# first child is the root node, cmis:acl
|
|
for e in self._xmlDoc.childNodes[0].childNodes:
|
|
if e.localName == 'permission':
|
|
# grab the principal/principalId element value
|
|
prinEl = e.getElementsByTagNameNS(CMIS_NS, 'principal')[0]
|
|
if prinEl and prinEl.childNodes:
|
|
prinIdEl = prinEl.getElementsByTagNameNS(CMIS_NS, 'principalId')[0]
|
|
if prinIdEl and prinIdEl.childNodes:
|
|
principalId = prinIdEl.childNodes[0].data
|
|
# grab the permission values
|
|
permEls = e.getElementsByTagNameNS(CMIS_NS, 'permission')
|
|
perms = []
|
|
for permEl in permEls:
|
|
if permEl and permEl.childNodes:
|
|
perms.append(permEl.childNodes[0].data)
|
|
# grab the direct value
|
|
dirEl = e.getElementsByTagNameNS(CMIS_NS, 'direct')[0]
|
|
if dirEl and dirEl.childNodes:
|
|
direct = dirEl.childNodes[0].data
|
|
# create an ACE
|
|
if (len(perms) > 0):
|
|
ace = AtomPubACE(principalId, perms, direct)
|
|
# append it to the dictionary
|
|
result[principalId] = ace
|
|
return result
|
|
|
|
def getXmlDoc(self):
|
|
|
|
"""
|
|
This method rebuilds the local XML representation of the ACL based on
|
|
the :class:`ACE` objects in the entries list and returns the resulting
|
|
XML Document.
|
|
"""
|
|
|
|
if not self.getEntries():
|
|
return
|
|
|
|
xmlDoc = minidom.Document()
|
|
aclEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:acl')
|
|
aclEl.setAttribute('xmlns:cmis', CMIS_NS)
|
|
for ace in self.getEntries().values():
|
|
permEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission')
|
|
#principalId
|
|
prinEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principal')
|
|
prinIdEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:principalId')
|
|
prinIdElText = xmlDoc.createTextNode(ace.principalId)
|
|
prinIdEl.appendChild(prinIdElText)
|
|
prinEl.appendChild(prinIdEl)
|
|
permEl.appendChild(prinEl)
|
|
#direct
|
|
directEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:direct')
|
|
directElText = xmlDoc.createTextNode(ace.direct)
|
|
directEl.appendChild(directElText)
|
|
permEl.appendChild(directEl)
|
|
#permissions
|
|
for perm in ace.permissions:
|
|
permItemEl = xmlDoc.createElementNS(CMIS_NS, 'cmis:permission')
|
|
permItemElText = xmlDoc.createTextNode(perm)
|
|
permItemEl.appendChild(permItemElText)
|
|
permEl.appendChild(permItemEl)
|
|
aclEl.appendChild(permEl)
|
|
xmlDoc.appendChild(aclEl)
|
|
self._xmlDoc = xmlDoc
|
|
return self._xmlDoc
|
|
|
|
entries = property(getEntries)
|
|
|
|
|
|
class AtomPubACE(ACE):
|
|
|
|
"""
|
|
Represents an individual Access Control Entry.
|
|
"""
|
|
|
|
def __init__(self, principalId=None, permissions=None, direct=None):
|
|
"""Constructor"""
|
|
self._principalId = principalId
|
|
if permissions:
|
|
if isinstance(permissions, str):
|
|
self._permissions = [permissions]
|
|
else:
|
|
self._permissions = permissions
|
|
self._direct = direct
|
|
|
|
self.logger = logging.getLogger('cmislib.model.ACE')
|
|
self.logger.info('Creating an instance of ACE')
|
|
|
|
@property
|
|
def principalId(self):
|
|
"""Getter for principalId"""
|
|
return self._principalId
|
|
|
|
@property
|
|
def direct(self):
|
|
"""Getter for direct"""
|
|
return self._direct
|
|
|
|
@property
|
|
def permissions(self):
|
|
"""Getter for permissions"""
|
|
return self._permissions
|
|
|
|
|
|
class AtomPubChangeEntry(ChangeEntry):
|
|
|
|
"""
|
|
Represents a change log entry. Retrieve a list of change entries via
|
|
:meth:`Repository.getContentChanges`.
|
|
|
|
>>> for changeEntry in rs:
|
|
... changeEntry.objectId
|
|
... changeEntry.id
|
|
... changeEntry.changeType
|
|
... changeEntry.changeTime
|
|
...
|
|
'workspace://SpacesStore/0e2dc775-16b7-4634-9e54-2417a196829b'
|
|
u'urn:uuid:0e2dc775-16b7-4634-9e54-2417a196829b'
|
|
u'created'
|
|
datetime.datetime(2010, 2, 11, 12, 55, 14)
|
|
'workspace://SpacesStore/bd768f9f-99a7-4033-828d-5b13f96c6923'
|
|
u'urn:uuid:bd768f9f-99a7-4033-828d-5b13f96c6923'
|
|
u'updated'
|
|
datetime.datetime(2010, 2, 11, 12, 55, 13)
|
|
'workspace://SpacesStore/572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
|
|
u'urn:uuid:572c2cac-6b26-4cd8-91ad-b2931fe5b3fb'
|
|
u'updated'
|
|
"""
|
|
|
|
def __init__(self, cmisClient, repository, xmlDoc):
|
|
"""Constructor"""
|
|
self._cmisClient = cmisClient
|
|
self._repository = repository
|
|
self._xmlDoc = xmlDoc
|
|
self._properties = {}
|
|
self._objectId = None
|
|
self._changeEntryId = None
|
|
self._changeType = None
|
|
self._changeTime = None
|
|
self.logger = logging.getLogger('cmislib.model.ChangeEntry')
|
|
self.logger.info('Creating an instance of ChangeEntry')
|
|
|
|
def getId(self):
|
|
"""
|
|
Returns the unique ID of the change entry.
|
|
"""
|
|
if self._changeEntryId == None:
|
|
self._changeEntryId = self._xmlDoc.getElementsByTagNameNS(ATOM_NS, 'id')[0].firstChild.data
|
|
return self._changeEntryId
|
|
|
|
def getObjectId(self):
|
|
"""
|
|
Returns the object ID of the object that changed.
|
|
"""
|
|
if self._objectId == None:
|
|
props = self.getProperties()
|
|
self._objectId = CmisId(props['cmis:objectId'])
|
|
return self._objectId
|
|
|
|
def getChangeType(self):
|
|
|
|
"""
|
|
Returns the type of change that occurred. The resulting value must be
|
|
one of:
|
|
|
|
- created
|
|
- updated
|
|
- deleted
|
|
- security
|
|
"""
|
|
|
|
if self._changeType == None:
|
|
self._changeType = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'changeType')[0].firstChild.data
|
|
return self._changeType
|
|
|
|
def getACL(self):
|
|
|
|
"""
|
|
Gets the :class:`ACL` object that is included with this Change Entry.
|
|
"""
|
|
|
|
# if you call getContentChanges with includeACL=true, you will get a
|
|
# cmis:ACL entry. change entries don't appear to have a self URL so
|
|
# instead of doing a reload with includeACL set to true, we'll either
|
|
# see if the XML already has an ACL element and instantiate an ACL with
|
|
# it, or we'll get the ACL_REL link, invoke that, and return the result
|
|
if not self._repository.getCapabilities()['ACL']:
|
|
return
|
|
aclEls = self._xmlDoc.getElementsByTagNameNS(CMIS_NS, 'acl')
|
|
aclUrl = self._getLink(ACL_REL)
|
|
if (len(aclEls) == 1):
|
|
return AtomPubACL(self._cmisClient, self._repository, aclEls[0])
|
|
elif aclUrl:
|
|
result = self._cmisClient.binding.get(aclUrl.encode('utf-8'),
|
|
self._cmisClient.username,
|
|
self._cmisClient.password)
|
|
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
|
|
|