Sending mail from an XML/SOAP call (R6)
So a recent request about sending mail from other applications prompted me to write a quick and dirty LotusScript agent that could send an e-mail from an XML request. This was enough to get the developers going but it also opened up the dialog about standardizing XML calls around the organization. The method that I employed in my example was a bastardized version of XML-RPC with a little bit of SOAP mixed in, but now we are going to make these calls full fledged SOAP calls and we will do it on Domino R6.
I mention this because R7 now has some new classes to make SOAP design alot easier, but our company is not that hot and heavy for R7 and we are unlikely to see any upgrade in the next year. Our admins will need to see R7 get past the Rx.uhOh verion before moving.
Too be honest, my SOAP kung-fu is not strong enough to speak authoritatively on the topic, so these next few weeks are going to be a learning experience for me. If any of you lurkers (I know you are there) see me stating something wrongly, please step in and put me right.
First things first; SOAP is nothing more than XML in a particular format so very little of the previous code actually has to be re-written. The gist of the required changes will be to create a “controller” agent that can determine the method the user wants, and then load the required script libraries. This allows for a single URL to access all of your Domino based web services. So this controlling agent will need to parse through the inbound XML to find the method, and that is the portion that we will look at today.
First, let’s look at a simple SOAP request that will be used to request some directory information based upon the first few letters of an employees last name:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <getAddressBookNames> <starts_with>cros</starts_with> </getAddressBookNames> </soap:Body> </soap:Envelope>
So we have the “Envelope” portion which is what it says it is, the wrapper for the call. The “xmlns” attribute is what’s called a NameSpace reference and it deserves an explanation of its own. Namespaces can be critical but are not always required. They exist so that you can mix and match different schemas and data types and not get naming conflicts.
For example, in Java we have Domino “Document” classes, and we have DOMParser “Document” classes, and if we did not have Java’s package hierarchy we would never be able to use the two side by side. As it is, we can create a new Domino Document object using its full package name like this:
lotus.domino.Document dominoDoc = db.createDocument;
then create the DOMParser Document like this:
org.w3c.dom.Document domDoc = DocumentBuilder.parse(java.io.inputstream);
XML namespaces allow us to use XML tags with the same names side by side. In our example, anytime a node name is prefixed by “soap”, it will be referring to the SOAP envelope schema that you can find at that URL.
So first we have the Envelope, and then we have the body. It is the XML within the Body that we need and typically the first element node within the body has the method name that we need. But there is a catch. Just because I used the “soap” prefix on my envelope and body does not mean everyone else will. That prefix is arbitrary and could be “foo” for all any SOAP client cares. So when I go and tell my parser to find the first element node in the Body, I need to first extract from the envelope tag what prefix the client is using on the Body tag. For this we use a very simple LS function:
Function getEnvelopePrefix(strInput As String) As String Dim intStart As Integer, intEnd As Integer intStart = Instr(strInput, "< ") + 1 intEnd = Instr(strInput, ":") getEnvelopePrefix = Mid$(strInput, intStart, intEnd-intStart) End Function
Using this function we can determine the Body prefix, which we use in our controller agent to find the first element node in the Body, and therefore find the method name the user is requesting. Here is my code for the controller agent called “ws”:
Sub Initialize On Error Goto errorCatch Dim s As New NotesSession Dim db As NotesDatabase Dim docCtx As NotesDocument Dim domParser As NotesDOMParser Dim nodeDoc As NotesDOMDocumentNode, nodeDocOut As NotesDOMDocumentNode Dim nodeNode As NotesDOMElementNode, nodeChild As NotesDOMNode Dim nodeList As NotesDomNodeList Dim streamOut As NotesStream Dim methodName As String, strInput As String, strOut As String Dim bSuccess As Boolean Dim x As Integer Dim ls As String Dim objRef As Variant Set db = s.CurrentDatabase Set docCtx = s.DocumentContext Set domParser = s.CreateDOMParser Set streamOut = s.CreateStream strInput = docCtx.Request_Content(0) bSuccess = False strOut = "invalid method name specified" Call domParser.parse(strInput) Set nodeDoc = domParser.Document ' grab list of user name nodes Set nodeList = nodeDoc.GetElementsByTagName(getEnvelopePrefix(strInput) + ":Body") If nodeList.NumberOfEntries > 0 Then ' if we have one Set nodeNode = nodeList.GetItem(1) ' grab the actual node If nodeNode.HasChildNodes Then ' check to see if it has values Set nodeChild = nodeNode.FirstChild While Not (nodeChild.IsNull) If nodeChild.NodeType = DOMNODETYPE_ELEMENT_NODE Then methodName = nodeChild.NodeName End If Set nodeChild = nodeChild.NextSibling Wend Else strOut = "Missing method name" Goto sendResponse ' we have a node with no values End If Else strOut = "Missing method name" Goto sendResponse ' we do not have a methodname node End If ls = | Use "| & methodName & |"| ' if we have an error at this point, it will be an invalid method name On Error Goto sendResponse Execute ls Exit Sub sendResponse: On Error Goto errorCatch Set domParser = s.CreateDOMParser Set nodeDoc = domParser.Document Set nodeNode = setDefaultResponse(nodeDoc, strOut, False) Call domParser.setOutput(streamOut) Call domParser.Serialize Print {content-type:text/xml; charset=utf-8} Print {< ?xml version="1.0" encoding="UTF-8"?>} + streamOut.ReadText Exit Sub errorCatch: strOut = “Error in xmlrpc:init in line: ” & Erl() & ” error$: ” & Error$ Goto sendResponse End Sub
The only piece missing here is the “setDefaultResponse” function. We use that function to return errors to the client, but otherwise the controller agent simply loads the script library with the method name specified and the library takes care of the normal response. Here is the response function:
Function setDefaultResponse(nodeDoc As NotesDOMDocumentNode, strOut As String, bSuccess As Boolean) As NotesDOMElementNode On Error Goto errorCatch Dim nodeFault As NotesDOMElementNode, nodeFaultString As NotesDOMElementNode Dim nodeEnv As NOtesDomElementNode, nodeBody As NotesDOMElementNode Dim nodeText As NotesDomTextNode Set nodeEnv = nodeDoc.CreateElementNode("soap:Envelope") Call nodeEnv.SetAttribute("xmlns:soap", "http://schemas.xmlsoap.org/soap/envelope/") Call nodeDoc.appendChild(nodeEnv) Set nodeBody = nodeDoc.CreateElementNode("soap:Body") Call nodeEnv.appendChild(nodeBody) Set nodeFault = nodeDoc.CreateElementNode("soap:Fault") Call nodeBody.AppendChild(nodeFault) Set nodeFaultString = nodeDoc.CreateElementNode("faultstring") Call nodeFaultString.SetAttribute("xsi:type","xsd:string") Call nodeFault.AppendChild(nodeFaultString) Set nodeText = NodeDoc.CreateTextNode(strOut) Call nodeFaultString.AppendChild(nodeText) Set setDefaultResponse = nodeEnv Exit Function errorCatch: Messagebox "Error in xmlrpc:setDefaultResponse on line: " & Erl & " - message:" & Error$() Exit Function End Function
The thing to note here is the use of the Fault node when reporting errors in SOAP. The SOAP specification allows for four elements in the fault node; “faultcode”, “faultstring”,”faultactor”, and “detail”. These are pretty self-explanatory except for the “faultactor” node which is basically a way to tell the client if it was a server-side error, or an error with the call from the client.
Next post will be changing the old agent to a script library and formatting its output as a SOAP response. Other topics I’ll hit another day will be WSDL files, testing, and calls using more complex data types.
October 26th, 2006 at 9:42 pm
This is exactly what I was looking for, thanks for the great information.