Announcement Announcement Module
Collapse
No announcement yet.
handleInvalidSubmit called, why? Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • handleInvalidSubmit called, why?

    I have a SimpleFormController, call it AnswerQuestions for our purposes.

    This controller gets called over and over (its success view is itself). Each time it gets called, it sets itself up with a different form backing object, one appropriate to the questions being asked in this iteration.

    The number of questions vary, and sometimes I have been getting NPE's as my JSP tries to index into questions which aren't there. So, I tried to find out why those questions weren't there.


    It turns out that the first time I submit one of these, it's fine. The second time, handleInvalidSubmit is getting called, and as a result I try and create a form backing object for the next group of questions, then that object is used for the current group, and of course NPE's ensue.

    I've worked around this using a session variable that I set to let me know which form backing object I should be building, but I would prefer to handle this in less of a hacked up way, especially since it creates problems if I don't clear that session variable once I'm done with it.

    My guess is that the first pass through the form, the backing object is written into the session attribute just fine, and then removed when getCommand() is called. For some reason, on the second pass through, the backing object never gets written into the session, therefore handleInvalidSubmit tries to build a new one for me.

    Is there something I need to do to ensure the backing object's session gets written in onSubmit, something that it's normally assumed would be done by the success view's showForm() method but that I'm not doing because that's only called once?

    TIA for your help,

    James

  • #2
    Adding this to onSubmit() seems to be doing the trick:

    // 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(getFormSessionAttrib uteName(), command);
    return new ModelAndView("AnswerQuestions", getCommandName(), command);

    Any gotchas I should know about here? I think the session object will be getting removed when they actually do the submit, and all my redirect/success view returns happen before that call.

    James

    Comment


    • #3
      Keeping the form object in session doesn't sound like the right thing to do

      Can you post your code please.

      Comment


      • #4
        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 &#123;
            private static Logger logger = Logger.getLogger&#40;AnswerQuestions.class&#41;;
        
            public ModelAndView onSubmit&#40;HttpServletRequest req, HttpServletResponse res, Object command, BindException bindException&#41; &#123;
                logger.debug&#40;"answering questions"&#41;;
        
                QuestionsDTO dto = &#40;QuestionsDTO&#41; command;
                AuditType audit = &#40;AuditType&#41; req.getSession&#40;&#41;.getAttribute&#40;"audit"&#41;;
        
                ArrayList<QuestionType> questions = dto.getQuestions&#40;&#41;;
                ArrayList<Answer> answers = dto.getAnswers&#40;&#41;;
                logger.debug&#40;"questions size&#58; " + questions.size&#40;&#41;&#41;;
                logger.debug&#40;"answers size&#58; "+ answers.size&#40;&#41; &#41;;
                for &#40;int i = 0; i < questions.size&#40;&#41;; i++&#41; &#123;
        
                    QuestionType questionType = questions.get&#40;i&#41;;
                    String answer = answers.get&#40;i&#41;.toString&#40;&#41;;
                    logger.debug&#40;"question&#58; " + questionType.getText&#40;&#41; + ", answer&#58; " + answer&#41;;
                &#125;
        
                // Pull up the next section's questions
                QuestionsDTO nextDto = buildNextBackingObject&#40;audit, dto.getSection&#40;&#41;&#41;;
        
                if&#40; nextDto == null &#41;&#123;
                    return new ModelAndView&#40;"AuditComplete", getCommandName&#40;&#41;, nextDto&#41;;
                &#125;
        
                else&#123;
                    // because showForm&#40;&#41; isn't being called, we need to set the session
                    // backing object ourselves, to prevent handleInvalidSubmit&#40;&#41; from
                    // being called when the user submits this form.    --james
                    req.getSession&#40;&#41;.setAttribute&#40;getFormSessionAttributeName&#40;&#41;, nextDto&#41;;
        
                    return new ModelAndView&#40;"AnswerQuestions", getCommandName&#40;&#41;, nextDto&#41;;
                &#125;
            &#125;
        
            protected Object formBackingObject&#40;HttpServletRequest req&#41; &#123;
                return buildBackingObject&#40;req&#41;;
            &#125;
        
            /**
             * 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&#40;HttpServletRequest req&#41; &#123;
                  ...
            &#125;
        
            /**
             * 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&#40;AuditType audit, SectionType section&#41; &#123;
                ...
            &#125;
        &#125;
        
        
        <%@ include file="include.jsp" %>
        
        <html>
        <body>
        
        Section&#58; $&#123;command.section.text&#125;
        
        <form action="AnswerQuestions.htm?s=$&#123;command.nextSection&#125;" method="post">
            <table>
                <c&#58;forEach var="question" items="$&#123;command.questions&#125;" varStatus="s">
                <tr>
                    <td>
                            $&#123;question.text&#125;
                    </td>
                    <spring&#58;bind path="command.answers&#91;$&#123;s.index&#125;&#93;.value">     
                    <td>
                           .... input/select/etc elements
                    </td>
                    <td>
                           .... status.error stuff
                    </td>
                    </spring&#58;bind>
                <tr>
                </c&#58;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&#40;false&#41;;
            if &#40;session == null || session.getAttribute&#40;getFormSessionAttributeName&#40;request&#41;&#41; == null&#41; &#123;
                // Cannot submit a session form if no form object is in the session.
                return handleInvalidSubmit&#40;request, response&#41;;
            &#125;
        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&#58; retrieve form object from HTTP session attribute.
        		HttpSession session = request.getSession&#40;false&#41;;
        		if &#40;session == null&#41; &#123;
        			throw new ServletException&#40;"Must have session when trying to bind &#40;in session-form mode&#41;"&#41;;
        		&#125;
        		String formAttrName = getFormSessionAttributeName&#40;request&#41;;
        		Object sessionFormObject = session.getAttribute&#40;formAttrName&#41;;
        		if &#40;sessionFormObject == null&#41; &#123;
        			throw new ServletException&#40;"Form object not found in session &#40;in session-form mode&#41;"&#41;;
        		&#125;
        
        		// Remove form object from HTTP session&#58; 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 &#40;logger.isDebugEnabled&#40;&#41;&#41; &#123;
        			logger.debug&#40;"Removing form session attribute &#91;" + formAttrName + "&#93;"&#41;;
        		&#125;
        		session.removeAttribute&#40;formAttrName&#41;;
        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

        Comment


        • #5
          I think I meant that if you need to store your form on the session *yourself* for spring to work usually means you have configured something wrong

          Also, conceptually, I do not believe you want your "questions" hanging around in the session.

          To be honest, given your "workflow", it would seem that AbstractWizardController would be a better fit. Hmm, it is quite an unusual requirement your got there

          Also, don't forget to add your errors object to the model returned from onSubmit:

          Code:
                      return new ModelAndView&#40;"AnswerQuestions", getCommandName&#40;&#41;, nextDto&#41;.addAll&#40;bindException.getModel&#40;&#41;&#41;;
          It would also be very useful for you to print out all global errors on the jsp, or at least debug the contents of bindException to see if there are any binding errors (and hence handleInvalid is called).

          Comment


          • #6
            I considered using AbstractWizardController, or more likely Spring Web Flow, but treating the entire thing like a little state machine seemed cleaner to me. I don't have a fixed page flow, I take an XML file which consists of a random number of questions of random types, some of which will only be asked depending on responses to some other question, and am supposed to step the user through it section by section.

            I could configure Spring Web Flow dynamically, using the XML file, but that's a lot of framework for such a conceptually simple task.

            Also, conceptually, I do not believe you want your "questions" hanging around in the session.
            Well, I couldn't agree more, if by 'hang out' you mean exist for more than the scope of a single form render/submit. That's why I really want Spring to manage the model object (command object in Spring MVC terminology). I'd much prefer that I not have to set it manually, but it seems there's no way to get that behavior without adding on another layer of code (ie, Web Flow or possibly AbstractWizard).

            Also, don't forget to add your errors object to the model returned from onSubmit
            Thanks, this is exactly the sort of thing I was looking for. As far as printing out global errors goes, I've stepped through the entire thing in the debugger and can confirm that the reason handleInvalidSubmit() is called the second time is simply that the model object isn't there at all -- unless of course I put it there.

            I supposed I could have two controllers just hand off back and forth, but it seems like a bigger hack to do that than to just do a little bit of patchup work on the session contents.

            --James

            Comment


            • #7
              Hello again, first off, set sessionForm to false, and then can you re-post your controller and xml, and jsp

              Comment


              • #8
                Same problem here

                I've got the same problem here. If I submit two forms really close one to the other, the first submit goes fine but the second one gives an

                Code:
                throw new SessionRequiredException("Form object not found in session (in session-form mode)");
                because the sessionFormObject is null in the AbstractFormController.getCommand() method.

                Simply put, the session object doesn't include the form object on the second submit. My guess is that since the form object gets removed from the session after a submit, the sessions must get mixed up and the first submit removes the object and therefore the second one doesn't find it's object.

                Code:
                // 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);
                Although I have no way of confirming this, what's your opinion ?

                Comment


                • #9
                  Originally posted by yatesco View Post
                  Hello again, first off, set sessionForm to false, and then can you re-post your controller and xml, and jsp
                  As a newbie in Spring I have struggled a lot with the double submission problem. This tip helped me to solve the problem.
                  There should be more information in the spring manual to handle such simple things because it isnīt so simple at the end at least when you are a newbie in Spring MVC.
                  If you for example donīt use JSF or AJAX you must have the possibility to refresh the lists by making many submits on a form and at the moment I donīt know if there are any other ways to do that when only using Spring MVC. If it is , I would be grateful to recive any examples.

                  regards
                  henrik

                  Comment


                  • #10
                    Similar Problem posting from form

                    I am a spring newbie facing a facing a similar problem and can't figure out why...

                    I have a simple .jsp page (call it home.jsp) that contains a link. I have javascript on the link that posts a form on home.jsp to another page (details.htm) . details.htm is configured with a SimpleUrlHandlerMapping as follows:

                    <pre>
                    <bean id="urlMapping" class="org.springframework.web.servlet.handler.Sim pleUrlHandlerMapping">
                    <property name="mappings">
                    <props>
                    <prop key="/hello.htm">testController</prop>
                    <prop key="/details.htm">detailsController</prop>
                    </property>
                    </bean>
                    </pre>

                    and uses the detailsController which is configured as follows:

                    <pre>
                    <bean id="detailsController" class="com.test.web.DetailsController">
                    <property name="sessionForm"><value>true</value></property>
                    <property name="commandName"><value>details</value></property>
                    <property name="commandClass"><value>com.test.app.market.Det ails</value></property>
                    <property name="validator"><ref bean="detailsValidator"/></property>
                    <property name="formView"><value>details</value></property>
                    <property name="successView"><value>hello.htm</value></property>
                    </bean>
                    </pre>

                    (note that 'hello.htm' is configured to return me to home.jsp)

                    My problem is:
                    - If I use a simple link on home.jsp to 'details.htm' on home.jsp (no form posting), it loads details.htm fine.
                    - If I use javascript to post the form on home.jsp (action = 'details.htm') and set the form method to 'GET', it loads details.htm fine
                    - BUT, if i post the form on home.jsp (action = 'details.htm') and set the method to 'POST', it callls the detailsController and ends up erroring with the handleInvalidSubmitt issue.

                    Why is it calling the detailsController?
                    Do I need to configure another controller for the home.jsp form?

                    These may be a silly question but i am a bit stuck...

                    Thanks,
                    Eric

                    Comment

                    Working...
                    X