wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

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.

Posted by on 05 September 2007 | Comments (1) | categories: Show-N-Tell Thursday

Comments

  1. posted by Dan Sickles on Thursday 06 September 2007 AD:
    "Programs that write programs are the happiest programs in the world." Nice work Stephan.