package core.framework.impl.validate; import core.framework.api.util.Exceptions; import core.framework.api.util.Lists; import core.framework.api.util.Maps; import core.framework.api.validate.Length; import core.framework.api.validate.Max; import core.framework.api.validate.Min; import core.framework.api.validate.NotEmpty; import core.framework.api.validate.NotNull; import core.framework.api.validate.Pattern; import core.framework.api.validate.Size; import core.framework.impl.reflect.Classes; import core.framework.impl.reflect.Fields; import core.framework.impl.reflect.GenericTypes; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; /** * @author neo */ public class ValidatorBuilder { private final Type instanceType; private final Function<Field, String> fieldNameProvider; public ValidatorBuilder(Type instanceType, Function<Field, String> fieldNameProvider) { this.instanceType = instanceType; this.fieldNameProvider = fieldNameProvider; } public Validator build() { Class<?> targetClass; if (GenericTypes.isList(instanceType)) { // type validator ensured list can only be generic type in advance targetClass = GenericTypes.listValueClass(instanceType); } else if (GenericTypes.isOptional(instanceType)) { targetClass = GenericTypes.optionalValueClass(instanceType); } else { targetClass = GenericTypes.rawClass(instanceType); } if (isValueClass(targetClass)) return new Validator(null); // not validate top level value, since no place to put annotation Optional<ObjectValidator> objectValidator = createObjectValidator(targetClass, null); if (objectValidator.isPresent()) { if (GenericTypes.isList(instanceType)) { return new Validator(new ListValidator(Lists.newArrayList(objectValidator.get()))); } else if (GenericTypes.isOptional(instanceType)) { return new Validator(new OptionalValidator(Lists.newArrayList(objectValidator.get()))); } else { return new Validator(objectValidator.get()); } } return new Validator(null); } private Optional<ObjectValidator> createObjectValidator(Class<?> instanceClass, String parentPath) { Map<Field, List<FieldValidator>> validators = Maps.newLinkedHashMap(); for (Field field : Classes.instanceFields(instanceClass)) { createValidators(field, parentPath) .ifPresent(fieldValidators -> validators.put(field, fieldValidators)); } if (validators.isEmpty()) return Optional.empty(); return Optional.of(new ObjectValidator(validators)); } private String fieldPath(String parentPath, Field field) { String fieldName = fieldNameProvider.apply(field); if (parentPath == null) return fieldName; return parentPath + "." + fieldName; } private Optional<List<FieldValidator>> createValidators(Field field, String parentPath) { List<FieldValidator> validators = Lists.newArrayList(); Type fieldType = field.getGenericType(); createNotNullValidator(field, parentPath).ifPresent(validators::add); createSizeValidator(field, parentPath, fieldType).ifPresent(validators::add); if (GenericTypes.isList(fieldType)) { List<FieldValidator> valueValidators = Lists.newArrayList(); addValidators(field, parentPath, GenericTypes.listValueClass(fieldType), valueValidators); if (!valueValidators.isEmpty()) validators.add(new ListValidator(valueValidators)); } else if (GenericTypes.isMap(fieldType)) { List<FieldValidator> valueValidators = Lists.newArrayList(); addValidators(field, parentPath, GenericTypes.mapValueClass(fieldType), valueValidators); if (!valueValidators.isEmpty()) validators.add(new MapValidator(valueValidators)); } else if (GenericTypes.isOptional(fieldType)) { List<FieldValidator> valueValidators = Lists.newArrayList(); addValidators(field, parentPath, GenericTypes.optionalValueClass(fieldType), valueValidators); if (!valueValidators.isEmpty()) validators.add(new OptionalValidator(valueValidators)); } else { Class<?> fieldClass = GenericTypes.rawClass(fieldType); addValidators(field, parentPath, fieldClass, validators); } if (validators.isEmpty()) return Optional.empty(); return Optional.of(validators); } private void addValidators(Field field, String parentPath, Class<?> targetClass, List<FieldValidator> validators) { createNotEmptyValidator(field, parentPath, targetClass).ifPresent(validators::add); createPatternValidator(field, parentPath, targetClass).ifPresent(validators::add); createLengthValidator(field, parentPath, targetClass).ifPresent(validators::add); createMinValidator(field, parentPath, targetClass).ifPresent(validators::add); createMaxValidator(field, parentPath, targetClass).ifPresent(validators::add); if (!isValueClass(targetClass)) createObjectValidator(targetClass, fieldPath(parentPath, field)).ifPresent(validators::add); } private Optional<NotNullValidator> createNotNullValidator(Field field, String parentPath) { NotNull notNull = field.getDeclaredAnnotation(NotNull.class); if (notNull != null) return Optional.of(new NotNullValidator(fieldPath(parentPath, field), notNull.message())); return Optional.empty(); } private Optional<NotEmptyValidator> createNotEmptyValidator(Field field, String parentPath, Class<?> targetClass) { NotEmpty notEmpty = field.getDeclaredAnnotation(NotEmpty.class); if (notEmpty != null) { if (!String.class.equals(targetClass)) throw Exceptions.error("@NotEmpty must on String, Optional<String>, List<String> or Map<String, String>, field={}, fieldClass={}", Fields.path(field), targetClass.getCanonicalName()); return Optional.of(new NotEmptyValidator(fieldPath(parentPath, field), notEmpty.message())); } return Optional.empty(); } private Optional<PatternValidator> createPatternValidator(Field field, String parentPath, Class<?> targetClass) { Pattern pattern = field.getDeclaredAnnotation(Pattern.class); if (pattern != null) { if (!String.class.equals(targetClass)) throw Exceptions.error("@Pattern must on String, Optional<String>, List<String> or Map<String, String>, field={}, fieldClass={}", Fields.path(field), targetClass.getCanonicalName()); return Optional.of(new PatternValidator(pattern.value(), fieldPath(parentPath, field), pattern.message())); } return Optional.empty(); } private Optional<LengthValidator> createLengthValidator(Field field, String parentPath, Class<?> targetClass) { Length length = field.getDeclaredAnnotation(Length.class); if (length != null) { if (!String.class.equals(targetClass)) throw Exceptions.error("@Length must on String, Optional<String>, List<String> or Map<String, String>, field={}, fieldClass={}", Fields.path(field), targetClass.getCanonicalName()); return Optional.of(new LengthValidator(fieldPath(parentPath, field), length)); } return Optional.empty(); } private Optional<SizeValidator> createSizeValidator(Field field, String parentPath, Type fieldType) { Size size = field.getDeclaredAnnotation(Size.class); if (size != null) { if (!GenericTypes.isList(fieldType) && !GenericTypes.isMap(fieldType)) throw Exceptions.error("@Size must on List<?> or Map<String, ?>, field={}, fieldType={}", Fields.path(field), fieldType.getTypeName()); return Optional.of(new SizeValidator(fieldPath(parentPath, field), size)); } return Optional.empty(); } private Optional<MaxValidator> createMaxValidator(Field field, String parentPath, Class<?> targetClass) { Max max = field.getDeclaredAnnotation(Max.class); if (max != null) { if (!Number.class.isAssignableFrom(targetClass)) throw Exceptions.error("@Max must on numeric field, field={}, fieldClass={}", field, targetClass.getCanonicalName()); return Optional.of(new MaxValidator(fieldPath(parentPath, field), max)); } return Optional.empty(); } private Optional<MinValidator> createMinValidator(Field field, String parentPath, Class<?> targetClass) { Min min = field.getDeclaredAnnotation(Min.class); if (min != null) { if (!Number.class.isAssignableFrom(targetClass)) throw Exceptions.error("@Min must on numeric field, field={}, fieldClass={}", field, targetClass.getCanonicalName()); return Optional.of(new MinValidator(fieldPath(parentPath, field), min)); } return Optional.empty(); } private boolean isValueClass(Class<?> fieldClass) { return String.class.equals(fieldClass) || Integer.class.equals(fieldClass) || Boolean.class.equals(fieldClass) || Long.class.equals(fieldClass) || Double.class.equals(fieldClass) || BigDecimal.class.equals(fieldClass) || LocalDate.class.equals(fieldClass) || LocalDateTime.class.equals(fieldClass) || ZonedDateTime.class.equals(fieldClass) || Instant.class.equals(fieldClass) || fieldClass.isEnum() || "org.bson.types.ObjectId".equals(fieldClass.getCanonicalName()); // not depends on mongo jar if application doesn't include mongo driver } }