3

[This is a list of common questions I see about Spring MVC, which are solved in similar ways. I've posted them here, so I can easily refer to them from other questions]

How do I update only a few fields of a model entity with forms?

How do I use the Post-Redirect-Get pattern with Spring MVC, especially with form validation?

How do I secure certain fields in my entities?

How do I implement Optimistic Concurrency Control?

Neil McGuigan
  • 46,580
  • 12
  • 123
  • 152
  • Please have a look at this question as well http://stackoverflow.com/questions/28845387/how-to-handle-addressing-to-static-pages-in-spring-mvc – Jack Mar 17 '15 at 01:29

1 Answers1

14
  1. To partially update an entity, you should use @SessionAttributes to store the model in session between requests. You could use hidden form fields, but session is more secure.

  2. To use P/R/G with validation, use flashAttributes

  3. To secure fields use webDataBinder.setAllowedFields("field1","field2",...) or create a class specific to the form then copy values to your entity. Entities don't require setters for id and version (if using Hibernate).

  4. To use Optimistic Concurrency Control use the @Version annotation in your Entity and use @SessionAttributes on your controller.

Example code:

@Controller
@RequestMapping("/foo/edit/{id}")
@SessionAttributes({FooEditController.ATTRIBUTE_NAME})
public class FooEditController {

    static final String ATTRIBUTE_NAME = "foo";
    static final String BINDING_RESULT_NAME = "org.springframework.validation.BindingResult." + ATTRIBUTE_NAME;

    @Autowired
    private FooRepository fooRepository;

    /*
     Without this, user can set any Foo fields they want with a custom HTTP POST
     setAllowedFields disallows all other fields. 
     You don't even need setters for id and version, as Hibernate sets them using reflection
    */
    @InitBinder
    void allowFields(WebDataBinder webDataBinder){
        webDataBinder.setAllowedFields("name"); 
    }

    /*
     Get the edit form, or get the edit form with validation errors
    */
    @RequestMapping(method = RequestMethod.GET)
    String getForm(@PathVariable("id") long id, Model model) {

        /* if "fresh" GET (ie, not redirect w validation errors): */
        if(!model.containsAttribute(BINDING_RESULT_NAME)) {
            Foo foo = fooRepository.findOne(id);
            if(foo == null) throw new ResourceNotFoundException();
            model.addAttribute(ATTRIBUTE_NAME, foo);
        }

        return "foo/edit-form";
    }

    /*
     @Validated is better than @Valid as it can handle http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-groups.html
     @ModelAttribute will load Foo from session but also set values from the form post
     BindingResult contains validation errors
     RedirectAttribute.addFlashAttribute() lets you put stuff in session for ONE request
     SessionStatus lets you clear your SessionAttributes
    */
    @RequestMapping(method = RequestMethod.POST)
    String saveForm(
       @Validated @ModelAttribute(ATTRIBUTE_NAME) Foo foo,
       BindingResult bindingResult, 
       RedirectAttributes redirectAttributes, 
       HttpServletRequest request, 
       SessionStatus sessionStatus
    ) {

        if(!bindingResult.hasErrors()) {
            try {
                fooRepository.save(foo);
            } catch (JpaOptimisticLockingFailureException exp){
                bindingResult.reject("", "This record was modified by another user. Try refreshing the page.");
            }
        }

        if(bindingResult.hasErrors()) {

            //put the validation errors in Flash session and redirect to self
            redirectAttributes.addFlashAttribute(BINDING_RESULT_NAME, bindingResult);
            return "redirect:" + request.getRequestURI();
        }

        sessionStatus.setComplete(); //remove Foo from session

        redirectAttributes.addFlashAttribute("message", "Success. The record was saved");
        return "redirect:" + request.getRequestURI();
    }
}

Foo.java:

@Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version //for optimistic concurrency control
    private int version;

    @NotBlank
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

edit-form.jsp (Twitter Bootstrap compatible):

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

<form:form modelAttribute="foo">

    <spring:hasBindErrors name="foo">
        <c:if test="${errors.globalErrorCount > 0}">
            <div class="alert alert-danger" role="alert"><form:errors/></div>
        </c:if>
    </spring:hasBindErrors>

    <c:if test="${not empty message}">
      <div class="alert alert-success"><c:out value="${message}"/></div>
    </c:if>

    <div class="panel panel-default">
        <div class="panel-heading">
            <button class="btn btn-primary" name="btnSave">Save</button>
        </div>

        <div class="panel-body">

            <spring:bind path="name">
                <div class="form-group${status.error?' has-error':''}">
                    <form:label path="name" class="control-label">Name <form:errors path="name"/></form:label>
                    <form:input path="name" class="form-control" />
                </div>
            </spring:bind>

        </div>
    </div>

</form:form>

ResourceNotFoundException.java:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
Neil McGuigan
  • 46,580
  • 12
  • 123
  • 152
  • Thanks for your answer, however, I have a few problems. First issue in my case just email address will be verified so if I do not type a valid email address it would show a little message otherwise it wont, I can even submit a blank form to server. Also should I show the successful submission message in the way that I did? – Jack Mar 17 '15 at 00:00
  • @Jack use `@NotBlank` and `@Email` validations from Hibernate Validator. For success message, set a flash attribute called "message" or similar, then display that message after redirect in the jsp – Neil McGuigan Mar 17 '15 at 00:06
  • Make sure Hibernate Validator is setup correctly. In maven: ` org.hibernate hibernate-validator 5.1.2.Final ` – Neil McGuigan Mar 17 '15 at 00:23
  • I got it it is not persisting the form;however, does not show the error messages. – Jack Mar 17 '15 at 00:27
  • It works now many thanks, may I know how to customize the messages? Also how to have contraints on a field that is optional? on one of the fields I have @Digits(fraction = 0, integer = 14) @Size(min = 10, max = 14) and if I dont enter any value it shows size must be between 7and 14 numeric value out of bounds (<14 digits>.<0 digits> expected) however the field is optional and only when it is entered the contraints should be applied. – Jack Mar 17 '15 at 00:45
  • read these: http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/ , http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#validation-beanvalidation – Neil McGuigan Mar 17 '15 at 00:47
  • Thanks, the last question is, as I am not using EscapeXML for it and the action value is added automatically, should I do anything to secure the form? – Jack Mar 17 '15 at 00:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/73121/discussion-between-neil-mcguigan-and-jack). – Neil McGuigan Mar 17 '15 at 00:49
  • This helped me heaps. Thank you. I would like more of an explanation about SessionStatus. – Al Grant Apr 11 '17 at 20:46
  • `"redirect:" + request.getRequestURI();` didn't work in my case, it resulted in the servlet context appearing twice in the HTTP 302 response header so it gave my browser a 404. Instead, I had to do `"redirect:" + "/.." + request.getRequestURI();` – Marcus Junius Brutus May 24 '17 at 17:34