diff --git a/arconia-spring-boot-autoconfigure/build.gradle b/arconia-spring-boot-autoconfigure/build.gradle index b12c89b..7189619 100644 --- a/arconia-spring-boot-autoconfigure/build.gradle +++ b/arconia-spring-boot-autoconfigure/build.gradle @@ -23,7 +23,7 @@ dependencies { optional 'jakarta.servlet:jakarta.servlet-api' optional 'org.springframework:spring-context' - optional 'org.springframework:spring-web' + optional 'org.springframework:spring-webmvc' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/MultitenancyWebAutoConfiguration.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/MultitenancyWebAutoConfiguration.java index 03dc6fc..2958705 100644 --- a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/MultitenancyWebAutoConfiguration.java +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/MultitenancyWebAutoConfiguration.java @@ -11,7 +11,7 @@ */ @AutoConfiguration(after = MultitenancyCoreAutoConfiguration.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@Import(HttpTenantResolutionConfiguration.class) +@Import({ HttpTenantResolutionConfiguration.class, WebMvcConfiguration.class }) public class MultitenancyWebAutoConfiguration { } diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/WebMvcConfiguration.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/WebMvcConfiguration.java new file mode 100644 index 0000000..0d6ebe4 --- /dev/null +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/WebMvcConfiguration.java @@ -0,0 +1,22 @@ +package io.arconia.autoconfigure.multitenancy.web; + +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import io.arconia.web.multitenancy.context.annotations.TenantIdentifierArgumentResolver; + +/** + * Register Arconia-specific Spring Web MVC configuration. + */ +@Configuration(proxyBeanMethods = false) +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new TenantIdentifierArgumentResolver()); + } + +} diff --git a/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifier.java b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifier.java new file mode 100644 index 0000000..e07724e --- /dev/null +++ b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifier.java @@ -0,0 +1,17 @@ +package io.arconia.web.multitenancy.context.annotations; + +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; + +/** + * Annotation that is used to resolve the current tenant identifier as a method argument. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantIdentifier { + +} diff --git a/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolver.java b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolver.java new file mode 100644 index 0000000..9a2fedc --- /dev/null +++ b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolver.java @@ -0,0 +1,43 @@ +package io.arconia.web.multitenancy.context.annotations; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import io.arconia.core.multitenancy.context.TenantContextHolder; + +/** + * Allows resolving the current tenant identifier using the {@link TenantIdentifier} + * annotation. + *

+ * Example: + * + *

+ * @RestController
+ * class MyRestController {
+ *     @GetMapping("/tenant")
+ *     String getCurrentTenant(@CurrentTenantIdentifier String tenantIdentifier) {
+ *         return tenantIdentifier;
+ *     }
+ * }
+ * 
+ */ +public final class TenantIdentifierArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(TenantIdentifier.class) != null + && parameter.getParameterType().getTypeName().equals(String.class.getTypeName()); + } + + @Nullable + @Override + public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { + return TenantContextHolder.getTenantIdentifier(); + } + +} diff --git a/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/package-info.java b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/package-info.java new file mode 100644 index 0000000..c153b52 --- /dev/null +++ b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/annotations/package-info.java @@ -0,0 +1,6 @@ +@NonNullApi +@NonNullFields +package io.arconia.web.multitenancy.context.annotations; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/arconia-web/src/test/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolverTests.java b/arconia-web/src/test/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolverTests.java new file mode 100644 index 0000000..3b65d27 --- /dev/null +++ b/arconia-web/src/test/java/io/arconia/web/multitenancy/context/annotations/TenantIdentifierArgumentResolverTests.java @@ -0,0 +1,80 @@ +package io.arconia.web.multitenancy.context.annotations; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.util.ReflectionUtils; + +import io.arconia.core.multitenancy.context.TenantContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link TenantIdentifierArgumentResolver}. + */ +class TenantIdentifierArgumentResolverTests { + + private final TenantIdentifierArgumentResolver argumentResolver = new TenantIdentifierArgumentResolver(); + + @AfterEach + void cleanup() { + TenantContextHolder.clear(); + } + + @Test + void doesNotSupportParameterWithoutAnnotation() { + assertThat(argumentResolver.supportsParameter(showTenantIdentifierNoAnnotation())).isFalse(); + } + + @Test + void supportsParameterWithAnnotation() { + assertThat(argumentResolver.supportsParameter(showTenantIdentifierAnnotation())).isTrue(); + } + + @Test + void doesNotSupportParameterWithWrongType() { + assertThat(argumentResolver.supportsParameter(showTenantIdentifierErrorOnInvalidType())).isFalse(); + } + + @Test + void resolveTenantIdentifierArgument() { + String expectedTenantIdentifier = "acme"; + TenantContextHolder.setTenantIdentifier(expectedTenantIdentifier); + String actualTenantIdentifier = (String) argumentResolver.resolveArgument(showTenantIdentifierAnnotation(), + null, null, null); + assertThat(actualTenantIdentifier).isEqualTo(expectedTenantIdentifier); + } + + private MethodParameter showTenantIdentifierNoAnnotation() { + return getMethodParameter("showTenantIdentifierNoAnnotation", String.class); + } + + private MethodParameter showTenantIdentifierAnnotation() { + return getMethodParameter("showTenantIdentifierAnnotation", String.class); + } + + private MethodParameter showTenantIdentifierErrorOnInvalidType() { + return getMethodParameter("showTenantIdentifierErrorOnInvalidType", Long.class); + } + + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { + Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); + return new MethodParameter(method, 0); + } + + static class TestController { + + public void showTenantIdentifierNoAnnotation(String tenantIdentifier) { + } + + public void showTenantIdentifierAnnotation(@TenantIdentifier String tenantIdentifier) { + } + + public void showTenantIdentifierErrorOnInvalidType(@TenantIdentifier Long tenantIdentifier) { + } + + } + +}