Generating and Downloading Files Using XPages’ Persistence Service

When developing an XPages application you may run into the need to generate files to be downloaded by the user.

For example:

  • Generating a PDF Report
  • Exporting Data to Excel Spreadsheet
  • Creating a Zip File of several attachments

Achieving these tasks usually raises 2 main problems to solve;

  1. What temporary place can I use to generate the files?
  2. How do I allow the user to download the generated files?

For the temporary place problem, some common solution is to either generate the files to some ‘in memory’ representation or alternatively use the Java Temporary Directory. For the downloading problem, a common solution is to provide a button which calls a SSJS or Java bean method that hijacks the HttpResponse and writes out the appropriate headers and file contents.

These solutions work fine, however built in to the XPages framework is the ‘Persistence Service’ which is a mechanism for temporarily Persisting files that need to be available for download . Although this persistence service was built primarily for the standard operations of XPages, there is nothing stopping us from also enjoying the way it solves these 2 problems.

Why does the Persistence Service exist?

An advantage of Domino Databases is how easy it is to store Files and Images inside ‘Rich Text’ Fields. When XPages needs to display these Embedded Images or allow Downloads of files from within the Document, then the user’s web browser will need to make request for these embedded objects using some sort of URL.

Domino provides URLs which provide direct access to these embedded files and images, however these only provide access to the ‘saved’ version of a document as it exists inside the Notes Database. These URLs are of no help for the ‘No mans land’ period of time in which someone is creating a new document, or when editing an existing document and uploads a new File or Image but has not yet saved the document.

When you open up Domino Document via XPages, the persistence service extracts the Document’s embedded images to a temporary directory in the filesystem, and also uses this directory as a place for newly uploaded files/images to live until they are finally decided to be saved into the document inside the Notes Database. The files in this temporary directory are made available to the user via a special URL.

How does the Persistence Service Work?

The XPages server has a server-wide property which determines the Base Persistence Directory.

The default value for this is <DominoDataDirectory>/xsppers  but you can configure it to be anywhere you like using the xsp property xsp.state.persistence.directory

When a new XPages Application starts up, it is assigned an ‘Application Id’ which is really just a sequential number, so the first application that starts up gets number 1, next number 2 etc.

When a user starts using an Application they are given a ‘session id’ which is a bit more like a Universal ID.

Side Note: I’m not sure how this technique goes with Anonymous access as I have only been programming for authenticated users.

The persistence service uses these 3 pieces of information to determine a user’s persistence directory for that application.

So lets assume, my Base persistence directory is C:\Domino\Data\xsppers, and the application id is 1 and my session id is 9847239OENTHUDO then we can find my persistence directory for that application in the file system at <xsppers>/<applicationid>/<sessionid> or in this example:

C:\Domino\Data\xsppers\1\9847239OENTHUDO\

Now here is the handy part, XPages has a resource provider which maps a special URL /xsp/.ibmmodres/persistence to the user’s persistence directory for that application.

So lets assume we create a folder ‘myfolder’ under my user persistence directory and put a file ‘myfile.pdf’ in that folder::

C:\Domino\Data\xsppers\1\9847239OENTHUDO\myfolder\myfile.pdf

This file would then be available via the url:

https://yourserver/yourapp.nsf/xsp/.ibmmodres/persistence/myfolder/myfile.pdf

If another user tried to use the same url it would not work because they would have a different session id, and would be mapping to a different directory

Cleaning up files

Another handy feature of the persistence service is that it cleans up after itself! So you can stress less about generating a massive zip file knowing that it won’t be left around for too long and end up filling up your server’s hard drive.

After a user session for an application times-out then the user’s persistence directory and all its contents will be deleted.

After the entire application times-out, then the application’s persistence directory  e.g. C:\Domino\Data\xsppers\1 and all it’s contents will be deleted.

So how do I actually use this?

So your main 2 weapons here are :

  • You have a User’s Persistence Directory in which you can generate files (which will also clean up after itself)
  • You have a handy URL available for the user to access the User’s Persistence Directory

To generate files into the User’s Persistence directory we can either just figure out the path of the user’s application directory and write files straight to it, or alternatively we can also ask the applications PersistenceService to do it for us.

Doing it ourselves

To do it ourselves we need to be able to figure out the User’s persistence directory.  I have included a Utility class at the bottom of this post with some handy methods. Let’s have a look at 3 of the methods:

So the main one here is getPersistenceSessionFolder and you can see it is just building up that folder location using the 3 parts mentioned earlier. We get the BasePersistenceFolder (xsppers) by querying the xsp property, and if it has not been set using an xsp property, we fall back to the default for the current platform.
We get the session id using the com.ibm.xsp.util.SessionUtil class

Once you have access to this folder, you can create folders and files directly in here. The convention of the PersistenceService is to place your generated files within a subfolder of the User Persistence Directory e.g. <userpersistencedir/<somefolder>/<somefile> instead of <userpersistencedir>/<somefile>. If you do not follow this convention then your download url will not work as it expects exactly this structure.

Doing it using the PersistenceService

An XPages Application has a PersistenceService which can be used for creating a ‘PersistedContent’ object. This persisted content object can be used to get an OutputStream to write to, and also the corresponding java ‘File’ object for that PersistedContent.

The utility class has a method which shows how to get the persistence service. Note the persistence service is marked as Deprecated but you can still use it.

Also the utility class has a method to help create PersistedContent. You can see all the Persistence service methods follow the convention of using a folder and a file name.

And you can see how this is used in the method persistInputStream

Note: I am using Apache Commons IOUtils for copying a stream but if you cannot use this then you can replace with another stream copying algorithm or perhaps using IBM Commons StreamUtil.copy(inputstream, outputstream). I am using Apache commons because it automatically buffers the streams so that if you are dealing with large files you don’t use too much memory

Providing a Link to download the file

This is the easy part. all you have to do is use the standard link control!

Summary

So I have found this way to be a very useful way to manage generating and downloading files, I hope it has been interesting / useful for you and if you have any questions or have found a mistake in this post please leave a comment and I promise to respond!

Utility Class

As promised here is the utility class which is also available as an XSnippet

 

 

You may also like...

8 Responses

  1. great code. will def. find a way in one of my projects. thanks for sharing!

  2. alayasf says:

    My little example for testing:

    /**
    *
    */
    package net.notesx;

    import java.io.PrintWriter;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.File;

    import javax.faces.context.FacesContext;

    import com.ibm.commons.util.io.json.JsonJavaObject;
    import com.ibm.domino.services.ServiceException;
    import com.ibm.domino.services.rest.RestServiceEngine;
    import com.ibm.xsp.extlib.component.rest.CustomService;
    import com.ibm.xsp.extlib.component.rest.CustomServiceBean;
    import com.ibm.commons.util.StringUtil;
    import com.ibm.xsp.application.ApplicationEx;
    import com.ibm.xsp.persistence.PersistedContent;
    import com.ibm.xsp.persistence.PersistenceService;
    import com.ibm.xsp.util.SessionUtil;
    import com.ibm.xsp.util.SystemUtil;

    /**
    * @author Oliver
    *
    */
    public class JSONService extends CustomServiceBean {
    public static String getSessionId() {
    return SessionUtil.getSessionId(FacesContext.getCurrentInstance());
    }

    public static String getBasePersistenceFolder() {

    ApplicationEx ex = ApplicationEx.getInstance();

    String propDir = ex.getProperty(“xsp.state.persistence.directory”, null);
    if (StringUtil.isEmpty(propDir)) {
    File defaultDir = SystemUtil.DEFAULT_PERSISTENCEDIR;
    return defaultDir.getAbsolutePath();
    } else {
    return propDir;
    }

    }

    public static String getPersistenceSessionFolder() {
    ApplicationEx ex = ApplicationEx.getInstance();
    return getBasePersistenceFolder() + File.separator + ex.getApplicationId() + File.separator + getSessionId();

    }

    @Override
    public void renderService(CustomService service, RestServiceEngine engine) throws ServiceException {
    HttpServletRequest request = engine.getHttpRequest();
    HttpServletResponse response = engine.getHttpResponse();
    response.setHeader(“Content-Type”, “application/json; charset=UTF-8”);
    try {
    String folder = getBasePersistenceFolder();
    String sessionid = getSessionId();

    ApplicationEx qqq = ApplicationEx.getInstance();
    PersistenceService ps = qqq.getPersistenceService();
    PersistedContent pd = ps.createPersistedContent(sessionid, folder, “file.txt”);

    PrintWriter writer = response.getWriter();
    JsonJavaObject jo = new JsonJavaObject();
    jo.put(“sessionid”, sessionid);
    jo.put(“folder”, folder);
    jo.put(“ps”, ps.toString());
    jo.put(“argument”, request.getParameter(“id”));
    writer.write(jo.toString());
    writer.close();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    Result:
    I’m sorry, because it would be useful to me.
    Some idea ?
    thank you so much

    27/09/2017 12.43.30 HTTP JVM: java.io.IOException: Unable to create directory for persisted content: C:\Users\ADMINI~1.PRA\AppData\Local\Temp\2\notes86A4BC\xsppers
    27/09/2017 12.43.30 HTTP JVM: at com.ibm.xsp.persistence.FilePersistenceService.getDirectory(FilePersistenceService.java:266)
    27/09/2017 12.43.30 HTTP JVM: at com.ibm.xsp.persistence.FilePersistenceService.getSessionFolder(FilePersistenceService.java:254)
    27/09/2017 12.43.30 HTTP JVM: at com.ibm.xsp.persistence.FilePersistedContent.(FilePersistedContent.java:49)
    27/09/2017 12.43.30 HTTP JVM: at com.ibm.xsp.persistence.FilePersistenceService.createPersistedContent(FilePersistenceService.java:73)
    ……..

    • camerongregor says:

      Hi!
      It looks like it is having a problem when it is trying to set up the folder structure in the file system.
      I think it could be because in your
      ps.createPersistedContent(sessionId, folder, “file.txt”)
      you are giving the ‘Base Persistence Folder’ as the folder. This is unnecessary when you are using the PersistenceService as the Persistence service already knows the base persistence folder. You only need to use BasePersistenceFolder when you are doing directy filesystem manipulation (without using the PersistenceService).

      Instead the ‘folder’ should just be a unique value that is used as a subfolder name to put files in different folders in the user’s session directory. e.g. When the peristence service is used normally, it puts files in folders that are named after the Document’s NoteID, so that they are kept sepearate.
      You could get away with just a word e.g. ‘myuploads’ or you you can come up with any naming scheme that is relative to what you are doing.

      Let me know if it works!

      Cameron

Leave a Reply

Your email address will not be published. Required fields are marked *