diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java index 9aea22c81d5..6a7d5f1ab2f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java @@ -29,16 +29,23 @@ import org.springframework.boot.actuate.web.ErrorController; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; import org.springframework.context.expression.MapAccessor; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.ClassUtils; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.web.servlet.DispatcherServlet; @@ -79,10 +86,28 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom @Bean(name = "error") @ConditionalOnMissingBean(name = "error") + @ConditionalOnExpression("${error.whitelabel.enabled:true}") + @Conditional(ErrorTemplateMissingCondition.class) public View defaultErrorView() { return this.defaultErrorView; } + private static class ErrorTemplateMissingCondition extends SpringBootCondition { + @Override + public Outcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + if (ClassUtils.isPresent("org.thymeleaf.spring3.SpringTemplateEngine", + context.getClassLoader())) { + if (DefaultTemplateResolverConfiguration.templateExists( + context.getEnvironment(), context.getResourceLoader(), "error")) { + return Outcome.noMatch("Thymeleaf template found for error view"); + } + } + // FIXME: add matcher for JSP view if Jasper detected + return Outcome.match("no error template view detected"); + }; + } + private static class SpelView implements View { private final String template; diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java index 3eb659e08f7..a14ec544782 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java @@ -58,10 +58,12 @@ import org.thymeleaf.templateresolver.TemplateResolver; @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class ThymeleafAutoConfiguration { + public static final String DEFAULT_PREFIX = "classpath:/templates/"; + public static final String DEFAULT_SUFFIX = ".html"; + @Configuration @ConditionalOnMissingBean(name = "defaultTemplateResolver") - protected static class DefaultTemplateResolverConfiguration implements - EnvironmentAware { + public static class DefaultTemplateResolverConfiguration implements EnvironmentAware { @Autowired private ResourceLoader resourceLoader = new DefaultResourceLoader(); @@ -96,9 +98,8 @@ public class ThymeleafAutoConfiguration { return "SPRING"; } }); - resolver.setPrefix(this.environment.getProperty("prefix", - "classpath:/templates/")); - resolver.setSuffix(this.environment.getProperty("suffix", ".html")); + resolver.setPrefix(this.environment.getProperty("prefix", DEFAULT_PREFIX)); + resolver.setSuffix(this.environment.getProperty("suffix", DEFAULT_SUFFIX)); resolver.setTemplateMode(this.environment.getProperty("mode", "HTML5")); resolver.setCharacterEncoding(this.environment.getProperty("encoding", "UTF-8")); @@ -107,6 +108,15 @@ public class ThymeleafAutoConfiguration { return resolver; } + public static boolean templateExists(Environment environment, + ResourceLoader resourceLoader, String view) { + String prefix = environment.getProperty("spring.thymeleaf.prefix", + ThymeleafAutoConfiguration.DEFAULT_PREFIX); + String suffix = environment.getProperty("spring.thymeleaf.suffix", + ThymeleafAutoConfiguration.DEFAULT_SUFFIX); + return resourceLoader.getResource(prefix + view + suffix).exists(); + } + } @Configuration @@ -182,6 +192,7 @@ public class ThymeleafAutoConfiguration { public SpringSecurityDialect securityDialect() { return new SpringSecurityDialect(); } + } } diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplicationTests.java index aa5f4b3ff56..de1fb02d67d 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/test/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplicationTests.java @@ -112,6 +112,8 @@ public class SampleActuatorUiApplicationTests { .contains("")); assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody() .contains("")); + assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody() + .contains("Please contact the operator with the above information")); } private RestTemplate getRestTemplate() { diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml index 62e4a205ead..97f4911d2bb 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml @@ -1,5 +1,5 @@ - + diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/SampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/SampleActuatorApplicationTests.java index 66213efee04..93959fa2550 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/SampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/SampleActuatorApplicationTests.java @@ -18,10 +18,12 @@ package org.springframework.boot.sample.ops; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -35,8 +37,12 @@ import org.junit.Test; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.properties.SecurityProperties; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; @@ -134,26 +140,42 @@ public class SampleActuatorApplicationTests { @Test public void testErrorPage() throws Exception { - ResponseEntity entity = getRestTemplate().getForEntity( - "http://localhost:8080/health", String.class); - assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals("ok", entity.getBody()); + ResponseEntity entity = getRestTemplate("user", getPassword()) + .getForEntity("http://localhost:8080/foo", String.class); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, entity.getStatusCode()); + String body = entity.getBody(); + assertNotNull(body); + assertTrue("Wrong body: " + body, + body.contains("\"error\":")); + } + + @Test + public void testHtmlErrorPage() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); + HttpEntity request = new HttpEntity(headers); + ResponseEntity entity = getRestTemplate("user", getPassword()).exchange( + "http://localhost:8080/foo", HttpMethod.GET, request, String.class); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, entity.getStatusCode()); + String body = entity.getBody(); + assertNotNull("Body was null", body); + assertTrue("Wrong body: " + body, + body.contains("This application has no explicit mapping for /error")); } @Test public void testTrace() throws Exception { - getRestTemplate().getForEntity( - "http://localhost:8080/health", String.class); + getRestTemplate().getForEntity("http://localhost:8080/health", String.class); @SuppressWarnings("rawtypes") - ResponseEntity entity = getRestTemplate("user", getPassword()).getForEntity( - "http://localhost:8080/trace", List.class); + ResponseEntity entity = getRestTemplate("user", getPassword()) + .getForEntity("http://localhost:8080/trace", List.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); @SuppressWarnings("unchecked") - List> list = (List>) entity.getBody(); - Map trace = list.get(list.size()-1); + List> list = (List>) entity.getBody(); + Map trace = list.get(list.size() - 1); @SuppressWarnings("unchecked") - Map map = (Map) ((Map) ((Map) - trace.get("info")).get("headers")).get("response"); + Map map = (Map) ((Map) ((Map) trace + .get("info")).get("headers")).get("response"); assertEquals("200", map.get("status")); } diff --git a/spring-boot-samples/spring-boot-sample-web-ui/src/main/java/org/springframework/boot/sample/ui/mvc/MessageController.java b/spring-boot-samples/spring-boot-sample-web-ui/src/main/java/org/springframework/boot/sample/ui/mvc/MessageController.java index 4831020739e..672f5799221 100644 --- a/spring-boot-samples/spring-boot-sample-web-ui/src/main/java/org/springframework/boot/sample/ui/mvc/MessageController.java +++ b/spring-boot-samples/spring-boot-sample-web-ui/src/main/java/org/springframework/boot/sample/ui/mvc/MessageController.java @@ -66,4 +66,10 @@ public class MessageController { redirect.addFlashAttribute("globalMessage", "Successfully created a new message"); return new ModelAndView("redirect:/{message.id}", "message.id", message.getId()); } + + @RequestMapping("/foo") + public String foo() { + throw new RuntimeException("Expected exception in controller"); + } + } diff --git a/spring-boot-samples/spring-boot-sample-web-ui/src/test/java/org/springframework/boot/sample/ui/MessageControllerWebTests.java b/spring-boot-samples/spring-boot-sample-web-ui/src/test/java/org/springframework/boot/sample/ui/MessageControllerWebTests.java index e4b659d5111..369380e49c1 100644 --- a/spring-boot-samples/spring-boot-sample-web-ui/src/test/java/org/springframework/boot/sample/ui/MessageControllerWebTests.java +++ b/spring-boot-samples/spring-boot-sample-web-ui/src/test/java/org/springframework/boot/sample/ui/MessageControllerWebTests.java @@ -1,5 +1,12 @@ package org.springframework.boot.sample.ui; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.util.regex.Pattern; import org.hamcrest.Description; @@ -15,16 +22,9 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * A Basic Spring MVC Test for the Sample Controller" - * + * * @author Biju Kunjummen */ @RunWith(SpringJUnit4ClassRunner.class)