A CRUD webservice - Part 2
In Part 1 of this Show-N-Tell Thursday I showed how to generate a WSDL definition out of a Notes form. While this is nice, it leaves quite a bit of work to actually implement the service. In this installment we take the opposite route. Instead of generating a WSDL definition, LotusScript code will be generated out of a Form definition. Unfortunately DXL doesn't support Web services very well (at least up to R8.0), so I will generate a text file that contains pure LotusScript code. This text file needs to be manually imported into an empty Web service (Copy and paste will do).
I generate a few classes including a custom class which you can overwrite to implement your specific additions to the code. You might want to put that into a different library, so you keep your customization. Nota bene: the generated code is not real production quality. I haven't implemented good logging or error handling ( OpenLog anybody). Once you see the code you get the idea and can complete that easily (feedback would be nice).
A web service requires a LotusScript class with public members, properties and/or functions. Domino Designer will then generate a WSDL file for us. One path to travel would be implementing generic code, that takes fields as name/value pairs. While tempting this has the clear disadvantage, that the web service becomes rather unspecific and we can't take advantage of a strong validation using XML schema. So my approach is to analyze the form and generate 2 XML structures: one for all input fields, used to create and update a document and one for all fields used to read a document. I don't take hide-when formulas into account.
There are a few challenges to overcome. First we need to deal with multi-value fields. In WSDL these are expressed as Arrays. Secondly we need to map the Domino data types to WSDL data types. Luckily half of the work is done since Domino includes "lsxds.lss" It translates WSDL data types into LotusScript. From LotusScript to NotesItems is a path well travelled.
Let's have a look at the XSLT stylesheet (if you don't know how to translate DXL using XSLT use this plug-in -- you are using FireFox are you?)
To instruct the XSLT processor to output plain text rather than XML or HTML we use this statement:
<xsl:output indent= "yes" method= "text" omit-xml-declaration= "yes" media-type= "text/lotuscript" xml:space= "preserve" />
Then inside the root tag the LotusScript Code is written as you would write in Domino Designer.
<xsl:template match= "/" >
<!-- We use the build-in data type mapping for LotusScript agents -->
%INCLUDE "lsxsd.lss"
' Autogenerated code to be embedded into a Domino Web Service
Public Class form <xsl:value-of select= "$formName" />
We have 2 levels of comments here. XSLT Comments looking like this: <!-- ... --> and regular LotusScript comments starting with '.
Whenever we need to get specific we can use <xsl:value-of to pick a value from the DXL form.
The class we will expose into the webservice is, other than its name and the content of the parameters pretty standard and (IMHO) self explaining:
Public Class form <xsl:value-of select= "$formName" />
'/**
'* createData takes in the custom form data and creates a new document.
'* the data given as parameter matches all input fields of the form
'*
'**/
Public Function createData(formData as <xsl:value-of select= "$formName" /> InputData) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set createData = curData.createData(formData)
End Function
Public Function createAndReadData(formData as <xsl:value-of select= "$formName" /> InputData) as ExtendedResult
Set createAndReadData = new ExtendedResult
Set createAndReadData.Data = new <xsl:value-of select= "$formName" /> CustomData
Set createAndReadData.Result = createAndReadData.Data.createData(formData)
End Function
Public Function readData(key as String) as <xsl:value-of select= "$formName" /> CustomData
Dim tmpResult as new <xsl:value-of select= "$formName" /> CustomData
Call tmpResult.readData(key)
Set readData = tmpResult
End Function
Public Function updateData(key as String, formData as <xsl:value-of select= "$formName" /> InputData) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set updateData = curData.updateData(key, formData)
End Function
Public Function updateAndReadData(key as String, formData as <xsl:value-of select= "$formName" /> InputData) as ExtendedResult
Set updateAndReadData = new ExtendedResult
Set updateAndReadData.Data = new <xsl:value-of select= "$formName" /> CustomData
Set updateAndReadData.Result = updateAndReadData.Data.updateData(key,formData)
End Function
Public Function deleteData(key as String) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set deleteData = curData.deleteData(key)
End Function
End Class
Public Class ResultCode
Public Value as Integer 'We use HTTP values
Public DBid as String 'Database and document we try our luck with
Public DocID as String
End Class
Public Class ExtendedResult
Public Result as ResultCode
Public Data as <xsl:value-of select= "$formName" /> CustomData
End Class
The more interesting part is in the generation of code per field. Code needs to be generated for fields in various parts of the code, so we take advantage of XSLT's mode attribute to specify what generation should take place:
<!-- Fields need to be specified per mode/edit/compute and data type -->
<xsl:template match= "d:field" >
<xsl:variable name= "datatype" >
<xsl:call-template name= "getdatatype" >
<xsl:with-param name= "curField" select= "." />
</xsl:call-template>
</xsl:variable>
Public <xsl:value-of select= "@name" /> as <xsl:value-of select= "$datatype" />
</xsl:template>
We actually treat multi-value fields slightly different, so we just add a qualifier to our match condition and we can be more specific what to do. I prefer match clauses to if conditions most of the time.
<!-- We need to treat multivalue fields a little different since they are arrays -->
<xsl:template match= "d:field[@allowmultivalues='true']" >
<xsl:variable name= "datatype" >
<xsl:call-template name= "getdatatype" >
<xsl:with-param name= "curField" select= "." />
</xsl:call-template>
</xsl:variable>
Public <xsl:value-of select= "@name" /> () as <xsl:value-of select= "$datatype" />
</xsl:template>
The variable datatype contains the mapping from a notes datatype into the XSD data type and is implemented as XSLT's version of sub routine. Once you make piece with XSLT's strictly set oriented way of thinking you will appreciate the power of the language. The mapping as of now looks like this:
<!-- Determin the datatype. This maps Notes field types to XML Datatypes. Change as needed -->
<xsl:template name= "getdatatype" >
<xsl:param name= "curField" />
<xsl:choose>
<xsl:when test= "$curField/@type='text'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='keyword'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='password'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='keyword'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='names'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='authors'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='readers'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='formula'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='number'" > XSD_DOUBLE </xsl:when>
<xsl:when test= "$curField/@type='datetime'" > XSD_DATETIME </xsl:when>
<!-- TODO: complete the datatype mapping! -->
<xsl:otherwise> XSD_ANYTYPE </xsl:otherwise>
</xsl:choose>
</xsl:template>
In this case I did choose the <xsl:choose> command since it allows for a compact display and the when conditions are not too complicated. One question to ask: if I only use the type of the field, why would I use the field in the parameter instead of just the type. The answer here is extensibility. You will want to be more specific on datetime and number fields. For a datetime field you could check what parts are visible date and/or time and create XSD_DATE, XSD_TIME instead of XSD_DATETIME. Similar for numbers: based on the display selection you could choose other data types then XSD_DOUBLE.
The sample XSLT stylesheet is available for download.
Next stop: Reverse the game - generate a form from a web service.
I generate a few classes including a custom class which you can overwrite to implement your specific additions to the code. You might want to put that into a different library, so you keep your customization. Nota bene: the generated code is not real production quality. I haven't implemented good logging or error handling ( OpenLog anybody). Once you see the code you get the idea and can complete that easily (feedback would be nice).
A web service requires a LotusScript class with public members, properties and/or functions. Domino Designer will then generate a WSDL file for us. One path to travel would be implementing generic code, that takes fields as name/value pairs. While tempting this has the clear disadvantage, that the web service becomes rather unspecific and we can't take advantage of a strong validation using XML schema. So my approach is to analyze the form and generate 2 XML structures: one for all input fields, used to create and update a document and one for all fields used to read a document. I don't take hide-when formulas into account.
There are a few challenges to overcome. First we need to deal with multi-value fields. In WSDL these are expressed as Arrays. Secondly we need to map the Domino data types to WSDL data types. Luckily half of the work is done since Domino includes "lsxds.lss" It translates WSDL data types into LotusScript. From LotusScript to NotesItems is a path well travelled.
Let's have a look at the XSLT stylesheet (if you don't know how to translate DXL using XSLT use this plug-in -- you are using FireFox are you?)
To instruct the XSLT processor to output plain text rather than XML or HTML we use this statement:
<xsl:output indent= "yes" method= "text" omit-xml-declaration= "yes" media-type= "text/lotuscript" xml:space= "preserve" />
Then inside the root tag the LotusScript Code is written as you would write in Domino Designer.
<xsl:template match= "/" >
<!-- We use the build-in data type mapping for LotusScript agents -->
%INCLUDE "lsxsd.lss"
' Autogenerated code to be embedded into a Domino Web Service
Public Class form <xsl:value-of select= "$formName" />
We have 2 levels of comments here. XSLT Comments looking like this: <!-- ... --> and regular LotusScript comments starting with '.
Whenever we need to get specific we can use <xsl:value-of to pick a value from the DXL form.
The class we will expose into the webservice is, other than its name and the content of the parameters pretty standard and (IMHO) self explaining:
Public Class form <xsl:value-of select= "$formName" />
'/**
'* createData takes in the custom form data and creates a new document.
'* the data given as parameter matches all input fields of the form
'*
'**/
Public Function createData(formData as <xsl:value-of select= "$formName" /> InputData) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set createData = curData.createData(formData)
End Function
Public Function createAndReadData(formData as <xsl:value-of select= "$formName" /> InputData) as ExtendedResult
Set createAndReadData = new ExtendedResult
Set createAndReadData.Data = new <xsl:value-of select= "$formName" /> CustomData
Set createAndReadData.Result = createAndReadData.Data.createData(formData)
End Function
Public Function readData(key as String) as <xsl:value-of select= "$formName" /> CustomData
Dim tmpResult as new <xsl:value-of select= "$formName" /> CustomData
Call tmpResult.readData(key)
Set readData = tmpResult
End Function
Public Function updateData(key as String, formData as <xsl:value-of select= "$formName" /> InputData) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set updateData = curData.updateData(key, formData)
End Function
Public Function updateAndReadData(key as String, formData as <xsl:value-of select= "$formName" /> InputData) as ExtendedResult
Set updateAndReadData = new ExtendedResult
Set updateAndReadData.Data = new <xsl:value-of select= "$formName" /> CustomData
Set updateAndReadData.Result = updateAndReadData.Data.updateData(key,formData)
End Function
Public Function deleteData(key as String) as ResultCode
Dim curData as new <xsl:value-of select= "$formName" /> CustomData
Set deleteData = curData.deleteData(key)
End Function
End Class
Public Class ResultCode
Public Value as Integer 'We use HTTP values
Public DBid as String 'Database and document we try our luck with
Public DocID as String
End Class
Public Class ExtendedResult
Public Result as ResultCode
Public Data as <xsl:value-of select= "$formName" /> CustomData
End Class
The more interesting part is in the generation of code per field. Code needs to be generated for fields in various parts of the code, so we take advantage of XSLT's mode attribute to specify what generation should take place:
<!-- Fields need to be specified per mode/edit/compute and data type -->
<xsl:template match= "d:field" >
<xsl:variable name= "datatype" >
<xsl:call-template name= "getdatatype" >
<xsl:with-param name= "curField" select= "." />
</xsl:call-template>
</xsl:variable>
Public <xsl:value-of select= "@name" /> as <xsl:value-of select= "$datatype" />
</xsl:template>
We actually treat multi-value fields slightly different, so we just add a qualifier to our match condition and we can be more specific what to do. I prefer match clauses to if conditions most of the time.
<!-- We need to treat multivalue fields a little different since they are arrays -->
<xsl:template match= "d:field[@allowmultivalues='true']" >
<xsl:variable name= "datatype" >
<xsl:call-template name= "getdatatype" >
<xsl:with-param name= "curField" select= "." />
</xsl:call-template>
</xsl:variable>
Public <xsl:value-of select= "@name" /> () as <xsl:value-of select= "$datatype" />
</xsl:template>
The variable datatype contains the mapping from a notes datatype into the XSD data type and is implemented as XSLT's version of sub routine. Once you make piece with XSLT's strictly set oriented way of thinking you will appreciate the power of the language. The mapping as of now looks like this:
<!-- Determin the datatype. This maps Notes field types to XML Datatypes. Change as needed -->
<xsl:template name= "getdatatype" >
<xsl:param name= "curField" />
<xsl:choose>
<xsl:when test= "$curField/@type='text'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='keyword'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='password'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='keyword'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='names'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='authors'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='readers'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='formula'" > XSD_STRING </xsl:when>
<xsl:when test= "$curField/@type='number'" > XSD_DOUBLE </xsl:when>
<xsl:when test= "$curField/@type='datetime'" > XSD_DATETIME </xsl:when>
<!-- TODO: complete the datatype mapping! -->
<xsl:otherwise> XSD_ANYTYPE </xsl:otherwise>
</xsl:choose>
</xsl:template>
In this case I did choose the <xsl:choose> command since it allows for a compact display and the when conditions are not too complicated. One question to ask: if I only use the type of the field, why would I use the field in the parameter instead of just the type. The answer here is extensibility. You will want to be more specific on datetime and number fields. For a datetime field you could check what parts are visible date and/or time and create XSD_DATE, XSD_TIME instead of XSD_DATETIME. Similar for numbers: based on the display selection you could choose other data types then XSD_DOUBLE.
The sample XSLT stylesheet is available for download.
Next stop: Reverse the game - generate a form from a web service.
Posted by Stephan H Wissel on 05 September 2007 | Comments (1) | categories: Show-N-Tell Thursday