001package ca.uhn.fhir.rest.server.interceptor;
002
003import ca.uhn.fhir.context.FhirVersionEnum;
004import ca.uhn.fhir.interceptor.api.Hook;
005import ca.uhn.fhir.interceptor.api.Interceptor;
006import ca.uhn.fhir.interceptor.api.Pointcut;
007import ca.uhn.fhir.parser.IParser;
008import ca.uhn.fhir.rest.api.Constants;
009import ca.uhn.fhir.rest.api.EncodingEnum;
010import ca.uhn.fhir.rest.api.RequestTypeEnum;
011import ca.uhn.fhir.rest.api.server.RequestDetails;
012import ca.uhn.fhir.rest.api.server.ResponseDetails;
013import ca.uhn.fhir.rest.server.RestfulServer;
014import ca.uhn.fhir.rest.server.RestfulServerUtils;
015import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
016import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
017import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
018import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
019import ca.uhn.fhir.util.StopWatch;
020import ca.uhn.fhir.util.UrlUtil;
021import org.apache.commons.io.FileUtils;
022import org.apache.commons.io.IOUtils;
023import org.apache.commons.text.StringEscapeUtils;
024import org.hl7.fhir.instance.model.api.IBaseBinary;
025import org.hl7.fhir.instance.model.api.IBaseResource;
026
027import javax.servlet.ServletException;
028import javax.servlet.ServletRequest;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031import java.io.IOException;
032import java.io.InputStream;
033import java.util.Date;
034import java.util.Enumeration;
035import java.util.Map;
036import java.util.Set;
037
038import static org.apache.commons.lang3.StringUtils.*;
039
040/*
041 * #%L
042 * HAPI FHIR - Server Framework
043 * %%
044 * Copyright (C) 2014 - 2019 University Health Network
045 * %%
046 * Licensed under the Apache License, Version 2.0 (the "License");
047 * you may not use this file except in compliance with the License.
048 * You may obtain a copy of the License at
049 * 
050 * http://www.apache.org/licenses/LICENSE-2.0
051 * 
052 * Unless required by applicable law or agreed to in writing, software
053 * distributed under the License is distributed on an "AS IS" BASIS,
054 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
055 * See the License for the specific language governing permissions and
056 * limitations under the License.
057 * #L%
058 */
059
060/**
061 * This interceptor detects when a request is coming from a browser, and automatically returns a response with syntax
062 * highlighted (coloured) HTML for the response instead of just returning raw XML/JSON.
063 *
064 * @since 1.0
065 */
066@Interceptor
067public class ResponseHighlighterInterceptor {
068
069        /**
070         * TODO: As of HAPI 1.6 (2016-06-10) this parameter has been replaced with simply
071         * requesting _format=json or xml so eventually this parameter should be removed
072         */
073        public static final String PARAM_RAW = "_raw";
074        public static final String PARAM_RAW_TRUE = "true";
075        public static final String PARAM_TRUE = "true";
076        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class);
077        private static final String[] PARAM_FORMAT_VALUE_JSON = new String[]{Constants.FORMAT_JSON};
078        private static final String[] PARAM_FORMAT_VALUE_XML = new String[]{Constants.FORMAT_XML};
079        private boolean myShowRequestHeaders = false;
080        private boolean myShowResponseHeaders = true;
081
082        /**
083         * Constructor
084         */
085        public ResponseHighlighterInterceptor() {
086                super();
087        }
088
089        private String createLinkHref(Map<String, String[]> parameters, String formatValue) {
090                StringBuilder rawB = new StringBuilder();
091                for (String next : parameters.keySet()) {
092                        if (Constants.PARAM_FORMAT.equals(next)) {
093                                continue;
094                        }
095                        for (String nextValue : parameters.get(next)) {
096                                if (isBlank(nextValue)) {
097                                        continue;
098                                }
099                                if (rawB.length() == 0) {
100                                        rawB.append('?');
101                                } else {
102                                        rawB.append('&');
103                                }
104                                rawB.append(UrlUtil.escapeUrlParam(next));
105                                rawB.append('=');
106                                rawB.append(UrlUtil.escapeUrlParam(nextValue));
107                        }
108                }
109                if (rawB.length() == 0) {
110                        rawB.append('?');
111                } else {
112                        rawB.append('&');
113                }
114                rawB.append(Constants.PARAM_FORMAT).append('=').append(formatValue);
115
116                String link = rawB.toString();
117                return link;
118        }
119
120        private int format(String theResultBody, StringBuilder theTarget, EncodingEnum theEncodingEnum) {
121                String str = StringEscapeUtils.escapeHtml4(theResultBody);
122                if (str == null || theEncodingEnum == null) {
123                        theTarget.append(str);
124                        return 0;
125                }
126
127                theTarget.append("<div id=\"line1\">");
128
129                boolean inValue = false;
130                boolean inQuote = false;
131                boolean inTag = false;
132                int lineCount = 1;
133
134                for (int i = 0; i < str.length(); i++) {
135                        char prevChar = (i > 0) ? str.charAt(i - 1) : ' ';
136                        char nextChar = str.charAt(i);
137                        char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' ';
138                        char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' ';
139                        char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' ';
140                        char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' ';
141                        char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
142
143                        if (nextChar == '\n') {
144                                lineCount++;
145                                theTarget.append("</div><div id=\"line");
146                                theTarget.append(lineCount);
147                                theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
148                                theTarget.append(lineCount);
149                                theTarget.append("');\">");
150                                continue;
151                        }
152
153                        if (theEncodingEnum == EncodingEnum.JSON) {
154
155                                if (inQuote) {
156                                        theTarget.append(nextChar);
157                                        if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
158                                                theTarget.append("quot;</span>");
159                                                i += 5;
160                                                inQuote = false;
161                                        } else if (nextChar == '\\' && nextChar2 == '"') {
162                                                theTarget.append("quot;</span>");
163                                                i += 5;
164                                                inQuote = false;
165                                        }
166                                } else {
167                                        if (nextChar == ':') {
168                                                inValue = true;
169                                                theTarget.append(nextChar);
170                                        } else if (nextChar == '[' || nextChar == '{') {
171                                                theTarget.append("<span class='hlControl'>");
172                                                theTarget.append(nextChar);
173                                                theTarget.append("</span>");
174                                                inValue = false;
175                                        } else if (nextChar == '{' || nextChar == '}' || nextChar == ',') {
176                                                theTarget.append("<span class='hlControl'>");
177                                                theTarget.append(nextChar);
178                                                theTarget.append("</span>");
179                                                inValue = false;
180                                        } else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
181                                                if (inValue) {
182                                                        theTarget.append("<span class='hlQuot'>&quot;");
183                                                } else {
184                                                        theTarget.append("<span class='hlTagName'>&quot;");
185                                                }
186                                                inQuote = true;
187                                                i += 5;
188                                        } else if (nextChar == ':') {
189                                                theTarget.append("<span class='hlControl'>");
190                                                theTarget.append(nextChar);
191                                                theTarget.append("</span>");
192                                                inValue = true;
193                                        } else {
194                                                theTarget.append(nextChar);
195                                        }
196                                }
197
198                        } else {
199                                if (inQuote) {
200                                        theTarget.append(nextChar);
201                                        if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
202                                                theTarget.append("quot;</span>");
203                                                i += 5;
204                                                inQuote = false;
205                                        }
206                                } else if (inTag) {
207                                        if (nextChar == '&' && nextChar2 == 'g' && nextChar3 == 't' && nextChar4 == ';') {
208                                                theTarget.append("</span><span class='hlControl'>&gt;</span>");
209                                                inTag = false;
210                                                i += 3;
211                                        } else if (nextChar == ' ') {
212                                                theTarget.append("</span><span class='hlAttr'>");
213                                                theTarget.append(nextChar);
214                                        } else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
215                                                theTarget.append("<span class='hlQuot'>&quot;");
216                                                inQuote = true;
217                                                i += 5;
218                                        } else {
219                                                theTarget.append(nextChar);
220                                        }
221                                } else {
222                                        if (nextChar == '&' && nextChar2 == 'l' && nextChar3 == 't' && nextChar4 == ';') {
223                                                theTarget.append("<span class='hlControl'>&lt;</span><span class='hlTagName'>");
224                                                inTag = true;
225                                                i += 3;
226                                        } else {
227                                                theTarget.append(nextChar);
228                                        }
229                                }
230                        }
231                }
232
233                theTarget.append("</div>");
234                return lineCount;
235        }
236
237        @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION, order = InterceptorOrders.RESPONSE_HIGHLIGHTER_INTERCEPTOR)
238        public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
239                throws ServletException, IOException {
240                /*
241                 * It's not a browser...
242                 */
243                Set<String> accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest);
244                if (!accept.contains(Constants.CT_HTML)) {
245                        return true;
246                }
247
248                /*
249                 * It's an AJAX request, so no HTML
250                 */
251                String requestedWith = theServletRequest.getHeader("X-Requested-With");
252                if (requestedWith != null) {
253                        return true;
254                }
255
256                /*
257                 * Not a GET
258                 */
259                if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) {
260                        return true;
261                }
262
263                if (theException.getOperationOutcome() == null) {
264                        return true;
265                }
266
267                streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome(), theServletRequest, theException.getStatusCode());
268
269                return false;
270        }
271
272        /**
273         * If set to <code>true</code> (default is <code>false</code>) response will include the
274         * request headers
275         */
276        public boolean isShowRequestHeaders() {
277                return myShowRequestHeaders;
278        }
279
280        /**
281         * If set to <code>true</code> (default is <code>false</code>) response will include the
282         * request headers
283         *
284         * @return Returns a reference to this for easy method chaining
285         */
286        @SuppressWarnings("UnusedReturnValue")
287        public ResponseHighlighterInterceptor setShowRequestHeaders(boolean theShowRequestHeaders) {
288                myShowRequestHeaders = theShowRequestHeaders;
289                return this;
290        }
291
292        /**
293         * If set to <code>true</code> (default is <code>true</code>) response will include the
294         * response headers
295         */
296        public boolean isShowResponseHeaders() {
297                return myShowResponseHeaders;
298        }
299
300        /**
301         * If set to <code>true</code> (default is <code>true</code>) response will include the
302         * response headers
303         *
304         * @return Returns a reference to this for easy method chaining
305         */
306        @SuppressWarnings("UnusedReturnValue")
307        public ResponseHighlighterInterceptor setShowResponseHeaders(boolean theShowResponseHeaders) {
308                myShowResponseHeaders = theShowResponseHeaders;
309                return this;
310        }
311
312        @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE, order = InterceptorOrders.RESPONSE_HIGHLIGHTER_INTERCEPTOR)
313        public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
314                throws AuthenticationException {
315
316                /*
317                 * Request for _raw
318                 */
319                String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW);
320                if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) {
321                        ourLog.warn("Client is using non-standard/legacy  _raw parameter - Use _format=json or _format=xml instead, as this parmameter will be removed at some point");
322                        return true;
323                }
324
325                boolean force = false;
326                String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT);
327                if (formatParams != null && formatParams.length > 0) {
328                        String formatParam = defaultString(formatParams[0]);
329                        int semiColonIdx = formatParam.indexOf(';');
330                        if (semiColonIdx != -1) {
331                                formatParam = formatParam.substring(0, semiColonIdx);
332                        }
333                        formatParam = trim(formatParam);
334
335                        if (Constants.FORMATS_HTML.contains(formatParam)) { // this is a set
336                                force = true;
337                        } else if (Constants.FORMATS_HTML_XML.equals(formatParam)) {
338                                force = true;
339                                theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_XML);
340                        } else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) {
341                                force = true;
342                                theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON);
343                        } else {
344                                return true;
345                        }
346                }
347
348                /*
349                 * It's not a browser...
350                 */
351                Set<String> highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest);
352                if (!force && highestRankedAcceptValues.contains(Constants.CT_HTML) == false) {
353                        return true;
354                }
355
356                /*
357                 * It's an AJAX request, so no HTML
358                 */
359                if (!force && isNotBlank(theServletRequest.getHeader("X-Requested-With"))) {
360                        return true;
361                }
362                /*
363                 * If the request has an Origin header, it is probably an AJAX request
364                 */
365                if (!force && isNotBlank(theServletRequest.getHeader(Constants.HEADER_ORIGIN))) {
366                        return true;
367                }
368
369                /*
370                 * Not a GET
371                 */
372                if (!force && theRequestDetails.getRequestType() != RequestTypeEnum.GET) {
373                        return true;
374                }
375
376                /*
377                 * Not binary
378                 */
379                if (!force && (theResponseObject.getResponseResource() instanceof IBaseBinary)) {
380                        return true;
381                }
382
383                streamResponse(theRequestDetails, theServletResponse, theResponseObject.getResponseResource(), theServletRequest, 200);
384
385                return false;
386        }
387
388        private void streamRequestHeaders(ServletRequest theServletRequest, StringBuilder b) {
389                if (theServletRequest instanceof HttpServletRequest) {
390                        HttpServletRequest sr = (HttpServletRequest) theServletRequest;
391                        b.append("<h1>Request</h1>");
392                        b.append("<div class=\"headersDiv\">");
393                        Enumeration<String> headerNamesEnum = sr.getHeaderNames();
394                        while (headerNamesEnum.hasMoreElements()) {
395                                String nextHeaderName = headerNamesEnum.nextElement();
396                                Enumeration<String> headerValuesEnum = sr.getHeaders(nextHeaderName);
397                                while (headerValuesEnum.hasMoreElements()) {
398                                        String nextHeaderValue = headerValuesEnum.nextElement();
399                                        b.append("<div class=\"headersRow\">");
400                                        b.append("<span class=\"headerName\">").append(nextHeaderName).append(": ").append("</span>");
401                                        b.append("<span class=\"headerValue\">").append(nextHeaderValue).append("</span>");
402                                        b.append("</div>");
403                                }
404                        }
405                        b.append("</div>");
406                }
407        }
408
409        private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource theResource, ServletRequest theServletRequest, int theStatusCode) {
410
411                if (theRequestDetails.getServer() instanceof RestfulServer) {
412                        RestfulServer rs = (RestfulServer) theRequestDetails.getServer();
413                        rs.addHeadersToResponse(theServletResponse);
414                }
415
416                IParser p;
417                Map<String, String[]> parameters = theRequestDetails.getParameters();
418                if (parameters.containsKey(Constants.PARAM_FORMAT)) {
419                        FhirVersionEnum forVersion = theResource.getStructureFhirVersionEnum();
420                        p = RestfulServerUtils.getNewParser(theRequestDetails.getServer().getFhirContext(), forVersion, theRequestDetails);
421                } else {
422                        EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding();
423                        p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext());
424                        RestfulServerUtils.configureResponseParser(theRequestDetails, p);
425                }
426
427                // This interceptor defaults to pretty printing unless the user
428                // has specifically requested us not to
429                boolean prettyPrintResponse = true;
430                String[] prettyParams = parameters.get(Constants.PARAM_PRETTY);
431                if (prettyParams != null && prettyParams.length > 0) {
432                        if (Constants.PARAM_PRETTY_VALUE_FALSE.equals(prettyParams[0])) {
433                                prettyPrintResponse = false;
434                        }
435                }
436                if (prettyPrintResponse) {
437                        p.setPrettyPrint(true);
438                }
439
440                EncodingEnum encoding = p.getEncoding();
441                String encoded = p.encodeResourceToString(theResource);
442
443                try {
444
445                        if (theStatusCode > 299) {
446                                theServletResponse.setStatus(theStatusCode);
447                        }
448                        theServletResponse.setContentType(Constants.CT_HTML_WITH_UTF8);
449
450                        StringBuilder outputBuffer = new StringBuilder();
451                        outputBuffer.append("<html lang=\"en\">\n");
452                        outputBuffer.append("   <head>\n");
453                        outputBuffer.append("           <meta charset=\"utf-8\" />\n");
454                        outputBuffer.append("       <style>\n");
455                        outputBuffer.append(".httpStatusDiv {");
456                        outputBuffer.append("  font-size: 1.2em;");
457                        outputBuffer.append("  font-weight: bold;");
458                        outputBuffer.append("}");
459                        outputBuffer.append(".hlQuot { color: #88F; }\n");
460                        outputBuffer.append(".hlQuot a { text-decoration: underline; text-decoration-color: #CCC; }\n");
461                        outputBuffer.append(".hlQuot a:HOVER { text-decoration: underline; text-decoration-color: #008; }\n");
462                        outputBuffer.append(".hlQuot .uuid, .hlQuot .dateTime {\n");
463                        outputBuffer.append("  user-select: all;\n");
464                        outputBuffer.append("  -moz-user-select: all;\n");
465                        outputBuffer.append("  -webkit-user-select: all;\n");
466                        outputBuffer.append("  -ms-user-select: element;\n");
467                        outputBuffer.append("}\n");
468                        outputBuffer.append(".hlAttr {\n");
469                        outputBuffer.append("  color: #888;\n");
470                        outputBuffer.append("}\n");
471                        outputBuffer.append(".hlTagName {\n");
472                        outputBuffer.append("  color: #006699;\n");
473                        outputBuffer.append("}\n");
474                        outputBuffer.append(".hlControl {\n");
475                        outputBuffer.append("  color: #660000;\n");
476                        outputBuffer.append("}\n");
477                        outputBuffer.append(".hlText {\n");
478                        outputBuffer.append("  color: #000000;\n");
479                        outputBuffer.append("}\n");
480                        outputBuffer.append(".hlUrlBase {\n");
481                        outputBuffer.append("}");
482                        outputBuffer.append(".headersDiv {\n");
483                        outputBuffer.append("  padding: 10px;");
484                        outputBuffer.append("  margin-left: 10px;");
485                        outputBuffer.append("  border: 1px solid #CCC;");
486                        outputBuffer.append("  border-radius: 10px;");
487                        outputBuffer.append("}");
488                        outputBuffer.append(".headersRow {\n");
489                        outputBuffer.append("}");
490                        outputBuffer.append(".headerName {\n");
491                        outputBuffer.append("  color: #888;\n");
492                        outputBuffer.append("  font-family: monospace;\n");
493                        outputBuffer.append("}");
494                        outputBuffer.append(".headerValue {\n");
495                        outputBuffer.append("  color: #88F;\n");
496                        outputBuffer.append("  font-family: monospace;\n");
497                        outputBuffer.append("}");
498                        outputBuffer.append(".responseBodyTable {");
499                        outputBuffer.append("  width: 100%;\n");
500                        outputBuffer.append("  margin-left: 0px;\n");
501                        outputBuffer.append("  margin-top: -10px;\n");
502                        outputBuffer.append("  position: relative;\n");
503                        outputBuffer.append("}");
504                        outputBuffer.append(".responseBodyTableFirstColumn {");
505//                      outputBuffer.append("  position: absolute;\n");
506//                      outputBuffer.append("  width: 70px;\n");
507                        outputBuffer.append("}");
508                        outputBuffer.append(".responseBodyTableSecondColumn {");
509                        outputBuffer.append("  position: absolute;\n");
510                        outputBuffer.append("  margin-left: 70px;\n");
511                        outputBuffer.append("  vertical-align: top;\n");
512                        outputBuffer.append("  left: 0px;\n");
513                        outputBuffer.append("  right: 0px;\n");
514                        outputBuffer.append("}");
515                        outputBuffer.append(".responseBodyTableSecondColumn PRE {");
516                        outputBuffer.append("  margin: 0px;");
517                        outputBuffer.append("}");
518                        outputBuffer.append(".sizeInfo {");
519                        outputBuffer.append("  margin-top: 20px;");
520                        outputBuffer.append("  font-size: 0.8em;");
521                        outputBuffer.append("}");
522                        outputBuffer.append(".lineAnchor A {");
523                        outputBuffer.append("  text-decoration: none;");
524                        outputBuffer.append("  padding-left: 20px;");
525                        outputBuffer.append("}");
526                        outputBuffer.append(".lineAnchor {");
527                        outputBuffer.append("  display: block;");
528                        outputBuffer.append("  padding-right: 20px;");
529                        outputBuffer.append("}");
530                        outputBuffer.append(".selectedLine {");
531                        outputBuffer.append("  background-color: #EEF;");
532                        outputBuffer.append("  font-weight: bold;");
533                        outputBuffer.append("}");
534                        outputBuffer.append("H1 {");
535                        outputBuffer.append("  font-size: 1.1em;");
536                        outputBuffer.append("  color: #666;");
537                        outputBuffer.append("}");
538                        outputBuffer.append("BODY {\n");
539                        outputBuffer.append("  font-family: Arial;\n");
540                        outputBuffer.append("}");
541                        outputBuffer.append("       </style>\n");
542                        outputBuffer.append("   </head>\n");
543                        outputBuffer.append("\n");
544                        outputBuffer.append("   <body>");
545
546                        outputBuffer.append("<p>");
547                        outputBuffer.append("This result is being rendered in HTML for easy viewing. ");
548                        outputBuffer.append("You may access this content as ");
549
550                        outputBuffer.append("<a href=\"");
551                        outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
552                        outputBuffer.append("\">Raw JSON</a> or ");
553
554                        outputBuffer.append("<a href=\"");
555                        outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
556                        outputBuffer.append("\">Raw XML</a>, ");
557
558                        outputBuffer.append(" or view this content in ");
559
560                        outputBuffer.append("<a href=\"");
561                        outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
562                        outputBuffer.append("\">HTML JSON</a> ");
563
564                        outputBuffer.append("or ");
565                        outputBuffer.append("<a href=\"");
566                        outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
567                        outputBuffer.append("\">HTML XML</a>.");
568
569                        Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
570                        if (startTime != null) {
571                                long time = System.currentTimeMillis() - startTime.getTime();
572                                outputBuffer.append(" Response generated in ");
573                                outputBuffer.append(time);
574                                outputBuffer.append("ms.");
575                        }
576
577                        outputBuffer.append("</p>");
578
579                        outputBuffer.append("\n");
580
581                        // status (e.g. HTTP 200 OK)
582                        String statusName = Constants.HTTP_STATUS_NAMES.get(theServletResponse.getStatus());
583                        statusName = defaultString(statusName);
584                        outputBuffer.append("<div class=\"httpStatusDiv\">");
585                        outputBuffer.append("HTTP ");
586                        outputBuffer.append(theServletResponse.getStatus());
587                        outputBuffer.append(" ");
588                        outputBuffer.append(statusName);
589                        outputBuffer.append("</div>");
590
591                        outputBuffer.append("\n");
592                        outputBuffer.append("\n");
593
594                        try {
595                                if (isShowRequestHeaders()) {
596                                        streamRequestHeaders(theServletRequest, outputBuffer);
597                                }
598                                if (isShowResponseHeaders()) {
599                                        streamResponseHeaders(theRequestDetails, theServletResponse, outputBuffer);
600                                }
601                        } catch (Throwable t) {
602                                // ignore (this will hit if we're running in a servlet 2.5 environment)
603                        }
604
605                        outputBuffer.append("<h1>Response Body</h1>");
606
607                        outputBuffer.append("<div class=\"responseBodyTable\">");
608
609                        // Response Body
610                        outputBuffer.append("<div class=\"responseBodyTableSecondColumn\"><pre>");
611                        StringBuilder target = new StringBuilder();
612                        int linesCount = format(encoded, target, encoding);
613                        outputBuffer.append(target);
614                        outputBuffer.append("</pre></div>");
615
616                        // Line Numbers
617                        outputBuffer.append("<div class=\"responseBodyTableFirstColumn\"><pre>");
618                        for (int i = 1; i <= linesCount; i++) {
619                                outputBuffer.append("<div class=\"lineAnchor\" id=\"anchor");
620                                outputBuffer.append(i);
621                                outputBuffer.append("\">");
622
623                                outputBuffer.append("<a href=\"#L");
624                                outputBuffer.append(i);
625                                outputBuffer.append("\" name=\"L");
626                                outputBuffer.append(i);
627                                outputBuffer.append("\" id=\"L");
628                                outputBuffer.append(i);
629                                outputBuffer.append("\">");
630                                outputBuffer.append(i);
631                                outputBuffer.append("</a></div>");
632                        }
633                        outputBuffer.append("</div></td>");
634
635                        outputBuffer.append("</div>");
636
637                        outputBuffer.append("\n");
638
639                        InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
640                        String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
641                        jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
642                        outputBuffer.append("<script type=\"text/javascript\">");
643                        outputBuffer.append(jsStr);
644                        outputBuffer.append("</script>\n");
645
646                        StopWatch writeSw = new StopWatch();
647                        theServletResponse.getWriter().append(outputBuffer);
648                        theServletResponse.getWriter().flush();
649
650                        theServletResponse.getWriter().append("<div class=\"sizeInfo\">");
651                        theServletResponse.getWriter().append("Wrote ");
652                        writeLength(theServletResponse, encoded.length());
653                        theServletResponse.getWriter().append(" (");
654                        writeLength(theServletResponse, outputBuffer.length());
655                        theServletResponse.getWriter().append(" total including HTML)");
656
657                        theServletResponse.getWriter().append(" in estimated ");
658                        theServletResponse.getWriter().append(writeSw.toString());
659                        theServletResponse.getWriter().append("</div>");
660
661
662                        theServletResponse.getWriter().append("</body>");
663                        theServletResponse.getWriter().append("</html>");
664
665                        theServletResponse.getWriter().close();
666                } catch (IOException e) {
667                        throw new InternalErrorException(e);
668                }
669        }
670
671        private void writeLength(HttpServletResponse theServletResponse, int theLength) throws IOException {
672                double kb = ((double) theLength) / FileUtils.ONE_KB;
673                if (kb <= 1000) {
674                        theServletResponse.getWriter().append(String.format("%.1f", kb)).append(" KB");
675                } else {
676                        double mb = kb / 1000;
677                        theServletResponse.getWriter().append(String.format("%.1f", mb)).append(" MB");
678                }
679        }
680
681        private void streamResponseHeaders(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, StringBuilder b) {
682                if (theServletResponse.getHeaderNames().isEmpty() == false) {
683                        b.append("<h1>Response Headers</h1>");
684
685                        b.append("<div class=\"headersDiv\">");
686                        for (String nextHeaderName : theServletResponse.getHeaderNames()) {
687                                for (String nextHeaderValue : theServletResponse.getHeaders(nextHeaderName)) {
688                                        /*
689                                         * Let's pretend we're returning a FHIR content type even though we're
690                                         * actually returning an HTML one
691                                         */
692                                        if (nextHeaderName.equalsIgnoreCase(Constants.HEADER_CONTENT_TYPE)) {
693                                                ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theRequestDetails.getServer().getDefaultResponseEncoding());
694                                                if (responseEncoding != null && isNotBlank(responseEncoding.getResourceContentType())) {
695                                                        nextHeaderValue = responseEncoding.getResourceContentType() + ";charset=utf-8";
696                                                }
697                                        }
698                                        b.append("<div class=\"headersRow\">");
699                                        b.append("<span class=\"headerName\">").append(nextHeaderName).append(": ").append("</span>");
700                                        b.append("<span class=\"headerValue\">").append(nextHeaderValue).append("</span>");
701                                        b.append("</div>");
702                                }
703                        }
704                        b.append("</div>");
705                }
706        }
707
708}