【IT168 资讯】Amazon.com 的老用户可能会记得“Eyes”服务,这个服务可以让您设置自己感兴趣的产品的关键字列表。在出现与您所设置的关键字列表匹配的新产品时,就会向您发送一个电子邮件通知,其中包含这些新产品的链接,以及一些您可以在自己的 Web 站点上使用的服务的介绍,如果您是 Amazon 的会员,这会非常有用。随着使用这项服务的人数不断增加,Amazon.com 决定停止这项服务,因为它对处理器的需求非常高,并且难以维护。
之后,Amazon.com 提供了一项称为 Amazon Web Service 或 AWS 的 Web 服务,您可以使用它在自己的 Web 站点上查询 Amazon 的搜索引擎。这个 Web 服务允许我们创建一个与 Eyes 服务类似的应用程序。您可以将 Amazon Web Service 集成到一个 Notes 应用程序中,从而创建一个比 Eyes 更好的服务;集成方案提供了一种方法,通过这种方法,您可以使用自己的标准对书籍进行分类、标记不相关的书籍,以及在某本书籍停止出版时进行跟踪记录。这种技术目前被用来更新 Notes FAQ Web 站点 上的书籍清单。当然,也可以对 Amazon 上其他类型的产品清单使用这种技术。
本文共有两部分,在第 1 部分中,我们将简要介绍如何在一个 Notes 应用程序中集成 Amazon Web Service(AWS),从而可以使用 Notes Java API 查询 Amazon.com 的搜索引擎。我们还将介绍如何创建一个代理来删除对过期书籍的引用。在本文的第 2 部分中,我们将介绍如何使用 RDBMS 和 J2EE 技术重新创建这个应用程序。本文假设您具有开发 Notes 应用程序的经验,并且具有 Java 编程技巧。您可以从 Amazon.com 的 Web 站点上直接下载 Amazon 的 Web Services SDK。
SOAP 和 Web 服务
通过 Web 服务进行通信有很多方法,但是通常都使用以下两种方法:XML/HTTP 或者 SOAP(Simple Object Access Protocol)。XML/HTTP 方法使用了一种针对请求的专门格式化了的 URL。由于 XML/HTTP 访问方法使用一些编码后的参数来发起请求,因此它限制了 URL 的最大长度,这个长度大概是 1,024 个字符(有些服务器可以支持的长度更长,但是 1,024 个字符是一个比较安全的设置)。您所接收到的结果是使用 XML/SOAP 进行编码过的。您可以使用 XSLT 对 XML 进行转换,或者使用 XML 解析器从 XML 读取所需的值。然而,编写 XML 解析器的工作是非常繁琐的。
用来访问 Web 服务的 SOAP 技术利用了 Web 服务提供者所提供的 WSDL(Web Services Description Language)的定义。如果您要发布一个 Web 服务,就需要 WSDL 定义;通常可以通过一个 URL 来使用 Web 服务,这一点在本文后面部分将会看到。在这里,Amazon Web Service 提供了 WSDL 的定义。WSDL 定义描述了如何对请求进行格式化,以及如何读取响应信息,还将告诉您可以使用哪些请求。WSDL 是位于 Web 服务之上的另外一个复杂的层次,但是它真正提供的是生成“封装程序”来访问 Web 服务所需的桥梁,就仿佛这些服务在您自己的机器上一样。
可以使用以友好方式支持 Web 服务的任何语言生成调用 Web 服务的封装程序,这些语言包括 Java、C、VB、PHP、Perl、TCL,等等。另外,在“SOAP 请求”中发送的信息在大小方面存在限制,因为这些信息是在一个 Web 请求体中发送的,而不是在 URL 中发送的;这可以让您发出针对 Web 服务的复杂请求,因为在 Web 请求体中没有大小的限制。
Amazon Web Service
在 Amazon.com Web Service 页面 中,您可以看到更多有关 Amazon Web 服务的信息。您可以看到 SDK 中提供的 Java、Perl、PHP 和 VB 的示例代码。这个工具中还包括使用 SDK 的文档。这里还有一个开发人员论坛,Amazon 会对其进行监视。在可以调用 Amazon Web 服务的 API 之前,首先需要注册一个开发人员帐号。在 Amazon 的客户使用 Amazon Associate Web 站点来添加 Amazon 的订单记录时,可以使用这个开发人员帐号来提供 Amazon Associate 的推荐记录。如果您使用 Amazon 的 XSLT 服务将 XML 结果转换成 HTML 格式,那么该帐号将自动嵌入所返回的 URL 中,这样您就可以获得推荐费了。
Amazon 在自己的 Web 服务实现中提供了很多功能,这是可以使用 Web 服务来实现哪些功能的一个很好例证。它们不但提供了对搜索引擎的调用功能,而且还提供了管理 Amazon 用户的购物车、购买意向和邮购信息等级的功能。实际上,Web 服务的意图在于允许 Amazon Associate(他们支付推荐费)使用一种更加灵活的方法来构建 Web 店面(Web storefront),从而帮助销售更多的 Amazon 产品。这就是为什么它是一个很好的 Web 服务的例子:它可以帮助 Web 服务的提供者增加收益。
对于本文来说,我们感兴趣的内容是 Amazon 搜索引擎的 Web Services API。在开始之前,我们需要准备以下内容:
安装 Amazon Web Service。有关的更多信息,请参阅 Amazon Web 服务的 SDK 中的文档。
为了进行测试,我们创建一个 Notes 应用程序来使用 AWS。
要使用 Amazon 搜索引擎,您必须在 Notes 应用程序中指定的第一项内容是称为模式的东西;这等同于您在 Amazon.com 主页上可以看到的高级附签:书籍、音乐、工具,等等。然后您就可以根据以下内容进行搜索:关键字、浏览节点(产品目录,例如如果将模式设置为书籍,就可以查找科幻小说)、ASIN、ISBM、UPC、作者、音乐家、演员、导演、发行商、ListMania! 和类似产品(顾客可能购买的其他产品)。对于我们正在构建的 BetterEyes 服务来说,我们将使用关键字进行搜索。
Notes/Domino 6 中对 XML 的支持
Notes/Domino 6 包含了对 XML 的支持,这与 Notes/Domino R5 不同,后者需要添加一个工具包才能支持 XML。我们将来了解一下如何使用 Notes 的 XML 支持来构建 BetterEyes 服务,然后再来了解一下为什么应该使用 SOAP。在 Amazon 的 Web 服务中有一个 XML/HTTP 的例子,请使用 Internet Explorer 5.5 或更高的版本来查看 这个 URL 的内容。
您会看到一个经过很好的格式化的 XML 结果,然后可以单击 + 和 - 符号来展开或收缩层次结构。
使用 Notes/Domino 5 或 Notes/Domino 6,就可以使用 Personal Web Navigator 来查询 Amazon.com 的内容,下面是 Notes LotusScript 引擎中的一段代码:
Dim filename as String
filename$ = "c:\temp\amazon.xml"
Dim NURLdb As New NotesDatabase("","")
' To reference the Internet browser database
Dim Nwebdoc As NotesDocument
If NURLdb.OpenURLDb Then
Dim filename As String
' download the XML encoded WS reply
If (False) Then
theURL = "http://xml.amazon.com/onca/xml?v=2.0&dev-t=webservices-20
&t=thelotusnotesfaqA&KeywordSearch=Lotus+Notes&mode=books
&type=lite&page=1&f=xml"
Set Nwebdoc=NURLdb.getdocumentbyurl(theUrl, True, False)
' get XML from attachment
Dim Item As NotesItem
Dim Object As NotesEmbeddedObject
Set Item = Nwebdoc.GetFirstItem("$FILE")
Dim ItemName As String
ItemName = Item.Values(0)
Set Object = Nwebdoc.GetAttachment(ItemName)
Call Object.ExtractFile(filename$)
End If
End If
当 Amazon 发回 XML 响应时,将其作为一个附件保存在 Personal Web Navigator 数据库中。需要将附件的内容保存到文件系统中,这样就可以从响应中处理 XML 的内容了。Notes/Domino 6 有两个内嵌的 XML 解析器:SAX 和 DOM。SAX 解析器使用的内存更少,因为它是一个回调风格的 API,因此它不需要将整个 XML 文档都保存在内存中。DOM 解析器更易于使用,因此我们将着重使用 DOM。有关 SAX 和 DOM 解析器的更多内容,请参阅 LDD Today 撰写的文章“LotusScript: XML classes in Notes/Domino 6”。
下面的代码将输出从 Amazon Web 服务查询中返回的 XML 结果:
' open XML file as a stream
Dim xml_in As NotesStream
Set xml_in=session.CreateStream
If xml_in.Open(filename$) Then
If xml_in.Bytes = 0 Then
Messagebox filename$ + " is empty"
End If
' create a temporary output file because it's not a pipelined XML operation
Dim xml_out As NotesStream
Dim outfilename as String
outfilename$ = "c:\temp\amazon.tmp" ' create output file
Set xml_out=session.CreateStream
If Not xml_out.Open(outfilename$) Then
Messagebox "Cannot create " & outfilename$,, "TXT file error"
Exit Sub
End If
xml_out.Truncate
' parse using DOM parser
Set domParser=session.CreateDOMParser(xml_in, xml_out)
On Error Goto DumpDOMLog
DOMParser.ExitOnFirstFatalError = True
DOMParser.InputValidationOption = 0
' disable validation of problematic Amazon XML results
domParser.Process
On Error Goto 0
Dim docNode As NotesDOMDocumentNode
Set docNode = domParser.Document
' last child at root is ProductInfo
Dim productinfoNode As NotesDOMNode
Set productinfoNode = docNode.LastChild
' iterate through all the Details children
Dim detailsNode As NotesDOMNode
Set detailsNode = productinfoNode.FirstChild
While (Not detailsNode.IsNull)
Dim productname As String
Dim publisher As String
productname = ""
Dim infoNode As NotesDOMNode
Set infoNode = detailsNode.FirstChild
While (Not infoNode.IsNull)
If (infoNode.NodeName = "ProductName") Then
productname = infoNode.FirstChild.NodeValue
Elseif (infoNode.NodeName = "Manufacturer") Then
publisher = infoNode.FirstChild.NodeValue
End If
Set infoNode = infoNode.NextSibling
Wend
If (productname <> "") Then
Messagebox "Found " + productname
+ " from " + publisher
End If
Set detailsNode = detailsNode.NextSibling
Wend
End If
Exit Sub
DumpDOMLog:
Msgbox "DOMParser error: " & DOMParser.log
Exit Sub
正如您可以看到的,这段代码非常复杂,它创建了一些代码来遍历 XML 文档,从而检索所需要的信息。XML 输入验证被关闭,因为 Amazon 的 XML 响应存在问题,在 DOMParser 的错误记录中,该问题表现为一个无效的 URL;关闭这个有效性验证在处理很多 XML 时是必需的,因为可能会错误地生成 XML。另外,还需要对 XML 模式有足够的了解,知道自己希望了解哪些级别的信息。在上面这个例子中,作者信息位于一个级别较低的节点上。您还需要了解这些节点是如何命名的,并且需要正确拼写它们(如果拼写错误,就不能进行什么错误检查了)。
在下一节中,我们将介绍 SOAP 封装程序是如何对此进行简化,并以更加直观和更加安全的方式表现它们。
Axis SOAP 封装程序
正如前面所介绍的,大部分语言都有一个 SOAP 封装程序生成器。这些生成器使用一个 WSDL 描述并生成一个封装对象,这样就可以使用 Web 服务,就像它是语言的一部分那样。单击这里 查看 Amazon 的 Web Service WSDL 描述的内容。这是最新版本的 Amazon Web Service WSDL 描述。您可以将其保存为一个文本文件 AmazonWebServices.wsdl。
在 Java 中,实际的 SOAP 封装程序来自 Apache Web 服务器项目。该项目的名称为 Axis,您可以从 Apache Axis Web 站点 上下载它。Axis 在很多工具中以各种形式得到了广泛的应用,这些工具中包括 IBM 的 Web Services Toolkit 和 Borland 的 JBuilder。尽管我们只是要使用它来生成一个 SOAP 封装程序与一个 Web 服务进行通信,但是这也需要生成封装程序,这样您就可以作为 Web 服务来提供一些 Java 类了。
在下载 Axis 之后,请按照 Web 站点上的提示来安装它。完成安装 Axis 之后,就可以使用下面以下命令行告诉 Axis 为 Amazon Web 服务生成封装类了:
java java org.apache.axis.wsdl.WSDL2Java AmazonWebServices.wsdl
--output build/axis --verbose --package com.amazon.soap.axis
这个命令将生成一些 Java 文件,它们将编译到 com.amazon.soap.axis Java 包中。下面是生成 ProductInfo XML 节点使用的代码:
/**
* ProductInfo.java
*
* This file was auto-generated from WSDL
* by the Apache Axis WSDL2Java emitter.
*/
package com.amazon.soap.axis;
public class ProductInfo implements java.io.Serializable {
private java.lang.String totalResults;
private java.lang.String listName;
private com.amazon.soap.axis.Details[] details;
public ProductInfo() {
}
public java.lang.String getTotalResults() {
return totalResults;
}
public void setTotalResults(java.lang.String totalResults) {
this.totalResults = totalResults;
}
public java.lang.String getListName() {
return listName;
}
public void setListName(java.lang.String listName) {
this.listName = listName;
}
public com.amazon.soap.axis.Details[] getDetails() {
return details;
}
public void setDetails(com.amazon.soap.axis.Details[] details) {
this.details = details;
}
private java.lang.Object __equalsCalc = null;
public synchronized boolean equals(java.lang.Object obj) {
if (!(obj instanceof ProductInfo)) return false;
ProductInfo other = (ProductInfo) obj;
if (obj == null) return false;
if (this == obj) return true;
if (__equalsCalc != null) {
return (__equalsCalc == obj);
}
__equalsCalc = obj;
boolean _equals;
_equals = true &&
((totalResults==null &&
other.getTotalResults()==null) ||
(totalResults!=null &&
totalResults.equals(other.getTotalResults()))) &&
((listName==null && other.getListName()==null) ||
(listName!=null &&
listName.equals(other.getListName()))) &&
((details==null && other.getDetails()==null) ||
(details!=null &&
java.util.Arrays.equals(details, other.getDetails())));
__equalsCalc = null;
return _equals;
}
private boolean __hashCodeCalc = false;
public synchronized int hashCode() {
if (__hashCodeCalc) {
return 0;
}
__hashCodeCalc = true;
int _hashCode = 1;
if (getTotalResults() != null) {
_hashCode += getTotalResults().hashCode();
}
if (getListName() != null) {
_hashCode += getListName().hashCode();
}
if (getDetails() != null) {
for (int i=0;
i<java.lang.reflect.Array.getLength(getDetails());
i++) {
java.lang.Object obj
= java.lang.reflect.Array.get(getDetails(), i);
if (obj != null &&
!obj.getClass().isArray()) {
_hashCode += obj.hashCode();
}
}
}
__hashCodeCalc = false;
return _hashCode;
}
// Type metadata
private static org.apache.axis.description.TypeDesc typeDesc =
new org.apache.axis.description.TypeDesc(ProductInfo.class);
static {
org.apache.axis.description.FieldDesc field
= new org.apache.axis.description.ElementDesc();
field.setFieldName("totalResults");
field.setXmlName(new javax.xml.namespace.QName("", "TotalResults"));
field.setXmlType(new javax.xml.namespace.QName
(" http://www.w3.org/2001/XMLSchema ", "string"));
typeDesc.addFieldDesc(field);
field = new org.apache.axis.description.ElementDesc();
field.setFieldName("listName");
field.setXmlName(new javax.xml.namespace.QName("", "ListName"));
field.setXmlType(new javax.xml.namespace.QName
(" http://www.w3.org/2001/XMLSchema ", "string"));
typeDesc.addFieldDesc(field);
field = new org.apache.axis.description.ElementDesc();
field.setFieldName("details");
field.setXmlName(new javax.xml.namespace.QName("", "Details"));
field.setXmlType(new javax.xml.namespace.QName
("urn:PI/DevCentral/SoapService", "DetailsArray"));
typeDesc.addFieldDesc(field);
};
/**
* Return type metadata object
*/
public static org.apache.axis.description.TypeDesc getTypeDesc() {
return typeDesc;
}
/**
* Get Custom Serializer
*/
public static org.apache.axis.encoding.Serializer getSerializer(
java.lang.String mechType,
java.lang.Class _javaType,
javax.xml.namespace.QName _xmlType) {
return
new org.apache.axis.encoding.ser.BeanSerializer(
_javaType, _xmlType, typeDesc);
}
/**
* Get Custom Deserializer
*/
public static org.apache.axis.encoding.Deserializer getDeserializer(
java.lang.String mechType,
java.lang.Class _javaType,
javax.xml.namespace.QName _xmlType) {
return
new org.apache.axis.encoding.ser.BeanDeserializer(
_javaType, _xmlType, typeDesc);
}
}
这段代码看起来非常复杂,但是它的基本功能是允许简单地访问 XML 的结果,实现这一点的方法是将 XML ProductInfo 节点的属性当作 Java String 对象看待,将下面的 XML 节点当作 Java 数组对象看待。封装程序还要使这些对象“连续”,这样您就可以将 XML 节点通过网络转换成一个 Java 程序,或者将 XML 节点保存到文件中。
在生成封装类之后,您就可以创建一个简单的程序来调用 Amazon Web Service。将以下代码保存到 AmazonUpdate.java 文件中:
import com.amazon.soap.axis.*;
public class AmazonUpdate
{
public static void main(String[] args) throws Exception
{
AmazonSearchService service = new AmazonSearchServiceLocator();
AmazonSearchPort port = service.getAmazonSearchPort();
KeywordRequest request = new KeywordRequest();
request.setKeyword(java.net.URLEncoder.encode("Lotus Notes"));
request.setMode("books");
request.setTag("");
request.setType("lite");
request.setDevtag("");
request.setPage("1");
ProductInfo result = null;
try {
result = port.keywordSearchRequest(request);
} catch (Exception e) {
e.printStackTrace();
}
Details[] details = result.getDetails();
for (int i = 0; i < details.length; i++) {
// read results out
String resultTitle = details[i].getProductName();
System.out.println("Title #" + i + " is " + resultTitle);
}
}
}
现在您可以使用下面的命令来编译这个程序和 SOAP 封装程序了:
javac AmazonUpdate.java build/axis/com/amazon/soap/axis/*.java
使用下面的命令来运行这个程序:
java -classpath build/axis/com/amazon/soap/axis;%CLASSPATH% AmazonUpdate
它会输出第一个结果页面中的书籍的标题。注意,您并不需要了解太多有关 Amazon Web 服务的内容,甚至不需要指定 URL 来访问 Amazon Web Service,因为这些信息都内嵌在 WSDL 文件中的。您还要进行 Java 的类型/方法检查,确保没有错误地调用 Amazon Web Services API,也没有使用错误的数据类型对结果进行解释。
AmazonUpdate Java 程序
现在我们已经有一个可以调用 Amazon Web 服务的基本框架了,接下来就可以开发所需要的完整应用程序了。不幸的是,在 Amazon 的 Web Services API 中有一个 bug:所返回的结果总数并不正确。每个 Web 服务调用最多可以返回 10 个结果。我们需要使用一个循环进行遍历,直到所返回的结果少于 10 个或出现错误为止。循环体如下所示:
boolean fKeepgoing = true;
int realcount = 0;
int page = 0;
do {
page++;
request.setPage("" + page);
ProductInfo result = null;
try {
result = port.keywordSearchRequest(request);
} catch (Exception e) {
org.apache.axis.AxisFault afe = null;
if (e instanceof org.apache.axis.AxisFault) {
afe = (org.apache.axis.AxisFault) e;
}
if ((afe == null) || (afe.getFaultString().indexOf("Bad Request") < 0)) {
// Amazon's results "end" when the page you request gets a request
error or if less than 10 results are returned
e.printStackTrace();
}
fKeepgoing = false;
break;
}
Details[] details = result.getDetails();
for (int i = 0; i < details.length; i++) {
// values that will be passed to tracker
String resultTitle = null;
String resultISBN = null;
String resultASIN = null;
String resultAuthors = null;
String resultPublisher = null;
// read results out
resultTitle = details[i].getProductName();
resultASIN = details[i].getAsin();
if (details[i].getIsbn() != null) {
resultISBN = details[i].getIsbn();
}
String authors[] = details[i].getAuthors();
String authorlist = "";
if (authors != null) {
for (int j = 0; j < authors.length; j++) {
if (j > 0) {
authorlist += ", ";
}
authorlist += authors[j];
}
}
resultAuthors = authorlist;
if (details[i].getManufacturer() != null) {
resultPublisher = details[i].getManufacturer();
}
// give results to tracker
tracker.updateBook(
resultTitle,
resultASIN,
resultISBN,
resultAuthors,
resultPublisher);
// increment result counter
realcount++;
}
if (details.length < 10) {
fKeepgoing = false;
}
} while (fKeepgoing);
BookUpdate 接口
您可能会纳闷 tracker.updateBook() 调用到底是用来干什么用的。它是一个 Java 接口调用,使用它可以更新我们要跟踪的书籍的数据库。通过将其声明为一个接口,可以使用不同的类来获取这些信息,这样就可以对应用程序进行调试,或者将信息写入一个 Notes 数据库、关系型数据库或其他数据存储器中。
public interface BookTracker
{
public void updateBook(String title, String ASIN, String ISBN,
String authors, String publisher, String rating, int numreviews)
throws Exception;
}
调试初步:将调试信息输出到标准输出设备上
正如您猜想的那样,调试类非常简单,因为它所做的工作不过是向标准输出设备输出一些信息,这些信息就是运行 Java 应用程序时在命令窗口中显示的信息。这个类如下所示:
public class DumpBookTracker implements BookTracker {
// update book from amazon query
public void updateBook(
String title,
String ASIN,
String ISBN,
String authors,
String publisher) {
System.out.println(title);
System.out.println("; ASIN: " + ASIN);
if (ISBN != null) {
System.out.println("; ISBN: " + ISBN);
}
if (publisher != null) {
System.out.println("; Publisher: " + publisher);
}
if (authors != null) {
System.out.println("; Authors: " + authors);
}
}
}
将新书保存到 Notes 数据库中
现在我们有地方来存放 Java 应用程序找到的每本书了,接下来可以编写一个 BookTracker 类,该类知道如何将这些书籍保存到 Notes 数据库中。它比那个调试类稍微复杂一些,但也不是很复杂。它使用 Notes Java API,因此您需要在自己的 classpath 中包含 notes.jar 文件(在 notes.exe 所在的 Notes 目录中):
import lotus.domino.*;
public class NotesBookTracker implements BookTracker {
lotus.domino.Session s;
Database db;
View booksview;
// constructor
public NotesBookTracker(String server, String dbname)
NotesException {
// initialize Notes access
NotesThread.sinitThread();
s = NotesFactory.createSession();
// get book ISBN view
db = s.getDatabase(server, dbname);
booksview = db.getView("(LUBookASIN)");
}
// Java pseudo-destructor
public void finalize() throws NotesException {
// clean up Notes
NotesThread.stermThread();
}
// update book from amazon query
public void updateBook(
String title,
String ASIN,
String ISBN,
String authors,
String publisher)
throws NotesException {
// check for blank ISBN
if (ISBN == null) {
System.out.println(title + " has a null ISBN!");
}
// check for blank ASIN
if (ASIN == null) {
System.out.println(title + " has a null ASIN!");
}
Document doc;
// see if we can find the book
doc = booksview.getDocumentByKey(ASIN, true);
if (doc == null) {
// new book, so we have to add it to the database
doc = db.createDocument();
doc.replaceItemValue("Form", "Book");
doc.replaceItemValue("Title", title);
doc.replaceItemValue("ASIN", ASIN);
if (ISBN != null) { // E-books don't have an ISBN
doc.replaceItemValue("ISBN", ISBN);
}
if (authors != null) {
doc.replaceItemValue("Authors", authors);
}
if (publisher != null) {
doc.replaceItemValue("Publisher", publisher);
}
doc.replaceItemValue("State", "Added");
}// tag it w/ timestamp so we know when it was last found
// so we can expire unfound books in a scheduled agent
DateTime timenow = s.createDateTime("Today");
timenow.setNow();
doc.replaceItemValue("LastFound", timenow);
// save document
doc.save();
}
}
结束语
我们已经创建了一个使用 Amazon Web Service 的非常有用的 Java 应用程序。开源的 Axis SOAP 代理生成器将这个任务变得太过简单,因为它看起来就像是我们正在使用一些普通的 Java 类;只要这个 Web 服务提供一个 WSDL 文件,就无需要担心如何访问这个 Web 服务,也无需要担心如何与之进行交谈。
通过将来自 Amazon Web Service 的信息保存到一个 Note 数据库中,可以构建一个比 Amazon 以前的 Eyes 服务更有用的程序,因为我们既可以添加新书,也可以跟踪那些已经不再出版的书籍。此外,可以将这个 Notes 数据库放到 Domino 服务器上,通过 Web 对其进行访问。所有的 Notes 工作都可以在几个小时内完成,而且为 Amazon 通过 Amazon Web 服务提供的许多有用信息提供了一个接口。