In the previous part we used a basic XAgent and a SmartNSF custom route to trigger Xots tasks. But both still required setting up a Xots task as a specific Java class. As long as all we’re wanting to do is process a REST service and send a basic response, there are more things we can do to make it even easier.
Making It Easier – Callback Method
A few weeks prior to putting all that code together I talked to Karsten Lehmann who explained to me, for a different reason, about Java callbacks. It’s something I had inadvertently been familiar with from Vaadin development and the buttons used there. It basically allows you to just write your code, streamlining your development and bootstrapping with standard code. So I added this kind of bootstrapping to ODA and for these scheduled tasks. The starting point is the SchedReallyEasyJavaArchive XPage, which calls com.paulwithers.Utils.processBackgroundCallback()
. This just calls com.paulwithers.forOda.GenericHttpRequestUtils.initialiseAndProcessBackgroundTask()
. This method has since been added to the latest FP9 and FP10 versions of ODA as XspUtils.initialiseAndProcessResponse()
.
public static void processBackgroundCallback() { GenericHttpRequestUtils.initialiseAndProcessBackgroundTask(new IXotsXspRunnableCallback() { @Override public void process(Map<String, String> params) { try { // Iterate 100 entries from the AllContacts view and archive them Database currDb = Factory.getSession(SessionType.CURRENT).getCurrentDatabase(); View contacts = currDb.getView("AllContacts"); ViewNavigator nav = contacts.createViewNav(); ViewEntry ent = nav.getFirst(); Integer archCount = Integer.parseInt(params.get("userCount")); for (int x = 0; x < archCount; x++) { if (null == ent) { break; } ViewEntry next = nav.getNext(); Document doc = ent.getDocument(); if (Utils.archiveDoc(doc)) { doc.remove(true); } ent = next; } } catch (Throwable t) { t.printStackTrace(); } } }); }
The method takes a new IXotsRunnableCallback
object which has a process()
method which has access to the query parameters passed to the XAgent and is where tou write your code. Here the code gets the “userCount” queryParam for how many documents to archive.
public static void initialiseAndProcessBackgroundTask(IXotsXspRunnableCallback callback) { try { FacesContext ctx = FacesContext.getCurrentInstance(); ExternalContext ext = ctx.getExternalContext(); HttpServletRequest request = (HttpServletRequest) ext.getRequest(); Enumeration queryParams = request.getParameterNames(); Map<String, String> params = new HashMap<String, String>(); while (queryParams.hasMoreElements()) { String key = (String) queryParams.nextElement(); params.put(key, request.getParameter(key)); } BasicXotsCallbackRunnable xotsTask = new BasicXotsCallbackRunnable(callback, params); Xots.getService().submit(xotsTask); XspHttpServletResponse response = (XspHttpServletResponse) ext.getResponse(); response.setContentType("application/json"); response.setHeader("Cache-Control", "no-cache"); PrintWriter writer = response.getWriter(); // This line highlights why you should use a JsonWriter - painful and prone to error writer.write("{\"message\": \"asynchronous task running\"}"); // Terminate the request processing lifecycle. FacesContext.getCurrentInstance().responseComplete(); } catch (Throwable t) { t.printStackTrace(); } }
The ODA method itself extracts the query parameters, creates and schedules a BasicXotsCallableRunnable
passing in those query parameters, before passing JSON back to say the Xots task has been scheduled. That class, which is set up in ODA for you, triggers your process()
method. It’s possible to access the header parameters from the request as well, but I haven’t done that here.
Generic XAgent
But we can make it smarter and easier, especially with Java 8. This is done from the GenericXAgent XPage. This calls com.paulwithers.GenericXAgentManager.doGet()
. The GenericXAgentManager class is another utility class that could be used to handle various XAgents in the NSF.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
/** * Generic GET request method. Don't panic, take a deep breath, and follow me down the rabbit hole to Wonderland! */ public static void doGet() { try { /* * One method call to Utils.initialiseAndProcessResponseAsJson() does all the boilerplating for extracting the request, response and JsonJavaObject * and properly terminating the response. JsonJavaObject is basically the same as a Java Map. All gotchas are handled for you! * * The bit that could be new to most is that Utils.initialiseAndProcessResponseAsJson() takes an anonymous inner class as its method. This is just * a way to pass in a process() method that can interact with the request and response set up by Utils.initialiseAndProcessResponseAsJson() * * With Java 8 it becomes more readable with lambdas: * * GenericHttpRequestUtils.initialiseAndProcessResponseAsJson((request, response, jsonObj) -> { * // do stuff here * }); */ GenericHttpRequestUtils.initialiseAndProcessResponseAsJson(new IXspHttpServletJsonResponseCallback() { /* (non-Javadoc) * @see com.paulwithers.IXspHttpServletResponseCallback#process(javax.servlet.http.HttpServletRequest, com.ibm.xsp.webapp.XspHttpServletResponse) */ public void process(HttpServletRequest request, XspHttpServletResponse response, JsonJavaObject jsonObj) throws IOException { // A. Has the user requested that the process runs asynchronously? boolean async = false; if (null != request.getParameter("doAsync")) { async = true; response.setStatus(HttpServletResponse.SC_ACCEPTED); // Tell the user it's queued, not processed } // B. Has the user included the "process" that should be run if (null == request.getParameter("process")) { // Invalid request!! response.sendError(HttpServletResponse.SC_BAD_REQUEST, "This endpoint expects a process type to be passed"); } else { // We're good to go try { // pre-Java 8 we need an if statement. switch statements using strings are Java 7+, so FP10+ if ("loadData".equals(request.getParameter("process"))) { // 1. LOAD ADDITIONAL USERS WITHOUT CLEARING THIS DATABASE DOWN // Was a number of users passed? We'll default to 200 int userCount = 200; if (null != request.getParameter("userCount")) { userCount = Integer.parseInt(request.getParameter("userCount")); } if (async) { // Async request, run in background and confirm in JSON Xots.getService().submit( new DataInitializerBackground(InitializerType.LOAD, userCount)); jsonObj.put("message", userCount + " users queued for asynchronous generation"); } else { // In real-time request, run and confirm in JSON DataInitializer d = new DataInitializer(); d.initUsers(userCount); d.initStates(); d.initAllTypes(); d.run(); jsonObj.put("message", userCount + " users created"); } } else if ("reloadData".equals(request.getParameter("process"))) { // 2. DELETE AND RELOAD ADDITIONAL USERS // Same as above, just including deletion. Was a number of users passed? We'll default to 200 int userCount = 200; if (null != request.getParameter("userCount")) { userCount = Integer.parseInt(request.getParameter("userCount")); } if (async) { // Async request, run in background and confirm in JSON Xots.getService().submit( new DataInitializerBackground(InitializerType.RELOAD, userCount)); jsonObj.put("message", "data clearance and " + userCount + " users queued for asynchronous generation"); } else { // In real-time request, run and confirm in JSON DataInitializer d = new DataInitializer(); d.initDeleteDocuments(); d.initUsers(userCount); d.initStates(); d.initAllTypes(); d.run(); jsonObj.put("message", "data cleared and " + userCount + " users created"); } } else if ("deleteData".equals(request.getParameter("process"))) { // 3. JUST CLEAR DOWN THE DATABASE. IF YOU WANT TO CLEAR DOWN THE ARCHIVE, GUESS WHAT - CALL THE REST SERVICE THERE! if (async) { // Async request, run in background and confirm in JSON Xots.getService().submit(new DataInitializerBackground(InitializerType.DELETE)); jsonObj.put("message", "data clearance queued for asynchronous generation"); } else { // In real-time request, run and confirm in JSON DataInitializer d = new DataInitializer(); d.initDeleteDocuments(); d.run(); jsonObj.put("message", "data cleared"); } } else { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid process parameter passed"); } } catch (Throwable t) { // Whoops! We've hit an unexpected error. Return an error 500 t.printStackTrace(); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, t.getMessage()); } } } }); } catch (Throwable t) { t.printStackTrace(); } } |
The code initially looks quite complex, but that’s because it’s designed to be flexible and allow various parameters to be passed. So let me walk through it. Lines 19-25 are required for Java 6. If you’re on FP10 or above, you can use a lambda, and replace those lines with GenericHttpRequestUtils.initialiseAndProcessResponseAsJson((request, response, jsonObj) -> {
. Unfortunately so far I can’t find a way to auto-populate the lambda, so you need to manually type “(request, response, jsonObj) -> {“.
Lines 28-32 look for a query parameter “doAsync”. If the query parameter hasn’t been passed, the code will run while the REST client waits for a response and, once complete, the response will be sent. If the query parameter has been passed, the code will run asynchronously in a Xots task com.paulwithers.xots.DataInitializerBackground
. An appropriate message will be passed back to the REST client with the response code 202, to denote that the request was accepted by the server but has not yet been actioned. The response code status is set in line 31.
At line 35 the code looks for a query parameter “process”. If that’s not passed, we throw a 400 error, denoting that an invalid request was made. We then check what process was requested. The valid options are “loadData”, “reloadData” or “deleteData”. This means this single XPage can perform multiple actions. If none of those options are passed then at line 102 we again throw a 400 error.
For the “loadData” and “reloadData” options, we also look for the number of users to create from the “userCount” query parameter, defaulting to 200. What those options do is self-explanatory and uses the Extension Library DataInitializer. We also have a try/catch block that, if it hits an error, returns an error 500 to the browser with the relevant error message, at line 109.
What isn’t done here is to check the request method, to ensure that REST service only accepts a GET request and not one of the other options. but request.getMethod()
could be used to check that.
This gives an idea of the kind of flexibility that an XAgent could provide, processing multiple processes depending on what parameters are passed. And while this uses query parameters, there are similar options in the request object to retrieve header parameters.
This leaves one step still missing, the ability to chain jobs together. That’s easily achievable though, and I’ll show that in the next part.