From 03b474edfe6252c8cf34da95b8f908b88b08bbfd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 09:42:09 -0400 Subject: [PATCH] Add Web Reactive Java config --- .../reactive/config/PathMatchConfigurer.java | 110 ++++++ .../UrlBasedViewResolverRegistration.java | 79 +++++ .../reactive/config/ViewResolverRegistry.java | 146 ++++++++ .../config/WebReactiveConfiguration.java | 316 ++++++++++++++++++ .../web/reactive/config/package-info.java | 4 + .../config/ViewResolverRegistryTests.java | 90 +++++ .../config/WebReactiveConfigurationTests.java | 277 +++++++++++++++ .../RequestMappingIntegrationTests.java | 80 +---- 8 files changed, 1028 insertions(+), 74 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java new file mode 100644 index 00000000000..c45565372ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java @@ -0,0 +1,110 @@ +/* + * 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 org.springframework.util.PathMatcher; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Assist with configuring {@code HandlerMapping}'s with path matching options. + * + * @author Rossen Stoyanchev + */ +public class PathMatchConfigurer { + + private Boolean suffixPatternMatch; + + private Boolean trailingSlashMatch; + + private Boolean registeredSuffixPatternMatch; + + private HttpRequestPathHelper pathHelper; + + private PathMatcher pathMatcher; + + + /** + * Whether to use suffix pattern match (".*") when matching patterns to + * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

By default this is set to {@code true}. + * @see #registeredSuffixPatternMatch + */ + public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + return this; + } + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a method mapped to "/users" also matches to "/users/". + *

The default value is {@code true}. + */ + public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + return this; + } + + /** + * Whether suffix pattern matching should work only against path extensions + * that are explicitly registered. This is generally recommended to reduce + * ambiguity and to avoid issues such as when a "." (dot) appears in the path + * for other reasons. + *

By default this is set to "true". + */ + public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + return this; + } + + /** + * Set a {@code HttpRequestPathHelper} for the resolution of lookup paths. + *

Default is {@code HttpRequestPathHelper}. + */ + public PathMatchConfigurer setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + return this; + } + + /** + * Set the PathMatcher for matching URL paths against registered URL patterns. + *

Default is {@link org.springframework.util.AntPathMatcher AntPathMatcher}. + */ + public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + return this; + } + + protected Boolean isUseSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + protected Boolean isUseTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + protected Boolean isUseRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + protected HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + protected PathMatcher getPathMatcher() { + return this.pathMatcher; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java new file mode 100644 index 00000000000..422449d7537 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java @@ -0,0 +1,79 @@ +/* + * 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 org.springframework.util.Assert; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; + +/** + * Assist with configuring properties of a {@link UrlBasedViewResolver}. + * + * @author Rossen Stoyanchev + */ +public class UrlBasedViewResolverRegistration { + + private final UrlBasedViewResolver viewResolver; + + + public UrlBasedViewResolverRegistration(UrlBasedViewResolver viewResolver) { + Assert.notNull(viewResolver); + this.viewResolver = viewResolver; + } + + + /** + * Set the prefix that gets prepended to view names when building a URL. + * @see UrlBasedViewResolver#setPrefix + */ + public UrlBasedViewResolverRegistration prefix(String prefix) { + this.viewResolver.setPrefix(prefix); + return this; + } + + /** + * Set the suffix that gets appended to view names when building a URL. + * @see UrlBasedViewResolver#setSuffix + */ + public UrlBasedViewResolverRegistration suffix(String suffix) { + this.viewResolver.setSuffix(suffix); + return this; + } + + /** + * Set the view class that should be used to create views. + * @see UrlBasedViewResolver#setViewClass + */ + public UrlBasedViewResolverRegistration viewClass(Class viewClass) { + this.viewResolver.setViewClass(viewClass); + return this; + } + + /** + * Set the view names (or name patterns) that can be handled by this view + * resolver. View names can contain simple wildcards such that 'my*', '*Report' + * and '*Repo*' will all match the view name 'myReport'. + * @see UrlBasedViewResolver#setViewNames + */ + public UrlBasedViewResolverRegistration viewNames(String... viewNames) { + this.viewResolver.setViewNames(viewNames); + return this; + } + + protected UrlBasedViewResolver getViewResolver() { + return this.viewResolver; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java new file mode 100644 index 00000000000..ce5989002d8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java @@ -0,0 +1,146 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; + + +/** + * Assist with the configuration of a chain of {@link ViewResolver}'s supporting + * different template mechanisms. + * + *

In addition, you can also configure {@link #defaultViews(View...) + * defaultViews} for rendering according to the requested content type, e.g. + * JSON, XML, etc. + * + * @author Rossen Stoyanchev + */ +public class ViewResolverRegistry { + + private final List viewResolvers = new ArrayList<>(4); + + private final List defaultViews = new ArrayList<>(4); + + private Integer order; + + private final ApplicationContext applicationContext; + + + public ViewResolverRegistry(ApplicationContext applicationContext) { + Assert.notNull(applicationContext); + this.applicationContext = applicationContext; + } + + + /** + * Register a {@code FreeMarkerViewResolver} with a ".ftl" suffix. + *

Note that you must also configure FreeMarker by + * adding a {@link FreeMarkerConfigurer} bean. + */ + public UrlBasedViewResolverRegistration freeMarker() { + if (this.applicationContext != null && !hasBeanOfType(FreeMarkerConfigurer.class)) { + throw new BeanInitializationException("In addition to a FreeMarker view resolver " + + "there must also be a single FreeMarkerConfig bean in this web application context " + + "(or its parent): FreeMarkerConfigurer is the usual implementation. " + + "This bean may be given any name."); + } + FreeMarkerRegistration registration = new FreeMarkerRegistration(); + UrlBasedViewResolver resolver = registration.getViewResolver(); + resolver.setApplicationContext(this.applicationContext); + this.viewResolvers.add(resolver); + return registration; + } + + protected boolean hasBeanOfType(Class beanType) { + return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.applicationContext, beanType, false, false)); + } + + /** + * Register a {@link ViewResolver} bean instance. This may be useful to + * configure a 3rd party resolver implementation or as an alternative to + * other registration methods in this class when they don't expose some + * more advanced property that needs to be set. + */ + public void viewResolver(ViewResolver viewResolver) { + this.viewResolvers.add(viewResolver); + } + + /** + * Set default views associated with any view name and selected based on the + * best match for the requested content type. + *

Use {@link org.springframework.web.reactive.result.view.HttpMessageConverterView + * HttpMessageConverterView} to adapt and use any existing + * {@code HttpMessageConverter} (e.g. JSON, XML) as a {@code View}. + */ + public void defaultViews(View... defaultViews) { + this.defaultViews.addAll(Arrays.asList(defaultViews)); + } + + /** + * Whether any view resolvers have been registered. + */ + public boolean hasRegistrations() { + return (!this.viewResolvers.isEmpty()); + } + + /** + * Set the order for the + * {@link org.springframework.web.reactive.result.view.ViewResolutionResultHandler + * ViewResolutionResultHandler}. + *

By default this property is not set, which means the result handler is + * ordered at {@link Ordered#LOWEST_PRECEDENCE}. + */ + public void order(int order) { + this.order = order; + } + + protected int getOrder() { + return (this.order != null ? this.order : Ordered.LOWEST_PRECEDENCE); + } + + protected List getViewResolvers() { + return this.viewResolvers; + } + + protected List getDefaultViews() { + return this.defaultViews; + } + + + private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration { + + public FreeMarkerRegistration() { + super(new FreeMarkerViewResolver()); + getViewResolver().setSuffix(".ftl"); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java new file mode 100644 index 00000000000..9f4b0e495a4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -0,0 +1,316 @@ +/* + * 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.DependencyUtils; + +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.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.format.Formatter; +import org.springframework.http.MediaType; +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.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.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 + */ +@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> messageConverters; + + private ApplicationContext applicationContext; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = 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.setMessageConverters(getMessageConverters()); + adapter.setConversionService(mvcConversionService()); + + 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 converters to use for decoding + * controller method arguments and encoding return values. + *

Use {@link #configureMessageConverters} to configure the list or + * {@link #extendMessageConverters} to add in addition to the default ones. + */ + protected final List> 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. + *

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> converters) { + } + + /** + * Adds default converters that sub-classes can call from + * {@link #configureMessageConverters(List)}. + */ + protected final void addDefaultHttpMessageConverters(List> converters) { + 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) { + JsonObjectDecoder objectDecoder = new JsonObjectDecoder(); + converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder(objectDecoder))); + } + } + + private static HttpMessageConverter converter(Encoder encoder, Decoder 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> converters) { + } + + // TODO: switch to DefaultFormattingConversionService + + @Bean + public GenericConversionService mvcConversionService() { + GenericConversionService service = new GenericConversionService(); + addFormatters(service); + return service; + } + + // TODO: switch to FormatterRegistry + + /** + * Override to add custom {@link Converter}s and {@link Formatter}s. + *

By default this method method registers: + *

+ */ + protected void addFormatters(ConverterRegistry registry) { + registry.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + if (DependencyUtils.hasRxJava1()) { + registry.addConverter(new ReactiveStreamsToRxJava1Converter()); + } + } + + @Bean + public SimpleHandlerAdapter simpleHandlerAdapter() { + return new SimpleHandlerAdapter(); + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService()); + } + + @Bean + public SimpleResultHandler simpleResultHandler() { + return new SimpleResultHandler(mvcConversionService()); + } + + @Bean + public ViewResolutionResultHandler viewResolutionResultHandler() { + ViewResolverRegistry registry = new ViewResolverRegistry(this.applicationContext); + configureViewResolvers(registry); + List 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) { + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java new file mode 100644 index 00000000000..8ada9a8cd4c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Defines Spring Web Reactive configuration. + */ +package org.springframework.web.reactive.config; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java new file mode 100644 index 00000000000..6e46b829b5c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java @@ -0,0 +1,90 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; + +import org.springframework.core.Ordered; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.reactive.result.view.HttpMessageConverterView; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ViewResolverRegistry}. + * + * @author Rossen Stoyanchev + */ +public class ViewResolverRegistryTests { + + private ViewResolverRegistry registry; + + + @Before + public void setUp() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerSingleton("freeMarkerConfigurer", FreeMarkerConfigurer.class); + this.registry = new ViewResolverRegistry(context); + } + + @Test + public void order() { + assertEquals(Ordered.LOWEST_PRECEDENCE, this.registry.getOrder()); + } + + @Test + public void hasRegistrations() { + assertFalse(this.registry.hasRegistrations()); + + this.registry.freeMarker(); + assertTrue(this.registry.hasRegistrations()); + } + + @Test + public void noResolvers() { + assertNotNull(this.registry.getViewResolvers()); + assertEquals(0, this.registry.getViewResolvers().size()); + assertFalse(this.registry.hasRegistrations()); + } + + @Test + public void customViewResolver() { + UrlBasedViewResolver viewResolver = new UrlBasedViewResolver(); + this.registry.viewResolver(viewResolver); + + assertSame(viewResolver, this.registry.getViewResolvers().get(0)); + assertEquals(1, this.registry.getViewResolvers().size()); + } + + @Test + public void defaultViews() throws Exception { + View view = new HttpMessageConverterView(new JacksonJsonEncoder()); + this.registry.defaultViews(view); + + assertEquals(1, this.registry.getDefaultViews().size()); + assertSame(view, this.registry.getDefaultViews().get(0)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java new file mode 100644 index 00000000000..74ddf2f7933 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -0,0 +1,277 @@ +/* + * 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.net.URI; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.Pojo; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +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.view.HttpMessageConverterView; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link WebReactiveConfiguration}. + * @author Rossen Stoyanchev + */ +public class WebReactiveConfigurationTests { + + private MockServerHttpRequest request; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, mock(WebSessionManager.class)); + } + + + @Test + public void requestMappingHandlerMapping() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "requestMappingHandlerMapping"; + RequestMappingHandlerMapping mapping = context.getBean(name, RequestMappingHandlerMapping.class); + assertNotNull(mapping); + + assertEquals(0, mapping.getOrder()); + + assertTrue(mapping.useSuffixPatternMatch()); + assertTrue(mapping.useTrailingSlashMatch()); + assertTrue(mapping.useRegisteredSuffixPatternMatch()); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, mapping.getContentTypeResolver()); + + this.request.setUri(new URI("/path.json")); + List list = Collections.singletonList(MediaType.APPLICATION_JSON); + assertEquals(list, resolver.resolveMediaTypes(this.exchange)); + + this.request.setUri(new URI("/path.xml")); + assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(this.exchange)); + } + + @Test + public void customPathMatchConfig() throws Exception { + ApplicationContext context = loadConfig(CustomPatchMatchConfig.class); + + String name = "requestMappingHandlerMapping"; + RequestMappingHandlerMapping mapping = context.getBean(name, RequestMappingHandlerMapping.class); + assertNotNull(mapping); + + assertFalse(mapping.useSuffixPatternMatch()); + assertFalse(mapping.useTrailingSlashMatch()); + } + + @Test + public void requestMappingHandlerAdapter() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "requestMappingHandlerAdapter"; + RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class); + assertNotNull(adapter); + + List> converters = adapter.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + + name = "mvcConversionService"; + ConversionService service = context.getBean(name, ConversionService.class); + assertSame(service, adapter.getConversionService()); + } + + @Test + public void customMessageConverterConfig() throws Exception { + ApplicationContext context = loadConfig(CustomMessageConverterConfig.class); + + String name = "requestMappingHandlerAdapter"; + RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class); + assertNotNull(adapter); + + List> converters = adapter.getMessageConverters(); + assertEquals(2, converters.size()); + + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + } + + @Test + public void mvcConversionService() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "mvcConversionService"; + ConversionService service = context.getBean(name, ConversionService.class); + assertNotNull(service); + + service.canConvert(CompletableFuture.class, Mono.class); + service.canConvert(Observable.class, Flux.class); + } + + + @Test + public void responseBodyResultHandler() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "responseBodyResultHandler"; + ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + assertNotNull(handler); + + assertEquals(0, handler.getOrder()); + + List> converters = handler.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + } + + @Test + public void viewResolutionResultHandler() throws Exception { + ApplicationContext context = loadConfig(CustomViewResolverConfig.class); + + String name = "viewResolutionResultHandler"; + ViewResolutionResultHandler handler = context.getBean(name, ViewResolutionResultHandler.class); + assertNotNull(handler); + + assertEquals(Ordered.LOWEST_PRECEDENCE, handler.getOrder()); + + List resolvers = handler.getViewResolvers(); + assertEquals(1, resolvers.size()); + assertEquals(FreeMarkerViewResolver.class, resolvers.get(0).getClass()); + + List views = handler.getDefaultViews(); + assertEquals(1, views.size()); + + MimeType type = MimeTypeUtils.parseMimeType("application/json;charset=UTF-8"); + assertEquals(type, views.get(0).getSupportedMediaTypes().get(0)); + } + + + private void assertHasConverter(List> converters, Class clazz, MediaType mediaType) { + ResolvableType type = ResolvableType.forClass(clazz); + assertTrue(converters.stream() + .filter(c -> c.canRead(type, mediaType) && c.canWrite(type, mediaType)) + .findAny() + .isPresent()); + } + + private ApplicationContext loadConfig(Class... configurationClasses) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(configurationClasses); + context.refresh(); + return context; + } + + + @Configuration + static class CustomPatchMatchConfig extends WebReactiveConfiguration { + + @Override + public void configurePathMatching(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); + configurer.setUseTrailingSlashMatch(false); + } + } + + @Configuration + static class CustomMessageConverterConfig extends WebReactiveConfiguration { + + @Override + protected void configureMessageConverters(List> converters) { + converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder())); + } + + @Override + protected void extendMessageConverters(List> converters) { + converters.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder())); + } + } + + @Configuration @SuppressWarnings("unused") + static class CustomViewResolverConfig extends WebReactiveConfiguration { + + @Override + protected void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + registry.defaultViews(new HttpMessageConverterView(new JacksonJsonEncoder())); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfig() { + return new FreeMarkerConfigurer(); + } + + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 217c52a6c26..3ad58a92b0a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -21,7 +21,6 @@ import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.xml.bind.annotation.XmlElement; @@ -40,19 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; -import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.JsonObjectDecoder; -import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -62,9 +49,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.reactive.CodecHttpMessageConverter; -import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; @@ -78,11 +62,9 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.result.SimpleResultHandler; -import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; -import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; -import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; @@ -385,61 +367,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Configuration @SuppressWarnings("unused") - static class FrameworkConfig { + static class FrameworkConfig extends WebReactiveConfiguration { - @Bean - public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); - } - - @Bean - public RequestMappingHandlerAdapter handlerAdapter() { - RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); - handlerAdapter.setMessageConverters(getDefaultMessageConverters()); - handlerAdapter.setConversionService(conversionService()); - return handlerAdapter; - } - - private List> getDefaultMessageConverters() { - return Arrays.asList( - new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), - new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder()), - new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), - new JacksonJsonDecoder(new JsonObjectDecoder()))); - } - - @Bean - public ConversionService conversionService() { - // TODO: test failures with DefaultConversionService - GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); - return service; - } - - @Bean - public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = new ArrayList<>(); - converters.add(new ResourceHttpMessageConverter()); - converters.addAll(getDefaultMessageConverters()); - return new ResponseBodyResultHandler(converters, conversionService()); - } - - @Bean - public SimpleResultHandler simpleHandlerResultHandler() { - return new SimpleResultHandler(conversionService()); - } - - @Bean - public ViewResolutionResultHandler viewResolverResultHandler() { - List resolvers = Collections.singletonList(freeMarkerViewResolver()); - return new ViewResolutionResultHandler(resolvers, conversionService()); - } - - @Bean - public ViewResolver freeMarkerViewResolver() { - return new FreeMarkerViewResolver("", ".ftl"); + @Override + protected void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); } @Bean