Spring + hibernate + form - spring

I have an User entity:
#Entity
#Table(name = "user")
public class User implements Serializable{
private enum Sex {MALE, FEMALE};
#Id #GeneratedValue
private Long id;
#NotNull
private String name;
#NotNull
private String password;
}
Controller:
#Controller
public class HomeController {
#RequestMapping("/addUser")
public String showHomePage(Map<String, Object> map) {
map.put("user", new User());
return "addUser";
}
and form in jsp file
<form:form method="post" action="add" commandName="user">
<form:input path="name"/>
<form:input path="password"/>
<form:input path="confirm-password"/>
</form:form>
This form generates error because there is no confirm-password field in User.class. I could of course add confirm-password field as #Transient to User.class, but what if I would like to add captcha field to my form. Is there a different way to add additional field to form?

It is not a good practice to use your models as form entities. You should have a bean or form class to get the data from the view and another for the model.
Reason is that they have different responsibilities thus needing to to be mapped to different classes. They are often very similar, but that separation promotes a cleaner coding and avoids security breaches, as a user could try to use your model variables from the view by using a request tamperer for example(like fire bug).
These small differences between them like the one you listed above justify the creation of another class.

Related

Change of multiple select handling in Spring Boot 2.3.1

Just faced a wired change of behavior after upgrading the Spring Boot version from 2.3.0 to 2.3.1
There is such a simple Thymleaf view with multi-select control
<div class="form-group">
<label for="roles">Roles</label>
<select multiple class="form-control" id="roles" th:field="*{roles}" required>
<option th:each="role : ${roles}"
th:text="${role.name}"
th:value="${role.id}"
th:selected="${user.roles?.contains(role)}"></option>
</select>
</div>
and POST handling controller method
#PostMapping
public String saveUser(#Valid User user, BindingResult bindingResult) {
logger.info("Save user method");
userService.save(user);
return "redirect:/user";
}
With version 2.3.0 if I submit the form with some roles selected I got them as a correct set of roles in roles field.
But with version 2.3.1 I got an incorrect list of roles. Id field in role object is empty in all of them and the name field is filled with id value. Looks like some Spring MVC or Thymleaf configuration is changed in this version but I see nothing obvious in the release notes here https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE
I understand that this problem could be resolved by implementing custom Formatter but why it works without it with Spring Boot 2.3.0.
User class is like that
public class User {
private Long id;
private String name;
private String password;
private Set<Role> roles;
// getters, setters, etc.
}
Role class
public class Role implements Serializable {
private Long id;
private String name;
// getters, setters, equals, hashcode, etc.
}

Adding a Spring Data Repository disables functionality

I have these two entities.
#Entity
public class Person {
#Id #GeneratedValue
private Long id;
private String name;
#ManyToOne(cascade=CascadeType.ALL)
private Location location;
public Person() {
}
#Entity
public class Location {
#Id #GeneratedValue
private Long id;
private String place;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "location")
private Set<Person> persons;
public Location() {
}
I also have this Controller.
#Controller
public class PersonController {
private final PersonRepository repo;
public PersonController(PersonRepository repo) {
this.repo = repo;
}
#GetMapping("/")
public String newPerson(Person person){
return "home";
}
#PostMapping("/")
public String newPerson(Person person, BindingResult result){
repo.save(person);
return "redirect:/person";
}
And this Repository.
#Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
Optional<Person> findFirstByName(String name);
}
I also have this backing form.
<form action="#" th:action="#{/}" th:object="${person}" method="post">
<table>
<tr>
<td>Name:</td>
<td><input type="text" th:field="*{name}" /></td>
</tr>
<tr>
<td>Location:</td>
<td><input type="text" th:field="*{location}" /></td>
</tr>
<tr>
<td><button type="submit">Submit</button></td>
</tr>
</table>
</form>
This all works fine when I submit some data. A Person object is saved and so is a Location object.
But when I add
#Repository
public interface LocationRepository extends JpaRepository<Location,
Long> {)
the Location object does not save to the database when I submit the same exact form. Why would just adding this repository cause this issue and what is the solution? Thanks.
You whould fix your form in order to write attribute of location property:
<td><input type="text" th:field="*{location.place}" /></td>
Also you don't have to put #Repository annotation on your repositories.
To elaborate on why things work as they work:
The form binding uses the ConversionService. Spring Data registers a conversion chain from String -> id type -> entity type for each repository managed domain class. So the moment you add a repository, the String submitted as value for Person.location will be interpreted as an identifier for an already existing location. It will cause a by-id lookup with the value submitted for the field named location.
This is handy in the following scenario: assume you're Location is basically a curated list of instances held in the database, e.g. a list of countries. They you don't want to arbitrarily create new ones but rather select one from the overall list, which basically boils down to having to use a dropdown box instead of a text field.
So conceptually, the fundamental things at odds are the cascades (as they indicate a composition, i.e. Location being part of the aggregate) and the existence of LocationRepository as a repository causes the managed type to implicitly becoming an aggregate root (which is fundamental DDD).
This in turn means you have to handle the lifecycle of that instance separately. A potential solution is to inspect the Location bound to the Person, check whether an instance with that place already exists (via a query method on LocationRepository) and if so, replace the one bound with the one loaded or just call LocationRepository.save(…) with the original instance to create a new one.
I still don't totally buy that the original attempt created a correct Location as from your template Spring Framework is not able to guess that what you submit as location is supposed to be the place actually. So I assume you saw a Location instance being created, but completely empty and the BindingResult actually carrying an error, claiming it couldn't transform the location form field into an instance of Location.

Spring form insert data in model object inside base domain model

I have a model like this:
public class User{
private int id;
private City city;
//Getter - setters
}
public class City{
private int id;
private String name;
//Getter - setters
}
Now on JSP I want to show a form for User model, which should ask for user's city in a dropdown, and corresponding city object should get stored in city object in User model.
For normal fields, I know we can write something like this:
<form:input path="name" />
But how can we bind a model object inside our base model? And how Spring will know, which object it should store in that after user selects any city?
Pretty easy, you need #modelAttribute in your controller method (or simply add it to model) on loading the form and use in your case path should be city.name
Your question is more like how to reference a submodel, spring follows the bean path, in your case, if you wanna post a city with user object, you just need to do this
<form:form method="POST" commandName="user">
<form:input path="city.name" />
<form:input path="attribute of user" />
</form:form>

How can I reload my hibernate dependent objects before de validation

I have an Spring controller with code like:
#RequestMapping("save")
public String save(#ModelAttribute #Valid Form form, BindingResult result){
if( result.hasErrors()){
[...]
My form contains a list of hibernate objects. All have their properties setted. I create an edit HTML form and in the controller I find that all the objects on the ManyToOne relationships is lost. I only have the ID. I could reload data from the database but it is too late for the validation casued by the #valid annotation.
public class Form{
#Valid
#NotNull
private List<Item> item;
#NotNull
private Foo foo;
[...]
And Item
#Entity
#Table(name = "item")
#XmlRootElement
public class Item{
#ManyToOne()
#JoinColumn(name = "dependent", referencedColumnName = "id", nullable = false)
#NotNull
private Dependent dependent;
#NotNull
private Currency currency;
How could I set the Dependent and Currency fields before the validation? Is there any alternative to reload data from the database?
(Disclaimer some names have been changes to protect the inocent)
If you are using Spring-Data-JPA you can register DomainClassConverter to do this work for you. In another case you may write such converter by yourself.
I found one way to do it:
Add to the controller a reference to SmartValidator.
#Autowired private SmartValidator validator;
Remove the #valid annotation. Reload all ManyToOne tables and call manually the validator.
#RequestMapping("save")
public String save(#ModelAttribute Form form, BindingResult result){
for(Item item : form.getItems()){
item.setDependant( myDAO.reload(item.getDependent()));
}
validator.validate(form, result);
if( result.hasErrors()){
[...]

Posting a complete model object to the controller when only few attributes are used in a form

I've read somewhere that for spring mvc, it is a expected behavior to get back NULL in case a form does not contain all the attributes of the model object set by the #ModelAttribute annotiation. S how can I use forms that don't have all the fields of the model object and still recieve the whole but updated object back to the post method of the controller.
A short example code of my intention:
Part of the controller:
....
#RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
public String editPost(Model model, #PathVariable Integer id) {
model.addAttribute("editPost", bPostService.getPost(id));
return "editPost";
}
#RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
public String editProcessPost(Model model, #PathVariable Integer id, #ModelAttribute BPost editPost) {
bPostService.updatePost(editPost);
model.addAttribute("posts", bPostService.getPosts());
return "redirect:/";
}
....
The entity mapped by hibernate:
#Entity
#Table(name = "sometable")
public class BPost {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "title")
private String title;
#Column(name = "description")
private String description;
#Column(name = "text")
private String text;
#Column(name = "anothertext")
private String anothertext;
// getters and setters
}
Part of the JSP view:
<form:form method="POST" modelAttribute="editPost" action="${pageContext.request.contextPath}/secure/post/edit/${editPost.id}">
<table>
<tbody>
<tr>
<td>title:</td>
<td><form:input path="title"></form:input></td>
</tr>
<tr>
<td>description:</td>
<td><form:input path="description"></form:input></td>
</tr>
<tr>
<td>text:</td>
<td><form:input path="text"></form:input></td>
</tr>
<tr>
<td><input value="Edit" type="submit"></td>
<td></td>
</tr>
</tbody>
</table>
</form:form>
As you can see the "anothertext" attribute is not used on the JSP, but I wan't it unchanged returned to the POST method of the controller. Is that possible?
I know some one probably already asked this question, but I cant find the answer to that.
Thank!
You might not want to use the entity as a form backing object which could have security implications. For example an malicious request could be forged to set some unwanted properties.
Therefor it's better in general to create a explicit form backing object for each form to process. It will require you to write more code but it also negates some common problems (like the one you're having).
When using a form backing object your handler looks more like:
Note that I altered the BPost argument to BPostForm.
#RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
public String editProcessPost(Model model, #PathVariable Integer id, #ModelAttribute BPostForm editPost) {
// fetch the original post
BPost post = bPostService.findById(editPost.getId());
// set the properties
post.setTitle(editPost.getTitle());
post.setDescription(editPost.getDescription());
post.setText(editPost.getText());
// update
bPostService.updatePost(post);
model.addAttribute("posts", bPostService.getPosts());
return "redirect:/";
}
P.s. Using bPostService.getPosts() to add posts to the model and immediately return a redirect seems rather pointless ;)
[EDIT] Validation
Your form backing object can be validated using declarative validation using the Hibernate annotations or settings a custom validator in the WebdataBinder.
Hibernate annotations
When using the Hibernate annotations you can place any annotation on a field or getter. For these validations to kick in you'll need to do two things.
Register a validator bean org.springframework.validation.beanvalidation.LocalValidatorFactoryBean.
Annotate the form backing object's argument in your handler with #valid.
Example: public String editProcessPost(Model model, #PathVariable Integer id, #ModelAttribute #Valid BPostForm editPost, BindingResult result)
Note that using validation needs a BindingResult to be present in the argument list and it needs to be directly after the backing object. This BindingResult will be a container for all validation errors.
Custom validator
A custom validator is a bit more work. You will need to write your own first.
MyPostValidator extends org.springframework.validation.Validator
After writing the validator you can add it to the WebDataBinder.
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new MyPostValidator());
}
It's much easier than this actually....I've been using this method for years
In your Controller class do the following:
// this method gets called by Spring after the GET/POST request but before the // binding of request parameters...
// for POST requests, we want the enity
#ModelAttribute("formName") // <---- Note the attribute name...this is important
public Object getFormBackingObject(HttpServletRequest request) {
if(!request.getMethod().equals("POST")) {
return null;
}
// find primary key
String id = request.getParameter("id");
return serviceObject.getMyEntity(id);
}
#RequestMapping(value="/edit/{id}", method=RequestMethod.POST)
public String editProcessPost(#PathVariable Integer id, #ModelAttribute("formName") BPostForm editPost) {
// editPost is the fully populated entity from the DB after request params
// have been bound to it.
myService.save(editPost);
return "whatever....";
}
As suggested by #Bart if possible replace the usage of entity pojo to form pojo directly in the jsp. If you want to continue with the existing approach you can use those fields as hidden parameters.
<form:hidden path="anothertext"/>
So when the form is getting submitted this value will be set automatically.
Here you may have another 2 issues.
Hidden Field with another value
Suppose you want to keep it as hidden value, but value should be different, then you can use like below.
<input type="hidden" name="anothertext" value ="{object.value}">
where object is any object available in the view scope.
Hidden Field As Object
What if you have have anothertext as object instead of plain text. For example if it is a User object with id and name as value and you have used like below under your entity
#OneToOne
#JoinColumn(name = "ownerName", referencedColumnName = "name")
private User owner;
In that case you have to use two values as hidden parameters. The id field of the object and the linking field (here it is name)
So it will go like below.
<form:hidden path="owner.id"/>
<form:hidden path="owner.name"/>
While persisting this back, hibernate will automatically merge with existing user from db with same id.

Resources