Practical Web services in IBM Lotus Domino 7: Writing complex Web services

In the third and last article in our IBM Lotus Domino Web services series, we discuss more advanced techniques for using Domino Web services, such as complex data types, enumerations, file attachments, and custom faults.

Julian Robichaux, Developer, Independent Consultant

Julian Robichaux is a software developer and professional programmer specializing in IBM Lotus Notes and Java development. He is available for hire on small and large projects involving development, architecture, and training. In his spare time, he adds to his personal Web site/blog at http://www.nsftools.com. His family can't understand why he takes his laptop with him everywhere. He doesn't quite understand it either.



21 November 2006

Also available in Chinese Russian

The first article in this series, "Practical Web services in IBM Lotus Domino 7: What are Web services and why are they important?," covered the basics of Web service and SOA terminology, and the second article, "Practical Web services in IBM Lotus Domino 7: Writing and testing simple Web services," outlined the details of writing and testing simple Web services in Lotus Domino V7. This third and final article in the series discusses techniques for writing more complex (and useful) Web services in Lotus Domino V7.

The examples discussed in this article are available in a Notes database that you can download (see the download section at the end of this article). As with the previous installment, all code examples in the article are written in LotusScript. However, there are identical Java examples in the sample database that accompanies this article. The database also contains a few sample agents that you can use to call and test the Web services.

About complex data types

So far, we have dealt only with simple parameters and return values: strings, integers, simple arrays, and the like. We have even shown you how to return multiple values using InOut parameters. However, as you develop more sophisticated Web services, you may soon find yourself in situations in which you need to send and receive entire data structures. This is done quite often in XML documents, for example:

Listing 1. Example XML book structure
<book>
    <author>Barry Allen</author>
    <title>Life in the Fast Lane</title>
    <booktype>Biography</booktype>
</book>

Using a structure like this, you can easily send whole <book> objects back and forth as parameters or responses. You can even create nested structures in the following manner:

Listing 2. Example XML bookshelf structure
<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>

In LotusScript, you can use custom types or classes to do this. In a Web service, you use an object called a complex data type.

Make sure complex data type properties are alphabetical

Make sure that the properties in your LotusScript complex data types are alphabetical. We are not sure why, but certain clients (Apache Axis and Apache SOAP, for example) can have problems when the LotusScript complex data type properties are not in alphabetical order. Non-alphabetic properties can cause some data elements in the complex type to go missing in either the request or the response.

A complex data type in a LotusScript Web service is simply a public class with one or more public properties. Each public property shows up as an element of the complex data type. For example, to model the <book> structure as a LotusScript class, you can write the following code:

Listing 3. Example LotusScript book class
Class Book
	Public author As String
	Public booktype As String
	Public title As String
End Class

This is reflected in the WSDL file as a structure like this:

Listing 4. Example WSDL complex data type definition for the LotusScript book class
<complexType name="BOOK">
    <sequence>
        <element name="AUTHOR" type="xsd:string"/>
        <element name="BOOKTYPE" type="xsd:string"/>
        <element name="TITLE" type="xsd:string"/>
    </sequence>
</complexType>

Notice that the data type name and the properties are all converted to uppercase in the WSDL file. This is something that Lotus Domino does with LotusScript Web services because LotusScript is not case-sensitive, but the Web services are.

Using complex data types in your LotusScript Web services, you can write methods with signatures like this:

Listing 5. Example LotusScript methods using the book class
Public Function findTitle (searchString As String) As Book
Public Sub uploadNewBook (newBook As Book)

This is a lot easier to manage than methods with large numbers of parameters that need to be sent and/or returned.


The Web Services Bookstore: our example database

The example database that accompanies this article (see the download section for a link) is a bookstore database that uses Web services to allow people to search for, download, and upload files containing the contents of a book. The database is pre-populated with a few public domain eText files from Project Gutenberg.

Figure 1 shows an example of a document in the database.

Figure 1. A document in our sample Web Services Bookstore database
A document in our sample Web Services Bookstore database

You can see that each book document has a title, author, book type, description, and file attachment.

There are two LotusScript Web services in the database:

  • BookSearch. Allows you to search for books and return title, author, book type, and description.
  • BookDownloadUpload. Allows you to retrieve information about a book, including the file attachment. It also allows you to upload new book files.

We discuss the code of both Web services throughout the rest of this article. The second service is more complicated than the first because it allows for file upload/download and uses a Web service data structure known as an enumeration.


Inside the BookSearch Web service

The BookSearch Web service lets you search for books and get simple information such as title, author, book type, and description. For methods that always return information about a single book, a BookInfo complex data type is returned. For methods that may return information about one or more books, a BookInfoArray complex data type is returned, containing an array of BookInfo objects and an element indicating how many items should be in the array.

The general structure of the BookInfo class in LotusScript is:

Listing 6. LotusScript BookInfo class
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

We have a number of data elements that map to fields on or information about the book document in the database. One of the things we return is the NoteID of the document to make it easy for a client to retrieve a single title after it receives a BookInfoArray containing several titles.

There is also a helper method inside the class:

Listing 7. The getDocContents method in the LotusScript BookInfo class
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

This function is useful for taking a NotesDocument object from within this database and reading its information into the public properties of this BookInfo object. Notice that this method does NOT occur anywhere in the complex data type definition, even though it is public. Only the public properties are available for a complex data type.

The general structure of the BookInfoArray object is:

Listing 8. LotusScript BookInfoArray class
Class BookInfoArray
	Public bookArray() As BookInfo
	Public count As Integer
End Class

The bookArray property is an array of BookInfo objects, and the count property is the number of elements that should be contained within the bookArray. While not strictly necessary, we find it very useful to include a count property to correspond with arrays like this because it makes it easier for Web service clients to determine whether or not the array is empty. They can simply check to see if the count is zero (empty) or a positive number (not empty). Otherwise, there can be confusion over whether an empty array is a null object, an array with no elements, or an array with a single object containing null or empty values.

The BookInfoArray class also has a helper method:

Listing 9. The setArrayFromCollection method in the LotusScript BookInfoArray class
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

Much like the getDocContents method in the BookInfo class, this method takes a NotesDocumentCollection of Book documents in this database and adds each NotesDocument in the collection to the internal bookArray property. It also properly sets the count property based on the number of documents in the collection.


Throwing faults and dealing with errors

Before we examine the BookSearch class, let's take a short break to discuss the concept of faults in a LotusScript Web service.

In Web service terminology, a fault is an error. If you have no fault generation or error handling in a LotusScript Web service and an error occurs when you call a method of the service, you receive the following SOAP response:

Listing 10. Generic LotusScript SOAP fault
   <soapenv:Body>
      <soapenv:Fault>
         <faultcode>soapenv:Server.generalException</faultcode>
         <faultstring>LotusScript did not run to completion.</faultstring>
         <detail/>
      </soapenv:Fault>
   </soapenv:Body>

The actual error that occurred appears on the server console and in the log.nsf file, but unfortunately, the fault that is returned to the user is extremely generic and not very useful.

In many cases, if not all, you want to generate your own faults when errors happen; otherwise, the Web service cannot return a value. To do this, you can add what is called explicit fault handling to your methods. This requires the following steps:

  1. Make sure that the %INCLUDE "lsxsd.lss" line is in the Options section of the Web service code.
  2. Add a WS_FAULT parameter as the last parameter of your method (it must be the last parameter).
  3. When an error occurs or when you want to return a fault to the user, set the WS_FAULT object and exit the method with an Exit function or Exit sub statement.

Here's an example:

Listing 11. An example of throwing an explicit LotusScript fault
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

When the client calls the stringToNumber method, it sees only a single string object to be passed to the method. The fault parameter is hidden. However, if an error occurs, you can set the fault message to anything you want. In this example, if the client passed a bogus value to the method (such as foo), it receives the following response:

Listing 12. The SOAP fault returned by our explicitly generated fault
<soapenv:Fault>
    <faultcode>soapenv:Server.generalException</faultcode>
    <faultstring>Type mismatch</faultstring>
    <detail/>
</soapenv:Fault>

Tip: Use a fault generation method

In the BookSearch and BookDownloadUpload classes, we call a separate method whenever we need to generate a fault. This allows you to easily add global logging or notifications whenever faults occur in your Web service.

The <faultstring> element in this case is much more descriptive than a generic "LotusScript did not run to completion" message.

There are two other things that are worth mentioning about the processError block in the stringToNumber method:

  • You must explicitly set the fault object to True, using setFault.
  • The Messagebox statement is optional, but it shows that you can also write errors (or other information) to the server console and log.nsf file at any point in the Web service execution using Messagebox statements. This can be useful for simple debugging or logging.

Anatomy of the BookSearch class

Back to our BookSearch Web service example. The BookSearch class, which is exposed as an interface for this Web service, has three public methods:

  • getFirstTitleMatch. Returns a single BookInfo object for the first book with a title that matches a given search string.
  • getAllTitleMatches. Returns a BookInfoArray object with all books having titles that match a given search string.
  • getDocByNoteID. Returns a single BookInfo object for the book document at the specified NoteID.

There are two private helper methods that are not available to Web service clients:

  • throwFault. Called by the methods when they need to generate a fault, need to provide a common way to create, and need to log any faults that are returned.
  • findDocsByTitle. Returns a NotesDocumentCollection containing book documents in this database that match a given search string.

While the findDocsByTitle method is a basic NotesView searching function, it's worth taking a look at the throwFault method. Here is the code:

Listing 13. The throwFault method in the LotusScript BookSearch class
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

We did the same thing in the fault example earlier, but instead of setting fault properties in the individual Web service methods, we have each of the methods call this function instead. The advantage to doing this is noted in the comment at the end of the function: "do any other error logging things here."

Generating faults using a routine like this makes it easy to provide a central place to manage and log the faults that occur in your Web service. You might write a message to the server console using a Messagebox statement, write to a central NotesLog, or even use a custom error logging database such as OpenLog. Again, the separate method for handling faults is not strictly necessary, but is a good practice.

As for the public methods in our BookSearch class, the code for these methods is as follows:

Listing 14. The LotusScript BookSearch class that is used as a Web service implementation
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

The code in the methods is fairly short and easy to understand, but we will point out a few things:

  • Notice that the getFirstTitleMatch and getDocByNoteID methods generate a fault if a search is empty, but getAllTitleMatches only generates a fault if there is a LotusScript error. This demonstrates that we can return a fault object even if a true runtime error didn't occur. We could also have returned an empty BookInfo object in this case.
  • The brevity of the code in the methods demonstrates the usefulness of the BookInfo.getDocContents method and the BookInfoArray.setArrayFromCollection method. Including these helper methods in the complex data type classes not only keeps our Web service code shorter, but it also allows us to add or modify that common bit of functionality in a central place.

If you want to test the Web service methods, you can either use the test agents in the Web Services Bookstore database or use one of the techniques described in the previous article, "Practical Web services in IBM Lotus Domino V7: Writing and testing simple Web services."


Using enumerations

Before we talk about the code in the BookDownloadUpload Web service, there is one other data structure that we need to discuss. It is a structure called an enumeration.

Only use enumerations in Lotus Domino V7.0.1 and later

You should not use enumerations unless you are running Lotus Domino V7.0.1 or later. There was a problem with the enumeration handling in the initial release of Lotus Domino V7.0 in which the data types that were expected and returned for enumerations were incorrect. The issue was resolved in release 7.0.1.

Enumerations are simply fixed lists of items that can be used as parameters of a Web service method or elements of a complex data type. In our bookstore example, there are only four categories of books that we allow:

  • Fiction
  • Non-fiction
  • Reference
  • Comic book

On a Domino form or a Web page, we can control this sort of input using a selection list, so that users cannot add arbitrary data to a restricted field. In a Web service, we use enumerations. On the WSDL document, an enumeration looks something like:

Listing 15. An example of what an enumeration looks like in a WSDL document
<simpleType name="BOOKTYPE">
    <restriction base="xsd:string">
        <enumeration value="Fiction"/>
        <enumeration value="Non-Fiction"/>
        <enumeration value="Reference"/>
        <enumeration value="Comic Book"/>
    </restriction>
</simpleType>

Web service clients that use parameters or complex data types with this type of enumeration can only pass one of the values listed in the enumeration definition as a value. Any other values generate either an error in the client code or a fault when the Web service is called.

In LotusScript, there are a number of very specific things you have to do to generate an enumeration structure. As an example, here is the code for the BookType enumeration in the BookDownloadUpload Web service:

Listing 16. The LotusScript BookType enumeration code
'** 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

Here's what you need to do to create an enumeration in LotusScript:

  1. Decide on a name for your enumeration (in our case, BookType).
  2. Create a global Const variable for each item in the enumeration with each Const name starting with the enumeration name followed by an underscore (in our case, BookType_xxx).
  3. Create a global object for each item in the enumeration with a data type equal to the enumeration name and each object name ending with an underscore followed by the enumeration name (in our case, xxx_BookType).
  4. Create a global list with a data type equal to the enumeration name and with a name of Enum_ followed by the enumeration name (in our case, Enum_BookType).
  5. Create a custom class with the same name as the enumeration name. This class must have a single public property called Value of the same data type as the Const variables you created for this enumeration in step 2.
  6. When the Web service is called, run code to initialize all the global enumeration objects with proper values (in our case, by creating an instance of the BookTypeInitializer class).

When you are done, you can use the custom enumeration class that you wrote in step 5 anywhere that you want the enumeration to be used. It shows up in the WSDL file as an enumeration element, and the value restrictions specified by your Enum list in step 4 apply.

On one hand, it's a lot of very specific code to write to generate a restricted list of values that a client can use. On the other hand, it's the proper way to handle such a list in a Web service.

To make this code-writing process easier, try the Domino Enumeration Code Generator page, which allows you to enter an enumeration name, a data type, and all the values you want to use. It generates the LotusScript and Java code for you. This can make it a lot faster to get started with Web service enumerations in Lotus Domino.


Inside the BookDownloadUpload Web service

The other Web service in our Web Services Bookstore database is the BookDownloadUpload service. This service not only allows you to retrieve information about a book document in our bookstore, it also allows you to download the file containing the contents of one or more books. In addition, you can upload new books with this service. There are four main components of this Web service code:

  • The BookType enumeration, which we've already discussed.
  • The BookInfoAndFile complex data type, which contains book information and the file data.
  • The BookInfoAndFileArray complex data type, which is an array of BookInfoAndFile objects (exactly like the BookInfoArray type in the BookSearch service).
  • The BookDownloadUpload class, which is the interface class for our Web service.

We outlined the BookType enumeration in the previous section. The BookInfoAndFileArray class is essentially identical to the BookInfoArray class in the BookSearch service. The BookInfoAndFile and BookDownloadUpload classes are discussed in more detail later.


Dealing with file attachments

The big difference between the BookDownloadUpload service and the BookSearch service is that you can upload and download files with the BookDownloadUpload service. To do this, you use the XSD_BASE64BINARY class that is defined in the lsxsd.lss file.

To start with, here is the code for the BookInfoAndFile class:

Listing 17. The LotusScript BookInfoAndFile class
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 is the public property that holds file attachments, so you declare it as an XSD_BASE64BINARY data type (which shows up as an xsd:base64Binary data type in the WSDL file). This LotusScript data type is defined in the lsxsd.lss file and has two methods that are used for getting and setting the file contents: SetValueFromNotesStream and GetValueAsNotesStream.

Java Base64 encoding/decoding is much faster than LotusScript

If you use base64 encoding and decoding for files larger than 20 KB, consider using Java (either a Java Web service or LS2J) to handle the encoding for you. The LotusScript base64 routines are very slow with anything other than small files -- especially the decoding routines.

The BookDownloadUpload Web service in the sample database has an example of using LS2J to handle the base64 functions, and there is also a BookDownloadUploadJava Web service in the database that demonstrates how to do the whole process in Java.

When a NotesStream is read into the object (using SetValueFromNotesStream), you can pass a binary or ASCII stream of data, and the class encodes the data in base64 for you. Likewise, for file data stored inside the object, you can extract the data (using GetValueAsNotesStream) as a binary NotesStream, and the base64-decoding is handled for you.

The trick, then, is working with NotesStreams. If you read or write files to the local file system, it is very straightforward to convert the files to streams (see the Lotus Domino Designer Help for examples). To convert a file attachment on a NotesDocument or NotesRichTextItem to a NotesStream, you must first detach the file, and then write it to a stream. Likewise, to attach a NotesStream to a document as an attachment, you must write the stream to a temporary file, and then attach the temporary file to the document. The custom firstAttachmentToStream and createTempFile functions in our Web service handle that functionality for you (see the code in the example database to see how it works).

On the client side, when a user sends or receives a file attachment, the client code has to handle the base64 encoding and decoding itself. Some clients (such as Apache Axis) do this for you. Others (such as MSSOAP) may require you to do it manually.


Anatomy of the BookDownloadUpload class

The BookDownloadUpload class is very similar to the BookSearch class that we looked at earlier. It has five public methods:

  • getFirstTitleMatch. Returns a single BookInfoAndFile object for the first book with a title that matches a given search string.
  • getAllTitleMatches. Returns a BookInfoAndFileArray object with all books having titles that match a given search string.
  • getDocByNoteID. Returns a single BookInfoAndFile object for the book document at the specified NoteID.
  • addNewFileComplex. Allows clients to create new book documents by uploading a BookInfoAndFile object.
  • addNewFile. Allows clients to create new book documents by passing all the individual elements of a book as separate parameters.

Just like the BookSearch class, there are two private helper methods (throwFault and findDocsByTitle) that are not available to Web service clients.

The code for the BookDownloadUpload class is listed below:

Listing 18. The LotusScript BookDownloadUpload class that is used as a Web service implementation

Click to see code listing

Listing 18. The LotusScript BookDownloadUpload class that is used as a Web service implementation

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

As you can see, the code for the getFirstTitleMatch, getAllTitleMatches, and getDocByNoteID methods are essentially identical to the code for those same functions in the BookSearch class, except we use BookInfoAndFile objects instead of BookInfo objects. We also added a line of code in the New sub to initialize our enumeration using the BookTypeInitializer class, and the throwFault method demonstrates the use of Messagebox to write error messages to the server console. Otherwise, the BookDownloadUpload class very closely resembles the BookSearch class.

As for the addNewFile and addNewFileComplex methods, those have very little code because the logic for creating new documents in the database from uploaded book data has been written in the BookInfoAndFile class.


Testing the example database

If you want to test either of the services in the Web Services Bookstore example database, we have provided two sample agents in the database that call all Web service methods (including downloading and uploading files). These agents are written in Java and use Apache Axis stub files that were generated by the open source Stubby database available at OpenNTF.org.

We also provide some examples of using MSSOAP to perform a simple book search -- be aware that unless you have the latest MSSOAP 3.0 library installed, the MSSOAP examples work only if you leave the Web services as RPC/Encoded, and they do not work with the BookDownloadUpload service because MSSOAP 1.x cannot interpret the BookType enumeration that we use in that service. See the previous article, "Practical Web services in IBM Lotus Domino V7: Writing and testing simple Web services," for more details on the limitations of using MSSOAP.

You can also use the other testing tools that were outlined in the previous article in this series.


Caveats and troubleshooting

Here are some general caveats and troubleshooting guidelines to assist you if you have problems with your Web service code.

When testing locally, make sure the Notes HTTP service is running

If you have a local copy of a database with a Web service that you want to test, you need to make sure that the Notes HTTP service is running in the background first. The easiest way to do that is to:

  1. Open the database in Domino Designer.
  2. Select a form or view in the database.
  3. Choose Design - Preview in Web browser - Default Browser and wait for the form or view to appear in a Web browser

Once you see the form or view as a Web page, close the browser window and test your Web service using localhost as your server name. The HTTP service runs in the background until you close the Notes client completely.

NOTE: Some personal firewalls block the operation of the local HTTP service, so you may have to adjust your firewall settings.

If a user does not have access to a Web service, he receives a 401 Access Denied error

Make sure you understand the security requirements for accessing your Web service. If an anonymous user does not have at least Reader access to the database and the Web service, he receives a 401 Access Denied error when trying to read the WSDL file or when calling a Web service method. In the initial 7.0 version of Lotus Domino, a 404 Not Found error was generated in this situation, which was a little less clear.

Use Messagebox or System.out.println to output information to the server console

While it's not usually a good error logging technique to simply dump messages to the server console (and log.nsf file), if you need to do some quick temporary logging, it is an easy answer. With a Domino Web service, messages written using Messagebox (in LotusScript) or System.out.println (in Java) are sent to the server console.

A better way, of course, is to use a more global logging technique such as NotesLog or the OpenLog database. If you use a central method to handle your fault generation (as we did in the example Web services in this article), you can easily add calls to a logging system like that to track your messages and errors.

Runtime security for server agents that call Web services and services that read/write to files

For Web services that read/write files or access the network or for server-based agents that make calls to Web services, make sure that the following conditions are true:

  • The agent/Web service has a Runtime Security Level of at least "2. Allow Restricted Operations" (this can be set in the Properties box that is available when the agent or Web service design element is edited).
  • The signer of the agent/Web service has "Run unrestricted methods and operations" rights on the Server document.
  • The signer of the agent/Web service has "Run restricted LotusScript/Java agents" rights on the Server document.

If these rights are not set properly, you see security errors (usually on the server's console and in the log.nsf file) when the agent or Web service is run.

Web service returns an error 500 when you need to recompile LotusScript

If you receive an HTTP error 500 with a message of "Unknown LotusScript Error" when you try to access the WSDL file or when you run a method on your Web service, then you need to recompile all LotusScript in the database. This error often occurs when your Web service uses functions from a script library, and the library has been updated more recently than the Web service. This is similar to the condition you are in when the Lotus Notes client displays "Error Loading USE or USELSX" messages.

Convert variant arrays to fixed arrays

If you return an array from a LotusScript Web service, make sure that you return a fixed array instead of a dynamic variant array. For example, the following types of LotusScript calls generate variant arrays:

  • varval = doc.SomeFieldName
  • varval = Split("I am superman", " ")
  • varval = Evaluate(|@DbColumn("";"":"somedatabase";"some view";2)|)

Fortunately, this is an easy situation to get around. You can write the variant array to a fixed array with code like this:

Listing 19. LotusScript code for converting a variant array to a fixed array
Dim returnArray() As String
varval = doc.MultiValueCategoryField
Redim returnArray(Ubound(varval))
For i = 0 To Ubound(varval)
	returnArray(i) = varval(i)
Next

NOTE: The array needs to be returned within one of the ARRAY_HOLDER data types defined in the lsxsd.lss file (the previous article in this series discusses this situation in more detail).

Use Lotus Domino V7.0.1 or later, especially if you use enumerations

A few subtle, but important issues were fixed in Lotus Domino V7.0.1. Most notably, there was a problem in the initial Lotus Domino V7.0 release that caused enumerations not to work properly. Make sure you use a version of Lotus Domino at or above the V7.0.1 patch level, and check the public Notes/Domino fix list and Lotus Support Knowledgebase for suspected bugs.

Even if the Web service client times out, the Web service may keep running

If a Web service client makes a call to a Web service method that takes a long time to complete, it is possible that the client code will time out before the Web service finishes doing whatever it's doing (60 seconds is a common default Web service client timeout value).

Be aware that even if the client times out waiting for a response, the Web service will continue to process the request until it either finishes or reaches the agent timeout limit set in the Domino Server document. For a large file upload or a complex database transaction, the Web service client may get a timeout message and think that the upload or process did not take place, even though it actually did (possibly finishing a few minutes later).

On a related note, don't forget that you can use the new agent profiling technique in Lotus Domino V7 to generate a log of how long various parts of your Web service take to run. See the Lotus Domino Designer Help for more information on how to enable and use agent and Web service profiling.

Make sure public properties in LotusScript classes used as complex data types are alphabetical

This issue may be resolved in a later version of Lotus Domino, but as of release 7.0.1 there can be problems if the public properties of a LotusScript complex data type class are not alphabetical. For example, this code works:

Listing 20. Alphabetical LotusScript complex data type properties (GOOD)
Class Book
	Public author As String
	Public booktype As String
	Public title As String
End Class

But this code may cause problems:

Listing 21. Non-alphabetical LotusScript complex data type properties (BAD)
Class Book
	Public title As String
	Public author As String
	Public booktype As String
End Class

The symptom of this problem is that a complex data object that is sent as a parameter or returned as a response may be missing certain elements. This can be a confusing problem to troubleshoot, and it can often be resolved by arranging the public properties into alphabetical order.

Note, though, that simply rearranging the properties in the source code does not work because the WSDL is not regenerated as long as the list of properties remain the same (even though their order has changed). What you need to do is:

  1. Rearrange the properties.
  2. Add a new bogus property.
  3. Save and close the Web service.
  4. Re-open the Web service.
  5. Remove the bogus property.
  6. Save and close the Web service.

We are unsure which Web service clients this issue is limited to, but we noticed it when using both Apache Axis and Apache SOAP.

Be careful of writing large strings (> 32 KB) received from a Web service to a plain text field on a NotesDocument

It is common to have a Web service that takes a parameter or an element of a complex data type and writes the value to a NotesDocument. If you do this, remember that a plain Notes text field on a document is limited to 64 KB (about 32,000 characters). If you try to write values larger than this to a text field, you receive unexpected errors.

If there's a chance that the data you write to a text field on a NotesDocument is larger than 64 KB, you may want to write the data to a Rich Text field instead.

Consider using LS2J and Java if you use base64 encoding/decoding in LotusScript

If you perform base64 encoding or decoding operations in a Web service on files greater than 20 KB, you may want to use Java for the base64 operations instead of the native LotusScript encoding/decoding that is available in the XSD_BASE64BINARY class. LotusScript tends to be relatively slow at parsing large strings of data, while Java is especially fast at doing so.

See the examples in the Web Services Bookstore database (available from the download section) for a way to use LS2J and Java to provide this functionality. The getFile and setFile methods in the BookInfoAndFile class of the BookDownloadUpload Web service make calls to an LS2J script library called Base64 LS2J, which uses a custom Java class in the Base64Java script library.

In testing with a file of about 150 KB, the base64 decoding done by the LS2J routines was about 100 times faster than the native LS2J routines (one or two seconds versus almost two minutes on our workstation).


Summary

This three-part article series on Web services in Lotus Domino V7.0 led you through the entire process of understanding and using Web services with Lotus Domino from the basic concepts all the way through writing complex services. We discussed the use of simple data types, arrays, complex data types, enumerations, file attachments, and custom faults in LotusScript Web services, including practical sample code for each of these concepts and example databases that you can use right away. The example databases also have Java Web services that mirror the LotusScript ones, so you can see how the LotusScript methods map to Java. We even went over several different methods to test the Web services using a variety of free tools, and the example databases you can download contain several agents that make calls to Web services directly from the Lotus Notes V7.0 client.

With these powerful techniques at your disposal, you should now be ready to turn your Domino databases into active participants in an enterprise SOA architecture. This can open many doors for your applications and for your entire organization both from a functional and a design standpoint. It is just one more way in which Lotus Notes/Domino is moving forward, and why it continues to be such a valuable and productive application development platform.


Download

DescriptionNameSize
Sample database for articleWSBookstore.nsf1.82 MB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into IBM collaboration and social software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Lotus, SOA and web services
ArticleID=174559
ArticleTitle=Practical Web services in IBM Lotus Domino 7: Writing complex Web services
publish-date=11212006