Building XPages servlets with FacesContext access
Thu Sep 06 10:05:00 EDT 2012
I have a confession to make: I'm not crazy about XAgents. Don't get me wrong - they do everything they're supposed to and do it well. However, it's always kind of bothered me that you take a visual design element like an XPage and turn off all the higher levels to get back down to the core servlet. Plus, it muddies the list of XPages in the DB - some are actual XPages, some are just wrappers for scripts. So my objection is essentially pedantry.
However, the fact that my objection is wildly exaggerated has never stopped me from sinking a lot of time into finding the "right" answer before, and it hasn't stopped me now. When what I want to build is, say, an Excel exporter for a particular type of document in a reporting DB, what I really want is a basic servlet that exists only in the database and has access to the surrounding XSP environment and custom classes. Fortunately, between a post by Sven Hasselbach, the referenced Chinese-language developerWorks article, the XSP Starter Kit, and a bit about facesContext.release() on a JSF-focused article and reinforced by the FacesContextServlet
class from the ExtLib, I made it work.
The developerWorks article covers the bulk of the work - creating the Factory, setting up the file in META-INF, etc.. After that, you can set up your servlets by extending (using the Starter Kit method) DesignerFacesServlet
. It turns out that the important thing to remember is to close out your context when you're done - I ran into a lot of trouble from not doing this, which would break XPages visited after the servlet. As an example, here's the test class I ended up building while figuring out how to make it not blow up my application:
import java.io.*; import java.util.*; import com.ibm.commons.util.StringUtil; import com.ibm.xsp.webapp.DesignerFacesServlet; import javax.faces.context.FacesContext; import javax.servlet.*; import javax.servlet.http.*; public class TestServlet extends DesignerFacesServlet implements Serializable { private static final long serialVersionUID = -1152176824225969420L; @SuppressWarnings("unchecked") @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { // Set up handy environment variables HttpServletRequest req = (HttpServletRequest)servletRequest; HttpServletResponse res = (HttpServletResponse)servletResponse; ServletOutputStream out = res.getOutputStream(); FacesContext facesContext = this.getFacesContext(req, res); try { res.setContentType("text/plain"); out.println("start"); // The sessionScope is available via the ExternalContext. Resolving the variable // would work as well Map<Object, Object> sessionScope = facesContext.getExternalContext().getSessionMap(); sessionScope.put("counter", sessionScope.containsKey("counter") ? (Integer)sessionScope.get("counter") + 1 : 1); out.println("Counter: " + sessionScope.get("counter")); // A query string map is available via the request. This method, as opposed to // getting the "param" variable, returns arrays of strings, allowing things like // "?foo=bar&foo=baz" properly Map<String, String[]> param = req.getParameterMap(); for(String key : param.keySet()) { out.println(key + " => " + StringUtil.concatStrings(param.get(key), ';', false)); } out.println("done"); } catch(Exception e) { e.printStackTrace(new PrintStream(out)); } finally { out.close(); // It shouldn't be null if things are going well, but a check never hurt if(facesContext != null) { facesContext.responseComplete(); facesContext.release(); } } } }
I set up my ServletFactory
to use this one for "/test", and so it's available via a URL like "/database.nsf/xsp/test?foo=bar". As with an XPage, you can also chain more path bits on after the servlet name, like "/database.nsf/xsp/test/some/other/stuff" and get to that via req.getPathInfo() - though with the caveat that, unlike with an XPage, the servlet path is included in the path info, so it would return "/xsp/test/some/other/stuff".
So long as I don't run into any other app-exploding problems, I plan to go this route for non-UI requests like exports and actions. When I have a use for it, I'll also go down the related path of writing custom services, for which the ExtLib provides an extensive foundation.
Nathan T. Freeman - Thu Sep 06 15:25:33 EDT 2012
"close out your context when you're done - I ran into a lot of trouble from not doing this, which would break XPages visited after the servlet"
Yes, because the .release() is when XPages disposes of a number of ThreadLocal variables like, oh, the Session! No .release() means no .recycle() and no .sTerm();