001/*
002 * Copyright 2002-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 *      https://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.test.web.servlet.result;
018
019import java.util.Collections;
020import java.util.Enumeration;
021import java.util.Map;
022import java.util.stream.Collectors;
023
024import javax.servlet.http.Cookie;
025import javax.servlet.http.HttpServletRequest;
026import javax.servlet.http.HttpSession;
027
028import org.springframework.core.style.ToStringCreator;
029import org.springframework.http.HttpHeaders;
030import org.springframework.lang.Nullable;
031import org.springframework.mock.web.MockHttpServletRequest;
032import org.springframework.mock.web.MockHttpServletResponse;
033import org.springframework.test.web.servlet.MvcResult;
034import org.springframework.test.web.servlet.ResultHandler;
035import org.springframework.util.LinkedMultiValueMap;
036import org.springframework.util.MultiValueMap;
037import org.springframework.util.ObjectUtils;
038import org.springframework.validation.BindingResult;
039import org.springframework.validation.Errors;
040import org.springframework.web.method.HandlerMethod;
041import org.springframework.web.servlet.FlashMap;
042import org.springframework.web.servlet.HandlerInterceptor;
043import org.springframework.web.servlet.ModelAndView;
044import org.springframework.web.servlet.support.RequestContextUtils;
045
046/**
047 * Result handler that prints {@link MvcResult} details to a given output
048 * stream — for example: {@code System.out}, {@code System.err}, a
049 * custom {@code java.io.PrintWriter}, etc.
050 *
051 * <p>An instance of this class is typically accessed via one of the
052 * {@link MockMvcResultHandlers#print print} or {@link MockMvcResultHandlers#log log}
053 * methods in {@link MockMvcResultHandlers}.
054 *
055 * @author Rossen Stoyanchev
056 * @author Sam Brannen
057 * @since 3.2
058 */
059public class PrintingResultHandler implements ResultHandler {
060
061        private static final String MISSING_CHARACTER_ENCODING = "<no character encoding set>";
062
063        private final ResultValuePrinter printer;
064
065
066        /**
067         * Protected constructor.
068         * @param printer a {@link ResultValuePrinter} to do the actual writing
069         */
070        protected PrintingResultHandler(ResultValuePrinter printer) {
071                this.printer = printer;
072        }
073
074        /**
075         * Return the result value printer.
076         span class="sourceLineNo">077         */
078        protected ResultValuePrinter getPrinter() {
079                return this.printer;
080        }
081
082        /**
083         * Print {@link MvcResult} details.
084         */
085        @Override
086        public final void handle(MvcResult result) throws Exception {
087                this.printer.printHeading("MockHttpServletRequest");
088                printRequest(result.getRequest());
089
090                this.printer.printHeading("Handler");
091                printHandler(result.getHandler(), result.getInterceptors());
092
093                this.printer.printHeading("Async");
094                printAsyncResult(result);
095
096                this.printer.printHeading("Resolved Exception");
097                printResolvedException(result.getResolvedException());
098
099                this.printer.printHeading("ModelAndView");
100                printModelAndView(result.getModelAndView());
101
102                this.printer.printHeading("FlashMap");
103                printFlashMap(RequestContextUtils.getOutputFlashMap(result.getRequest()));
104
105                this.printer.printHeading("MockHttpServletResponse");
106                printResponse(result.getResponse());
107        }
108
109        /**
110         * Print the request.
111         */
112        protected void printRequest(MockHttpServletRequest request) throws Exception {
113                String body = (request.getCharacterEncoding() != null ?
114                                request.getContentAsString() : MISSING_CHARACTER_ENCODING);
115
116                this.printer.printValue("HTTP Method", request.getMethod());
117                this.printer.printValue("Request URI", request.getRequestURI());
118                this.printer.printValue("Parameters", getParamsMultiValueMap(request));
119                this.printer.printValue("Headers", getRequestHeaders(request));
120                this.printer.printValue("Body", body);
121                this.printer.printValue("Session Attrs", getSessionAttributes(request));
122        }
123
124        protected final HttpHeaders getRequestHeaders(MockHttpServletRequest request) {
125                HttpHeaders headers = new HttpHeaders();
126                Enumeration<?> names = request.getHeaderNames();
127                while (names.hasMoreElements()) {
128                        String name = (String) names.nextElement();
129                        Enumeration<String> values = request.getHeaders(name);
130                        while (values.hasMoreElements()) {
131                                headers.add(name, values.nextElement());
132                        }
133                }
134                return headers;
135        }
136
137        protected final MultiValueMap<String, String> getParamsMultiValueMap(MockHttpServletRequest request) {
138                Map<String, String[]> params = request.getParameterMap();
139                MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
140                params.forEach((name, values) -> {
141                        if (params.get(name) != null) {
142                                for (String value : values) {
143                                        multiValueMap.add(name, value);
144                                }
145                        }
146                });
147                return multiValueMap;
148        }
149
150        protected final Map<String, Object> getSessionAttributes(MockHttpServletRequest request) {
151                HttpSession session = request.getSession(false);
152                if (session != null) {
153                        Enumeration<String> attrNames = session.getAttributeNames();
154                        if (attrNames != null) {
155                                return Collections.list(attrNames).stream().
156                                                collect(Collectors.toMap(n -> n, session::getAttribute));
157                        }
158                }
159                return Collections.emptyMap();
160        }
161
162        protected void printAsyncResult(MvcResult result) throws Exception {
163                HttpServletRequest request = result.getRequest();
164                this.printer.printValue("Async started", request.isAsyncStarted());
165                Object asyncResult = null;
166                try {
167                        asyncResult = result.getAsyncResult(0);
168                }
169                catch (IllegalStateException ex) {
170                        // Not set
171                }
172                this.printer.printValue("Async result", asyncResult);
173        }
174
175        /**
176         * Print the handler.
177         */
178        protected void printHandler(@Nullable Object handler, @Nullable HandlerInterceptor[] interceptors)
179                        throws Exception {
180
181                if (handler == null) {
182                        this.printer.printValue("Type", null);
183                }
184                else {
185                        if (handler instanceof HandlerMethod) {
186                                HandlerMethod handlerMethod = (HandlerMethod) handler;
187                                this.printer.printValue("Type", handlerMethod.getBeanType().getName());
188                                this.printer.printValue("Method", handlerMethod);
189                        }
190                        else {
191                                this.printer.printValue("Type", handler.getClass().getName());
192                        }
193                }
194        }
195
196        /**
197         * Print exceptions resolved through a HandlerExceptionResolver.
198         */
199        protected void printResolvedException(@Nullable Exception resolvedException) throws Exception {
200                if (resolvedException == null) {
201                        this.printer.printValue("Type", null);
202                }
203                else {
204                        this.printer.printValue("Type", resolvedException.getClass().getName());
205                }
206        }
207
208        /**
209         * Print the ModelAndView.
210         */
211        protected void printModelAndView(@Nullable ModelAndView mav) throws Exception {
212                this.printer.printValue("View name", (mav != null) ? mav.getViewName() : null);
213                this.printer.printValue("View", (mav != null) ? mav.getView() : null);
214                if (mav == null || mav.getModel().size() == 0) {
215                        this.printer.printValue("Model", null);
216                }
217                else {
218                        for (String name : mav.getModel().keySet()) {
219                                if (!name.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
220                                        Object value = mav.getModel().get(name);
221                                        this.printer.printValue("Attribute", name);
222                                        this.printer.printValue("value", value);
223                                        Errors errors = (Errors) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);
224                                        if (errors != null) {
225                                                this.printer.printValue("errors", errors.getAllErrors());
226                                        }
227                                }
228                        }
229                }
230        }
231
232        /**
233         * Print "output" flash attributes.
234         */
235        protected void printFlashMap(FlashMap flashMap) throws Exception {
236                if (ObjectUtils.isEmpty(flashMap)) {
237                        this.printer.printValue("Attributes", null);
238                }
239                else {
240                        flashMap.forEach((name, value) -> {
241                                this.printer.printValue("Attribute", name);
242                                this.printer.printValue("value", value);
243                        });
244                }
245        }
246
247        /**
248         * Print the response.
249         */
250        protected void printResponse(MockHttpServletResponse response) throws Exception {
251                String body = (response.getCharacterEncoding() != null ?
252                                response.getContentAsString() : MISSING_CHARACTER_ENCODING);
253
254                this.printer.printValue("Status", response.getStatus());
255                this.printer.printValue("Error message", response.getErrorMessage());
256                this.printer.printValue("Headers", getResponseHeaders(response));
257                this.printer.printValue("Content type", response.getContentType());
258                this.printer.printValue("Body", body);
259                this.printer.printValue("Forwarded URL", response.getForwardedUrl());
260                this.printer.printValue("Redirected URL", response.getRedirectedUrl());
261                printCookies(response.getCookies());
262        }
263
264        /**
265         * Print the supplied cookies in a human-readable form, assuming the
266         * {@link Cookie} implementation does not provide its own {@code toString()}.
267         * @since 4.2
268         */
269        private void printCookies(Cookie[] cookies) {
270                String[] cookieStrings = new String[cookies.length];
271                for (int i = 0; i < cookies.length; i++) {
272                        Cookie cookie = cookies[i];
273                        cookieStrings[i] = new ToStringCreator(cookie)
274                                .append("name", cookie.getName())
275                                .append("value", cookie.getValue())
276                                .append("comment", cookie.getComment())
277                                .append("domain", cookie.getDomain())
278                                .append("maxAge", cookie.getMaxAge())
279                                .append("path", cookie.getPath())
280                                .append("secure", cookie.getSecure())
281                                .append("version", cookie.getVersion())
282                                .append("httpOnly", cookie.isHttpOnly())
283                                .toString();
284                }
285                this.printer.printValue("Cookies", cookieStrings);
286        }
287
288        protected final HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
289                HttpHeaders headers = new HttpHeaders();
290                for (String name : response.getHeaderNames()) {
291                        headers.put(name, response.getHeaders(name));
292                }
293                return headers;
294        }
295
296
297        /**
298         * A contract for how to actually write result information.
299         */
300        protected interface ResultValuePrinter {
301
302                void printHeading(String heading);
303
304                void printValue(String label, @Nullable Object value);
305        }
306
307}