python开发工具:一个用于 Python 的 CMIS API 库 第 2 部分: 使用 Python 和 cmislib 构建真正的 ECM 工具

  结合 Python 和 CMIS

  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 


Tags:  pythonapichm python工具 pythonapi python开发工具

延伸阅读

最新评论

发表评论