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