认识 Atom 发布协议,第 3 部分: Apache Abdera 项目简介
本系列的前几期介绍了 Atom 发布协议(Publishing Protocol),描述了在实际应用程序中使用的各种方式。本文将举例说明如何使用一个新的开放源代码项目 Abdera 实现支持 Atom 的应用程序,该项目目前处于 Apache Software Foundation 孵化阶段。
本文假设您已经阅读过 Atom Format 规范,对连锁有一定了解(请参阅 参考资料)。所有的例子都是用 Java 编写,并提供了包含全部代码例子的 Eclipse 项目供您下载。
开始
首先要保证安装了 Apache Abdera 的当前版本。源代码可以从 Apache Subversion 资料库(http://svn.apache.org/repos/asf/incubator/abdera/java/branches/0.2.0-incubating/)下载。要检索源代码,需要安装 subversion 客户机并使用下面的命令:
> svn co http://svn.apache.org/repos/asf/incubator/abdera/java/branches/0.2.0-incubating/ |
下载源代码镜像之后,就可以使用 Ant 1.6.5 或更高版本构建 Abdera 了。
> cd trunk > ant -f build/build.xml dist |
构建完成之后,编译后的 jar 和依赖关系文件存放在新建的 “dist” 目录中。运行本文中的例子需要在类路径中包含下列 jar。并非所有例子都需要依赖关系列表中的全部 jar:
表 1. 运行示例需要的 jar
Abdera (dist) | 依赖关系(dist/lib) |
---|
- abdera.client.0.2.0-incubating.jar
- abdera.core.0.2.0-incubating.jar
- abdera.parser.0.2.0-incubating.jar
- abdera.protocol.0.2.0-incubating.jar
|
- axiom-api-1.0.jar
- axiom-impl-1.0.jar
- commons-codec-1.3.jar
- commons-httpclient-3.0.1.jar
- commons-logging-1.0.4.jar
- geronimo-activation_1.0.2_spec-1.1.jar
- log4j-1.2.12.jar
- stax-1.1.2-dev.jar
- stax-api-1.0.jar
- jaxen-1.1-beta-7.jar
|
起步
Abdera 项目包含一组独立的模块,下表按字母顺序列出了这些模块。当然,最重要也是最常用的模块是 core、dependencies 和 parser:
表 2. Apache Abdera 项目模块
模块 | 说明 | 依赖关系 |
---|
build | 整个项目的 Ant 构建 | Apache Ant 1.6.5+ |
client | Atom Publishing Protocol Client | core, parser, protocol, commons-codec-1.3.jar, commons-httpclient-3.0.1.jar |
core | Feed Object Model 接口 | Java Activation Framework, commons-logging-1.0.4.jar, log4j-1.2.12.jar |
dependencies | 所有模块共用的依赖关系 | |
extensions | 提要语法和协议扩展 | core, protocol, json-1.0.jar |
parser | StAX 和基于 Axiom 的默认 Feed Object Model (FOM)实现 | core, axiom-api-1.0.jar, axiom-impl-1.0.jar, stax-1.1.2-dev.jar, stax-api-1.0.jar, jaxen-1.1-beta-7.jar |
protocol | Common Atom Publishing Protocol 代码 | core, parser |
security | XML Digital Signature 和 Encryption 支持 | core, parser, xmlsec-1.3.0.jar, Bouncy Castle JCE 实现 |
server | Atom Publishing Protocol 服务器实现 | core, parser, protocol, Servlet API |
下表列出了 core 模块中的包,该模块定义了 Abdera 所称的 “Feed Object Model”,它是按照 Atom Syndication Format 规范建模的一组接口,用于解析、创建和操作 Atom 文档。
表 3. Abdera “core” 模块中的包
包 | 说明 |
---|
org.apache.abdera | 主包,包含一个 “Abdera” 对象 |
org.apache.abdera.factory | 定义了创建新的 Feed Object Model 对象实例的 Factory 接口 |
org.apache.abdera.filter | 定义了在解析过程中筛选 ATom 文档的接口 |
org.apache.abdera.model | 定义了处理 Atom Feed 和 Entry 文档的基本接口,该模型还包括处理 Atom Publishing Protocol Service 和 Category 文档的支持。 |
org.apache.abdera.parser | 定义了从 XML 文档创建 Feed Object Model 新的对象实例的 Parser 接口 |
org.apache.abdera.util | 提供了各种工具类,主要针对希望扩展或替换 Abdera 默认解析器和工厂实现的开发人员 |
org.apache.abdera.writer | 定义了用于序列化 Feed Object Model 对象实例的 Write 接口 |
org.apache.abdera.xpath | 定义了使用 XPath 导航 Feed Object Model 的接口 |
下载并构建 Abdera 源代码后,如果需要的话,可以花点时间通过浏览构建过程中生成的 Javadoc 文档(位于创建的 $ABDERA_HOME/dist/docs 目录下)来熟悉这种 API。
创建记录和提要
Abdera Feed Object Model 的两个主要功能是简化 Atom 提要和记录文档的生产与消费。
创建 Atom 文档首先要获得 org.apache.abdera.factory.Factory 的实例并设置提要或记录的属性。清单 1 显示了一个简单 Atom Entry Document 的创建:
清单 1. 创建简单的 Atom Entry 文档
Abdera abdera = new Abdera(); Factory factory = abdera.getFactory();
Entry entry = factory.newEntry(); entry.setId("http://example.org/foo/entry"); entry.setUpdated(new java.util.Date()); entry.setTitle("This is an entry"); entry.setContentAsXhtml("This is
markup"); entry.addAuthor("James"); entry.addLink("http://www.example.org");
entry.writeTo(System.out);
|
这个例子说明了 Feed Object Model 的所有主要特点。
- 首先创建一个 org.apache.abdera.Abdera 实例,它为初始化和访问各种重要的子组件如 Abdera Parser 和 Factory 提供了顶级入口。
- 其次获得一个 org.apache.abdera.factory.Factory 实例。这是引导创建所有 Feed Object Model 对象的接口。
- 第三步,是用 Factory 创建新的 org.apache.abdera.model.Entry 实例。Factory 接口两个最常用的方法是 newEntry()和 newFeed()。
- 最后使用默认的 Abdera Writer 将 Atom 文档序列化到 OutputStream。
清单 1 的输出如清单 2 所示。请注意,XML 名称空间声明、日期格式和 atom:content 元素的序列化都是由 Feed Object Model 实现自动完成的。但是因为没有换行和缩进,连成一串的结果不便于阅读:
清单 2. 创建的 Atom Entry 文档
http://example.org/foo/entry 2006-09-04T19:27:01.068ZThis is an entrytype="xhtml">
This is markup
James
|
序列化不使用空白和换行是设计优化的结果,可以提高文档的传输效率。也可使用 prettyxml writer 插入换行和缩进:
清单 3. 使用 prettyxml writer 序列化
Writer writer = abdera.getWriterFactory().getWriter("prettyxml"); writer.writeTo(entry, System.out);
|
得到的结果看起来更清楚一点:
清单 4. 漂亮的 XML 输出
<?xml version='1.0' encoding='UTF-8'?>
http://example.org/foo/entry 2006-09-04T19:28:58.237Z This is an entry
This is markup
James
|
使用 prettyxml writer 要注意,因为可能会造成一些意外的副作用。比方说,使用这种 writer 序列化包含 XML 数字签名的 Atom Feed 或 Entry 可能造成签名失效。
清单 5 示范了 Atom Feed Document 的创建。可以想象,这个过程和创建 Atom Entry Document 是一样的:
清单 5. 创建简单的 Atom Feed Document
Abdera abdera = new Abdera(); Factory factory = abdera.getFactory();
Feed feed = factory.newFeed(); feed.setId("http://example.org/foo"); feed.setUpdated(new java.util.Date()); feed.setTitle("This is a feed"); feed.addLink("http://www.example.org"); feed.addLink("http://www.example.org/foo", "self");
Entry entry = factory.newEntry(); entry.setId("http://example.org/foo/entry"); entry.setUpdated(new java.util.Date()); entry.setTitle("This is an entry"); entry.setContentAsXhtml("This is
markup"); entry.addAuthor("James"); entry.addLink("http://www.example.org");
feed.addEntry(entry);
Writer writer = abdera.getWriterFactory().getWriter("prettyxml"); writer.writeTo(feed, System.out);
|
清单 6. 创建的 Atom Feed Document
<?xml version='1.0' encoding='UTF-8'?>
http://example.org/foo 2006-09-04T19:31:45.286Z This is a feed
http://example.org/foo/entry 2006-09-04T19:31:44.695Z This is an entry
This is markup
James
|
Abdera 的 Feed Object Model 接口严格遵循 Atom Syndication Format 模式,因此成为熟悉 RFC 4287 规范的开发人员创建有效 Atom 文档的自然选择。但必须指出,该实现对输入没有进行任何验证。比如,RFC 4287 要求所有的 atom:entry 和 atom:feed 元素必须且只能包含一个值为规范化 IRI 的 atom:id 元素。但是如果开发人员创建和序列化没有 atom:id 或者包含多个 atom:id 的记录或提要,Abdera 也不会抛出异常。需要开发人员来保证生成的文档是有效的。使用 FeedValidator 可以检查 Atom 文档的有效性。
向文档添加扩展
Feed Object Model 允许处理 Atom 格式扩展,而不需要开发人员事先编写模块实现这些扩展。比如,清单 7 示范了如何在一个记录的 “tag:example.org,2006:/namespaces” 添加 “foo”。清单 8 给出了序列化后的结果:
清单 7. 增加扩展元素
QName extensionQName = new QName("tag:example.org,2006:/namespaces", "foo", "f"); Element element = entry.addExtension(extensionQName); element.setAttributeValue("foo", "bar"); element.setText("fooBar");
|
清单 8. 包含扩展元素的 Entry Document
<?xml version='1.0' encoding='UTF-8'?>
http://example.org/foo/entry 2006-09-04T19:34:10.322Z This is an entry
This is markup
James
fooBar
|
清单 9 显示了一个更可能的例子,向提要中增加 OpenSearch version 1.1 规范的元素:
清单 9. 向 Atom Feed 中增加 OpenSearch version 1.1 元素
final String OSURI = "http://a9.com/-/spec/opensearch/1.1/"; final QName TOTALRESULTS = new QName(OSURI, "totalResults"); final QName STARTINDEX = new QName(OSURI, "startIndex"); final QName ITEMSPERPAGE = new QName(OSURI, "itemsPerPage"); final QName QUERY = new QName(OSURI, "Query");
Feed feed = factory.newFeed(); feed.addExtension(TOTALRESULTS).setText("4230000"); feed.addExtension(STARTINDEX).setText("21"); feed.addExtension(ITEMSPERPAGE).setText("10"); Element query = feed.addExtension(QUERY); query.setAttributeValue("role", "request"); query.setAttributeValue("searchTerms", "New York History"); query.setAttributeValue("startPage", "1");
|
灵活的内容选项
Atom 格式相对于其他连锁格式一个重要的区别在于它支持各种各样的、灵活的内容类型。Atom 记录可以包含扁平文本、转义的 HTML、结构良好的 XHTML、XML、任何字符集编码的数据和 Base64 编码的内容。Atom 记录还可以通过 URI 引用内容。Feed Object Model 提供了一种简单的方法处理各种选项。清单 10 和 11 示范了如何将记录的内容设置为扁平文本:
清单 10. 将内容设置为扁平文本
Entry entry = factory.newEntry(); entry.setContent("This is not markup");
|
清单 11. 扁平文本内容元素
使用扁平文本内容(清单 10)和转义 HTML(清单 12)时,必须注意,序列化后的内容元素基本相同(清单 11 和 13)。重要的是提要的处理程序必须注意内容类型属性的值。在提要阅读器中显示这样的内容时,清单 11 中的扁平文本必须呈现为没有格式化的文字串 “This is not markup”,而清单 13 中的转义 HTML 必须带格式:“This is markup”。
清单 12. 将内容设置为转义 HTML
Entry entry = factory.newEntry(); entry.setContentAsHtml("This is markup");
|
清单 13. HTML 内容元素
对于提要发布者来说,替代转义 HTML 的首选是通过 Entry 接口的 setContentAsXhtml 方法在记录中使用结构良好的 XHTML 片段,如清单 14 所示:
清单 14. 将内容设置为 XHTML
Entry entry = factory.newEntry(); entry.setContentAsXhtml("This is markup");
|
清单 15. XHTML 内容元素
Abdera 解析传递给 setContextAsXHTML 方法的 XHTML 输入保证结构良好性,但不检查是否为有效的 XHTML。类似的,也可将记录内容设置为任何结构良好的 XML,如清单 16 和 17 所示:
清单 16. 将内容设置为 XML
Entry entry = factory.newEntry(); entry.setContent("foo", Content.Type.XML);
|
清单 17. XML 内容元素
有 时候仅仅将基于字符的内容标记为 type="text" 或 type="html" 还不够。比如,Atom 记录有可能包含完整的 HTML 文档。但是,如果 type="html",内容元素中的转义标记必须响应包含在 HTML DIV 元素中。为了包含完整的文档,type 属性必须指定 HTML媒体类型,如清单 18 和 19 所示:
清单 18. 使用基于文本的媒体内容
Entry entry = factory.newEntry(); entry.setContent( "
Foo
This is new", "text/html");
|
清单 19. 基于文本的媒体内容元素
如 果提要发布者希望连锁非基于字符的内容,必须把这些内容采用 Base64 编码,并使用内容类型属性指定编码后的二进制数据的媒体类型。为此,需要在 Abdera 中使用 Java Activation Framework DataHandler 接口。当作为记录内容传入 DataHandler 的时候,Abdera 自动使用 Base64 编码内容:
清单 20. 设置 Base64 编码的二进制内容
Entry entry = factory.newEntry(); URL url = new URL("file:/home/jasnell/mozilla-firefox.png"); URLDataSource ds = new URLDataSource(url); DataHandler dh = new DataHandler(ds); entry.setContent(dh);
|
清单 21. 包含 Base64 编码数据的内容元素
iVBORw0KGgoAAAANSUhEUgAAA...
|
由于 Base64 编码本身效率低,有时候直接在 Atom 记录中包含内容也不合适(比如连锁流媒体或视频的时候),可以通过 URI 引用内容,如清单 22 和 23 所示:
清单 22. 在内容元素上设置 src 属性
Entry entry = factory.newEntry(); URI uri = new URI("http://example.org"); entry.setContent(uri.toString(), "application/xhtml+xml");
|
清单 23. 使用 src 属性的内容元素
解析提要和记录文档
Abdera 的第二个基本功能是解析 Atom 文档。首先需要获得一个 Parser 实例,然后传递给要解析的文档的 InputStream 或 Reader:
清单 24. 解析 Atom Feed Document
Abdera abdera = new Abdera(); Parser parser = abdera.getParser(); URL url = Listing7.class.getResource("/example.xml");
Document feed_doc = parser.parse(url.openStream()); Feed feed = feed_doc.getRoot();
|
由 于 Abdera 默认的解析器实现采用拉式模型解析 XML,对 Parser 对象调用 parse 方法不会完全消费传递给该方法的 InputStream 或 Reader。而是随着请求文档中的信息逐步地消费流。可以预料,调用 writeTo、clone 之类的方法将导致把流完全消费掉。但是 Parser 接口的非默认实现不一定如此。
解析 Document 后,开发人员就可以使用 Feed Object Model API 或者 XPath 的方法访问提要内容了:
清单 25. 遍历 Feed Object Model
System.out.println(feed.getBaseUri()); System.out.println(feed.getLanguage()); System.out.println(feed.getId()); System.out.println(feed.getTitle()); System.out.println(feed.getTitleType()); System.out.println(feed.getUpdated()); System.out.println(feed.getAlternateLinkResolvedHref()); System.out.println(feed.getSelfLinkResolvedHref());
List entries = feed.getEntries();
for (Entry entry : entries) {
System.out.println(entry.getId()); System.out.println(entry.getTitle()); System.out.println(entry.getTitleType()); System.out.println(entry.getUpdated()); System.out.println(entry.getAlternateLinkResolvedHref()); System.out.println(entry.getEnclosureLinkResolvedHref()); System.out.println(entry.getContent()); System.out.println(entry.getContentType());
Element element = entry.getExtension( new QName("tag:example.org,2006:/namespaces", "foo"));
System.out.println(element.getText());
}
|
如果不想遍历提要和记录对象并调用各种 get 方法,开发人员也可使用 XPath 表达式导航解析后的文档,如清单 26 所示:
清单 26. 使用 XPath 导航 Feed Object Model
Abdera abdera = new Abdera(); XPath xpath = abdera.getXPath();
System.out.println(xpath.valueOf("/a:feed/a:id", feed)); System.out.println(xpath.valueOf("/a:feed/@xml:base", feed)); System.out.println(xpath.valueOf("/a:feed/@xml:lang", feed)); System.out.println(xpath.valueOf("/a:feed/a:title", feed)); System.out.println(xpath.valueOf("/a:feed/a:title/@type", feed)); System.out.println(xpath.valueOf("/a:feed/a:updated", feed)); System.out.println(xpath.valueOf("/a:feed/a:link[not(@rel)]/@href", feed)); System.out.println(xpath.valueOf("/a:feed/a:link[@rel='self']/@href", feed));
|
除了选择单个节点、节点组、文本、布尔和数字值以外,还支持函数和轴这些标准 XPath 特性:
清单 27. 使用标准 XPath 函数
System.out.println(xpath.valueOf("count(//a:entry)", feed) + " entry"); System.out.println(xpath.valueOf("name(//a:entry/ancestor::*)", feed));
|
使用 XPath 选择节点返回的 Feed Object Model 对象本身可用 XPath 求值,如清单 28 所示。注意和上例不同的是,传递给 valueOf 函数的第二个参数不是提要,而是通过 selectSingleNode 方法选择的 Div 参数。XPath 根据传递给该方法的 Feed Object Model 元素求值:
清单 28. 对单个 FOM 对象计算 XPath 表达式
Div div = (Div) xpath.selectSingleNode( "//a:entry/a:content/x:div", feed, namespaces); System.out.println(xpath.valueOf("namespace-uri()", div)); System.out.println(xpath.valueOf("x:p", div, namespaces));
|
使 用 XPath 导航 Abdera 文档带来了一些好处,特别是对那些使用核心格式扩展的开发人员而言。比方说,使用 xpath.booleanValueOf 方法很快就能确定给定的提要或记录是否包含特定扩展元素,而且一般来说比使用各种 get 方法和迭代器遍历文档结构更有效。
Feed Object Model 的另一个重要特性是能够使用标准 Java Transformation API 对解析后的文档和元素应用 XSLT 转换。在 XPath 的支持下,可以对解析文档的任何部分应用 XSLT 转换:
清单 29. 使用 XSLT 转换 Abdera 文档
Abdera abdera = new Abdera(); Parser parser = abdera.getParser();
URL url = Listing8.class.getResource("/example.xml"); URL xslt = Listing8.class.getResource("/example.xslt");
Document feed_doc = parser.parse(url.openStream()); Document xslt_doc = parser.parse(xslt.openStream());
Source feed_source = new AbderaSource(feed_doc); Source xslt_source = new AbderaSource(xslt_doc);
TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = factory.newTransformer(xslt_source);
Result result = new StreamResult(System.out); transformer.transform(feed_source, result);
|
转换结果可以发送到 Java Transformaer API 支持的任何 Result 实现。上例中结果直接输出到 stdout。也可以将结果直接发送给 Abdera 解析器生成其他文档。
Atom Publishing 支持
Abdera 项目启动时,宣称其目标是为 Atom Syndication Format 和 Atom Publishing Protocol 提供功能完善的实现。到目前为止,本系列文章大量讨论了对 Syndication Format 的支持,并举例说明了如何生成和消费提要。下一期将讨论 Atom Publishing Protocol 客户机和正在实现之中的服务器支持。清单 30 给出了 Abdera APP 客户机的一个简单例子:
清单 30. 简单的 Atom Publishing Protocol 客户机
Abdera abdera = new Abdera(); Client client = new CommonsClient(abdera); RequestOptions options = client.getDefaultRequestOptions(); options.setIfModifiedSince(new java.util.Date()); options.setNoCache(true);
ClientResponse response = client.get("http://example.org/entries/1", options);
System.out.println(response.getStatus()); System.out.println(response.getStatusText()); System.out.println(response.getEntityTag()); System.out.println(response.getLastModified()); System.out.println(response.getContentType());
response.release();
|
Abdera 和线程安全
结 束本文之前,有必要指出大部分核心 Abdera 组件,如 Abdera、Factory 和 Parser 对象,都是线程安全和无状态的,因此可以在多个线程之间高效地共享实例。不过它们创建的 Feed Object Model(FOM)对象不是线程安全的。由于种种原因,这些 FOM 对象都不是同步的。如果需要允许多个线程访问或修改记录和提要对象,需要自己提供同步机制:
清单 31. 非同步的并发修改 FOM 对象会造成无法预料的结果
final Abdera abdera = new Abdera(); final Factory factory = abdera.getFactory(); final Entry entry = factory.newEntry();
final Thread[] threads = new Thread[100]; for (int n = 0; n < threads.length; n++) { final int i = n; threads[n] = new Thread( new Runnable() { public void run() { try { int s = (new java.util.Random( System.currentTimeMillis())).nextInt(10); Thread.sleep(s); } catch (InterruptedException e) {} try { IRI id = factory.newID(); id.setValue("urn:thread:" + i); entry.setIdElement(id); System.out.println(i + " " + entry); // unpredictable results } catch (Exception e) { e.printStackTrace(); } } } ); }
for (Thread t : threads) t.start();
|
清单 32. 清单 31 的无效输出
85 urn:thread:85 86 urn:thread: 99 91 urn:thread:91 org.apache.axiom.om.OMException at org.apache.axiom.om.impl.llom.OMNodeImpl.insertSiblingBefore(OMNodeImpl.java:232) at org.apache.abdera.parser.stax.FOMElement._setChild(Unknown Source) at org.apache.abdera.parser.stax.FOMEntry.setIdElement(Unknown Source) at abdera.examples.atom.Listing11$1.run(Listing11.java:29) at java.lang.Thread.run(Thread.java:788) 87 urn:thread:87 88 urn:thread:88
|
将记录的修改包装到一个同步块中就能解决这个问题。不过建议在一个线程中创建和使用 FOM 对象。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130303/,如需转载,请注明出处,否则将追究法律责任。