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'>&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 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}