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