この連載第 1 回の「Practical Web services in IBM Lotus Domino 7: What are Web services and why are they important?」では、Web サービスの基本と SOA の用語を解説しました。また第 2 回「IBM Lotus Domino 7 での実用的 Web サービス: 単純な Web サービスを作成してテストする (Practical Web services in IBM Lotus Domino 7: Writing and testing simple Web services)」では、Lotus Domino V7 で単純な Web サービスを作成し、テストするための詳細について、要点を説明しました。この第 3 回の最後の記事では、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 サービスでの複合データ型は、1 つ以上の public プロパティーを持つ public クラスにすぎません。各 public プロパティーは、複合データ型の要素として現れます。例えば <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 ファイルでは、データ型の名前とプロパティーがすべて大文字に変換されていることに注意してください。LotusScript は大文字小文字を無視しますが Web サービスは区別するため、Lotus Domino が LotusScript Web サービスを処理する場合、この変換を行います。
LotusScript Web サービスで複合データ型を使うと、次のようなシグニチャーを持つメソッドを作ることができます。
リスト 5. book クラスを使った LotusScript メソッドの例
Public Function findTitle (searchString As String) As Book
Public Sub uploadNewBook (newBook As Book)
|
この方が、送受信が必要なパラメーターを大量に持つメソッドよりも、ずっと管理が容易です。
サンプル・データベース: Web Services Bookstore
この記事で使用するサンプル・データベース (「ダウンロード」セクションにリンクがあります) は、Web サービスを使った書店データベースです。本の内容を含むファイルの検索やダウンロード、アップロードを、Web サービスを使って行うことができます。このデータベースには、Project Gutenberg によるパブリック・ドメインの eText ファイルがいくつか事前に入れられています。
図 1 は、このデータベースの中の文書の一例を示しています。
図 1. Web Services Bookstore サンプル・データベースの中の文書
これを見ると、それぞれの book 文書に標題 (title) と著者 (author)、本の種類 (book type)、備考 (description)、そして添付ファイルがあることがわかります。
このデータベースには、次のような 2 つの LotusScript Web サービスがあります。
- BookSearch: 本を検索すると、本の標題と著者、本の種類、そして備考を返します。
- BookDownloadUpload: 添付ファイルを含めて、本に関する情報を取得します。また、新しい本のファイルをアップロードすることもできます。
この記事のこれから先では、この 2 つの Web サービスのコードについて解説します。 2 番目のサービスではファイルのアップロードやダウンロードができ、また列挙と呼ばれる Web サービスのデータ構造を使用するため、このサービスの方が最初のサービスよりも複雑です。
BookSearch Web サービスでは本を検索することができ、また標題や著者、本の種類、備考など、単純な情報を取得することができます。常に 1 冊の本に関する情報を返すメソッドに対しては、BookInfo 複合データ型が返されます。1 冊以上の本に関する情報を返すメソッドに対しては、BookInfoArray 複合データ型が返されます (BookInfoArray には、BookInfo オブジェクトの配列と、その配列がいくつの項目を含むべきかを示す 1 つの要素が含まれています)。
下記は、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
|
ここで、データベースの book 文書のフィールドあるいは情報にマップされる、いくつかのデータ要素があります。返されるものの 1 つに、文書の NoteID があります。いくつかの標題を含む BookInfoArray を受信した後で 1 つの標題を取得する際には、NoteID を使えば簡単です。
このクラスの中には、ヘルパー・メソッドもあります。
リスト 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 オブジェクトの public プロパティーの中に読み込む際には、この関数が便利です。このメソッドは、public であるにもかかわらず、複合データ型定義のどこにも現れないことに注意してください。複合データ型で利用できるのは public プロパティーのみです。
下記は、BookInfoArray オブジェクトの一般的な構造を示しています。
リスト 8. LotusScript BookInfoArray クラス
Class BookInfoArray
Public bookArray() As BookInfo
Public count As Integer
End Class
|
bookArray プロパティーは BookInfo オブジェクトの配列であり、また count プロパティーは、bookArray 内に含まれるべき要素の数です。厳密に必要なことではありませんが、このような配列と対応するように count プロパティーをインクルードすると、とても便利なことがわかっています。そうすることで、その配列が空かどうかを Web サービス・クライアントが容易に判断できます。つまり Web サービス・クライアントは、count がゼロ (空) か正の数 (空ではない) かを単純にチェックするだけでよいのです。こうしておかないと、空の配列がヌル・オブジェクトなのか、要素を持たない配列なのか、あるいはヌルまたは空の値を含む 1 つのオブジェクトなのか、わからなくなる恐れがあります。
BookInfoArray クラスも、ヘルパー・メソッドを持っています。
リスト 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 メソッドと同様、このメソッドは、このデータベースの Book 文書の NotesDocumentCollection を取り出し、このコレクションの中の各 NotesDocument を内部の bookArray プロパティーに追加します。また、このコレクションの中にある文書の数に基づいて、count プロパティーを適切に設定します。
BookSearch クラスを検証する前に、少し休憩して LotusScript Web サービスでのフォールトの概念について説明しましょう。
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 サービスは値を返すことができません。そこで、いわゆる明示的フォールト処理をメソッドに追加します。その手順は以下のとおりです。
- Web サービス・コードの Options セクションに %INCLUDE "lsxsd.lss" という行があることを確認します。
- WS_FAULT パラメーターを、そのメソッドの最後のパラメーターとして追加します (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 メソッドをコールする場合、クライアントには 1 つのストリング・オブジェクトが stringToNumber メソッドに渡されることしか見えません。フォールト・パラメーターは、隠れて見えません。しかし、もしエラーが発生すると、フォールト・メッセージを任意に設定することができます。この例では、もしクライアントが偽の値 (例えば foo) をこのメソッドに渡すと、クライントは次のようなレスポンスを受信します。
リスト 12. 明示的に生成されたフォールトによって返される SOAP フォールト
<soapenv:Fault>
<faultcode>soapenv:Server.generalException</faultcode>
<faultstring>Type mismatch</faultstring>
<detail/>
</soapenv:Fault>
|
この場合の <faultstring> 要素の方が、汎用のメッセージ「LotusScript did not run to completion」よりも、ずっと具体的です。
stringToNumber メソッドの processError ブロックに関しては、他にも 2 つ注意すべきことがあります。
- setFault を使って、フォールト・オブジェクトを明示的に True に設定する必要があります。
- Messagebox ステートメントはオプションですが、Messagebox ステートメントがあるということは、Web サービスを実行している任意の時点で、Messagebox ステートメントを使ってサーバー・コンソールと log.nsf ファイルにエラー (あるいは他の情報) を書き出せるということです。これは単純なデバッグやロギングの際に便利です。
では、BookSearch Web サービスの例に戻りましょう。この Web サービスのインターフェースとして公開される BookSearch クラスは、次の 3 つの public メソッドを持っています。
- getFirstTitleMatch: 与えられた検索ストリングに一致する標題を持つ最初の本に関する 1 つの BookInfo オブジェクトを返します。
- getAllTitleMatches: 与えられた検索ストリングに一致する標題を持つすべての本に関する BookInfoArray オブジェクトを返します。
- getDocByNoteID: 指定された NoteID の book 文書に関する 1 つの BookInfo オブジェクトを返します。
また、次の 2 つの private ヘルパー・メソッドがあります (Web サービス・クライアントには利用できません)。
- throwFault: メソッドがフォールトを生成する必要がある場合、メソッドがフォールトを作成する共通の方法を提供する必要がある場合、そしてメソッドがすべての返されるフォールトをログする必要がある場合に、そのメソッドによってコールされます。
- findDocsByTitle: このデータベースの中の、与えられた検索ストリングに一致する book 文書を含む 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 サービス・メソッドの中でフォールト・プロパティーを設定するのではなく、それぞれのメソッドにこの関数をコールさせています。こうすることの利点は、この関数の最後のコメントにあるとおりです。つまり、「do any other error logging things here (他のすべてのエラー・ロギングをここで行う)」ということです。
このようなルーチンを使ってフォールトを生成することによって、Web サービスで発生するフォールトの管理やログを行う中心となる場所を容易に提供することができます。Messagebox ステートメントを使ってサーバー・コンソールにメッセージを書き出したり、中心となる NotesLog に書き出したり、さらには OpenLog のようなカスタムのエラー・ログ・データベースを使うこともできます。この場合も、フォールト処理のために必ず別メソッドが必要なわけではありませんが、そうした方が適切です。
BookSearch クラスの public メソッド群に関するコードは、以下のとおりです。
リスト 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 エラーがある場合にしかフォールトを生成しないことに注意してください。これはつまり、本当のランタイム・エラーが起きていなくてもフォールト・オブジェクトを返せる、ということです。またこの場合には、空の BookInfo オブジェクトを返すこともできます。
- BookInfo.getDocContents メソッドと BookInfoArray.setArrayFromCollection メソッドのコードの短さから、これらのコードが使いやすいものであることがわかります。こうしたヘルパー・メソッドを複合データ型クラスの中に含めることによって、Web サービスのコードを短くできるだけではなく、重要なところにちょっとした一般的な機能を追加したり、そうした機能を修正したりすることもできます。
Web サービスのメソッドをテストする場合には、Web Services Bookstore データベースのテスト・エージェントを使うか、あるいは前回の記事「IBM Lotus Domino 7 での実用的 Web サービス: 単純な Web サービスを作成してテストする (Practical Web services in IBM Lotus Domino V7: Writing and testing simple Web services)」で解説した方法の中の 1 つを使います。
BookDownloadUpload Web サービスのコードについて説明する前に、もう 1 つのデータ構造について説明しておく必要があります。それは、列挙と呼ばれる構造です。
列挙は、Web サービス・メソッドのパラメーターとして使われる、あるいは複合データ型の要素として使われる項目の、単なる固定リストです。この書店の例で許されているのは、次の 4 種類のみです。
- フィクション (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 と呼ばれる 1 つの public プロパティーを持つ必要があります。そしてこのプロパティーのデータ型は、(ステップ 2 で) この列挙のために作成した Const 変数と同じデータ型である必要があります。
- Web サービスがコールされたら、(この場合は BookTypeInitializer クラスのインスタンスを作成することによって) すべてのグローバル列挙オブジェクトを適切な値で初期化するコードを実行します。
それが終了すれば、ステップ 5 で作成したカスタムの列挙クラスを、その列挙を使おうとする場所のどこででも使えるようになります。このクラスは WSDL ファイルの中では列挙要素として現れ、また値に関して、ステップ 4 の Enum リストで指定される制限が適用されます。
こうすると、クライアントが使うことができる制限付きの値のリストを生成するには、かなり特定的なコードを大量に作成しなければなりません。しかし一方でこれは、そうしたリストを Web サービスで処理する上での適切な方法でもあるのです。
このコード作成プロセスを容易にするために、Domino Enumeration Code Generator ページ を試してみてください。ここには、列挙名やデータ型、そしてこれから使おうとするあらゆる値を入力することができます。またこのページは、LotusScript と Java のコードを生成してくれます。これによって、Lotus Domino で Web サービス列挙をすぐに使い始めることができます。
BookDownloadUpload Web サービスの内部
Web Services Bookstore データベースの中にある、もう 1 つの Web サービスが、BookDownloadUpload サービスです。このサービスを利用すると、この書店の book 文書に関する情報を取得できるだけではなく、1 冊、あるいは 1 冊以上の本の内容を含んだファイルをダウンロードすることもできます。さらに、このサービスを使って、新しい本をアップロードすることもできます。この Web サービス・コードには、次の 4 つの主なコンポーネントがあります。
- 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 は添付ファイルを保持する public プロパティーなので、これを XSD_BASE64BINARY データ型として宣言します (WSDL ファイルでは xsd:base64Binary データ型として現れます)。この LotusScript データ型は lsxsd.lss ファイルの中で定義され、ファイル内容の取得、設定に使用される 2 つのメソッド (SetValueFromNotesStream と GetValueAsNotesStream) を持っています。
サンプル・データベースの BookDownloadUpload Web サービスには、LS2J を使って base64 機能を処理する例が置いてあります。またサンプル・データベースには、全プロセスを Java で行う方法を示した BookDownloadUploadJava Web サービスもあります。
(SetValueFromNotesStream を使って) NotesStream がオブジェクトの中に読み込まれると、バイナリーまたは ASCII のデータ・ストリームを渡すことができます。そしてこのクラスは、データを base64 でエンコードしてくれます。同様に、オブジェクト内に保存されたファイル・データに関しては、(GetValueAsNotesStream を使うと) バイナリーの NotesStream としてデータを抽出でき、そして base64 デコーディングが処理されます。
そうすると問題は、NotesStream の処理になります。ローカル・ファイルシステムに対してファイルを読み書きするのであれば、ファイルをストリームに変換するのは非常に単純です (この例については Lotus Domino Designer の Help を参照してください)。NotesDocument または NotesRichTextItem の添付ファイルを NotesStream に変換するためには、まずそのファイルを取り出してからストリームに書き込む必要があります。同様に、NotesStream を添付として文書に付加する場合は、まずそのストリームを一時ファイルに書き込んだ後、その一時ファイルを文書に添付する必要があります。この Web サービスのカスタム関数、firstAttachmentToStream と createTempFile は、この処理を行います (その動作についてはサンプル・データベースのコードを参照してください)。
クライアント側では、ユーザーが添付ファイルを送信または受信する際には、クライアント・コード自体で base64 のエンコーディングとデコーディングを処理する必要があります。一部のクライアント (Apache Axis など) は、この処理を行います。他のクライアント (MSSOAP など) の場合は、皆さんが手動で処理する必要があるかもしれません。
BookDownloadUpload クラスは、先ほど見た BookSearch クラスと非常に似ています。このクラスには、次の 5 つのメソッドがあります。
- getFirstTitleMatch: 与えられた検索ストリングに一致する標題を持つ最初の本に関する 1 つの BookInfoAndFile オブジェクトを返します。
- getAllTitleMatches: 与えられた検索ストリングに一致する標題を持つすべての本に関する BookInfoAndFileArray オブジェクトを返します。
- getDocByNoteID: 指定された NoteID の book 文書に関する 1 つの BookInfoAndFile オブジェクトを返します。
- addNewFileComplex: これを使うことで、クライアントは BookInfoAndFile オブジェクトをアップロードして新しい book 文書を作成することができます。
- addNewFile: これを使うことで、クライアントは book の個々の要素をすべて別々のパラメーターとして渡して新しい book 文書を作成することができます。
BookSearch クラスの場合と同じく、Web サービスのクライアントには利用できない 2 つの private ヘルパー・メソッド (throwFault と findDocsByTitle) があります。
下記は 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 メソッドのコードは、(BookInfo オブジェクトではなく BookInfoAndFile オブジェクトを使うことを除いて) BookSearch クラスの同じ関数のコードと基本的に同じです。このリストでは、BookTypeInitializer クラスを使って列挙を初期化するコード行を New sub に追加してあります。また throwFault メソッドは、Messagebox を使ってサーバー・コンソールにエラー・メッセージを書き出す方法を示しています。これらを除けば、BookDownloadUpload クラスは BookSearch クラスと非常によく似ています。
addNewFile メソッドと addNewFileComplex メソッドには、ほとんどコードがありません。これは、アップロードされた book データからデータベースの中に新しい文書を作成するためのロジックは、既に BookInfoAndFile の中で作成されているためです。
Web Services Bookstore サンプル・データベースのサービスのいずれかをテストする場合のために、(ファイルのダウンロードとアップロードを含めて) すべての Web サービス・メソッドをコールする 2 つのサンプル・エージェントがデータベースの中に用意されています。これらのエージェントは Java で書かれており、また OpenNTF.org で入手できるオープンソースの Stubby データベースによって生成された、Apache Axis スタブ・ファイルを使っています。
また、MSSOAP を使って単純に本の検索を行うサンプルも、いくつか含まれています。ただし、最新の MSSOAP 3.0 ライブラリーをインストールしておかないと、MSSOAP のサンプルは Web サービスを RPC/Encoded にした場合にしか動作せず、また BookDownloadUpload サービスを使った場合にも動作しません (MSSOAP 1.x は、このサービスで使用する BookType 列挙を解釈できないため)。MSSOAP を使う場合の制約の詳細については、前回の記事「IBM Lotus Domino 7 での実用的 Web サービス: 単純な Web サービスを作成してテストする (Practical Web services in IBM Lotus Domino V7: Writing and testing simple Web services)」を参照してください。
また、この連載のこれまでの記事で概要を説明した、他のテスト・ツールを使うこともできます。
以下は、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 サービスにアクセスするためのセキュリティー要求をよく理解する必要があります。ある匿名ユーザーが WSDL ファイルを読もうとする場合、あるいは Web サービスのメソッドをコールしようとする場合、そのユーザーがデータベースと Web サービスに対して少なくとも Reader アクセス権を持っていないと、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 サービスをコールするサーバー・ベースのエージェントに関しては、次のような条件が満足されていることを確認します。
- そのエージェントまたは Web サービスの Runtime Security Level が、少なくとも「2. Allow Restricted Operations」であること (これは、エージェントまたは Web サービスの設計要素を編集する際に利用する Properties ボックスの中で設定できます)。
- そのエージェントまたは Web サービスの署名者が、Server 文書に対して「Run unrestricted methods and operations (制限付きはでないメソッドやオペレーションを実行する)」権限を持っていること。
- そのエージェントまたは Web サービスの署名者が、Server 文書に対して「Run restricted LotusScript/Java agents (制限付きの LotusScript/Java エージェントを実行する)」権限を持っていること。
こうした権限が適切に設定されていない場合、そのエージェントまたは Web サービスが実行されると、(通常はサーバー・コンソールと log.nsf ファイルの中に) セキュリティー・エラーが現れます。
Web サービスが error 500 を返す場合には LotusScript の再コンパイルが必要
WSDL ファイルをアクセスしようとすると、あるいは Web サービスのメソッドを実行すると、「Unknown LotusScript Error」メッセージと共に HTTP error 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 データ型の 1 つとして返される必要があります (これに関しては、この連載の前回の記事で詳しく説明されています)。
Lotus Domino V7.0.1 以降を使う (特に、列挙を使う場合には)
Lotus Domino V7.0.1 では、微妙ながら重要ないくつかの問題が修正されています。最も注目すべき点として、最初の Lotus Domino V7.0 リリースには、列挙が適切に動作しないという問題がありました。Lotus Domino のバージョンが V7.0.1 パッチ・レベル以上であることを確認し、またバグの疑いがある場合には、公開されている Notes/Domino fix list と Lotus Support Knowledgebase をチェックしてください。
Web サービス・クライアントがタイムアウトしても、Web サービスは実行を継続している可能性があります
コンパイルに長い時間を要する Web サービス・メソッドを Web サービス・クライアントがコールする場合には、Web サービスがその処理を終了する前にクライアント・コードがタイムアウトしてしまう可能性があります (Web サービス・クライアントのタイムアウトのデフォルト値は 60 秒が一般的です)。
たとえレスポンスを待っているクライアントがタイムアウトしてしまっても、Web サービスはその処理が終了するまで、あるいは Domino Server 文書で設定されるエージェント・タイムアウト・リミットまでリクエストの処理を続けるので、注意が必要です。大きなファイルをアップロードする場合、あるいは複雑なデータベース・トランザクションの場合には、Web サービス・クライアントはタイムアウト・メッセージを受け取るかもしれず、また実際にはアップロードまたはプロセスが行われていても (そして数分後には終了するとしても)、それは行われなかったと考えるかもしれません。
これに関連した注意として、Lotus Domino V7 での新しいエージェント・プロファイリング手法を使うことによって、Web サービスのさまざまな部分を実行するために必要な時間のログを生成できることを忘れないでください。エージェントや Web サービスのプロファイリングを有効にし、使用するための方法の詳細に関しては、Lotus Domino Designer Help を参照してください。
複合データ型として使われる、LotusScript クラスの public プロパティーがアルファベット順であることを確認する
この問題は今後のバージョンの Lotus Domino では解決されるかもしれませんが、リリース 7.0.1 の時点では、LotusScript 複合データ型クラスの public プロパティーがアルファベット順でないと、問題が起こる可能性があります。例えば、次のコードは動作します。
リスト 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
|
この問題による症状としては、パラメーターとして送信される、あるいはレスポンスとして返される複合データ・オブジェクトに、一部の要素が欠けてしまうことです。これは解決するのがやっかいな問題ですが、多くの場合は public プロパティーをアルファベット順に並べることで解決できます。
ただし、単純にソースコードの中でプロパティーを並べ替えるだけでは不十分なことに注意してください。これは、(たとえプロパティーの順序が変更されても) プロパティーのリストが同じである限り WSDL は再生成されないためです。そこで、次のようにする必要があります。
- プロパティーを並べ替える
- 新しい、偽のプロパティーを追加する
- Web サービスを保存して閉じる
- 再度 Web サービスを開く
- 偽のプロパティーを削除する
- Web サービスを保存して閉じる
これがどの Web サービス・クライアントに限定された問題なのか、私達にもわかっていませんが、Apache Axis と Apache SOAP の両方を使用する際に起きることはわかっています。
Web サービスから受信した大きなストリング (32 KB 以上) を NotesDocument のプレーン・テキスト・フィールドに書き込む際には注意する
複合データ型のパラメーターあるいは要素を使い、その値を NotesDocument に書き込む Web サービスは一般的です。これを行う場合には、1 つの文書に対する Notes のプレーン・テキスト・フィールドが 64 KB (約 32,000 文字) に制限されていることに注意してください。これよりも大きな値をテキスト・フィールドに書き込もうとすると、予期せぬエラーを受信する羽目になります。
NotesDocument のテキスト・フィールドに書き込むデータが 64 KB よりも大きくなる可能性がある場合は、そのデータを Rich Text フィールドに書き込んだ方が無難です。
LotusScript で base64 エンコーディング/デコーディングを使用する場合は、LS2J と Java を使うことを検討する
Web サービスの中で、20 KB よりも大きいファイルに対して base64 エンコーディングまたはデコーディング・オペレーションを行う場合には、XSD_BASE64BINARY クラスの中にあるネイティブの LotusScript エンコーディング/デコーディングではなく、Java を使った方が得策かもしれません。大きなデータ・ストリングを構文解析する場合、LotusScript は比較的遅いのですが、Java は非常に高速です。
この機能を LS2J と Java を使って提供する方法については、Web Services Bookstore データベースの例を見てください (ダウンロード・セクションにあります)。BookDownloadUpload Web サービスの BookInfoAndFile クラスの getFile メソッドと setFile メソッドは、Base64 LS2J という LS2J スクリプト・ライブラリーをコールします (このライブラリーは Base64Java スクリプト・ライブラリーのカスタム Java クラスを使っています)。
約 150 KB のファイルを使ってテストしたところ、LS2J ルーチンを使って行った base64 デコーディングは、ネイティブの LS2J ルーチンを使った場合よりも約 100 倍高速でした (私達のワークステーションでは、1 秒か 2 秒に対して約 2 分という違いでした)。
Lotus Domino V7.0 での Web サービスに関するこの 3 回の連載では、Lotus Domino を使った Web サービスを理解し、使いこなすために、基本的な概念から複雑なサービスの作成に至るまでの全プロセスを解説しました。そして、単純なデータ型や配列、複合データ型、列挙、ファイル添付、そして LotusScript Web サービスでのカスタム・フォールトなどの使い方を解説し、そうした概念それぞれに関する実用的なサンプル・コードと、すぐに使用できるサンプル・データベースなども紹介しました。サンプル・データベースには、LotusScript での Web サービスに対応する Java Web サービスも含まれているため、それらを見れば LotusScript メソッドがどのように Java にマッピングされるかも理解できるはずです。さらに、さまざまなフリー・ツールを利用して Web サービスをテストする方法についても解説しました。またダウンロードできるサンプル・データベースには、Lotus Notes V7.0 クライアントから直接 Web サービスをコールするためのエージェントが、いくつか含まれています。
こうした強力な手法を自在に使いこなせば、Domino データベースをエンタープライズ SOA アーキテクチャーへ積極的に取り込むことができます。それによって、機能的な面でも設計的な面でも、アプリケーションや組織全体にとって大きな可能性が生まれます。これは Lotus Notes/Domino が前進する方向の単なる 1 つに過ぎず、また Lotus Notes/Domino が重要で生産的なアプリケーション開発のプラットフォームであり続ける理由の1 つでしかありません。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample database for article | WSBookstore.nsf | 1.82 MB | HTTP |
学ぶために
- developerWorks に用意された Lotus に関する記事、「Practical Web services in IBM Lotus Domino 7: What are Web services and why are they important?」を読んでください。
- developerWorks に用意された Lotus に関する記事、「IBM Lotus Domino 7 での実用的 Web サービス: 単純な Web サービスを作成してテストする (Practical Web services in IBM Lotus Domino 7: Writing and testing simple Web services)」を読んでください。
- developerWorks に用意された Lotus に関する記事、「Lotus Notes/Domino 7 Web services」を読んでください。
- developerWorks のチュートリアル、「Quickly create Domino Web services: New Web services function in Domino 7 speeds development」を読んでください。
- developerWorks に用意された Lotus に関する記事、「Consuming Web services from a Lotus Domino Java agent」を読んでください。
製品や技術を入手するために
- LotusScript から Web サービスを呼び出す最新の MSSOAP ツールキットをダウンロードしてください。
- Lotus Notes Java エージェントで使用する Apache Axis スタブ・ファイルを生成するための、Stubby データベースをダウンロードしてください。
議論するために
-
developerWorks blogs を訪れ、developerWorks のコミュニティーに加わってください。