| « | November 2025 | » | | 日 | 一 | 二 | 三 | 四 | 五 | 六 | | | | | | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | | | | | | | |
| 公告 |
| 本博客在此声明所有文章均为转摘,只做资料收集使用。并无其他商业用途。 |
| Blog信息 |
|
blog名称: 日志总数:210 评论数量:205 留言数量:-19 访问次数:926598 建立时间:2007年5月10日 |

| |
|
[内容管理框架]介绍 Java Content Repository API 文章收藏, 网上资源, 软件技术, 电脑与网络
李小白 发表于 2008/3/5 17:58:50 |
|
介绍 Java Content Repository API- -
2005 年 9 月 12 日
随着内容管理应用程序的日益普及,对用于内容仓库的普通、标准化 API 的需求已凸现出来。Content Repository for Java Technology API (JSR-170) 的目标就是提供这样一个接口。在本文中,通过设计一个简单的类 Wikipedia 的综合后端,您将使用 JSR-170 的开放源码的 Apache Jackrabbit 实现来探讨这个期望的框架提供的特性。
如果您曾经试图开发一个内容管理应用程序,那么您肯定非常了解实现内容系统有多困难。技术上形成了分立局面,很多供应商都提供专有的仓库引擎。这些困难加剧了此类系统的复杂性和不可维护性,促进了供应商封闭,并增加了企业市场中长期遗留支持的需要。随着公司 weblogs 和电子的公司文档管理的日益普及,对标准的内容仓库接口的需求到达了前所未有的紧迫程度。
Content Repository for Java Technology 规范由 Java Community Process 开发为 JSR-170,目的就是满足这些行业需求。该规范在 javax.jcr 名称空间下提供一个统一的 API,允许人们以供应商中立的方式访问任何规范兼容的仓库实现。
但是 API 标准并不是 Java Content Repository (JCR) 提供的惟一特性。JSR-170 的一个主要优点是,它不绑定到任何特定的底层架构。例如,JSR-170 实现的后端数据存储可以是文件系统、WebDAV 仓库、支持 XML 的系统,甚至还可以是 SQL 数据库。此外,JSR-170 的导出和导入功能允许一个集成器在内容后端与 JCR 实现之间无缝地切换。最后,JCR 提供一个直观的界面,可以出现在各种现有内容仓库上面,同时同步地标准化诸如版本控制、访问控制和搜索之类的复杂功能。
在讨论 JCR 时,有好几种方法可以采用。在本文中,我从开发人员的角度介绍 JSR-170 规范提供的特性,集中讲述可用的 API 和界面,它们允许程序员在设计内容应用程序时高效地使用 JSR-170 仓库。作为一个虚构的例子,我将为类 Wikipedia 的综合系统实现一个简单的后端,叫做 JCRWiki,它支持二进制内容、版本控制、备份和搜索。在开发这个应用程序的过程中,我将用到 Apache Jackrabbit,这是 JSR-170 的一个开放源码的实现。
Repository 模型
为了熟悉 JCR,我们首先对 Repository 模型进行高级讨论。Repository 模型是一个简单的层次结构,看起来很像一棵 n 层的树。它包含单个内容仓库和一个或多个工作区 (workspace)。(在本文中,我只讨论单个工作区。)每个工作区包含一棵条目 (item) 树;条目可以是节点 (node) 或者属性 (property)。节点可以具有零个或多个子节点,以及零个或多个相关的属性,实际内容就存储在这些属性中。
每个节点有且只有一个主节点类型 (primary node type)。主节点类型定义节点的特征,比如允许节点具有的属性和子节点。除了主节点类型之外,节点还可能具有一个或多个 mixin 类型。mixin 类型很像修饰器 (decorator),为节点提供额外的特征。具体来说,JCR 实现可以提供三种预定义的 mixin 类型:
mix:versionable,它允许节点支持版本控制。
mix:lockable,它为节点启用锁定功能。
mix:referenceable,它提供一个自动创建的 jcr:uuid 属性,该属性给予节点一个惟一的、可引用的标识符。
图 1 中阐释了这一结构。圆圈代表节点,矩形代表属性。有趣的是,节点 A、B 和 C 起源于单个根节点。节点 A 有两个属性:一个字符串“John”和一个整数 22。图 1. 一个具有多个工作区的 Repository 模型500)this.width=500'>
预定义的节点类型
每个仓库都必须支持主节点类型 nt:base。还有很多仓库可以支持的其他公共节点类型:
nt:unstructured 是最灵活的节点类型。它允许任意数量的子节点或属性,而这些子节点和属性又可以具有任意的名称。该节点类型将用于表示 JCRWiki 项。
nt:file 用于表示文件。它需要单个子节点,叫做 jcr:content。该节点类型将用于表示图像和 JCRWiki 项中的其他二进制内容。
nt:folder 节点类型可用于表示文件夹,比如常规文件系统中的文件夹。
nt:resource 通常用于表示文件的实际内容。
nt:version 支持版本控制的仓库所必需的节点类型。
在 JSR-170 规范的第 6.7.22.1 节中可以找到整个节点类型层次结构(参见 参考资料 部分的链接)。
名称空间
Repository 模型的一个有用却常被忽视的特性是它对名称空间 (namespace) 的支持。名称空间防止来自不同来源和应用领域的条目及节点类型之间出现命名冲突。名称空间利用一个前缀进行定义,并由一个 :(冒号)字符分隔。在本文中,您已经遇到了针对 JCR 内部属性的 jcr、针对 mixin 类型的 mix 和针对节点类型的 nt 等名称空间。在 JCRWiki 中,您会将 wiki 名称空间用于您的所有数据。
500)this.width=500'>
500)this.width=500'>
回页首
安装 JCR
Apache Jackrabbit 是由 Apache Foundation 提供的 JSR-170 的开放源码实现,在撰写本文时,它还是一个 Apache Incubator 项目,因此还没有正式的二进制版本供下载。尽管 Jackrabbit 本身不难使用,但是最初的配置比较复杂,因为它还处于开发状态,并且使用了各种企业 Java 技术。因此,本节将逐步地介绍如何完成 JCR 实现,并使之运行得尽可能快。
编译 Apache Jackrabbit
我们首先来为 JCR 开发核心初始化代码。在可以使用 Jackrabbit 之前,需要从源代码编译它。为了成功地构建 Jackrabbit,您需要 Subversion 以访问源代码树,需要 Maven 以构建项目。(参见 参考资料 部分到所有这些包的链接。)
清单 1 阐释了在典型命令行环境中发出的用于编译 Jackrabbit 的命令。如果您使用的是 Windows 或 Mac OS,那么图形版本控制前端、集成的 Java 开发编辑器及构建指令都有可能不同。清单 1. 在 Linux 下构建 Apache Jackrabbit
$ svn co \
http://svn.apache.org/repos/asf/incubator/jackrabbit/trunk jackrabbit
$ cd jackrabbit
$ maven
Maven 将自动下载项目的必要依赖关系,并在构建期间运行单元测试和其他全面的检查。如果您使用的是 JDK 5.0,则需要从 Xalan-Java 下载包含 Xalan Interpretive Processor 的 xalan.jar 文件,并将它放置在 $MAVEN_HOME/lib/endorsed 中,以构建 Jackrabbit 源代码。
需要的库
要成功地编译和运行本文中的例子,请使得下面的库在类路径中可用:
commons-collections,一个框架,包含强大的数据结构,能加速 Java 应用程序的开发。
concurrent,一个库,提供标准化的、有效版本的实用工具类,在并发 Java 程序设计中通常会遇到这些类。
jackrabbit-api、jackrabbit-commons 和 jackrabbit-core,一起组成 JSR-170 规范的开放源码的 Apache 实现。
jcr,一组符合 JSR-170 规范的接口。
log4j,一个运行时日志记录库。
lucene,一个高性能的、全功能的文本搜索引擎库。
xerces,一个支持 SAX Version 2、DOM Level 1 和 SAX Version 1 API 的高级 XML 解析器。
所有这些 JAR 文件都是在 Jackrabbit 构建过程中下载的,并在 Maven 缓存目录中可用。在 Linux 下,可以在 home 目录中的 .maven 下找到这些 JAR。jcr-1.0.jar 被附加包含在 JSR-170 规范中,可在 Java Community Process Web 站点上得到它。
500)this.width=500'>
500)this.width=500'>
回页首
特定于 Jackrabbit 的首要步骤
JSR-170 没有确切指定某人应该如何最初获得 Repository 对象;这被留作每个仓库供应商的实现细节。对于 Jackrabbit,通过使用 JNDI 以及一个以程序方式加载的叫做 repository.xml 的配置文件,可以获得仓库。
仓库配置
第一步是最容易的一步,就是为 Jackrabbit 创建 repository.xml 文件。这个配置文件完成很多重要任务;除了其他事情之外,它还指定底层后备存储、访问控制机制、可用的工作区、版本控制系统以及搜索子系统。清单 2 给出了一个例子:清单 2. 示例 repository.xml 配置文件
<?xml version="1.0" encoding="ISO-8859-1"?>
<Repository>
<FileSystem
class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
<param name="path" value="${rep.home}/repository"/>
</FileSystem>
<Security appName="Jackrabbit">
<AccessManager
class="org.apache.jackrabbit.core.security.
SimpleAccessManager"/>
</Security>
<Workspaces
rootPath="${rep.home}/workspaces"
defaultWorkspace="default" />
<Workspace name="${wsp.name}">
<FileSystem
class="org.apache.jackrabbit.core.fs.local.
LocalFileSystem">
<param name="path" value="${wsp.home}"/>
</FileSystem>
<PersistenceManager
class="org.apache.jackrabbit.core.state.xml.
XMLPersistenceManager" />
<SearchIndex
class="org.apache.jackrabbit.core.query.lucene.
SearchIndex">
<FileSystem
class="org.apache.jackrabbit.core.fs.local.
LocalFileSystem">
<param name="path" value="${wsp.home}/index"/>
</FileSystem>
</SearchIndex>
</Workspace>
<Versioning rootPath="${rep.home}/versions">
<FileSystem
class="org.apache.jackrabbit.core.fs.local.
LocalFileSystem">
<param name="path" value="${rep.home}/versions"/>
</FileSystem>
<PersistenceManager
class="org.apache.jackrabbit.core.state.xml.
XMLPersistenceManager" />
</Versioning>
</Repository>
在该配置中,我为存储仓库数据使用本地文件系统,为访问控制使用 SimpleAccessManager。文件的其余部分基本上是自解释的;您可以简单地将它逐字复制到自己的仓库目录。
安全性配置
通过使用放置在项目根目录中的 JAAS 配置文件 jaas.config,可以提供一般的安全性。清单 3 提供了一个例子:清单 3. 示例 JAAS 配置
Jackrabbit {
org.apache.jackrabbit.core.security.SimpleLoginModule required
anonymousId="anonymous";
};
仓库初始化代码
清单 4 中详细介绍的包用于初始化仓库:清单 4. 初始化导入语句
import org.apache.jackrabbit.core.jndi.RegistryHelper;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.jcr.*;
import java.util.Hashtable;
import javax.jcr.query.*;
import javax.jcr.version.*;
import java.util.Calendar;
import java.io.*;
import sun.net.www.MimeTable;
要获得 Repository 对象,请将 configFile 变量设置为指向 repository.xml 文件,将 repHomeDir 变量设置为指向本地文件系统目录,这里就是您想要让仓库驻留的地方。当结合使用 JNDI 和 RegistryHelper 时,获得仓库很简单,如清单 5 所示:清单 5. 利用 JNDI 获得 Repository 对象
String configFile = "repotest/repository.xml";
String repHomeDir = "repotest";
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"org.apache.jackrabbit.core.jndi" +
".provider.DummyInitialContextFactory");
env.put(Context.PROVIDER_URL, "localhost");
InitialContext ctx = new InitialContext(env);
RegistryHelper.registerRepository(ctx,
"repo",
configFile,
repHomeDir,
true);
Repository r = (Repository) ctx.lookup("repo");
接下来,SimpleCredentials 用于获得 Session 对象。在这个实现中,SimpleCredentials 接受任何和所有用户名。另一个可选的 JCR 实现可以提供更复杂的身份验证机制,也许连接到 LDAP 服务器或外部数据库的人需要提供凭证信息。(身份验证和访问控制的完整功能超出了本文的范畴。有关详细信息,请参阅 JSR-170 规范的第 6.9 节。)
Session 对象为程序员提供一个瞬态存储层,类似于传统的对象关系型映射工具中的瞬态存储层,同时也充当到特定工作区的链接。它允许客户访问与该会话关联的任何节点或属性。从该会话,您获得一个工作区,然后您又可从该工作区获得根节点。所有这些步骤都在清单 6 中的简短代码片段中完成。清单 6. 获得工作区和根节点
SimpleCredentials cred = new SimpleCredentials("userid",
"".toCharArray());
Session session = r.login(cred, null);
Workspace ws = session.getWorkspace();
Node rn = session.getRootNode();
使用会话、工作区和根节点引用,您现在可以通过不同的抽象级别访问仓库特性了。最后,为了验证仓库已经成功地初始化了,您可以利用 rn.getPrimaryNodeType().getName() 简单地输出根节点的名称。这应该导致下面的输出:
rep:root
因为您使用的是 JAAS,所以请记住包含 -Djava.security.auth.login.config==jaas.config. 作为一个 Java VM 参数。
JCRWiki 名称空间
在这个练习中,所有的 JCRWiki 内容都将置于 wiki 名称空间下。为了仓库能识别该名称空间,必须在初始化期间注册名称空间,如下所示:
ws.getNamespaceRegistry().registerNamespace
("wiki", "http://www.barik.net/wiki/1.0");
祝贺!现在您已配置完仓库了。
500)this.width=500'>
500)this.width=500'>
回页首
JCRWiki 设计策略
现在我们来整体看一下 JCRWiki 仓库内容层次结构。在本例中,您将创建“rose”和“Shakespeare”这两个项,它们都是 nt:unstructured 类型的。按照设计约定,每个综合项都将具有三个属性:项的标题、项的内容,以及一个多值的类别属性(如果项具有多个类别)或者一个单值的类别属性(如果项具有单个类别)。一个多值属性程序上相当于一组值。
图 2 中概要画出了 JCRWiki 设计策略的图形图表:图 2. JCRWiki 拓扑的高级图表500)this.width=500'>
500)this.width=500'>
500)this.width=500'>
回页首
JCRWiki 功能
没有内容的仓库是没什么用的。在本节中,我要介绍 JSR-170 提供的基本内容操纵功能,并描述一些比较高级的、可选的仓库特性,比如版本控制,以及导入和导出 XML 内容。
添加内容
清单 7 以向仓库添加内容节点开始,以便它类似于图 2 中的 JCRWiki 拓扑:清单 7. 向 JCR 仓库添加内容
Node encyclopedia = rn.addNode("wiki:encyclopedia");
Node p = encyclopedia.addNode("wiki:entry");
p.setProperty("wiki:title", new StringValue("rose"));
p.setProperty("wiki:content", new
StringValue("A rose is a flowering shrub."));
p.setProperty("wiki:category",
new Value[]{
new StringValue("flower"),
new StringValue("plant"),
new StringValue("rose")});
Node n = encyclopedia.addNode("wiki:entry");
n.setProperty("wiki:title", new StringValue("Shakespeare"));
n.setProperty("wiki:content", new
StringValue("A famous poet who likes roses."));
n.setProperty("wiki:category", new StringValue("poet"));
session.save();
默认情况下,Jackrabbit 中的节点被设置为 nt:unstructured。注意,“rose”中的类别属性是多值的。代码片段的最后一行保存会话。回想一下,添加和设置节点及节点属性只修改瞬态会话存储层。要将这些更改持久存储到仓库中,必须用 session.save() 保存会话。可以在期望的节点上调用 Node.remove() 而删除节点。
访问内容
JSR-170 提供两种方法用于访问节点:遍历 访问和直接 访问。遍历访问涉及到使用相关路径遍历内容树,而直接访问允许您利用一个绝对路径,或者如果节点是可引用的话,也可以利用一个 UUID,而直接跳到一个节点。由于两种类型的访问方法很类似,所以本文中我只着重讲述遍历访问。
遍历方法由于任何 Node 对象及其方法 Node.getNode() 和 Node.getProperty() 而可用。使用 JCRWiki 拓扑,您可以利用下面的代码从根节点获得综合节点:
Node encyclopedia = rn.getNode("wiki:encyclopedia");
您可以进一步遍历到一个属性。例如,在来自根节点的“rose”综合项中,假设具有 JCRWiki 拓扑的优先知识,您可以遍历到一个属性,如下所示:
String roseTitle = rn.getProperty
("wiki:encyclopedia/wiki:entry[1]/wiki:title").getString()
注意,您遍历通过了 wiki:entry[1]。当具有多个带有相同名称的兄弟节点时,您可以通过使用索引符号来消除兄弟节点的歧义。在 JCR 中,相同名称兄弟节点的索引从 1 而不是 0 开始。此外,是基于通过 Node.getNodes() 在迭代器中返回节点的顺序而对节点进行索引的。
然后通过获得将会返回特定节点的子节点的 NodeIterator,您可以浏览所有的 JCRWiki 项,如清单 8 所示:清单 8. 浏览内容仓库
Node encyclopedia = rn.getNode("wiki:encyclopedia");
NodeIterator entries = encyclopedia.getNodes("wiki:entry");
while (entries.hasNext()) {
Node entry = entries.nextNode();
System.out.println(entry.getName());
System.out.println(entry.getProperty("wiki:title").getString());
System.out.println(entry.getProperty("wiki:content").getString());
System.out.println(entry.getPath());
Property category = entry.getProperty("wiki:category");
try {
String c = category.getValue().getString();
System.out.println("Category: " + c);
} catch (ValueFormatException e) {
Value[] categories = category.getValues();
for (Value c : categories) {
System.out.println("Category: " + c.getString());
}
}
}
因为类别属性可以是多值的,也可以是单值的,所以您可以用一个 try-catch 语句来检查它。如果在多值属性上调用 getValue(),则会抛出一个 ValueFormatException。一般来说,直接访问和遍历访问都需要了解内部节点结构。因此我们来看一下一些更加直观的访问节点的方式,即使用搜索。
利用 XPath 搜索内容
正如您所看到的,遍历访问和直接访问需要了解文件的位置。一个获得特定项的更好方式是使用 JCR 的 XPath 搜索功能。由于工作区模型在是一个树结构方面非常类似于 XML 文档,所以 XPath 对于查找节点是一个理想的语法。XPath 查询是通过 QueryManager 对象执行的。过程类似于通过 JDBC 访问记录,如清单 9 中所示:清单 9. 利用 XPath 搜索内容
QueryManager qm = ws.getQueryManager();
Query q = qm.createQuery
("//wiki:encyclopedia/wiki:entry[@wiki:title = 'rose']",
Query.XPATH);
QueryResult result = q.execute();
NodeIterator it = result.getNodes();
while (it.hasNext()) {
Node n = it.nextNode();
System.out.println(n.getName());
System.out.println(n.getProperty("wiki:title").getString());
System.out.println(n.getProperty("wiki:content").getString());
}
createQuery() 的第二个参数指定使用的查询语言。JCR 实现可能被附加地选择来为 SQL 语法支持 Query.SQL。您也可以执行更加复杂的查询。例如,您可以查询所有其内容包含单词 rose 的项:
Query q = qm.createQuery
("//wiki:encyclopedia/" +
"wiki:entry[jcr:contains(@wiki:content, 'rose')]",
Query.XPATH);
利用 XML 导入和导出内容
JSR-170 已经付出努力,以确保跨 JCR 实现的可移植性。它促进这种可移植性的一种方式是,使用它的标准化 XML 导入和导出特性。通过使用这些功能,来自一个兼容供应商仓库的内容可以容易地转换到另一个兼容供应商仓库。将 XML 用于串行化的另一个优势是,导出的仓库可以用传统的 XML 解析工具进行操纵。只要使用清单 10 中的三行代码就可以执行导出:清单 10. 导出数据
File outputFile = new File("systemview.xml");
FileOutputStream out = new FileOutputStream(outputFile);
session.exportSystemView("/wiki:encyclopedia", out, false, false);
产生的 XML 文件然后又可以转换到另一个新的仓库,如清单 11 所示:清单 11. 转换数据
File inputFile = new File("systemview.xml");
FileInputStream in = new FileInputStream(inputFile);
session.importXML
("/", in, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
session.save();
添加二进制内容
到现在为止,您一直对节点属性使用 StringValue。但是 JCR 也支持其他类型,包括 Booleans、日期和长整型。清单 12 使用 JCR 中可用的流类型,演示了节点中二进制图像的存储。在该清单中,您添加文件 rose.gif 作为 nt:file 节点的元数据。文件数据本身存储为 nt:resource 子节点。清单 12. 添加二进制内容
File file = new File("rose.gif");
MimeTable mt = MimeTable.getDefaultTable();
String mimeType = mt.getContentTypeFor(file.getName());
if (mimeType == null) mimeType = "application/octet-stream";
Node fileNode = roseMode.addNode(file.getName(), "nt:file");
Node resNode = fileNode.addNode("jcr:content", "nt:resource");
resNode.setProperty("jcr:mimeType", mimeType);
resNode.setProperty("jcr:encoding", "");
resNode.setProperty("jcr:data", new FileInputStream(file));
Calendar lastModified = Calendar.getInstance();
lastModified.setTimeInMillis(file.lastModified());
resNode.setProperty("jcr:lastModified", lastModified);
在使用 MimeTable 类来确定内容类型之后,您使用 FileInputStream 来下载文件。然后向 nt:resource 节点类型(其中包含实际的文件数据)添加正确命名的属性就很简单了。
版本控制
JSR-170 支持很多可选的特性,包括访问控制、事务、锁定和版本控制。这些特性本身就是一些完整的主题,所以我只简要介绍最流行的版本控制。在最简单的情况下,可以通过向任何节点添加 mix:versionable mixin 类型来执行版本控制。节点上的版本控制可以通过该节点上的一组类似 CVS 操作的方法来完成,如清单 13 所示:清单 13. 版本控制方法
n.checkout();
n.setProperty("wiki:content", "Updated content for the entry.");
n.save();
n.checkin();
JCR 中可用的其他方法包括更新、合并和还原以前的版本。要浏览一个给定节点的整个版本历史,请执行清单 14 中的步骤:清单 14. 浏览版本历史
VersionHistory vh = n.getVersionHistory();
VersionIterator vi = vh.getAllVersions();
vi.skip(1);
while (vi.hasNext()) {
Version v = vi.nextVersion();
NodeIterator ni = v.getNodes();
while (ni.hasNext()) {
Node nv = ni.nextNode();
System.out.println("Version: " +
v.getCreated().getTime());
System.out.println(nv.getProperty("wiki:title").getString());
System.out.println(nv.getProperty("wiki:content").getString());
}
}
vi.skip(1) 的使用乍一看有些奇怪,但是当您明白了版本历史在内部是如何存储的时,它的使用就很清楚了,如图 3 所示:图 3. 节点版本历史的结构化模型500)this.width=500'>
在节点的版本历史中,每个版本存储为版本历史的一个子节点,具有指示版本继承者的参考。在图 3 中,版本 Vb 是版本 Va 的继承者,版本 Vc 和 Va 是 Vroot 的继承者,版本图从 Vroot 开始。Vroot 是一个自动创建的子节点,是版本图的起点;它不包含任何状态信息。因此,当应用程序迭代通过版本历史时,Vroot 被跳过。
500)this.width=500'>
500)this.width=500'>
回页首
结束语
在本文中,我广泛地介绍了 JSR-170 规范提供的很多特性。最终的规范版本于 2005 年 6 月 17 日核准,并带来了两个商业实现:Day Software 的 CRX 和 Obinary 的 Magnolia Power Pack。介绍 JSR-170 也引出了公司开放源码门户和内容管理系统,比如 Magnolia 和 eXo platform。最重要的是,JSR-170 得到行业领先者(包括 SAP AG、Macromedia 和 IBM)的强大支持,在企业范围内得到应用,并树立了重要地位。正如对象关系映射框架转换的数据库编程一样,Java Content Repository API 有潜力极大地改变我们考虑和开发内容应用程序的方式。 |
|
|