/*
* 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 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.Encoder;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter;
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.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.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.http.converter.reactive.SseEventHttpMessageWriter;
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.
*
*
Import directly or extend and override protected methods to customize.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
@Configuration
public class WebReactiveConfiguration implements ApplicationContextAware {
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebReactiveConfiguration.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebReactiveConfiguration.class.getClassLoader());
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", WebReactiveConfiguration.class.getClassLoader());
private static final boolean rxJava1Present =
ClassUtils.isPresent("rx.Observable", WebReactiveConfiguration.class.getClassLoader());
private PathMatchConfigurer pathMatchConfigurer;
private List> messageReaders;
private List> messageWriters;
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 getDefaultMediaTypeMappings() {
Map 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 resolvers = new ArrayList<>();
addArgumentResolvers(resolvers);
if (!resolvers.isEmpty()) {
adapter.setCustomArgumentResolvers(resolvers);
}
adapter.setMessageReaders(getMessageReaders());
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 resolvers) {
}
/**
* Main method to access message readers to use for decoding
* controller method arguments with.
* Use {@link #configureMessageReaders} to configure the list or
* {@link #extendMessageReaders} to add in addition to the default ones.
*/
protected final List> getMessageReaders() {
if (this.messageReaders == null) {
this.messageReaders = new ArrayList<>();
configureMessageReaders(this.messageReaders);
if (this.messageReaders.isEmpty()) {
addDefaultHttpMessageReaders(this.messageReaders);
}
extendMessageReaders(this.messageReaders);
}
return this.messageReaders;
}
/**
* Override to configure the message readers to use for decoding
* controller method arguments.
* If no message readres are specified, default will be added via
* {@link #addDefaultHttpMessageReaders}.
* @param messageReaders a list to add message readers to, initially an empty
*/
protected void configureMessageReaders(List> messageReaders) {
}
/**
* Adds default converters that sub-classes can call from
* {@link #configureMessageReaders(List)}.
*/
protected final void addDefaultHttpMessageReaders(List> readers) {
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
if (jaxb2Present) {
readers.add(new DecoderHttpMessageReader<>(new Jaxb2Decoder()));
}
if (jackson2Present) {
readers.add(new DecoderHttpMessageReader<>(new JacksonJsonDecoder()));
}
}
/**
* Override this to modify the list of message readers after it has been
* configured, for example to add some in addition to the default ones.
*/
protected void extendMessageReaders(List> messageReaders) {
}
@Bean
public FormattingConversionService mvcConversionService() {
FormattingConversionService service = new DefaultFormattingConversionService();
addFormatters(service);
return service;
}
/**
* Override to add custom {@link Converter}s and {@link Formatter}s.
*/
protected void addFormatters(FormatterRegistry registry) {
}
/**
* Return a global {@link Validator} instance for example for validating
* {@code @RequestBody} method arguments.
* 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, getClass().getClassLoader());
}
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.instantiateClass(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();
}
@Bean
public ResponseEntityResultHandler responseEntityResultHandler() {
return new ResponseEntityResultHandler(getMessageWriters(), mvcContentTypeResolver());
}
@Bean
public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(getMessageWriters(), mvcContentTypeResolver());
}
/**
* Main method to access message writers to use for encoding return values.
*
Use {@link #configureMessageWriters(List)} to configure the list or
* {@link #extendMessageWriters(List)} to add in addition to the default ones.
*/
protected final List> getMessageWriters() {
if (this.messageWriters == null) {
this.messageWriters = new ArrayList<>();
configureMessageWriters(this.messageWriters);
if (this.messageWriters.isEmpty()) {
addDefaultHttpMessageWriters(this.messageWriters);
}
extendMessageWriters(this.messageWriters);
}
return this.messageWriters;
}
/**
* Override to configure the message writers to use for encoding
* return values.
* If no message readers are specified, default will be added via
* {@link #addDefaultHttpMessageWriters}.
* @param messageWriters a list to add message writers to, initially an empty
*/
protected void configureMessageWriters(List> messageWriters) {
}
/**
* Adds default converters that sub-classes can call from
* {@link #configureMessageWriters(List)}.
*/
protected final void addDefaultHttpMessageWriters(List> writers) {
List> sseDataEncoders = new ArrayList<>();
writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writers.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
writers.add(new ResourceHttpMessageWriter());
if (jaxb2Present) {
writers.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
}
if (jackson2Present) {
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder));
sseDataEncoders.add(jacksonEncoder);
}
writers.add(new SseEventHttpMessageWriter(sseDataEncoders));
}
/**
* Override this to modify the list of message writers after it has been
* configured, for example to add some in addition to the default ones.
*/
protected void extendMessageWriters(List> messageWriters) {
}
@Bean
public ViewResolutionResultHandler viewResolutionResultHandler() {
ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext());
configureViewResolvers(registry);
List resolvers = registry.getViewResolvers();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, mvcContentTypeResolver());
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) {
}
}
}