Announcement Announcement Module
Collapse
No announcement yet.
Returnn Default Value on BindResult Error Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Returnn Default Value on BindResult Error

    Hello,

    I am working on an app which has a controller (configured via annotations). One feature I am trying to do is that if a user leaves a field blank then I catch it in my validator, register the error and then set a default value in the backing object based upon some data in another property of the backing object.

    I am currently trying this:
    Note - returnVal is the result of a call which is determined here to be a binding result. The return val will have a field error in name if the user left the name blank.
    <code>
    BindingResult br = (BindingResult) returnVal;
    if (br.getFieldError("name") != null) {
    ModelAndView mav = new ModelAndView();
    mav.getModel().putAll(br.getModel());

    logger.debug("error in name");
    TransformationExperiment mavTE = (TransformationExperiment) mav.getModel().get("transformationExperiment");

    if (mavTE.getTransformation() != null) {
    mavTE.setName("EXP-" + mavTE.getTransformation().getTransformationId());
    } else {
    mavTE.setName("EXP");
    }
    logger.debug("name set to - " + ((TransformationExperiment) mav.getModel().get("transformationExperiment")).ge tName());
    returnVal = mav;
    }
    </code>

    However, this does not work. Any values which were bound from the original form (as in html form) appear in the new form. However the name which have just set and added into the ModelAndView does not change.

    I have also tried changing the value passed into the method via a @ModelAttribute and also trying to change the value in the session attribute (as this needs to store the value between the GET and the POST I have @SessionAttributes("transformationExperiment") at the top of my class). These have not worked either. I have also tried hacking the value into my validator (bad practice - didn't work anyway).

    Has anyone done this, if so could you please give some tips as how to achieve this task?

    Thanks, in advance, for your help.

    Cheers,

    Neil
    Last edited by neil.benn; Aug 15th, 2008, 07:39 AM. Reason: Heading typo

  • #2
    For this forum, to format code it's
    PHP Code:
    [code] ... [/code
    not
    PHP Code:
    <code> ... </code

    Comment


    • #3
      And could you post the entire method, including its declaration (and annotations), instead of just its guts?

      Comment


      • #4
        Complete code

        Hello,

        Sorry about the mistake with the code tags, here is the complete method (it is quite long as it does a bunch of stuff with storing uploaded binary files):

        Code:
        	@SuppressWarnings("unchecked")
        	@RequestMapping(method = RequestMethod.POST)
        	public Object onSubmit(
        			@ModelAttribute("transformationExperiment") TransformationExperiment transformationExperiment,
        			BindingResult result,
        			HttpServletRequest request,
        			@RequestParam(value = "transformationId", required = true) String transformationId,
        			@RequestParam(value = "cancel", required = false) String cancel,
        			@RequestParam(value = "delete", required = false) String delete)
        			throws IOException {
        
        		List<StoredImage> imgsToRemove = new ArrayList<StoredImage>();
        		// only process the inputs if the user has not clicked cancel or delete
        		if (StringUtils.isBlank(cancel) && StringUtils.isBlank(delete)) {
        			logger.debug("params");
        
        			String[] paramValues = request.getParameterValues("removeFile");
        			if (paramValues != null) {
        				// remove files named by the parameter
        				for (String name : request.getParameterValues("removeFile")) {
        					StoredImage toRemove = null;
        					for (StoredImage img : transformationExperiment
        							.getStoredImages()) {
        						if (img.getSourceBaseName().equals(name)) {
        							toRemove = img;
        							break;
        						}
        					}
        					if (toRemove == null) {
        						throw new IllegalArgumentException("image with name"
        								+ name + " not found");
        					} else {
        						transformationExperiment.getStoredImages().remove(
        								toRemove);
        						logger.debug("removed " + toRemove.getSourceBaseName());
        						imgsToRemove.add(toRemove);
        						toRemove = null;
        					}
        				}
        			}
        
        			Set<StoredImage> imgs = new HashSet<StoredImage>();
        			MultipartHttpServletRequest mulReq = (MultipartHttpServletRequest) request;
        			Iterator<?> oIt = mulReq.getFileNames();
        			while (oIt.hasNext()) {
        				String fileName = (String) oIt.next();
        				MultipartFile mpf = mulReq.getFile(fileName);
        				if (mpf.getSize() == 0) {
        					continue;
        				}
        				StoredImage si = multipartToStoredImage(mpf);
        				imgs.add(si);
        				logger.debug("file name = " + fileName);
        			}
        
        			transformationExperiment.getStoredImages().addAll(imgs);
        
        			logger.debug("transformation files "
        					+ transformationExperiment.getStoredImages());
        
        			transformationExperiment.setTransformation(transformationService
        					.getByTransformationId(transformationId));
        		}
        		Object returnVal = super.onSubmit(transformationExperiment, result,
        				cancel, delete);
        		// if there was no name then fill in a default, note that a default
        		// name is specified and mentioned in the validator. This is hardcoded
        		// for now
        		if (returnVal instanceof BindingResult) {
        
        			logger.debug("super returned a binding result");
        			BindingResult br = (BindingResult) returnVal;
        			if (br.getFieldError("name") != null) {
        				ModelAndView mav = new ModelAndView();
        				mav.getModel().putAll(br.getModel());
        
        				logger.debug("error in name");
        				TransformationExperiment mavTE = (TransformationExperiment) mav
        						.getModel().get("transformationExperiment");
        
        				if (mavTE.getTransformation() != null) {
        					mavTE.setName("EXP-"
        							+ mavTE.getTransformation().getTransformationId());
        				} else {
        					mavTE.setName("EXP");
        				}
        				logger.debug("name set to - " + ((TransformationExperiment) mav
        						.getModel().get("transformationExperiment")).getName());
        				returnVal = mav;
        			}
        		}
        		
        		if (imgsToRemove.size() > 0) {
        			for (StoredImage si : imgsToRemove) {
        				try {
        					this.storedImageService.deleteData(si);
        				} catch (Exception e) {
        					logger.warn("failed to remove file");
        				}
        			}
        		}
        		return returnVal;
        	}
        I did some further investigation, what is interesting is that the corrected transformationExperiment object with the correct name actually _does_ appear in the jsp page. I checked this out by logging the object, here is the jsp page:
        Code:
        <%@ include file="../common/include.jsp"%>
        
        <script type="text/javascript" language="javascript" src="<c:url value='/js/addFile.js'/>"></script>
        <script type="text/javascript" language="javascript" src="<c:url value='/js/modifyFile.js'/>"></script>
        
        <title><fmt:message key="editTransExp.title" /></title>
        <content tag="heading">
        <fmt:message key="editTransExp.edit" />
        </content>
        <log:debug category="de.mpicbg.sweng.jsp">
        ${transformationExperiment}
        </log:debug>
        <form:form name="formEditTransformationExperiment" 
        	commandName="transformationExperiment" enctype="multipart/form-data">
        <form:errors path="*" cssClass="errorBox" />
        <div class="contentelement">
        <table class="edit">
        
            <tr>
                <th><fmt:message key="transexp.name" /></th>
                <td><form:input path="name" cssClass="text"/></td>
            </tr>
        
            <tr>
                <th><fmt:message key="transexp.color" /></th>
                <td><form:select path="color" cssClass="input" items="${options.color}"/></td>
            </tr>
        --cutting out a load of superfluous stuff--
            <tr>
                <th><fmt:message key="generic.text.description" /></th>
                <td><form:textarea path="description" cssClass="text" rows="8" /></td>
            </tr>
        
        --cutting out a load of superfluous stuff--
        </form:form>
        If I replace the line :
        Code:
        <td><form:input path="name" cssClass="text"/></td>
        with
        [code]
        <td><input class="text" type="text" name="name" value="${transformationExperiment.name}"></td>
        [/cpde]
        it works fine. In addition, if I try and set a different parameter in my onSubmit method, such as description this also gets propagated when the page is resent to the client. Only name does not.

        For now I have the <em>raw</em> html in there which is a bit of a dirty hack but I'd like to get to the bottom of what is going wrong here.

        Well, that was a rather long message - sorry for that. If anyone has any ideas I'd be v. grateful.

        Cheers,

        Neil

        Comment


        • #5
          Sorry, my dinky little brain is having trouble digesting all that code.

          Some questions as to style pop up though. For example, why are you doing
          Code:
          for (String name : request.getParameterValues("removeFile")) {
          It seems to me that instead of getParameterValues you should be able to do in your onSubmit parameter list
          Code:
          @RequestParam(value = "removeFiles", required = false) String[] removeFiles)
          And then replace
          Code:
          String[] paramValues = request.getParameterValues("removeFile");
          if (paramValues != null) {
              // remove files named by the parameter
              for (String name : request.getParameterValues("removeFile")) {
          with
          Code:
              if (removeFiles != null) {
                  for (String name : removeFiles)) {
          And then in that for loop where you have the 'else'
          Code:
          } else {
              transformationExperiment.getStoredImages().remove(toRemove);
              logger.debug("removed " + toRemove.getSourceBaseName());
              imgsToRemove.add(toRemove);
              toRemove = null;
          }
          You could move that up to the part where you do "toRemove = img;" and no need to set toRemove to null again since it's done at the top of the loop. So you only have the "if" that throws.

          This is all stylistic. Yet another general stylistic thing I'd do is to use the "refactoring pattern" (gotta love those java buzz phrases) called "extract method", which in eclipse is simply a matter of dragging your mouse across the code and selecting extract method. That first for loop is building a list of files to remove; make that a method. Then there's the stuff with the MultipartHttpServletRequest which can probably also be turned into a method. And then the code controlled by the if test for instanceof can be moved into a routine, including the if.

          Not understanding exactly how the code works I can't say what is wrong, but I will say that the super.onSubmit is throwing red flags for me, likewise for it returning Object and the instanceof test.

          Comment


          • #6
            Default Value on Binding Error

            Hello,

            That is quite funny because I was refactoring that method to do similar things to which you have suggested - I pulled this version in because it is all one method and therefore a bit easier to read! However, thanks for the tips - I had missed declaring the string array as a parameter as I overlooked that a foreach loop will simply skip if the item being iterated over is null (which is why I missed out the @RequestParam in the first place). Anyways to elucidate and fill in missing gaps, the super method is below:

            Code:
            public Object onSubmit(TransformationExperiment transformationExperiment,
            		BindingResult result, String cancel, String delete) {
            
            	if (cancel != null) {
            		logger.debug("cancel");
            		return AbstractTransformationExperimentFormController.CANCEL_REDIRECT;
            	}
            	if (delete != null) {
            		logger.debug("delete");
            		transformationExperimentService.delete(transformationExperiment);
            		return AbstractTransformationExperimentFormController.DELETE_REDIRECT;
            	}
            
                   transformationExperimentValidator.validate(transformationExperiment,
            			result);
            	if (result.hasErrors()) {
            		logger.debug("returning errors");
            		logger.debug(result.getModel());
            		return result;
            	}
            	logger.debug("success");
            	transformationExperimentService.saveOrUpdate(transformationExperiment);
            	return AbstractTransformationExperimentFormController.SUCCESS_REDIRECT;
            }
            So, (SUCCESS|CANCEL|DELETE)_REDIRECT are strings of the format redirect:/listTransformationExperiments.html. If this is returned then the onSubmit method will redirect, typically if I return a binding result then the errors are shown and the user is redirected to the same view they are on with the errors rendered. However in this case, I need to modify teh backing object so I detect if the super (through the validator) returns a binding result - if it did then I get the contents out of the binding result, modify the backing object and then return a ModelAndView with no explicit view (so it stays on the same page). This does render the errors and the log statement in the jsp correctly logs the modified backing object via the log:debug call. However, the form tag in spring does not pick this up. Interestingly, only the name is not picked up - other properties such as description are showing up. I am thinking that this could be a thing to do with the errors, the error is in the name field and it is the name field which is giving me problems.
            Cheers,

            Neil
            Last edited by neil.benn; Aug 19th, 2008, 03:27 AM. Reason: typo

            Comment


            • #7
              This is just a guess, but I've always thought of the Model and ModelAndView as being a one way street; you can put things in them but you can't take things out or modify things that are in them (although you can modify things you just put in them, in the current method, but I prefer setting them up before I add them).

              And, in particular, it doesn't feel safe to me to fiddle with the Model that's in the BindingResult.

              The documentation (in the reference guide) for @ModelAttribute says:
              @ModelAttribute is also used at the method level to provide reference data for the model (see the populatePetTypes() method below). For this usage the method signature can contain the same types as documented above for the @RequestMapping annotation.
              So if you're using a method that's annotated with @ModelAttribute for providing your form backing data, could you give it a BindingResult parameter and look at it to fiddle with the form backing object you return? I've never thought of doing that but who knows.

              Comment

              Working...
              X