Disable whitelabel view if Thymeleaf error.html detected

This commit is contained in:
Dave Syer 2013-10-23 10:23:51 -04:00
parent 08cf5b4139
commit 0498617411
7 changed files with 92 additions and 26 deletions

View File

@ -29,16 +29,23 @@ import org.springframework.boot.actuate.web.ErrorController;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.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.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.context.annotation.Bean; 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.context.expression.MapAccessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@ -79,10 +86,28 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Bean(name = "error") @Bean(name = "error")
@ConditionalOnMissingBean(name = "error") @ConditionalOnMissingBean(name = "error")
@ConditionalOnExpression("${error.whitelabel.enabled:true}")
@Conditional(ErrorTemplateMissingCondition.class)
public View defaultErrorView() { public View defaultErrorView() {
return this.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 static class SpelView implements View {
private final String template; private final String template;

View File

@ -58,10 +58,12 @@ import org.thymeleaf.templateresolver.TemplateResolver;
@AutoConfigureAfter(WebMvcAutoConfiguration.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class ThymeleafAutoConfiguration { public class ThymeleafAutoConfiguration {
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
@Configuration @Configuration
@ConditionalOnMissingBean(name = "defaultTemplateResolver") @ConditionalOnMissingBean(name = "defaultTemplateResolver")
protected static class DefaultTemplateResolverConfiguration implements public static class DefaultTemplateResolverConfiguration implements EnvironmentAware {
EnvironmentAware {
@Autowired @Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader(); private ResourceLoader resourceLoader = new DefaultResourceLoader();
@ -96,9 +98,8 @@ public class ThymeleafAutoConfiguration {
return "SPRING"; return "SPRING";
} }
}); });
resolver.setPrefix(this.environment.getProperty("prefix", resolver.setPrefix(this.environment.getProperty("prefix", DEFAULT_PREFIX));
"classpath:/templates/")); resolver.setSuffix(this.environment.getProperty("suffix", DEFAULT_SUFFIX));
resolver.setSuffix(this.environment.getProperty("suffix", ".html"));
resolver.setTemplateMode(this.environment.getProperty("mode", "HTML5")); resolver.setTemplateMode(this.environment.getProperty("mode", "HTML5"));
resolver.setCharacterEncoding(this.environment.getProperty("encoding", resolver.setCharacterEncoding(this.environment.getProperty("encoding",
"UTF-8")); "UTF-8"));
@ -107,6 +108,15 @@ public class ThymeleafAutoConfiguration {
return resolver; 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 @Configuration
@ -182,6 +192,7 @@ public class ThymeleafAutoConfiguration {
public SpringSecurityDialect securityDialect() { public SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect(); return new SpringSecurityDialect();
} }
} }
} }

View File

@ -112,6 +112,8 @@ public class SampleActuatorUiApplicationTests {
.contains("<html>")); .contains("<html>"));
assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody() assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody()
.contains("<body>")); .contains("<body>"));
assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody()
.contains("Please contact the operator with the above information"));
} }
private RestTemplate getRestTemplate() { private RestTemplate getRestTemplate() {

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/> <include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- logger name="org.springframework" level="DEBUG"/ --> <!-- logger name="org.springframework.boot" level="DEBUG"/-->
</configuration> </configuration>

View File

@ -18,10 +18,12 @@ package org.springframework.boot.sample.ops;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -35,8 +37,12 @@ import org.junit.Test;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.properties.SecurityProperties; import org.springframework.boot.actuate.properties.SecurityProperties;
import org.springframework.context.ConfigurableApplicationContext; 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.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpRequestInterceptor;
@ -134,26 +140,42 @@ public class SampleActuatorApplicationTests {
@Test @Test
public void testErrorPage() throws Exception { public void testErrorPage() throws Exception {
ResponseEntity<String> entity = getRestTemplate().getForEntity( ResponseEntity<String> entity = getRestTemplate("user", getPassword())
"http://localhost:8080/health", String.class); .getForEntity("http://localhost:8080/foo", String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, entity.getStatusCode());
assertEquals("ok", entity.getBody()); 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<Void>(headers);
ResponseEntity<String> 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 @Test
public void testTrace() throws Exception { public void testTrace() throws Exception {
getRestTemplate().getForEntity( getRestTemplate().getForEntity("http://localhost:8080/health", String.class);
"http://localhost:8080/health", String.class);
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
ResponseEntity<List> entity = getRestTemplate("user", getPassword()).getForEntity( ResponseEntity<List> entity = getRestTemplate("user", getPassword())
"http://localhost:8080/trace", List.class); .getForEntity("http://localhost:8080/trace", List.class);
assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals(HttpStatus.OK, entity.getStatusCode());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Map<String,Object>> list = (List<Map<String, Object>>) entity.getBody(); List<Map<String, Object>> list = (List<Map<String, Object>>) entity.getBody();
Map<String, Object> trace = list.get(list.size()-1); Map<String, Object> trace = list.get(list.size() - 1);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) ((Map<String, Object>) Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) ((Map<String, Object>) trace
trace.get("info")).get("headers")).get("response"); .get("info")).get("headers")).get("response");
assertEquals("200", map.get("status")); assertEquals("200", map.get("status"));
} }

View File

@ -66,4 +66,10 @@ public class MessageController {
redirect.addFlashAttribute("globalMessage", "Successfully created a new message"); redirect.addFlashAttribute("globalMessage", "Successfully created a new message");
return new ModelAndView("redirect:/{message.id}", "message.id", message.getId()); return new ModelAndView("redirect:/{message.id}", "message.id", message.getId());
} }
@RequestMapping("/foo")
public String foo() {
throw new RuntimeException("Expected exception in controller");
}
} }

View File

@ -1,5 +1,12 @@
package org.springframework.boot.sample.ui; 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 java.util.regex.Pattern;
import org.hamcrest.Description; import org.hamcrest.Description;
@ -15,13 +22,6 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext; 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" * A Basic Spring MVC Test for the Sample Controller"
* *