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