一个用于 Python 的 CMIS API 库,第 1 部分: cmislib 简介

一个客户端 Content Management Interoperability Services API

这是一个两部分系列文章的第 1 部分,该系列将介绍 cmislib —— 一个用于处理 CMIS 内容库的客户端库。Content Management Interoperability Services (CMIS) 规范提供访问内容的标准方式,不管底层的存储库实现如何或者选择了何种前端编程语言。本文使用一些例子来介绍用于 Python 的 cmislib API。

Jeff Potts, Sr. Practice Director, ECM, Optaros

Jeff Potts 是 Optaros 公司 Enterprise Content Management Practice 部门的高级主管,Optaros 是一家全球咨询公司,专注于组装以开源组件为特性的 Content、Commerce 和 Community 解决方案。Jeff 有超过 12 年的 ECM 实践领导经历,和超过 18 年的在 IT 部门和专业服务组织从事 IT 和技术实现的经验。Jeff 对封闭式和开源的内容管理平台都具有丰富的经验,最值得一提的是 Alfresco,他是该社区活跃的著名成员。2008 年,Jeff 编著了 Alfresco Developer Guide 一书(Packt Publishing),此书后来受到了 Packt 颁发的 2008 年度优秀作者奖。在这一年早些时候,Jeff 因其对项目的 cmislib B



2010 年 4 月 29 日

cmislib 简介:一个用于 Python 的客户端 CMIS API

对于 Content Management Interoperability Services (CMIS) 规范来说,这是一个忙碌的春天。OASIS 正在准备其 1.0 发布的说明,存储库供应商正在努力完成服务器端实现,内容管理社区的开发人员正在发布客户端程序和 API,以便使以标准方式探索和处理富内容存储库更容易。

常用缩写词

  • ACL:访问控制列表
  • API:应用程序程序接口
  • HTTP:超文本传输协议
  • OASIS:结构化信息标准促进组织
  • REST:具象状态传输
  • SDK:软件开发工具包
  • SQL:结构化查询语言
  • URL:统一资源定位符
  • WSDL:Web 服务描述语言
  • XML:可扩展标记语言

如果您曾经构建过以内容为中心的应用程序,就会知道,首先遇到的困难通常是解决如何与底层内容存储库通信的问题。您的团队首先在存储库的 SDK 中获得初步方案。然后您设计包含呈现层和内容服务层之间的集成的应用程序。最后,您按计划执行程序并买来点心庆祝成功。但是遗憾的是,除了点心会让您长胖之外,整个过程要针对前端和后端的每个新组合进行重复,因为每个存储库都具有自己惟一的 API。如果您的应用程序与多个存储库通信(通常都是这样的),那么您必须学习编码到多个接口。

幸运的是,这个问题早已经解决了。模式与 SQL 标准化之前存在的模式相同。IBM® 和其他公司创建的关系数据库在 20 世纪 70 年代早期开始出现,但是围绕 SQL 查询语言的第一个正式标准直到 1986 年才问世。一旦标准问世之后,尤其是 1992 年进行重大修订之后,开发人员就可以创建前端应用程序并合理地保证它可以针对多个关系后端进行工作。就像 SQL 为关系数据库应用程序所做的一样,CMIS 有潜力为以内容为中心的应用程序做相同的事情。它规定一种与后端交互的标准方式,不管底层的存储库实现如何或者选用何种前端编程语言。这次我们谈论的不是行和列,而是通常位于分层文件夹结构中的非结构化和半结构化内容 — 一般是某种文件。

图 1. CMIS 提供一个公共接口,不管前端或后端是否包含图像
图中展示了呈现层(有 PHP、Python、django)和内容服务层(有 Alfresco、FileNet)

本文介绍一个用于从 Python 处理 CMIS 存储库的客户端库,叫做 cmislib。cmislib 现在是 Apache Chemistry 项目的一部分,目标是让 Python 开发人员编写可以处理任何兼容 CMIS 的后端、以内容为中心的应用程序更为轻松。对于很多人来说,API 是直接理解 CMIS 威力的简单方式。

我们来看为何创建这个 API,它为您做些什么,它是如何开发的(这里给出了提示,以防您想要用自己喜欢的语言编写一个这样的 API),cmislib 实际发挥作用的一些简单例子。本系列的下一篇文章将详细介绍该库的一个实际应用。

创建 API 的动机

出于很多原因,用 Python 创建一个用于 CMIS 的客户端 API 似乎是一个好主意。一些原因是战略上的,理想化的;另外一些原因更加偏向于战术上的,是从自身出发的。我们首先来看归类为 “Greater Good” 下的原因。

遵从 CMIS 的供应商必须提供一个 Web 服务绑定和一个 RESTful Atom Publishing Protocol (AtomPub) 绑定。每种绑定相对于其他绑定来说都具有各自的优点,但是区别在于 CMIS 服务是如何跨不同的服务器被发现和调用的。Web 服务绑定包含一个可用于自动生成客户端代码的 WSDL 文件。如果愿意使用 Web 服务绑定,您可以利用 CMIS 服务器的 WSDL 生成自己的客户端 API。

另一方面,RESTful AtomPub 绑定则缺少描述服务的标准方式。由于是 restful,所以它所有的服务都通过 URL 访问,但是 CMIS 规范将特定的 URL 留给每个供应商。所以,如果您想要使用 RESTful AtomPub 绑定编写将跨所有遵循 CMIS 的提供商工作的代码,那么您在客户端还有一点工作要做。不是跨项目重复该工作,而是让一个开源项目为每个人做此工作。

下一个原因跟应用程序开发人员和内容存储库供应商的采纳有关。开发人员可以观察 webinars 和博客帖子以及 Twitter 流量,但是直到他们自己动手和亲眼看到流量值,否则 CMIS 只不过是又一个被大肆宣扬的词汇。如果软件仍旧置于包装盒中出售,那么您可以想象包装盒上鲜艳的 “爆炸式” 图像,图像中的广告词大声疾呼:“马上使用 CMIS 吧!”对于那些寻求拨开这些夸张宣传的层层迷雾,看看 CMIS 到底能够为他们及其应用程序做些什么的人来说,Python — 清洁、高产、便于安装 — 似乎是最佳选择。cmislib API 向开发人员提供一种直观、面向对象、互操作的工作方法,使他们免受实现细节的困扰。希望开发人员能够到 CMIS 中去 “观光”,喜欢他们的所见所闻,然后将 CMIS 作为以内容为中心的自定义应用程序与富内容存储库进行互操作的一种标准方式使用,而不管客户端是使用 Python 还是其他语言构建。

如果仅有一两个供应商采用 CMIS,则上述全部目标将无法实现。因此,那些从某个标准获益的人竭尽所能推动供应商采用该标准肯定是有意义的。cmislib 发行版包含了一些单元测试,这只是作为一项最佳开发实践。这个测试套件对于确保测试功能不会随着这个 API 的发展而退化很有帮助,同时也是以一种重复的方式来检验互操作性的便捷方法。但是,真正 “炫酷” 的地方在于这些单元测试的作用是作为面向供应商的一个测试套件。IBM、Alfresco™、OpenText 和 Nuxeo 都利用 cmislib 来发现了它们的实现的问题。这并不局限于使用由社区构建的各种 CMIS 工具和客户端来验证它们的工作的 cmislib 供应商,这是非常好的事!

“所有对一个和一个对所有(all-for-one-and-one-for-all)” 是极好的动机,而且几乎不需要编写一行代码。每个开源项目都发端于某个开发人员想要挠的一处 “瘙痒”。在这里,这处 “瘙痒” 起源于 Optaros™ 开发的一个内联网项目,其目的是为客户提供 Django®(一个基于 Python 的 Web 应用程序开发框架)和 Alfresco(一个开源内容管理平台)之间的集成。这个项目开发之时,OASIS 还几乎没有 CMIS 这个概念,这个集成在服务器端依赖 Alfresco Web Scripts(一个用于将您自己的 RESTful API 滚动到 Alfresco 的框架)通过 HTTP 在基于 Django 的呈现层和 Alfresco 存储库之间来回移动 XML。集成效果很好,但这是特定于 Alfresco 的。重构 Django 集成以利用 CMIS 似乎是一个好主意。我们选择首先使用 cmislib 作为一个低级 Python API,而不是使集成特定于 Django。这样做的好处是:通过利用 cmislib,除 Django 外,其他 Python 项目(比如 Zope® 和 Plone®)以及自定义 Python 应用程序都能够更轻松地与 CMIS 存储库集成。

最后一个出于自身的原因是关于开发人员生产力的。多数企业没有只需处理单个存储库这样的好运。通常,解决方案不能预期只会使用某个特定存储库,至少需要在开发过程中的某个时点上有可以切换的选项。CMIS 标准显然有助于解决这些问题,但实际上要高效地完成工作,则还需一些客户端库。其他一些项目正处于开发之中,以便为 CMIS 提供基于 Java™ 和 PHP 的客户端库。但 Python 在呈现层上也非常流行,因此一个面向 CMIS 的、基于 Python 的客户端库很重要。

这个 API 的功能

cmislib 的目标是抽象掉 CMIS 的底层实现细节。要在 CMIS 存储库之上构建解决方案,开发人员不想、也无需了解 CMIS 的工作方式。相反,cmislib 提供一个容易理解的对象模型,任何使用一个内容存储库或阅读 CMIS 说明的开发人员都能迅速熟悉这个模型。开发人员使用 Repositories、Folders、Documents 和 ACLs 等自然的内容管理概念,而不是集合、条目和提要。

如前所述,cmislib 使用 RESTful AtomPub 绑定与 CMIS 服务器通信。一个重要的问题是:要确保 cmislib 不具有关于它正在处理的后端存储库的特定于供应商的知识 —— 这个库将 CMIS 供应商当作一个 “黑匣子”。当您使用 cmislib 连接到一个 CMIS 供应商时,需要向该库提供那个 CMIS 供应商的入口点(或者是服务 URL)和一些凭证。这个库通过询问服务器响应来确定如何进一步与服务器交互。

例如,假设您想获取一列当前被签出的文档。CMIS 规范将告知您:

  • 存在一个签出文档集合。
  • 调用 getCheckedOutDocs 服务时,repositoryId 是一个必要参数。
  • 可以传入几个可选参数,大部分用于处理结果集分页。
  • 来自服务的响应将是一个 Atom 提要。

但是,规范没有告诉您用于检索这个集合的确切 URL,这留给供应商处理。cmislib 的用途之一就是确定那个 URL 以及如何将响应转换为可以以 Python 方式使用的 Python 对象。清单 1 展示了这个交互在 Python shell 中的外观:

清单 1. 列示一个存储库中签出的文档
>>> rs = repo.getCheckedOutDocs()

>>> len(rs)
2
>>> for doc in rs:
...     doc.title
... 
u'Response XML (Working Copy)'
u'd1 (Working Copy)'

类似地,假设您想在一个文档上执行一个签出操作。CMIS 规范将告知您:要执行一个签出操作,您需要发布一个 Atom 条目到 checkedout 集合,存储库将返回一个 Atom 条目,该条目表示刚刚签出的对象的 Private Working Copy (PWC)。如果您使用 cmislib,则无需担心如何确定那个集合,如何构建和发布 Atom 条目 XML,以及如何处理 XML 响应。相反,您只需调用对象上的 checkout 方法,cmislib 将返回一个表示该 PWC 的 Document 对象。清单 2 展示了这个交互:

清单 2. 签出一个文档
>>> doc.title
u'testdoc'
>>> pwc = doc.checkout()
>>> pwc.title
u'testdoc (Working Copy)'
>>> doc.checkedOut
True

开发和测试方法

有一点很重要:cmislib 的编写应该尽可能遵循 CMIS 规范。因此,第一步是并排打开 Eclipse 和 CMIS 规范,标出(stub out)类和方法。我将对规范的交叉引用添加到行内(in-line)注释中,以便稍后我返回来实现该方法时能够迅速发现规范内的适用点。

我从一开始就建立起构建流程、文档和源代码控制。尽早建立这些内容很重要,这样其他开发人员才能快速加入项目并进入角色。

代码以迭代方式演变。每次迭代都首先为新功能编写单元测试,然后是实际编码,最后是通过单元测试。我从一些基础功能开始,比如查询存储库以了解它的功能,检索对象和对象属性来验证常规方法。然后,我以此为基础继续进行,开始全面编写操作、签出/签入、关系和 ACLs。

一开始测试很困难,因为不存在参考实现(那时 Apache Chemistry 的参考实现中的 AtomPub 绑定是只读的)且供应商还在开发他们的实现。Alfresco 是 CMIS 的一个早期采用者,当时拥有最成熟的实现,因此我从那里开始。当大多数单元测试都针对 Alfresco 顺利完成后,我就开始针对已经公开发布 CMIS 实现的其他供应商进行测试。IBM 慷慨地主动公开他们的实现。添加这个实现使我大开眼界,但这对于 cmislib 和所有相关供应商都是一个极好的练习。我们在客户端和服务器端都发现了一些问题,如果没有这种互操作性测试,我们不可能那么快地发现问题。

如果您正在开发 cmislib 这样的 CMIS 工具或 APIs,重要的是要针对尽可能多的不同服务器进行测试。CMIS 规范是全新的且供应商实现还不够成熟,即使那些宣称完全遵守规范草案的供应商实现也是如此。已发现的常见问题分为以下三类:

  • 不完整的实现。CMIS 还非常新,发现以下情况很正常:缺失的服务(ACL、关系、策略以及当时似乎最不支持的更改日志),还没有收到支持的强制特性(例如,没有提供的强制集合和链接),以及硬编码的值。
  • 对规范的不同解释。CMIS 规范是一个编写良好、容易阅读的文档,但有些内容仍然需要解释。例如,在草案 6 之前,checkedout 集合的内容比较模糊。该集合包含的是签出对象的 Private Working Copies (PWCs) 还是签出对象本身?不同的供应商对这一点的解释都不同,并根据它们的解释来实现。那个特定问题已经解决(PWCs 是正确答案,如果您感到好奇的话),但您可以从中看出那可能会使编写一个互操作的客户端出现困难。
  • 不良假设。某个供应商对 CMIS 规范的扩展有时明显,有时不明显。如果您针对一个服务器编码您的 API 并将其视为参考实现,那么您就等于做出了这样一个假设:其他实现将以相同的方式运行。现在的问题是:还没有一个带有 AtomPub 绑定的 CMIS 参考实现是功能齐备且 100% 遵守 CMIS 规范的。

几个示例

本系列下一篇文章将通过详细介绍一个 Python 脚本来展示 cmislib 库的实际应用,该脚本用于批量加载一个包含数字资产和元数据的 CMIS 存储库。下面的基本示例来自 cmislib 文档,用于展示如何使用这个 API 从 Python 交互式 shell 执行一些常见操作,其中包括:获取存储库相关信息,使用 Folders 和 Documents,以及通过 CMIS 查询、路径、对象 ID 或关系查找对象。

获取一个存储库对象

对于使用 CMIS 存储库能够完成的任务来说,CmisClient 和 Repository 对象是共同的出发点。获取一个实例很简单 —— 只需知道存储库的服务 URL 和凭证。操作步骤如下:

  1. 在命令行中输入 python 并按下 Enter 键,启动 Python shell。
  2. 导入 CmisClient:
    >>> from cmislib.model import CmisClient
  3. 将 CmisClient 指向存储库的服务 URL:
    >>> client = CmisClient('http://cmis.alfresco.com/s/cmis', 'admin', 'admin')
  4. 存储库都拥有一个 ID — 如果您知道这个 ID,也可以通过 ID 获取存储库。在本例中,我们将询问客户端以获取默认存储库 ID。
    >>> repo = client.defaultRepository
    >>> repo.id u'83beb297-a6fa-4ac5-844b-98c871c0eea9'
  5. 现在可以获取存储库的属性了。下面的 for 循环将列示 cmislib 知道的关于存储库的所有信息(为了更加简洁,我截短了输出清单)。
    >>> repo.name u'Main Repository'
    >>> info = repo.info
    >>> for k,v in info.items():
        ...     print "%s:%s" % (k,v)
        ...     
        cmisSpecificationTitle:Version 1.0 Committee Draft 04
        cmisVersionSupported:1.0
        repositoryDescription:None
        productVersion:3.2.0 (r2 2440)
        rootFolderId:workspace://SpacesStore/aa1ecedf-9551-49c5-831a-0502bb43f348
        repositoryId:83beb297-a6fa-4ac5-844b-98c871c0eea9
        repositoryName:Main Repository
        vendorName:Alfresco
        productName:Alfresco Repository (Community)

使用 Folders 和 Documents

获取 Repository 对象之后,现在可以开始使用存储库中的对象,比如 Folders 和 Documents。

  1. 在 root 中创建一个新文件夹。如果您一直在跟随我们的操作,那么为您的文件夹提供一个独特的名称,如果要针对一个公共存储库进行测试的话。
    >>> root = repo.rootFolder
    >>> someFolder = root.createFolder('someFolder')
    >>> someFolder.id 
    u'workspace://SpacesStore/91f344ef-84e7-43d8-b379-959c0be7e8fc'
  2. 然后,您可以创建一些内容:
    >>> someFile = open('test.txt', 'r')
    >>> someDoc = someFolder.createDocument('Test Document', contentFile=someFile)
  3. 如果愿意,可以转储新创建的文档的属性(以下是部分清单):
    >>> props = someDoc.properties
    >>> for k,v in props.items():
    ...     print '%s:%s' % (k,v)
    ...
    cmis:contentStreamMimeType:text/plain
    cmis:creationDate:2009-12-18T10:59:26.667-06:00
    cmis:baseTypeId:cmis:document
    cmis:isLatestMajorVersion:false
    cmis:isImmutable:false
    cmis:isMajorVersion:false
    cmis:objectId:workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479

获取对象

可以通过以下几种方式来获取一个对象:

  • 运行一个 CMIS 查询。
  • 请求存储库提供一个对应特定路径或对象 ID 的对象。
  • 使用一个文件夹的 “子” 文件夹和/或 “后代” 文件夹遍历存储库。
  • 获取通过一个关系绑定在一起的源对象和目标对象。

下面一些示例展示了如何使用这些选项:

  1. 通过全文本搜索查找上一小节中创建的文档。
    >>> results = repo.query("select * from cmis:document where contains('test')")
    >>> for result in results:
    ...     print result.name
    ...
    Test Document2
    Example test script.js
  2. 或者,也可以通过对象的路径获取对象,例如:
    >>> someDoc = repo.getObjectByPath('/someFolder/Test Document')
    >>> someDoc.id 
    u'workspace://SpacesStore/2cf36ad5-92b0-4731-94a4-9f3fef25b479'
  3. 或者使用对象 ID,例如:
    >>> someDoc = repo.getObject('workspace://SpacesStore/2cf36ad5-94a4-9f3fef25b479')
    >>> someDoc.name 
    u'Test Document'
  4. Folder 对象拥有 getChildren() 和 getDescendants() 方法,用于返回一个可分页的结果集:
    >>> children= someFolder.getChildren()
    >>> for child in children:
    ...     print child.name
    ...
    Test Document
    Test Document2
  5. Folders 和 Documents 都有一个 getRelationships()方法,用于返回一组 Relationship 对象。Relationship 对象能够在关系的每一端提供源对象和目标对象。
    >>> rel = testDoc.getRelationships(includeSubRelationshipTypes='true')[0]
    >>> rel.source.name
    'testDoc1'
    >>> rel.target.name
    'testDoc2'
    >>> rel.properties['cmis:objectTypeId']
    'R:sc:relatedDocuments'

在本系列的下一篇文章中,您将看到如何使用这个 API 的其他部分,包括检索对象类型定义的能力。


结束语

本文简要介绍了 cmislib,它从何而来,它能做什么,以及一些基本例子。希望本文激发了您对 CMIS 的兴趣,并打算深入了解它。如果您对 Python 感兴趣,不妨好好了解 cmislib。参考资料 部分提供了一个链接。如果没兴趣,可探究这里列出的其他工具和客户端库。最后,CMIS 社区需要您,您可以从很多方面帮助该社区:

  • 如果没有用您喜欢的语言编写的客户端库,您就编写一个作为开源项目。
  • 帮助您的存储库供应商测试他们的 CMIS 实现。
  • 为您喜欢的门户或呈现框架编写集成,以使之容易处理 CMIS 库。
  • 努力钻研现有开源 CMIS 项目(比如 cmislib)和 Apache Chemistry。
  • 通过加入 OASIS Technical Committee,为 CMIS 规范贡献力量。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, SOA and web services
ArticleID=486195
ArticleTitle=一个用于 Python 的 CMIS API 库,第 1 部分: cmislib 简介
publish-date=04292010