wissel.net

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

Reuse web agents that PRINT to the browser in XPages


When upgrading classic Domino applications to XPages one particular problem arises constantly: " what to do with the PRINT statements in existing agents that write back directly to the browser?" Currently there is no automatic way to capture this output.
However with a little refactoring of the agent the output can be recycled. You can use a computed field for the result showing it on a page that maintains the all over layout of your new application or use the XAgent approach to replace the whole screen (I'm not discussing the merits of that here). These are the steps:
  1. Make sure your agent is set to "Run as web user" in the agent properties
  2. Add the AgentSupport LotusScript library to your agent
  3. Initialize the ResultHandler class: Dim result as ResultHandler
    SET result = new ResultHandler
    that will take in the print statements
  4. Use Search & Replace in your agent and replace Print  with result.prn 
  5. Add at the end call result.save()
  6. In your XPage or CustomControl add the AgentSupportX SSJS library
  7. Get the result with a call to agentResult("[name-of-your-agent]"). You can process it further or display in in a computed field etc.
The sample code is for illustration only. You want to add proper error handling to it. My test XPage looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:this.resources>
        <xp:script src="/AgentSupportX.jss" clientSide="false"></xp:script>
    </xp:this.resources>
    This comes from an agent:
    <xp:text escape="false" id="computedField1">
        <xp:this.value> <![CDATA[#{javascript:agentResult("SampleAgent");}> </xp:this.value>
    </xp:text>
</xp:view>
The sample agent like this:
Option Public
Option Declare

Use "AgentSupport"

Dim result As ResultHandler

Sub Initialize
    Set result = New ResultHandler
   
    result. prt "Some message"
    result. prt "<h1> a header </h1>"
   
    Call result. save ( )
   
End Sub
Of course the interesting part are the two script libraries.The LotusScript library looks like this:
Update: (thx Tim): Altered the code to work when the agent doesn't print anything
%REM
    Library AgentSupport
    Created Mar 16, 2012 by Stephan H Wissel/Singapore/IBM
    Description: Routines to improve agent calling from XPages
%END REM

Option Public
Option Declare

%REM
    Class ResultHandler
    Description: Capture all the print statements and feed them back
%END REM

Public Class ResultHandler
    Private out As NotesStream 'This is where the result goes
    Private m_fieldName As String 'The field name for the result
    Private m_noPrintMessage as String 'The message if the agent didn't print anything
    Private m_printResult as Boolean 'If the agent is also used classic we need the print output
    Private s As NotesSession
    Private doc As NotesDocument 'The document handed over by XPages
   
    %REM
        Sub new
        Description: Initialize the variables
    %END REM

    Public Sub New
        Set me. s = New NotesSession
        Set me. doc = s. Documentcontext
        me. m_fieldName = "AgentPrintOutputResult"
        me. m_noPrintMessage = "[NoPrintOutput]"
        me. m_printResult = False
    End Sub
   
    %REM
        Sub prt
        Description: Replacement for the print command in web agents
    %END REM

    Public Sub prt (msg As String )
        If out Is Nothing Then
            Set out = s. Createstream ( )
        End if
        Call out. Writetext (msg, EOL_PLATFORM )
        If me. m_printResult Then
            Print msg
        End If
    End Sub
   
    %REM
        Property Set fieldName
        Description: ability to overwrite the field name
    %END REM

    Public Property Set fieldName As String
        me. m_fieldName = fieldName
    End Property

    %REM
        Property Set noPrintMessage
        Description: ability to overwrite the message if nothing was printed
    %END REM

    Public Property Set noPrintMessage As String
        me. m_noPrintMessage = noPrintMessage
    End Property
   
    %REM
        Property Set printResult
        Description: If the agent is used in classic, we need to print
    %END REM

    Public Property Set printResult As Boolean
        me. printResult = printResult
    End Property
   
    %REM
        Sub save
        Description: Saves the output back into the document
        One could consider naming it "Delete" so it runs automatically on object deletion
    %END REM

    Public Sub save
        Dim mimeFlag As Boolean
        Dim mimeEntry As NotesMIMEEntity
       
        'We need to make sure we don't have the item in the document
        If me. doc. Hasitem ( me. m_fieldName ) Then
            Call me. doc. Removeitem ( me. m_fieldName )
        End If
       
        'Set mime conversion to false - we want native
        mimeFlag = me. s. Convertmime
        me. s. Convertmime = False

        'There might not be a print output
        If me. out Is Nothing then
            Call me. agentDidNotPrintAnything ( )
        End If

       
        'Now write out
        me. out. Position = 0 'Make sure we write everything
        Set mimeEntry = doc. Createmimeentity ( me. m_fieldName )
        Call mimeEntry. Setcontentfromtext ( me. out, "text/plain;charset=UTF-8" ,ENC_NONE )
        Call me. doc. Closemimeentities ( true, me. m_fieldName )
       
        'Close stream
        Call me. out. Close ( )
        Set me. out = Nothing 'If we call it again in prt it gets reinitialized
       
        'Restore mime conversion to what it was
        me. s. Convertmime = mimeFlag
    End Sub

    Private Sub agentDidNotPrintAnything
        Set out = s. Createstream ( )
        Call out. Writetext ( me. m_noPrintMessage, EOL_PLATFORM )
    End Sub
   
End Class
The SSJS library looks like this:
function agentResult (agentName ) {
    var doc = database. createDocument ( ) ;
    var agent = database. getAgent (agentName ) ;

    // Here is the run
    try {
        agent. runWithDocumentContext (doc ) ;
    } catch (e ) {
        return e. message ;
    }  
    var out = session. createStream ( ) ;
    var mimeEntry = doc. getMIMEEntity ( "AgentPrintOutputResult" ) ;
    mimeEntry. getContentAsText (out ) ;
    out. setPosition ( 0 ) ;
    var result = out. readText ( ) ;

    //Cleanup
    mimeEntry. recycle ( ) ;
    doc. recycle ( ) ;
    out. recycle ( ) ;
    agent. recycle ( ) ;

    return result ;
}
I'm using MIME as intermediate storage format as pioneered by Tim Tripcony, so I don't need to worry about content length. You might need to play with the encoding/decoding mechanism. I haven't checked the edge cases yet.
Update: To successfully run such an agent it needs to have the property set "Run as web user", otherwise you get an error. Also you need to design your own handover of the document if you want to manipulate that.
As usual: YMMV

Posted by on 16 March 2012 | Comments (4) | categories: XPages

Comments

  1. posted by Niklas Heidloff on Friday 16 March 2012 AD:
    Looks great. Could you pls put on OpenNTF?
  2. posted by Tim Tripcony on Friday 16 March 2012 AD:
    Cool technique... glad to see you found a use for the MIME storage approach.

    Just a suggestion: make the ResultHandler's out a property and move the instantiation to a Property Get... unless I'm misreading that class, save() will throw an "Object variable not set" error if prt() never gets called.
  3. posted by Stephan H. Wissel on Monday 02 April 2012 AD:
    @Tim: added a "Agent didn't print anything message"

    @Niklas: added to XSnippets
  4. posted by Erik van der Arend on Wednesday 04 July 2012 AD:
    Hi Stephan,

    Thanks for sharing...
    a little thing in sub prt:

    printResult should be m_printResult

    Greetings,
    Erik van der Arend