We are trying to implement a special partial update function in Spring 3.2. We are using Spring for the backend and have a simple Javascript frontend. I've not been able to find a straight-forward solution to our requirements, which is The update() function should take in any number of field:values and update the persistence model accordingly.
We have in-line editing for all of our fields, so that when the user edits a field and confirms, an id and the modified field get passed to the controller as json. The controller should be able to take in any number of fields from the client (1 to n) and update only those fields.
e.g., when a user with id==1 edits his displayName, the data posted to the server looks like this:
{"id":"1", "displayName":"jim"}
Currently, we have an incomplete solution in the UserController as outlined below:
#RequestMapping(value = "/{id}", method = RequestMethod.POST )
public #ResponseBody ResponseEntity<User> update(#RequestBody User updateUser) {
dbUser = userRepository.findOne(updateUser.getId());
customObjectMerger(updateUser, dbUser);
userRepository.saveAndFlush(updateUuser);
...
}
The code here works, but has some issues: The #RequestBody creates a new updateUser, fills in the id and the displayName. CustomObjectMerger merges this updateUser with the corresponding dbUser from the database, updating the only fields included in updateUser.
The problem is that Spring populates some fields in updateUser with default values and other auto-generated field values, which, upon merging, overwrites valid data that we have in dbUser. Explicitly declaring that it should ignore these fields is not an option, as we want our update to be able to set these fields as well.
I am looking into some way to have Spring automatically merge ONLY the information explicitly sent into the update() function into the dbUser (without resetting default/auto field values). Is there any simple way to do this?
Update: I've already considered the following option which does almost what I'm asking for, but not quite. The problem is that it takes update data in as #RequestParam and (AFAIK) doesn't do JSON strings:
//load the existing user into the model for injecting into the update function
#ModelAttribute("user")
public User addUser(#RequestParam(required=false) Long id){
if (id != null) return userRepository.findOne(id);
return null;
}
....
//method declaration for using #MethodAttribute to pre-populate the template object
#RequestMapping(value = "/{id}", method = RequestMethod.POST )
public #ResponseBody ResponseEntity<User> update(#ModelAttribute("user") User updateUser){
....
}
I've considered re-writing my customObjectMerger() to work more appropriately with JSON, counting and having it take into consideration only the fields coming in from HttpServletRequest. but even having to use a customObjectMerger() in the first place feels hacky when spring provides almost exactly what I am looking, minus the lacking JSON functionality. If anyone knows of how to get Spring to do this, I'd greatly appreciate it!
I've just run into this same problem. My current solution looks like this. I haven't done much testing yet, but upon initial inspection it looks to be working fairly well.
#Autowired ObjectMapper objectMapper;
#Autowired UserRepository userRepository;
#RequestMapping(value = "/{id}", method = RequestMethod.POST )
public #ResponseBody ResponseEntity<User> update(#PathVariable Long id, HttpServletRequest request) throws IOException
{
User user = userRepository.findOne(id);
User updatedUser = objectMapper.readerForUpdating(user).readValue(request.getReader());
userRepository.saveAndFlush(updatedUser);
return new ResponseEntity<>(updatedUser, HttpStatus.ACCEPTED);
}
The ObjectMapper is a bean of type org.codehaus.jackson.map.ObjectMapper.
Hope this helps someone,
Edit:
Have run into issues with child objects. If a child object receives a property to partially update it will create a fresh object, update that property, and set it. This erases all the other properties on that object. I'll update if I come across a clean solution.
We are using #ModelAttribute to achive what you want to do.
Create a method annotated with#modelattribute which loads a user based on a pathvariable throguh a repository.
create a method #Requestmapping with a param #modelattribute
The point here is that the #modelattribute method is the initializer for the model. Then spring merges the request with this model since we declare it in the #requestmapping method.
This gives you partial update functionality.
Some , or even alot? ;) would argue that this is bad practice anyway since we use our DAOs directly in the controller and do not do this merge in a dedicated service layer. But currently we did not ran into issues because of this aproach.
I build an API that merge view objects with entities before call persiste or merge or update.
It's a first version but I think It's a start.
Just use the annotation UIAttribute in your POJO`S fields then use:
MergerProcessor.merge(pojoUi, pojoDb);
It works with native Attributes and Collection.
git: https://github.com/nfrpaiva/ui-merge
Following approach could be used.
For this scenario, PATCH method would be more appropriate since the entity will be partially updated.
In controller method, take the request body as string.
Convert that String to JSONObject. Then iterate over the keys and update matching variable with the incoming data.
import org.json.JSONObject;
#RequestMapping(value = "/{id}", method = RequestMethod.PATCH )
public ResponseEntity<?> updateUserPartially(#RequestBody String rawJson, #PathVariable long id){
dbUser = userRepository.findOne(id);
JSONObject json = new JSONObject(rawJson);
Iterator<String> it = json.keySet().iterator();
while(it.hasNext()){
String key = it.next();
switch(key){
case "displayName":
dbUser.setDisplayName(json.get(key));
break;
case "....":
....
}
}
userRepository.save(dbUser);
...
}
Downside of this approach is, you have to manually validate the incoming values.
I've a customized and dirty solution employs java.lang.reflect package. My solution worked well for 3 years with no problem.
My method takes 2 arguments, objectFromRequest and objectFromDatabase both have the type Object.
The code simply does:
if(objectFromRequest.getMyValue() == null){
objectFromDatabase.setMyValue(objectFromDatabase.getMyValue); //change nothing
} else {
objectFromDatabase.setMyValue(objectFromRequest.getMyValue); //set the new value
}
A "null" value in a field from request means "don't change it!".
-1 value for a reference column which have name ending with "Id" means "Set it to null".
You can also add many custom modifications for your different scenarios.
public static void partialUpdateFields(Object objectFromRequest, Object objectFromDatabase) {
try {
Method[] methods = objectFromRequest.getClass().getDeclaredMethods();
for (Method method : methods) {
Object newValue = null;
Object oldValue = null;
Method setter = null;
Class valueClass = null;
String methodName = method.getName();
if (methodName.startsWith("get") || methodName.startsWith("is")) {
newValue = method.invoke(objectFromRequest, null);
oldValue = method.invoke(objectFromDatabase, null);
if (newValue != null) {
valueClass = newValue.getClass();
} else if (oldValue != null) {
valueClass = oldValue.getClass();
} else {
continue;
}
if (valueClass == Timestamp.class) {
valueClass = Date.class;
}
if (methodName.startsWith("get")) {
setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("get", "set"),
valueClass);
} else {
setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("is", "set"),
valueClass);
}
if (newValue == null) {
newValue = oldValue;
}
if (methodName.endsWith("Id")
&& (valueClass == Number.class || valueClass == Integer.class || valueClass == Long.class)
&& newValue.equals(-1)) {
setter.invoke(objectFromDatabase, new Object[] { null });
} else if (methodName.endsWith("Date") && valueClass == Date.class
&& ((Date) newValue).getTime() == 0l) {
setter.invoke(objectFromDatabase, new Object[] { null });
}
else {
setter.invoke(objectFromDatabase, newValue);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
In my DAO class, simcardToUpdate comes from http request:
simcardUpdated = (Simcard) session.get(Simcard.class, simcardToUpdate.getId());
MyUtil.partialUpdateFields(simcardToUpdate, simcardUpdated);
updatedEntities = Integer.parseInt(session.save(simcardUpdated).toString());
The main problem lies in your following code:
#RequestMapping(value = "/{id}", method = RequestMethod.POST )
public #ResponseBody ResponseEntity<User> update(#RequestBody User updateUser) {
dbUser = userRepository.findOne(updateUser.getId());
customObjectMerger(updateUser, dbUser);
userRepository.saveAndFlush(updateUuser);
...
}
In the above functions, you call some of your private functions & classes (userRepository, customObjectMerger, ...), but give no explanation how it works or how those functions look like. So I can only guess:
CustomObjectMerger merges this updateUser with the corresponding
dbUser from the database, updating the only fields included in
updateUser.
Here we don't know what happened in CustomObjectMerger (that's your function, and you don't show it). But from what you describe, I can make a guess: you copy all the properties from updateUser to your object at database. This is absolutely a wrong way, since when Spring map the object, it will fill all the data. And you only want to update some specific properties.
There are 2 options in your case:
1) Sending all the properties (including the unchanged properties) to the server. This may cost a little more bandwidth, but you still keep your way
2) You should set some special values as the default value for the User object (for example, id = -1, age = -1...). Then in customObjectMerger you just set the value that is not -1.
If you feel the 2 above solutions aren't satisfied, consider parsing the json request yourself, and don't bother with Spring object mapping mechanism. Sometimes it just confuse a lot.
Partial updates can be solved by using #SessionAttributes functionality, which are made to do what you did yourself with the customObjectMerger.
Look at my answer here, especially the edits, to get you started:
https://stackoverflow.com/a/14702971/272180
I've done this with a java Map and some reflection magic:
public static Entidade setFieldsByMap(Map<String, Object> dados, Entidade entidade) {
dados.entrySet().stream().
filter(e -> e.getValue() != null).
forEach(e -> {
try {
Method setter = entidade.getClass().
getMethod("set"+ Strings.capitalize(e.getKey()),
Class.forName(e.getValue().getClass().getTypeName()));
setter.invoke(entidade, e.getValue());
} catch (Exception ex) { // a lot of exceptions
throw new WebServiceRuntimeException("ws.reflection.error", ex);
}
});
return entidade;
}
And the entry point:
#Transactional
#PatchMapping("/{id}")
public ResponseEntity<EntityOutput> partialUpdate(#PathVariable String entity,
#PathVariable Long id, #RequestBody Map<String, Object> data) {
// ...
return new ResponseEntity<>(obj, HttpStatus.OK);
}
Related
I have implemented by project using Spring-Data-Rest. I am trying to do an update on an existing record in a table. But when I try to send only a few fields instead of all the fields(present in Entity class) through my request, Spring-Data-Rest thinking I am sending null/empty values. Finally when I go and see the database the fields which I am not sending through my request are overridden with null/empty values. So my understanding is that even though I am not sending these values, spring data rest sees them in the Entity class and sending these values as null/empty. My question here is, is there a way to disable the fields when doing UPDATE that I am not sending through the request. Appreciate you are any help.
Update: I was using PUT method. After reading the comments, I changed it to PATCH and its working perfectly now. Appreciate all the help
Before update, load object from database, using jpa method findById return object call target.
Then copy all fields that not null/empty from object-want-to-update to target, finally save the target object.
This is code example:
public void update(Object objectWantToUpdate) {
Object target = repository.findById(objectWantToUpdate.getId());
copyNonNullProperties(objectWantToUpdate, target);
repository.save(target);
}
public void copyNonNullProperties(Object source, Object target) {
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
public String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] propDesList = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<String>();
for(PropertyDescriptor propDesc : propDesList) {
Object srcValue = src.getPropertyValue(propDesc.getName());
if (srcValue == null) {
emptyNames.add(propDesc.getName());
}
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
As some of the comments pointed out using PATCH instead of PUT resolved the issue. Appreciate all the inputs. The following is from Spring Data Rest Documentation:
"The PUT method replaces the state of the target resource with the supplied request body.
The PATCH method is similar to the PUT method but partially updates the resources state."
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.hiding-repository-crud-methods
Also, I like #Tran Quoc Vu answer but not implementing it for now since I dont have to use custom controller. If there is some logic(ex: validation) involved when updating the entity, I am in favor of using the custom controller.
I have a controller that has a few methods that get an optional of entity from service, checks if is present and proceeds with some other actions or redirects with message "Entity not found".
It looks like that:
#GetMapping("action")
public String method(#PathVariable Long id,
final RedirectAttributes redirectAttributes){
Optional<Entity> eOpt = entityService.findById(id);
if(eOpt.isEmpty()){
alertHandler.set(redirectAttributes, Status.ENTITY_NOT_FOUND);
return "redirect:/entity/list"
}
Entity e = eOpt.get();
// other actions that are using e
return "view-name";
}
The six lines repeat in a few methods and for different entities too. Is there a way to assign it to some private method? The only thing I came up with is using a private method like:
private Optional<Entity> getEntityOpt(Long id){
Optional<Entity> eOpt = entityService.findById(id);
if(eOpt.isEmpty()){
alertHandler.set(redirectAttributes, Status.ENTITY_NOT_FOUND);
}
return Optional.empty();
}
This only saves me one line in mapped methods, so I don't have to set up alert message. Otherwise I still have to check again if the Optional is empty to redirect it.
So I guess the question really is - can I set up the private method to either return entity or redirect like:
Entity e = getEntityOrRedirect(Long id);
or maybe you have different ways to handle that problem. Or maybe it is what it is and you have to repeat yourself...
You may treat empty Optional as an exceptional situation.
In that case you may provide your own RuntimeException containing path to redirect.
public class EntityNotFoundException extends RuntimeException {
private final String fallbackView;
public EntityNotFoundException(final String fallbackView) {
this.fallbackView = fallbackView;
}
public String getFallbackView() {
return fallbackView;
}
Then provide a method annotated with #ExceptionHandler to your controller class (or if the situation is common for multiple controllers then provide such method to class annotated with #ControllerAdvice). Your exception handler should catch just defined exception and do a redirect.
#ExceptionHandler(EntityNotFoundException.class)
public String redirectOnEntityNotFoundException(final EntityNotFoundException exception,
final RedirectAttributes redirectAttributes) {
alertHandler.set(redirectAttributes, Status.ENTITY_NOT_FOUND);
return exception.getFallbackView();
}
Finally you achieved some kind of getEntityOrRedirect. Now you may use the above setup as following:
#GetMapping("action")
public String method(#PathVariable Long id){
Entity e = entityService.findById(id)
.orElseThrow(() -> new EntityNotFoundException("redirect:/entity/list"));
// other actions that are using e
return "view-name";
}
Code not tested so apologize for typos in advance.
Note I believe it would work for Spring >= 4.3.5 as otherwise RedirectAttributes wouldn't be resolved for #ExceptionHandler (as stated here)
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
I have a form whose purpose is to allow a user to create a new entry in a database table. The form is very large, ~50 fields in total. I'm needing a way get all of these values to my controller, though I don't see an easy way. Every solution I've seen is to have a #RequestParam('field'), but with around 50 fields that is a little crazy. Maybe if using a #RequestParam Map<T, T> is possible?
What I tried at first was to create an AJAX POST call to
baseapplication.com/add?field1=value1&field2=value2&...&field50=value50
but then the servlet complained about not finding the add.jsp file. This is sort of reasonable because that file does not exist, but I created a controller mapping #RequestMapping(value="/add") so I shouldn't actually need that file. I have another method which creates an AJAX GET call to /search with some url parameters, and that works fine. There is also no search.jsp file.
This problem is hard to explain, I hope I did a halfway decent job. Let's look at some code now, with omissions because dealing with ~50 form fields is very lengthy.
Starting with the JavaScript, which initiates this whole process:
ctx = "baseapplication.com"
$('#addNewRecordBtn').on('click', function(){
var m_insId = document.getElementById('m_insId');
//This repeats for every field
var url = ctx + '/add?insuredId=' + m_insId /** + all other fields **/;
addCase(url);
});
function addCase(url) {
$.ajax({
url: url,
type: 'POST'
}).success(function(data){
alert("Successfully added row");
}).fail(function(jzXHR, textStatus, errorThrown){
alert(jzXHR);
alert(textStatus);
alert(errorThrown);
});
}
So the flow of this is as follows: The user clicks on the addNewRecordBtn button, which fires the first function. This function grabs the value of every field in the form, then builds a URL with parameters for each of these values. The addCase() function is then called, which creates an AJAX POST (not sure what to call this?) to the URL that was just built. This function does not succeed, the error alerts provide zero information, but the server console claims Failed to find resource /WEB-INF/views/add.jsp
Now we move into the controller.
#Controller
public class ApplicationController {
#Autowired
SpecialClaimsCaseManager caseManager;
#RequestMapping(value="/add")
public void addRow(#RequestParam Map<String, String> requestParams) {
SpecialClaimsCase newCase = new SpecialClaimsCase();
newCase.setInsuredId(requestParams.get("insuredId"));
//Repeat this for all parameters
caseManager.addNewSpecialClaimsCase(newCase);
}
The caseManager.addNewSpecialClaimsCase(newCase) call just creates a DTO object out of this model object, and then adds that new object to the database via some Hibernate magic. I don't know much about that side, other than it works.
So, I'm not sure if I'm going about this the right way. I hear there's a way to map a model object to a JSP form using Spring's tag library, but that would require a ton of rewriting as the form is huge. I'm also using Bootstrap to build the interface, and I'm not sure if Bootstrap and Spring's tag library mix well. I can't imagine why not.
I'm not sure if I need to be using AJAX here or not. I went with it because I don't want the page to have to reload or anything. I'm not usually a web developer, so I am sure I am lacking some fundamental knowledge here.
My main question is: given my sitatuation, what is the best way to get this massive form of information into my controller?
Thanks in advance for reading this wall of text and code and for any assistance you can offer!
Create a domain class that has all of these needed fields and generate getters and setters and also a constructor. Once you get all these fields/some of these fields POST as json to your controller. The appropriate controller will then call the required service and then the DAO will handle the persistence part.
In short, send the data you need as a JSON object. The json will be made as java object and the operation on the same will be performed.
Here is the controller
#Controller
#RequestMapping(value = "/students/association")
public class StudentDepartmentController {
#Autowired
private StudentService studentService;
#Autowired
private StudentDepartmentService studentDepartmentService;
#RequestMapping(value = "/add-department", method = RequestMethod.POST)
public ResponseEntity<StudentDepartment> createStudentDepartmentAssociation(
#RequestBody final StudentDepartment studentDepartment) {
StudentDepartment newStudentDepartment;
// check if the student exists
Student student = studentService.getStudentByUuid(studentDepartment
.getStudentUuid().getUuid());
if (null == student) {
throw new IllegalArgumentException("No students found!");
}
// check the status of student
if (student.getStatus() == Liveliness.INACTIVE) {
throw new IllegalArgumentException(
"cannot create an association with an inactive student! activate student first");
}
// check for valid department
if (null == studentDepartment.getDepartment().getName()) {
throw new IllegalArgumentException("No such Department");
}
// check if the association already exists
if (null != findOneAssociationAgainstStudent(student)) {
throw new IllegalArgumentException(
"cannot create student department association, as "
+ student.getUsn()
+ " already present in another association ( "
+ studentDepartment.getDepartment().getName()
+ " )");
}
try {
newStudentDepartment = studentDepartmentService
.createNewAssociation(studentDepartment);
} catch (DataIntegrityViolationException ex) {
throw new AutomationTransactionException(
"cannot create student department association, as "
+ student.getUsn()
+ " already present in another association ( "
+ studentDepartment.getDepartment().getName()
+ " )", ex);
}
return new ResponseEntity<StudentDepartment>(newStudentDepartment,
HttpStatus.CREATED);
}
private StudentDepartment findOneAssociationAgainstStudent(Student student) {
return studentDepartmentService.findOneAssociation(student);
}
private StudentDepartment findOne(Uuid uuid) {
String studentDepartmentUuid = uuid.getUuid();
return findOne(studentDepartmentUuid);
}
private StudentDepartment findOne(String uuid) {
return studentDepartmentService.findOne(uuid);
}
#RequestMapping(value = "/delete-association", method = RequestMethod.DELETE)
public ResponseEntity<String> deleteStudentDepartmentAssociationByUuid(
#RequestBody final StudentDepartment studentDepartment) {
// check if association exists
StudentDepartment association = findOne(studentDepartment.getUuid());
if (null == association) {
throw new IllegalArgumentException("No such association found!");
}
studentDepartmentService.deleteAssociation(association);
return new ResponseEntity<String>("success", HttpStatus.OK);
}
}`
the #RequestBody annotation helps you to make the json object into java object.
with this, you can take the payload as json, and get the java object and send the json back to the UI with ResponseEntity<Class> annotation
Below is the mapped method that I am having trouble with, no matter what value I pass to it the validation returns "passed validation."
#RequestMapping(value = "test", method = RequestMethod.POST)
#ResponseBody
public String getTest(#RequestBody #Valid #Max(32) long longValue, BindingResult result) {
if (result.hasErrors()) {
return "failed validation";
} else {
return "passed validation";
}
}
I know the #Max works with my application because I use it for validation on custom objects that return a lot of data to the controller. It only doesn't run the validation in this case where I call #Valid and the type of validation on the object in the method parameters.
Is this not allowed with hibernate-validator?
I am hoping to not have to define an object that only contains a long value just so that I can run validation on it.
I am hoping to not have to define an object that only contains a long
value just so that I can run validation on it.
Defining a wrapping bean would IMHO be the smartest move, as hibernate-validator is completly centered around the notion of the bean, and is, after all, a reference implementation of the bean validation specification. One of the primary motivators of the spec is to acknowledge validation as a cross-cutting concern that spans across different app layers, and provide a mechanism to gracefully handle this. That is the reason why it is centered around beans, its the objects that get passed through the layers.
On the other hand, validating primitves programtically is not a big deal after all, your code can simply be something like
#RequestMapping(value = "test", method = RequestMethod.POST)
#ResponseBody
public String getTest(#RequestBody long longValue, BindingResult result) {
if (longValue > 32) {
result.rejectValue("longValue", "error.longValue", "longValue max constrained failed");
return "failed validation";
} else {
return "passed validation";
}
}
So in my opinion, either go for the programatic validation if its simple enough, or simply wrap the value.
No, it is not allowed. I see your point however the JSR-303 specification (which hibernate validator implements) are meant for beans validation, see here
First, as stated by other answers, Hibernate Validator doesn't allow to validate directly primitives like the example in the question. In order to use Hibernate Validator, defining a new class containing a long value is exactly what is required in order to use Hibernate Validator.
Second, and most important, whereas programatic validation is something that works and it is quite simple, it is not clean and maintanable. Let me illustrate this with some examples:
#RequestMapping(value = "testA", method = RequestMethod.POST)
#ResponseBody
public String getTestA(#RequestBody long longValue, BindingResult result) {
if (longValue > 32) {
result.rejectValue("longValue", "error.longValue", "longValue max constrained failed");
return "failed validation for test A";
} else {
return "passed validation for test A";
}
}
#RequestMapping(value = "testB", method = RequestMethod.POST)
#ResponseBody
public String getTestB(#RequestBody long longValue, BindingResult result) {
if (longValue > 32) {
result.rejectValue("longValue", "error.longValue", "longValue max constrained failed");
return "failed validation for test B";
} else {
return "passed validation for test B";
}
}
#RequestMapping(value = "testC", method = RequestMethod.POST)
#ResponseBody
public String getTestC(#RequestBody long longValue, BindingResult result) {
if (longValue > 32) {
result.rejectValue("longValue", "error.longValue", "longValue max constrained failed");
return "failed validation for test C";
} else {
return "passed validation for test C";
}
}
First thing to notice, is withing 3 simple functions, all the validation code is duplicated.
Secondly, if at some point your validations requirements change, and now all the long values must be bigger than 35, you need to change every single validation function, and in this example is really simple, because there is only 3 functions with the same validation, but imagine for one moment it is 100 functions where you perform the same validation, now that is painful.
So just defining a new class, with the long value and the validation annotation, and using that class on every method, you removed code duplication, and also when your validation requirements change, applying changes in one single place, you have mantained the code.
Also, there is nothing wrong about having to define classes for one specific task, in fact, that is exactly what Single Responsability Principle tells you to do.
Edit: added a link to a SRP description.