In my current project I frequently use bulk requests. I have simple BulkRequest<T> class:
import java.util.List;
import javax.validation.constraints.NotNull;
public class BulkRequest<T> {
#NotNull private List<T> requests;
public List<T> getRequests() { return this.requests; }
public void setRequests(List<T> requests) { this.requests = requests; }
}
It very simple to use with other beans, for example:
#RequestMapping(value = "/departments/{departmentId}/patterns",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> post(
final #PathVariable long departmentId,
final #Valid #RequestBody BulkRequest<AddPatternRequest> bulkRequest
) {
...
}
AddPatternRequest contains own rules for validation and represents only one request, which can be collected to bulk request:
import javax.validation.constraints.NotNull;
public class AddPatternRequest {
#NotNull private Long pattern;
public Long getPattern() { return this.pattern; }
public void setPattern(Long pattern) { this.pattern = pattern; }
}
But there's a problem. After the controller receives the bulk request, it validates only BulkRequest and checks if requests collection is null or not, but I need to validate nested request too.
How can I do it?
Add #Valid to the requests. Like this
#NotNull
#Valid
private List<T> requests;
Then nested objects are also validated
Related
How can I get the matched request path in an HTTP mapping method in a spring application? In the following example, I want matchedPath variable to have the value /api/products/{product_id}/comments which is a combination of #RequestMapping and #PostMapping value.
#RestController
#RequestMapping("/api")
public class Example {
#PostMapping("/products/{product_id}/comments")
public ResponseEntity doSomething(#PathVariable("product_id") String id) {
// String matchedPath = getPath();
return ResponseEntity.ok().build();
}
}
I found it is doable via one of HandlerMapping attributes BEST_MATCHING_PATTERN_ATTRIBUTE
import javax.servlet.http.HttpServletRequest;
#RestController
#RequestMapping("/api")
public class Example {
#PostMapping("/products/{product_id}/comments")
public ResponseEntity doSomething(HttpServletRequest req, #PathVariable("product_id") String id) {
String matchedPath = String.valueOf(req.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
return ResponseEntity.ok().build();
}
}
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/HandlerMapping.html
I have a DTO which is validated at Controller layer with a mix of BeanValidation (javax.validation) and a custom Validator (org.springframework.validation.Validator). This way I can check if the input provided is valid and then convert the DTO in an entity and forward it to the Service layer.
#Data
public class UserDTO {
#NotBlank
#Size(max = 25)
private String name;
#NotNull
private Date birthday;
#NotNull
private Date startDate;
private Date endDate;
private Long count;
}
public class UserDTOValidator implements Validator {
private static final String START_DATE= "startDate";
private static final String END_DATE= "endDate";
private static final String COUNT= "count";
#Override
public boolean supports(Class<?> clazz) {
return UserDTO.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
UserDTO vm = (UserDTO) target;
if (vm.getEndDate() != null) {
if (vm.getStartDate().after(vm.getEndDate())) {
errors.rejectValue(START_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
if (vm.getEndDate().equals(vm.getStartDate()) || vm.getEndDate().before(vm.getStartDate())) {
errors.rejectValue(END_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
}
if (vm.getCount() < 1) {
errors.rejectValue(COUNT, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
.....
}
}
public class UserController {
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new UserDTOValidator());
}
#PostMapping()
public ResponseEntity<UserDTO> create(#RequestBody #Valid UserDTO userDTO) {
.....
}
.....
}
Then there is the business logic validation. For example: the #Entity User's startDate must be after some event occurred and the count has to be greater than some X if the last created User's birthDay is in Summer, in other case, the entity should be discarded by the User service.
#Service
#Transactional
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private SomeEventService someEventService ;
#Override
public User create(User entity) {
String error = this.validateUser(entity);
if (StringUtils.isNotBlank(error)) {
throw new ValidationException(error);
}
return this.userRepository.save(entity);
}
....
private String validateUser(User entity) {
SomeEvent someEvent = this.someEventService.get(entity.getName());
if (entity.getStartDate().before(someEvent.getDate())) {
return "startDate";
}
User lastUser = this.userRepository.findLast();
....
}
}
However I feel like this is not the best approach to handle business logic validation. What should I do? ConstraintValidator/HibernateValidator/JPA Event listeners? Can they work at #Entity class level or I have to create X of them for each different field check? How do you guys do it in a real production application?
In my suggestion,
Use classic field level validation by #Valid
sample
void myservicemethod(#Valid UserDTO user)
For custom business level validation in entity level, create validate method in DTO itself
sample
class UserDTO {
//fields and getter setter
void validate() throws ValidationException {
//your entity level business logic
}
}
This strategy will help to keep entity specific validation logic will be kept within the entity
If still you have validation logic that requires some other service call, then create custom validation annotation with custom ConstraintValidator (eg. question on stackoverflow). In this case, my preference will be to invoke UserDTO.validate() from this custom validator in spiote of calling from service
This will help to keep your validation logic separated from service layer and also portable and modular
Let's say I would like to validate incoming ID parameter for all my RESTful methods (>50).
As an example I have:
#RequestMapping(
value = "/{id}",
method = RequestMethod.GET,
produces = {"application/json"})
#ResponseStatus(HttpStatus.OK)
public
#ResponseBody
Metadata getMetadata(
#PathVariable("id") Long id,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
return metadataService.get(id);
}
I would like to reject all requests if id < 1. As a solution I've implemented:
#RequestMapping(
value = "/{id}",
method = RequestMethod.GET,
produces = {"application/json"})
#ResponseStatus(HttpStatus.OK)
public
#ResponseBody
Metadata getMetadata(
#Valid Id id,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
return metadataService.get(id.asLong());
}
public class Id {
#Min(1)
#NotNull
private Long id;
public void setId(Long id) {
this.id = id;
}
public Long asLong() {
return id;
}
}
But now I have to implicitly put #Valid annotation for each and every method for Id argument, which seems quite redundant . Is there a way to tell Spring that if there's an Id object as an incoming parameter it should always be #Valid. Without putting annotation each time?
Thanks.
So I've ended up with solution like this:
public class CustomModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
public CustomModelAttributeMethodProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(final WebDataBinder binder, final NativeWebRequest request) {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
((ServletRequestDataBinder) binder).bind(servletRequest);
}
#Override
protected void validateIfApplicable(final WebDataBinder binder, final MethodParameter parameter) {
if (binder.getTarget().getClass().equals(Id.class)) {
binder.validate();
return;
}
super.validateIfApplicable(binder, parameter);
}
}
And configuration:
#Configuration
#EnableWebMvc
public class ApplicationConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
super.addArgumentResolvers(argumentResolvers);
argumentResolvers.add(new CustomModelAttributeMethodProcessor(true));
}
}
A bit overhead to check class of each and every incoming parameter, but works as expected. Now #Valid annotation can be omitted as validation performs by custom processor.
I'm using Spring Data REST JPA to build a RESTful web service. So far, Spring is auto-generating all the responses for all the possible methods and for listing all the resources available and even for searches over them:
#RepositoryRestResource(collectionResourceRel = "scans", path = "scans")
public interface ScanRepository extends PagingAndSortingRepository<Scan, Long> {
List<Scan> findByProjectId(#Param("pid") String pid);
}
Now I would like to modify what is returned "only" to POST requests while leaving intact the support to all the others.
I thought I'd create a controller for this purpose like the following:
#Controller
public class ScanController {
#RequestMapping(value = "/scans", method = POST, produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody Result parseScan(#RequestParam String projectId, #RequestParam String tool) {
return null;
}
However when I do this, the JPA-data auto-generated responses for all the other methods and searches etc. ceases to exist. For instance, I get "Method not allowed" if I forward a GET request.
Besides, how could I access a JSON payload from the controller?
UPDATE
Now only one of the exposed resource does back to the default methods for requests not manually handled in my own controller. However I have no idea why it does and why this doesn't happen for any of the other resources.*
Despite they all only differ in their entity's attributes.
The following particular resource is the one that does back to the default request handlers for anything that is not POST scan/ or GET /scan/// which I declared in the controller:
#Controller
public class ScanController {
#Autowired
private ScanService scanService;
#RequestMapping(
value = "/scan",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan parseScan(#RequestBody Scan rbody) {
<...do something...>
}
#RequestMapping(value = "/scans/{id}/{totvuln}/{nth}", method = RequestMethod.GET,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan getScan(#PathVariable String id, #PathVariable int totvuln, #PathVariable int nth) throws ScanNotFound {
<...do something...>
}
It has the following repository interface:
public interface ScanRepository extends PagingAndSortingRepository<Scan, Long> {}
and the following service:
#Service
public class ScanServiceImpl implements ScanService {
#Resource
private ScanRepository scanRepository;
#Resource
private ResultRepository resultRepository;
#Override
#Transactional
public Scan create(Scan shop) {
<some code>
}
#Override
#Transactional
public Scan findById(long id) {
<some code>
}
#Override
#Transactional(rollbackFor = ScanNotFound.class)
public Scan delete(long id) throws ScanNotFound {
<some code>
}
#Override
#Transactional
public List<Scan> findAll() {
<some code>
}
#Override
#Transactional(rollbackFor = ScanNotFound.class)
public Scan update(Scan scan) throws ScanNotFound {
<some code>
}
}
and the resource itself has the following attributes:
#Entity
public class Scan {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long projectId;
#OneToMany
private Collection<Result> result;
private int totV;
<getters and setters>
}
While the following semi-identical resource "Rules" does not back to any of the default request handlers. It returns "Method not Allowed" for anything different from POST /rule:
#Controller
public class RulesController {
#Autowired
private RulesService rService;
#Resource
private ScanRepository scanRepository;
#RequestMapping(
value = "/rule",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Rules generateRules(#RequestBody Scan rbody) throws Exception {
<do something>
}
}
It has the same repository interface:
public interface RulesRepository extends PagingAndSortingRepository<Rules, Long> {}
and also the same service implementation:
#Service
public class RulesServiceImpl implements RulesService {
#Resource
private RulesRepository rRepository;
#Resource
private ResultRepository resultRepository;
#Override
#Transactional
public Rules create(Rules shop) {
<do something>
}
#Override
#Transactional
public Rules findById(long id) {
<do something>
}
#Override
#Transactional(rollbackFor = RulesNotFound.class)
public Rules delete(long id) throws RulesNotFound {
<do something>
}
#Override
#Transactional
public List<Rules> findAll() {
<do something>
}
#Override
#Transactional
public Rules findByScanId(long id) throws RulesNotFound {
<do something>
}
#Override
#Transactional(rollbackFor = RulesNotFound.class)
public Rules update(Rules scan) throws RulesNotFound {
<do something>
}
}
and the resource Rules itself has the following attributes:
#Entity
public class Rules {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne
private Scan scan;
#OneToMany
private Collection<Result> result;
private String rules;
<getters and setters>
}
Why isn't Spring exposing the default request handlers also for "Rules" for any request that hasn't been specified manually in my controller class?
I would truly appreciate if you could point out why. Thank you so much!
I've figured out how to access a JSON payload from the controller:
#RequestMapping(
value = "/scan",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan parseScan(#RequestBody Scan rbody) {
Scan scan = new Scan();
scan.setProjectId(rbody.getProjectId());
scan.setTool(rbody.getTool());
return scan;
}
Also I've realised the automatic CRUD operations were actually being already supported for every request not handled by my own controller: I was just requesting the wrong URL.
I got the list of correct URLs to use by requesting "curl http://localhost:8080"
However, the preferred URL for any of the auto-generated operations can be set with
#RepositoryRestResource(collectionResourceRel = pref_URL_suffix, path = pref_URL_suffix)
^^somehow during all the changes I tried, that line above went missing.
What is the proper way to handle a bad parameter in a RESTful service? I have an endpoint right now that returns a 400 if the wrong data type is sent.
#ResponseBody
#RequestMapping(produces = "application/json", method = RequestMethod.GET, value="/v1/test")
public MyResponse getSomething(#RequestParam BigDecimal bd) {
MyResponse r = new MyResponse();
r.setBd(bd);
return r;
}
It would be really nice if the end user were to pass, say, a String instead of a BigDecimal, that the response would return a json with the response code, status, and whatever else I'd like it to contain, rather than just a 400. Is there a way to do this?
Update: My initial thought was to wrap every parameter and then check to see if it's the correct type in that wrapper class. This seems a bit silly. Isn't there a validator that I could just add to the classpath that would recognize something like this?
Also, there is a way to handle this quite easily with a Bean type that I could create on my own, but what about standard types like BigDecimal?
UPDATE-2: This update addresses answer that uses #ExceptionHandler.
TestController.java
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
#RequestMapping("/")
public class TestController {
//this is where correct input from user is passed, no binding errors
#RequestMapping(produces = "application/json", method = RequestMethod.GET, value="/v1/test")
public
#ResponseBody
MyResponse getSomething(#RequestParam BigDecimal bd) {
MyResponse r = new MyResponse();
r.setBd(bd);
return r;
}
//this will handle situation when you except number and user passess string (A123.00 for example)
#ExceptionHandler(ServletRequestBindingException.class)
public #ResponseBody MyErrorResponse handleMyException(Exception exception, HttpServletRequest request) {
MyErrorResponse r = new MyErrorResponse();
r.setEexception(exception);
return r;
}
}
TestUnitTest.java
public class TestUnitTest {
protected MockMvc mockMvc;
#Autowired
protected WebApplicationContext wac;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
public void test() throws Exception {
String url = "/v1/test?bd=a123.00";
log.info("Testing Endpoint::: " + url);
MvcResult result = mockMvc.perform(get(url))
.andExpect(status().isOk())
.andReturn();
log.info("RESPONSE::: " + result.getResponse().getContentAsString());
}
}
MyResponse.java
import java.math.BigDecimal;
public class MyResponse {
private BigDecimal bd;
public BigDecimal getBd() {
return bd;
}
public void setBd(BigDecimal bd) {
this.bd = bd;
}
}
MyErrorResponse.java
public class MyErrorResponse {
private Exception exception;
public Exception getException() {
return exception;
}
public void setEexception(Exception e) {
this.exception = e;
}
}
Use Spring #ExceptionHandler along with standard #RequestMapping annotation like this:
//this is where correct input from user is passed, no binding errors
#RequestMapping(produces = "application/json", method = RequestMethod.GET, value="/v1/test")
public
#ResponseBody
MyResponse getSomething(#RequestParam BigDecimal bd) {
MyResponse r = new MyResponse();
r.setBd(bd);
return r;
}
//this will handle situation when there's exception during binding, for example you except number and user passess string (A123.00 for example)
#ExceptionHandler(TypeMismatchException.class)
public
#ResponseBody
MyErrorResponse handleMyException(Exception exception, HttpServletRequest request) {
//....
}
TypeMismatchException is general exception thrown when trying to set a bean property. You can generalize code even more and catch every binding exception with few methods, for example:
#ExceptionHandler(TypeMismatchException.class)
public
#ResponseBody
String typeMismatchExpcetionHandler(Exception exception, HttpServletRequest request) {
return "type mismatch";
}
#ExceptionHandler(MissingServletRequestParameterException.class)
public
#ResponseBody
String missingParameterExceptionHandler(Exception exception, HttpServletRequest request) {
return "missing param";
}
#ExceptionHandler(Exception.class)
public
#ResponseBody
String generalExceptionHandler(Exception exception, HttpServletRequest request) {
return "general exception";
}
It's very flexible, allowing many parameters in signature and returned objects Annotation Type ExceptionHandler
With #ResponseBody you may return any object, that can be serialized into JSON. It's only required to have jackson library in your classpath but I assume that you already know this