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.