i have a PATCH mapping method that currently accepts a Map<String, Object>
as @ResponseBody
, because i've seen ReflectionUtils
as one of ways to implement the PatchMapping, but when I want to pass for example String
, Integer
and Long
, then JSON doesn't distinguish which one are which one. I would like to be able to pass EntityID which is Long, some Value which is Integer and some Strings at once.
This is my current Controller method
@PatchMapping("/grades/{id}")
public GradeEto partialUpdate(@PathVariable("id") final Long id, @RequestBody Map<String, Object> updateInfo) {
return gradeService.partialUpdate(id, updateInfo);
}
This is main part of the ReflectionUtils
update
@Override
public GradeEto partialUpdate(Long id, Map<String, Object> updateInfo) {
Grade grade = gradeRepository.findById(id).get();
GradeEto gradeeto = GradeMapper.mapToETO(grade);
updateInfo.forEach((key, value) -> {
Field field = ReflectionUtils.findField(GradeEto.class, key);
field.setAccessible(true);
ReflectionUtils.setField(field, gradeeto, value);
});
And i want to be able to pass for example
{
"value": 2, <-- Integer
"comment": "Test Idea", <-- String
"subjectEntityId": 2 <-- Long
}
But it gives me IllegalArgumentException
currently
How should i do it? And if im doing it the wrong way then how should I do it?
CodePudding user response:
You can make use of the functionality offered by Jackson to perform the partial update.
For that, instead of reading the data to patch as a Map
you specify in your Controller the type of the request body as ObjectNode
:
@PatchMapping("/grades/{id}")
public GradeEto partialUpdate(@PathVariable("id") final Long id,
@RequestBody ObjectNode updateInfo) {
return gradeService.partialUpdate(id, updateInfo);
}
Then inside the Service to operate with ObjectNode
s you need to turn GradeEto
instance into a node tree using ObjectMapper.valueToTree()
(assuming that Service class is a managed Bean you can inject ObjectMapper
into it instead of instantiating it locally).
Method ObjectNode.fields()
can be used to iterate over the fields and corresponding values of updateInfo
. Methods isNull()
and isEmpty()
, which ObjectNode
derives from its super types, would be handy to check if a particular property needs to be updated.
Then, finally, you would need to parse the ObjectNode
back into GradeEto
type.
So the code in the Service might look like this:
@Service
public class GradeService {
private ObjectMapper mapper;
public GradeEto partialUpdate(Long id, ObjectNode updateInfo) throws IOException {
Grade grade = gradeRepository.findById(id).get();
GradeEto gradeeto = GradeMapper.mapToETO(grade);
ObjectNode gradeEtoTree = mapper.valueToTree(gradeeto);
updateInfo.fields().forEachRemaining(e -> {
if (!e.getValue().isNull() && !e.getValue().isEmpty())
gradeEtoTree.replace(e.getKey(), e.getValue());
});
return mapper
.readerFor(GradeEto.class) // creates an ObjectReader
.readValue(gradeEtoTree);
}
}
Note: it's worth to draw the reader's attention to the fact we also need to update the information in the Database. This logic is missing in both the Question and this Answer, since it's not relevant to the narrow technical problem of extracting the data that should be patched from JSON, but we definitely need to save the patched object (for instance a method that produces patched object can be wrapped with another method responsible for retrieving/updating the data in the database).
CodePudding user response:
I do believe org.springframework.cglib.beans.BeanMap
can solve your problem in elegant way:
public static void main(String[] args) throws Exception {
String json = "{\n"
" \"value\": 2,\n"
" \"comment\": \"Test Idea\",\n"
" \"subjectEntityId\": 20\n"
"}";
Grade grade = new Grade();
BeanMap map = BeanMap.create(grade);
map.putAll(new ObjectMapper().readValue(json, Map.class));
assertThat(grade.value)
.isEqualTo(2);
assertThat(grade.subjectEntityId)
.isEqualTo(20);
assertThat(grade.comment)
.isEqualTo("Test Idea");
}
@Data
static class Grade {
int value;
String comment;
long subjectEntityId;
}