001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.autoconfigure.web.servlet.error;
018
019import java.util.Date;
020import java.util.List;
021import java.util.Map;
022import java.util.stream.Collectors;
023
024import javax.servlet.Servlet;
025import javax.servlet.http.HttpServletRequest;
026import javax.servlet.http.HttpServletResponse;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
032import org.springframework.beans.BeansException;
033import org.springframework.beans.factory.ObjectProvider;
034import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
035import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
036import org.springframework.boot.autoconfigure.AutoConfigureBefore;
037import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
038import org.springframework.boot.autoconfigure.condition.ConditionMessage;
039import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
040import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
041import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
042import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
043import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
044import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
045import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
046import org.springframework.boot.autoconfigure.condition.SearchStrategy;
047import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
048import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
049import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
050import org.springframework.boot.autoconfigure.web.ResourceProperties;
051import org.springframework.boot.autoconfigure.web.ServerProperties;
052import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
053import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
054import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
055import org.springframework.boot.context.properties.EnableConfigurationProperties;
056import org.springframework.boot.web.server.ErrorPage;
057import org.springframework.boot.web.server.ErrorPageRegistrar;
058import org.springframework.boot.web.server.ErrorPageRegistry;
059import org.springframework.boot.web.server.WebServerFactoryCustomizer;
060import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
061import org.springframework.boot.web.servlet.error.ErrorAttributes;
062import org.springframework.boot.web.servlet.error.ErrorController;
063import org.springframework.context.ApplicationContext;
064import org.springframework.context.annotation.Bean;
065import org.springframework.context.annotation.ConditionContext;
066import org.springframework.context.annotation.Conditional;
067import org.springframework.context.annotation.Configuration;
068import org.springframework.core.Ordered;
069import org.springframework.core.type.AnnotatedTypeMetadata;
070import org.springframework.web.servlet.DispatcherServlet;
071import org.springframework.web.servlet.View;
072import org.springframework.web.servlet.view.BeanNameViewResolver;
073import org.springframework.web.util.HtmlUtils;
074
075/**
076 * {@link EnableAutoConfiguration Auto-configuration} to render errors via an MVC error
077 * controller.
078 *
079 * @author Dave Syer
080 * @author Andy Wilkinson
081 * @author Stephane Nicoll
082 * @author Brian Clozel
083 */
084@Configuration
085@ConditionalOnWebApplication(type = Type.SERVLET)
086@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
087// Load before the main WebMvcAutoConfiguration so that the error View is available
088@AutoConfigureBefore(WebMvcAutoConfiguration.class)
089@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class,
090                WebMvcProperties.class })
091public class ErrorMvcAutoConfiguration {
092
093        private final ServerProperties serverProperties;
094
095        private final DispatcherServletPath dispatcherServletPath;
096
097        private final List<ErrorViewResolver> errorViewResolvers;
098
099        public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
100                        DispatcherServletPath dispatcherServletPath,
101                        ObjectProvider<ErrorViewResolver> errorViewResolvers) {
102                this.serverProperties = serverProperties;
103                this.dispatcherServletPath = dispatcherServletPath;
104                this.errorViewResolvers = errorViewResolvers.orderedStream()
105                                .collect(Collectors.toList());
106        }
107
108        @Bean
109        @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
110        public DefaultErrorAttributes errorAttributes() {
111                return new DefaultErrorAttributes(
112                                this.serverProperties.getError().isIncludeException());
113        }
114
115        @Bean
116        @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
117        public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
118                return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
119                                this.errorViewResolvers);
120        }
121
122        @Bean
123        public ErrorPageCustomizer errorPageCustomizer() {
124                return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
125        }
126
127        @Bean
128        public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
129                return new PreserveErrorControllerTargetClassPostProcessor();
130        }
131
132        @Configuration
133        static class DefaultErrorViewResolverConfiguration {
134
135                private final ApplicationContext applicationContext;
136
137                private final ResourceProperties resourceProperties;
138
139                DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
140                                ResourceProperties resourceProperties) {
141                        this.applicationContext = applicationContext;
142                        this.resourceProperties = resourceProperties;
143                }
144
145                @Bean
146                @ConditionalOnBean(DispatcherServlet.class)
147                @ConditionalOnMissingBean
148                public DefaultErrorViewResolver conventionErrorViewResolver() {
149                        return new DefaultErrorViewResolver(this.applicationContext,
150                                        this.resourceProperties);
151                }
152
153        }
154
155        @Configuration
156        @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
157        @Conditional(ErrorTemplateMissingCondition.class)
158        protected static class WhitelabelErrorViewConfiguration {
159
160                private final StaticView defaultErrorView = new StaticView();
161
162                @Bean(name = "error")
163                @ConditionalOnMissingBean(name = "error")
164                public View defaultErrorView() {
165                        return this.defaultErrorView;
166                }
167
168                // If the user adds @EnableWebMvc then the bean name view resolver from
169                // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
170                @Bean
171                @ConditionalOnMissingBean
172                public BeanNameViewResolver beanNameViewResolver() {
173                        BeanNameViewResolver resolver = new BeanNameViewResolver();
174                        resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
175                        return resolver;
176                }
177
178        }
179
180        /**
181         * {@link SpringBootCondition} that matches when no error template view is detected.
182         */
183        private static class ErrorTemplateMissingCondition extends SpringBootCondition {
184
185                @Override
186                public ConditionOutcome getMatchOutcome(ConditionContext context,
187                                AnnotatedTypeMetadata metadata) {
188                        ConditionMessage.Builder message = ConditionMessage
189                                        .forCondition("ErrorTemplate Missing");
190                        TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
191                                        context.getClassLoader());
192                        TemplateAvailabilityProvider provider = providers.getProvider("error",
193                                        context.getEnvironment(), context.getClassLoader(),
194                                        context.getResourceLoader());
195                        if (provider != null) {
196                                return ConditionOutcome
197                                                .noMatch(message.foundExactly("template from " + provider));
198                        }
199                        return ConditionOutcome
200                                        .match(message.didNotFind("error template view").atAll());
201                }
202
203        }
204
205        /**
206         * Simple {@link View} implementation that writes a default HTML error page.
207         */
208        private static class StaticView implements View {
209
210                private static final Log logger = LogFactory.getLog(StaticView.class);
211
212                @Override
213                public void render(Map<String, ?> model, HttpServletRequest request,
214                                HttpServletResponse response) throws Exception {
215                        if (response.isCommitted()) {
216                                String message = getMessage(model);
217                                logger.error(message);
218                                return;
219                        }
220                        StringBuilder builder = new StringBuilder();
221                        Date timestamp = (Date) model.get("timestamp");
222                        Object message = model.get("message");
223                        Object trace = model.get("trace");
224                        if (response.getContentType() == null) {
225                                response.setContentType(getContentType());
226                        }
227                        builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
228                                        "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
229                                        .append("<div id='created'>").append(timestamp).append("</div>")
230                                        .append("<div>There was an unexpected error (type=")
231                                        .append(htmlEscape(model.get("error"))).append(", status=")
232                                        .append(htmlEscape(model.get("status"))).append(").</div>");
233                        if (message != null) {
234                                builder.append("<div>").append(htmlEscape(message)).append("</div>");
235                        }
236                        if (trace != null) {
237                                builder.append("<div style='white-space:pre-wrap;'>")
238                                                .append(htmlEscape(trace)).append("</div>");
239                        }
240                        builder.append("</body></html>");
241                        response.getWriter().append(builder.toString());
242                }
243
244                private String htmlEscape(Object input) {
245                        return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
246                }
247
248                private String getMessage(Map<String, ?> model) {
249                        Object path = model.get("path");
250                        String message = "Cannot render error page for request [" + path + "]";
251                        if (model.get("message") != null) {
252                                message += " and exception [" + model.get("message") + "]";
253                        }
254                        message += " as the response has already been committed.";
255                        message += " As a result, the response may have the wrong status code.";
256                        return message;
257                }
258
259                @Override
260                public String getContentType() {
261                        return "text/html";
262                }
263
264        }
265
266        /**
267         * {@link WebServerFactoryCustomizer} that configures the server's error pages.
268         */
269        private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
270
271                private final ServerProperties properties;
272
273                private final DispatcherServletPath dispatcherServletPath;
274
275                protected ErrorPageCustomizer(ServerProperties properties,
276                                DispatcherServletPath dispatcherServletPath) {
277                        this.properties = properties;
278                        this.dispatcherServletPath = dispatcherServletPath;
279                }
280
281                @Override
282                public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
283                        ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
284                                        .getRelativePath(this.properties.getError().getPath()));
285                        errorPageRegistry.addErrorPages(errorPage);
286                }
287
288                @Override
289                public int getOrder() {
290                        return 0;
291                }
292
293        }
294
295        /**
296         * {@link BeanFactoryPostProcessor} to ensure that the target class of ErrorController
297         * MVC beans are preserved when using AOP.
298         */
299        static class PreserveErrorControllerTargetClassPostProcessor
300                        implements BeanFactoryPostProcessor {
301
302                @Override
303                public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
304                                throws BeansException {
305                        String[] errorControllerBeans = beanFactory
306                                        .getBeanNamesForType(ErrorController.class, false, false);
307                        for (String errorControllerBean : errorControllerBeans) {
308                                try {
309                                        beanFactory.getBeanDefinition(errorControllerBean).setAttribute(
310                                                        AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
311                                }
312                                catch (Throwable ex) {
313                                        // Ignore
314                                }
315                        }
316                }
317
318        }
319
320}