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