Python 和 CMIS 联合工作时表现更好要简要了解 Oasis Content Management Interoperability Services (CMIS) 规范标准和 cmislib请参阅本系列第 1 部分
背景知识
这是本系列第 2篇文章包含 3 个主要部分:
常用缩写词
API:应用编程接口
ECM:企业内容管理
IDE:集成开发环境
OASIS:结构化信息标准促进组织
PDF:可移植文档格式
REST:具象状态传输
URL:统资源定位符
XML:可扩展标记语言
Python 和 CMIS — 本部分简要介绍并讨论 Python 为何是编写 CMIS 相关工具理想语言
代码详解 — 本部分详细解释源代码各部分如何相互配合以便您可以针对其他类型元数据和源轻松扩展它
运行工具 — 本部分探索这个工具运行时特征以及如何设置依赖项如果您对该工具如何形成以及怎样工作解释不感兴趣只想下载并使用它那么可以直接跳到 运行工具 部分
通过工具构建学习门新语言
在我开始撰写本文个月的前我正在寻找个能够用于自学 Python 小项目对我而言在需要学习门新语言时我可以从头到尾阅读本教科书但周的后我就会完全忘记书里讲了些什么我永远也不会真正了解那种语言及其相关工具将如何为我服务如果不使用那种语言做些有用事该语言相关概念就不会在我记忆中得到巩固碰巧最近我和 Jeff Potts 起合作以便解决他新 CMIS Python 库(cmislib)和 IBM CMIS 技术预览服务器的间些互操作性问题这使我有机会研究如何为长期存在系统构建工具问题
长期存在系统工具
首先我将通过多年来直近距离接触个例子来解释什么是 “长期存在系统”旦部署ECM 系统就有可能在个公司或部门运行很长段时间替换它们可能会是项艰巨、复杂且代价高昂任务这就是如果它们表现良好就不要改动它们原因所在因此这些系统往往能够在多次合并和并购中幸免长期生存下来因此当我谈论 CMIS 存储库时从定义上来说我谈论往往也是这些系统长期存在系统积累了些非常成熟由用户和管理员创建工具我认为结合了 cmislib 库 Python 脚本有潜力这样存在下去但愿您在看到这两种技术联合使用个简单却强大举例的后能够同意我观点
为何使用标准解释语言?
需要为这些系统开发工具时比较让人放心做法是运行经过解释脚本我猜想这对我而言是控制感觉假设您拥有个用 Java™ 或 C 语言编写工具且需要修复和更改些地方这并不总是项轻松工作即使您拥有源代码您是否多次尝试过重新编译多年以前编写代码最后却发现需要某个特殊构建环境、某些库以及些没有记录、早就忘记了设置?不错大多数开发人员肯定会备份他们源代码但是备份构建环境要困难得多通常只能在更严格维护环境中才能完成这个问题通常不属于工具领域尽管它很重要但如果使用脚本构建环境可能只包含个常用文本编辑器和现成运行时(注意:我并非建议在进行重要 Python 编程时不使用个良好 Python IDE比如带有 Pydev Eclipse)
当我得知以后可以将我脚本迁移到另种类型操作系统并且行为不会改变时我此前提到过控制感甚至更加强烈了如果我确信自己能够这样做那么我更有可能花费时间来编写个更好工具我知道今后不必针对另个平台重新编写它只要能够得到某个指定平台解释我就不必担心而且只要这种语言像 Python 样拥有大量流行支持可用性就不成问题这些天我进行开发时在 Microsoft® Windows® 和 Linux® 的间频繁切换我甚至不知道两年以后我将使用哪个系统这就是我们需要研究问题以及最近我为何非常喜欢 Python 原因
选择要解决问题
我在 IBM 强化 CMIS 服务器时需要用到个工具是个不错存储库填充工具当然CMIS 存储库中数据并不仅仅是文档负载还可能包括很多和该文档相关元数据开发人员都经常使用种带有元数据常见文档类型就是 JPG 图像我的所以选择 JPG 文件用于测试原因是它们头部通常拥有组丰富有趣元数据这意味着我不必额外编写代码来表达虚构值这种 EXchangeable Image File Format (Ex) 数据对那些涉足数码摄影人来说并不陌生如果您还不熟悉这种格式建议您先参阅有关这个主题 Wikipedia 文章
工具要求
您即将创建工具需要完成以下任务:
将本地文件系统中文件层次结构复制到任何指定 CMIS 兼容存储库并将文件文件名保留为新 CMIS 文档 cmis:name
如果这个工具在 xcopy 期间遇到 JPG 类型文件还要尝试将和图像关联所有 Ex 数据复制到存储库中前提是假设存储库包含兼容属性定义这是这个工具真正有趣地方尽管 xcopy 功能本身非常有用但是用篇文章专门介绍它可能有些单调乏味(虽然您可以将这个工具只用作个简单旧文件系统到 CMIS xcopy如果这是您需要这个工具惟原因话)
代码详解
现在您可以查看这个工具参数和代码了
定义输入
现在我们来定义这个工具将被如何使用首先我根据原来 xcopy 建模该工具以便它将是个命令行工具参数是:
-s 复制源目录
-f 文件过滤器(例如*.doc、*.jpg、*.* 等)
-t target path 复制操作目标目录完整路径(例如:/pictures/Fiji_August_2010/)这个工具将假定目标路径存在并自动创建所有子文件夹
serviceURL 目标 CMIS 存储库 XML 服务文档完整 URL(例如:http://localhost:9080/cmis/service)
targetClassName 用于指定将为新文档创建 cmis:document 子类可选类类型
例如位摄影师内容管理系统可能拥有个名为 CmisJpg 类这个 CmisJpg 类包含某些常用 Ex 值属性定义这位摄影师可能会在搜索她图像目录时查询这些值如果这个参数未指定这个工具将把所有文档创建为 cmis:document 类型
debug 调试模式(可选)
参数值为 true 时调试模式只会尝试重新创建目标目录结构但不会复制任何文档
如果省略默认值为 false(复制所有数据)
代码
现在我将逐步介绍这个工具代码些重要部分描述它们是如何工作注意为简便起见我不会详细介绍所有代码但是我跳过部分都比较简单是不言而喻要获取整个文件(和注释)请参见 下载 部分下面是我将讨论项目高级列表:
将 6 个运行时参数读入工具
化 cmislib 库并获取个存储库对象该对象将用作和 CMIS 存储库所有通信根
验证目标文件夹和目标类定义有效并存在于目标库中
审查基本 xcopy 逻辑
从 JPG 文件读入 Ex 头部数据
使用 cmislib 创建个带有元数据文档
基于类型将 Ex 数据动态填充到目标类属性中
步骤 1:解析参数
首先需要在运行时将这 6 个参数(参见 定义输入)传入工具这里我决定将它们分割为两个类别第类是我希望在命令行上传入值(我不想在命令行上指定所有 6 个参数它们中半对于个给定存储库不会有太多改变)由于我是根据 xcopy 建模所以我将只接受前 3个参数(源、目标和过滤器)其他 3个参数如何办?对于它们我将使用个 .cfg 文本文件它们对于个给定存储而言是静态将这个配置文件放置到脚本所在目录中并将其命名为 cmisxcopy.cfg
清单 1. 样例 cmisxcopy.cfg 文件
[cmis_repository]
# service url for the repository that you will be copying to
serviceURL=http://localhost:8080/p8cmis/resources/DaphneA/Service
# TARGET CLASS
# the cmis:objectTypeId of the that you wish to create
targetClassName=cmis:document
# DEBUG MODE
debug=false
#USER CREDENTIALS
user_id=admin
password=password
标准 Python 库 ConfigParser 将有效地读取这个配置文件数据如 清单 2 所示:
清单 2. 使用 ConfigParser 读取配置文件值
import ConfigParser
# config file related constants
configFileName = 'cmisxcopy.cfg'
cmisConfigSectionName = 'cmis_repository'
# read in the config values
config = ConfigParser.RawConfigParser
config.read(configFileName)
try:
UrlCmisService = config.get(cmisConfigSectionName, "serviceURL")
targetClassName = config.get(cmisConfigSectionName, "targetClassName")
user_id = config.get(cmisConfigSectionName, "user_id")
password = config.get(cmisConfigSectionName, "password")
debugMode = config.get(cmisConfigSectionName, "debug")
except:
pr "There was a problem finding the config file:" + configFileName + \
" or one of the tings in the [" + cmisConfigSectionName + "] section ."
sys.exit
要实现这个目种更简单思路方法是额外使用个 config.py 文件其中只包含这 3个常量然后主脚本只需导入这个 config.py 并直接使用那些变量即可下面我将解释命令行解析
清单 3 展示了如何使用这个 Python 库中 optparse 来进行其他 3个参数命令行解析设置 usage 串来显示个提示以免提交无效参数使用 add_option 思路方法分别为源、目标和过滤器添加 -s、-t 和 -f 参数最后执行个 parse_args 将这些值序列化到您 options 对象中
清单 3. 使用 optparse 收集命令行参数
from optparse import OptionParser
usage = "usage: %prog -s sourcePathToCopy -t targetPathOnRepository
-f fileFilter(default=*.*)"
parser = OptionParser(usage=usage)
## get the values for source and target from the command line
parser.add_option("-s", "--source", action="store", type="", dest="source",
help="Top level of local source directory tree to copy")
parser.add_option("-t", "--target", action="store", type="", dest="target",
help="path to (existing) target CMIS folder. All children will be created
during copy.")
parser.add_option("-f", "--filter", action="store", type="", dest="filter",
default="*.*", help="File filter. e.g. *.jpg or *.* ")
(options, args) = parser.parse_args
startingSourceFolderForCopy = options.source
targetCmisFolderStartingPath = options.target
步骤 2:使用 cmislib 化您 CMIS 连接
清单 4 最终开始执行些真正 CMIS 工作首先必须导入 cmislib 模块首先使用此前(在清单 2 和 3 中)获取值化客户端对象;然后获取 defaultRepository 对象 repo接下来使用 getObjectByPath(path) 尝试获取目标文件夹(注意cmislib 能够帮助您轻松获取这个对象就像从这个标准 Python 库获取个本地文件夹对象样)如果这个操作由于某种原因失败(比如指定文件夹不存在)则这个任务将失败并显示条恰当消息那么您需要使用 getTypeDefinition 对目标类型定义执行个类似健全性检查
拥有这两个有效 cmislib 对象后您知道您拥有和目标系统相关所有信息都是正确因此可以继续进行处理注意用于化 folderCacheDict dictionary 对象那行如果稍后再次需要这个文件夹对象可以从这个缓存Cache获取它而不是再往返次去获取它注意这个缓存Cache对于您使用特定遍历算法并不真正需要我在这里使用它只是为了展示当您将来需要扩展这个工具时应该怎样做
清单 4. 化 CmisClient 并获取目标文件夹和目标类对象
from cmislib.model import CmisClient
# initialize the client object based on the passed in values
client = CmisClient(UrlCmisService, user_id, password)
repo = client.defaultRepository
# test to see the target folder is valid
targetCmisLibFolder = None
try:
targetCmisLibFolder = repo.getObjectByPath(targetCmisFolderStartingPath)
except:
# terminate we can't get a folder object
pr "The target folder specied can not be found:" + targetCmisFolderStartingPath
sys.exit
# initialize the folder cache with the starting folder
folderCacheDict = {targetCmisFolderStartingPath : targetCmisLibFolder}
# test to see the target type is valid
targetTypeDef = None
try:
targetTypeDef = repo.getTypeDefinition(targetClassName)
except:
# terminate we can't get the target type definition object
pr "The target type specied can not be found:" + targetClassName
sys.exit
步骤 3:实现基本 xcopy 逻辑
在这个步骤中您将遍历源文件系统树您将寻找需要复制文件在目标文件系统中创建任何必要子目录以便复制后层次结构能够匹配要遍历源目录结构需要使用 Python 模块 os 中 walk 思路方法这将为源文件系统树中每个目录返回个 “ 3元组”(dirname、dirs 和 files)您将使用这个 3元组来供给您 processDirectory 思路方法(参见 下载 中完整清单)然后processDirectory 继续创建目标目录(如果还不存在)并传递到 copyFilesToCmis 思路方法以将这些文件实际复制到新创建目标文件夹中这个思路方法将迭代接收到每个文件过滤出没有请求文件并获取 .jpg 文件 Ex 数据我们还将在稍后讨论元数据时深入介绍 copyFilesToCmis 思路方法
步骤 4:读取 Ex 数据
对于遇到每个类型为 JPG 文件您需要提取所有 Ex 值以便它们可以保留在目标对象中因此当您需要读取 JPG 头部(Ex)数据时有很多思路方法可以解决这个问题这里我不想自己编写个思路方法已经有几个现成库我选择使用 ex-py它以种非常常见方式返回标记:个 “键/值” 对字典其中所有值都是串我认为如果您想在这里使用些更 “洋气”(或更自定义)库来替换 ex-py您可以轻易做到这点我料想大多数库都使用个字典来表示属性集合即便不是您也可以轻松地将它们调整为这样做完成这个任务实际代码非常简单如 清单 5 所示
清单 5. 从个 JPG 文件读取 Ex 数据
import EXIF
def getExTagsForFile(filename):
f = open(filename, 'rb')
tags = EXIF.process_file(f)
tags
步骤 5:带有元数据文档创建
在这个步骤中您需要使用列属性在目标存储库中创建个文档这些属性必须设置以便 CMIS 存储库确切知道要创建什么例如在 CMIS 中当您将个新文档 POST(在逻辑上表示创建)到个文件夹时正是对象上属性列表告知 CMIS 要例子化什么您意思是要创建 cmis:document 个例子呢还是想要名为 CmisJpg 文档个子类呢?这个信息正是通过属性列表来进行通信
我原以为在这样个工具中元数据将是使事情变得复杂(需要更多代码)地方但是我惊喜地发现只需很少代码就可以实现这种类型映射我要向 Jeff Potts(cmislib 作者)脱帽致敬是他使这切如此轻松!
对于将在目标文件夹中创建每个文档将 createCMISDoc即 清单 6 中外层思路方法作为最后个参数传入 propBag 是从 getExTagsForFile 思路方法获取 Ex 标记列表您对 createPropertyBag 执行个以便设置 cmis:objectTypeId 属性(这指定要创建对象类型)并将所有标记处理到类型适当对象中这些对象将匹配那个特定属性目标存储库定义最后目标文件夹对象中实际文档创建只需行代码:Doc = folder.createDocument(…)
清单 6. createCMISDoc 思路方法
def createCMISDoc(folder, targetClass, docLocalPath, docName, propBag):
"""
Create document in CMIS repository in the folder specied.
Create the document of type targetClass
Take stream for this document from docLocalPath
Set the name of the document to be docName
Set the properties on the object using the propBag
"""
def createPropertyBag(sourceProps, targetClassObj):
"""
Take the ex tags and a props collection to submit
on the doc create method
"""
# the object id first.
propsForCreate = {'cmis:objectTypeId':targetClassObj.id}
for sourceProp in sourceProps:
# First see there is a matching property by display name
(sourceProp in targetClassObj.propsKeyedByDisplayName):
# there’s a matching property in the repo's type !
pr "Found matching metadata: " + sourceProp
# now make the data fit
addPropertyOfTheCorrectTypeToPropbag(propsForCreate,
targetClassObj.propsKeyedByDisplayName[sourceProp],
sourceProps[sourceProp] )
propsForCreate
props = createPropertyBag(propBag, targetClass)
f = open(docLocalPath, 'rb')
Doc = folder.createDocument(docName, props, contentFile=f)
pr "Cmislib create ed id=" + Doc.id
f.close
步骤 6. 到目标文档动态元数据映射
如 清单 7 所示本文最后个思路方法用于处理动态元数据映射:从 Ex 数据中属性映射到为目标文档类定义属性这个思路方法是 addPropetyOfThecorrectTypeToPropbag;我想说是我喜欢这种具有描述性名称这个思路方法将完成整个脚本中最复杂工作但是如您所见它非常简单这要归功于 cmislib 作用例如如果 valueToAdd(在来自 ex-py 时总是个串)包含值 56 且 typeObj 属性类型是 那么您将它转换为个适当 对象并设置这个值如果目标存储库认为它应该是个串那么就不用转换它如果转换没有效果(比如这个值包含 f2.0)那么转换将失败并跳过这个属性但文档仍将创建因此不管您在目标 CMIS 存储库中如何设置属性定义这个代码将尝试使其有效
清单 7. addPropertyOfTheCorrectTypeToPropbag 思路方法
def addPropertyOfTheCorrectTypeToPropbag(targetProps, typeObj, valueToAdd):
"""
Determine what type 'typeObj' is, then convert 'valueToAdd'
to that type and it in targetProps the property is updateable.
Currently only supports 3 types: , eger and datetime
"""
cmisUpdateability = typeObj.getUpdatability
cmisPropType = typeObj.getPropertyType
cmisId = typeObj.id
(cmisUpdateability "readwrite"):
# first lets handle types
(cmisPropType ''):
# this will be easy
targetProps[cmisId] = valueToAdd
(cmisPropType 'eger'):
try:
Value = (valueToAdd.values[0])
targetProps[cmisId] = Value
except: pr "error converting property id:" + cmisId
(cmisPropType 'datetime'):
try:
dateValue = valueToAdd.values
dtVal = datetime.datetime.strptime(dateValue ,
"%Y:%m:%d %H:%M:%S")
targetProps[cmisId] = dtVal
except: pr "error converting datetime property id:" \
+ cmisId
最新评论