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'>""); 208 } else { 209 theTarget.append("<span class='hlTagName'>""); 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'>""); 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'>></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'>""); 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'><</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}