本文介绍了在 IBM DB2 Content Manager 当中,如何使用管理 API 来创建工作流程。通过本文的学习,读者可以了解如何通过管理 API 在 IBM DB2 Content Manager 当中创建复杂的和系统管理客户端兼容的工作流程。
IBM DB2 Content Manager 的 SDK 提供了丰富的 API 供开发人员使用。从功能上可以把 API 划分为两大类:一类是用于开发和内容管理业务相关的前台应用;例如:把文档导入到 Content Manager 当中,或者通过某些属性在内容管理服务器当中查找符合条件的数据等等。另外一类是用于开发和内容管理相关的后台系统管理和数据建模;例如:创建项类型 (ItemType),创建权限集合,增加新用户等等。
本文的内容放在了如何使用 Java 语言来进行系统管理的开发方面。
要调用和运行这些系统管理 API,只要在自己的集成开发环境或者 CLASSPATH 环境变量当中引入 cmbsdk81.jar 这个文件就可以,对于 windows 系统,其路径一般在“X:\Program Files\IBM\db2cmv8\lib”目录当中。对于 Linux 系统,其路径一般在“/opt/IBM/db2cmv8/lib”目录当中。
使用系统管理 API 可以为 Content Manager 服务器完成认证,授权,数据建模和文档路由,这四个方面的系统管理方面的任务。
- 认证方面的系统管理包括:管理用户和管理用户组。
- 授权方面的系统管理包括:管理特权集合和管理访问控制列表等。
- 数据建模方面的系统管理包括:管理属性,管理项类型等。
- 文档路由方面的系统管理包括:管理工作节点,管理流程和管理工作列表等等。
下面的一些代码示例,依次介绍了如何通过管理 API 来完成这些系统管理的任务。
首先是如何和 Content Manager 服务器建立连接,这是所有 API 操作的前提。
清单 1.创建 Content Manager 连接的代码示例
// 创建对象 DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 设置 Content Manager 服务器的名称,用户名称,登录密码 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 执行相关的操作 …… // 断开连接 dkDatastore.disconnect(); |
下面是如何通过 API 来在 Content Manager 服务器当中创建一个新的用户组。和服务器管理相关的类是 DKDatastoreAdminICM。
清单 2.创建用户组的代码示例
import com.ibm.mm.sdk.common.DKDatastoreAdminICM; import com.ibm.mm.sdk.common.DKException; import com.ibm.mm.sdk.common.DKUserGroupDefICM; import com.ibm.mm.sdk.common.DKUserMgmtICM; import com.ibm.mm.sdk.server.DKDatastoreICM; public class CreateUserGroup { public static void main(String[] args) throws DKException, Exception { DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 创建连接 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 创建服务器管理对象 DKDatastoreAdminICM dkAdmin = new DKDatastoreAdminICM(dkDatastore); // 创建用户管理对象 DKUserMgmtICM userMgmt = (DKUserMgmtICM)dkAdmin.userManagement(); // 创建用户组定义对象 DKUserGroupDefICM userGroup = new DKUserGroupDefICM(dkDatastore); // 设置新创建的用户组的名称 userGroup.setName("NewUserGroup"); // 在 Content Manager 当中创建新的用户组 userMgmt.add(userGroup); // 断开连接 dkDatastore.disconnect(); } } |
在 Content Manager 种使用 API 创建访问控制列表的时候,访问控制列表必须指定是用户级别还是用户组级别,当然也可以为两者同时指定。和访问控制列表管理相关的类是 DKAccessControlListICM。这个类也是通过服务器管理类 DKDatastoreAdminICM 来创建。
清单 3. 创建访问控制列表代码清单
import com.ibm.mm.sdk.common.DKACLData; import com.ibm.mm.sdk.common.DKAccessControlListICM; import com.ibm.mm.sdk.common.DKAuthorizationMgmtICM; import com.ibm.mm.sdk.common.DKConstant; import com.ibm.mm.sdk.common.DKDatastoreAdminICM; import com.ibm.mm.sdk.common.DKException; import com.ibm.mm.sdk.common.DKSequentialCollection; import com.ibm.mm.sdk.server.DKDatastoreICM; public class CreateACL { public static void main(String[] args) throws DKException, Exception { DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 创建连接 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 创建服务器管理对象 DKDatastoreAdminICM dkAdmin = new DKDatastoreAdminICM(dkDatastore); // 创建授权管理对面 DKAuthorizationMgmtICM authMgmt = new DKAuthorizationMgmtICM(dkDatastore); // 创建访问控制列表对象 DKAccessControlListICM aclList = (DKAccessControlListICM) authMgmt .createAccessControlList(); // 为 ACL 设置名称 aclList.setName("NewACL"); // 设置 ACL 的描述信息,这项内容是可选的 aclList.setDescription("ACL for test"); // 创建集合对象 DKSequentialCollection dkCollection = new DKSequentialCollection(); // 为访问控制列表设置管理域 dkCollection.addElement(dkAdmin.adminDomainsMgmt().retrieve("SuperDomain")); aclList.setAdminDomains(dkCollection); // 创建访问控制列表数据对象 DKACLData dkACLData = new DKACLData(); dkACLData.setPatronType(DKConstant.DK_CM_USER_KIND_GROUP); // 设置访问控制列表所绑定的应户组名称 dkACLData.setUserGroupName("NewUserGroup"); // 为访问呢控制列表设置权限集合 dkACLData.setPrivilegeSet(authMgmt.retrievePrivilegeSet("AllPrivs")); // 保存 ACL 数据 aclList.addACLData(dkACLData); // 创建访问控制列表对象 authMgmt.add(aclList); dkDatastore.disconnect(); } } |
在 Content Manager 当中,构成工作流程的基本内容是工作节点 (WorkNode)。这些工作节点包括:起始节点,终止节点,普通节点,分割节点,连接节点,集合节点,决策节点,子流程节点和业务应用节点。其中起始节点和终止节点是在系统当中默认存在的,不需要通过 API 来创建。其他类型的工作节点可以通过 API 进行创建。在 API 当中,分别为可以操作的工作节点定义了类型常量。
- 普通节点 (Regular Node) DK_ICM_DR_WB_NODE_TYPE
- 分割节点 (Split Node) DK_ICM_DR_SPLIT_NODE_TYPE
- 连接节点 (Join Node) DK_ICM_DR_JOIN_NODE_TYPE
- 集合节点 (Collection Node) DK_ICM_DR_WB_NODE_TYPE
- 决策节点 (Decision Point) DK_ICM_DR_DP_NODE_TYPE
- 子流程节点 (Sub-Process) DK_ICM_DR_SUB_PROCESS_NODE_TYPE
- 业务应用节点 (Business Application Node) DK_ICM_DR_BA_NODE_TYPE
其中对于决策节点会涉及到内部和外部规则的转换,转换的方式会在随后的内容当中涉及到。
清单 4. 创建工作节点代码清单
import com.ibm.mm.sdk.common.DKDocRoutingServiceMgmtICM; import com.ibm.mm.sdk.common.DKException; import com.ibm.mm.sdk.common.DKWorkNodeICM; import com.ibm.mm.sdk.server.DKDatastoreICM; public class CreateWorkNode { public static void main(String[] args) throws DKException, Exception { DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 创建连接 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 设置路由服务管理对象 DKDocRoutingServiceMgmtICM routingMgmt = new DKDocRoutingServiceMgmtICM( dkDatastore); // 创建工作节点对象 DKWorkNodeICM workNode = new DKWorkNodeICM(); // 设置节点名称 workNode.setName("NewWorkNode"); // 为节点设置访问控制列表 workNode.setACLName("NewACL"); // 设置节点类型,此处为普通节点 workNode.setType(DKConstantICM.DK_ICM_DR_WB_NODE_TYPE); // 创建工作节点 routingMgmt.add(workNode); // 断开连接 dkDatastore.disconnect(); } } |
通过前面的内容,我们介绍了如同通过 Content Manager 的 API 来为服务器创建相关的管理对象。下面我们给出一个完整的示例,如何通过 API 来创建只包含普通节点的工作流程。一个简单的工作流程应该包含一个起始节点,一个终止节点和至少一个工作节点,并且在两个不同的节点之间设置合适的路由动作。
通过 API 创建工作流程的时候,所引用的管理对象应该已经在内容管理服务器当中存在,例如访问控制列表对象,工作节点对象等。
如果我们通过 API 创建一个下图所示的简单工作流程,可以参考如下的示例代码。
图 1. 简单工作流程
在这个简单的工作流程当中,仅包含两个普通类型的工作节点。
在 API 当中和工作流程相关的类是 DKProcessICM 这个对象。不同节点之间的路由关系的维护是由 DKRouteListEntryICM 这个对象来负责的。
清单 5. 创建简单工作流程代码清单
import com.ibm.mm.sdk.common.DKConstantICM; import com.ibm.mm.sdk.common.DKDocRoutingServiceMgmtICM; import com.ibm.mm.sdk.common.DKException; import com.ibm.mm.sdk.common.DKProcessICM; import com.ibm.mm.sdk.common.DKRouteListEntryICM; import com.ibm.mm.sdk.common.DKSequentialCollection; import com.ibm.mm.sdk.common.dkCollection; import com.ibm.mm.sdk.server.DKDatastoreICM; public class CreateProcess { public static void main(String[] args) throws DKException, Exception { DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 创建连接 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 设置路由服务管理对象 DKDocRoutingServiceMgmtICM routingMgmt = new DKDocRoutingServiceMgmtICM( dkDatastore); // 创建流程定义对象 DKProcessICM process = new DKProcessICM(); // 设置流程名称和流程描述 process.setName("NewProcess"); process.setDescription("a new process"); // 设置流程的访问控制列表 process.setACLName("DocRouteACL"); // 设置路由关系集合对象 dkCollection routes = new DKSequentialCollection(); // 设置路由关系,也就是两个工作节点之间的关联 DKRouteListEntryICM start2nodeOne = new DKRouteListEntryICM(); // 设置开始节点,起始节点作为系统常量存在 start2nodeOne.setFrom(DKConstantICM.DK_ICM_DR_START_NODE); // 设置目标节点,也就是目标工作节点的名称 start2nodeOne.setTo("TEMP_NODE_1"); // 设置两个节点之间的路由关系 start2nodeOne.setSelection("Continue"); // 将路由关系对象保存到集合当中 routes.addElement(start2nodeOne); // 设置工作节点 TEMP_NODE_1 和 TEMP_NODE_2 之间的路由关系 DKRouteListEntryICM nodeOne2nodeTwo = new DKRouteListEntryICM(); nodeOne2nodeTwo.setFrom("TEMP_NODE_1"); nodeOne2nodeTwo.setTo("TEMP_NODE_2"); nodeOne2nodeTwo.setSelection("Continue"); routes.addElement(nodeOne2nodeTwo); // 设置工作节点 TEMP_NODE_1 和终止节点之间的路由关系 DKRouteListEntryICM nodeOne2stop = new DKRouteListEntryICM(); nodeOne2stop.setFrom("TEMP_NODE_1"); nodeOne2stop.setTo(DKConstantICM.DK_ICM_DR_END_NODE); nodeOne2stop.setSelection("Exception"); routes.addElement(nodeOne2stop); // 设置工作节点 TEMP_NODE_2 和终止节点之间的路由关系 DKRouteListEntryICM nodeTwo2stop = new DKRouteListEntryICM(); nodeTwo2stop.setFrom("TEMP_NODE_2"); nodeTwo2stop.setTo(DKConstantICM.DK_ICM_DR_END_NODE); nodeTwo2stop.setSelection("Complete"); routes.addElement(nodeTwo2stop); // 在工作流程当中设置工作节点之间路由关系 process.setRoute(routes); // 设置工作流程的状态 process.setState(DKConstantICM.DK_ICM_DR_PROCESS_VERIFIED_STATE); // 在服务器当中创建所定义的工作流程 routingMgmt.add(process); // 断开连接 dkDatastore.disconnect(); } } |
通过 API 所创建的工作流程可以在 Content Manager 的系统管理客户端被打开并且被正确显示。由于在程序当中只给出了创建工作流程所需要的最基本信息,所以当工作流程被系统管理客户端第一次打开的时候,会出现一个警告信息。这个信息不会影响工作流程的正常显示和工作。
图2. 在系统管理客户端所显示的警告信息
如果所创建的流程当中所包含其他类型节点的时候,例如:分割节点,联合节点和决策节点的时候,创建的工作流程通过系统管理客户端打开的时候会遇到问题。这个问题的解决会在后面进行描述。
如果创建的流程当中包含分割,联合,决策点等类型工作节点的时候,工作流程无法被 Content Manager 的管理客户端打开,进行正确的显示。其原因是所设置的工作节点没有给出所要显示的位置坐标信息,从而使管理客户端无法进行布局。
图3. 在系统管理客户端所显示的错误信息
为了能够在系统管理客户端当中正确的显示各个工作节点的位置,需要在创建工作节点的时候,为其指定所要显示的位置信息。其坐标的定义示意图如下。
- 原点坐标位于左上角。
- X 轴横坐标水平向右延伸。
- Y 轴纵坐标垂直向下延伸。
图 4. 坐标位置布局示意图
工作节点的位置信息的在 Content Manager 当中是通过 XML 的格式的形式来定义的。例如设置某个工作节点的位置信息,其横坐标为 300,纵坐标为 200,可以采用这样的格式:
清单 6. XML 格式的工作节点坐标
300200 |
在决策节点当中可以通过节点变量,工作包特性和项类型属性来定义某些规则。从而触发不同的路由流程。这些规则的描述方式是通过字符串表达式的方式编写的。字符串表达式方便人的直接阅读和理解。但是在 Content Manager 服务器的内部,需要将这些规则按照特定的格式转化为类似 SQL 语句的形式,从而内容管理服务器能够理解和执行这些规则。
下面以通过项类型属性来定义规则来说明如何进行内部和外部规则的转换。
项类型属性规则表达式:
OneItemTypeName.OneAttributeName <> 1 |
表示项类型 (OneItemType) 当中的属性 (OneAttributeName) 的值不等于1的时候,条件满足,执行这个规则。
转换为 Content Manager 的外部规则格式为:
SELECT 1 FROM @SCHEMA@.ICMUT00204001 WP, @SCHEMA@. OneItemTypeName UT1 WHERE ( ( UT1. OneAttributeName <> '1' AND WP.RTARGETITEMID=UT1.ITEMID AND WP.RTARGETVERSIONID=UT1.VERSIONID ) ) AND (WP.COMPONENTID=@?) |
规则的格式是固定的,只需要根据具体的项类型和属性进行相关的替换就可以了。
转换为 Content Manager 的内部规则格式为:
SELECT 1 FROM @SCHEMA@.ICMUT00204001 WP, @SCHEMA@.ICMUT01026001 UT1 WHERE ( ( UT1.ATTR0000001261 <> '1' AND WP.RTARGETITEMID=UT1.ITEMID AND WP.RTARGETVERSIONID=UT1.VERSIONID ) ) AND (WP.COMPONENTID=@?) |
内部规则和外部规则表示方式的区别在于,内部规则表达式需要把项类型的名字和属性的名字转换为 Content Manager 服务器当中实际表示的对象的名称。
查找系统所定义的项类型在服务器当中所表示对象的名称,可以通过如下的 SQL 语句来查找。
清单 7. 查询项类型信息的 SQL 语句,以 OneItemType 为例
SELECT DISTINCT cd.COMPONENTTYPEID FROM icmstnlskeywords k1, icmstcompdefs cd WHERE k1.keywordclass = 2 AND k1.keywordname = 'OneItemType' AND k1.keywordcode = cd.itemtypeid AND cd.PARENTCOMPTYPEID = 0 |
得到的 SQL 查询结果是一个长度为 4 位的数字,通过 ICMUT + XXXX + 001 这个组合方式,就获取了项类型在规则当中实际需要的对象名称。
清单 8. 查询属性信息的 SQL 语句,以 OneAttributeName 为例
SELECT DISTINCT ca.ATTRIBUTEID FROM icmstcompattrs ca, icmstnlskeywords k1 WHERE ca.componenttypeid = XXXX( 项类型的 ID 数字 ) AND k1.KEYWORDCLASS = 1 AND k1.KEYWORDNAME = 'OneAttributeName' AND k1.KEYWORDCODE = ca.ATTRIBUTEID |
得到的查询结果也是一个长度为4位的数字,通过 ATTR000000 + XXXX 的组合方式,就可以得到属性在规则当中所需要的对象名称。
决策节点当中所包含的规则,在 Content Manager 服务器当中也是通过 XML 的格式来进行描述的。
对于项类型属性规则表达式:
OneItemTypeName.OneAttributeName <> 1 |
使用 XML 方式定义的格式如下所示。
清单 9. XML 格式的决策点规则定义
1]]> no1 '1' AND WP.RTARGETITEMID=UT1.ITEMID AND WP.RTARGETVERSIONID=UT1.VERSIONID ) ) AND (WP.COMPONENTID=@?)]]> '1' AND WP.RTARGETITEMID=UT1.ITEMID AND WP.RTARGETVERSIONID=UT1.VERSIONID ) ) AND (WP.COMPONENTID=@?)]]> 1]]> |
使用 XML 格式为 Content Manager 服务器定义工作流程的时候,需要包含三部分的内容。第一部分是整个工作流程的总体信息描述:主要包括工作流程的名称,所使用的访问控制列表信息,以及其他相关的显示信息。第二部分是工作节点相关的信息描述:主要包括工作节点的名称,节点的类型,以及工作节点所要显示的位置坐标信息。第三部分是两个节点之间路由关系的描述;主要包括源节点的名称,目标节点名称,路由方式已经两个节点之间的路由动作。
在 XML 格式的定义当中,使用 … 元素来描述工作流的信息;使用 … 元素来描述工作节点的信息;使用 … 元素来描述节点之间的路由关系。
一个完整工作流程的 XML 格式描述总体结构如下所示:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> …… …… …… …… …… …… |
其中 和 元素可以出现多次。
下面详细介绍每种元素的具体格式。
CM 8.3 xml0OFFONOFFOFFOFFOFFOFFON2
DECISION_POINTno |
如果一个决策点有多个规则的定义,那就需要定义多个 … 元素。
如果通过 API 直接创建的流程当中包含分割,联合,决策点等类型工作节点的时候,工作流程无法被 Content Manager 的管理客户端打开,进行正确的显示。但是在导入 XML 格式所定义的工作流程,通过 API 所创建的流程就可以被 Content Manager 的管理客户端正确的打开和显示。
import com.ibm.mm.sdk.common.DKConstantICM; import com.ibm.mm.sdk.common.DKDocRoutingServiceMgmtICM; import com.ibm.mm.sdk.common.DKException; import com.ibm.mm.sdk.common.DKProcessICM; import com.ibm.mm.sdk.common.DKRouteListEntryICM; import com.ibm.mm.sdk.common.DKSequentialCollection; import com.ibm.mm.sdk.common.dkCollection; import com.ibm.mm.sdk.server.DKDatastoreICM; public class CreateProcess { public static void main(String[] args) throws DKException, Exception { DKDatastoreICM dkDatastore = new DKDatastoreICM(); // 创建连接 dkDatastore.connect("ICMNLSDB", "icmadmin", "password", "SCHEMA=ICMADMIN"); // 设置路由服务管理对象 DKDocRoutingServiceMgmtICM routingMgmt = new DKDocRoutingServiceMgmtICM( dkDatastore); // 创建流程定义对象 DKProcessICM process = new DKProcessICM(); // 设置流程名称和流程描述 process.setName("NewProcess"); process.setDescription("a new process"); // 设置流程的访问控制列表 process.setACLName("DocRouteACL"); // 设置路由关系集合对象 dkCollection routes = new DKSequentialCollection(); // 设置路由关系,也就是两个工作节点之间的关联 DKRouteListEntryICM start2nodeOne = new DKRouteListEntryICM(); // 设置开始节点,起始节点作为系统常量存在 start2nodeOne.setFrom(DKConstantICM.DK_ICM_DR_START_NODE); // 设置目标节点,也就是目标工作节点的名称 start2nodeOne.setTo("TEMP_NODE_1"); // 设置两个节点之间的路由关系 start2nodeOne.setSelection("Continue"); // 将路由关系对象保存到集合当中 routes.addElement(start2nodeOne); // 设置工作节点 TEMP_NODE_1 和 TEMP_NODE_2 之间的路由关系 DKRouteListEntryICM nodeOne2nodeTwo = new DKRouteListEntryICM(); nodeOne2nodeTwo.setFrom("TEMP_NODE_1"); nodeOne2nodeTwo.setTo("TEMP_NODE_2"); nodeOne2nodeTwo.setSelection("Continue"); routes.addElement(nodeOne2nodeTwo); // 设置工作节点 TEMP_NODE_1 和终止节点之间的路由关系 DKRouteListEntryICM nodeOne2stop = new DKRouteListEntryICM(); nodeOne2stop.setFrom("TEMP_NODE_1"); nodeOne2stop.setTo(DKConstantICM.DK_ICM_DR_END_NODE); nodeOne2stop.setSelection("Exception"); routes.addElement(nodeOne2stop); // 设置工作节点 TEMP_NODE_1 和决策节点之间的路由关系 DKRouteListEntryICM nodeOne2decision = new DKRouteListEntryICM(); nodeOne2decision.setFrom("TEMP_NODE_1"); nodeOne2decision.setTo(“DecisionPointNode”); nodeOne2stop.setSelection("GotoDecision"); routes.addElement(nodeOne2decision); // 设置决策节点和工作节点 TEMP_NODE_2 之间的路由关系 DKRouteListEntryICM decision2nodeTwo = new DKRouteListEntryICM(); decision2nodeTwo.setFrom("DecisionPointNode "); decision2nodeTwo.setTo("TEMP_NODE_2"); // 设置决策点路由规则名称 decision2nodeTwo.setSelection("OneRule"); // 设置内部路由规则格式 decision2nodeTwo.setDecisionRuleInternal("SELECT 1 ……"); // 设置外部路由规则格式 decision2nodeTwo.setDecisionRuleExternal("SELECT 1 ……"); // 设置路由规则的优先级别 decision2nodeTwo.setPrecedence(1); routes.addElement(decision2nodeTwo); // 设置决策节点和终止节点之间的路由关系 DKRouteListEntryICM decision2stop = new DKRouteListEntryICM(); decision2stop.setFrom("DecisionPointNode "); decision2stop.setTo("TEMP_NODE_2"); // 设置决策点路由规则名称 decision2stop.setSelection("defaultRule"); // 对于决策点的默认规则,不需要设置内部和外部规则 decision2stop.setDecisionRuleInternal(""); decision2stop.setDecisionRuleExternal(""); // 对于决策点的默认规则,设置优先级别为0 decision2stop.setPrecedence(0); routes.addElement(decision2stop); // 设置工作节点 TEMP_NODE_2 和终止节点之间的路由关系 DKRouteListEntryICM nodeTwo2stop = new DKRouteListEntryICM(); nodeTwo2stop.setFrom("TEMP_NODE_2"); nodeTwo2stop.setTo(DKConstantICM.DK_ICM_DR_END_NODE); nodeTwo2stop.setSelection("Complete"); routes.addElement(nodeTwo2stop); // 在工作流程当中设置工作节点之间路由关系 process.setRoute(routes); // 设置工作流程的状态 process.setState(DKConstantICM.DK_ICM_DR_PROCESS_VERIFIED_STATE); // 设置 XML 格式的流程定义 String xml = “<?xml version=……”; // 设置 XML 格式定义的工作流程数据 process.setDiagramDefinition(xml.getBytes()); // 在服务器当中创建所定义的工作流程 routingMgmt.add(process); // 断开连接 dkDatastore.disconnect(); } } |
通过本文的介绍,相信您会对 IBM DB2 Content Manager 的管理 API 有了一定的了解和学习。您可以通过这些 API 所提供的强大功能进行 Content Manager 管理功能的开发,编写出更加富有创造性和想象力的软件,发挥出 IBM DB2 Content Manager 这款产品的强大作用。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/15082138/viewspace-594520/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/15082138/viewspace-594520/