Programming Thoughts
Struts 2 - Post/Redirect/Get
Forwarding to View Action
Struts 2 framework is not helping with complex pages
Struts 2 is a popular MVC framework for Java-based web applications. Like all good frameworks, it does a lot of drudge work and is highly flexible and extensible. Its basic workflow should always be useful but creates unmaintainable, spaghetti code when adding the complex data and UI elements users are using a working system for.
The problem
Tutorials about form processing, such as the Apache documentation, dispatch to a JSP page when processing is complete. This is fine till used in practice. Consider the screenshot of a typical page below and notice it has four different forms, one of which uses a dropdown list of countries. The list of countries is not hardcoded in the JSP and must be supplied by the Action. The Struts Action for each form is only concerned about its own job but when it dispatches to the same JSP page, must know how to construct all the other forms and any ancillary data.
Now consider getting a ticket to modify the contacts section. You locate and modify the processing Action, modify the contacts section, check it works, and resolve the ticket. Later, you get a ticket that using any of the other three forms you forgot about messes up the contact section. You now have spaghetti code in what's supposed to be a straightforward workflow.
First solution attempt
Obviously, each form processing Action should delegate displaying results to common code and as there's a viewer Struts Action for displaying the JSP in the first place, that's the candidate. Dispatching to the viewer Action works and code is more maintainable. Alas, when the processing Action writes a message, it isn't displayed whereas any written by the viewer Action does.
What's going wrong is message display code in the JSP page is searching the ValueStack for the first object
implementing getActionMessages()
and the like and that's the viewer action. The top object in the
stack, the viewer Action, is hiding all lower objects with messages. Struts 2 is designed around an Action
forwarding to a JSP, so splitting workflow responsibilities to reduce duplicated code breaks Struts 2 design.
Second solution attempt
As more than one Action messes up with Struts 2 design, don't dispatch to another one and delegate display logic to common, non-Action, helper code. That means form processing Actions must present all the same fields and ancillary data the viewer Action displays. That's still a lot of replicated code, which is what we're trying to avoid. Forms can be shared, separate classes but this only reduces the replication, still leaving maintenance problems.
Action chaining
Action Chaining is a viable solution but even the documentation recommends against it. The links in the documentation to Martin Fowler's articles don't work and the articles are Transaction Script and Facade Pattern. The danger with chaining is it could become part of business logic and can't be accessed by other services, leading to duplicate code. This is not a problem as long as Actions are only used for presentation logic.
Actual solution
A form processing Action should be redirecting, not forwarding, to another display action anyway and the solution described in Post/Redirect/Get already solves this.
Alternate solution
Dispatching from one viewer Action to another can be useful, such as a products view Action dispatching to particular view Action for the product type.
The hidden messages can be solved with an interceptor on the last Action. The intercept
function should be the following.
@Override public String intercept(ActionInvocation invocation) throws Exception { invocation.addPreResultListener(this); return invocation.invoke(); }
The beforeResult
function read all messages from every object in the ValueStack and writes them
to the current Action, which is at the top of the stack.
private void amalgamateMessages(ValidationAware viewAction, ActionInvocation invocation) { ValidationAware other; CompoundRoot root; Collection<String> actionErrors, actionMessages; List<String> fieldErrors; Map<String,List<String>> allFieldErrors; actionErrors = viewAction.getActionErrors(); actionMessages = viewAction.getActionMessages(); allFieldErrors = viewAction.getFieldErrors(); root = invocation.getStack().getRoot(); for (Object o: root) { // Copy messages from anything that can hold them except the target itself if (o instanceof ValidationAware && (o != viewAction)) { other = (ValidationAware)o; actionErrors.addAll(other.getActionErrors()); actionMessages.addAll(other.getActionMessages()); for (Entry<String,List<String>> entrySet: other.getFieldErrors().entrySet()) { fieldErrors = allFieldErrors.get(entrySet.getKey()); if (fieldErrors == null) { fieldErrors = new ArrayList<String>(); } fieldErrors.addAll(entrySet.getValue()); allFieldErrors.put(entrySet.getKey(), fieldErrors); } } } viewAction.setActionErrors(actionErrors); viewAction.setActionMessages(actionMessages); viewAction.setFieldErrors(allFieldErrors); } @Override public void beforeResult(ActionInvocation invocation, String resultCode) { Action action; action = (Action)invocation.getAction(); if (action instanceof ValidationAware) { amalgamateMessages((ValidationAware)action, invocation); } }
Next part
Continued in Redisplaying a Form.