Skip to content

DATACMNS-1412: Add support for QueryDSL Predicate, Pageable, Sort and ProjectedPayload on WebFlux controllers #2667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Class<?>, 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<Throwable> 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<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {

ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
if (mapper == null) {
throw new IllegalStateException("No ObjectMapper for " + elementType);
}

Flux<DataBuffer> 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<String, Object> hints) throws DecodingException {

return projectionFactory.createProjection(ResolvableType.forType(targetType.getType()).resolve(Object.class),
dataBuffer.asInputStream());
}

}
Original file line number Diff line number Diff line change
@@ -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<String> IGNORED_PACKAGES = Arrays.asList("java", "org.springframework");

private final SpelAwareProxyProjectionFactory proxyFactory;
private final ObjectFactory<ConversionService> conversionService;

/**
* Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}.
*
* @param conversionService must not be {@literal null}.
*/
public ReactiveProxyingHandlerMethodArgumentResolver(ObjectFactory<ConversionService> 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<Object> 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<Void> bindRequestParameters(WebExchangeDataBinder binder, ServerWebExchange request) {
return Mono.never();
}

}
Original file line number Diff line number Diff line change
@@ -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.
* <ul>
* <li>{@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}.</li>
* <li>{@link PageableHandlerMethodArgumentResolver} - to allow injection of
* {@link org.springframework.data.domain.Pageable} instances into controller methods automatically created from request
* parameters.</li>
* <li>{@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.</li>
* </ul>
* If Spring HATEOAS is present on the classpath we will register the following beans:
* <ul>
* <li>{@link org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver} - instead of
* {@link PageableHandlerMethodArgumentResolver}</li>
* <li>{@link org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver} - instead of
* {@link org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver}</li>
* <li>{@link org.springframework.data.web.PagedResourcesAssembler} - for injection into web components</li>
* <li>{@link org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver} - for injection of
* {@link org.springframework.data.web.PagedResourcesAssembler} into controller methods</li>
* </ul>
*
* @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<ClassLoader> resourceLoader = Optional.empty();

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = Optional.of(resourceLoader).map(ResourceLoader::getClassLoader);
}

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {

List<String> 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];
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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> 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> 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;
}

}

}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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> conversionService;
private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();

private final Lazy<ReactiveSortHandlerMethodArgumentResolver> sortResolver;
private final Lazy<ReactivePageableHandlerMethodArgumentResolver> pageableResolver;
private final Lazy<ReactivePageableHandlerMethodArgumentResolverCustomizer> pageableResolverCustomizer;
private final Lazy<ReactiveSortHandlerMethodArgumentResolverCustomizer> sortResolverCustomizer;
private final Lazy<ReactiveAdapterRegistry> adapterRegistry;

public SpringDataWebFluxConfiguration(ApplicationContext context,
@Qualifier("webFluxConversionService") ObjectFactory<ConversionService> 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<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
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);
}
}

}