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.Collections;
020import java.util.EnumMap;
021import java.util.Map;
022
023import javax.servlet.http.HttpServletRequest;
024import javax.servlet.http.HttpServletResponse;
025
026import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
027import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
028import org.springframework.boot.autoconfigure.web.ResourceProperties;
029import org.springframework.context.ApplicationContext;
030import org.springframework.core.Ordered;
031import org.springframework.core.io.Resource;
032import org.springframework.http.HttpStatus;
033import org.springframework.http.HttpStatus.Series;
034import org.springframework.http.MediaType;
035import org.springframework.util.Assert;
036import org.springframework.util.FileCopyUtils;
037import org.springframework.web.servlet.ModelAndView;
038import org.springframework.web.servlet.View;
039
040/**
041 * Default {@link ErrorViewResolver} implementation that attempts to resolve error views
042 * using well known conventions. Will search for templates and static assets under
043 * {@code '/error'} using the {@link HttpStatus status code} and the
044 * {@link HttpStatus#series() status series}.
045 * <p>
046 * For example, an {@code HTTP 404} will search (in the specific order):
047 * <ul>
048 * <li>{@code '/<templates>/error/404.<ext>'}</li>
049 * <li>{@code '/<static>/error/404.html'}</li>
050 * <li>{@code '/<templates>/error/4xx.<ext>'}</li>
051 * <li>{@code '/<static>/error/4xx.html'}</li>
052 * </ul>
053 *
054 * @author Phillip Webb
055 * @author Andy Wilkinson
056 * @since 1.4.0
057 */
058public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
059
060        private static final Map<Series, String> SERIES_VIEWS;
061
062        static {
063                Map<Series, String> views = new EnumMap<>(Series.class);
064                views.put(Series.CLIENT_ERROR, "4xx");
065                views.put(Series.SERVER_ERROR, "5xx");
066                SERIES_VIEWS = Collections.unmodifiableMap(views);
067        }
068
069        private ApplicationContext applicationContext;
070
071        private final ResourceProperties resourceProperties;
072
073        private final TemplateAvailabilityProviders templateAvailabilityProviders;
074
075        private int order = Ordered.LOWEST_PRECEDENCE;
076
077        /**
078         * Create a new {@link DefaultErrorViewResolver} instance.
079         * @param applicationContext the source application context
080         * @param resourceProperties resource properties
081         */
082        public DefaultErrorViewResolver(ApplicationContext applicationContext,
083                        ResourceProperties resourceProperties) {
084                Assert.notNull(applicationContext, "ApplicationContext must not be null");
085                Assert.notNull(resourceProperties, "ResourceProperties must not be null");
086                this.applicationContext = applicationContext;
087                this.resourceProperties = resourceProperties;
088                this.templateAvailabilityProviders = new TemplateAvailabilityProviders(
089                                applicationContext);
090        }
091
092        DefaultErrorViewResolver(ApplicationContext applicationContext,
093                        ResourceProperties resourceProperties,
094                        TemplateAvailabilityProviders templateAvailabilityProviders) {
095                Assert.notNull(applicationContext, "ApplicationContext must not be null");
096                Assert.notNull(resourceProperties, "ResourceProperties must not be null");
097                this.applicationContext = applicationContext;
098                this.resourceProperties = resourceProperties;
099                this.templateAvailabilityProviders = templateAvailabilityProviders;
100        }
101
102        @Override
103        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
104                        Map<String, Object> model) {
105                ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
106                if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
107                        modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
108                }
109                return modelAndView;
110        }
111
112        private ModelAndView resolve(String viewName, Map<String, Object> model) {
113                String errorViewName = "error/" + viewName;
114                TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
115                                .getProvider(errorViewName, this.applicationContext);
116                if (provider != null) {
117                        return new ModelAndView(errorViewName, model);
118                }
119                return resolveResource(errorViewName, model);
120        }
121
122        private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
123                for (String location : this.resourceProperties.getStaticLocations()) {
124                        try {
125                                Resource resource = this.applicationContext.getResource(location);
126                                resource = resource.createRelative(viewName + ".html");
127                                if (resource.exists()) {
128                                        return new ModelAndView(new HtmlResourceView(resource), model);
129                                }
130                        }
131                        catch (Exception ex) {
132                        }
133                }
134                return null;
135        }
136
137        @Override
138        public int getOrder() {
139                return this.order;
140        }
141
142        public void setOrder(int order) {
143                this.order = order;
144        }
145
146        /**
147         * {@link View} backed by an HTML resource.
148         */
149        private static class HtmlResourceView implements View {
150
151                private Resource resource;
152
153                HtmlResourceView(Resource resource) {
154                        this.resource = resource;
155                }
156
157                @Override
158                public String getContentType() {
159                        return MediaType.TEXT_HTML_VALUE;
160                }
161
162                @Override
163                public void render(Map<String, ?> model, HttpServletRequest request,
164                                HttpServletResponse response) throws Exception {
165                        response.setContentType(getContentType());
166                        FileCopyUtils.copy(this.resource.getInputStream(),
167                                        response.getOutputStream());
168                }
169
170        }
171
172}