编写复杂的Web服务

日期: 2008-05-22 作者:Julian Robichaux 来源:TechTarget中国

  本系列介绍与 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 结构示例
    
  
      Barry Allen
     
      Biography
  

  使用这样的结构,您可以轻松地将整个 对象作为参数或响应来回发送。甚至还可以通过以下这样的方法创建嵌套结构:

  清单 2. XML bookshelf 结构示例
    
  
      1
      JLA Main Office
     
          Barry Allen
         
          Biography
     

     
          Bruce Wayne
         
          Reference
     

  

  在 LotusScript 中,可使用定制类型或类来完成这一任务。在 Web 服务中,使用一个称为复杂数据类型的对象。

  LotusScript Web 服务中的复杂数据类型只是一个带有一个或多个公共属性的公共类。每个公共属性都显示为复杂数据类型中的一个元素。例如,要将 结构建模为一个 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 复杂数据类型定义示例
    
  
     
         
         
         
     

  

  请注意,数据类型名称和属性在 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)

  这比那些带有大量需发送和/或返回的参数的方法要易于管理得多。

  Web 服务 Bookstore:我们的示例数据库

  本文附带的示例数据库(参见 下载 部分中的链接)是一个 bookstore 数据库,它利用 Web 服务来允许人们搜索、下载和上传包含与图书相关的内容的文件。这个数据库预先填充了一些来自 Project Gutenberg 的一些公共领域 eText 文件。

  图 1 展示了此数据库中的一个档案示例

  图 1. 示例 Web Services Bookstore 数据库中的一个档案
 
  可以看到,每个图书档案都有标题、作者、图书类型、图书描述和文件附件。

  数据库中有两个 LotusScript Web 服务:

  BookSearch。允许 您搜索图书,并返回标题、作者、图书类型、图书描述。
  BookDownloadUpload。允许您检索关于图书的信息,包括文件附件。它还允许您上传新的图书文件。

  在下文中,我们将讨论这两个 Web 服务的代码。第二个服务比第一个更复杂,因为它允许上传/下载文件以及使用成为枚举的 Web 服务数据类型。

  BookSearch 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:Server.generalException
           LotusScript did not run to   completion.
          
       

    

  所发生的实际错误出现在服务器控制台和 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:Server.generalException
      Type mismatch
     
  

  本例中的 元素比一般的 “不运行到完成的 LotusScript” 消息要更富于描述性。

  关于 stringToNumber 方法中的 processError 块,还有两点值得一提:

  必须使用 setFault 显式地将 fault 对象设置为 True。

  Messagebox 语句是可选的,但它显示出,您可以利用 Messagebox 语句,在 Web 服务执行的任意点将错误(或其他信息)写到服务器控制台和 log.nsf 文件。这对于简化调试或日志记录是非常有用的。

  BookSearch 类剖析

  回到我们的 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 文档中的枚举形式示例
    
  
     
         
         
         
         
     

  

  将参数或复杂数据类型随此类枚举一起使用的 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 类剖析

  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 服务代码有问题,那么下面这些通用的告诫及故障检修指导原则可以给您一些帮助。

  本地测试时,确保 Notes HTTP 服务正在运行

  如果您有一个数据库的本地副本,它带有您想测试的 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 服务客户机调用了一个需要很长时间才能完成的 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 发展的途径之一,也说明了它为什么依然是一种如此有价值、高生产力的应用程序开发平台。

  关于作者

  Julian Robichaux 是专门研究 IBM Lotus Notes 和 Java 开发的软件开发人员和专业程序员。他擅长于各种与开发、架构及培训有关的项目。在业余时间,他喜欢在 http://www.nsftools.com 上添加个人 Web 站点/博客。他的家人无法理解他为什么总要随身携带笔记本电脑,就连他自己也不是很清楚其中的原因。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐