Why would that be wrong? That's exactly what AbstractFormController does when you set sessionForm to true.
Here's the code bits, modified a little so I can post them:
Code:
<bean id="AnswersValidator" class="app.model.AnswersValidator"/>
<bean id="AnswerQuestions" class="app.controller.AnswerQuestions">
<property name="sessionForm"><value>true</value></property>
<property name="commandClass"><value>app.model.QuestionsDTO</value></property>
<property name="validator"><ref bean="AnswersValidator"/></property>
<property name="formView"><value>AnswerQuestions</value></property>
<property name="successView"><value>AnswerQuestions</value></property>
</bean>
public class AnswerQuestions extends SimpleFormController {
private static Logger logger = Logger.getLogger(AnswerQuestions.class);
public ModelAndView onSubmit(HttpServletRequest req, HttpServletResponse res, Object command, BindException bindException) {
logger.debug("answering questions");
QuestionsDTO dto = (QuestionsDTO) command;
AuditType audit = (AuditType) req.getSession().getAttribute("audit");
ArrayList<QuestionType> questions = dto.getQuestions();
ArrayList<Answer> answers = dto.getAnswers();
logger.debug("questions size: " + questions.size());
logger.debug("answers size: "+ answers.size() );
for (int i = 0; i < questions.size(); i++) {
QuestionType questionType = questions.get(i);
String answer = answers.get(i).toString();
logger.debug("question: " + questionType.getText() + ", answer: " + answer);
}
// Pull up the next section's questions
QuestionsDTO nextDto = buildNextBackingObject(audit, dto.getSection());
if( nextDto == null ){
return new ModelAndView("AuditComplete", getCommandName(), nextDto);
}
else{
// because showForm() isn't being called, we need to set the session
// backing object ourselves, to prevent handleInvalidSubmit() from
// being called when the user submits this form. --james
req.getSession().setAttribute(getFormSessionAttributeName(), nextDto);
return new ModelAndView("AnswerQuestions", getCommandName(), nextDto);
}
}
protected Object formBackingObject(HttpServletRequest req) {
return buildBackingObject(req);
}
/**
* Build a backing object from an HttpRequest. Should only happen when we are entering
* the Controller from some external context, like a link off a web page or an email.
* @param req
* @return new QuestionsDTO from Audit, section
*/
private QuestionsDTO buildBackingObject(HttpServletRequest req) {
...
}
/**
* Build a backing object from an Audit and a Section. Basically we'll just grab the next
* section in the audit and build the DTO from there. Null return value signals that we can't
* find any more sections for this audit.
* @param audit
* @param section
* @return next sesssion's QuestionsDTO
*/
private QuestionsDTO buildNextBackingObject(AuditType audit, SectionType section) {
...
}
}
<%@ include file="include.jsp" %>
<html>
<body>
Section: ${command.section.text}
<form action="AnswerQuestions.htm?s=${command.nextSection}" method="post">
<table>
<c:forEach var="question" items="${command.questions}" varStatus="s">
<tr>
<td>
${question.text}
</td>
<spring:bind path="command.answers[${s.index}].value">
<td>
.... input/select/etc elements
</td>
<td>
.... status.error stuff
</td>
</spring:bind>
<tr>
</c:forEach>
</table>
<input type="submit" value="submit">
</form>
</body>
</html>
Given all this code, I'll describe what was happening again using a few more words:
1) Visit page for the first time. showForm() is called, which calls formBackingObject(HttpSerlvetRequest). As my code shows, this builds my model object and hands it back. showForm() then stores this model object in the session if sessionForm is true (read AbstractFormController if you want to prove this to yourself).
2) Fill out form and hit submit. handleRequestInternal() is called inside AbstractFormController(). Since isFormSubmission is true, and isSessionForm is true, the following code gets called:
Code:
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(getFormSessionAttributeName(request)) == null) {
// Cannot submit a session form if no form object is in the session.
return handleInvalidSubmit(request, response);
}
This works fine (this time), because showForm() stored the command object in the session for us.
_However_, the next thing that happens is that getCommand() is called. getCommand does this when isFormSession is true:
Code:
// Session-form mode: retrieve form object from HTTP session attribute.
HttpSession session = request.getSession(false);
if (session == null) {
throw new ServletException("Must have session when trying to bind (in session-form mode)");
}
String formAttrName = getFormSessionAttributeName(request);
Object sessionFormObject = session.getAttribute(formAttrName);
if (sessionFormObject == null) {
throw new ServletException("Form object not found in session (in session-form mode)");
}
// Remove form object from HTTP session: we might finish the form workflow
// in this request. If it turns out that we need to show the form view again,
// we'll re-bind the form object to the HTTP session.
if (logger.isDebugEnabled()) {
logger.debug("Removing form session attribute [" + formAttrName + "]");
}
session.removeAttribute(formAttrName);
The comment that "we'll re-bind the form object" turns out not to be true, because showForm() isn't being called again the second time through.
3) onSubmit(...) is called, my debug code prints out the correct question/answer pairs, and then I build the next model object based on that. This model object is returned with the new ModelAndView() call. Note that showForm() is never called after this value is returned. Obviously onSubmit() shouldn't be calling showForm() directly, but I had assumed _something_ would call it.
4) JSP renders again, with the new questions from the new model. User fills out the form and hits submit.
5) handleRequestInternal gets called again, isFormSession is still true, isFormSubmission is true, and as the code block above shows, handleInvalidSubmit() is called. handleInvalidSubmit() then tries to build a backing object by calling formBackingObject().
6) My code sees the call to formBackingObject() and builds the *next* iteration's backing object, which handleInvalidSubmit() then tries to bind to the form results and throws an NPE because the arrays aren't the right size.
I hacked my way around this successfully by giving formBackingObject more state to work with, so it could determine whether or not this was a call via handleInvalidSubmit or it was something legit. I'd rather not have to build 2 backing objects for every pass through the controller, though, which means the command object has to get into the session somehow. Me putting it there seems to work, but I'd of course prefer that Spring MVC put it there for me. However, since no page request is happening between step 3 and step 4, AbstractFormController isn't going to do it.
--James