001package ca.uhn.fhir.rest.server.interceptor; 002 003import static org.apache.commons.lang3.StringUtils.isBlank; 004import static org.apache.commons.lang3.StringUtils.isNotBlank; 005 006/* 007 * #%L 008 * HAPI FHIR - Core Library 009 * %% 010 * Copyright (C) 2014 - 2017 University Health Network 011 * %% 012 * Licensed under the Apache License, Version 2.0 (the "License"); 013 * you may not use this file except in compliance with the License. 014 * You may obtain a copy of the License at 015 * 016 * http://www.apache.org/licenses/LICENSE-2.0 017 * 018 * Unless required by applicable law or agreed to in writing, software 019 * distributed under the License is distributed on an "AS IS" BASIS, 020 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 021 * See the License for the specific language governing permissions and 022 * limitations under the License. 023 * #L% 024 */ 025 026import java.io.IOException; 027import java.util.Date; 028import java.util.Map; 029import java.util.Set; 030 031import javax.servlet.ServletException; 032import javax.servlet.ServletRequest; 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035 036import org.apache.commons.lang3.StringEscapeUtils; 037import org.hl7.fhir.instance.model.api.IBaseResource; 038 039import ca.uhn.fhir.parser.IParser; 040import ca.uhn.fhir.rest.api.RequestTypeEnum; 041import ca.uhn.fhir.rest.method.RequestDetails; 042import ca.uhn.fhir.rest.server.Constants; 043import ca.uhn.fhir.rest.server.EncodingEnum; 044import ca.uhn.fhir.rest.server.RestfulServer; 045import ca.uhn.fhir.rest.server.RestfulServerUtils; 046import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; 047import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 048import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 049import ca.uhn.fhir.util.UrlUtil; 050 051/** 052 * This interceptor detects when a request is coming from a browser, and automatically returns a response with syntax 053 * highlighted (coloured) HTML for the response instead of just returning raw XML/JSON. 054 * 055 * @since 1.0 056 */ 057public class ResponseHighlighterInterceptor extends InterceptorAdapter { 058 059 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class); 060 private static final String[] PARAM_FORMAT_VALUE_JSON = new String[] { Constants.FORMAT_JSON }; 061 private static final String[] PARAM_FORMAT_VALUE_XML = new String[] { Constants.FORMAT_XML }; 062 063 /** 064 * TODO: As of HAPI 1.6 (2016-06-10) this parameter has been replaced with simply 065 * requesting _format=json or xml so eventually this parameter should be removed 066 */ 067 public static final String PARAM_RAW = "_raw"; 068 069 public static final String PARAM_RAW_TRUE = "true"; 070 071 public static final String PARAM_TRUE = "true"; 072 073 private String format(String theResultBody, EncodingEnum theEncodingEnum) { 074 String str = StringEscapeUtils.escapeHtml4(theResultBody); 075 if (str == null || theEncodingEnum == null) { 076 return str; 077 } 078 079 StringBuilder b = new StringBuilder(); 080 081 if (theEncodingEnum == EncodingEnum.JSON) { 082 083 boolean inValue = false; 084 boolean inQuote = false; 085 for (int i = 0; i < str.length(); i++) { 086 char prevChar = (i > 0) ? str.charAt(i - 1) : ' '; 087 char nextChar = str.charAt(i); 088 char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' '; 089 char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' '; 090 char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' '; 091 char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' '; 092 char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' '; 093 if (inQuote) { 094 b.append(nextChar); 095 if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { 096 b.append("quot;</span>"); 097 i += 5; 098 inQuote = false; 099 } else if (nextChar == '\\' && nextChar2 == '"') { 100 b.append("quot;</span>"); 101 i += 5; 102 inQuote = false; 103 } 104 } else { 105 if (nextChar == ':') { 106 inValue = true; 107 b.append(nextChar); 108 } else if (nextChar == '[' || nextChar == '{') { 109 b.append("<span class='hlControl'>"); 110 b.append(nextChar); 111 b.append("</span>"); 112 inValue = false; 113 } else if (nextChar == '{' || nextChar == '}' || nextChar == ',') { 114 b.append("<span class='hlControl'>"); 115 b.append(nextChar); 116 b.append("</span>"); 117 inValue = false; 118 } else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { 119 if (inValue) { 120 b.append("<span class='hlQuot'>""); 121 } else { 122 b.append("<span class='hlTagName'>""); 123 } 124 inQuote = true; 125 i += 5; 126 } else if (nextChar == ':') { 127 b.append("<span class='hlControl'>"); 128 b.append(nextChar); 129 b.append("</span>"); 130 inValue = true; 131 } else { 132 b.append(nextChar); 133 } 134 } 135 } 136 137 } else { 138 boolean inQuote = false; 139 boolean inTag = false; 140 for (int i = 0; i < str.length(); i++) { 141 char nextChar = str.charAt(i); 142 char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' '; 143 char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' '; 144 char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' '; 145 char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' '; 146 char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' '; 147 if (inQuote) { 148 b.append(nextChar); 149 if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { 150 b.append("quot;</span>"); 151 i += 5; 152 inQuote = false; 153 } 154 } else if (inTag) { 155 if (nextChar == '&' && nextChar2 == 'g' && nextChar3 == 't' && nextChar4 == ';') { 156 b.append("</span><span class='hlControl'>></span>"); 157 inTag = false; 158 i += 3; 159 } else if (nextChar == ' ') { 160 b.append("</span><span class='hlAttr'>"); 161 b.append(nextChar); 162 } else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { 163 b.append("<span class='hlQuot'>""); 164 inQuote = true; 165 i += 5; 166 } else { 167 b.append(nextChar); 168 } 169 } else { 170 if (nextChar == '&' && nextChar2 == 'l' && nextChar3 == 't' && nextChar4 == ';') { 171 b.append("<span class='hlControl'><</span><span class='hlTagName'>"); 172 inTag = true; 173 i += 3; 174 } else { 175 b.append(nextChar); 176 } 177 } 178 } 179 } 180 181 return b.toString(); 182 } 183 184 @Override 185 public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) 186 throws ServletException, IOException { 187 /* 188 * It's not a browser... 189 */ 190 Set<String> accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); 191 if (!accept.contains(Constants.CT_HTML)) { 192 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 193 } 194 195 /* 196 * It's an AJAX request, so no HTML 197 */ 198 String requestedWith = theServletRequest.getHeader("X-Requested-With"); 199 if (requestedWith != null) { 200 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 201 } 202 203 /* 204 * Not a GET 205 */ 206 if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) { 207 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 208 } 209 210 if (theException.getOperationOutcome() == null) { 211 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 212 } 213 214 streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome(), theServletRequest, theException.getStatusCode()); 215 216 return false; 217 } 218 219 @Override 220 public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) 221 throws AuthenticationException { 222 223 /* 224 * Request for _raw 225 */ 226 String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW); 227 if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) { 228 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"); 229 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 230 } 231 232 boolean force = false; 233 String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT); 234 if (formatParams != null && formatParams.length > 0) { 235 String formatParam = formatParams[0]; 236 if (Constants.FORMATS_HTML.contains(formatParam)) { // this is a set 237 force = true; 238 } else if (Constants.FORMATS_HTML_XML.equals(formatParam)) { 239 force = true; 240 theRequestDetails.getParameters().put(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_XML); 241 } else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) { 242 force = true; 243 theRequestDetails.getParameters().put(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON); 244 } else { 245 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 246 } 247 } 248 249 /* 250 * It's not a browser... 251 */ 252 Set<String> highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); 253 if (!force && highestRankedAcceptValues.contains(Constants.CT_HTML) == false) { 254 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 255 } 256 257 /* 258 * It's an AJAX request, so no HTML 259 */ 260 if (!force && isNotBlank(theServletRequest.getHeader("X-Requested-With"))) { 261 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 262 } 263 /* 264 * If the request has an Origin header, it is probably an AJAX request 265 */ 266 if (!force && isNotBlank(theServletRequest.getHeader(Constants.HEADER_ORIGIN))) { 267 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 268 } 269 270 /* 271 * Not a GET 272 */ 273 if (!force && theRequestDetails.getRequestType() != RequestTypeEnum.GET) { 274 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 275 } 276 277 /* 278 * Not binary 279 */ 280 if (!force && "Binary".equals(theRequestDetails.getResourceName())) { 281 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 282 } 283 284 streamResponse(theRequestDetails, theServletResponse, theResponseObject, theServletRequest, 200); 285 286 return false; 287 } 288 289 private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource, ServletRequest theServletRequest, int theStatusCode) { 290 IParser p; 291 Map<String, String[]> parameters = theRequestDetails.getParameters(); 292 if (parameters.containsKey(Constants.PARAM_FORMAT)) { 293 p = RestfulServerUtils.getNewParser(theRequestDetails.getServer().getFhirContext(), theRequestDetails); 294 } else { 295 EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding(); 296 p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext()); 297 RestfulServerUtils.configureResponseParser(theRequestDetails, p); 298 } 299 300 // This interceptor defaults to pretty printing unless the user 301 // has specifically requested us not to 302 boolean prettyPrintResponse = true; 303 String[] prettyParams = parameters.get(Constants.PARAM_PRETTY); 304 if (prettyParams != null && prettyParams.length > 0) { 305 if (Constants.PARAM_PRETTY_VALUE_FALSE.equals(prettyParams[0])) { 306 prettyPrintResponse = false; 307 } 308 } 309 if (prettyPrintResponse) { 310 p.setPrettyPrint(prettyPrintResponse); 311 } 312 313 EncodingEnum encoding = p.getEncoding(); 314 String encoded = p.encodeResourceToString(resource); 315 316 try { 317 318 if (theStatusCode > 299) { 319 theServletResponse.setStatus(theStatusCode); 320 } 321 theServletResponse.setContentType(Constants.CT_HTML_WITH_UTF8); 322 323 StringBuilder b = new StringBuilder(); 324 b.append("<html lang=\"en\">\n"); 325 b.append(" <head>\n"); 326 b.append(" <meta charset=\"utf-8\" />\n"); 327 b.append(" <style>\n"); 328 b.append(".hlQuot {\n"); 329 b.append(" color: #88F;\n"); 330 b.append("}\n"); 331 b.append(".hlAttr {\n"); 332 b.append(" color: #888;\n"); 333 b.append("}\n"); 334 b.append(".hlTagName {\n"); 335 b.append(" color: #006699;\n"); 336 b.append("}\n"); 337 b.append(".hlControl {\n"); 338 b.append(" color: #660000;\n"); 339 b.append("}\n"); 340 b.append(".hlText {\n"); 341 b.append(" color: #000000;\n"); 342 b.append("}\n"); 343 b.append(".hlUrlBase {\n"); 344 b.append("}"); 345 b.append(".headersDiv {\n"); 346 b.append(" background: #EEE;"); 347 b.append("}"); 348 b.append(".headerName {\n"); 349 b.append(" color: #888;\n"); 350 b.append(" font-family: monospace;\n"); 351 b.append("}"); 352 b.append(".headerValue {\n"); 353 b.append(" color: #88F;\n"); 354 b.append(" font-family: monospace;\n"); 355 b.append("}"); 356 b.append("BODY {\n"); 357 b.append(" font-family: Arial;\n"); 358 b.append("}"); 359 b.append(" </style>\n"); 360 b.append(" </head>\n"); 361 b.append("\n"); 362 b.append(" <body>"); 363 364 b.append("<p>"); 365 b.append("This result is being rendered in HTML for easy viewing. "); 366 b.append("You may access this content as "); 367 368 b.append("<a href=\""); 369 b.append(createLinkHref(parameters, Constants.FORMAT_JSON)); 370 b.append("\">Raw JSON</a> or "); 371 372 b.append("<a href=\""); 373 b.append(createLinkHref(parameters, Constants.FORMAT_XML)); 374 b.append("\">Raw XML</a>, "); 375 376 b.append(" or view this content in "); 377 378 b.append("<a href=\""); 379 b.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON)); 380 b.append("\">HTML JSON</a> "); 381 382 b.append("or "); 383 b.append("<a href=\""); 384 b.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML)); 385 b.append("\">HTML XML</a>."); 386 387 Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME); 388 if (startTime != null) { 389 long time = System.currentTimeMillis() - startTime.getTime(); 390 b.append(" Response generated in "); 391 b.append(time); 392 b.append("ms."); 393 } 394 395 b.append("</p>"); 396 397 b.append("\n"); 398 399 // if (isEncodeHeaders()) { 400 // b.append("<h1>Request Headers</h1>"); 401 // b.append("<div class=\"headersDiv\">"); 402 // for (int next : theRequestDetails.get) 403 // b.append("</div>"); 404 // b.append("<h1>Response Headers</h1>"); 405 // b.append("<div class=\"headersDiv\">"); 406 // b.append("</div>"); 407 // b.append("<h1>Response Body</h1>"); 408 // } 409 b.append("<pre>"); 410 b.append(format(encoded, encoding)); 411 b.append("</pre>"); 412 b.append(" </body>"); 413 b.append("</html>"); 414 //@formatter:off 415 String out = b.toString(); 416 //@formatter:on 417 418 theServletResponse.getWriter().append(out); 419 theServletResponse.getWriter().close(); 420 } catch (IOException e) { 421 throw new InternalErrorException(e); 422 } 423 } 424 425 private String createLinkHref(Map<String, String[]> parameters, String formatValue) { 426 StringBuilder rawB = new StringBuilder(); 427 for (String next : parameters.keySet()) { 428 if (Constants.PARAM_FORMAT.equals(next)) { 429 continue; 430 } 431 for (String nextValue : parameters.get(next)) { 432 if (isBlank(nextValue)) { 433 continue; 434 } 435 if (rawB.length() == 0) { 436 rawB.append('?'); 437 } else { 438 rawB.append('&'); 439 } 440 rawB.append(UrlUtil.escape(next)); 441 rawB.append('='); 442 rawB.append(UrlUtil.escape(nextValue)); 443 } 444 } 445 if (rawB.length() == 0) { 446 rawB.append('?'); 447 } else { 448 rawB.append('&'); 449 } 450 rawB.append(Constants.PARAM_FORMAT).append('=').append(formatValue); 451 452 String link = rawB.toString(); 453 return link; 454 } 455 456}