diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index ba51f4d3457..5ba57740a7d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; @@ -42,6 +43,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -59,6 +61,8 @@ import org.springframework.web.reactive.config.ResourceHandlerRegistry; import org.springframework.web.reactive.config.ViewResolverRegistry; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; @@ -93,6 +97,20 @@ public class WebFluxAutoConfiguration { return new OrderedHiddenHttpMethodFilter(); } + @Configuration(proxyBeanMethods = false) + public static class WelcomePageConfiguration { + + @Bean + public RouterFunction welcomePageRouterFunction(ApplicationContext applicationContext, + WebFluxProperties webFluxProperties, ResourceProperties resourceProperties) { + WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory( + new TemplateAvailabilityProviders(applicationContext), applicationContext, + resourceProperties.getStaticLocations(), webFluxProperties.getStaticPathPattern()); + return factory.createRouterFunction(); + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class }) @Import({ EnableWebFluxConfiguration.class }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java new file mode 100644 index 00000000000..656ff434205 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2020 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.boot.autoconfigure.web.reactive; + +import java.util.Arrays; + +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +/** + * A {@link RouterFunction} factory for an application's welcome page. Supports both + * static and templated files. If both a static and templated index page are available, + * the static page is preferred. + * + * @author Brian Clozel + */ +final class WelcomePageRouterFunctionFactory { + + private final String staticPathPattern; + + private final Resource welcomePage; + + private final boolean welcomePageTemplateExists; + + WelcomePageRouterFunctionFactory(TemplateAvailabilityProviders templateAvailabilityProviders, + ApplicationContext applicationContext, String[] staticLocations, String staticPathPattern) { + this.staticPathPattern = staticPathPattern; + this.welcomePage = getWelcomePage(applicationContext, staticLocations); + this.welcomePageTemplateExists = welcomeTemplateExists(templateAvailabilityProviders, applicationContext); + } + + private Resource getWelcomePage(ResourceLoader resourceLoader, String[] staticLocations) { + return Arrays.stream(staticLocations).map((location) -> getIndexHtml(resourceLoader, location)) + .filter(this::isReadable).findFirst().orElse(null); + } + + private Resource getIndexHtml(ResourceLoader resourceLoader, String location) { + return resourceLoader.getResource(location + "index.html"); + } + + private boolean isReadable(Resource resource) { + try { + return resource.exists() && (resource.getURL() != null); + } + catch (Exception ex) { + return false; + } + } + + private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders, + ApplicationContext applicationContext) { + return templateAvailabilityProviders.getProvider("index", applicationContext) != null; + } + + RouterFunction createRouterFunction() { + if (this.welcomePage != null && "/**".equals(this.staticPathPattern)) { + return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)), + (req) -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(this.welcomePage)); + } + else if (this.welcomePageTemplateExists) { + return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)), + (req) -> ServerResponse.ok().render("index")); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java new file mode 100644 index 00000000000..0b5405e7fdd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2012-2020 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.boot.autoconfigure.web.reactive; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WelcomePageRouterFunctionFactory} + * + * @author Brian Clozel + */ +class WelcomePageRouterFunctionFactoryTests { + + private StaticApplicationContext applicationContext; + + private final String[] noIndexLocations = { "classpath:/" }; + + private final String[] indexLocations = { "classpath:/public/", "classpath:/welcome-page/" }; + + @BeforeEach + void setup() { + this.applicationContext = new StaticApplicationContext(); + this.applicationContext.refresh(); + } + + @Test + void handlesRequestForStaticPageThatAcceptsTextHtml() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void handlesRequestForStaticPageThatAcceptsAll() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void doesNotHandleRequestThatDoesNotAcceptTextHtml() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isNotFound(); + } + + @Test + void handlesRequestWithNoAcceptHeader() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void handlesRequestWithEmptyAcceptHeader() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").header(HttpHeaders.ACCEPT, "").exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void producesNotFoundResponseWhenThereIsNoWelcomePage() { + WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.noIndexLocations, "/**"); + assertThat(factory.createRouterFunction()).isNull(); + } + + @Test + void handlesRequestForTemplateThatAcceptsTextHtml() { + WebTestClient client = withTemplateIndex(); + client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-template"); + } + + @Test + void handlesRequestForTemplateThatAcceptsAll() { + WebTestClient client = withTemplateIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-template"); + } + + @Test + void prefersAStaticResourceToATemplate() { + WebTestClient client = withStaticAndTemplateIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + private WebTestClient createClient(WelcomePageRouterFunctionFactory factory) { + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build(); + } + + private WebTestClient createClient(WelcomePageRouterFunctionFactory factory, ViewResolver viewResolver) { + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()) + .handlerStrategies(HandlerStrategies.builder().viewResolver(viewResolver).build()).build(); + } + + private WebTestClient withStaticIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.indexLocations, "/**"); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build(); + } + + private WebTestClient withTemplateIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.noIndexLocations); + TestViewResolver testViewResolver = new TestViewResolver(); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()) + .handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build(); + } + + private WebTestClient withStaticAndTemplateIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.indexLocations); + TestViewResolver testViewResolver = new TestViewResolver(); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()) + .handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build(); + } + + private WelcomePageRouterFunctionFactory factoryWithoutTemplateSupport(String[] locations, + String staticPathPattern) { + return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders(), this.applicationContext, + locations, staticPathPattern); + } + + private WelcomePageRouterFunctionFactory factoryWithTemplateSupport(String[] locations) { + return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders("index"), + this.applicationContext, locations, "/**"); + } + + static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders { + + TestTemplateAvailabilityProviders() { + super(Collections.emptyList()); + } + + TestTemplateAvailabilityProviders(String viewName) { + this((view, environment, classLoader, resourceLoader) -> view.equals(viewName)); + } + + TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) { + super(Collections.singletonList(provider)); + } + + } + + static class TestViewResolver implements ViewResolver { + + @Override + public Mono resolveViewName(String viewName, Locale locale) { + return Mono.just(new TestView()); + } + + } + + static class TestView implements View { + + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + + @Override + public Mono render(Map model, MediaType contentType, ServerWebExchange exchange) { + DataBuffer buffer = this.bufferFactory.wrap("welcome-page-template".getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html index 8b137891791..10babc65119 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/welcome-page/index.html @@ -1 +1 @@ - +welcome-page-static \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index a649cf770e0..dcd76a47209 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -2796,6 +2796,14 @@ Any resources with a path in `+/webjars/**+` are served from jar files if they a TIP: Spring WebFlux applications do not strictly depend on the Servlet API, so they cannot be deployed as war files and do not use the `src/main/webapp` directory. +[[boot-features-webflux-welcome-page]] +==== Welcome Page +Spring Boot supports both static and templated welcome pages. +It first looks for an `index.html` file in the configured static content locations. +If one is not found, it then looks for an `index` template. +If either is found, it is automatically used as the welcome page of the application. + + [[boot-features-webflux-template-engines]] ==== Template Engines