Skip to content

Commit

Permalink
Add support for disabling constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
agrancaric committed Oct 26, 2023
1 parent a7f51b3 commit 6122767
Show file tree
Hide file tree
Showing 22 changed files with 1,666 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.api.constraint;

import jakarta.validation.Payload;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Adds support for disabling constraints. Constraints can be disabled either on property, method or type level.
*/
@SuppressWarnings("unused")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(DisableConstraints.List.class)
@Documented
public @interface DisableConstraints {

/**
* Array of constraint types to disable.
* @return array of constraint types to disable
*/
Class<? extends Annotation>[] value();

/**
* Property name for which to disable constraints (only applicable on type).
* @return property name for which to disable constraints
*/
String propertyName() default "";

Class<? extends Payload>[] payload() default {};

/**
* Defines several {@link DisableConstraints} annotations on the same element.
*
* @see DisableConstraints
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {

DisableConstraints[] value();
}
}
62 changes: 62 additions & 0 deletions nrich-validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,65 @@ public class ExampleTypeRequest {
```

In an example above the property uuid is required to be a valid UUID format. The format check is achieved by calling `validateUuid` method of `spelValidationTestService` bean.

Module also adds support for disabling inherited constraints with the following annotation:

#### DisableConstraints

Assuming the following parent class:

```java

@Setter
@Getter
public class ExampleParentRequest {

@Size(max = 200)
@NotBlank
private String name;

@NotNull
private Integer age;

}


```

Constraints on name and age can be disabled by either using type annotation (if property name is not found it will just be ignored):

```java

@DisableConstraints(value = { NotBlank.class, Size.class }, propertyName = "name")
@DisableConstraints(value = NotNull.class, propertyName = "age")
@Setter
@Getter
public class ExampleTypeRequest extends ExampleParentRequest {


}


```

or by using method/property annotation:

```java

public class ExampleMethodRequest extends ExampleParentRequest {

@DisableConstraints({ NotBlank.class, Size.class })
@Override
public String getName() {
return super.getName();
}

@DisableConstraints(NotNull.class)
@Override
public Integer getAge() {
return super.getAge();
}
}


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.constraint.support.disableconstraints;

import lombok.RequiredArgsConstructor;

import jakarta.validation.metadata.BeanDescriptor;
import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.ConstructorDescriptor;
import jakarta.validation.metadata.MethodDescriptor;
import jakarta.validation.metadata.MethodType;
import jakarta.validation.metadata.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class BeanDescriptorAdapter implements BeanDescriptor {

private final BeanDescriptor target;

private final Map<String, List<Class<? extends Annotation>>> disabledConstraintsPathMap;

@Override
public PropertyDescriptor getConstraintsForProperty(String propertyName) {
String path = PathUtil.getPath(target.getElementClass(), propertyName);

return new PropertyDescriptorAdapter(target.getConstraintsForProperty(propertyName), disabledConstraintsPathMap.getOrDefault(path, Collections.emptyList()));
}

@Override
public Set<PropertyDescriptor> getConstrainedProperties() {
return target.getConstrainedProperties().stream()
.map(propertyDescriptor -> getConstraintsForProperty(propertyDescriptor.getPropertyName()))
.collect(Collectors.toSet());
}

@Override
public ConstraintFinder findConstraints() {
ConstraintFinder finder = target.findConstraints();
String path = PathUtil.getPath(target.getElementClass(), null);

return new ConstraintFinderAdapter(finder, disabledConstraintsPathMap.getOrDefault(path, Collections.emptyList()));
}

@Override
public boolean isBeanConstrained() {
return target.isBeanConstrained();
}

@Override
public MethodDescriptor getConstraintsForMethod(String methodName, Class<?>... parameterTypes) {
return target.getConstraintsForMethod(methodName, parameterTypes);
}

@Override
public Set<MethodDescriptor> getConstrainedMethods(MethodType methodType, MethodType... methodTypes) {
return target.getConstrainedMethods(methodType, methodTypes);
}

@Override
public ConstructorDescriptor getConstraintsForConstructor(Class<?>... parameterTypes) {
return target.getConstraintsForConstructor(parameterTypes);
}

@Override
public Set<ConstructorDescriptor> getConstrainedConstructors() {
return target.getConstrainedConstructors();
}

@Override
public boolean hasConstraints() {
return target.hasConstraints();
}

@Override
public Class<?> getElementClass() {
return target.getElementClass();
}

@Override
public Set<ConstraintDescriptor<?>> getConstraintDescriptors() {
return target.getConstraintDescriptors();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.constraint.support.disableconstraints;

import lombok.RequiredArgsConstructor;

import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.ElementDescriptor;
import jakarta.validation.metadata.Scope;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class ConstraintFinderAdapter implements ElementDescriptor.ConstraintFinder {

private final ElementDescriptor.ConstraintFinder target;

private final List<Class<? extends Annotation>> disabledConstraintTypes;

@Override
public Set<ConstraintDescriptor<?>> getConstraintDescriptors() {
Set<ConstraintDescriptor<?>> constraintDescriptors = target.getConstraintDescriptors();

return constraintDescriptors.stream().filter(constraintDescriptor -> !disabledConstraintTypes.contains(constraintDescriptor.getAnnotation().annotationType()))
.collect(Collectors.toSet());
}

@Override
public ElementDescriptor.ConstraintFinder unorderedAndMatchingGroups(Class<?>... groups) {
target.unorderedAndMatchingGroups(groups);

return this;
}

@Override
public ElementDescriptor.ConstraintFinder lookingAt(Scope scope) {
target.lookingAt(scope);

return this;
}

@Override
public ElementDescriptor.ConstraintFinder declaredOn(ElementType... types) {
target.declaredOn(types);

return this;
}

@Override
public boolean hasConstraints() {
return target.hasConstraints();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package net.croz.nrich.validation.constraint.support.disableconstraints;

import net.croz.nrich.validation.api.constraint.DisableConstraints;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DisableConstraintsAnnotationProcessor {

private static final Pattern GETTER_METHOD_PATTERN = Pattern.compile("^(get|is)([A-Z].*)$");

private final ConcurrentMap<Class<?>, Map<String, List<Class<? extends Annotation>>>> disableConstraintsHolderMap = new ConcurrentHashMap<>();

public Map<String, List<Class<? extends Annotation>>> getDisabledConstraintForType(Class<?> type) {
return disableConstraintsHolderMap.computeIfAbsent(type, this::createDisableConstraintsPathMap);
}

private Map<String, List<Class<? extends Annotation>>> createDisableConstraintsPathMap(Class<?> type) {
Map<String, List<Class<? extends Annotation>>> pathHolderMap = new HashMap<>();

ReflectionUtils.doWithFields(type, field -> {
DisableConstraints[] disableConstraints = field.getAnnotationsByType(DisableConstraints.class);

registerDisableConstraints(pathHolderMap, disableConstraints, PathUtil.getPath(type, field.getName()), false);
});

ReflectionUtils.doWithMethods(type, method -> {
DisableConstraints[] disableConstraints = method.getAnnotationsByType(DisableConstraints.class);
Matcher matcher = GETTER_METHOD_PATTERN.matcher(method.getName());

if (matcher.matches()) {
String propertyName = StringUtils.uncapitalize(matcher.group(2));

registerDisableConstraints(pathHolderMap, disableConstraints, PathUtil.getPath(type, propertyName), false);
}
});

DisableConstraints[] disableConstraints = type.getAnnotationsByType(DisableConstraints.class);

registerDisableConstraints(pathHolderMap, disableConstraints, PathUtil.getPath(type, null), true);

return pathHolderMap;
}

private void registerDisableConstraints(Map<String, List<Class<? extends Annotation>>> pathHolderMap, DisableConstraints[] disableConstraints, String path, boolean isTypeAnnotation) {
Arrays.stream(disableConstraints).forEach(disableConstraint -> {
if (StringUtils.hasText(disableConstraint.propertyName()) && !isTypeAnnotation) {
throw new IllegalArgumentException("Property name not allowed on method or property annotation.");
}

String fullPath = PathUtil.getPath(path, disableConstraint.propertyName());

pathHolderMap.put(fullPath, List.of(disableConstraint.value()));
});
}
}
Loading

0 comments on commit 6122767

Please sign in to comment.