Skip to content

Commit 059dc75

Browse files
authored
Adds Spring Starter (#200)
* adds spring starter * Update module-info.java * Update pom.xml * Update pom.xml * add inject plugin directly to main validator * add named annotation to generated method validators Spring doesn't actually respect @singleton * working
1 parent 864c8f2 commit 059dc75

File tree

20 files changed

+386
-19
lines changed

20 files changed

+386
-19
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<module>validator-generator</module>
3838
<module>validator-http-plugin</module>
3939
<module>validator-inject-plugin</module>
40+
<module>validator-spring-starter</module>
4041
</modules>
4142

4243
<profiles>

validator-generator/src/main/java/io/avaje/validation/generator/SimpleParamBeanWriter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ private void writeClassStart() {
6464
writer
6565
.append(
6666
"""
67-
@Generated
68-
@%s
69-
public final class %s implements MethodAdapterProvider {
70-
""",
67+
@Generated("avaje-validator-generator")
68+
@Named
69+
@%s
70+
public final class %s implements MethodAdapterProvider {""",
7171
Util.shortName(diAnnotation()), adapterShortName)
7272
.eol();
7373
}

validator-generator/src/main/java/io/avaje/validation/generator/ValidMethodReader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ final class ValidMethodReader {
2525
this.params = element.getParameters();
2626
importTypes.add(type);
2727
importTypes.add("java.util.List");
28-
importTypes.add(diAnnotation());
2928
importTypes.add("java.util.Set");
3029
importTypes.add("java.util.Map");
3130
importTypes.add("io.avaje.validation.adapter.MethodAdapterProvider");
3231
importTypes.add("io.avaje.validation.adapter.ValidationAdapter");
3332
importTypes.add("io.avaje.validation.adapter.ValidationContext");
3433
importTypes.add("io.avaje.validation.spi.Generated");
3534
importTypes.add("java.lang.reflect.Method");
35+
final var diAnnotation = diAnnotation();
36+
importTypes.add(diAnnotation);
37+
importTypes.add(diAnnotation.contains("javax") ? "javax.inject.Named" : "jakarta.inject.Named");
3638
paramAnnotations = params.stream().map(ElementAnnotationContainer::create).toList();
3739
returnElementAnnotation = ElementAnnotationContainer.create(element);
3840
}

validator-generator/src/main/java/io/avaje/validation/generator/ValidationProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ private boolean isController(TypeElement typeElement) {
269269
private void writeAdapterForConstraint(TypeElement typeElement) {
270270
if (ElementFilter.methodsIn(typeElement.getEnclosedElements()).stream()
271271
.noneMatch(m -> "message".equals(m.getSimpleName().toString()))) {
272-
logError(typeElement, "Constraint annotations must contain a message method");
272+
logError(typeElement, "Constraint annotations must contain a `String message()` method");
273273
}
274274
final ContraintReader beanReader = new ContraintReader(typeElement);
275275
writeAdapter(typeElement, beanReader);

validator-http-plugin/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<dependency>
1616
<groupId>io.avaje</groupId>
1717
<artifactId>avaje-validator</artifactId>
18-
<version>1.2</version>
18+
<version>1.4</version>
1919
<scope>provided</scope>
2020
<optional>true</optional>
2121
</dependency>

validator-inject-plugin/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<dependency>
1616
<groupId>io.avaje</groupId>
1717
<artifactId>avaje-validator</artifactId>
18-
<version>1.0-RC3</version>
18+
<version>1.4</version>
1919
<scope>provided</scope>
2020
<optional>true</optional>
2121
</dependency>

validator-inject-plugin/src/main/java/io/avaje/validation/inject/aspect/AOPMethodValidator.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,7 @@ public void post(ValidationContext ctx, Map<Method, MethodAdapterProvider> map)
3030
public MethodInterceptor interceptor(Method method, ValidMethod aspectAnnotation) {
3131

3232
final var localeStr = aspectAnnotation.locale();
33-
final Locale locale;
34-
if (localeStr.isBlank()) {
35-
locale = null;
36-
} else {
37-
locale = Locale.forLanguageTag(localeStr);
38-
}
33+
final Locale locale = localeStr.isBlank() ? null : Locale.forLanguageTag(localeStr);
3934
final var interceptor =
4035
new ParamInterceptor(locale, method, aspectAnnotation.throwOnParamFailure());
4136
consumers.add(interceptor::postConstruct);

validator-inject-plugin/src/main/java/module-info.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
module io.avaje.validation.plugin {
22

3-
exports io.avaje.validation.inject.aspect;
4-
53
requires transitive io.avaje.validation;
64
requires transitive io.avaje.inject;
75

validator-spring-starter/pom.xml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>io.avaje</groupId>
7+
<artifactId>avaje-validator-parent</artifactId>
8+
<version>1.5-SNAPSHOT</version>
9+
</parent>
10+
<artifactId>avaje-validator-spring-starter</artifactId>
11+
12+
<dependencyManagement>
13+
<dependencies>
14+
<dependency>
15+
<groupId>org.springframework.boot</groupId>
16+
<artifactId>spring-boot-dependencies</artifactId>
17+
<version>3.3.0</version>
18+
<type>pom</type>
19+
<scope>import</scope>
20+
</dependency>
21+
</dependencies>
22+
</dependencyManagement>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>io.avaje</groupId>
27+
<artifactId>avaje-validator</artifactId>
28+
<version>${project.version}</version>
29+
</dependency>
30+
31+
<dependency>
32+
<groupId>jakarta.inject</groupId>
33+
<artifactId>jakarta.inject-api</artifactId>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-starter-aop</artifactId>
39+
<exclusions>
40+
<exclusion>
41+
<groupId>org.springframework.boot</groupId>
42+
<artifactId>spring-boot-starter-logging</artifactId>
43+
</exclusion>
44+
<exclusion>
45+
<groupId>org.yaml</groupId>
46+
<artifactId>snakeyaml</artifactId>
47+
</exclusion>
48+
<exclusion>
49+
<groupId>io.micrometer</groupId>
50+
<artifactId>micrometer-observation</artifactId>
51+
</exclusion>
52+
<exclusion>
53+
<groupId>org.springframework</groupId>
54+
<artifactId>spring-core</artifactId>
55+
</exclusion>
56+
<exclusion>
57+
<groupId>jakarta.annotation</groupId>
58+
<artifactId>jakarta.annotation-api</artifactId>
59+
</exclusion>
60+
</exclusions>
61+
</dependency>
62+
63+
<dependency>
64+
<groupId>io.avaje</groupId>
65+
<artifactId>avaje-validator-constraints</artifactId>
66+
<version>${project.version}</version>
67+
<scope>test</scope>
68+
</dependency>
69+
70+
<dependency>
71+
<groupId>org.springframework.boot</groupId>
72+
<artifactId>spring-boot-starter-test</artifactId>
73+
<scope>test</scope>
74+
</dependency>
75+
</dependencies>
76+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.avaje.validation.spring.aspect;
2+
3+
import java.util.List;
4+
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.context.annotation.EnableAspectJAutoProxy;
9+
10+
import io.avaje.validation.Validator;
11+
import io.avaje.validation.adapter.MethodAdapterProvider;
12+
13+
@Configuration
14+
@EnableAspectJAutoProxy
15+
public class MethodValidationAutoConfiguration {
16+
17+
@Bean
18+
public SpringAOPMethodValidator methodValidator(
19+
Validator validator, List<MethodAdapterProvider> providers) throws Exception {
20+
return new SpringAOPMethodValidator(validator, providers);
21+
}
22+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.avaje.validation.spring.aspect;
2+
3+
import java.util.List;
4+
import java.util.Locale;
5+
6+
import org.aspectj.lang.ProceedingJoinPoint;
7+
8+
import io.avaje.validation.adapter.ValidationAdapter;
9+
import io.avaje.validation.adapter.ValidationContext;
10+
11+
final class ParamInterceptor {
12+
13+
private final Locale locale;
14+
private final boolean throwOnParamFailure;
15+
private final List<ValidationAdapter<Object>> paramValidationAdapter;
16+
private final ValidationAdapter<Object> returnValidationAdapter;
17+
private final ValidationContext ctx;
18+
private final ValidationAdapter<Object[]> crossParamAdapter;
19+
20+
public ParamInterceptor(
21+
Locale locale,
22+
boolean throwOnParamFailure,
23+
ValidationContext ctx,
24+
List<ValidationAdapter<Object>> paramValidationAdapter,
25+
ValidationAdapter<Object> returnValidationAdapter,
26+
ValidationAdapter<Object[]> crossParamAdapter) {
27+
this.locale = locale;
28+
this.throwOnParamFailure = throwOnParamFailure;
29+
this.paramValidationAdapter = paramValidationAdapter;
30+
this.returnValidationAdapter = returnValidationAdapter;
31+
this.ctx = ctx;
32+
this.crossParamAdapter = crossParamAdapter;
33+
}
34+
35+
public void invoke(ProceedingJoinPoint invocation) throws Throwable {
36+
37+
final var args = invocation.getArgs();
38+
final var req = ctx.request(locale, List.of());
39+
var i = 0;
40+
for (final var adapter : paramValidationAdapter) {
41+
final Object object = args[i];
42+
adapter.validate(object, req);
43+
++i;
44+
}
45+
crossParamAdapter.validate(args, req);
46+
if (throwOnParamFailure) {
47+
req.throwWithViolations();
48+
}
49+
returnValidationAdapter.validate(invocation.proceed(), req);
50+
req.throwWithViolations();
51+
}
52+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.avaje.validation.spring.aspect;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Locale;
7+
import java.util.Map;
8+
9+
import org.aspectj.lang.ProceedingJoinPoint;
10+
import org.aspectj.lang.annotation.Around;
11+
import org.aspectj.lang.annotation.Aspect;
12+
import org.aspectj.lang.reflect.MethodSignature;
13+
14+
import io.avaje.validation.ValidMethod;
15+
import io.avaje.validation.Validator;
16+
import io.avaje.validation.adapter.MethodAdapterProvider;
17+
18+
@Aspect
19+
public class SpringAOPMethodValidator {
20+
21+
private final Map<Method, ParamInterceptor> interceptorMap = new HashMap<>();
22+
23+
public SpringAOPMethodValidator(Validator validator, List<MethodAdapterProvider> providers)
24+
throws Exception {
25+
var ctx = validator.context();
26+
for (var provider : providers) {
27+
28+
var method = provider.method();
29+
ValidMethod validMethod = method.getAnnotation(ValidMethod.class);
30+
final var localeStr = validMethod.locale();
31+
final Locale locale = localeStr.isBlank() ? null : Locale.forLanguageTag(localeStr);
32+
var paramValidationAdapter = provider.paramAdapters(ctx);
33+
var returnValidationAdapter = provider.returnAdapter(ctx);
34+
var crossParamAdapter = provider.crossParamAdapter(ctx);
35+
36+
interceptorMap.put(
37+
method,
38+
new ParamInterceptor(
39+
locale,
40+
validMethod.throwOnParamFailure(),
41+
ctx,
42+
paramValidationAdapter,
43+
returnValidationAdapter,
44+
crossParamAdapter));
45+
}
46+
}
47+
48+
@Around("@annotation(io.avaje.validation.ValidMethod)")
49+
public void interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
50+
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
51+
var methodValidator = interceptorMap.get(signature.getMethod());
52+
methodValidator.invoke(joinPoint);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.avaje.validation.spring.validator;
2+
3+
import java.time.Duration;
4+
import java.time.temporal.ChronoUnit;
5+
import java.util.Arrays;
6+
import java.util.Locale;
7+
import java.util.Optional;
8+
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.Configuration;
13+
14+
import io.avaje.validation.Validator;
15+
16+
/** Autoconfiguration of Avaje Validator. */
17+
@Configuration
18+
public class AvajeValidatorAutoConfiguration {
19+
20+
@Bean
21+
@ConditionalOnMissingBean
22+
Validator validator(
23+
@Value("${validation.failFast:false}") boolean failFast,
24+
@Value("${validation.resourcebundle.names:#{null}}") Optional<String> resourceBundleNames,
25+
@Value("${validation.locale.default:#{null}}") Optional<String> defaultLocal,
26+
@Value("${validation.locale.addedLocales:#{null}}") Optional<String> addedLocales,
27+
@Value("${validation.temporal.tolerance.value:#{null}}") Optional<String> temporalTolerance,
28+
@Value("${validation.temporal.tolerance.chronoUnit:MILLIS}") ChronoUnit chronoUnit) {
29+
final var validator = Validator.builder().failFast(failFast);
30+
31+
resourceBundleNames.map(s -> s.split(",")).ifPresent(validator::addResourceBundles);
32+
defaultLocal.map(Locale::forLanguageTag).ifPresent(validator::setDefaultLocale);
33+
34+
addedLocales.stream()
35+
.flatMap(s -> Arrays.stream(s.split(",")))
36+
.map(Locale::forLanguageTag)
37+
.forEach(validator::addLocales);
38+
39+
temporalTolerance
40+
.map(Long::valueOf)
41+
.ifPresent(
42+
duration -> {
43+
final var unit = chronoUnit;
44+
validator.temporalTolerance(Duration.of(duration, unit));
45+
});
46+
return validator.build();
47+
}
48+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module io.avaje.validation.spring {
2+
3+
exports io.avaje.validation.spring.aspect;
4+
exports io.avaje.validation.spring.validator;
5+
6+
requires transitive io.avaje.validation;
7+
requires transitive jakarta.inject;
8+
requires transitive org.aspectj.weaver;
9+
requires transitive spring.beans;
10+
requires transitive spring.context;
11+
requires transitive spring.boot.autoconfigure;
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
io.avaje.validation.spring.validator.AvajeValidatorAutoConfiguration
2+
io.avaje.validation.spring.aspect.MethodValidationAutoConfiguration

0 commit comments

Comments
 (0)