Last week, I wrote about the Spring based Chained Controller to chain various controllers into a single request. This week, I provide some examples of its usage, and describe some modifications I made to it so as to make it easier to use.
Using a ThreadLocal to store shared context
In my original post, I had suggested using a the Spring BeanFactory as a ThreadLocal to store context between two controllers in a sequential chain so they can communicate. However, thinking about it some more, I figured that it would be friendlier to just provide a per-thread ChainedController context, so thats what I did. This involves a small modification in the original ChainedController code. We create a ThreadLocal as a private static member variable of the ChainedController, and provide public getters and setters. The variable declaration is shown below, the getContext() and setContext() methods follow the standard pattern:
1 2 3 4 5 | private static ThreadLocal<Map<String,Object>> context = new ThreadLocal<Map<String,Object>>() {
protected synchronized Map<String,Object> initialValue() {
return new HashMap<String,Object>();
}
};
|
To use it between two controllers A and B in a chain, A would set a variable into the context and B would retrieve it later and perform some decision based on the content of the variable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class A implements Controller {
public ModelAndView handleRequest(...) {
...
Map<String,Object> chainCtx = ChainedController.getContext();
chainCtx.put("someVariable", "someValue");
ChainedController.setContext(chainCtx);
}
}
public class B implements Controller {
public ModelAndView handleRequest(...) {
...
Map<String,Object> chainCtx = ChainedController.getContext();
Object someVariable = chainCtx.get("someVariable");
// do something with someVariable
...
}
}
|
Obviously, the context is only meaningful when used in a sequential chain. By definition, controllers in a parallel chain should have no dependencies on each other, which implies that parallel chains can have no shared context.
Example of Extending a ChainedController
I had mentioned the possibility of extending a ChainedController to plug in any special processing that needs to be done, but in retrospect, I think it may just be better to extend the chain with another controller which contains the necessary logic to do this. However, here is an (admittedly contrived) example, in case you really need to do this. The example computes and prints the response time at the bottom of the generated page. To solve this problem using configuration alone, you will need to add two controllers to the chain, one to capture the start time, and another to capture the end time and compute the difference. Extending is much simpler in this case, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ResponseTimeReportingChainedController extends ChainedController {
public ResponseTimeReportingChainedController() {
super();
}
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
StopWatch watch = new StopWatch();
watch.start("response time");
ModelAndView mav = super.handleRequest(request, response);
watch.stop();
mav.addObject("responseTime", watch.getTotalTimeMillis());
return mav;
}
}
|
The change to configure this controller instead of a ChainedController is to simply replace the class attribute for the controller in the Spring BeanFactory.
Bug in CallableController
I also discovered a bug in the CallableController inner class. The constructor signature took the HttpServletRequest and HttpServletResponse, but I was not passing them in. I did not notice the bug because my test case did not have to use the request for anything, but when I wrote the example of extending the ChainedController, I did, which exposed the problem. So the constructor for the CallableController will change to:
1 2 3 4 5 6 7 8 9 10 | private class CallableController implements Callable<ModelAndView> {
...
public CallableController(Controller controller, HttpServletRequest request, HttpServletResponse response) {
this.controller = controller;
// the following two lines need to be added.
this.request = request;
this.response = response;
}
...
}
|
Son of Spring in Chains - the DesignView
Sorry about the cheesy subtitle, but in a sense it is appropriate. Let me explain. The example web application I wrote to test the ChainedController uses JSTL on the front-end. Each controller in the chain is associated with one (or more, but at least one) JSP file. Thus the controller + JSP can be thought of as a single component. The components are brought together on the web page by plugging them into a Tiles layout.
For most web applications (at least the ones I am familiar with), the Java/JSP developer does not write the HTML that the user sees on his browser. A graphic artist (usually a HTML/CSS guru) builds a mockup with data that goes "Lorem Ipsum..." and gives it to the web developer. The web developer then works backward to fill out the "Lorem Ipsum..." stuff with valid data from a real data source. I feel this process is backward, since it is not round-trip. If there is a change, the HTML designer is generally not able to tweak the JSP to reflect that. So the web developer is stuck merging presentation changes from the new mockup into the JSP, which is more difficult and time-consuming than if the presentation changes were directly applied to the JSP.
One team I know actually reversed this flow, and they used Velocity as their view layer. Since Velocity syntax is simpler, the HTML writer was able to double as a Velocity coder as well. What the Java developer provided was full documentation of the bean(s) available in the page context. Since JSTL provides a similar bean abstraction (by means of a dotted notation), the DesignView is an attempt to provide, via reflection, a table of key-value pairs of all variables (and their values, based on test data) available to the JSTL coder in the page context. Here is the code for the DesignView:
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | package org.springchains.framework.views;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.View;
/**
* A simple view implementation that renders the contents of the Model as
* a tabular display of key-value pairs. The keys are displayed as JSTL
* variables, which can provide information to the HTML designer.
* @author Sujit Pal
* @version $Revision: 1.1 $
*/
public class DesignView implements View {
public DesignView() {
super();
}
/**
* The View renderer.
* @param model the Model from the ModelAndView object generated by the
* Controller.
* @param request the HttpServletRequest object.
* @param response the HttpServletResponse object.
* @throws Exception if one is thrown.
*/
@SuppressWarnings("unchecked")
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
List<Map<String,String>> keyValuePairList = new ArrayList<Map<String,String>>();
Set<String> keys = model.keySet();
for (String key : keys) {
Object obj = model.get(key);
Map<String,String> keyValuePairs = new LinkedHashMap<String,String>();
keyValuePairs.put(key, obj.getClass().getName());
renderBeanAsNameValuePairs(keyValuePairs, obj, key);
keyValuePairList.add(keyValuePairs);
}
PrintWriter writer = response.getWriter();
String htmlOutput = renderHtmlFromKeyValuePairList(keyValuePairList);
response.setContentLength(htmlOutput.length());
response.setContentType("text/html");
writer.print(htmlOutput);
writer.flush();
writer.close();
}
/**
* Traverses the bean graph for the bean specified by obj, and populates
* a Map of keyValue pairs. This is a recursive function, which will be
* called on dependent objects for the top-level bean. Recursion stops when
* the Object encountered is "printable" as reported by isPrintable().
* @param keyValuePairs the Map of key-value pairs to populate.
* @param obj the object to render.
* @param prefix the prefix (the top level call is the key name), and any
* subsequent recursive calls adds extra parts to the prefix.
* @throws Exception if one is thrown.
*/
@SuppressWarnings("unchecked")
private void renderBeanAsNameValuePairs(Map<String,String> keyValuePairs, Object obj, String prefix) throws Exception {
Map<String,Object> properties = PropertyUtils.describe(obj);
for (String propertyName : properties.keySet()) {
if ("class".equals(propertyName)) {
continue;
}
Object property = properties.get(propertyName);
if (property.getClass().isPrimitive()) {
// the property is a primitive, such as int, long, double, etc.
keyValuePairs.put(prefix + "." + propertyName, String.valueOf(property));
} else if (isPrintableObject(property)) {
// the property is a printable, see isPrintable()
keyValuePairs.put(prefix + "." + propertyName, StringUtils.defaultString(property.toString(), "NULL"));
} else if (property instanceof Collection) {
// the property is a collection, iterate and recurse
Collection collectionProperty = (Collection) property;
keyValuePairs.put(prefix + "." + propertyName + ".size()", String.valueOf(collectionProperty.size()));
int i = 0;
for (Iterator it = collectionProperty.iterator(); it.hasNext();) {
Object indexedProperty = it.next();
renderBeanAsNameValuePairs(keyValuePairs, indexedProperty, prefix + "." + propertyName + "[" + i + "]");
i++;
}
} else {
// recurse down the object graph
renderBeanAsNameValuePairs(keyValuePairs, property, prefix + "." + propertyName);
}
}
}
/**
* Returns true if the object is a printable object, ie, a toString()
* on the object will return a descriptive idea of the contents. These
* include all the base classes in java.lang (String, Integer, etc)
* and some others such as java.util.Date and java.math.BigDecimal.
* This list is subject to change as we encounter other classes which
* can be considered printable for the purpose of our DesignView.
* @param obj the Object to test.
* @return true or false.
*/
private boolean isPrintableObject(Object obj) {
if (obj == null) {
return true;
}
String className = obj.getClass().getName();
if (className.startsWith("java.lang.")) {
return true;
} else if ("java.util.Date".equals(className)) {
return true;
} else if ("java.math.BigDecimal".equals(className)) {
return true;
}
return false;
}
/**
* Generates a standard HTML document (with a gray screen) with an
* embedded table showing the variables available to the HTML designer
* to embed dynamic content into the page.
* @param keyValuePairList a List of Maps, each Map corresponds to a single
* Model key.
* @return a String which can be pushed into the response.
*/
private String renderHtmlFromKeyValuePairList(List<Map<String,String>> keyValuePairList) {
StringBuffer buf = new StringBuffer();
buf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">").
append("<html>").
append("<head>").
append("<title>Design View</title>").
append("<body>").
append("<table cellspacing=\"3\" cellpadding=\"3\" border=\"1\">");
for (Map<String,String> keyValuePair : keyValuePairList) {
int i = 0;
for (String key : keyValuePair.keySet()) {
if (i == 0) {
// header, blue background
buf.append("<tr bgcolor=\"blue\" fgcolor=\"white\">"). append("<td><b>").
append(StringEscapeUtils.escapeHtml(key)).
append("</b></td>").
append("<td>").
append(StringEscapeUtils.escapeHtml(keyValuePair.get(key))).
append("</td>").
append("</tr>");
} else if (key.endsWith(".size()")) {
// collection, make light gray
buf.append("<tr bgcolor=\"gray\">").
append("<td><b>").
append(StringEscapeUtils.escapeHtml(key)).
append("</b></td>").
append("<td>").
append(StringEscapeUtils.escapeHtml(keyValuePair.get(key))).
append("</td>").
append("</tr>");
} else {
// not header, white background
buf.append("<tr>").
append("<td><b>${").
append(StringEscapeUtils.escapeHtml(key)).
append("}</b></td>").
append("<td>").
append(StringEscapeUtils.escapeHtml(keyValuePair.get(key))).
append("</td>").
append("</tr>");
}
i++;
}
}
buf.append("</table>").
append("</body>").
append("</head>").
append("</html>");
return buf.toString();
}
}
|
Yes, I know that the rendering is done in Java instead of using a template, and that its harder to maintain. However, this is unlikely to be modified very frequently, so its probably ok to not use an external template to do this.
Each component controller is configured with the DesignView by default. The JSTL coder will be able to change it to something else via configuration if desired. An example of how to set up the DesignView as the default is shown below:
1 2 3 4 5 6 | public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView();
// do something with the model
...
mav.setView(new DesignView());
return mav;
|
A screenshot of the DesignView of the central component (MainBody) is shown below on the left, and the actual page is shown on below on the right. The DesignView specifies the name of the bean that has to be bound using a jsp:useBean in blue. All other rows refer to JSTL variables available to the JSTL programmer.