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.web.servlet.error;
018
019import java.io.PrintWriter;
020import java.io.StringWriter;
021import java.util.Date;
022import java.util.LinkedHashMap;
023import java.util.Map;
024
025import javax.servlet.ServletException;
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028
029import org.springframework.core.Ordered;
030import org.springframework.core.annotation.Order;
031import org.springframework.http.HttpStatus;
032import org.springframework.util.StringUtils;
033import org.springframework.validation.BindingResult;
034import org.springframework.validation.ObjectError;
035import org.springframework.web.bind.MethodArgumentNotValidException;
036import org.springframework.web.context.request.RequestAttributes;
037import org.springframework.web.context.request.WebRequest;
038import org.springframework.web.servlet.HandlerExceptionResolver;
039import org.springframework.web.servlet.ModelAndView;
040
041/**
042 * Default implementation of {@link ErrorAttributes}. Provides the following attributes
043 * when possible:
044 * <ul>
045 * <li>timestamp - The time that the errors were extracted</li>
046 * <li>status - The status code</li>
047 * <li>error - The error reason</li>
048 * <li>exception - The class name of the root exception (if configured)</li>
049 * <li>message - The exception message</li>
050 * <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception
051 * <li>trace - The exception stack trace</li>
052 * <li>path - The URL path when the exception was raised</li>
053 * </ul>
054 *
055 * @author Phillip Webb
056 * @author Dave Syer
057 * @author Stephane Nicoll
058 * @author Vedran Pavic
059 * @since 2.0.0
060 * @see ErrorAttributes
061 */
062@Order(Ordered.HIGHEST_PRECEDENCE)
063public class DefaultErrorAttributes
064                implements ErrorAttributes, HandlerExceptionResolver, Ordered {
065
066        private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName()
067                        + ".ERROR";
068
069        private final boolean includeException;
070
071        /**
072         * Create a new {@link DefaultErrorAttributes} instance that does not include the
073         * "exception" attribute.
074         */
075        public DefaultErrorAttributes() {
076                this(false);
077        }
078
079        /**
080         * Create a new {@link DefaultErrorAttributes} instance.
081         * @param includeException whether to include the "exception" attribute
082         */
083        public DefaultErrorAttributes(boolean includeException) {
084                this.includeException = includeException;
085        }
086
087        @Override
088        public int getOrder() {
089                return Ordered.HIGHEST_PRECEDENCE;
090        }
091
092        @Override
093        public ModelAndView resolveException(HttpServletRequest request,
094                        HttpServletResponse response, Object handler, Exception ex) {
095                storeErrorAttributes(request, ex);
096                return null;
097        }
098
099        private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
100                request.setAttribute(ERROR_ATTRIBUTE, ex);
101        }
102
103        @Override
104        public Map<String, Object> getErrorAttributes(WebRequest webRequest,
105                        boolean includeStackTrace) {
106                Map<String, Object> errorAttributes = new LinkedHashMap<>();
107                errorAttributes.put("timestamp", new Date());
108                addStatus(errorAttributes, webRequest);
109                addErrorDetails(errorAttributes, webRequest, includeStackTrace);
110                addPath(errorAttributes, webRequest);
111                return errorAttributes;
112        }
113
114        private void addStatus(Map<String, Object> errorAttributes,
115                        RequestAttributes requestAttributes) {
116                Integer status = getAttribute(requestAttributes,
117                                "javax.servlet.error.status_code");
118                if (status == null) {
119                        errorAttributes.put("status", 999);
120                        errorAttributes.put("error", "None");
121                        return;
122                }
123                errorAttributes.put("status", status);
124                try {
125                        errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
126                }
127                catch (Exception ex) {
128                        // Unable to obtain a reason
129                        errorAttributes.put("error", "Http Status " + status);
130                }
131        }
132
133        private void addErrorDetails(Map<String, Object> errorAttributes,
134                        WebRequest webRequest, boolean includeStackTrace) {
135                Throwable error = getError(webRequest);
136                if (error != null) {
137                        while (error instanceof ServletException && error.getCause() != null) {
138                                error = ((ServletException) error).getCause();
139                        }
140                        if (this.includeException) {
141                                errorAttributes.put("exception", error.getClass().getName());
142                        }
143                        addErrorMessage(errorAttributes, error);
144                        if (includeStackTrace) {
145                                addStackTrace(errorAttributes, error);
146                        }
147                }
148                Object message = getAttribute(webRequest, "javax.servlet.error.message");
149                if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
150                                && !(error instanceof BindingResult)) {
151                        errorAttributes.put("message",
152                                        StringUtils.isEmpty(message) ? "No message available" : message);
153                }
154        }
155
156        private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
157                BindingResult result = extractBindingResult(error);
158                if (result == null) {
159                        errorAttributes.put("message", error.getMessage());
160                        return;
161                }
162                if (result.hasErrors()) {
163                        errorAttributes.put("errors", result.getAllErrors());
164                        errorAttributes.put("message",
165                                        "Validation failed for object='" + result.getObjectName()
166                                                        + "'. Error count: " + result.getErrorCount());
167                }
168                else {
169                        errorAttributes.put("message", "No errors");
170                }
171        }
172
173        private BindingResult extractBindingResult(Throwable error) {
174                if (error instanceof BindingResult) {
175                        return (BindingResult) error;
176                }
177                if (error instanceof MethodArgumentNotValidException) {
178                        return ((MethodArgumentNotValidException) error).getBindingResult();
179                }
180                return null;
181        }
182
183        private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
184                StringWriter stackTrace = new StringWriter();
185                error.printStackTrace(new PrintWriter(stackTrace));
186                stackTrace.flush();
187                errorAttributes.put("trace", stackTrace.toString());
188        }
189
190        private void addPath(Map<String, Object> errorAttributes,
191                        RequestAttributes requestAttributes) {
192                String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
193                if (path != null) {
194                        errorAttributes.put("path", path);
195                }
196        }
197
198        @Override
199        public Throwable getError(WebRequest webRequest) {
200                Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
201                if (exception == null) {
202                        exception = getAttribute(webRequest, "javax.servlet.error.exception");
203                }
204                return exception;
205        }
206
207        @SuppressWarnings("unchecked")
208        private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
209                return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
210        }
211
212}