wissel.net

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

XAgents - Web Agents XPages style


In Domino Agents are the Swiss Army Knife of development. They can be called from the client, triggered on schedule, automatically run on an event, be associated with a web action (Open, Close) or directly called from an URL using ?OpenAgent. The ?OpenAgent use of agents is subject of this blog entry. A agent triggered from an URL can use Print statements to output results back to the browser. When the first line of your print statements is "Content-Type:..." you also can serve XML or JSON or whatever exotic format your agent logic was able to conceive. Agents come with the usual drawback: the runtime environment is intitialized new for every agent run, so you need to update a document if you want to keep track (Replication conflicts anyone?)
XPages don't have a notion of agents (you can call them in the backend, but you won't get anything back from the print statements), so it seems a step back. On closer inspection however one can see, that XPages offer a greater flexibility for the same pattern of use. Every element on an XPages has a "render" attribute which determines if the element should be rendered. "Every" includes the page itself. So when you set the page's render property to false, well the XPage engine renders nothing. So you get an empty canvas you can paint on. XPages still provides you with the page event and access to scoped variables, so you can code better performing "agents" since you can keep lookups in memory. To get the equivalent to the LotusScript print you need the ResponseWriter from the externalContext. To set the content type you use the response.setContentType also to be found in the external Context. A sample snippet you could put in the afterRenderResponse event of your page would look like this (error handling omitted):
// The external context gives access to the servlet environment
var exCon = facesContext.getExternalContext();

// The writer is the closest you get to a PRINT statement
// If you need to output binary data, use the stream instead
var writer = facesContext.getResponseWriter();

// The servlet's response, check the J2EE documentation what you can do
var response = exCon.getResponse();

// In this example we want to deliver xml and make sure it doesn't get cached
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");

// Here all your output will be written
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<christmas date\"24Dec\" />");

// We tell the writer we are through
writer.endDocument();
facesContext.responseComplete();
writer.close();
// Update:On 8.5.2 or later writer.close() seems not be necessary/be rather harmful. Read the revisited entry
More information about the FacesContext can be found in the Domino Developer Wiki.

Bonus Tip: Since you do not render anything from the XPage you just used, why not take that empty page and write the documentation for that XAgent?
Update: Other posts on rendering your own output in XPages: Have fun writing XAgents!

Posted by on 19 December 2008 | Comments (19) | categories: Show-N-Tell Thursday XPages

Comments

  1. posted by Fredrik Stöckel on Wednesday 07 January 2009 AD:
    Very useful and powerful! I really like the native access to the servlet backend. Direct binary outupt is awesome.

    Do you know if it's possible to set or manage desired charset in some way? I have tried

    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/xml; charset=UTF-8");

    request.setCharacterEncoding("UTF-8");

    ...but it doesn't seem to work when "echoing" variables from the querystring of a request (request.getParameter(<paramName>))


  2. posted by Sigurbjorn on Wednesday 28 January 2009 AD:
    While trying to use facesContext.getResponseStream() it returns null, but if we do
    facesContext.getExternalContext().getResponse().getOutputStream()
    it throws the Exception 'Can't get an OutputStream while a Writer is already in use'.

    Any suggestions?
  3. posted by simon mottram on Friday 06 March 2009 AD:
    Interesting but do I really have to write serious code in JavaScript?

    Could you use Java instead? Enough languages in Domino already and I'm very unwilling to get into writing complex code in a language that has no type checking, no variable declaration, no debugging etc.
  4. posted by Simon Mottram on Wednesday 06 May 2009 AD:
    I have to say I don't believe this works for binary output. I get exactly the same result as Sigurbjorn.

    We cannot get a binary outputstream it seems.

    Can you confirm you have sent binary data? XML dont count =)

    S
  5. posted by Stephan H. Wissel on Friday 08 May 2009 AD:
    @Simon: You can do variable declarations on server side javascript to include the type: var myvar:String = "Hello World";
    You can write a java class to do your bidding (just not in the default namespace) and then do:
    var x = new com.acme.outputstuff.RenderX();
    return x.toString();
    If you want to return binary data the writer is the wrong object. You need the stream. There the same caution like servlet programming applies: once you (or something else) opens a writer to the output you can't get a stream object anymore. So when you put code into the "afterRenderResponse" event XPages had initialized the writer, so no more access to the stream is possible. To get to the stream you need to act "earlier" and use the beforeRenderResult.
    Emoticon shocked.gif stw
  6. posted by Simon on Friday 08 May 2009 AD:
    Hi Stephan

    Thanks very much for the reply. Yeah I understand all that, the bit I didn't get was that the Writer had already been setup by the afterRenderResponse event.

    I had actually had that idea myself but the trouble is, I can get no output from the beforeRenderResponse event using either Writer or Stream, so I had given up. Am I missing something really obvious here?

    This would really make my life fun if I could get this to work =)

    Cheers

    Simon
  7. posted by Simon on Sunday 10 May 2009 AD:
    Hi Stephan

    Sorry if this constitutes spam =) Bet you are sorry I found this page! I did get time to do a few tests this weekend though...

    First, having discovered print()!, I tried the following fragment in all events, one at a time.

    print("start");
    var rs = facesContext.getResponseStream();
    if(rs == null) {
    print("fc.getResponseStream() == null");
    } else {
    print(rs.getClass().getName());
    }
    print("end")

    This prints fce.getResponseStream() == null, in all events. I don't know if this is broken or 'working as intended'


    I then tried the following.

    print("start");
    var ex = facesContext.getExternalContext();
    var response = ex.getResponse();
    var rs = response.getOutputStream();
    if(rs == null) {
    print("response.getOutputStream() == null");
    } else {
    print(rs.getClass().getName());
    }
    print("end")

    As you predicted, the getOutputStream() fails on the afterRenderResponse() event. On all other events I see the following:

    10/05/2009 19:02:25 HTTP JVM: start
    10/05/2009 19:02:25 HTTP JVM: com.ibm.xsp.webapp.XspHttpServletResponse$InternalOutputStream
    10/05/2009 19:02:25 HTTP JVM: end
    10/05/2009 19:02:25 HTTP JVM: SEVERE: CLFAD####E: Exception thrown
    10/05/2009 19:02:25 HTTP JVM: SEVERE: CLFAD####E: Exception occurred servicing request for: /test/TestCharts.nsf/test.xsp - HTTP Code: 500
    10/05/2009 19:02:25 HTTP Web Server: Command Not Handled Exception [/test/TestCharts.nsf/test.xsp]

    Somehow, just instantiating this object causes the XSP server to explode. I have tried flushing/closing/printing rubbish.

    Feels like I'm either just one step away, or doomed forever!

    Thanks for your help, appreciated.

    S
  8. posted by Simon on Monday 11 May 2009 AD:
    More spam, but I beat you to it =)

    If you put code into the beforeRenderResponse event, you need to tell JSF that you have finished rendering and not to proceed to the afterRenderReponse()

    cue:
    facesContext.responseComplete();

    Really close, thanks for the pointer!

    S
  9. posted by Andrew Tjecklowsky on Friday 15 May 2009 AD:
    Hi Stephan!
    I've struggled with XPages and the request stream for several weeks now and found this entry that shows me that you might be the right guy to get an answer from Emoticon smile.gif

    I'm trying to create an XPage which will be called from an HTML form POST. The HTML form will contain form fields and one or multiple fileupload controls. The XPage is created with NO UI Components (thus no FileUpload control).

    I can create logic in the afterRenderResponse event that outputs the field names and values posted from the HTML form, but I can't seem to find a way to get the inputstreams (or File object or what-ever) of the uploaded files.

    If I output the parameters comming in from the HTML form I can output the names and values of all fields posted, and for file uploads I get the name of the file upload control and the value is something like 'com.ibm.xsp.http.UploadedFile@6cc16cc1'. I guess that somehow it should be possible to get to the InputStream or File object of each uploaded file without using Apache Commons FileUpload or the like.

    Do you know of a way to get a reference to the uploaded file(s)?
  10. posted by Stephan H. Wissel on Tuesday 19 May 2009 AD:
    @Andrew: No website, no email -> no advise.
    Emoticon biggrin.gif stw

    P.S.: It's mime multi-parts there are no fields in a file upload, only stream.
  11. posted by Andrew Tjecklowsky on Thursday 21 May 2009 AD:
    Hi Stephan, sorry Emoticon wink.gif, here is my email and some more information about what I've tried to do:

    Put Apache Commons File Upload ({ Link } into WebContent/WEB-INF/lib (and included the file in the Java build path) and then tried the following in both beforeRenderResponse and afterRenderResponse events:

    var exCon = facesContext.getExternalContext();
    var request:HttpServletRequest = exCon.getRequest();
    var parser:com.acme.FormParser = new com.acme.FormParser();
    parser.parseRequest(request);

    com.acme.FormParser class is like:

    package com.acme;

    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;

    import javax.servlet.http.HttpServletRequest;

    import org.apache.commons.fileupload.FileItemIterator;
    import org.apache.commons.fileupload.FileItemStream;
    import org.apache.commons.fileupload.FileUploadException;
    import org.apache.commons.fileupload.servlet.ServletFileUpload;
    import org.apache.commons.fileupload.util.Streams;


    public class FormParser {


    public FormParser() {
    System.out.println("FormParser...");
    }

    public void parseRequest(HttpServletRequest request) {
    System.out.println("parseRequest(request)...");
    try {
    // Check that we have a file upload request
    boolean isMultipart = ServletFileUpload.isMultipartContent(request);
    String contentType = request.getContentType();
    String characterEncoding = request.getCharacterEncoding();
    System.out.println("Multipart: " + isMultipart);
    System.out.println("Content type: " + contentType);
    System.out.println("Character encoding: " + characterEncoding);

    ServletFileUpload upload = new ServletFileUpload();
    FileItemIterator iter = upload.getItemIterator(request);
    while (iter.hasNext()) {
    // We never get to this statement <==================================!!!
    FileItemStream item = iter.next();
    String name = item.getFieldName();
    InputStream stream = item.openStream();
    if (item.isFormField()) {
    System.out.println("Form field " + name + " with value " + Streams.asString(stream) + " detected.");
    } else {
    System.out.println("File field " + name + " with file name " + item.getName() + " detected.");
    }
    }
    } catch (FileUploadException fue) {
    fue.printStackTrace();
    } catch (IOException ioe) {
    ioe.printStackTrace();
    }
    }

    }
  12. posted by Andrew Tjecklowsky on Thursday 21 May 2009 AD:
    Hi Stephan,
    sorry Emoticon wink.gif, here is my email and some more information about what I've tried to do:

    Put Apache Commons File Upload ({ Link } into WebContent/WEB-INF/lib (and included the file in the Java build path) and then tried the following in both beforeRenderResponse and afterRenderResponse events:

    var exCon = facesContext.getExternalContext();
    var request:HttpServletRequest = exCon.getRequest();
    var parser:com.acme.FormParser = new com.acme.FormParser();
    parser.parseRequest(request);

    com.acme.FormParser class is like:

    package com.acme;

    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;

    import javax.servlet.http.HttpServletRequest;

    import org.apache.commons.fileupload.FileItemIterator;
    import org.apache.commons.fileupload.FileItemStream;
    import org.apache.commons.fileupload.FileUploadException;
    import org.apache.commons.fileupload.servlet.ServletFileUpload;
    import org.apache.commons.fileupload.util.Streams;


    public class FormParser {


    public FormParser() {
    System.out.println("FormParser...");
    }

    public void parseRequest(HttpServletRequest request) {
    System.out.println("parseRequest(request)...");
    try {
    // Check that we have a file upload request
    boolean isMultipart = ServletFileUpload.isMultipartContent(request);
    String contentType = request.getContentType();
    String characterEncoding = request.getCharacterEncoding();
    System.out.println("Multipart: " + isMultipart);
    System.out.println("Content type: " + contentType);
    System.out.println("Character encoding: " + characterEncoding);

    ServletFileUpload upload = new ServletFileUpload();
    FileItemIterator iter = upload.getItemIterator(request);
    while (iter.hasNext()) {
    System.out.println("We never get to this statement????");
    FileItemStream item = iter.next();
    String name = item.getFieldName();
    InputStream stream = item.openStream();
    if (item.isFormField()) {
    System.out.println("Form field " + name + " with value " + Streams.asString(stream) + " detected.");
    } else {
    System.out.println("File field " + name + " with file name " + item.getName() + " detected.");
    }
    }
    } catch (FileUploadException fue) {
    fue.printStackTrace();
    } catch (IOException ioe) {
    ioe.printStackTrace();
    }
    }

    }

  13. posted by Andrew Tjecklowsky on Tuesday 02 June 2009 AD:
    Hi Stephan,
    I've just realized that I have submitted the above question twice. Sorry about that...
    I'm still looking around for a solution or some more guidelines about how to overcome my problem regarding upload files. Hoping that you could give me some more hints as to where to look?
  14. posted by Edwin on Sunday 07 June 2009 AD:
    Thanks all for your help.
  15. posted by Joe Randel on Saturday 31 October 2009 AD:
    Thanks this post is very helpful.
    I'm new to xPage development and would like to use this technique to export the contents of a joined notes view to JSON. I have the view joined following Nathans tutorial, just don't know how to get it to JSON. Any ideas?
  16. posted by Simon O'Doherty on Sunday 28 February 2010 AD:
    Very cool. Will suggest this as a workaround where customer needs an intensive web agent.


  17. posted by Meenakshi on Tuesday 02 March 2010 AD:
    Hi,

    I have created one XPage and have a similar form.But whatever data I am saving is not getting updated in the view after i pressed the save button in XPages.

    Documents are not created.

    Please share your thoughts.

  18. posted by Todd Harris on Wednesday 03 March 2010 AD:
    Stephan,
    Is it possible to grab the response html that has been generated by JSF and sent to the browser? I understand from your comment @5 why my attempt to use facesContext.getResponseStream won't work from the afterRenderResponse event, but is there another way?
    Thank you.
  19. posted by Stephan H. Wissel on Wednesday 03 March 2010 AD:
    Few clarifications:
    1. The method described here is for rendering your own response, instead of the XPages stuff.
    2. The XPage needs to be set to "render=false".
    3. For the Writer afterRenderResponse is fine.
    4. For the Stream use beforeRenderResponse
    5. You can only have one: the writer or the stream.
    6. You can't (at least to my current knowledge) intercept the render output from XPages.
    7. "Agents XPages style" are for OUTPUT not for upload. For uploads use the XPages file upload control or (soon) the Quickr controls.
    Emoticon biggrin.gif stw
  20. posted by Meenakshi on Friday 27 August 2010 AD:
    Hi Stephan,

    I Have a java agent for exporting notes doc to pdf,now I want to export the xpage into PDF.Kindly advice me how to use this existing java agent in xpages

    Thank you so much


  21. posted by Stephan H. Wissel on Thursday 02 December 2010 AD:
    @Meenakshi,
    Presumably your agent is written well and just calls a custom class that does all the work: Just add that to the XPages classpath and use Java from JavaScript. Check the Domino designer wiki for info how to do that.

    @Wright
    dojo is client side JavaScript. You can't use that in SSJS. AfterRenderResponse is SSJS. Checkout the Extension Library, they have JSON support.
  22. posted by Wright Furman on Thursday 02 December 2010 AD:
    Stephan,

    This got me started and was just what I was looking for, but I can't seem to include dojo.js from my server file system when render is set to false for the XPage and use it in the AfterRenderResponse event to turn a JavaScript object into JSON using dojo.toJson(). Have you tried that?

    When I do it, it errors out I and I get an empty page.
  23. posted by Don Mottolo on Friday 03 December 2010 AD:
    VERY IMPORTANT: If you are using Domino 8.5.2 you must remove the last line:
    writer.close();

    Otherwise, you'll get "Error 500 HTTP Web Server: Command Not Handled Exception"

  24. posted by Doug on Thursday 09 December 2010 AD:
    I am trying to create XML that contains HTML. I have tried every combination of cdata that I can think of.

    Any thoughts. An xagent seems like a great idea, but I can not get it to work.

    The same type of code using Print statement in a Lotusscript agent works fine.

    Thanks Doug
  25. posted by Philippe on Wednesday 29 December 2010 AD:
    Hi Stephan,

    Great article!

    Your sample did not work on 8.5.2, but the solution of @23 Don Mottolo fixes this. Maybe put this info in your article? So other people won't lose time and get a little frustrated like me, before they notice the 23th comment Emoticon smile.gif

    Also many thanks to Don Mottolo