/*
* Copyright 2002-2016 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
*
* http://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.web.reactive.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import reactor.core.converter.Converters;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.MediaType;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.result.SimpleHandlerAdapter;
import org.springframework.web.reactive.result.SimpleResultHandler;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
/**
* The main class for Spring Web Reactive configuration.
*
* <p>Import directly or extend and override protected methods to customize.
*
* @author Rossen Stoyanchev
*/
@Configuration @SuppressWarnings("unused")
public class WebReactiveConfiguration implements ApplicationContextAware {
private static final ClassLoader classLoader = WebReactiveConfiguration.class.getClassLoader();
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
private PathMatchConfigurer pathMatchConfigurer;
private List<HttpMessageConverter<?>> messageConverters;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
protected ApplicationContext getApplicationContext() {
return this.applicationContext;
}
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setContentTypeResolver(mvcContentTypeResolver());
PathMatchConfigurer configurer = getPathMatchConfigurer();
if (configurer.isUseSuffixPatternMatch() != null) {
mapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
}
if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
mapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
}
if (configurer.isUseTrailingSlashMatch() != null) {
mapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
}
if (configurer.getPathMatcher() != null) {
mapping.setPathMatcher(configurer.getPathMatcher());
}
if (configurer.getPathHelper() != null) {
mapping.setPathHelper(configurer.getPathHelper());
}
return mapping;
}
/**
* Override to plug a sub-class of {@link RequestMappingHandlerMapping}.
*/
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean
public RequestedContentTypeResolver mvcContentTypeResolver() {
RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder();
builder.mediaTypes(getDefaultMediaTypeMappings());
configureRequestedContentTypeResolver(builder);
return builder.build();
}
/**
* Override to configure media type mappings.
* @see RequestedContentTypeResolverBuilder#mediaTypes(Map)
*/
protected Map<String, MediaType> getDefaultMediaTypeMappings() {
Map<String, MediaType> map = new HashMap<>();
if (jackson2Present) {
map.put("json", MediaType.APPLICATION_JSON);
}
return map;
}
/**
* Override to configure how the requested content type is resolved.
*/
protected void configureRequestedContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
}
/**
* Callback for building the {@link PathMatchConfigurer}. This method is
* final, use {@link #configurePathMatching} to customize path matching.
*/
protected final PathMatchConfigurer getPathMatchConfigurer() {
if (this.pathMatchConfigurer == null) {
this.pathMatchConfigurer = new PathMatchConfigurer();
configurePathMatching(this.pathMatchConfigurer);
}
return this.pathMatchConfigurer;
}
/**
* Override to configure path matching options.
*/
public void configurePathMatching(PathMatchConfigurer configurer) {
}
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
addArgumentResolvers(resolvers);
if (!resolvers.isEmpty()) {
adapter.setCustomArgumentResolvers(resolvers);
}
adapter.setMessageConverters(getMessageConverters());
adapter.setConversionService(mvcConversionService());
adapter.setValidator(mvcValidator());
return adapter;
}
/**
* Override to plug a sub-class of {@link RequestMappingHandlerAdapter}.
*/
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new RequestMappingHandlerAdapter();
}
/**
* Provide custom argument resolvers without overriding the built-in ones.
*/
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
/**
* Main method to access message converters to use for decoding
* controller method arguments and encoding return values.
* <p>Use {@link #configureMessageConverters} to configure the list or
* {@link #extendMessageConverters} to add in addition to the default ones.
*/
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
/**
* Override to configure the message converters to use for decoding
* controller method arguments and encoding return values.
* <p>If no converters are specified, default will be added via
* {@link #addDefaultHttpMessageConverters}.
* @param converters a list to add converters to, initially an empty
*/
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* Adds default converters that sub-classes can call from
* {@link #configureMessageConverters(List)}.
*/
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> converters) {
List<Encoder<?>> sseDataEncoders = new ArrayList<>();
converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(converter(new StringEncoder(), new StringDecoder()));
converters.add(new ResourceHttpMessageConverter());
if (jaxb2Present) {
converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder()));
}
if (jackson2Present) {
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder();
converters.add(converter(jacksonEncoder, jacksonDecoder));
sseDataEncoders.add(jacksonEncoder);
} else {
}
converters.add(converter(new SseEventEncoder(sseDataEncoders), null));
}
private static <T> HttpMessageConverter<T> converter(Encoder<T> encoder, Decoder<T> decoder) {
return new CodecHttpMessageConverter<>(encoder, decoder);
}
/**
* Override this to modify the list of converters after it has been
* configured, for example to add some in addition to the default ones.
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Bean
public FormattingConversionService mvcConversionService() {
FormattingConversionService service = new DefaultFormattingConversionService();
addFormatters(service);
return service;
}
/**
* Override to add custom {@link Converter}s and {@link Formatter}s.
* <p>By default this method method registers:
* <ul>
* <li>{@link MonoToCompletableFutureConverter}
* <li>{@link ReactorToRxJava1Converter}
* </ul>
*/
protected void addFormatters(FormatterRegistry registry) {
registry.addConverter(new MonoToCompletableFutureConverter());
if (Converters.hasRxJava1()) {
registry.addConverter(new ReactorToRxJava1Converter());
}
}
/**
* Return a global {@link Validator} instance for example for validating
* {@code @RequestBody} method arguments.
* <p>Delegates to {@link #getValidator()} first. If that returns {@code null}
* checks the classpath for the presence of a JSR-303 implementations
* before creating a {@code OptionalValidatorFactoryBean}. If a JSR-303
* implementation is not available, a "no-op" {@link Validator} is returned.
*/
@Bean
public Validator mvcValidator() {
Validator validator = getValidator();
if (validator == null) {
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
Class<?> clazz;
try {
String name = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
clazz = ClassUtils.forName(name, classLoader);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException("Could not find default validator class", ex);
}
catch (LinkageError ex) {
throw new BeanInitializationException("Could not load default validator class", ex);
}
validator = (Validator) BeanUtils.instantiate(clazz);
}
else {
validator = new NoOpValidator();
}
}
return validator;
}
/**
* Override this method to provide a custom {@link Validator}.
*/
protected Validator getValidator() {
return null;
}
@Bean
public SimpleHandlerAdapter simpleHandlerAdapter() {
return new SimpleHandlerAdapter();
}
@Bean
public SimpleResultHandler simpleResultHandler() {
return new SimpleResultHandler(mvcConversionService());
}
@Bean
public ResponseEntityResultHandler responseEntityResultHandler() {
return new ResponseEntityResultHandler(getMessageConverters(), mvcConversionService(),
mvcContentTypeResolver());
}
@Bean
public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService(),
mvcContentTypeResolver());
}
@Bean
public ViewResolutionResultHandler viewResolutionResultHandler() {
ViewResolverRegistry registry = new ViewResolverRegistry(this.applicationContext);
configureViewResolvers(registry);
List<ViewResolver> resolvers = registry.getViewResolvers();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, mvcConversionService());
handler.setDefaultViews(registry.getDefaultViews());
handler.setOrder(registry.getOrder());
return handler;
}
/**
* Override this to configure view resolution.
*/
protected void configureViewResolvers(ViewResolverRegistry registry) {
}
private static final class NoOpValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return false;
}
@Override
public void validate(Object target, Errors errors) {
}
}
}