diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index a7aa2c6bd1e..16eda352113 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -162,6 +162,8 @@ public class Constants { public static final String TAG_SUBSETTED_SYSTEM = "http://hl7.org/fhir/v3/ObservationValue"; public static final String URL_TOKEN_HISTORY = "_history"; public static final String URL_TOKEN_METADATA = "metadata"; + public static final String FORMATS_HTML_JSON = "html/json"; + public static final String FORMATS_HTML_XML = "html/xml"; static { Map valToEncoding = new HashMap(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index 3be5c0512e1..a7040d705fc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -24,9 +24,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ import java.io.IOException; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.List; import java.util.Map; import java.util.Set; @@ -56,25 +53,19 @@ import ca.uhn.fhir.util.UrlUtil; */ public class ResponseHighlighterInterceptor extends InterceptorAdapter { - public static final String PARAM_RAW_TRUE = "true"; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class); + private static final String[] PARAM_FORMAT_VALUE_JSON = new String[] { Constants.FORMAT_JSON }; + private static final String[] PARAM_FORMAT_VALUE_XML = new String[] { Constants.FORMAT_XML }; + + /** + * TODO: As of HAPI 1.6 (2016-06-10) this parameter has been replaced with simply + * requesting _format=json or xml so eventually this parameter should be removed + */ public static final String PARAM_RAW = "_raw"; -// private boolean myEncodeHeaders = false; -// -// /** -// * Should headers be included in the HTML response? -// */ -// public boolean isEncodeHeaders() { -// return myEncodeHeaders; -// } -// -// /** -// * Should headers be included in the HTML response? -// */ -// public void setEncodeHeaders(boolean theEncodeHeaders) { -// myEncodeHeaders = theEncodeHeaders; -// } - + public static final String PARAM_RAW_TRUE = "true"; + + public static final String PARAM_TRUE = "true"; private String format(String theResultBody, EncodingEnum theEncodingEnum) { String str = StringEscapeUtils.escapeHtml4(theResultBody); if (str == null || theEncodingEnum == null) { @@ -185,17 +176,67 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { return b.toString(); } -private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class); + + @Override + public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException { + /* + * It's not a browser... + */ + Set accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); + if (!accept.contains(Constants.CT_HTML)) { + return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + } + + /* + * It's an AJAX request, so no HTML + */ + String requestedWith = theServletRequest.getHeader("X-Requested-With"); + if (requestedWith != null) { + return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + } + + /* + * Not a GET + */ + if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) { + return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + } + + if (theException.getOperationOutcome() == null) { + return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + } + + streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome()); + + return false; + } + @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + /* + * Request for _raw + */ + String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW); + if (rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) { + 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"); + return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); + } + boolean force = false; String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT); - if (formatParams != null) { - for (String next : formatParams) { - if (Constants.FORMATS_HTML.contains(next)) { - force = true; - } + if (formatParams != null && formatParams.length > 0) { + String formatParam = formatParams[0]; + if (Constants.FORMATS_HTML.contains(formatParam)) { // this is a set + force = true; + } else if (Constants.FORMATS_HTML_XML.equals(formatParam)) { + force = true; + theRequestDetails.getParameters().put(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_XML); + } else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) { + force = true; + theRequestDetails.getParameters().put(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON); + } else { + return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); } } @@ -235,14 +276,6 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); } - /* - * Request for _raw - */ - String[] rawParamValues = theRequestDetails.getParameters().get(PARAM_RAW); - if (!force && rawParamValues != null && rawParamValues.length > 0 && rawParamValues[0].equals(PARAM_RAW_TRUE)) { - return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); - } - streamResponse(theRequestDetails, theServletResponse, theResponseObject); return false; @@ -264,32 +297,6 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger theServletResponse.setContentType(Constants.CT_HTML_WITH_UTF8); - StringBuilder rawB = new StringBuilder(); - for (String next : parameters.keySet()) { - if (next.equals(PARAM_RAW)) { - continue; - } - for (String nextValue : parameters.get(next)) { - if (isBlank(nextValue)) { - continue; - } - if (rawB.length() == 0) { - rawB.append('?'); - }else { - rawB.append('&'); - } - rawB.append(UrlUtil.escape(next)); - rawB.append('='); - rawB.append(UrlUtil.escape(nextValue)); - } - } - if (rawB.length() == 0) { - rawB.append('?'); - }else { - rawB.append('&'); - } - rawB.append(PARAM_RAW).append('=').append(PARAM_RAW_TRUE); - StringBuilder b = new StringBuilder(); b.append("\n"); b.append(" \n"); @@ -330,19 +337,41 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger b.append(" \n"); b.append("\n"); b.append(" "); - b.append("This result is being rendered in HTML for easy viewing. Click here to disable this.

"); -// if (isEncodeHeaders()) { -// b.append("

Request Headers

"); -// b.append("
"); -// for (int next : theRequestDetails.get) -// b.append("
"); -// b.append("

Response Headers

"); -// b.append("
"); -// b.append("
"); -// b.append("

Response Body

"); -// } + + b.append("

"); + b.append("This result is being rendered in HTML for easy viewing. "); + b.append("You may view this content as "); + + b.append("Raw JSON, "); + + b.append("Raw XML, "); + + b.append("HTML JSON, "); + + b.append("or "); + b.append("HTML XML."); + b.append("

"); + + b.append("\n"); + + // if (isEncodeHeaders()) { + // b.append("

Request Headers

"); + // b.append("
"); + // for (int next : theRequestDetails.get) + // b.append("
"); + // b.append("

Response Headers

"); + // b.append("
"); + // b.append("
"); + // b.append("

Response Body

"); + // } b.append("
");
 		b.append(format(encoded, encoding));
 		b.append("
"); @@ -360,38 +389,35 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger } } - @Override - public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException { - /* - * It's not a browser... - */ - Set accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); - if (!accept.contains(Constants.CT_HTML)) { - return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + private String createLinkHref(Map parameters, String formatValue) { + StringBuilder rawB = new StringBuilder(); + for (String next : parameters.keySet()) { + if (Constants.PARAM_FORMAT.equals(next)) { + continue; + } + for (String nextValue : parameters.get(next)) { + if (isBlank(nextValue)) { + continue; + } + if (rawB.length() == 0) { + rawB.append('?'); + } else { + rawB.append('&'); + } + rawB.append(UrlUtil.escape(next)); + rawB.append('='); + rawB.append(UrlUtil.escape(nextValue)); + } } - - /* - * It's an AJAX request, so no HTML - */ - String requestedWith = theServletRequest.getHeader("X-Requested-With"); - if (requestedWith != null) { - return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); + if (rawB.length() == 0) { + rawB.append('?'); + } else { + rawB.append('&'); } + rawB.append(Constants.PARAM_FORMAT).append('=').append(formatValue); - /* - * Not a GET - */ - if (theRequestDetails.getRequestType() != RequestTypeEnum.GET) { - return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); - } - - if (theException.getOperationOutcome() == null) { - return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); - } - - streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome()); - - return false; + String link = rawB.toString(); + return link; } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index 43cb185e07e..79545676208 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -33,7 +33,6 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.ebaysf.web.cors.CORSFilter; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; @@ -70,6 +69,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; public class ResponseHighlightingInterceptorTest { @@ -509,6 +509,126 @@ public class ResponseHighlightingInterceptorTest { assertEquals("{\"resourceType\":\"Binary\",\"id\":\"1\",\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", responseContent); } + @Test + public void testForceApplicationJson() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + @Test + public void testForceApplicationJsonFhir() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json+fhir"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + @Test + public void testForceApplicationJsonPlusFhir() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escape("application/json+fhir")); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + + @Test + public void testForceJson() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=json"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + + + + @Test + public void testForceHtmlJson() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, containsString("html")); + assertThat(responseContent, containsString(">{<")); + assertThat(responseContent, not(containsString("<"))); + + ourLog.info(responseContent); + } + + @Test + public void testForceHtmlXml() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/xml"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, containsString("html")); + assertThat(responseContent, not(containsString(">{<"))); + assertThat(responseContent, containsString("<")); + } + + @Test + public void testForceApplicationXml() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/xml"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + @Test + public void testForceApplicationXmlFhir() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/xml+fhir"); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } + @Test + public void testForceApplicationXmlPlusFhir() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escape("application/xml+fhir")); + httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent, not(containsString("html"))); + } @BeforeClass public static void beforeClass() throws Exception { diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 72affb619bb..9258d24992c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -304,6 +304,21 @@ incorrectly claim that the method will not return null when in fact it can. Thanks to Rick Riemer for reporting! + + ResponseHighlightingInterceptor has been modified based on consensus + on Zulip with Grahame that requests that have a parameter of + _format=json]]> or + _format=xml]]> will output raw FHIR content + instead of HTML highlighting the content as they previously did. + HTML content can now be forced via the (previously existing) + _format=html]]> or via the two newly added + values + _format=html/json]]> and + _format=html/xml]]>. Because of this + change, the custom + _raw=true]]> mode has been deprecated and + will be removed at some point. +