- About complex data types
- The Web Services Bookstore: our example database
- Inside the BookSearch Web service
- Throwing faults and dealing with errors
- Anatomy of the BookSearch class
- Using enumerations
- Inside the BookDownloadUpload Web service
- Dealing with file attachments
- Anatomy of the BookDownloadUpload class
- Testing the example database
- Caveats and troubleshooting
- Downloadable resources
- Related topics
Practical Web services in IBM Lotus Domino 7
Writing complex Web services
This content is part # of # in the series: Practical Web services in IBM Lotus Domino 7
This content is part of the series:Practical Web services in IBM Lotus Domino 7
Stay tuned for additional content in this series.
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.
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
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:
- Make sure that the
%INCLUDE "lsxsd.lss"line is in the Options section of the Web service code.
- Add a WS_FAULT parameter as the last parameter of your method (it must be the last parameter).
- 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>
<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
getDocByNoteIDmethods generate a fault if a search is empty, but
getAllTitleMatchesonly 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.getDocContentsmethod and the
BookInfoArray.setArrayFromCollectionmethod. 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."
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.
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:
- 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:
- Decide on a name for your enumeration (in our case, BookType).
- 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).
- 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).
- 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).
- 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.
- 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:
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 (
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
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
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
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:
- Open the database in Domino Designer.
- Select a form or view in the database.
- 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.
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:
- Rearrange the properties.
- Add a new bogus property.
- Save and close the Web service.
- Re-open the Web service.
- Remove the bogus property.
- 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).
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.
- developerWorks Lotus article, "Practical Web services in IBM Lotus Domino 7: What are Web services and why are they important?"
- developerWorks Lotus article, "Practical Web services in IBM Lotus Domino 7: Writing and testing simple Web services"
- developerWorks Lotus article, "Lotus Notes/Domino 7 Web services"
- developerWorks tutorial, "Quickly create Domino Web services: New Web services function in Domino 7 speeds development"
- developerWorks Lotus article,"Consuming Web services from a Lotus Domino Java agent"
- Download the latest MSSOAP toolkit to call Web services via LotusScript.
- Download the Stubby database to generate Apache Axis stub files for use with Lotus Notes Java agents.