001package ca.uhn.fhir.rest.server.interceptor; 002 003import static org.apache.commons.lang3.StringUtils.isBlank; 004 005/* 006 * #%L 007 * HAPI FHIR - Core Library 008 * %% 009 * Copyright (C) 2014 - 2016 University Health Network 010 * %% 011 * Licensed under the Apache License, Version 2.0 (the "License"); 012 * you may not use this file except in compliance with the License. 013 * You may obtain a copy of the License at 014 * 015 * http://www.apache.org/licenses/LICENSE-2.0 016 * 017 * Unless required by applicable law or agreed to in writing, software 018 * distributed under the License is distributed on an "AS IS" BASIS, 019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 020 * See the License for the specific language governing permissions and 021 * limitations under the License. 022 * #L% 023 */ 024 025import java.io.IOException; 026import java.util.Set; 027 028import javax.servlet.ServletException; 029import javax.servlet.http.HttpServletRequest; 030import javax.servlet.http.HttpServletResponse; 031 032import org.apache.commons.lang3.StringEscapeUtils; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034 035import ca.uhn.fhir.parser.IParser; 036import ca.uhn.fhir.rest.api.RequestTypeEnum; 037import ca.uhn.fhir.rest.method.RequestDetails; 038import ca.uhn.fhir.rest.server.Constants; 039import ca.uhn.fhir.rest.server.EncodingEnum; 040import ca.uhn.fhir.rest.server.RestfulServerUtils; 041import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; 042import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 043import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 044import ca.uhn.fhir.util.UrlUtil; 045 046/** 047 * This interceptor detects when a request is coming from a browser, and automatically returns a response with syntax 048 * highlighted (coloured) HTML for the response instead of just returning raw XML/JSON. 049 * 050 * @since 1.0 051 */ 052public class ResponseHighlighterInterceptor extends InterceptorAdapter { 053 054 public static final String PARAM_RAW_TRUE = "true"; 055 public static final String PARAM_RAW = "_raw"; 056 057// private boolean myEncodeHeaders = false; 058// 059// /** 060// * Should headers be included in the HTML response? 061// */ 062// public boolean isEncodeHeaders() { 063// return myEncodeHeaders; 064// } 065// 066// /** 067// * Should headers be included in the HTML response? 068// */ 069// public void setEncodeHeaders(boolean theEncodeHeaders) { 070// myEncodeHeaders = theEncodeHeaders; 071// } 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 outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { 186 187 /* 188 * It's not a browser... 189 */ 190 Set<String> highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); 191 if (highestRankedAcceptValues.contains(Constants.CT_HTML) == false) { 192 return super.outgoingResponse(theRequestDetails, theResponseObject, 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.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 201 } 202 203 /* 204 * Not a GET 205 */ 206 if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) { 207 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 208 } 209 210 String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW); 211 if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) { 212 return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); 213 } 214 215 streamResponse(theRequestDetails, theServletResponse, theResponseObject); 216 217 return false; 218 } 219 220 private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource) { 221 IParser p; 222 if (theRequestDetails.getParameters().containsKey(Constants.PARAM_FORMAT)) { 223 p = RestfulServerUtils.getNewParser(theRequestDetails.getServer().getFhirContext(), theRequestDetails); 224 } else { 225 EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding(); 226 p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext()); 227 RestfulServerUtils.configureResponseParser(theRequestDetails, p); 228 } 229 230 EncodingEnum encoding = p.getEncoding(); 231 String encoded = p.encodeResourceToString(resource); 232 233 theServletResponse.setContentType(Constants.CT_HTML_WITH_UTF8); 234 235 StringBuilder rawB = new StringBuilder(); 236 for (String next : theRequestDetails.getParameters().keySet()) { 237 if (next.equals(PARAM_RAW)) { 238 continue; 239 } 240 for (String nextValue : theRequestDetails.getParameters().get(next)) { 241 if (isBlank(nextValue)) { 242 continue; 243 } 244 if (rawB.length() == 0) { 245 rawB.append('?'); 246 }else { 247 rawB.append('&'); 248 } 249 rawB.append(UrlUtil.escape(next)); 250 rawB.append('='); 251 rawB.append(UrlUtil.escape(nextValue)); 252 } 253 } 254 if (rawB.length() == 0) { 255 rawB.append('?'); 256 }else { 257 rawB.append('&'); 258 } 259 rawB.append(PARAM_RAW).append('=').append(PARAM_RAW_TRUE); 260 261 StringBuilder b = new StringBuilder(); 262 b.append("<html lang=\"en\">\n"); 263 b.append(" <head>\n"); 264 b.append(" <meta charset=\"utf-8\" />\n"); 265 b.append(" <style>\n"); 266 b.append(".hlQuot {\n"); 267 b.append(" color: #88F;\n"); 268 b.append("}\n"); 269 b.append(".hlAttr {\n"); 270 b.append(" color: #888;\n"); 271 b.append("}\n"); 272 b.append(".hlTagName {\n"); 273 b.append(" color: #006699;\n"); 274 b.append("}\n"); 275 b.append(".hlControl {\n"); 276 b.append(" color: #660000;\n"); 277 b.append("}\n"); 278 b.append(".hlText {\n"); 279 b.append(" color: #000000;\n"); 280 b.append("}\n"); 281 b.append(".hlUrlBase {\n"); 282 b.append("}"); 283 b.append(".headersDiv {\n"); 284 b.append(" background: #EEE;"); 285 b.append("}"); 286 b.append(".headerName {\n"); 287 b.append(" color: #888;\n"); 288 b.append(" font-family: monospace;\n"); 289 b.append("}"); 290 b.append(".headerValue {\n"); 291 b.append(" color: #88F;\n"); 292 b.append(" font-family: monospace;\n"); 293 b.append("}"); 294 b.append("BODY {\n"); 295 b.append(" font-family: Arial;\n"); 296 b.append("}"); 297 b.append(" </style>\n"); 298 b.append(" </head>\n"); 299 b.append("\n"); 300 b.append(" <body>"); 301 b.append("This result is being rendered in HTML for easy viewing. <a href=\""); 302 b.append(rawB.toString()); 303 b.append("\">Click here</a> to disable this.<br/><br/>"); 304// if (isEncodeHeaders()) { 305// b.append("<h1>Request Headers</h1>"); 306// b.append("<div class=\"headersDiv\">"); 307// for (int next : theRequestDetails.get) 308// b.append("</div>"); 309// b.append("<h1>Response Headers</h1>"); 310// b.append("<div class=\"headersDiv\">"); 311// b.append("</div>"); 312// b.append("<h1>Response Body</h1>"); 313// } 314 b.append("<pre>"); 315 b.append(format(encoded, encoding)); 316 b.append("</pre>"); 317 b.append(" </body>"); 318 b.append("</html>"); 319 //@formatter:off 320 String out = b.toString(); 321 //@formatter:on 322 323 try { 324 theServletResponse.getWriter().append(out); 325 theServletResponse.getWriter().close(); 326 } catch (IOException e) { 327 throw new InternalErrorException(e); 328 } 329 } 330 331 @Override 332 public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException { 333 /* 334 * It's not a browser... 335 */ 336 Set<String> accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); 337 if (!accept.contains(Constants.CT_HTML)) { 338 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 339 } 340 341 /* 342 * It's an AJAX request, so no HTML 343 */ 344 String requestedWith = theServletRequest.getHeader("X-Requested-With"); 345 if (requestedWith != null) { 346 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 347 } 348 349 /* 350 * Not a GET 351 */ 352 if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) { 353 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 354 } 355 356 if (theException.getOperationOutcome() == null) { 357 return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); 358 } 359 360 streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome()); 361 362 return false; 363 } 364 365}