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 {
....
....
}
Related
I need to ignore the field when return the response from spring boot. Pls find below info,
I have one pojo called Student as below
Student {
id,
name,
lastName
}
i am getting a body for as PostRequest as below
{
id:"1",
name:"Test",
lname:"Test"
}
i want get all the data from frontEnd (id,name,Lname) But i just want to return the same pojo class without id as below,
{
name:"Test",
lName:"Test"
}
I have tried #JsonIgnore for column id, But it makes the id column as null(id=null -it is coming like this even when i send data to id field from postman) when i get the data from frontEnd.
I would like to use only one pojo to get the data with proper data(withoud getting id as Null), and need to send back the data by ignoring the id column.
Is there any way to achieve it instead of using another pojo?
You just need to use #JsonInclude(JsonInclude.Include.NON_NULL) at class level and it will be helpful for ignore all your null fields.
For example :
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Test {
// Fields
// Constructors
// Getters - setters
}
As of now you are using only one POJO it's not good practice because it's your main entity into your project, so good practice is always make DTO for the same.
This is possible via the #JsonView annotation that is part of Jackson. Spring can leverage it to define the views used on the controller.
You'd define your DTO class like this:
class User {
User(String internalId, String externalId, String name) {
this.internalId = internalId;
this.externalId = externalId;
this.name = name;
}
#JsonView(User.Views.Internal.class)
String internalId;
#JsonView(User.Views.Public.class)
String externalId;
#JsonView(User.Views.Public.class)
String name;
static class Views {
static class Public {
}
static class Internal extends Public {
}
}
}
The Views internal class acts as a marker to jackson, in order to tell it which fields to include in which configuration. It does not need to be an inner class, but that makes for a shorter code snippet to paste here. Since Internal extends Public, all fields marked with Public are also included when the Internal view is selected.
You can then define a controller like this:
#RestController
class UserController {
#GetMapping("/user/internal")
#JsonView(User.Views.Internal.class)
User getPublicUser() {
return new User("internal", "external", "john");
}
#GetMapping("/user/public")
#JsonView(User.Views.Public.class)
User getPrivateUser() {
return new User("internal", "external", "john");
}
}
Since Spring is aware of the JsonView annotations, the JSON returned by the /public endpoint will contain only externalId and name, and the /internal endpoint will additionally include the internalId field.
Note that fields with no annotation will not be included if you enable any view. This behaviour can be controlled by MapperFeature.DEFAULT_VIEW_INCLUSION, which was false in the default Spring ObjectMapper when I used this for the last time.
You can also annotate your #RequestBody parameters to controller methods with JsonView, to allow/disallow certain parameters on input objects, and then use a different set of parameters for output objects.
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
I am using the jersey implementation of JAX-RS for the web service. I am very new to this JAX-RS.
I am trying to add a method in the service which accept an Employee object and returns the employee Id based on the Employee object values (there is a DB hit for this).
Following the Restful principles, I made the method as #GET and provided the url path as shown below:
#Path("/EmployeeDetails")
public class EmployeeService {
#GET
#Path("/emp/{param}")
public Response getEmpDetails(#PathParam("param") Employee empDetails) {
//Get the employee details, get the db values and return the Employee Id.
return Response.status(200).entity("returnEmployeeId").build();
}
}
For testing purpose, I wrote this Client:
public class ServiceClient {
public static void main(String[] args) {
ClientConfig config = new DefaultClientConfig();
Client client = Client.create(config);
WebResource service = client.resource(getBaseURI());
Employee emp = new Employee();
emp.name = "Junk Name";
emp.age = "20";
System.out.println(service.path("rest").path("emp/" + emp).accept(MediaType.TEXT_PLAIN).get(String.class));
}
private static URI getBaseURI() {
return UriBuilder.fromUri("http://localhost:8045/AppName").build();
}
}
When I run it, I am getting the error: Method, public javax.ws.rs.core.Response com.rest.EmployeeService.getEmpDetails(com.model.Employee), annotated with GET of resource, class com.rest.EmployeeService, is not recognized as valid resource method.
Edit:
Model:
package com.model;
public class Employee {
public String name;
public String age;
}
Please let me know where is the issue, I am a beginner in this and struggling to understand the concepts :(
JAX-RS cannot automatically convert a #PathParam (which is a string value), into an Employee object. Requirements for objects that can be automatically created from a #PathParam are:
String (defacto, because the data is already a string)
Objects with a constructor that accepts a (single) string as argument
Objects with a static valueOf(String) method
For cases 2 & 3 the object would be required to parse the string data and populate its internal state. This is not normally done (because it forces you to make assumptions about the content type of the data). For your situation (just beginning to learn JAX-RS), its best to just accept the incoming #PathParam data as a String (or Integer, or Long).
#GET
#Path("/emp/{id}")
public Response getEmpDetails(#PathParam("id") String empId) {
return Response.status(200).entity(empId).build();
}
Passing a complex object representation to a REST service in a GET method doesn't make much sense, unless its being used, eg, as a search filter. Based on your feedback, that is what you are trying to do. I've actually done this on a project before (generic implementation of search filters), the one caveat being that you need to strictly define the format of the search data. So, lets define JSON as the accepted format (you can adapt the example to other formats as needed). The 'search object' will be passed to the service as a query parameter called filter.
#GET
#Path("/emp")
public Response getEmployees(#QueryParam("filter") String filter) {
// The filter needs to be converted to an Employee object. Use your
// favorite JSON library to convert. I will illustrate the conversion
// with Jackson, since it ships with Jersey
final Employee empTemplate = new ObjectMapper().readValue(filter, Employee.class);
// Do your database search, etc, etc
final String id = getMatchingId(empTemplate);
// return an appropriate response
return Response.status(200).entity(id).build();
}
And in your client class:
final String json = new ObjectMapper().writeValueAsString(emp);
service
.path("rest")
.path("emp")
.queryParam("filter", json)
.accept(emp, MediaType.TEXT_PLAIN)
.get(String.class)
I got the same issue but I just fixed it
jersey jars used in your application should have same versions.
It's quite strange that you want to serialize an entity (Employee) into an url path segment. I'm not saying it's impossible, but strange, and then you have to make sure that the Employee class satisfies the following criteria (which is from the javadoc of #PathParam)
The type of the annotated parameter, field or property must either:
... (irrelevant part)
Have a constructor that accepts a single String argument.
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String)).
You might want to pass the Employee object in the request body raher than in a path param. To chieve this, annotate the Employee class with #XmlRootElement,remove the #PathParam annotation from the method's empDetails argument, and change #GET to #POST.
Creating a restful api for a web application in Spring is pretty easy.
Let's say we have a Movie entity, with a name, year, list of genres and list of actors. In order to return a list of all movies in json format, we just create a method in some controller that will query a database and return a list as a body of ResponseEntity. Spring will magically serialize it, and all works great :)
But, what if I, in some case, want that list of actors in a movie to be serialized, and not in other? And in some other case, alongside the fields of the movie class, I need to add some other properties, for each movie in the list, which values are dynamically generated?
My current solution is to use #JsonIgnore on some fields or to create a MovieResponse class with fields like in Movie class and additional fields that are needed, and to convert from Movie to MovieResponse class each time.
Is there a better way to do this?
The point of the JSONIgnore annotation is to tell the DispatcherServlet (or whatever component in Spring handles rendering the response) to ignore certain fields if those fields are null or otherwise omitted.
This can provide you with some flexibility in terms of what data you expose to the client in certain cases.
Downside to JSONIgnore:
However, there are some downsides to using this annotation that I've recently encountered in my own projects. This applies mainly to the PUT method and cases where the object that your controller serializes data to is the same object that is used to store that data in the database.
The PUT method implies that you're either creating a new collection on the server or are replacing a collection on the server with the new collection you're updating.
Example of Replacing a Collection on the server:
Imagine that you're making a PUT request to your server, and the RequestBody contains a serialized Movie entity, but this Movie entity contains no actors because you've omitted them! Later on down the road, you implement a new feature that allows your users to edit and correct spelling errors in the Movie description, and you use PUT to send the Movie entity back to the server, and you update the database.
But, let's say that -- because it's been so long since you added JSONIgnore to your objects -- you've forgotten that certain fields are optional. In the client side, you forget to include the collection of actors, and now your user accidentally overwrites Movie A with actors B, C, and D, with Movie A with no actors whatsoever!
Why is JSONIgnore opt-in?
It stands to reason that the intention behind forcing you to opt-out of making certain fields required is precisely so that these types of data integrity issues are avoided. In a world where you're not using JSONIgnore, you guarantee that your data can never be replaced with partial data unless you explicitly set that data yourself. With JSONIgnore, you remove these safeguards.
With that said, JSONIgnore is very valuable, and I use it myself in precisely the same manner to reduce the size of the payload sent to the client. However, I'm beginning to rethink this strategy and instead opt for one where I use POJO classes in a separate layer for sending data to the frontend than what I use to interact with the database.
Possible Better Setup?:
The ideal setup -- from my experience dealing with this particular problem -- is to use Constructor injection for your Entity objects instead of setters. Force yourself to have to pass in every parameter at instantiation time so that your entities are never partially filled. If you try to partially fill them, the compiler stops you from doing something you may regret.
For sending data to the client side, where you may want to omit certain pieces of data, you could use a separate, disconnected entity POJO, or use a JSONObject from org.json.
When sending data from the client to the server, your frontend entity objects receive the data from the model database layer, partially or full, since you don't really care if the frontend gets partial data. But then when storing the data in the datastore, you would first fetch the already-stored object from the datastore, update its properties, and then store it back in the datastore. In other words, if you were missing the actors, it wouldn't matter because the object you're updating from the datastore already has the actors assigned to it's properties. Thus, you only replace the fields that you explicitly intend to replace.
While there would be more maintenance overhead and complexity to this setup, you would gain a powerful advantage: The Java compiler would have your back! It won't let you or even a hapless colleague do anything in the code that might compromise the data in the datastore. If you attempt to create an entity on the fly in your model layer, you'll be forced to use the constructor, and forced to provide all of the data. If you don't have all of the data and cannot instantiate the object, then you'll either need to pass empty values (which should signal a red flag to you) or fetch that data from the datastore first.
I ran into this problem, and really wanted to keep using #JsonIgnore, but also use the entities/POJO's to use in the JSON calls.
After a lot of digging I came up with the solution of automatically retrieving the ignored fields from the database, on every call of the object mapper.
Ofcourse there are some requirements which are needed for this solution. Like you have to use the repository, but in my case this works just the way I need it.
For this to work you need to make sure the ObjectMapper in MappingJackson2HttpMessageConverter is intercepted and the fields marked with #JsonIgnore are filled. Therefore we need our own MappingJackson2HttpMessageConverter bean:
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter)converter).setObjectMapper(objectMapper());
}
}
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new FillIgnoredFieldsObjectMapper();
Jackson2ObjectMapperBuilder.json().configure(objectMapper);
return objectMapper;
}
}
Each JSON request is than converted into an object by our own objectMapper, which fills the ignored fields by retrieving them from the repository:
/**
* Created by Sander Agricola on 18-3-2015.
*
* When fields or setters are marked as #JsonIgnore, the field is not read from the JSON and thus left empty in the object
* When the object is a persisted entity it might get stored without these fields and overwriting the properties
* which where set in previous calls.
*
* To overcome this property entities with ignored fields are detected. The same object is than retrieved from the
* repository and all ignored fields are copied from the database object to the new object.
*/
#Component
public class FillIgnoredFieldsObjectMapper extends ObjectMapper {
final static Logger logger = LoggerFactory.getLogger(FillIgnoredFieldsObjectMapper.class);
#Autowired
ListableBeanFactory listableBeanFactory;
#Override
protected Object _readValue(DeserializationConfig cfg, JsonParser jp, JavaType valueType) throws IOException, JsonParseException, JsonMappingException {
Object result = super._readValue(cfg, jp, valueType);
fillIgnoredFields(result);
return result;
}
#Override
protected Object _readMapAndClose(JsonParser jp, JavaType valueType) throws IOException, JsonParseException, JsonMappingException {
Object result = super._readMapAndClose(jp, valueType);
fillIgnoredFields(result);
return result;
}
/**
* Find all ignored fields in the object, and fill them with the value as it is in the database
* #param resultObject Object as it was deserialized from the JSON values
*/
public void fillIgnoredFields(Object resultObject) {
Class c = resultObject.getClass();
if (!objectIsPersistedEntity(c)) {
return;
}
List ignoredFields = findIgnoredFields(c);
if (ignoredFields.isEmpty()) {
return;
}
Field idField = findIdField(c);
if (idField == null || getValue(resultObject, idField) == null) {
return;
}
CrudRepository repository = findRepositoryForClass(c);
if (repository == null) {
return;
}
//All lights are green: fill the ignored fields with the persisted values
fillIgnoredFields(resultObject, ignoredFields, idField, repository);
}
/**
* Fill the ignored fields with the persisted values
*
* #param object Object as it was deserialized from the JSON values
* #param ignoredFields List with fields which are marked as JsonIgnore
* #param idField The id field of the entity
* #param repository The repository for the entity
*/
private void fillIgnoredFields(Object object, List ignoredFields, Field idField, CrudRepository repository) {
logger.debug("Object {} contains fields with #JsonIgnore annotations, retrieving their value from database", object.getClass().getName());
try {
Object storedObject = getStoredObject(getValue(object, idField), repository);
if (storedObject == null) {
return;
}
for (Field field : ignoredFields) {
field.set(object, getValue(storedObject, field));
}
} catch (IllegalAccessException e) {
logger.error("Unable to fill ignored fields", e);
}
}
/**
* Get the persisted object from database.
*
* #param id The id of the object (most of the time an int or string)
* #param repository The The repository for the entity
* #return The object as it is in the database
* #throws IllegalAccessException
*/
#SuppressWarnings("unchecked")
private Object getStoredObject(Object id, CrudRepository repository) throws IllegalAccessException {
return repository.findOne((Serializable)id);
}
/**
* Get the value of a field for an object
*
* #param object Object with values
* #param field The field we want to retrieve
* #return The value of the field in the object
*/
private Object getValue(Object object, Field field) {
try {
field.setAccessible(true);
return field.get(object);
} catch (IllegalAccessException e) {
logger.error("Unable to access field value", e);
return null;
}
}
/**
* Test if the object is a persisted entity
* #param c The class of the object
* #return true when it has an #Entity annotation
*/
private boolean objectIsPersistedEntity(Class c) {
return c.isAnnotationPresent(Entity.class);
}
/**
* Find the right repository for the class. Needed to retrieve the persisted object from database
*
* #param c The class of the object
* #return The (Crud)repository for the class.
*/
private CrudRepository findRepositoryForClass(Class c) {
return (CrudRepository)new Repositories(listableBeanFactory).getRepositoryFor(c);
}
/**
* Find the Id field of the object, the Id field is the field with the #Id annotation
*
* #param c The class of the object
* #return the id field
*/
private Field findIdField(Class c) {
for (Field field : c.getDeclaredFields()) {
if (field.isAnnotationPresent(Id.class)) {
return field;
}
}
return null;
}
/**
* Find a list of all fields which are ignored by json.
* In some cases the field itself is not ignored, but the setter is. In this case this field is also returned.
*
* #param c The class of the object
* #return List with ignored fields
*/
private List findIgnoredFields(Class c) {
List ignoredFields = new ArrayList();
for (Field field : c.getDeclaredFields()) {
//Test if the field is ignored, or the setter is ignored.
//When the field is ignored it might be overridden by the setter (by adding #JsonProperty to the setter)
if (fieldIsIgnored(field) ? setterDoesNotOverrideIgnore(field) : setterIsIgnored(field)) {
ignoredFields.add(field);
}
}
return ignoredFields;
}
/**
* #param field The field we want to retrieve
* #return True when the field is ignored by json
*/
private boolean fieldIsIgnored(Field field) {
return field.isAnnotationPresent(JsonIgnore.class);
}
/**
* #param field The field we want to retrieve
* #return true when the setter is ignored by json
*/
private boolean setterIsIgnored(Field field) {
return annotationPresentAtSetter(field, JsonIgnore.class);
}
/**
* #param field The field we want to retrieve
* #return true when the setter is NOT ignored by json, overriding the property of the field.
*/
private boolean setterDoesNotOverrideIgnore(Field field) {
return !annotationPresentAtSetter(field, JsonProperty.class);
}
/**
* Test if an annotation is present at the setter of a field.
*
* #param field The field we want to retrieve
* #param annotation The annotation looking for
* #return true when the annotation is present
*/
private boolean annotationPresentAtSetter(Field field, Class annotation) {
try {
Method setter = getSetterForField(field);
return setter.isAnnotationPresent(annotation);
} catch (NoSuchMethodException e) {
return false;
}
}
/**
* Get the setter for the field. The setter is found based on the name with "set" in front of it.
* The type of the field must be the only parameter for the method
*
* #param field The field we want to retrieve
* #return Setter for the field
* #throws NoSuchMethodException
*/
#SuppressWarnings("unchecked")
private Method getSetterForField(Field field) throws NoSuchMethodException {
Class c = field.getDeclaringClass();
return c.getDeclaredMethod(getSetterName(field.getName()), field.getType());
}
/**
* Build the setter name for a fieldName.
* The Setter name is the name of the field with "set" in front of it. The first character of the field
* is set to uppercase;
*
* #param fieldName The name of the field
* #return The name of the setter
*/
private String getSetterName(String fieldName) {
return String.format("set%C%s", fieldName.charAt(0), fieldName.substring(1));
}
}
Maybe not the most clean solution in all cases, but in my case it does the trick just the way I want it to work.
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.