Spring boot Thymeleaf same instance passed between endpoints as #ModelAttribute - java

I have a question regarding Thymeleaf and Spring Boot. I'm creating a form wizard and I would like to have the same object passed between multiple controllers, so that the object (SimpleCharacter) stores each time the value from each page.
What I have right now is, that with each endpoint, I get a new object created that "forgets" what I wanted to store from the previous page. How can I achieve that to have the same instance of the object passed between endpoints and in the end fully completed object with fields from each previous endpoint?
private static final String CHARACTER = "character";
#GetMapping(value = "/new-char/01_race")
public String showCharWizRace(Model model) {
CharacterDto character = new SimpleCharacter();
model.addAttribute(CHARACTER, character);
return "new-char/01_race";
}
#PostMapping(value = "/new-char/02_class")
public String showCharWizClass(Model model, #ModelAttribute CharacterDto character) {
model.addAttribute(CHARACTER, character);
model.addAttribute("classes", charClassService.findAll());
return "new-char/02_class";
}
#PostMapping(value = "/new-char/03_attributes")
public String showCharWizAttributes(Model model, #ModelAttribute CharacterDto character) {
model.addAttribute(CHARACTER, character);
return "new-char/03_attributes";
}
Thank you very much for all valuable hints and help. I've searched the Web, but couldn't find anything useful to point me in the right direction.
EDIT: But if you make CharacterDto have more fields for example race, class, attributes and use each time only one page (one form) to provide one field, spring "forgets" the other property when opening the next form. For example: 1st page: race is set, 2nd page (no race field existing here) class is set but in this place the previously set race had been already forgotten.
CharacterDto fields, that should be filled step by step on each page:
private String race;
private String charClass;
private int strength;
private int endurance;
private int dexterity;
private int charisma;
private int intelligence;
private int perception;
private String name;
private String surname;
private String description;
private String title;
private String avatar;

First, your character field are inside a spring form?
If yes, you also could to store your variable in a hidden field and pass this by #RequestParam.
Follow a example:
<input th:field="*{character}" name="character"/>
And in your controller method add a request parameter variable
showCharWizClass(#RequestParam(value = "character", required = false) String character, otherVariables){}
If it doesn't work, you also try to use something like a template strategy with session.
Putting your variable in a session scope, changing the variable with each request and removing it on last access.
Here a good link about access data from templates:
https://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
UPDATE
You need to combine Model and Session Attributes in your workflow pages.
In your controller add a SessionAttribute pointing to the DTO that is using, like this:
#Controller
#SessionAttributes("character")
public class WizardController { ..
And when you have finished your flow, you can end session attributes this way.
#GetMapping(value = "/new-char/04_clear")
public String clearSession(SessionStatus sessionStatus) {
sessionStatus.setComplete();
return "new-char/04_clear";
}
If you look at my example code I add a new page to clean session and restart a form with a default DTO values.

Related

#JsonIgnore only for response but not for requests

Is there a way to make #JsonIgnore annotation that it will only ignore during API or HTTP response but not ignore when doing API request.
I also understand that Jackson is used with several frameworks like Restlet, Spring, etc. so what is the generic way of doing this with the ignore annotation. The annotation class does not seem to have any parameters to set this.
Consider the code below:
public class BoxModel extends Model {
#JsonIgnore
private String entityId;
#JsonIgnore
private String secret;
}
In this example, the "secret" field should not be ignored during an API request but should not return back when doing a response, e.g. a JSON response. setting this field to null does not make the field go away, it just sets the value to null and so the field is still on the response payload.
Actually, the standard way is to have 2 separate classes for request and response, so you won't have any problem at all.
If you really need to use the same class for both cases, you can put #JsonInclude(Include.NON_NULL) onto the field instead of #JsonIgnore and set secret = null; before returning the response (as you said in question) - nullable field will be hidden after that. But it's some kind of a trick.
You could potentially find a way to achieve this using Jackson JSON Views by hiding fields when serializing the object.
Example
public class Item {
#JsonView(Views.Public.class)
public int id;
#JsonView(Views.Public.class)
public String itemName;
#JsonView(Views.Internal.class)
public String ownerName;
}
#JsonView(Views.Public.class)
#RequestMapping("/items/{id}")
public Item getItemPublic(#PathVariable int id) {
return ItemManager.getById(id);
}

Can model class implement Model UI?

So far in my Java code with Spring Boot I was using models, or POJO objects to achieve better control of my objects, etc. Usually I am creating Entities, Repositories, Services, Rest controllers, just like documentation and courses are suggesting.
Now however I am working with Thymeleaf templates, HTML a bit of Bootstrap and CSS in order to create browser interface. For methods in #Controller, as parameter, I am passing Model from Spring Model UI like this:
#GetMapping("/employees")
private String viewAllEmployees(Model employeeModel) {
employeeModel.addAttribute("listEmployees", employeeService.getAllEmployees());
return "employeeList";
}
My question is: How can I use my POJO objects instead of org.springframework.ui.Model;?
My first guess was this:
public class EmployeeModel implements Model{
private long employeeId;
private String firstName;
private String lastName;
private String email;
private String phone;
private long companyId;
//getter and setter methods
}
And in order to do that I have to #Override Model methods which is fine with me. And it looks like Java, Spring etc. does not complain in compile time, and I can use this POJO object in my #Controller like this:
#Controller
public class EmployeeController {
#Autowired
private EmployeeService employeeService;
#GetMapping("/employees")
private String viewAllEmployees(EmployeeModel employeeModel) {
employeeModel.addAttribute("listEmployees", employeeService.getAllEmployees());
return "employeeList";
}}
I run the code and it starts, shows my /home endpoint which works cool, however when I want to go to my /employees endpoing where it should show my eployees list it throws this:
Method [private java.lang.String com.bojan.thyme.thymeApp.controller.EmployeeController.viewAllEmployees(com.bojan.thyme.thymeApp.model.EmployeeModel)] with argument values:[0] [type=org.springframework.validation.support.BindingAwareModelMap] [value={}] ] with root cause java.lang.IllegalArgumentException: argument type mismatch
exception.
Please note that Rest controller is working perfectly in browser and Postman.
Is it possible that String as a method is the problem? Should my method be of some other type like List<EmployeeModel> or maybe EmployeeModel itself? If it is so, how to tell the method that I want my employeeList.html to be returned?
I sincerely hope that someone can halp me with this one :)
How can I use my POJO objects instead of org.springframework.ui.Model;?
I don't think that is the best practice when you are working with Thymeleaf. According to their documentation, you should attach your Objects to your Model. So in your controller you would be manipulating models that contain your Pojos.
Example:
#RequestMapping(value = "message", method = RequestMethod.GET)
public ModelAndView messages() {
ModelAndView mav = new ModelAndView("message/list");
mav.addObject("messages", messageRepository.findAll());
return mav;
}
You should always use org.springframework.ui.Model as argument. This class is basically a Map with key/value pairs that are made available to Thymeleaf for rendering.
Your first example is how you should do it:
#GetMapping("/employees") //<1>
private String viewAllEmployees(Model model) {
model.addAttribute("employees", employeeService.getAllEmployees()); // <2>
return "employeeList"; // <3>
}
<1> This is the URL that the view will be rendered on
<2> Add any Java object you want as attribute(s) to the model
<3> Return the name of the Thymeleaf template. In a default Spring Boot with Thymeleaf application, this will refer to the template at src/main/resources/templates/employeeList.html. In that template, you will be able to access your model value with ${employees}.

Thymeleaf form can't handle org.bson.Document type

I have an entity class with fields of type org.bson.Document. These are values that I am not allowed to modify, but when using Spring Data I need to map them in my model class so that after saving the document back to Mongo these values won't be lost. So the document is fetched from Mongo, mapped to a User POJO and then passed to a Thymeleaf form. When I try to send Thymeleaf form back to the controller I get 400 Bad Request "Validation failed for object..." error and I know it's because of these two additional Document fields. How can I pass these fields to Thymeleaf and then back to the controller? They aren't modified in the form, just appear as hidden inputs:
<input id="resetPassword" th:field="${user.resetPassword}" type="hidden"/>
<input id="consents" th:field="${user.consents}" type="hidden"/>
And my User class:
#Data
#Document(collection = "users")
#NoArgsConstructor
public class User {
#Id
private ObjectId id;
private String email;
private String name;
private String surname;
private String phone;
private String password;
private String country;
private SecurityLevel securityLevel = SecurityLevel.LOW;
private Timestamp created = Timestamp.from(Instant.now());
private Boolean blocked = false;
private org.bson.Document resetPassword;
private org.bson.Document consents;
}
It sounds like the object is being successfully injected into the Thymeleaf template, but not parsed correctly in Spring when the form is returned.
You should examine the representation in the web page (expecting json?) and then ensure that you have a handler defined in Spring that can successfully deserialise the returned object.
If the Document type does not have a conventional constructor (no-args or all-args), or some of the fields are 'hidden' (without the standard getXxx and setXxx methods), then Spring will not be able to reconstruct the object when the form is submitted without a custom handler.
Similarly, if there are not getters for all of the fields (And sub fields) of the object, the Thymeleaf template will have an incomplete object embedded that will not upload correctly.
Take a look at this blog post for some further info: https://www.rainerhahnekamp.com/en/spring-mvc-json-serialization/
I solved it by creating a custom Formatter like that:
public class BsonDocumentFormatter implements Formatter<Document> {
#Override
public Document parse(String s, Locale locale) throws ParseException {
return Document.parse(s);
}
#Override
public String print(Document document, Locale locale) {
return document.toJson();
}
}
And then I registered it in my WebMvcConfigureruration:
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new BsonDocumentFormatter());
}

Accessing object fields added to model map in jsp page

In my Spring web project, I have added a class object in to the model map. This is the class
public class ProjectDetailsBean {
private String title;
private String type;
private String addedBy;
private String status;
private String updatedOn;
private String relavantBranch;
// getters and setters are here
}
After setting all the attributes to a ProjectDetailsBean object instance, I add that to the model map in my controller class.
epb is that instance.
model.addAttribute("projectDetails", epb);
Now I need to access the fields of projectDetails in a jsp page. Those values need to be assigned to separate input fields. I cannot figure out how to do that. Could you please tell me how to do that.
Thank you!
You need to access through Expression language
${projectDetails.title}
${projectDetails.addedBy}
....... so on

JSR-303 / Spring MVC - validate conditionally using groups

I worked out a concept to conditionally validate using JSR 303 groups. "Conditionally" means that I have some fields which are only relevant if another field has a specific value.
Example: There is an option to select whether to register as a person or as a company. When selecting company, the user has to fill a field containing the name of the company.
Now I thought I use groups for that:
class RegisterForm
{
public interface BasicCheck {}
public interface UserCheck {}
public interface CompanyCheck {}
#NotNull(groups = BasicCheck.class)
private Boolean isCompany
#NotNull(groups = UserCheck.class)
private String firstName;
#NotNull(groups = UserCheck.class)
private String lastName;
#NotNull(groups = CompanyCheck.class)
private String companyName;
// getters / setters ...
}
In my controller, I validate step by step depending on the respective selection:
#Autowired
SmartValidator validator;
public void onRequest(#ModelAttribute("registerForm") RegisterForm registerForm, BindingResult result)
{
validator.validate(registerForm, result, RegisterForm.BasicCheck.class);
if (result.hasErrors()
return;
// basic check successful => we can process fields which are covered by this check
if (registerForm.getIsCompany())
{
validator.validate(registerForm, result, RegisterForm.CompanyCheck.class)
}
else
{
validator.validate(registerForm, result, RegisterForm.UserCheck.class);
}
if (!result.hasErrors())
{
// process registration
}
}
I only want to validate what must be validated. If the user selects "company" fills a field with invalid content and then switches back to "user", the invalid company related content must be ignored by the validator. A solution would be to clear those fields using Javascript, but I also want my forms to work with javascript disabled. This is why I totally like the approach shown above.
But Spring breaks this idea due to data binding. Before validation starts, Spring binds the data to registerForm. It adds error to result if, for instance, types are incompatible (expected int-value, but user filled the form with letters). This is a problem as these errors are shown in the JSP-view by <form:errors /> tags
Now I found a way to prevent Spring from adding those errors to the binding result by implementing a custom BindingErrorProcessor. If a field contains null I know that there was a validation error. In my concept null is not allowed - every field gets annotated with #NotNull plus the respective validation group.
As I am new to Spring and JSR-303 I wonder, whether I am totally on the wrong path. The fact that I have to implement a couple of things on my own makes me uncertain. Is this a clean solution? Is there a better solution for the same problem, as I think this is a common problem?
EDIT
Please see my answer here if you are interested in my solution in detail: https://stackoverflow.com/a/30500985/395879
You are correct that Spring MVC is a bit picky in this regard,and it is a common problem. But there are work-arounds:
Make all your backing fields strings, and do number/date etc conversions and null checks manually.
Use JavaScript to set fields to null when they become irrelevant.
Use JavaScript to validate fields when they are entered. This will fix almost all of your problems.
Good luck!
I know this question is old, but I came upon it looking for an answer for a different situation.
I think for your situation you could use inheritance for the forms and then use two controller methods:
The forms would look like this:
public class RegistrationForm
{
// Common fields go here.
}
public class UserRegistrationForm
extends RegistrationForm
{
#NotNull
private String firstName;
#NotNull
private String lastName;
// getters / setters ...
}
public class CompanyRegistrationForm
extends RegistrationForm
{
#NotNull
private String companyName;
// getters / setters ...
}
The controller methods would look like this:
#RequestMapping(method = RequestMethod.POST, params = "isCompany=false")
public void onRequest(
#ModelAttribute("registerForm") #Valid UserRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
#RequestMapping(method = RequestMethod.POST, params = "isCompany=true")
public void onRequest(
#ModelAttribute("registerForm") #Valid CompanyRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
Notice that the #RequestMapping annotations include a params attribute so the value of the isCompany parameter determines which method is called.
Also notice that the #Valid annotation is place on the form parameter.
Finally, no groups are needed in this case.

Categories

Resources