IBM Lotus Quickr REST 服务简介

学习如何使用 IBM Lotus Quickr Representational State Transfer(REST)服务对文档执行基本操作。

Ramajeyam Gopalraj, 咨询软件工程师, IBM

Ramajeyam Gopalraj 是 IBM 的一位咨询软件工程师,在位于 NC 的 Research Triangle Park 的 Lotus Quickr 团队工作。



Derek Carr, 咨询软件工程师, IBM

Derek Carr 是 IBM 的一位咨询软件工程师,在位于 NC 的 Research Triangle Park 的 Lotus Quickr 团队工作。



Gregory Melahn, 架构师和资深技术主管, EMC

Greg Melahn 是 Lotus Division of IBM Software Group 的架构师和资深技术主管。



2010 年 4 月 30 日

IBM Lotus Quickr 让用户能够轻松地共享业务内容。Lotus Quickr 的一个重要设计原则是在开放的接口上构建应用程序,这让客户和合作伙伴可以在任何地方和时间访问内容——这称为按需信息(information on demand)原则。本文主要关注 Lotus Quickr Representational State Transfer(REST)服务,包括概述和一个解释如何使用 REST 服务的简单示例。我们要介绍可以用 Quickr REST 服务执行的基本操作;后续的文章将解释更高级的操作。

前提条件

您应该很好地了解 Java 和 Web 2.0 编程,才能从本文得到最大的收获。为了使用示例代码,需要基本了解 Eclipse 框架和 Apache Abdera 客户机工具箱。关于 Apache Abdera 客户机工具箱,请参考 developerWorks 文章 “认识 Atom 发布协议,第 3 部分: Apache Abdera 项目简介”。


概述

Quickr REST 服务的目标是让开发人员能够尽可能轻松地构建协作解决方案。这些服务是围绕开放标准和 Web 2.0 技术设计的,所以我们只需基本了解现有的 Web 技术(比如 HTTP 和 XML),就可以构建应用程序。尤其是,可以使用 REST 样式的 URL 操作内容。这些服务基于 Atom Syndication Format(在RFC 4287中描述过,用于从 Lotus Quickr 访问内容)和 Atom Publishing Protocol(APP,用于向 Lotus Quickr 发布内容)。

按照 APP 的原理,文档库被当作 APP 集合,文档作为 APP 资源。为每个集合和资源定义 URL,客户机可以对 APP 定义的这些 URL 调用适当的 HTTP 方法,比如 GET、POST、PUT 和 DELETE。另外,这些 URL 符合一种可预测的模式,所以客户机可以编写 URL,而不必要求服务器提供它们。关于这些服务的完整细节,请参考 IBM Lotus Quickr Developer's Guide


关于 Quickr Connector

假设您是 Acme Corporation 的应用程序开发人员,这家公司配置了 Lotus Quickr。Acme 的架构师和设计师使用 Lotus Quickr 在一个 Lotus Quickr 位置存储体系结构和设计文档。开发人员常常需要这些文档,所以如果能够在 Eclipse 开发平台(这也是在 Acme 广泛使用的系统)中轻松地访问这些文档,那么开发人员的工作效率会更高。因此,您决定编写一个 Quickr Connector,Quickr Connector 是现有应用程序或框架的扩展,它使我们能够在任何地方和时间访问内容。例如,用于 IBM Lotus Notes 的 Quickr Connector 可以将 Lotus Quickr 文档的链接嵌入在邮件中。

在这个示例中,Lotus Connector 实现为一个 Eclipse 视图插件,可以通过它访问文档库。这个插件称为 QuickrNavigator,它提供一个包含树控件的 Eclipse 视图。可以配置这个视图,让它显示来自 Lotus Quickr 服务器的文档库。树控件将库显示为顶级节点,库下面的每个文件夹是一个可展开的节点,文档是叶节点。这个插件的最初设计支持以下操作:

  • 将本地文件上载到库或库中的文件夹
  • 刷新文件夹或库,列出最新的内容
  • 将文档下载到本地文件系统
  • 通过双击在相关联的应用程序中查看文档
  • 删除文档

在这个示例中,使用开放源码的 Apache Abdera 客户机解析 Atom feed 并创建到服务器的 HTTP 请求。


获取库的列表

首先,必须获得库的列表,这样才能在导航树中建立顶级节点。按照 Atom 的术语,文档库表示为集合。Atom 定义一个服务文档,作为获取集合的机制。要获得这个服务文档,需要知道服务器名称以及连接服务器所用的用户 ID 和密码。使用的 URL 是一个内省 URL(introspection URL),并且因为文档中发布了 URL 格式,所以对于给定的服务器,您可以自己构建 URL。对于此处的服务器,URL 如下所示:

http://quickr.acme.com/dm/atom/introspection

对这个 URL 执行 GET 请求来获取库列表。响应包含一个 Atom 服务文档,其中包含一个 <workspace> 元素和每个库的 <collection> 元素(见清单 1)。

清单 1. Atom 服务文档
<service>
 <workspace title="Teamspace Documents">
  <collection title="Architecture Documents" href="http://quickr.acme.com/library/
  5d06ab0044ed8129bd5ebd4caeec5df1/feed">
	<accept>application/*,image/*,*/*</accept>
  </collection>
  <collection title="Design Documents" href="http://quickr.acme.com/library/
  3c06ab0044ed8129bd5ebd4cbeec5dc4/feed">
	<accept>application/*,image/*,*/*</accept>
  </collection>
 </workspace>
</service>

title 属性的值用作树节点的标签。当在树中展开节点时,使用 <collection> 元素的 href 属性中的 URL 获取库的内容。清单 2 给出处理这个响应的部分代码。

清单 2. 处理响应的部分代码
try {
	httpClient.addCredentials(serverConfig.getUrl(), null, null, 
new UsernamePasswordCredentials(serverConfig.getUserId(), 
serverConfig.getPassword()));

	httpClient.usePreemptiveAuthentication(true);

	ClientResponse response = httpClient.get(serverConfig.getUrl()
							+ "/dm/atom/introspection");

	if (response.getStatus() == 200) {

		Document serviceDoc = response.getDocument();
		Element element = serviceDoc.getRoot().getFirstChild();
		while (element != null) {
			if (element instanceof Workspace) {
				Workspace ws = (Workspace) element;
				Iterator collections = ws.getCollections()
								.iterator();
				while (collections.hasNext()) {
					TreeParent collection = new TreeParent(
						(Collection) collections.next(),
						true);
						invisibleRoot.addChild(collection);
				}
			}
			element = element.getNextSibling();
		}
	}
} catch (Exception e) {
	e.printStackTrace();
	showMessage(e.toString());
}

处理了 Atom 服务文档之后,插件就可以与用户进行交互了(见图 1)。

图 1. 插件显示从服务器获得的库列表
插件显示从服务器获得的库列表

获得文件夹和文档的列表

既然已经建立了树的顶级节点,就该显示库中的文档和文件夹了。为此,需要检索库中的内容并建立子节点(文件夹和文档)。因为已经存储了每个库节点的 feed URL,所以很容易获取内容;只需对这个 URL 发出一个 GET 请求,以返回一个 Atom Feed 文档(见清单 3):

http://quickr.acme.com/dm/atom/library/5d06ab0044ed8129bd5ebd4caeec5df1/feed

清单 3. Atom Feed 文档
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:base="http://quickr.acme.com/dm/atom/library/
5d06ab0044ed8129bd5ebd4caeec5df1/" xmlns="http://www.w3.org/2005/Atom">
  <generator uri="http://quickr.acme.com/dm/atom" 
  version="1.0">Teamspace Documents</generator>
  <id>urn:lsid:ibm.com:td:5d06ab0044ed8129bd5ebd4caeec5df1</id>
  <link href="feed" rel="self"></link>
  <link href="http://quickr.acme.com/wps/mypoc?uri=
  dm:5d06ab0044ed8129bd5ebd4caeec5df1&verb=view" rel="alternate"></link>
  <link href="feed?pagesize=2&page=3" rel="next"></link>
  <link href="feed?pagesize=2&page=1" rel="previous"></link>
  <collection href="feed" xmlns="http://purl.org/atom/app#">
    <atom:title type="text" xmlns:atom="http://www.w3.org/2005/
    Atom">Architecture Documents</atom:title>
    <accept>application/*,image/*,*/*</accept>
  </collection>
  <author>
    <uri>uid=jsmith,o=acme</uri>
    <name>John Smith</name>
    <email>jsmith@acme.com</email>
  </author>
  <title type="text">Architecture Documents</title>
  <updated>2007-04-10T13:07:00.672Z</updated>

  <entry xml:lang="en">
    <id>urn:lsid:ibm.com:td:7f37550044f10d5b9144bbd6afe18010</id>
    <link href="document/7f37550044f10d5b9144bbd6afe18010/entry" rel="self"></link>
    <link href="http://quickr.acme.com/wps/mypoc?uri=
    dm:7f37550044f10d5b9144bbd6afe18010&verb=view" rel="alternate"></link>
    <link href="document/7f37550044f10d5b9144bbd6afe18010/entry" rel="edit"></link>
    <link href="document/7f37550044f10d5b9144bbd6afe18010/media" 
    rel="edit-media"></link>
    <link href="document/7f37550044f10d5b9144bbd6afe18010/media" 
    rel="enclosure" type="application/msword" title="Architecture Guidelines.doc" 
    hreflang="en" length="19968"></link>
    <category term="document" scheme="tag:ibm.com,2006:td/type" 
    label="document"></category>
    <author>
      <uri>uid=jsmith,o=acme</uri>
      <name>John Smith</name>
      <email>jsmith@acme.com</email>
    </author>
    <title type="text">Architecture Guidelines.doc</title>
    <published>2007-04-11T16:51:04.594Z</published>
    <updated>2007-04-11T16:51:04.594Z</updated>
    <td:created>2007-05-13T19:03:25.500Z</td:created>
    <td:modified>2007-05-13T19:03:25.500Z</td:modified>
    <td:modifier>
      <td:uri> uid=jdoe,o=acme </td:uri>
      <td:name>John Doe</td:name>
      <td:email>jdoe@acme.com </td:email>
    </td:modifier>
    <summary type="html"><span><img align="middle" 
    src="thumbnail/7f37550044f10d5b9144bbd6afe18010/
    media></span><span>&nbsp;&nbsp;</span><span>
    General guidelines for architecture </span></summary>
    <content type="application/msword" xml:lang="en" 
    src="document/7f37550044f10d5b9144bbd6afe18010/media"></content>
  </entry>

  <entry>
    <id>urn:lsid:ibm.com:td:5dc3f38044eee4ca90d8bad6afe18010</id>
    <link href="folder/5dc3f38044eee4ca90d8bad6afe18010/entry" rel="self"></link>
    <link href="http://quickr.acme.com/wps/mypoc?uri=
    dm:5dc3f38044eee4ca90d8bad6afe18010&verb=view" rel="alternate"></link>
    <link href="folder/5dc3f38044eee4ca90d8bad6afe18010/entry" rel="edit"></link>
    <category term="folder" scheme="tag:ibm.com,2006:td/type" 
    label="folder"></category>
    <author>
      <uri>uid=jsmith,o=acme</uri>
      <name>John Smith</name>
      <email>jsmith@acme.com</email>
    </author>
    <title type="text">Johns Folder</title>
    <published>2007-04-10T23:58:29.219Z</published>
    <updated>2007-04-10T23:58:29.219Z</updated>
    <td:created>2007-05-13T19:03:25.500Z</td:created>
    <td:modified>2007-05-13T19:03:25.500Z</td:modified>
    <td:modifier>
      <td:uri> uid=jdoe,o=acme </td:uri>
      <td:name>John Doe</td:name>
      <td:email>jdoe@quickr.acme.com </td:email>
    </td:modifier>
    <summary type="text">John's architecture documents</summary>
    <content type="application/atom+xml" 
    src="folder/5dc3f38044eee4ca90d8bad6afe18010/feed"></content>
  </entry>

</feed>

对于库根下面的每个文件夹和文档,这个 feed 包含一个 Atom Entry。<category> 元素指出这个条目对应于一个文件夹,还是对应于文档。在树中将文件夹显示为可展开的节点,因为它们可以包含子文件夹和文档。文档显示为叶节点。在 feed 文档中可以看到,每个条目都有一个链接到本身的URL,这是 Atom 中另一个有用的模式,也是链接元素的用途。链接提供了内容项目上相关的内容、视图或动作的 URL。这使应用程序不必处理编写这些 URL 的一些工作(尽管,正如前面提到的,这些 URL 是可预测的)。

对于文件夹条目,将 self 链接存储在 TreeParent 实例中,后面将用它来编写文件夹的 feed URL。就像使用库 feed URL 存储和检索库内容一样,可以使用文件夹的 feed URL 存储或检索文件夹的内容。

对于文档条目,需要获取文档的内容。因此,把文档条目的 edit-media 链接存储在它的 TreeObject 实例中。关于这些链接关系的说明,请参阅 Atom Publishing Protocol 规范。清单 4 给出将 Atom Entry 处理为树节点的部分代码。

清单 4. 将 Atom Entry 处理为树节点的部分代码
ClientResponse response = httpClient.get(url);
if (response.getStatus() == 200) {
	Feed feed = (Feed) response.getDocument().getRoot();
	Iterator entries = feed.getEntries().iterator();
	while (entries.hasNext()) {
		boolean isFolder = false;
		Entry entry = (Entry) entries.next();
		try {
			Iterator categories = entry.getCategories(
				"tag:ibm.com,2006:td/type").iterator();
			while (categories.hasNext()) {
				Category cat = (Category) categories.next();
				if (cat.getTerm().equals("folder")) {
					isFolder = true;
					break;
				}
			}
		} catch (IRISyntaxException e) {
			e.printStackTrace();
		}
		if (isFolder) {
			TreeParent folder = new TreeParent(entry, false);
			addChild(folder);
		} else {
			TreeObject document = new TreeObject(entry);
			addChild(document);
		}
	}
}

在处理了库的 Atom Feed 文档之后,插件显示库的内容(见图 2)。

图 2. 插件显示库的内容
插件显示库的内容

获取文档内容

文档的媒体内容是用户希望阅读或更新的数据。例如,媒体内容可以是 PDF 文件。在显示库中的文档列表之后,用户可以通过双击文档查看其媒体内容。当用户双击文档时,需要在相关联的应用程序中打开文档(比如用 Adobe Reader 打开 PDF 文件)。首先,需要获得文档的媒体内容,让应用程序有数据可处理。这需要使用保存在文档节点的 TreeObject 实例中的 edit-media 链接:

http://quickr.acme.com/dm/atom/library/5d06ab0044ed8129bd5ebd4caeec5df1
/document/7f37550044f10d5b9144bbd6afe18010/media

在对这个 URL 发出 GET 请求时,会在响应中返回文档的媒体内容。读取响应,将它保存在一个临时文件中,然后使用相关联的应用程序运行这个临时文件。清单 5 给出执行这个操作的代码。

清单 5. 下载和运行文档的部分代码
public void doOpen(String url) {
    TreeObject item = (TreeObject) ((IStructuredSelection) viewer
        .getSelection()).getFirstElement();
    try {
        String tmpDir = File.createTempFile("qkr",
            "tmp").getParent();
        File file = new File(tmpDir + "/" + item.getName());
        if (file.exists()) {
            file.delete();
        }
        ClientResponse response = httpClient.get(url);
        if (response.getStatus() == 200) {
                BufferedInputStream bis = new BufferedInputStream(
                    response.getInputStream());
                file.createNewFile();
                BufferedOutputStream bos = new BufferedOutputStream(
                    new FileOutputStream(file));
                byte[] bytes = new byte[1024];
                int count = 0;
                while ((count = bis.read(bytes)) != -1) {
                    bos.write(bytes, 0, count);
                }
                bis.close();
                bos.close();

                // open the local file in editor
                IWorkspace ws = ResourcesPlugin.getWorkspace();
                IProject project = ws.getRoot().getProject("Quickr Files");
                if (!project.exists()) {
                    project.create(null);
                }
                if (!project.isOpen()) {
                    project.open(null);
                }
                IPath location = new Path(file.getPath());
                IFile iFile =
                    project.getFile(location.lastSegment());
                if (!iFile.exists()) {
                    iFile.createLink(location, IResource.NONE, null);
                }
                FileEditorInput fileEditorInput = new FileEditorInput(iFile);
                IEditorRegistry registry = PlatformUI.getWorkbench()
		    .getEditorRegistry();
                IWorkbenchPage page = getViewSite().getWorkbenchWindow()
                    .getActivePage();
                if (page != null) {				
                    if( registry.isSystemExternalEditorAvailable(
                        iFile.getName()))
                    {
                        page.openEditor( fileEditorInput,	
                            IEditorRegistry.SYSTEM_EXTERNAL_EDITOR_ID);

                    } else if
                        (registry.isSystemInPlaceEditorAvailable(iFile
                        .getName())) {

                        page.openEditor(fileEditorInput,
                            IEditorRegistry.SYSTEM_INPLACE_EDITOR_ID);
                    } else {
                        showMessage("No viewer available for this file type. 
                            You can download it instead.");
                    }
                }
        }
        response.release();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (CoreException e) {
        e.printStackTrace();
    }
}

上载文件

现在假设您希望从库(或文件夹)的上下文菜单中选择 Upload,从而将本地文件存储在 Lotus Quickr 中。这需要使用树节点的 TreeParent 实例中存储的 feed URL:

http://quickr.acme.com/dm/atom/library/5d06ab0044ed8129bd5ebd4caeec5df1/feed

在前面,通过向这个 URL 发送 GET 请求来检索库的内容。现在,通过向这个 URL 发送 POST 请求来存储文档。这个请求是由 APP 规范定义的,它使用 POST 操作创建资源。在发送 POST 请求时,把文档的媒体内容写在请求内容中。但是,服务器如何知道把文件保存在库中的什么地方以及使用哪个文件名?

答案就是 Slug 头,可以通过 Slug 头向 Lotus Quickr 建议采用哪个名称作为文档的文件名。将 Slug 头的值设置为文档相对于库根的相对路径。例如,如果希望将文件 MyFile.doc 存储在库中的 MyFolder 文件夹中,那么应该将 Slug 头设置为 /MyFolder/MyFile.doc。清单 6 给出了在插件中执行这个操作的代码。

清单 6. 在文档库中存储本地文件的代码
public void doUpload(String url) {
    FileDialog dlg = new FileDialog(getViewSite().getShell(), SWT.OPEN);
    String path = dlg.open();
    if (path != null) {
        File file = new File(path);
        RequestOptions options = new RequestOptions();
        options.setSlug(file.getName());
        options.setContentType("unknown/unkown");
        options.setHeader("Content-Length", String.valueOf(file.length()));
        try {
            ClientResponse response = httpClient.post(url,
                new FileInputStream(file), options);
            if (response.getStatus() != 201) {
                showMessage(response.getStatusText());
            } else {
                ISelection selection = viewer.getSelection();
                TreeParent parent = (TreeParent) ((IStructuredSelection) selection)
                    .getFirstElement();
                parent.clear();
                viewer.refresh(parent);
            }
            response.release();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在成功地处理这个请求之后,服务在响应中返回状态码 201。响应还包含一个 Location 头,其中包含刚创建的文档内容的 URL(即 edit-media 链接)。可以使用这一信息更新树,以显示新的文档节点。为了简化,这个插件会刷新父节点来显示新添加的文档。最终,文件会添加到库中并显示在树中。


删除文档

现在,看看如何使用这个插件从库中删除文档。这需要使用库节点下面每个节点中存储的 self 链接。Quickr REST 服务不支持删除库本身,所以我们不在库节点上提供 Delete 菜单(见清单 7)。

清单 7. 删除文档的代码
	public void doDelete(String url) {
		String entryUrl = url;
		if (url.endsWith("/media")) {
			entryUrl = entryUrl.substring(0, url.lastIndexOf('/'));
			entryUrl += "/entry";
		}
		TreeObject item = (TreeObject) ((IStructuredSelection) viewer
				.getSelection()).getFirstElement();
		TreeParent parent = item.getParent();
		RequestOptions options = new RequestOptions();
		options.setHeader("X-Method-Override", "DELETE");
		ClientResponse response = httpClient.post(entryUrl,
				new StringRequestEntity(""), options);
		if (response.getStatus() == 200) {
			parent.clear();
			viewer.refresh(parent);
		}
	}

最后,简要介绍一下其他菜单。Refresh 操作的作用是获取库或文件夹的内容。Download 操作是 Doubleclick 操作的一部分,Doubleclick 操作会下载并运行文件。


结束语

Lotus Quickr REST 服务提供了一种操作文档的简便方法。因为操作是通过 REST 样式的 URL 定义的,所以您可以使用自己喜欢的编程语言(这个示例中使用 Java)。本文演示了如何使用这些服务执行基本的文档操作,并解释了每种操作的 URL 请求和响应。

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Lotus
ArticleID=266773
ArticleTitle=IBM Lotus Quickr REST 服务简介
publish-date=04302010