Custom String Method Validation JSR303 SpringMVC - java

I have a REST-Api where I want to validate the parameters on method Level. With #Valid and a custom object where validation annotations are inside, it works fine, but it fails, if I use a String and write the annotations directly into the method signiture.
My validator
#Documented
#Constraint(validatedBy = {})
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Pattern(regexp = "^[A-Za-z]{5}[0-9]{3,128}$")
public #interface CustomValidator{
public abstract String message() default "Custom Message"
public abstract Class<?>[] groups() default {};
public abstract Class<?>[] payload() default {};
}
My controller class
#RestController
public class MyController {
...
}
What works but is not so nice Code Style
#Data
public class FoobarForm{
#NotNull
#CustomValidator
String foobar;
}
#RequestMapping(value = "/foo/{bar}", method = RequestMethod.GET)
public Bank checkFoobar(#Valid #CustomValidator #PathVariable String foobar) {
What I want
#RequestMapping(value = "/foo/{bar}", method = RequestMethod.GET)
public Bank checkIban(#Validated #NotNull #CustomValidator #PathVariable String foobar) {
As you can see, I want to validate a String parameter on method Level with a custom Validator. But that does not work. If I put an invalid argument in the method, it passes and the validation is never performed.
Of course I can check the Regex in another way since it is a path variable, but I have the same scenario at other Methods and want to have a general Solution for this problem.
Info:
I currently use Spring Version: 4.0.7

Related

How to cascade validation in a custom validator?

In a Spring project (4.3.14.RELEASE) I need to validate Map<String, List<InnerObj>> map in the MVC layer.
For this purpose I wrote a custom validator
public class MapValidator implements ConstraintValidator<ValidMap, Map<String, List<InnerObj>>> {
#Override
public boolean isValid(final Map<String, List<InnerObj>> map,
final ConstraintValidatorContext context) {
if (map == null || map.size() == 0) {
return false;
}
// iterations over all objects and validation
return true;
}
and annotation
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = MapValidator.class)
public #interface ValidMap {
String message() default "valid.map";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
How to force the validation to propagate (something like #Valid) to the inner object (List and InnerObj) from my validator?
In principle, you can inject the Spring-configured Validator into your MapValidator and call its validate method on list elements. You'll need, however, to integrate the resulting Set<ConstraintViolation<ISoftBetFRBCoinDTO>> into the ConstraintValidatorContext argument somehow.
This approach seems to be an overkill.
Actually, Bean Validation does support validating collection elements via #Valid for method arguments:
public void method(Map<String, List<#Valid Address>> addressMap) {
This usage of #Valid is called method validation in the Bean Validation specification.
It's just in order to activate method validation in Spring, you have to annotate the containing class with Spring's #Validated annotation:
#RestController
#Validated
public class HelloWorldController {
#PostMapping("/hello")
public Map<String, List<Address>> helloWorld(#RequestBody Map<String, List<#Valid Address>> addressMap) {
return addressMap;
}
}
From Spring documentation:
To be eligible for Spring-driven method validation, all target classes need to be annotated with Spring’s #Validated annotation, which can optionally also declare the validation groups to use. See MethodValidationPostProcessor for setup details with the Hibernate Validator and Bean Validation 1.1 providers.
See also the linked javadoc of MethodValidationPostProcessor.

Custom annotation isn't validating method parameter

I created a custom annotation
#Documented
#Constraint(validatedBy = CheckGranularityValidator.class)
#Target( { ElementType.PARAMETER} )
#Retention(RetentionPolicy.RUNTIME)
public #interface CheckGranularity {
String message() default "Duration has to be a multiple of granularity";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
With a validator like so
public class CheckGranularityValidator implements ConstraintValidator<CheckGranularity, AssetCostsRequest> {
#Override
public void initialize(final CheckGranularity constraintAnnotation) {
}
#Override
public boolean isValid(final AssetCostsRequest value, final ConstraintValidatorContext context) {
return value.getRange().getDuration() % value.getGranularity() == 0;
}
}
I tried using it in my RestController
#RestController
public class CalcApiController extends CalcApi {
#Override
public ResponseEntity<String> calcProfitability(#Valid #CheckGranularity #RequestBody final AssetCostsRequest assetCostsRequest) {
return ResponseEntity.ok("Works");
}
I tried using this annotation by writing a test:
#Test
public void calcTest() {
final AssetCostsRequest request = new AssetCostsRequest()
.setRange(new TimeRange(100L, 200L))
.setGranularity(26L);
given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/calc")
.then()
.statusCode(HttpStatus.SC_BAD_REQUEST);
}
Relevant part of AssetCostsRequest:
public class AssetCostsRequest {
#JsonProperty
#NotNull
private TimeRange range;
#JsonProperty
#NotNull
private Long granularity = 30L;
...getters & setters
}
Test method returns with 200. When I try to set a breakpoint in isValid method, it isn't hit when I run the test. I tried changing order of annotations, getting rid of #Valid, changing #Target in CheckGranularity class, nothing helped. I'm using RestAssured for testing.
How do I make it, so my annotation is properly validating a parameter?
Change CheckGranularity's target to ElementType.TYPE and add #CheckGranularity directly on AssetCostsRequest. Also remove #CheckGranularity from endpoint definition.
How it works. By adding #Valid on endpoint's parameter you tell spring to validate it. Adding validation like #CheckGranularity won't work on the same level as Valid. It has to be added somewhere inside parameters class.

Is there a declarative way to validate request parameters depending from one of request fields?

I'm looking for something like JSR-303 Validation Groups (bean validation, when you mark method argument with #Validated(GroupName.class) in controller and specify group in request class fields where needed), but it should decide how to validate at runtime depending on one of request fields.
For example, if we have controller class like this
#Controller
public class MyController {
#Autowired
private MyService myService;
#ResponseBody
#RequestMapping(value = "/path", method = RequestMethod.PUT)
public ResponseVo storeDetail(/*maybe some annotation here*/ DetailRequestVo requestVo) {
return myService.storeDetail(requestVo);
}
class DetailRequestVo {
String type;
Long weight;
Long radius;
}
}
And we want validation depending on type field value: if type = "wheel" then radius and weight fields should be presented, if type = "engine" then only weight field should be presented.
Does Spring (as of 3.2.17) provide API to implement these rules in more declarative approach? org.springframework.validation.Validator looks like not an option here, because its method boolean supports(Class<?> clazz) decides based on class info, not instance info.
Thanks in advance
Not sure about Spring but you can do that with plain JSR-303 using a custom Validator for the class itself... like
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE })
#Constraint(validatedBy = TypeValidator.class)
#Documented
public #interface ValidType {
...
}
public class TypeValidator implements ConstraintValidator<ValidType , Object> {
public boolean isValid(final Object target, final ConstraintValidatorContext context) {
DetailRequestVo request = (DetailRequestVo) target;
// do your checks here
}
and used like
#ValidType
class DetailRequestVo {
String type;
Long weight;
Long radius;
}
Since the custom Validator has access to the whole DetailRequestVo-Object you can do your check of field A depending on field B etc.

Spring aliasFor for Annotations with Target(PARAMETER)

I am trying to use the Meta-annotation of spring using the aliasFor annotation to create a custom annotation for the springs RequestParam
Simply 'extend/replace'
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface RequestParam {
#AliasFor("name")
String value() default "";
----
}
with my annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Inherited
public #interface QueryParam {
#AliasFor(annotation = RequestParam.class, attribute = "name")
String name() default "";
#AliasFor(annotation = RequestParam.class, attribute = "required")
boolean required() default false;
#AliasFor(annotation = RequestParam.class, attribute = "defaultValue")
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
This way it throws the Exception
org.springframework.core.annotation.AnnotationConfigurationException: #AliasFor declaration on attribute [name] in annotation [package.QueryParam] declares an alias for attribute [name] in meta-annotation [org.springframework.web.bind.annotation.RequestParam] which is not meta-present.
Problem is that without the RequestParam annotated on the QueryParam this doesn't work. And it is not possible to put the RequestParam as it PARAMETER targeted.
#RequestParam <--This is not possible.
public #interface QueryParam
So is there another way to achieve this ?
Basically what you want to achieve is not possible now, at least for the Spring v 4.3.3 There are main two problems, the first one is the fact that annotations like #RequestParam are declared with #Target(ElementType.PARAMETER) which make it impossible to be used as part of meta annotations. Furthermore, Spring MVC looks up annotations on method parameters using org.springframework.core.MethodParameter.getParameterAnnotations() which does not support meta-annotations or composed annotations. But if you really need some customizations there you can use HandlerMethodArgumentResolver instead of meta annotations.
So you code will look something like
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface QueryParam {
String name() default "";
boolean required() default false;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
Then using the HandlerMethodArgumentResolver add the custom logic which you need.
public class QueryParamResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(QueryParam.class) != null;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
QueryParam attr = parameter.getParameterAnnotation(QueryParam.class);
// here you can use any logic which you need
return webRequest.getParameter(attr.value());
}
}
Then we need to register our HandlerMethodArgumentResolver
#Configuration
#EnableWebMvc
public class Config extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new QueryParamResolver());
}
}
And last lets use our custom annotation
#GetMapping("/test")
public String test(#QueryParam("foo") String foo){
// something here
}
i had same problem and I entered the wrong package for GetAmpping. I must import «import org.springframework.web.bind.annotation.GetMapping» .

Override value of #PreAuthorize using #AliasFor

I have the following combined custom annotation with Springs #PreAuthorize annotation,
#RequestMapping(
produces = MimeTypeUtils.APPLICATION_JSON_VALUE + ";charset=UTF-8",
method = RequestMethod.GET)
#ResponseBody
#PreAuthorize(value = "hasRole('permitAll()')")
#ResponseStatus(HttpStatus.OK)
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface Get {
#AliasFor(annotation = RequestMapping.class, attribute = "value")
String[] value() default {};
#AliasFor(annotation = PreAuthorize.class, attribute = "value")
String authorize() default "permitAll()";
}
And I've got the following client using it
#Get(value = "/users", authorize = "hasRole('ROLE_GET_USERS')")
public List<User> retrieveUsers() {
// body
}
As you can see the purpose is to allow clients of #Get annotation to override the #PreAuthorize so that they can provide the role they require for.
I din't have so far any problem using #AliasFor, even in this example #RequestMapping is working, but unfortunately it does not override the value of #PreAuthorize and everyone can still access the resources as the default value is permitAll().
I wonder first of all why this does not work, and second if it is possible to make it work?

Categories

Resources