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