The ability to work with XML within Domino provides the foundation on which we can enable Domino applications for Web services, both as Web service providers and Web service consumers. As discussed in earlier chapters, enabling an application for Web services involves working with three major formats and protocols: SOAP for message exchange, WSDL for service definition, and UDDI for service awareness. Each of these formats/protocols is underpinned by XML; thus, enabling Web services in Domino is largely a matter of working with XML in Domino applications. In this section, we focus on techniques to provide or consume Web services from Domino applications. The focus is definitely on technique. You won't find much about "why Web services should be used" here ”just "how is it done." Providing a Web Service Within a Domino Application We'll look at providing Web services first. Although not required, the SOAP protocol for Web service invocation is most often implemented "on top" of HTTP, and we will discuss SOAP over HTTP exclusively here. A SOAP request for a Web service is not much more than a HTTP request (POST) with a specific XML formatted message. Hence the invocation takes the form of a URL pointing to a Web service handler. A Web service handler can be anything that responds to an HTTP request. For Domino applications, the handler is naturally implemented as a "Web agent," that is, a Domino database agent that can be invoked by a URL to the Domino server. (The "Web agent" distinction is in contrast to the more traditional uses for Domino agents as scheduled or event-driven server tasks .) Let us digress a bit about Web service handlers. Since a handler is usually a Web application server that is running the application that provides the Web service, you may have multiple handlers (servers) for the set of Web services you provide. You then have the issue of to what extent you need provide access to these Web service handlers. Does each handler need to be open to service requests, or should the requests be routed through a single access point? These are indeed important design questions. However, since the request for a particular service must ultimately be handled at the application providing the service, we focus on the handler serving the Web service. So we can define a Web agent in our Domino application to act as the Web service handler. What is required of this agent/handler? Primarily it parses the SOAP (XML) request, performs the service, then formats and returns the SOAP response. Let's look at Java agent code that does just that. We'll call the agent WSHandler . Figure 9-11 shows the agent properties we set for our WSHandler Web agent. Note that we selected "Agent list selection" and a Target of "None" for the Runtime settings, as these are the appropriate choices for a Web agent. (Recall that a Domino Web agent is simply a Domino agent that is exclusively invoked via a URL from a browser.) Figure 9-11. WSHandler agent properties. Here is the WSHandler agent code: import lotus.domino.*; import java.io.*; import java.net.*; public class JavaAgent extends AgentBase { public void NotesMain() { Document aRespDoc = null; // Holds SOAP response try { Session session = getSession(); AgentContext agentContext = session.getAgentContext(); Database aDB = agentContext.getCurrentDatabase(); // Get parameters, request content from the HTTP call (1) Document aDoc = agentContext.getDocumentContext(); if (aDoc != null) { // 'Log' the request as DXL BufferedWriter aBW = new BufferedWriter(new FileWriter("c:\temp\book-wsreq.xml")); (2) aDoc.generateXML(aBW); aBW.close(); // Process the SOAP request. (3) Item aReqContent = aDoc.getFirstItem("REQUEST_CONTENT"); // Get a SAX parser via its factory org.xml.sax.Parser aParser = org.xml.sax.helpers.ParserFactory.makeParser("com.ibm.xml.parsers.SAXParser"); // Set the SAX document handler as our own SOAPHandler. SOAPHandler aHandler = new SOAPHandler(); aParser.setDocumentHandler(aHandler); aParser.parse(aReqContent.getInputSource()); (4) System.out.println("SOAP-ENV: " + aHandler.itsEnv); System.out.println("Service: " + aHandler. itsService); System.out.println("Method: " + aHandler.itsMethod); System.out.println("Encoding: " + aHandler.itsEncodingStyle.); // Process the web service request according to the method // and arguments (not shown) // Prepare the SOAP response aRespDoc = aDB.createDocument(); aRespDoc.appendItemValue("Form", "SOAPRespXML"); (5) aRespDoc.appendItemValue("SOAPRespBody", "<SOAP-ENV:Fault>" + "<faultcode>SOAP-ENV:Client</faultcode>" + "<faultstring>Service not supported.</faultstring>" + "</SOAP-ENV:Fault>"); aRespDoc.save(); PutResponse(aRespDoc.getUniversalID()); aRespDoc.remove(true); } } catch(Exception e) { System.out.println("WSHandler error: " + e); } } // Generates SOAP response from SOAPRespXML document with given // UNID, copies response to agent output (HTTP response). // private void PutResponse(String theUNID) throws MalformedURLException, IOException { // Set URL to get document with XML form applied. (6) URL aURL = new URL("http", "localhost", 80, "/booklist.nsf/0/" + theUNID); URLConnection aCnctn = aURL.openConnection(); aCnctn.connect(); // Copy SOAP response XML to agent output (HTTP response). PrintWriter aWtr = getAgentOutput(); (7) aWtr.println("Content-Type: text/xml "); aWtr.println(" "); BufferedReader aRdr = new BufferedReader(new InputStreamReader(aCnctn.getInputStream())); String aLine = aRdr.readLine(); while (aLine != null) { aWtr.println(aLine); aLine = aRdr.readLine(); } aWtr.close(); } } The agent code is worth a step-by-step examination. We'll use the (n) annotations as reference points. -
The Domino database document object returned by AgentContext.getDocumentContext() contains all of the HTTP request data, including the SOAPAction verb and request content. -
A nice way to view the HTTP request data is to format the context document as DXL and write it to a file. -
The SOAP request XML is obtained from the HTTP request content field. -
We use a SAX parser to parse the SOAP request into its constituent elements, especially the service name, method name , and encoding style. We do not show the actual service processing, as we are most interested here in how to implement the Web service request and response. But this is the point at which the service would be performed. Also there are other options to using a SAX parser for the SOAP request. We present a more in-depth discussion of the parsing options later in this section. -
For the SOAP response XML, we use the "XML form" technique discussed in the previous section. We defined a SOAPRespXML form with the static SOAP envelope and body XML and a single field to contain the variable response XML itself (see Figure 9-12). To build the SOAP response, we create a document using this form and set the field to the response XML. Figure 9-12. XML form for SOAP response. -
The PutResponse method obtains the formatted SOAP response via an OpenDocument URL specifying the UNID of the document built in step five. (Note that since we built this document with the desired form name, we can use the default view identifier of for the view. There is no need to have a different form applied in this case.) -
Finally, we copy the response text to the Web agent's output stream, which is the HTTP response stream. We explicitly set the content type as "text/xml." We have found that the response XML text (written in the SOAPRespXML form) did not need to contain the XML header ( <?xml ?> ) declaration, as this is generated by Domino. When we did include it as part of the response text, our SOAP client reported a SOAP parsing error that there was a processing instruction in the response XML that was not permitted by the SOAP standard. There it is ”a fairly generic Domino Web agent for processing Web service requests. (Look Ma, no WebSphere!) In order to invoke the Web service, you create a URL to invoke the Domino Web agent. Using the WSHandler Web agent example, the URL would look like: http://hostname/database.nsf/WSHandler?OpenAgent If you are more interested in a LotusScript version of a Web service agent, the Designer help documentation shows a detailed example of one. It takes a more direct approach to parsing the request and formatting the response, but the previously described techniques can be coded just as well in LotusScript. As mentioned here, there are other options for parsing the SOAP request and for building the SOAP response XML. We have investigated using some of the SOAP processing libraries (in Java), such as those from Apache ( org.apache.soap ) and WAS itself. Unfortunately, this proved to be problematic . The primary source of problems was the somewhat out-of-date level of the Domino R6 XML libraries. The level of SAX and DOM parser code provided by Domino (SAX 1.0 and DOM Level 1) is a major version behind that used by the SOAP libraries (SAX 2.0 and DOM Level 2). At least with the Java libraries, it is possible for Domino's XML parsing libraries to be upgraded by simply replacing the XML- related .jar files with newer versions, but this is not supported by IBM and could affect existing Domino code. At this stage of product evolution, we must make do with the somewhat outdated XML support in Domino. Even so, it is still possible to write a SOAP parser using the SAX version available with Domino R6. (See the last section for the SAX handler code we wrote to parse the SOAP request for the Web agent.) Invoking a Web Service Within a Domino Application Next we look at how to invoke a Web service from within a Domino application. There are several options available, each with its ensuing set of advantages and disadvantages. One approach is to incorporate an external SOAP package into Domino and utilize it within application code. For example, there are the Java-based SOAP implementations from the Apache project, Apache SOAP (which originated in IBM), and Apache Axis. There is also the IBM/Lotus provided SOAPConnect toolkit, which makes it possible to invoke Web services from Lotus Script code. The biggest disadvantage to this approach is the potential for incompatibility between these packages and Domino itself. As we saw in the previous section, the incompatibilities are most likely to arise within the XML libraries. In fact, the SOAPConnect toolkit requires that the standard Domino XML libraries be replaced with those in the toolkit and yet warns that this should not be done in a production environment. Another approach is to provide the basic SOAP function using standard Domino features. These features, along with the XML processing techniques that we have discussed previously, are sufficient to create and parse the SOAP messages required to invoke a Web service. For Web services that do not involve complex requests or responses, this approach is perhaps the safest from a product support viewpoint and likely the most efficient. The disadvantage with this approach is the amount of code required. We will look at an example of invoking a Web service using this approach, and you can be the judge. Basically, the code required to invoke a Web service is similar to that for invoking a servlet or EJB, except that we need to provide the SOAP formatted request message as the HTTP request content and, of course, process the SOAP response message. The main steps are as follows : -
Build the SOAP request message as XML, including the service-specific request data. -
Make the HTTP request to the Web service host with the SOAP request XML as the HTTP request content. -
Parse the HTTP response content containing the SOAP response message. Here is an agent, WSRequest, which implements these steps in Java: import lotus.domino.*; import java.io.*; import java.net.*; public class JavaAgent extends AgentBase { public void NotesMain() { try { Session session = getSession(); AgentContext agentContext = session.getAgentContext(); // Get selected document from which to get request argument DocumentCollection aDocs = agentContext.getUnprocessedDocuments(); (1) Document aDoc = aDocs.getFirstDocument(); if (aDoc == null) { System.out.println("WSRequest error: No selected document!"); return; } // Build SOAP request - set parameter in a SOAPReqXML document (2) Document aReqDoc = agentContext.getCurrentDatabase().createDocument(); aReqDoc.replaceItemValue("Form", "SOAPReqXML"); aReqDoc.replaceItemValue("WSbooktitle", aDoc.getFirstItem("booktitle")); aReqDoc.save(); // Get SOAPReqXML document with form applied. (3) URL aURL = new URL("http", "localhost", 80, "/booklist.nsf/0/" + aReqDoc.getUniversalID()); URLConnection aCnctn = aURL.openConnection(); aCnctn.connect(); // Issue the SOAP request StringWriter aSOAPResp = new StringWriter(); (4) MakeSOAPRequest("http://localhost/booklist.nsf/WSHandler?OpenAgent", aCnctn.getInputStream(), aSOAPResp); // Get a SAX parser to parse the response org.xml.sax.Parser aParser = org.xml.sax.helpers.ParserFactory.makeParser("com.ibm.xml.parsers.SAXParser"); // Set the SAX document handler as our own SOAPHandler. (5) SOAPHandler aHandler = new SOAPHandler(); aParser.setDocumentHandler(aHandler); aParser.parse(new org.xml.sax.InputSource(new StringReader(aSOAPResp.toString()))); aHandler.Log(); // Do something with the response value } catch(Exception e) { System.out.println("WSRequest error: " + e); } } Let's examine this agent code in detail ”using the (n) annotations in the listing above: -
Our agent is set up to work on selected Domino documents or from a simple form action or button. We get the document here. We'll use this document to get the Web service request parameter(s). -
We create a Domino document and set it to use a form we have created to contain the SOAP request XML, SOAPReqXML . We copy the request parameter field from the -
selected document to this SOAPReqXML document. -
The SOAP request XML is obtained by requesting the combined document and form we created in step two. -
The SOAP request is sent to the Web service host. We'll examine the MakeSOAPRequest method that follows. -
The SOAP response returned in the StringWriter object is parsed using a SAX parser and our SAXHandler class as described in the previous section. The response value can then be gotten from the SAXHandler and used in some way. Here is the code to perform a SOAP request and obtain the response via HTTP: // Makes SOAP request given request XML as InputStream // theTarget - Web service target URL // theIS - SOAP request content // theOutput - On exit, contains SOAP response content // private void MakeSOAPRequest(String theTarget, InputStream theIS, StringWriter theOutput) { try { // Set up the URL object for the HTTP request and get the // HttpURLConnection object which will control the request. // (1) URL aUrl = new URL(theTarget); HttpURLConnection aUC = (HttpURLConnection)aUrl.openConnection(); // Here we set up a HTTP POST request as required by SOAP. aUC.setRequestMethod("POST"); aUC.setDoOutput(true); // Set the content type as XML, and as a SOAPAction (2) aUC.setRequestProperty("Content-type", "text/xml"); aUC.setRequestProperty("SOAPAction", ""); PrintWriter aWtr = new PrintWriter(aUC.getOutputStream()); BufferedReader aRdr = new BufferedReader((3) new InputStreamReader(theIS)); String aLine = aRdr.readLine(); while (aLine != null) { aWtr.println(aLine); aLine = aRdr.readLine(); } aWtr.close(); // Now make the connection with the request header we set up, // get the response code/message, then disconnect. // (4) aUC.connect(); int aRspCode = aUC.getResponseCode(); String aRspMsg = aUC.getResponseMessage(); // Copy response content (SOAP response) to output object aWtr = new PrintWriter(theOutput); aRdr = new BufferedReader((5) new InputStreamReader(aUC.getInputStream())); aLine = aRdr.readLine(); while (aLine != null) { aWtr.println(aLine); aLine = aRdr.readLine(); } aWtr.close(); aUC.disconnect(); } catch (Exception e) { System.out.println("SOAPRequest error: " + e); } } The details: -
We first set up a HttpURLConnection object to use for the SOAP request. Note that openConnection() does not actually connect to the server, it just returns the URLConnection object associated with the URL object. This allows the request header to be set up prior to the request send. -
Here we set the necessary HTTP request header items for SOAP. -
Next, the SOAP request that XML passed to this method is copied to the HTTP request content. -
Now the network connection is made and the request sent. On return from this method, the response content is available. -
Here we copy the response content, the SOAP response XML, as text to the output object. As you can see, the required code is not too complex (thanks to the Java libraries used). Well almost. The SAXHandler we used to parse the SOAP XML here and in the previous section is admittedly oversimplified. It will handle request and response formats containing the basic set of SOAP parameter encoding types, but it needs to be enhanced to handle more complex data encodings such as arrays and structures. Here is where a full-function SOAP package would be worth its price. Additional Code Used for Domino Web Services We include the code here that we used in the previous sections to parse XML. The first listing is code to perform SAX parsing of XML text. The code provides an implementation of the org.xml.sax.HandlerBase interface. import org.xml.sax.*; public class SAXHandler extends HandlerBase { // Notes Document object used as target for parsed data. lotus.domino.Document itsNotesDoc; // Holds root element name, also set as form for itsNotesDoc. String itsRootName; // Holds current element, value being parsed. String itsCurElt; StringBuffer itsValue = new StringBuffer(); public void setNotesDoc(lotus.domino.Document theDoc) { itsNotesDoc = theDoc; } public void startDocument() { System.out.println("Start SAX API parse of document."); } public void endDocument() { // Set root element name as Notes document Form, save it. if (itsNotesDoc != null && itsRootName != null) { try { itsNotesDoc.appendItemValue("Form", itsRootName); itsNotesDoc.save(); } catch(lotus.domino.NotesException e) { System.out.println("Parse error: " + e); } } System.out.println("End SAX parse."); } public void startElement(String theName, AttributeList theAttrs) throws SAXException { // If root element name not set, assume this is it. if (itsRootName == null) itsRootName = theName; itsCurElt = theName; } public void endElement(String theName) throws SAXException { // If current element has a text value, add it to the Notes // document as an Item. if (itsNotesDoc != null && itsCurElt != null && itsValue.length() > 0) { try { itsNotesDoc.appendItemValue(itsCurElt,itsValue.toString()); } catch(lotus.domino.NotesException e) { System.out.println("Parse error: " + e); } } itsCurElt = null; itsValue.setLength(0); } public void processingInstruction(String theTarget, String theDescr) throws SAXException {} public void characters(char theChars[], int theStart, int theLength) throws SAXException { if (itsCurElt != null) itsValue.append(theChars, theStart, theLength); } public String getRootName() { return itsRootName; } } The next code listing implements a SAX parser to specifically parse SOAP requests. import org.xml.sax.*; public class SOAPHandler extends HandlerBase { // SOAP Request attributes public String itsEnv; public String itsService; public String itsMethod; public String itsEncodingStyle; // State of parse processing int itsState; // Parse SOAP Envelope static final int kStateParseEnvelope = 0; // Indicates next element is SOAP Body static final int kStateParseBody = 1; // Look for request parameters // (we take any text element as a parameter). static final int kStateParseParms = 2; static final int kStateEnd = 999; // Inner class to capture parameters public class SOAPParm { public String itsName; public String itsType; public String itsValue; } // Holds parsed parameters as SOAPParm objects public java.util.Vector itsParameters; // Holds current element, type (if set), value being parsed. String itsCurElt; String itsCurType; StringBuffer itsCurValue = new StringBuffer(); public void startDocument() { System.out.println("Start parse of SOAP request."); itsState = kStateParseEnvelope; itsParameters = new java.util.Vector(); } public void endDocument() { System.out.println("End SOAP parse."); } public void startElement(String theName, AttributeList theAttrs) throws SAXException { itsCurElt = theName; // If we're expecting the Body element, assume this is it... switch (itsState) { case kStateParseEnvelope: // If SOAP Envelope, look for SOAP-ENV attribute if (theName.equals("SOAP-ENV:Envelope")) itsEnv = theAttrs.getValue("xmlns:SOAP-ENV"); else if (theName.equals("SOAP-ENV:Body")) itsState = kStateParseBody; break; case kStateParseBody: itsService = theAttrs.getValue("xmlns:ns1"); if (itsService == null) itsService = theAttrs.getValue("xmlns"); itsMethod = theName; itsEncodingStyle = theAttrs.getValue("SOAP-ENV:encodingStyle"); itsState = kStateParseParms; break; case kStateParseParms: // Remember type if specified itsCurType = theAttrs.getValue("xsi:type"); break; default: } } public void endElement(String theName) throws SAXException { if (itsState == kStateParseParms && itsCurValue.length() > 0) { // Create new SOAPParm object to hold this parameter element SOAPParm aParm = new SOAPParm(); aParm.itsName = itsCurElt; aParm.itsType = itsCurType; aParm.itsValue = itsCurValue.toString(); itsParameters.addElement(aParm); } itsCurElt = null; itsCurValue.setLength(0); } public void characters(char theChars[], int theStart, int theLength) throws SAXException { if (itsCurElt != null) itsCurValue.append(theChars, theStart, theLength); } // Logs SOAPHandler contents to System.out public void Log() { System.out.println("SOAP-ENV: " + itsEnv); System.out.println("Service: " + itsService); System.out.println("Method: " + itsMethod); System.out.println("Encoding: " + itsEncodingStyle); System.out.println("Parameters: " + itsParameters.size()); for (int i = 0; i < itsParameters.size(); i++) { SOAPHandler.SOAPParm aParm = (SOAPHandler.SOAPParm)itsParameters.elementAt(i); System.out.println(" " + aParm.itsName + " = " + aParm.itsValue + " (" + aParm.itsType + ")"); } } } |