Skip to content

Commit

Permalink
adds spring starter
Browse files Browse the repository at this point in the history
  • Loading branch information
SentryMan committed May 25, 2024
1 parent 864c8f2 commit 268c45b
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 11 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<module>validator-generator</module>
<module>validator-http-plugin</module>
<module>validator-inject-plugin</module>
<module>validator-spring-starter</module>
</modules>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ private boolean isController(TypeElement typeElement) {
private void writeAdapterForConstraint(TypeElement typeElement) {
if (ElementFilter.methodsIn(typeElement.getEnclosedElements()).stream()
.noneMatch(m -> "message".equals(m.getSimpleName().toString()))) {
logError(typeElement, "Constraint annotations must contain a message method");
logError(typeElement, "Constraint annotations must contain a `String message()` method");
}
final ContraintReader beanReader = new ContraintReader(typeElement);
writeAdapter(typeElement, beanReader);
Expand Down
2 changes: 1 addition & 1 deletion validator-inject-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-validator</artifactId>
<version>1.0-RC3</version>
<version>1.3</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ public void post(ValidationContext ctx, Map<Method, MethodAdapterProvider> map)
public MethodInterceptor interceptor(Method method, ValidMethod aspectAnnotation) {

final var localeStr = aspectAnnotation.locale();
final Locale locale;
if (localeStr.isBlank()) {
locale = null;
} else {
locale = Locale.forLanguageTag(localeStr);
}
final Locale locale = localeStr.isBlank() ? null : Locale.forLanguageTag(localeStr);
final var interceptor =
new ParamInterceptor(locale, method, aspectAnnotation.throwOnParamFailure());
consumers.add(interceptor::postConstruct);
Expand Down
76 changes: 76 additions & 0 deletions validator-spring-starter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.avaje</groupId>
<artifactId>avaje-validator-parent</artifactId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>avaje-validation-spring-starter</artifactId>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-validator</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
<exclusion>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-validator-constraints</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.avaje.validation.spring.aspect;

import java.util.List;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import io.avaje.validation.Validator;
import io.avaje.validation.adapter.MethodAdapterProvider;

@Configuration
@EnableAspectJAutoProxy
@ConditionalOnBean({Validator.class, MethodAdapterProvider.class})
public class MethodValidationAutoConfiguration {

public SpringAOPMethodValidator validator(
Validator validator, List<MethodAdapterProvider> providers) throws Exception {
return new SpringAOPMethodValidator(validator, providers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.avaje.validation.spring.aspect;

import java.util.List;
import java.util.Locale;

import org.aspectj.lang.ProceedingJoinPoint;

import io.avaje.validation.adapter.ValidationAdapter;
import io.avaje.validation.adapter.ValidationContext;

final class ParamInterceptor {

private final Locale locale;
private final boolean throwOnParamFailure;
private final List<ValidationAdapter<Object>> paramValidationAdapter;
private final ValidationAdapter<Object> returnValidationAdapter;
private final ValidationContext ctx;
private final ValidationAdapter<Object[]> crossParamAdapter;

public ParamInterceptor(
Locale locale,
boolean throwOnParamFailure,
ValidationContext ctx,
List<ValidationAdapter<Object>> paramValidationAdapter,
ValidationAdapter<Object> returnValidationAdapter,
ValidationAdapter<Object[]> crossParamAdapter) {
this.locale = locale;
this.throwOnParamFailure = throwOnParamFailure;
this.paramValidationAdapter = paramValidationAdapter;
this.returnValidationAdapter = returnValidationAdapter;
this.ctx = ctx;
this.crossParamAdapter = crossParamAdapter;
}

public void invoke(ProceedingJoinPoint invocation) throws Throwable {

final var args = invocation.getArgs();
final var req = ctx.request(locale, List.of());
var i = 0;
for (final var adapter : paramValidationAdapter) {
final Object object = args[i];
adapter.validate(object, req);
++i;
}
crossParamAdapter.validate(args, req);
if (throwOnParamFailure) {
req.throwWithViolations();
}
returnValidationAdapter.validate(invocation.proceed(), req);
req.throwWithViolations();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.avaje.validation.spring.aspect;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import io.avaje.validation.ValidMethod;
import io.avaje.validation.Validator;
import io.avaje.validation.adapter.MethodAdapterProvider;

@Aspect
public class SpringAOPMethodValidator {

private final Map<Method, ParamInterceptor> interceptorMap = new HashMap<>();

public SpringAOPMethodValidator(Validator validator, List<MethodAdapterProvider> providers)
throws Exception {
var ctx = validator.context();
for (var provider : providers) {

var method = provider.method();
ValidMethod validMethod = method.getAnnotation(ValidMethod.class);
final var localeStr = validMethod.locale();
final Locale locale = localeStr.isBlank() ? null : Locale.forLanguageTag(localeStr);
var paramValidationAdapter = provider.paramAdapters(ctx);
var returnValidationAdapter = provider.returnAdapter(ctx);
var crossParamAdapter = provider.crossParamAdapter(ctx);

interceptorMap.put(
method,
new ParamInterceptor(
locale,
validMethod.throwOnParamFailure(),
ctx,
paramValidationAdapter,
returnValidationAdapter,
crossParamAdapter));
}
}

@Around("@annotation(io.avaje.validation.ValidMethod)")
public void interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
var methodValidator = interceptorMap.get(signature.getMethod());
methodValidator.invoke(joinPoint);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.avaje.validation.spring.validator;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.avaje.validation.Validator;

/** Autoconfiguration of Avaje Validator. */
@Configuration
public class AvajeValidatorAutoConfiguration {

@Bean
@ConditionalOnMissingBean
Validator validator(
@Value("${validation.failFast:false}") boolean failFast,
@Value("${validation.resourcebundle.names:#{null}}") Optional<String> resourceBundleNames,
@Value("${validation.locale.default:#{null}}") Optional<String> defaultLocal,
@Value("${validation.locale.addedLocales:#{null}}") Optional<String> addedLocales,
@Value("${validation.temporal.tolerance.value:#{null}}") Optional<String> temporalTolerance,
@Value("${validation.temporal.tolerance.chronoUnit:MILLIS}") ChronoUnit chronoUnit) {
final var validator = Validator.builder().failFast(failFast);

resourceBundleNames.map(s -> s.split(",")).ifPresent(validator::addResourceBundles);
defaultLocal.map(Locale::forLanguageTag).ifPresent(validator::setDefaultLocale);

addedLocales.stream()
.flatMap(s -> Arrays.stream(s.split(",")))
.map(Locale::forLanguageTag)
.forEach(validator::addLocales);

temporalTolerance
.map(Long::valueOf)
.ifPresent(
duration -> {
final var unit = chronoUnit;
validator.temporalTolerance(Duration.of(duration, unit));
});
return validator.build();
}
}
13 changes: 13 additions & 0 deletions validator-spring-starter/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module io.avaje.validation.spring {

exports io.avaje.validation.spring.aspect;
exports io.avaje.validation.spring.validator;

requires transitive io.avaje.validation;
requires transitive jakarta.inject;
requires transitive org.aspectj.weaver;
requires transitive spring.beans;
requires transitive spring.context;
requires transitive spring.boot.autoconfigure;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
io.avaje.validation.spring.aspect.MethodValidationAutoConfiguration
io.avaje.validation.spring.validator.AvajeValidatorAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.avaje.validation.spring.aspect;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import io.avaje.validation.ConstraintViolationException;
import io.avaje.validation.Validator;
import io.avaje.validation.spring.validator.AvajeValidatorAutoConfiguration;
import jakarta.inject.Inject;

@SpringBootTest(
classes = {
AvajeValidatorAutoConfiguration.class,
MethodValidationAutoConfiguration.class,
SpringAOPMethodValidator.class,
TestParamProvider.class,
ValidMethodClass.class,
Validator.class
})
class MethodValidationTest {

@Inject private ValidMethodClass test;

@BeforeAll
static void setUpBeforeClass() throws Exception {}

@Test
void test() {
assertThatNoException().isThrownBy(() -> test.test(List.of(""), 1, null));
}

@Test
void invalid() {
assertThatThrownBy(() -> test.test(null, 0, null))
.isInstanceOf(ConstraintViolationException.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.avaje.validation.spring.aspect;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.stereotype.Component;

import io.avaje.validation.adapter.MethodAdapterProvider;
import io.avaje.validation.adapter.ValidationAdapter;
import io.avaje.validation.adapter.ValidationContext;
import io.avaje.validation.constraints.NotEmpty;
import io.avaje.validation.constraints.NotNull;
import io.avaje.validation.constraints.Positive;
import jakarta.inject.Singleton;

@Singleton
public final class TestParamProvider implements MethodAdapterProvider {

@Override
public Method method() throws Exception {

return ValidMethodClass.class.getDeclaredMethod("test", List.class, int.class, String.class);
}

@Override
public List<ValidationAdapter<Object>> paramAdapters(ValidationContext ctx) {
return List.of(
ctx.<Object>adapter(
NotEmpty.class, Map.of("groups", Set.of(), "message", "{avaje.NotEmpty.message}"))
.list()
.andThenMulti(
ctx.adapter(
NotNull.class,
Map.of("groups", Set.of(), "message", "{avaje.NotEmpty.message}"))),
ctx.<Object>adapter(
Positive.class, Map.of("groups", Set.of(), "message", "{avaje.Positive.message}")),
ctx.noop());
}
}
Loading

0 comments on commit 268c45b

Please sign in to comment.