diff --git a/src/main/java/org/springframework/data/web/ProjectingJackson2JsonDecoder.java b/src/main/java/org/springframework/data/web/ProjectingJackson2JsonDecoder.java new file mode 100644 index 0000000000..6cd6289046 --- /dev/null +++ b/src/main/java/org/springframework/data/web/ProjectingJackson2JsonDecoder.java @@ -0,0 +1,167 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import org.reactivestreams.Publisher; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.MimeType; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * {@link org.springframework.http.codec.HttpMessageDecoder} implementation to enable projected JSON binding to + * interfaces annotated with {@link ProjectedPayload}. + * + * @author Matías Hermosilla + * @since 3.0 + */ +public class ProjectingJackson2JsonDecoder extends Jackson2JsonDecoder + implements BeanClassLoaderAware, BeanFactoryAware { + + private final SpelAwareProxyProjectionFactory projectionFactory; + private final Map, Boolean> supportedTypesCache = new ConcurrentReferenceHashMap<>(); + + /** + * Creates a new {@link ProjectingJackson2JsonDecoder} using a default {@link ObjectMapper}. + */ + public ProjectingJackson2JsonDecoder() { + this.projectionFactory = initProjectionFactory(getObjectMapper()); + } + + /** + * Creates a new {@link ProjectingJackson2JsonDecoder} for the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + */ + public ProjectingJackson2JsonDecoder(ObjectMapper mapper) { + + super(mapper); + + this.projectionFactory = initProjectionFactory(mapper); + } + + /** + * Creates a new {@link SpelAwareProxyProjectionFactory} with the {@link JsonProjectingMethodInterceptorFactory} + * registered for the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + * @return + */ + private static SpelAwareProxyProjectionFactory initProjectionFactory(ObjectMapper mapper) { + + Assert.notNull(mapper, "ObjectMapper must not be null"); + + SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + projectionFactory.registerMethodInvokerFactory( + new JsonProjectingMethodInterceptorFactory(new JacksonJsonProvider(mapper), + new JacksonMappingProvider(mapper))); + + return projectionFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + projectionFactory.setBeanClassLoader(classLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + projectionFactory.setBeanFactory(beanFactory); + } + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + return false; + } + JavaType javaType = mapper.constructType(elementType.getType()); + // Skip String: CharSequenceDecoder + "*/*" comes after + if (CharSequence.class.isAssignableFrom(elementType.toClass()) || !supportsMimeType(mimeType)) { + return false; + } + if (!logger.isDebugEnabled()) { + return mapper.canDeserialize(javaType); + } else { + AtomicReference causeRef = new AtomicReference<>(); + if (mapper.canDeserialize(javaType, causeRef)) { + Class rawType = javaType.getRawClass(); + Boolean result = supportedTypesCache.get(rawType); + + if (result != null) { + return result; + } + + result = rawType.isInterface() && AnnotationUtils.findAnnotation(rawType, ProjectedPayload.class) != null; + supportedTypesCache.put(rawType, result); + + return result; + } + logWarningIfNecessary(javaType, causeRef.get()); + return false; + } + } + + @Override + public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + ObjectMapper mapper = selectObjectMapper(elementType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + elementType); + } + + Flux processed = processInput(input, elementType, mimeType, hints); + + return DataBufferUtils.join(processed, this.getMaxInMemorySize()) + .flatMap(dataBuffer -> Mono.just(decode(dataBuffer, elementType, mimeType, hints))) + .flatMapMany(object -> { + if (object instanceof Iterable) { + return Flux.fromIterable((Iterable) object); + } + return Flux.just(object); + }); + } + + @Override + public Object decode(DataBuffer dataBuffer, ResolvableType targetType, @Nullable MimeType mimeType, + @Nullable Map hints) throws DecodingException { + + return projectionFactory.createProjection(ResolvableType.forType(targetType.getType()).resolve(Object.class), + dataBuffer.asInputStream()); + } + +} diff --git a/src/main/java/org/springframework/data/web/ReactiveProxyingHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ReactiveProxyingHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..528a6cf8d0 --- /dev/null +++ b/src/main/java/org/springframework/data/web/ReactiveProxyingHandlerMethodArgumentResolver.java @@ -0,0 +1,123 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.support.WebExchangeDataBinder; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * {@link HandlerMethodArgumentResolver} to create Proxy instances for interface based controller method parameters. + * + * @author Oliver Gierke + * @author Matias Hermosilla + * @since 3.0 + */ +public class ReactiveProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodArgumentResolver + implements BeanFactoryAware, BeanClassLoaderAware { + + private static final List IGNORED_PACKAGES = Arrays.asList("java", "org.springframework"); + + private final SpelAwareProxyProjectionFactory proxyFactory; + private final ObjectFactory conversionService; + + /** + * Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public ReactiveProxyingHandlerMethodArgumentResolver(ObjectFactory conversionService, + ReactiveAdapterRegistry adapterRegistry, boolean annotationNotRequired) { + + super(adapterRegistry, annotationNotRequired); + + this.proxyFactory = new SpelAwareProxyProjectionFactory(); + this.conversionService = conversionService; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.proxyFactory.setBeanFactory(beanFactory); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.proxyFactory.setBeanClassLoader(classLoader); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + + if (!super.supportsParameter(parameter)) { + return false; + } + + Class type = parameter.getParameterType(); + + if (!type.isInterface()) { + return false; + } + + // Annotated parameter + if (parameter.getParameterAnnotation(ProjectedPayload.class) != null) { + return true; + } + + // Annotated type + if (AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null) { + return true; + } + + // Fallback for only user defined interfaces + String packageName = ClassUtils.getPackageName(type); + + return !IGNORED_PACKAGES.stream().anyMatch(it -> packageName.startsWith(it)); + } + + @Override + public Mono resolveArgument( + MethodParameter parameter, BindingContext context, ServerWebExchange exchange) { + + MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService.getObject()); + binder.bind(new MutablePropertyValues(exchange.getAttributes())); + + return Mono.just(proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget())); + } + + @Override + protected Mono bindRequestParameters(WebExchangeDataBinder binder, ServerWebExchange request) { + return Mono.never(); + } + +} diff --git a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebFluxSupport.java b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebFluxSupport.java new file mode 100644 index 0000000000..da32c0789d --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebFluxSupport.java @@ -0,0 +1,131 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.querydsl.QuerydslUtils; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.util.ClassUtils; + +/** + * Annotation to automatically register the following beans for usage with Spring MVC. Note that using this annotation + * will require Spring 3.2. + *
    + *
  • {@link org.springframework.data.repository.support.DomainClassConverter} - to allow usage of domain types managed + * by Spring Data repositories as controller method arguments bound with + * {@link org.springframework.web.bind.annotation.PathVariable} or + * {@link org.springframework.web.bind.annotation.RequestParam}.
  • + *
  • {@link PageableHandlerMethodArgumentResolver} - to allow injection of + * {@link org.springframework.data.domain.Pageable} instances into controller methods automatically created from request + * parameters.
  • + *
  • {@link org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver} - to allow injection of + * {@link org.springframework.data.domain.Sort} instances into controller methods automatically created from request + * parameters.
  • + *
+ * If Spring HATEOAS is present on the classpath we will register the following beans: + *
    + *
  • {@link org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver} - instead of + * {@link PageableHandlerMethodArgumentResolver}
  • + *
  • {@link org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver} - instead of + * {@link org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver}
  • + *
  • {@link org.springframework.data.web.PagedResourcesAssembler} - for injection into web components
  • + *
  • {@link org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver} - for injection of + * {@link org.springframework.data.web.PagedResourcesAssembler} into controller methods
  • + *
+ * + * @since 3.0 + * @see SpringDataWebFluxConfiguration + * @see HateoasAwareSpringDataWebConfiguration + * @author Oliver Gierke + * @author Matías Hermosilla + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Inherited +@Import({ EnableSpringDataWebFluxSupport.SpringDataWebConfigurationImportSelector.class, + EnableSpringDataWebFluxSupport.QuerydslActivator.class }) +public @interface EnableSpringDataWebFluxSupport { + + /** + * Import selector to import the appropriate configuration class depending on whether Spring HATEOAS is present on the + * classpath. We need to register the HATEOAS specific class first as apparently only the first class implementing + * {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport} gets callbacks invoked (see + * https://jira.springsource.org/browse/SPR-10565). + * + * @author Oliver Gierke + * @author Jens Schauder + */ + static class SpringDataWebConfigurationImportSelector implements ImportSelector, ResourceLoaderAware { + + private Optional resourceLoader = Optional.empty(); + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = Optional.of(resourceLoader).map(ResourceLoader::getClassLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + + List imports = new ArrayList<>(); + + imports.add(ReactiveProjectingArgumentResolverRegistrar.class.getName()); + + imports.add(resourceLoader// + .filter(it -> ClassUtils.isPresent("org.springframework.hateoas.Link", it))// + .map(it -> HateoasAwareSpringDataWebConfiguration.class.getName())// + .orElseGet(() -> SpringDataWebFluxConfiguration.class.getName())); + + resourceLoader// + .filter(it -> ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", it))// + .map(it -> SpringFactoriesLoader.loadFactoryNames(SpringDataJacksonModules.class, it))// + .ifPresent(it -> imports.addAll(it)); + + return imports.toArray(new String[imports.size()]); + } + } + + /** + * Import selector to register {@link ReactiveQuerydslWebConfiguration} as configuration class if Querydsl is on the + * classpath. + * + * @author Oliver Gierke + * @soundtrack Anika Nilles - Chary Life + * @since 1.11 + */ + static class QuerydslActivator implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return QuerydslUtils.QUERY_DSL_PRESENT ? new String[] { ReactiveQuerydslWebConfiguration.class.getName() } + : new String[0]; + } + } +} diff --git a/src/main/java/org/springframework/data/web/config/ReactivePageableHandlerMethodArgumentResolverCustomizer.java b/src/main/java/org/springframework/data/web/config/ReactivePageableHandlerMethodArgumentResolverCustomizer.java new file mode 100644 index 0000000000..269a1b9a41 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/ReactivePageableHandlerMethodArgumentResolverCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web.config; + +import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link ReactivePageableHandlerMethodArgumentResolver} configuration. + * + * @author Vedran Pavic + * @author Oliver Gierke + * @author Matías Hermosilla + * @since 3.0 + */ +@FunctionalInterface +public interface ReactivePageableHandlerMethodArgumentResolverCustomizer { + + /** + * Customize the given {@link ReactivePageableHandlerMethodArgumentResolver}. + * + * @param pageableResolver the {@link ReactivePageableHandlerMethodArgumentResolver} to customize, will never be + * {@literal null}. + */ + void customize(ReactivePageableHandlerMethodArgumentResolver pageableResolver); +} diff --git a/src/main/java/org/springframework/data/web/config/ReactiveProjectingArgumentResolverRegistrar.java b/src/main/java/org/springframework/data/web/config/ReactiveProjectingArgumentResolverRegistrar.java new file mode 100644 index 0000000000..ad342dbed7 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/ReactiveProjectingArgumentResolverRegistrar.java @@ -0,0 +1,129 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web.config; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; +import org.springframework.data.web.ReactiveProxyingHandlerMethodArgumentResolver; +import org.springframework.lang.Nullable; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; + +/** + * Configuration class to register a {@link BeanPostProcessor} to augment {@link RequestMappingHandlerAdapter} with a + * {@link ProxyingHandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Johannes Englmeier + * @author Matías Hermosilla + * @since 3.0 + * @soundtrack Apparat With Soap & Skin - Goodbye (Dark Theme Song - https://www.youtube.com/watch?v=66VnOdk6oto) + */ +@Configuration(proxyBeanMethods = false) +public class ReactiveProjectingArgumentResolverRegistrar { + + /** + * Registers a {@link BeanPostProcessor} to modify {@link RequestMappingHandlerAdapter} beans in the application + * context to get a {@link ProxyingHandlerMethodArgumentResolver} configured as first + * {@link HandlerMethodArgumentResolver}. + * + * @param conversionService the Spring MVC {@link ConversionService} in a lazy fashion, so that its initialization is + * not triggered yet. + * @return + */ + @Bean + static ProjectingArgumentResolverBeanPostProcessor projectingArgumentResolverBeanPostProcessor( + @Qualifier("webFluxConversionService") ObjectFactory conversionService, + ReactiveAdapterRegistry adapterRegistry) { + return new ProjectingArgumentResolverBeanPostProcessor(conversionService, adapterRegistry); + } + + /** + * A {@link BeanPostProcessor} to modify {@link RequestMappingHandlerAdapter} beans in the application context to get + * a {@link ProxyingHandlerMethodArgumentResolver} configured as first {@link HandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + * @soundtrack Apparat With Soap & Skin - Goodbye (Dark Theme Song - https://www.youtube.com/watch?v=66VnOdk6oto) + */ + static class ProjectingArgumentResolverBeanPostProcessor + implements BeanPostProcessor, BeanFactoryAware, BeanClassLoaderAware { + + private ReactiveProxyingHandlerMethodArgumentResolver resolver; + + /** + * A {@link BeanPostProcessor} to modify {@link RequestMappingHandlerAdapter} beans in the application context to + * get a {@link ProxyingHandlerMethodArgumentResolver} configured as first {@link HandlerMethodArgumentResolver}. + * + * @param conversionService the Spring MVC {@link ConversionService} in a lazy fashion, so that its initialization + * is not triggered yet. + */ + ProjectingArgumentResolverBeanPostProcessor(ObjectFactory conversionService, + ReactiveAdapterRegistry adapterRegistry) { + this.resolver = new ReactiveProxyingHandlerMethodArgumentResolver(conversionService, adapterRegistry, false); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.resolver.setBeanFactory(beanFactory); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.resolver.setBeanClassLoader(classLoader); + } + + @Nullable + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Nullable + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (!RequestMappingHandlerAdapter.class.isInstance(bean)) { + return bean; + } + + RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; + ArgumentResolverConfigurer argumentResolverConfigurer = adapter.getArgumentResolverConfigurer(); + + if (argumentResolverConfigurer == null) { + throw new IllegalStateException( + String.format("No HandlerMethodArgumentResolvers found in RequestMappingHandlerAdapter %s", beanName)); + } + + argumentResolverConfigurer.addCustomResolver(resolver); + + return adapter; + } + + } + +} diff --git a/src/main/java/org/springframework/data/web/config/ReactiveSortHandlerMethodArgumentResolverCustomizer.java b/src/main/java/org/springframework/data/web/config/ReactiveSortHandlerMethodArgumentResolverCustomizer.java new file mode 100644 index 0000000000..6e8e8216b6 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/ReactiveSortHandlerMethodArgumentResolverCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web.config; + +import org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link ReactiveSortHandlerMethodArgumentResolver} configuration. + * + * @author Vedran Pavic + * @author Oliver Gierke + * @author Matías Hermosilla + * @since 3.0 + */ +@FunctionalInterface +public interface ReactiveSortHandlerMethodArgumentResolverCustomizer { + + /** + * Customize the given {@link ReactiveSortHandlerMethodArgumentResolver}. + * + * @param sortResolver the {@link ReactiveSortHandlerMethodArgumentResolver} to customize, will never be {@literal null}. + */ + void customize(ReactiveSortHandlerMethodArgumentResolver sortResolver); +} diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebFluxConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebFluxConfiguration.java new file mode 100644 index 0000000000..9d3b320d63 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/SpringDataWebFluxConfiguration.java @@ -0,0 +1,168 @@ +/* + * Copyright 2022-2022 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 org.springframework.data.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.geo.format.DistanceFormatter; +import org.springframework.data.geo.format.PointFormatter; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.util.Lazy; +import org.springframework.data.web.*; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; + +/** + * Configuration class to register {@link PageableHandlerMethodArgumentResolver}, + * {@link SortHandlerMethodArgumentResolver} and {@link DomainClassConverter}. + * + * @author Oliver Gierke + * @author Vedran Pavic + * @author Jens Schauder + * @author Mark Paluch + * @author Greg Turnquist + * @author Matías Hermosilla + * @since 3.0 + */ +public class SpringDataWebFluxConfiguration implements WebFluxConfigurer, BeanClassLoaderAware { + + private final ApplicationContext context; + private final ObjectFactory conversionService; + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private final Lazy sortResolver; + private final Lazy pageableResolver; + private final Lazy pageableResolverCustomizer; + private final Lazy sortResolverCustomizer; + private final Lazy adapterRegistry; + + public SpringDataWebFluxConfiguration(ApplicationContext context, + @Qualifier("webFluxConversionService") ObjectFactory conversionService) { + + Assert.notNull(context, "ApplicationContext must not be null"); + Assert.notNull(conversionService, "ConversionService must not be null"); + + this.context = context; + + this.conversionService = conversionService; + this.sortResolver = Lazy.of(() -> context.getBean("sortResolver", ReactiveSortHandlerMethodArgumentResolver.class)); + this.pageableResolver = Lazy.of( // + () -> context.getBean("pageableResolver", ReactivePageableHandlerMethodArgumentResolver.class)); + this.pageableResolverCustomizer = Lazy.of( // + () -> context.getBeanProvider(ReactivePageableHandlerMethodArgumentResolverCustomizer.class).getIfAvailable()); + this.sortResolverCustomizer = Lazy.of( // + () -> context.getBeanProvider(ReactiveSortHandlerMethodArgumentResolverCustomizer.class).getIfAvailable()); + this.adapterRegistry = Lazy.of( // + () -> context.getBeanProvider(ReactiveAdapterRegistry.class).getIfAvailable()); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Bean + public ReactivePageableHandlerMethodArgumentResolver pageableResolver() { + + ReactivePageableHandlerMethodArgumentResolver pageableResolver = // + new ReactivePageableHandlerMethodArgumentResolver(sortResolver.get()); + customizePageableResolver(pageableResolver); + return pageableResolver; + } + + @Bean + public ReactiveSortHandlerMethodArgumentResolver sortResolver() { + + ReactiveSortHandlerMethodArgumentResolver sortResolver = new ReactiveSortHandlerMethodArgumentResolver(); + customizeSortResolver(sortResolver); + return sortResolver; + } + + @Override + public void addFormatters(FormatterRegistry registry) { + + registry.addFormatter(DistanceFormatter.INSTANCE); + registry.addFormatter(PointFormatter.INSTANCE); + + if (!(registry instanceof FormattingConversionService conversionService)) { + return; + } + + DomainClassConverter converter = new DomainClassConverter( + conversionService); + converter.setApplicationContext(context); + } + + @Override + public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { + + configurer.addCustomResolver(sortResolver.get()); + configurer.addCustomResolver(pageableResolver.get()); + + ReactiveProxyingHandlerMethodArgumentResolver resolver = new ReactiveProxyingHandlerMethodArgumentResolver( + conversionService, this.adapterRegistry.get(), true); + resolver.setBeanFactory(context); + forwardBeanClassLoader(resolver); + + configurer.addCustomResolver((HandlerMethodArgumentResolver) resolver); + } + + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + + if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader()) && ClassUtils.isPresent( + "com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) { + + ObjectMapper mapper = context.getBeanProvider(ObjectMapper.class).getIfUnique(ObjectMapper::new); + + ProjectingJackson2JsonDecoder decoder = new ProjectingJackson2JsonDecoder(mapper); + decoder.setBeanFactory(context); + forwardBeanClassLoader(decoder); + + configurer.customCodecs().register(decoder); + + } + + } + + protected void customizePageableResolver(ReactivePageableHandlerMethodArgumentResolver pageableResolver) { + pageableResolverCustomizer.getOptional().ifPresent(c -> c.customize(pageableResolver)); + } + + protected void customizeSortResolver(ReactiveSortHandlerMethodArgumentResolver sortResolver) { + sortResolverCustomizer.getOptional().ifPresent(c -> c.customize(sortResolver)); + } + + private void forwardBeanClassLoader(BeanClassLoaderAware target) { + if (beanClassLoader != null) { + target.setBeanClassLoader(beanClassLoader); + } + } + +}