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'>&quot;");
121                                                } else {
122                                                        b.append("<span class='hlTagName'>&quot;");
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'>&gt;</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'>&quot;");
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'>&lt;</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}