Skip to content

Commit

Permalink
Add welcome page support for Spring WebFlux
Browse files Browse the repository at this point in the history
This commit adds the support for static and templated welcome pages with
Spring WebFlux. The implementation is backed by a `RouterFunction`
that's serving a static `index.html` file or rendering an `index` view.

Closes gh-9785
  • Loading branch information
bclozel committed May 22, 2020
1 parent ed4a7d4 commit 525e03d
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand All @@ -42,6 +43,7 @@
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;
Expand All @@ -59,6 +61,8 @@
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;
Expand Down Expand Up @@ -93,6 +97,20 @@ public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

@Configuration(proxyBeanMethods = false)
public static class WelcomePageConfiguration {

@Bean
public RouterFunction<ServerResponse> 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 })
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ServerResponse> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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<View> resolveViewName(String viewName, Locale locale) {
return Mono.just(new TestView());
}

}

static class TestView implements View {

private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();

@Override
public Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange) {
DataBuffer buffer = this.bufferFactory.wrap("welcome-page-template".getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}

}

}
Original file line number Diff line number Diff line change
@@ -1 +1 @@

welcome-page-static
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 525e03d

Please sign in to comment.