简介: 本系列介绍与 IBM Lotus Domino Web 服务相关的内容,这是系列中第三篇也是最后一篇文章,我们探讨了使用 Domino Web 服务的更高级的技术,例如复杂数据类型、枚举、文件附件(file attachment)和定制故障。
本系列的第一篇文章 “IBM Lotus Domino 7 中的实用 Web 服务:什么是 Web 服务以及为何如此重要 ?” 涵盖了 Web 服务和 SOA 概念;第二篇文章 “IBM Lotus Domino 7 中的实用 Web 服务:编写和测试简单的 Web 服务” 概述了在 Lotus Domino V7 中编写和测试简单 Web 服务的细节。本系列的第三篇、也是最后一篇文章探讨了在 Lotus Domino V7 中编写更为复杂(也更加有用)的 Web 服务的技术。
这篇文章中讨论的示例可在下载的 Notes 数据库中找到(参见本文末尾处的 下载部分)。与上一期一样,本文中的所有代码示例都是以 LotusScript 编写的。然而,在这篇文章附带的示例数据库中有一些相同的 java 例子。数据库还包含一些样本代理,您可用它们来调用和测试 Web 服务。
至此,我们仅处理过简单的参数和返回值:字符串、整型、简单数组等等。我们甚至还为您展示了使用 InOut 参数返回多个值的方法。但在您开发更为精密的 Web 服务时,可能很快就会发现,自己正处于需要发送和接收整个数据结构的境地之中。这往往是在 XML 文档中完成的,例如:
清单 1. XML book 结构示例
<book> <author>Barry Allen</author> <title>Life in the Fast Lane</title> <booktype>Biography</booktype> </book> |
使用这样的结构,您可以轻松地将整个 <book>
对象作为参数或响应来回发送。甚至还可以通过以下这样的方法创建嵌套结构:
清单 2. XML bookshelf 结构示例
<bookshelf> <shelfnumber>1</shelfnumber> <location>JLA Main Office</location> <book> <author>Barry Allen</author> <title>Life in the Fast Lane</title> <booktype>Biography</booktype> </book> <book> <author>Bruce Wayne</author> <title>Dark Times</title> <booktype>Reference</booktype> </book> </bookshelf> |
在 LotusScript 中,可使用定制类型或类来完成这一任务。在 Web 服务中,使用一个称为复杂数据类型的对象。
LotusScript Web 服务中的复杂数据类型只是一个带有一个或多个公共属性的公共类。每个公共属性都显示为复杂数据类型中的一个元素。例如,要将 <book>
结构建模为一个 LotusScript 类,可以编写如下代码:
清单 3. LotusScript book 类示例
Class Book Public author As String Public booktype As String Public title As String End Class |
这在 WSDL 文件中反映为如下结构:
清单 4. LotusScript book 类的 WSDL 复杂数据类型定义示例
<complexType name="BOOK"> <sequence> <element name="AUTHOR" type="xsd:string"/> <element name="BOOKTYPE" type="xsd:string"/> <element name="TITLE" type="xsd:string"/> </sequence> </complexType> |
请注意,数据类型名称和属性在 WSDL 文件中全部转换成大写形式。这是 Lotus Domino 借助于 LotusScript Web 服务完成的,因为 LotusScript 对于大小写不敏感,但 Web 服务是大小写敏感的。
在您的 LotusScript Web 服务中使用复杂数据类型,您可以编写带有签名的方法,如下:
清单 5. 使用 book 类的 LotusScript 方法示例
Public Function findTitle (searchString As String) As Book Public Sub uploadNewBook (newBook As Book) |
这比那些带有大量需发送和 / 或返回的参数的方法要易于管理得多。
本文附带的示例数据库(参见 下载部分中的链接)是一个 bookstore 数据库,它利用 Web 服务来允许人们搜索、下载和上传包含与图书相关的内容的文件。这个数据库预先填充了一些来自 Project Gutenberg的一些公共领域 eText 文件。
图 1 展示了此数据库中的一个档案示例
图 1. 示例 Web Services Bookstore 数据库中的一个档案
![示例 Web Services Bookstore 数据库中的一个档案](https://i-blog.csdnimg.cn/blog_migrate/8a8fa8443cbf0d51f2545886c65930ad.png)
可以看到,每个图书档案都有标题、作者、图书类型、图书描述和文件附件。
数据库中有两个 LotusScript Web 服务:
- BookSearch。允许 您搜索图书,并返回标题、作者、图书类型、图书描述。
- BookDownloadUpload。允许您检索关于图书的信息,包括文件附件。它还允许您上传新的图书文件。
在下文中,我们将讨论这两个 Web 服务的代码。第二个服务比第一个更复杂,因为它允许上传 / 下载文件以及使用成为枚举的 Web 服务数据类型。
BookSearch Web 服务允许您搜索图书,并获取简单信息,例如标题、作者、图书类型和图书描述。对于总是返回一本图书的信息的那些方法,将返回的是 BookInfo 复杂数据类型。对于可能返回关于一本或多本图书的信息的那些方法,则返回 BookInfoArray 复杂数据类型,其中包含一个 BookInfo 对象数组和一个表明数组中应有多少项的元素。
LotusScript 内 BookInfo 类的一般结构如下:
清单 6. LotusScript BookInfo 类
Class BookInfo Public author As String Public description As String Public fileName As String Public noteID As String Public title As String Public typeOfBook As String End Class |
有许多数据元素映射到数据库中图书档案上的字段或关于这些档案的信息。返回的内容之一就是档案的 NoteID,以使客户机能够在接收到包含多个标题的 BookInfoArray 后轻松检索其中的一个标题。
此类中还有一个 helper 方法:
清单 7. LotusScript BookInfo 类中的 getDocContents 方法
Public Function getDocContents (doc As NotesDocument) As Integer title = doc.GetItemValue(TITLE_FIELD)(0) description = doc.GetItemValue(DESCRIPTION_FIELD)(0) author = doc.GetItemValue(AUTHOR_FIELD)(0) typeOfBook = doc.GetItemValue(BOOK_TYPE_FIELD)(0) noteID = doc.NoteID '** firstAttachmentFileName is a custom Function to get '** the file name of the attachment on this document fileName = firstAttachmentFileName(doc, ATTACHMENT_FIELD) getDocContents = True End Function |
若要从此数据库中获取一个 NotesDocument 对象,并将其信息读入 BookInfo 对象的公共属性,那么这个函数是非常有用的。请注意,尽管此方法是公共的,但它不会出现在复杂数据类型定义中。对于复杂数据类型来说,仅有公共属性才是可用的。
BookInfoArray 对象的一般结构为:
清单 8. LotusScript BookInfoArray 类
Class BookInfoArray Public bookArray() As BookInfo Public count As Integer End Class |
bookArray 属性是一个 BookInfo 对象数组,count 属性是应包含在 bookArray 中的元素数。尽管并非绝对必要,但我们发现包含这样一个与数组相对应的 count 属性是非常有用的,因为这使 Web 服务客户机可以更轻松地确定数组是否为空。它们可以轻松检查 count 是零(空)还是正数(非空)。否则,可能对一个空数组是 null 对象还是没有任何元素的数组,亦或带有一个对象且此对象包含 null 或空值的数组产生混淆。
BookInfoArray 类还有一个 helper 方法:
清单 9. LotusScript BookInfoArray 类中的 setArrayFromCollection 方法
Public Sub setArrayFromCollection (dc As NotesDocumentCollection) count = dc.Count If (count = 0) Then Redim bookArray(0) Else Redim bookArray(count - 1) Dim doc As NotesDocument Dim dcCount As Integer Set doc = dc.GetFirstDocument Do Until (doc Is Nothing) Dim book As New BookInfo Call book.getDocContents(doc) Set bookArray(dcCount) = book Set doc = dc.GetNextDocument(doc) dcCount = dcCount + 1 Loop End If End Sub |
与 BookInfo 类中的 getDocContents 方法极为类似,该方法也接受此数据库中一个图书档案的 NotesDocumentCollection,并将其放在内部 bookArray 属性中。它还恰当地根据集合中的档案数量设置 count 属性。
开始查看 BookSearch 类之前,先花点时间来讨论 LotusScript Web 服务中故障(fault)的概念。
在 Web 服务术语学中,故障就是错误。如果在 LotusScript Web 服务中没有故障生成或错误处理,而在您调用该服务的一个方法时出现了一个错误,那么就会接收到如下 SOAP 响应:
清单 10. 一般 LotusScript SOAP 故障
<soapenv:Body> <soapenv:Fault> <faultcode>soapenv:Server.generalException</faultcode> <faultstring>LotusScript did not run to completion.</faultstring> <detail/> </soapenv:Fault> </soapenv:Body> |
所发生的实际错误出现在服务器控制台和 log.nsf 文件中,但遗憾的是,返回给用户的故障非常一般化,不是非常有用。
在很多情况下(即便不是所有的情况),您都希望在错误发生时生成自己的故障,否则 Web 服务无法返回值。为此,可在您的方法中添加所谓的显式处理。这需要完成以下几步:
- 确保
%INCLUDE "lsxsd.lss"
行位于 Web 服务代码的 Options 部分中。 - 添加一个 WS_FAULT 参数,作为方法的最后一个参数(它必须是 最后一个参数)。
- 当错误发生时,或者在您希望向用户返回一个故障时,设置 WS_FAULT 对象,并使用一个 Exit 函数或 Exit 子语句退出方法。
下面给出一个示例:
清单 11. 抛出一个显式 LotusScript 故障的示例
Public function stringToNumber (s As String, returnFault As WS_FAULT) As Integer On Error Goto processError stringToNumber = Cint(s) Exit Function processError: Call returnFault.setFault(True) Call returnFault.setFaultString(Error$) Messagebox "Logging to console: " & Error$ Exit Function End Function |
客户机调用 stringToNumber 方法时,仅注意要传递给方法的那一个 string 对象。fault 参数是隐藏的。但若发生错误。您可将故障消息设置为所需的任何内容。在本例中,如果客户机向方法传递了一个伪值(bogus value)(例如 foo),它就会接收到如下响应:
清单 12. 显式生成的故障所返回的 SOAP 故障
<soapenv:Fault> <faultcode>soapenv:Server.generalException</faultcode> <faultstring>Type mismatch</faultstring> <detail/> </soapenv:Fault> |
本例中的 <faultstring>
元素比一般的 “不运行到完成的 LotusScript” 消息要更富于描述性。
关于 stringToNumber 方法中的 processError 块,还有两点值得一提:
- 必须使用 setFault 显式地将 fault 对象设置为 True。
- Messagebox 语句是可选的,但它显示出,您可以利用 Messagebox 语句,在 Web 服务执行的任意点将错误(或其他信息)写到服务器控制台和 log.nsf 文件。这对于简化调试或日志记录是非常有用的。
回到我们的 BookSearch Web 服务示例上来。BookSearch 类被公开为此 Web 服务的一个接口具有三个公共方法:
- getFirstTitleMatch。为标题与给定搜索字符串相匹配的第一本图书返回一个 BookInfo 对象。
- getAllTitleMatches。返回一个 BookInfoArray 对象,其中的所有图书都具有与给定搜索字符串相匹配的标题。
- getDocByNoteID。返回具有指定 NoteID 的图书档案的一个 BookInfo 对象。
有两个私有 helper 方法对于 Web 服务客户机是不可用的:
- throwFault。在方法需要生成故障、需要提供一个通用的创建方法、需要记录所返回的任何故障时,由方法调用。
- findDocsByTitle。返回数据库中包含与给定搜索字符串相匹配的图书档案的 NotesDocumentCollection。
尽管 findDocsByTitle 方法是一个基本的 NotesView 搜索函数,但 throwFault 方法值得探讨一下。代码如下:
清单 13. LotusScript BookSearch 类中的 throwFault 方法
Private Sub throwFault (fault As WS_FAULT, faultText As String) Call fault.setFault(True) Call fault.setFaultString(faultText) '** do any other error logging things here... End Sub |
我们完成的任务与上文中的故障示例相同,但没有像前面那样在单个 Web 服务方法中设置 fault 属性,而是使每个方法调用此函数。这样做的好处已在函数末尾处的注释中提到:“在这里进行任何其他日志记录操作。”
使用这样的例程生成故障使您可以更轻松地提供一个中心位置,管理和记录 Web 服务中发生的故障。您可使用 Messagebox 语句向服务器控制台写入一条消息、向中心 Noteslog 写入消息,甚至是使用一个 OpenLog这样的定制错误记录数据库。在这里,处理故障的单独方法也不是绝对必要的,但使用它是一项良好的习惯。
与 BookSearch 类中的公共方法类似,这些方法的代码如下:
清单 14. 用作 Web 服务实现的 LotusScript BookSearch 类
Public Function getFirstTitleMatch (searchString As String, _ returnFault As WS_FAULT) As BookInfo On Error Goto processError Dim dc As NotesDocumentCollection Set dc = findDocsByTitle(searchString) If (dc.Count = 0) Then Call throwFault(returnFault, "No matches found for search string: " & _searchString) Exit Function End If Set getFirstTitleMatch = New BookInfo Call getFirstTitleMatch.getDocContents(dc.GetFirstDocument) Exit Function processError: Call throwFault(returnFault, "Error searching documents: " & Error) Exit Function End Function Public Function getAllTitleMatches (searchString As String, _ returnFault As WS_FAULT) As BookInfoArray On Error Goto processError Dim dc As NotesDocumentCollection Dim doc As NotesDocument Set dc = findDocsByTitle(searchString) Set getAllTitleMatches = New BookInfoArray Call getAllTitleMatches.setArrayFromCollection(dc) Exit Function processError: Call throwFault(returnFault, "Error searching documents: " & Error) Exit Function End Function Public Function getDocByNoteID (noteID As String, _ returnFault As WS_FAULT) As BookInfo On Error Resume Next Dim session As New NotesSession Dim db As NotesDatabase Dim doc As NotesDocument Set db = session.CurrentDatabase Set doc = db.GetDocumentByID(noteID) If (doc Is Nothing) Then Call throwFault(returnFault, "No doc found for Note ID " & noteID) Exit Function End If Set getDocByNoteID = New BookInfo Call getDocByNoteID.getDocContents(doc) End Function |
这些方法中的代码非常简短、易于理解,但有几点要说明一下:
- 请注意,如果搜索为空,
getFirstTitleMatch
和getDocByNoteID
方法会生成一个故障,但getAllTitleMatches
仅在存在 LotusScript 错误时生成故障。这显示出,即便未发生真正的运行时错误,我们也可以返回一个 fault 对象。在本例中,我们也已返回了一个空的 BookInfo 对象。 - 这些方法中代码的简短性展示出
BookInfo.getDocContents
方法和BookInfoArray.setArrayFromCollection
方法是多么有用。在复杂数据类型类中包含这些 helper 方法不仅会使 Web 服务代码简短,还会允许我们在一个中心位置添加或修改功能性的公共部分。
如果您希望测试 Web 服务方法,可以在 Web Services Bookstore 数据库中使用测试代理,也可以使用前一篇文章 “IBM Lotus Domino 7 中的实用 Web 服务:编写和测试简单的 Web 服务” 中介绍的技术之一。
开始探讨 BookDownloadUpload Web 服务中的代码之前,我们需要介绍一种新的数据结构。也就是称为枚举(enumeration)的结构。
枚举其实只是多个项的固定列表,这些项可作为一个 Web 服务方法的参数或一个复杂数据类型的元素使用。在我们的 bookstore 示例中,我们仅允许四种图书分类:
- Fiction
- Non-fiction
- Reference
- Comic book
在 Domino 表单或 Web 页面中,可以使用选择列表来控制此类输入,这样用户就无法向受限制的字段中添加随机数据类。在 Web 服务中,我们使用枚举。在 WSDL 文档中,枚举的形式如下所示:
清单 15. WSDL 文档中的枚举形式示例
<simpleType name="BOOKTYPE"> <restriction base="xsd:string"> <enumeration value="Fiction"/> <enumeration value="Non-Fiction"/> <enumeration value="Reference"/> <enumeration value="Comic Book"/> </restriction> </simpleType> |
将参数或复杂数据类型随此类枚举一起使用的 Web 服务客户机仅能传递将枚举定义中列出的值之一作为值来传递。其他任何值都会在客户机代码中生成一个错误,如果调用了 Web 服务,则生成一个故障。
在 LotusScript 中,要生成一个枚举结构,您必须完成许多非常具体的任务。作为一个示例,下面给出了 BookDownloadUpload Web 服务中 BookType 枚举的代码:
清单 16. LotusScript BookType 枚举代码
'** These constant names MUST begin with "BookType_": Const BookType_Fiction = "Fiction" Const BookType_Nonfiction = "Non-Fiction" Const BookType_Reference = "Reference" Const BookType_Comic = "Comic Book" '** These global variable names MUST end with "_BookType": Dim Fiction_BookType As BookType Dim NonFiction_BookType As BookType Dim Reference_BookType As BookType Dim Comic_BookType As BookType '** This list of possible BookTypes MUST be called "Enum_BookType": Dim Enum_BookType List As BookType '** The actual BookType enumeration class: Class BookType '** we MUST have a Public property called "Value", '** of the same data type as the Const values above Public Value As String Public Sub Initialize (typeString As String) Value = typeString Set Enum_BookType(Cstr(Value)) = Me End Sub End Class '** This class will initialize all the global _BookType objects. '** Is should be called when the BookDownloadUpload class is instantiated. Class BookTypeInitializer Public Sub New () Set Fiction_BookType = New BookType Call Fiction_BookType.Initialize(BookType_Fiction) Set NonFiction_BookType = New BookType Call NonFiction_BookType.Initialize(BookType_NonFiction) Set Reference_BookType = New BookType Call Reference_BookType.Initialize(BookType_Reference) Set Comic_BookType = New BookType Call Comic_BookType.Initialize(BookType_Comic) End Sub End Class |
要在 LotusScript 中创建一个枚举,您需要完成以下任务:
- 确定枚举的名称(在我们的示例中是 BookType)。
- 为枚举中的每项创建一个 Const 全局变量,各 Const 的名称都以枚举名称开头,后接下划线(在本例中为 BookType_xxx)。
- 为枚举中的每项创建一个全局对象,使其数据类型等同于枚举名称和各对象名称,并以下划线后接枚举名称结尾(在本例中为 xxx_BookType)。
- 创建一个全局列表,使其数据类型等同于枚举名称,且名称以 Enum_ 开头,后接枚举名称(在本例中为 Enum_BookType)。
- 创建一个定制类,使其名称与枚举名称相同。此类 必须具有一个称为 Value 的公共属性,与第 2 步中为此枚举创建的 Const 变量具有相同的数据类型。
- 调用 Web 服务时,运行代码来以恰当的值初始化所有全局枚举对象(在本例中是创建一个 BookTypeInitializer 类的实例来完成的)。
完成上述步骤后,就可以在希望使用枚举的任何位置使用第 5 步中编写的定制枚举类了。它在 WSDL 文件中显示为一个枚举元素,第 4 步中枚举列表所指定的值限制将应用。
一方面,要生成一个可供客户机使用的受限制值列表,需要编写大量非常具体的代码。而另一方面,这正是在 Web 服务中处理此类列表的合理方法。
要简化这一代码编写过程,请尝试 Domino Enumeration Code Generator页面,它允许您输入枚举名称、数据类型和希望使用的所有值。它会为您生成 LotusScript 和 Java 代码。这会大大加快在 Lotus Domino 中开始使用 Web 服务枚举的过程。
深入了解 BookDownloadUpload Web 服务
Web Services Bookstore 数据库中的另外一个 Web 服务就是 BookDownloadUpload 服务。此服务不仅允许您检索书店中一个图书档案的相关信息,还允许您下载包含一本或多本图书的内容的文件。此外,您还可以利用此服务上传新图书。这个 Web 服务的代码中有四个主要组件:
- BookType枚举,上文已经讨论过了。
- BookInfoAndFile复杂数据类型,其中包含图书信息和文件数据。
- BookInfoAndFileArray复杂数据类型,这是一个 BookInfoAndFile 对象数组(就像 BookSearch 服务中的 BookInfoArray 类型一样)。
- BookDownloadUpload类,这是我们的 Web 服务的接口类。
在上一部分中,我们已经简单地介绍了 BookType 枚举。BookInfoAndFileArray 类实际上就相当于 BookSearch 服务中的 BookInfoArray 类。下文将更详细地讨论 BookInfoAndFile 和 BookDownloadUpload 类。
BookDownloadUpload 服务和 BookSearch 服务间最大的差异就在于您可以利用 BookDownloadUpload 服务上传和下载文件。为此,可使用 lsxsd.lss 文件中定义的 XSD_BASE64BINARY 类。
首先给出 BookInfoAndFile 类的代码:
清单 17. LotusScript BookInfoAndFile 类
Class BookInfoAndFile Public author As String Public base64file As XSD_BASE64BINARY Public description As String Public fileName As String Public noteID As String Public title As String Public typeOfBook As BookType '** instantiate the base64file object on creation Public Sub New () Set base64file = New XSD_BASE64BINARY End Sub '** shortcut way to set the contents of the base64file field '** from a NotesStream Public Sub setFile (stream As NotesStream) Call base64file.SetValueFromNotesStream(stream) End Sub '** shortcut way to get the contents of the base64file field '** as a NotesStream Public Function getFile () As NotesStream Set getFile = base64file.GetValueAsNotesStream() End Function '** shortcut way to set the contents of the typeOfBook field Public Sub setBookType (bookTypeString As String) Set typeOfBook = Enum_BookType(bookTypeString) End Sub '** shortcut way to get the contents of the typeOfBook field Public Function getBookType () As String getBookType = typeOfBook.toString() End Function '** take a NotesDocument from this database and populate the '** values of the Public properties based on its field values Public Function getDocContents (doc As NotesDocument) As Integer title = doc.GetItemValue(TITLE_FIELD)(0) description = doc.GetItemValue(DESCRIPTION_FIELD)(0) author = doc.GetItemValue(AUTHOR_FIELD)(0) Call setBookType(doc.GetItemValue(BOOK_TYPE_FIELD)(0)) '** custom functions to convert an attachment to a NotesStream fileName = firstAttachmentFileName(doc, ATTACHMENT_FIELD) Call setFile(firstAttachmentToStream(doc, ATTACHMENT_FIELD)) noteID = doc.NoteID getDocContents = True End Function '** create a new document in this database, based on the contents '** of the Public properties in this object Public Function createNewDoc () As Integer Dim session As New NotesSession Dim doc As NotesDocument Set doc = New NotesDocument(session.CurrentDatabase) doc.Form = FORM_NAME Call doc.ReplaceItemValue(TITLE_FIELD, title) Call doc.ReplaceItemValue(DESCRIPTION_FIELD, description) Call doc.ReplaceItemValue(AUTHOR_FIELD, author) Call doc.ReplaceItemValue(BOOK_TYPE_FIELD, getBookType()) Dim rtitem As New NotesRichTextItem(doc, ATTACHMENT_FIELD) Dim tempFileName As String '** custom function to write a NotesStream to a temp file tempFileName = createTempFile(fileName, Me.getFile()) Call rtitem.EmbedObject(EMBED_ATTACHMENT, "", _ tempFileName, fileName) Call doc.Save(True, False) noteID = doc.NoteID Kill tempFileName createNewDoc = True End Function End Class |
Base64file 是一个容纳文件附件的公共属性,因此您可以将其声明为 XSD_BASE64BINARY 数据类型(在 WSDL 文件中显示为 xsd:base64Binary
数据类型)。这个 LotusScript 数据类型是在 lsxsd.lss 文件中定义的,具有两个用于获取和设置文件内容的方法:SetValueFromNotesStream
和 GetValueAsNotesStream
。
在 NotesStream 读入对象时(使用 SetValueFromNotesStream
),您可以传递一个二进制或 ASCII 数据流,该类会以 base64 为您编码数据。类似地,对于存储在对象之内的文件数据,可将数据提取为二进制 NotesStream(使用 GetValueAsNotesStream
),它会为您处理 base64 解码。
那么,技巧就在于使用 NotesStreams。如果您向本地文件系统读或写文件,那么将文件转换成流是非常简单的(相关示例请参见 Lotus Domino Designer Help)。要将 NotesDocument 或 NotesRichTextItem 上的文件附件转换成 NotesStream,必须首先分离此文件,然后将其写入一个流。类似地,要将一个 NotesStream 作为附件附加到文档上,必须将此流写入一个临时文件,然后将临时文件附加到文档上。我们这个 Web 服务中定制的 firstAttachmentToStream 和 createTempFile 函数就会为您处理这一功能(参见示例数据库中的代码,了解其工作原理)。
在客户端,当一名用户发送或接收一个文件附件时,客户机代码必须处理 base64 编码,并自行解码。某些客户机(例如 Apache Axis)会为您完成这一任务。而其他一些客户机(例如 MSSOAP)可能要求您手动完成此任务。
BookDownloadUpload 类与之前介绍的 BookSearch 类极为类似。它具有五个公共方法:
- getFirstTitleMatch。为标题与给定搜索字符串相匹配的第一本图书返回一个 BookInfoAndFile 对象。
- getAllTitleMatches。返回一个 BookInfoAndFileArray 对象,其中的所有图书都具有与给定搜索字符串相匹配的标题。
- getDocByNoteID。返回具有指定 NoteID 的图书档案的一个 BookInfoAndFile 对象。
- addNewFileComplex。允许客户机通过上传 BookInfoAndFile 对象创建一个新的图书档案。
- addNewFile。允许客户机通过将一本图书的所有独立元素作为多个独立的参数传递而创建新图书档案。
与 BookSearch 类相似,有两个私有 helper 方法(throwFault
和 findDocsByTitle
)对于 Web 服务客户机不可用。
BookDownloadUpload 类的代码如下:
清单 18. 用作 Web 服务实现的 LotusScript BookDownloadUpload 类
Class BookDownloadUpload '** initialize all the global BookType objects before we start Public Sub New () Dim bookInit As New BookTypeInitializer End Sub Public Function getFirstTitleMatch (searchString As String, _ returnFault As WS_FAULT) As BookInfoAndFile On Error Goto processError Dim dc As NotesDocumentCollection Set dc = findDocsByTitle(searchString) If (dc Is Nothing) Then Call throwFault(returnFault, "No matches found for search string: " _ & searchString) Exit Function Elseif (dc.Count = 0) Then Call throwFault(returnFault, "No matches found for search string: " _ & searchString) Exit Function End If Set getFirstTitleMatch = New BookInfoAndFile Call getFirstTitleMatch.getDocContents(dc.GetFirstDocument) Exit Function processError: Call throwFault(returnFault, "Error searching documents: " & Error) Exit Function End Function Public Function getAllTitleMatches (searchString As String, _ returnFault As WS_FAULT) As BookInfoAndFileArray On Error Goto processError Dim dc As NotesDocumentCollection Dim doc As NotesDocument Set dc = findDocsByTitle(searchString) Set getAllTitleMatches = New BookInfoAndFileArray Call getAllTitleMatches.setArrayFromCollection(dc) Exit Function processError: Call throwFault(returnFault, "Error searching documents: " & Error) Exit Function End Function Public Function getDocByNoteID (noteID As String, _ returnFault As WS_FAULT) As BookInfoAndFile On Error Resume Next Dim session As New NotesSession Dim db As NotesDatabase Dim doc As NotesDocument Set db = session.CurrentDatabase Set doc = db.GetDocumentByID(noteID) If (doc Is Nothing) Then Call throwFault(returnFault, "No doc found for Note ID " & noteID) Exit Function End If Set getDocByNoteID = New BookInfoAndFile Call getDocByNoteID.getDocContents(doc) End Function Private Sub throwFault (fault As WS_FAULT, faultText As String) Call fault.setFault(True) Call fault.setFaultString(faultText) '** do any other error logging things here... If (Err > 0) Then '** Messagebox will write to log.nsf, just like a backend agent will Messagebox LogError() End If End Sub Private Function findDocsByTitle (searchString As String) _ As NotesDocumentCollection Dim session As New NotesSession Dim db As NotesDatabase Dim view As NotesView Set db = session.CurrentDatabase Set view = db.GetView(TITLE_LOOKUP_VIEW) Set findDocsByTitle = view.GetAllDocumentsByKey(searchString, False) End Function Public Function addNewFileComplex (book As BookInfoAndFile, _ returnFault As WS_FAULT) As String On Error Goto processError Call book.createNewDoc() addNewFileComplex = book.noteID Exit Function processError: Call throwFault(returnFault, "Error creating new doc: " & Error) Exit Function End Function Public Function addNewFile (title As String, author As String, _ description As String, _ typeOfBook As BookType, fileName As String, base64file As XSD_BASE64BINARY, _ returnFault As WS_FAULT) As String Dim book As New BookInfoAndFile book.title = title book.author = author Set book.typeOfBook = typeOfBook book.description = description book.fileName = fileName Set book.base64file = base64file addNewFile = addNewFileComplex(book, returnFault) End Function End Class |
如您所见,getFirstTitleMatch
、getAllTitleMatches
和 getDocByNoteID
方法的代码与 BookSearch 类中相同函数的代码基本相同,区别就在于我们使用了 BookInfoAndFile 对象,而不是 BookInfo 对象。此外,我们还在 New
子方法中添加了一行代码,来使用 BookTypeInitializer 类初始化我们的枚举,throwFault
方法展示了利用 Messagebox 向服务器控制台写错误消息的方法。除此之外,BookDownloadUpload 类非常类似于 BookSearch 类。
就像 addNewFile
和 addNewFileComplex
方法那样,那些方法的代码非常少,因为通过上传图书数据在数据库中创建新档案的逻辑已编写在 BookInfoAndFile 类中。
如果您希望在 Web Services Bookstore 示例数据库中测试任一服务,我们在数据库中提供了两个样本代理,它们调用所有 Web 服务方法(包括下载和上传文件)。这些代理是以 Java 编写的,并使用了可在 OpenNTF.org 找到的开源 Stubby 数据库所生成的 Apache Axis 存根文件。
我们还提供了一些使用 MSSOAP 执行简单图书搜索的示例,请注意,必须安装最新的 MSSOAP 3.0 库,仅在您保持 Web 服务为 RPC/Encoded 时,MSSOAP 示例才能工作,它们无法与 BookDownloadUpload 服务一起使用,因为 MSSOAP 1.x 无法解释我们在该服务中使用的 BookType 枚举。关于使用 MSSOAP 的限制的更多细节,请参见本系列的前一篇文章 “IBM Lotus Domino 7 中的实用 Web 服务:编写和测试简单的 Web 服务”。
您还可以使用本系列前一篇文章中列举的其他测试工具。
如果您的 Web 服务代码有问题,那么下面这些通用的告诫及故障检修指导原则可以给您一些帮助。
如果您有一个数据库的本地副本,它带有您想测试的 Web 服务,那么,首先就要确保 Notes HTTP 服务正在后台运行。完成这一任务的最简单的方法如下:
- 在 Domino Designer 中打开数据库。
- 在数据库中选择一个表单或视图。
- 选择 Design - Preview in Web browser - Default Browser,然后等候表单或视图在 Web 浏览器中显示出来。
将表单或视图作为 Web 页面查看之后,关闭浏览器窗口,使用 localhost 作为服务器名来测试 Web 服务。HTTP 服务将一直在后台运行,直至您彻底关闭 Notes 客户端。
注意:某些个人防火墙会阻塞本地 HTTP 服务的操作,因此务必调整好防火墙的设置。
如果用户无权访问一个 Web 服务,那么他会接收到一个 401 Access Denied 错误
确保您充分理解了访问您的 Web 服务的安全性需求。如果一名不具备最起码的 Reader 权限的匿名用户访问数据库和 Web 服务,那么他就会在尝试读取 WSDL 文件或调用 Web 服务方法时接收到一个 401 Access Denied 错误。在 Lotus Domino 的初始 7.0 版本中,这种情况会生成一个 404 Not Found 错误,其明确性略逊。
使用 Messagebox
或 System.out.println
向服务器控制台输出信息
虽然简单地将消息转储到服务器控制台(以及 log.nsf 文件)并不总是一种良好的错误记录技术,但若您需要快速完成某些临时记录任务,那么这是一种简单的方案。使用一个 Domino Web 服务,使用 Messagebox
(在 LotusScript 中)或 System.out.println
(在 Java 中)编写的消息将发送到服务器控制台。
当然,更好的方法是使用一种更为全局化的记录技术,例如 NotesLog 或 OpenLog数据库。如果您使用了一种中央方法来处理故障生成(就像本文的示例 Web 服务中所做的那样),那么可以像那样轻松地添加一个对记录系统的调用,以便跟踪您的消息和错误。
调用 Web 服务和读 / 写文件的服务的服务器代理的运行时安全性
对于读 / 写文件、访问网络的 Web 服务或调用 Web 服务的基于服务器的代理,确保满足了以下条件:
- 代理 /Web 服务的运行时安全性级别至少为 “2. Allow Restricted Operations”(可在编辑代理或 Web 服务的设计元素时可用的 Properties 对话框中设置)。
- 代理 /Web 服务的签署者具有 Server 文档的 “Run unrestricted methods and operations” 权力。
- 代理 /Web 服务的签署者具有 Server 文档的 “Run restricted LotusScript/Java agents” 权力。
如果未正确设置这些权力,那么在运行代理或 Web 服务时,您就会看到安全性错误(通常是在服务器控制台和 log.nsf 文件中)。
在您需要重新编译 LotusScript 时,Web 服务返回一个错误 500。
如果在尝试访问 WSDL 文件或在 Web 服务上运行一个方法时,您接收到了一个带有 “Unknown LotusScript Error” 消息的 HTTP 错误 500,那么就需要重新编译数据库中的所有 LotusScript。这个错误往往是在您的 Web 服务使用了来自一个脚本库的函数,而该库完成过 Web 服务未完成的更新时发生的。这与 Lotus Notes 客户端显示 “Error Loading USE or USELSX” 消息时的情况类似。
如果您从 LotusScript Web 服务返回了一个数组,那么请确保您返回的是一个固定数组,而不是动态的变长数组。例如,下面这些类型的 LotusScript 调用会生成变长数组:
varval = doc.SomeFieldName
varval = Split("I am superman", " "
)varval = Evaluate(|@DbColumn("";"":"somedatabase";"some view";2)|)
幸运的是,这种问题非常容易解决。您可以编写下面这样的代码,将变长数组转换成固定数组:
清单 19. 将变长数组转换成固定数组的 LotusScript 代码
Dim returnArray() As String varval = doc.MultiValueCategoryField Redim returnArray(Ubound(varval)) For i = 0 To Ubound(varval) returnArray(i) = varval(i) Next |
注意:数组需要在 lsxsd.lss 文件内定义的 ARRAY_HOLDER 数据类型之一中返回(本系列的上一篇文章更详细地讨论了这种情况)。
使用 Lotus Domino V7.0.1 或更新版本,特别是在使用枚举时
Lotus Domino V7.0.1 中修复了一个不太显著但非常重要的问题。最值得一提的是,在最初的 Lotus Domino V7.0 发布版中有一个问题,导致枚举无法正常工作。确保您使用的 Lotus Domino 版本是 V7.0.1 或更新的修订版,查看公共 Notes/Domino 补丁列表和 Lotus Support Knowledgebase了解未定的 bug。
如果一个 Web 服务客户机调用了一个需要很长时间才能完成的 Web 服务方法,那么有可能客户机代码会在 Web 服务方法完成其操作之前超时(60 秒是常见的默认 Web 服务客户机超时值)。
请注意,即便客户机超时并等待响应,Web 服务依然会继续处理请求,直至完成,或达到 Domino Server 文档中设定的代理超时界限。对于大文件的上传或复杂数据库的事务来说,Web 服务客户机可能会获得一个超时消息,并认为上传或处理未发生,即使实际上已经发生(很可能在几分钟后就会完成)。
还有一个与此相关的提示,不要忘记,您可以在 Lotus Domino V7 中使用新代理记录技术来生成一份日志,记录 Web 服务的各个部分运行所花费的时间。关于如何启用和利用代理及 Web 服务记录的更多信息,请查看 Lotus Domino Designer Help。
确保 LotusScript 类中的公共属性作为按字母排序的复杂数据类型使用
或许未来的某个 Lotus Domino 版本会解决这个问题,但就 7.0.1 版而言,如果 LotusScript 复杂数据类型类的的公共属性 未按字母排序,这就会成为一个问题。例如,以下代码能够正常工作:
清单 20. 按字母排序的 LotusScript 复杂数据类型属性(好的)
Class Book Public author As String Public booktype As String Public title As String End Class |
但以下代码会导致问题:
清单 21. 未按字母排序的 LotusScript 复杂数据类型属性(糟糕的)
Class Book Public title As String Public author As String Public booktype As String End Class |
此问题的征兆就是:一个作为参数发送或作为响应返回的复杂数据对象丢失了某些元素。在故障检修时,这可能是一个令人迷惑的问题,往往能够通过按字母顺序排列公共属性解决。
但请注意,简单地在源代码中重新排列属性是没有用的,因为只要属性列表响应(即使其顺序已更改),WSDL 就不会重新生成。您需要通过如下方法来进行处理:
- 重新排列属性。
- 添加一个新的伪属性。
- 保存并关闭 Web 服务。
- 重新打开 Web 服务。
- 删除伪属性。
- 保存并关闭 Web 服务。
我们并不确定此问题限于哪些 Web 服务客户机,但在使用 Apache Axis 和 Apache SOAP 时都发现了这样的问题。
注意避免将接收自 Web 服务的大字符串(超过 32 KB)写入 NotesDocument 的纯文本字段
接收一个复杂数据类型的参数或元素并将值写入 NotesDocument 的 Web 服务并不罕见。如果您有这样的服务,切记,文档中的 Notes 纯文本字段仅限于 64 KB(约 32,000 个字符)。如果您尝试将超过这个界限的值写入文本字段,就会得到无法预料的错误。
如果您写入 NotesDocument 的一个文本字段的数据可能超过 64 KB,那么您可能会希望将该数据写入一个 Rich Text 字段。field instead.
如果在 LotusScript 中使用 base64 编码 / 解码,那么请考虑使用 LS2J 和 Java
如果您在超过 20 KB 的文件上的 Web 服务中编码或解码操作,可能希望使用 Java 来处理 base64 操作,而不是 XSD_BASE64BINARY 类中可用的本地 LotusScript 编码 / 解码。LotusScript 在解析较大的字符串数据时可能非常缓慢,但 Java 在这种情况下速度极快。
关于使用 LS2J 和 Java 来提供此功能的方法,请参见 Web Services Bookstore 数据库中的示例(可在 下载部分中获得)。BookDownloadUpload Web 服务的 BookInfoAndFile 类中的 getFile 和 setFile 方法调用一个称为 Base64 LS2J 的 LS2J 脚本库,它使用 Base64Java 脚本库中的一个定制 Java 类。
测试一个大小约有 150 KB 的文件时,通过 LS2J 例程完成的 base64 解码速度要比本地 LS2J 例称快 100 倍(在我们的工作站上,那是一两秒钟与接近两分钟的对比)。
这个共分三部分的系列文章探讨了 Lotus Domino V7.0 中的 Web 服务,引领您完成了理解 Web 服务并将其与 Lotus Domino 一起使用的整个过程,从基本概念入手,一直到复杂服务的编写。我们探讨了简单数据类型、数组、复杂数据类型、枚举、文件附件和定制故障在 LotusScript Web 服务中的使用,包括各概念的实际代码和可立即使用的示例数据库。示例数据库还有一些与 LotusScript Web 服务完全对应的 Java Web 服务,这样您就可以了解 LotusScript 方法是如何映射到 Java 中的。我们甚至仔细探讨了几种不同的方法,以使用各种免费工具测试 Web 服务,还详细说明了可供您下载的示例数据库,其中包含一些直接从 Lotus Notes V7.0 客户端调用 Web 服务的代理。
有了这些可供您任意支配的强大技术,您应已准备好使您的 Domino 数据库转变为企业 SOA 体系结构中的积极参与者。无论是从功能角度来看,还是从设计角度来看,这都会为您的应用程序和您的整个组织带来许多机遇。这只是 Lotus Notes/Domino 发展的途径之一,也说明了它为什么依然是一种如此有价值、高生产力的应用程序开发平台。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
本文示例数据库 | WSBookstore.nsf | 1.82 MB | HTTP |
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
- 查看完整的 “IBM Lotus Domino 7 中的实用 Web 服务” 系列文章。
- developerWorks Lotus 文章 “IBM Lotus Domino 7 中的实用 Web 服务:什么是 Web 服务以及为何如此重要 ?”
- developerWorks Lotus 文章 “IBM Lotus Domino 7 中的实用 Web 服务:编写和测试简单的 Web 服务”
- developerWorks Lotus 文章 “Lotus Notes/Domino 7 Web services”
- developerWorks 教程 “快速创建 Domino Web 服务,利用 Domino 7 中新的 Web 服务功能加快开发速度”
- developerWorks Lotus 文章 “通过 Lotus Domino Java 代理消费 Web 服务”
获得产品和技术
- 下载最新的 MSSOAP 工具包,通过 LotusScript 调用 Web 服务。
- 下载 Stubby 数据库,生成 Apache Axis存根文件,以便与 Lotus Notes Java 代理一起使用。
讨论
- 通过参与 developerWorks blogs加入 developerWorks 社区。