The structure that I have is something like below:
Class A{
String str;
int i;
List<B> bs;
C c;
#NotNull
List<D> ds;
}
Class B{
#NotNull
List<E> es;
}
Class C{
List<String> s;
}
Class E{
#NotNull
List<String> s;
}
For the list variables that are annotated with #NotNull I need to throw validation error if any of them variables has one or more null objects. While for the other list variables I just need to remove the nulls;
What would be the best way to achieve this?
If you are using validation 2.0+ you can put annotation inside: List<#NotNull String> s;
You should define custom annotation for validating.
so define custom annotation like bellow.
#Target({ElementType.FIELD, ElementType.PARAMETER,ElementType.ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = ValidateListValidator.class)
#Documented
public #interface ValidateList {
}
and implement ValidateListValidator like this:
public class ValidateListValidator implements ConstraintValidator<ValidateList, List<Object>> {
private ValidateList validateList;
#Override
public void initialize(ValidateList validateList) {
this.validateList = validateList;
}
#Override
public boolean isValid( List<Object> list, ConstraintValidatorContext constraintValidatorContext) {
return list.stream().noneMatch(Objects::isNull);
}
}
and for test it
#Test
public void test() {
boolean valid = validator.isValid(Arrays.asList("test","this",null),context);
assertThat(valid, is(false));
}
This is the final code that I wrote, just a few tweaks to the code that Hadi posted. I hope it helps:
Annotation:
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ListValidator.class)
#Documented
public #interface ValidList {
String message() default "Null values are not allowed in array fields.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator Class:
import java.util.List;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ListValidator implements ConstraintValidator<ValidList, List<? extends Object>> {
#Override
public boolean isValid(List<? extends Object> list, ConstraintValidatorContext context) {
return list.stream().noneMatch(Objects::isNull);
}
#Override
public void initialize(ValidList constraintAnnotation) {}
}
Related
Is there any standard validator annotation I can use for a DataSize field?
Something like javax #Min and #Max, or Hibernate's #DurationMin and #DurationMax.
For class DataSize, there's no standard validator, so you should implement your own. But it's quite easy :)
Annotation interface:
import org.springframework.util.unit.DataUnit;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
#Documented
#Constraint(validatedBy = DataSizeMaxValidator.class)
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface DataSizeMax {
String message() default "must be less than or equal to {value} {unit}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
long value();
DataUnit unit();
}
Validator:
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
#Component
public class DataSizeMaxValidator implements ConstraintValidator<DataSizeMax, DataSize> {
private DataSize dataSizeMax;
#Override
public void initialize(DataSizeMax dataSizeMax) {
this.dataSizeMax = DataSize.of(dataSizeMax.value(), dataSizeMax.unit());
}
#Override
public boolean isValid(DataSize value, ConstraintValidatorContext context) {
return dataSizeMax.compareTo(value) >= 0;
}
}
That's it. Then use this annotation as here:
import pizza.nikiforov.validators.DataSizeMax;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
public class Data {
#DataSizeMax(value = 15L, unit = DataUnit.MEGABYTES)
private DataSize dataSize;
// other fields
// constructors, getters and setters
}
My goal is code generation via annotation processor. I want to generate new class on the top of the existent base class by excluding some fields according to annotations and adding some constarint validators etc. I have 3 modules. First one is base module which contains Car class and annotations BaseClass, A and B. Second module is annotation processor module. It contains CustomCodeGenerator annotaion and its processor. And third module is the module which I want to generate NewCar class onto it and use that NewCar class in it.
Car.Class
#BaseClass
public class Car {
#A
private int seatCount;
#B
private String name;
private String dummy;
public Car(int seatCount, String name) {
this.seatCount = seatCount;
this.name = name;
}
public int getSeatCount() {
return seatCount;
}
public void setSeatCount(int seatCount) {
this.seatCount = seatCount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDummy() {
return dummy;
}
public void setDummy(String dummy) {
this.dummy = dummy;
}
}
CustomCodeGenerator.class
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.SOURCE)
#Target(ElementType.TYPE)
public #interface CustomCodeGenerator {
}
CustomCodeGeneratorProcessor.class
import com.squareup.javapoet.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.io.IOException;
import java.util.List;
import java.util.Set;
#SupportedAnnotationTypes("*")
public class CustomCodeGeneratorProcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
private Types typeUtils;
#Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
}
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
try {
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(BaseClass.class);
for (Element element : elementsAnnotatedWith) {
TypeElement element1 = (TypeElement) element;
List<? extends Element> enclosedElements = element1.getEnclosedElements();
MethodSpec main = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(Integer.class, "seatCount")
.addStatement("this.$N = $N", "seatCount", "seatCount")
.build();
TypeSpec.Builder builder = TypeSpec.classBuilder("NewCar")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("", "ValidPassengerCount")).build())
.addMethod(main);
outer:
for (Element enclosedElement : enclosedElements) {
if (enclosedElement.getKind().isField()) {
List<? extends AnnotationMirror> annotationMirrors = enclosedElement.getAnnotationMirrors();
for (AnnotationMirror declaredAnnotation : annotationMirrors) {
if (!typeUtils.isSameType(elementUtils.getTypeElement("A").asType(), declaredAnnotation.getAnnotationType())) {
continue outer;
}
}
builder.addField(TypeName.get(enclosedElement.asType()), enclosedElement.getSimpleName().toString(), Modifier.PUBLIC);
}
}
JavaFile javaFile = JavaFile.builder("", builder.build())
.build();
javaFile.writeTo(filer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
#Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
The third module is like below as well.
Main.class
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
#CustomCodeGenerator
public class Main {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
NewCar car = new NewCar(-1);
Set<ConstraintViolation<NewCar>> violationSet = validator.validate(car);
System.out.println(violationSet.iterator().next().getMessage());
}
}
ValidPassengerCount.class
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = {ValidPassengerCountValidator.class})
public #interface ValidPassengerCount {
String message() default "invalid passenger count!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValidPassengerCountValidator.class
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ValidPassengerCountValidator
implements ConstraintValidator<ValidPassengerCount, NewCar> {
public void initialize(ValidPassengerCount constraintAnnotation) {
}
public boolean isValid(NewCar car, ConstraintValidatorContext context) {
if (car == null) {
return true;
}
return 0 <= car.seatCount;
}
}
The problem is roundEnv.getElementsAnnotatedWith(BaseClass.class) in CustomCodeGeneratorProcessor.class returns empty list. If I move Car into the 3rd module it works. However my goal is generating new code from the base class which comes from dependent module which is module 1 for this example. Is there any way to reach annotated elements of dependent module?
I would like to create a custom annotation in my Spring Boot application which always adds a prefix to my class level RequestMapping path.
My Controller:
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;
#MyApi("/users")
public class UserController {
#GetMapping("/stackoverflow")
public String get() {
return "Best users";
}
}
My custom annotation
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#RestController
#RequestMapping(path = "/api")
public #interface MyApi {
#AliasFor(annotation = RequestMapping.class)
String value();
}
GOAL: a mapping like this in the end: /api/users/stackoverflow
Notes:
server.servlet.context-path is not an option because I want to create
several of these
I'm using Spring Boot version 2.0.4
I was not able to find an elegant solution for the issue. However, this worked:
Slightly modified annotation, because altering behavior of value turned out to be more difficult.
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#RestController
#RequestMapping
public #interface MyApi {
#AliasFor(annotation = RequestMapping.class, attribute = "path")
String apiPath();
}
Bean Annotation Processor
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
#Component
public class MyApiProcessor implements BeanPostProcessor {
private static final String ANNOTATIONS = "annotations";
private static final String ANNOTATION_DATA = "annotationData";
public Object postProcessBeforeInitialization(#NonNull final Object bean, String beanName) throws BeansException {
MyApi myApi = bean.getClass().getAnnotation(MyApi.class);
if (myApi != null) {
MyApi alteredMyApi = new MyApi() {
#Override
public Class<? extends Annotation> annotationType() {
return MyApi.class;
}
#Override
public String apiPath() {
return "/api" + myApi.apiPath();
}
};
alterAnnotationOn(bean.getClass(), MyApi.class, alteredMyApi);
}
return bean;
}
#Override
public Object postProcessAfterInitialization(#NonNull Object bean, String beanName) throws BeansException {
return bean;
}
#SuppressWarnings("unchecked")
private static void alterAnnotationOn(Class clazzToLookFor, Class<? extends Annotation> annotationToAlter, Annotation annotationValue) {
try {
// In JDK8 Class has a private method called annotationData().
// We first need to invoke it to obtain a reference to AnnotationData class which is a private class
Method method = Class.class.getDeclaredMethod(ANNOTATION_DATA, null);
method.setAccessible(true);
// Since AnnotationData is a private class we cannot create a direct reference to it. We will have to manage with just Object
Object annotationData = method.invoke(clazzToLookFor);
// We now look for the map called "annotations" within AnnotationData object.
Field annotations = annotationData.getClass().getDeclaredField(ANNOTATIONS);
annotations.setAccessible(true);
Map<Class<? extends Annotation>, Annotation> map = (Map<Class<? extends Annotation>, Annotation>) annotations.get(annotationData);
map.put(annotationToAlter, annotationValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Controller:
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;
#MyApi(apiPath = "/users")
public class UserController {
#GetMapping("/stackoverflow")
public String get() {
return "Best users";
}
}
Using Spring Boot, I've created an example application.
package hello;
import org.springframework.web.bind.annotation.RestController;
import constraint.CheckHelloId;
import dto.HelloDto;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
#RestController
public class HelloController {
#RequestMapping(value = "/", method = RequestMethod.POST)
public String index(#RequestBody #Valid final HelloDto hello) {
hello.setId(null);
validateFromMethodHeader(hello);
return "Greetings from Spring Boot!";
}
private void validateFromMethodHeader(#CheckHelloId final HelloDto helloDto) {
System.out.println("Validating DTO...");
}
}
I'm trying to add a custom constraint to the DTO HelloDto in the private method that checks if the id field is null or not.
The interface:
package constraint;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({ FIELD, ANNOTATION_TYPE, TYPE, METHOD, PARAMETER })
#Retention(RUNTIME)
#Constraint(validatedBy = CheckHelloIdValidator.class)
#Documented
public #interface CheckHelloId {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The validator:
package constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import org.apache.commons.lang3.StringUtils;
import dto.HelloDto;
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CheckHelloIdValidator implements ConstraintValidator<CheckHelloId, HelloDto> {
private static final String ID_VALIDATION_ERROR = "Null or blank ID.";
#Override
public void initialize(CheckHelloId arg0) {
}
#Override
public boolean isValid(HelloDto helloDto, ConstraintValidatorContext context) {
if (StringUtils.isBlank(helloDto.getId())){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(ID_VALIDATION_ERROR).addConstraintViolation();
return false;
}
return true;
}
}
The DTO:
package dto;
import java.io.Serializable;
public class HelloDto implements Serializable {
private static final long serialVersionUID = 8792903048191496378L;
private String id;
private String message;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
As seen in the private method at the controller, I'm trying to apply the validation of the DTO at a parameter level (I know I can just do it at the index via entity validation, but I want to test this concrete case, that's why I set the id field as null at the controller).
When I run the application and perform the call, the constraint does not apply in the private method, even when the id field is null. Could anybody shed some light on this? Thank you in advance.
To validate consuming json on rest controller beside annotated method parameters #RequestBody #Valid final HelloDto hello you have to annotate filed of your json data class with special constraints. For you case it enough to use hibernate validation constraints #NotNull or #NotEmpty:
package dto;
import java.io.Serializable;
public class HelloDto implements Serializable {
private static final long serialVersionUID = 8792903048191496378L;
#NotEmpty
private String id;
I want to validate two fields of a Request Class in manner that Either one field is valid OR another field is valid.
Eg:
Request Bean
public class CarRequest {
#NotEmpty
private String customerName;
#NotEmpty
private String customerId;
Controller Method
public #ResponseBody CarResponse addCar(
#ModelAttribute #Valid CarRequest request, BindingResult results)
throws RuntimeException, ValidationException {
if (results.hasErrors()) {
LOG.error("error occured while adding the car");
throw new ValidationException(
"Error Occoured while validiating car request");
}
}
Here I want to check that either customerName should be NotEmpty OR customerId should be NotEmpty. then my validation should pass. How can I implement it . Please suggest!!
You need to create custom validator to validate multiple fields.
create a custom annotation:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Documented
#Constraint(validatedBy = CarRequestValidator.class)
#Target({ ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestAnnotation {
String message() default "{RequestAnnotation}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
create a custom validator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CarRequestValidator implements
ConstraintValidator<RequestAnnotation, CarRequest> {
#Override
public void initialize(RequestAnnotation constraintAnnotation) {
}
#Override
public boolean isValid(CarRequest value, ConstraintValidatorContext context) {
// validation logic goes here
return false;
}
}
Now, annotate your model with custom annotation:
#RequestAnnotation
public class CarRequest {
private String customerName;
private String customerId;
}