Skip to content
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

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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();
}

}
Loading