I have a data model that is something like this:
public class Report {
// report owner
private User user;
... typical getter setter ...
}
public class User {
... omitted for clarity
}
What happens is when a report is created, the current user is set to the report user object. When the report is edited, the spring controller handling the POST request is receiving a report where the user object is null. Here is what my controller looks like:
#Controller
#RequestMapping("/report")
public class ReportController {
#RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
public String editReport(#PathVariable Long id, Model model) {
Report r = backend.getReport(id); // fully loads object
model.addAttribute("report", report);
return "report/edit";
}
#RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
public String process(#ModelAttribute("report") Report r) {
backend.save(r);
return "redirect:/report/show" + r.getId();
}
}
I ran things throw the debugger and it looks like in the editReport method the model object is storing the fully loaded report object (I can see the user inside the report). On the form jsp I can do the following:
${report.user.username}
and the correct result is rendered. However, when I look at the debugger in the process method, the passed in Report r has a null user. I don't need to do any special data binding to ensure that information is retained do I?
It seems that unless the object being edited is stored in the #SessionAttributes, then spring will instantiate a new object from the information included in the form. Tagging the controller with #SessionAttributes("report") resolved my issue. Not sure of the potential impact of doing so however.
Related
I'm trying to write a spring endpoint that generates different reports, depending on the request parameters
#GetMapping
#ResponseBody
public ResponseEntity<String> getReport(
#RequestParam(value = "category") String category,
#Valid ReportRequestDTO reportRequestDTO) {
Optional<ReportCategory> reportCategory = ReportCategory.getReportCategoryByRequest(category);
if (reportCategory.isEmpty()) {
throw new ApiRequestException("Requested report category does not exist.");
}
try {
Report report = reportFactory.getReport(reportCategory.get());
return ResponseEntity.ok().body(report.generate(reportRequestDTO));
} catch (Exception e) {
throw new ApiRequestException("Could not generate report.", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
The ReportCategory is an enum and Report is an abstract class of which multiple concrete implementations exist. Depending on the passed category the ReportFactory will instantiate the right Report. ReportRequestDTO is a class that contains all parameters that are required to generate the report. If this is passed to the generate() method, the report is generated.
Depending on the ReportCategory, different parameters may be required and need to be validated, but there can also be some common ones.
Is it possible to have an abstract class ReportRequestDTO with the common parameters and then a concrete DTO implementation for each report with its unique parameters, that is instantiated and validated depending on the report category before it is passed to the generate() method?
Edit:
I want something like this for shared parameters:
#Data
public abstract class ReportRequestDTO {
#NotEmpty
private String foo;
#NotEmpty
private String bar;
}
And then for each Report the individual parameters:
#Data
public class ReportADTO extends ReportRequestDTO {
#NotEmpty
private String foobar;
}
But I can't use and abstract class as DTO, because it can't be instantiated.
Also this would try to validate foobar even if I don't need it in ReportB.
Basically I want this endpoint to be able to generate all reports. Since I don't know yet which reports exist and may be added in the future and which parameters they require, I'd like to have the DTO extendable so that I don't have to touch the endpoint anymore and simply implement the report and create a DTO that extends ReportRequestDTO with the required parameters for that report.
So what I need is an Object that I can use as ReportRequestDTO that is extendable with all parameters for all reports so that I can pass them on the request, and then I would instantiate the DTO for the particular report with the request parameters and validate it.
You can use post-validation. I do not see why you need it for you because you can have only one input structure in the one request endpoint body. Would you like to cut the data from the request and ignore what is not used? This is also a solution anyway.
Option 1:
Inject javax.validation.Validator interface and call validate. It can be autowired. API It is just the result Set.
Option 2:
If you would like to throw exception like controller, you have to create a/more bean(s) with #Validated annotation such as:
public class ModelA {
#NotEmpty
private String text;
// getter setter
}
#Component // or use #Configuration with #Bean
#Validated
public class ReportA {
public void generate(#Valid ModelA model) { ... }
}
So I ended up changing it to a POST request and allowing a JSON body, that is then parsed to the required DTO like so:
ReportRequestDTO reportRequestDTO = report.getDto();
reportRequestDTO = new ObjectMapper().readValue(paramsJson,
reportRequestDTO.getClass());
getDTO() returns an instance of the concrete DTO that is populated with the JSON data and it is then validated as in #Numichi answer
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}.
After going through some tutorials and initial document reading from the docs.spring.org reference I understood that it is created in the controller of a POJO class created by the developer.
But while reading this I came across the paragraph below:
An #ModelAttribute on a method argument indicates the argument should be retrieved from the model. If not present in the model, the argument should be instantiated first and then added to the model. Once present in the model, the argument's fields should be populated from all request parameters that have matching names. This is known as data binding in Spring MVC, a very useful mechanism that saves you from having to parse each form field individually.
#RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(#ModelAttribute Pet pet) {
}
Spring Documentation
In the paragraph what is most disturbing is the line:
"If not present in the model ... "
How can the data be there in the model? (Because we have not created a model - it will be created by us.)
Also, I have seen a few controller methods accepting the Model type as an argument. What does that mean? Is it getting the Model created somewhere? If so who is creating it for us?
If not present in the model, the argument should be instantiated first and then added to the model.
The paragraph describes the following piece of code:
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
...
}
}
...
mavContainer.addAllAttributes(attribute);
(taken from ModelAttributeMethodProcessor#resolveArgument)
For every request, Spring initialises a ModelAndViewContainer instance which records model and view-related decisions made by HandlerMethodArgumentResolvers and HandlerMethodReturnValueHandlers during the course of invocation of a controller method.
A newly-created ModelAndViewContainer object is initially populated with flash attributes (if any):
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
It means that the argument won't be initialised if it already exists in the model.
To prove it, let's move to a practical example.
The Pet class:
public class Pet {
private String petId;
private String ownerId;
private String hiddenField;
public Pet() {
System.out.println("A new Pet instance was created!");
}
// setters and toString
}
The PetController class:
#RestController
public class PetController {
#GetMapping(value = "/internal")
public void invokeInternal(#ModelAttribute Pet pet) {
System.out.println(pet);
}
#PostMapping(value = "/owners/{ownerId}/pets/{petId}/edit")
public RedirectView editPet(#ModelAttribute Pet pet, RedirectAttributes attributes) {
System.out.println(pet);
pet.setHiddenField("XXX");
attributes.addFlashAttribute("pet", pet);
return new RedirectView("/internal");
}
}
Let's make a POST request to the URI /owners/123/pets/456/edit and see the results:
A new Pet instance was created!
Pet[456,123,null]
Pet[456,123,XXX]
A new Pet instance was created!
Spring created a ModelAndViewContainer and didn't find anything to fill the instance with (it's a request from a client; there weren't any redirects). Since the model is empty, Spring had to create a new Pet object by invoking the default constructor which printed the line.
Pet[456,123,null]
Once present in the model, the argument's fields should be populated from all request parameters that have matching names.
We printed the given Pet to make sure all the fields petId and ownerId had been bound correctly.
Pet[456,123,XXX]
We set hiddenField to check our theory and redirected to the method invokeInternal which also expects a #ModelAttribute. As we see, the second method received the instance (with own hidden value) which was created for the first method.
To answer the question i found few snippets of code with the help of #andrew answer. Which justify a ModelMap instance[a model object] is created well before our controller/handler is called for specific URL
public class ModelAndViewContainer {
private boolean ignoreDefaultModelOnRedirect = false;
#Nullable
private Object view;
private final ModelMap defaultModel = new BindingAwareModelMap();
....
.....
}
If we see the above snippet code (taken from spring-webmvc-5.0.8 jar). BindingAwareModelMap model object is created well before.
For better Understanding adding the comments for the class BindingAwareModelMap
/**
* Subclass of {#link org.springframework.ui.ExtendedModelMap} that automatically removes
* a {#link org.springframework.validation.BindingResult} object if the corresponding
* target attribute gets replaced through regular {#link Map} operations.
*
* <p>This is the class exposed to handler methods by Spring MVC, typically consumed through
* a declaration of the {#link org.springframework.ui.Model} interface. There is no need to
* build it within user code; a plain {#link org.springframework.ui.ModelMap} or even a just
* a regular {#link Map} with String keys will be good enough to return a user model.
*
#SuppressWarnings("serial")
public class BindingAwareModelMap extends ExtendedModelMap {
....
....
}
Is this possible?
For example I have a controller method like the follows with the binding result.
#RequestMapping(value = "/employee", method = RequestMethod.POST)
public String employee(#ModelAttribute #Validated(BasicDetails.class) Employee employee, BindingResult binding {
if(binding.hasErrors()){
return "failView";
}
return "successView";
}
My employee class below(I have omitted a lot of unnecessary details). The name field is part of the BasicDetails groups ,when this field fails validation(when it is empty) it appears in the errors property of binding result but I can't see any information on the group. Does the group ever get passed to binding results? Is there any nice way to access the group information in my above controller method?
public class Employee{
#Size(min=1,groups=BasicDetails.class)
public String name;
public interface BasicDetails{}
}
Thanks.
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.