Add out-of-the-box support for status error pages

Allow convention based status error pages. Static HTML or templates
can be used by placing the appropriately named file under a `/error`
folder. For example:

	/src/main/resource/templates/error/404.ftl
or	/src/main/resource/public/error/404.html

Pages can also be named after the status series (5xx or 4xx).

Fixes gh-2691
This commit is contained in:
Phillip Webb
2016-04-29 10:36:34 -07:00
parent 0bd246a3ed
commit 0bf025af7b
15 changed files with 646 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-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.
@@ -16,15 +16,20 @@
package org.springframework.boot.autoconfigure.web;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
/**
* Abstract base class for error {@link Controller} implementations.
@@ -38,9 +43,27 @@ public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
private final List<ErrorViewResolver> errorViewResolvers;
public AbstractErrorController(ErrorAttributes errorAttributes) {
this(errorAttributes, null);
}
public AbstractErrorController(ErrorAttributes errorAttributes,
List<ErrorViewResolver> errorViewResolvers) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
}
private List<ErrorViewResolver> sortErrorViewResolvers(
List<ErrorViewResolver> resolvers) {
List<ErrorViewResolver> sorted = new ArrayList<ErrorViewResolver>();
if (resolvers != null) {
sorted.addAll(resolvers);
AnnotationAwareOrderComparator.sortIfNecessary(sorted);
}
return sorted;
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
@@ -72,4 +95,26 @@ public abstract class AbstractErrorController implements ErrorController {
}
}
/**
* Resolve any specific error views. By default this method delegates to
* {@link ErrorViewResolver ErrorViewResolvers}.
* @param request the request
* @param response the response
* @param status the HTTP status
* @param model the suggested model
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
* @since 1.4.0
*/
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.web;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@@ -58,7 +60,19 @@ public class BasicErrorController extends AbstractErrorController {
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
super(errorAttributes);
this(errorAttributes, errorProperties,
Collections.<ErrorViewResolver>emptyList());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
* @param errorViewResolvers error view resolvers
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@@ -71,10 +85,12 @@ public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("error", model);
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
@RequestMapping

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2012-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.boot.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
/**
* Default {@link ErrorViewResolver} implementation that attempts to resolve error views
* using well known conventions. Will search for templates and static assets under
* {@code '/error'} using the {@link HttpStatus status code} and the
* {@link HttpStatus#series() status series}.
* <p>
* For example, an {@code HTTP 404} will search (in the specific order):
* <ul>
* <li>{@code '/<templates>/error/404.<ext>'}</li>
* <li>{@code '/<static>/error/404.html'}</li>
* <li>{@code '/<templates>/error/4xx.<ext>'}</li>
* <li>{@code '/<static>/error/4xx.html'}</li>
* </ul>
*
* @author Phillip Webb
* @since 1.4.0
*/
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<TemplateAvailabilityProvider> templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this(applicationContext, resourceProperties,
loadTemplateAvailabilityProviders(applicationContext));
}
private static List<TemplateAvailabilityProvider> loadTemplateAvailabilityProviders(
ApplicationContext applicationContext) {
return SpringFactoriesLoader.loadFactories(TemplateAvailabilityProvider.class,
applicationContext == null ? null : applicationContext.getClassLoader());
}
DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
List<TemplateAvailabilityProvider> templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
ModelAndView modelAndView = resolveTemplate(viewName, model);
if (modelAndView == null) {
modelAndView = resolveResource(viewName, model);
}
return modelAndView;
}
private ModelAndView resolveTemplate(String viewName, Map<String, Object> model) {
for (TemplateAvailabilityProvider templateAvailabilityProvider : this.templateAvailabilityProviders) {
if (templateAvailabilityProvider.isTemplateAvailable("error/" + viewName,
this.applicationContext.getEnvironment(),
this.applicationContext.getClassLoader(), this.applicationContext)) {
return new ModelAndView("error/" + viewName, model);
}
}
return null;
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative("error/" + viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@link View} backed by a HTML resource.
*/
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
FileCopyUtils.copy(this.resource.getInputStream(),
response.getOutputStream());
}
}
}

View File

@@ -27,11 +27,13 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -40,9 +42,11 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ErrorPageRegistrar;
import org.springframework.boot.web.servlet.ErrorPageRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
@@ -69,18 +73,28 @@ import org.springframework.web.util.HtmlUtils;
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication
// Ensure this loads before the main WebMvcAutoConfiguration so that the error View is
// available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
private final ServerProperties properties;
private final ApplicationContext applicationContext;
public ErrorMvcAutoConfiguration(ServerProperties properties) {
this.properties = properties;
private final ServerProperties serverProperties;
private final ResourceProperties resourceProperties;
@Autowired(required = false)
private List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
ServerProperties serverProperties, ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.serverProperties = serverProperties;
this.resourceProperties = resourceProperties;
}
@Bean
@@ -92,12 +106,21 @@ public class ErrorMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.properties.getError());
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.properties);
return new ErrorPageCustomizer(this.serverProperties);
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
}
@Bean

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-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.boot.autoconfigure.web;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
/**
* Interface that can be implemented by beans that resolve error views.
*
* @author Phillip Webb
* @since 1.4.0
*/
public interface ErrorViewResolver {
/**
* Resolve an error view for the specified details.
* @param request the source request
* @param status the http status of the error
* @param model the suggested model to be used with the view
* @return a resolved {@link ModelAndView} or {@code null}
*/
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model);
}

View File

@@ -32,6 +32,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.MinimalWebConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@@ -39,6 +40,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
@@ -173,6 +175,17 @@ public class BasicErrorControllerIntegrationTests {
assertThat(resp).contains(MethodArgumentNotValidException.class.getName());
}
@Test
public void testConventionTemplateMapping() throws Exception {
load();
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/noStorage")))
.accept(MediaType.TEXT_HTML).build();
ResponseEntity<String> entity = new TestRestTemplate().exchange(request,
String.class);
String resp = entity.getBody().toString();
assertThat(resp).contains("We are out of storage");
}
private void assertErrorAttributes(Map<?, ?> content, String status, String error,
Class<?> exception, String message, String path) {
assertThat(content.get("status")).as("Wrong status").isEqualTo(status);
@@ -201,6 +214,7 @@ public class BasicErrorControllerIntegrationTests {
@Configuration
@MinimalWebConfiguration
@Import(FreeMarkerAutoConfiguration.class)
public static class TestConfiguration {
// For manual testing
@@ -254,12 +268,22 @@ public class BasicErrorControllerIntegrationTests {
return body.content;
}
@RequestMapping(path = "/noStorage")
public String noStorage() {
throw new InsufficientStorageException();
}
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
@SuppressWarnings("serial")
private static class ExpectedException extends RuntimeException {
}
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
private static class InsufficientStorageException extends RuntimeException {
}
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@SuppressWarnings("serial")
private static class NoReasonExpectedException extends RuntimeException {

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2012-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.boot.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link DefaultErrorViewResolver}.
*
* @author Phillip Webb
*/
public class DefaultErrorViewResolverTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private DefaultErrorViewResolver resolver;
@Mock
private TemplateAvailabilityProvider templateAvailabilityProvider;
private ResourceProperties resourceProperties;
private Map<String, Object> model = new HashMap<String, Object>();
private HttpServletRequest request = new MockHttpServletRequest();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
this.resourceProperties = new ResourceProperties();
List<TemplateAvailabilityProvider> templateAvailabilityProviders = Collections
.singletonList(this.templateAvailabilityProvider);
this.resolver = new DefaultErrorViewResolver(applicationContext,
this.resourceProperties, templateAvailabilityProviders);
}
@Test
public void createWhenApplicationContextIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ApplicationContext must not be null");
new DefaultErrorViewResolver(null, new ResourceProperties());
}
@Test
public void createWhenResourcePropertiesIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ResourceProperties must not be null");
new DefaultErrorViewResolver(mock(ApplicationContext.class), null);
}
@Test
public void resolveWhenNoMatchShouldReturnNull() throws Exception {
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNull();
}
@Test
public void resolveWhenExactTemplateMatchShouldReturnTemplate() throws Exception {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class))).willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNotNull();
assertThat(resolved.getViewName()).isEqualTo("error/404");
verify(this.templateAvailabilityProvider).isTemplateAvailable(eq("error/404"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class));
verifyNoMoreInteractions(this.templateAvailabilityProvider);
}
@Test
public void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() throws Exception {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/5xx"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class))).willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.SERVICE_UNAVAILABLE, this.model);
assertThat(resolved.getViewName()).isEqualTo("error/5xx");
}
@Test
public void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() throws Exception {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class))).willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(resolved.getViewName()).isEqualTo("error/4xx");
}
@Test
public void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/exact");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(render(resolved)).isEqualTo("exact/404");
}
@Test
public void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/4xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(render(resolved)).isEqualTo("4xx/4xx");
}
@Test
public void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/5xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.INTERNAL_SERVER_ERROR, this.model);
assertThat(render(resolved)).isEqualTo("5xx/5xx");
}
@Test
public void resolveWhenTemplateAndResourceMatchShouldFavorTemplate()
throws Exception {
setResourceLocation("/exact");
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class))).willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(resolved.getViewName()).isEqualTo("error/404");
}
@Test
public void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource()
throws Exception {
setResourceLocation("/exact");
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class))).willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(render(resolved)).isEqualTo("exact/404");
}
@Test
public void orderShouldBeLowest() throws Exception {
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
}
@Test
public void setOrderShouldChangeOrder() throws Exception {
this.resolver.setOrder(123);
assertThat(this.resolver.getOrder()).isEqualTo(123);
}
private void setResourceLocation(String path) {
String packageName = getClass().getPackage().getName();
this.resourceProperties.setStaticLocations(new String[] {
"classpath:" + packageName.replace(".", "/") + path + "/" });
}
private String render(ModelAndView modelAndView) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
modelAndView.getView().render(this.model, this.request, response);
return response.getContentAsString().trim();
}
}

View File

@@ -0,0 +1 @@
We are out of storage