Render narrative in HTML resource view (#4702)
* Working * Add changelog
This commit is contained in:
parent
e69fe05e96
commit
ee8b5b39d4
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 4702
|
||||||
|
title: "The ResponseHighlightingInterceptor (which renders a stylized HTML view
|
||||||
|
of resources) will now include the resource narrative in the rendered view
|
||||||
|
if one is present."
|
|
@ -19,8 +19,9 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.rest.server.interceptor;
|
package ca.uhn.fhir.rest.server.interceptor;
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.interceptor.api.Hook;
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
@ -38,24 +39,31 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
||||||
|
import ca.uhn.fhir.util.ClasspathUtil;
|
||||||
import ca.uhn.fhir.util.FhirTerser;
|
import ca.uhn.fhir.util.FhirTerser;
|
||||||
import ca.uhn.fhir.util.StopWatch;
|
import ca.uhn.fhir.util.StopWatch;
|
||||||
import ca.uhn.fhir.util.UrlUtil;
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.apache.commons.text.StringEscapeUtils;
|
import org.apache.commons.text.StringEscapeUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -63,6 +71,7 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
|
||||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
@ -89,6 +98,7 @@ public class ResponseHighlighterInterceptor {
|
||||||
private static final String[] PARAM_FORMAT_VALUE_TTL = new String[]{Constants.FORMAT_TURTLE};
|
private static final String[] PARAM_FORMAT_VALUE_TTL = new String[]{Constants.FORMAT_TURTLE};
|
||||||
private boolean myShowRequestHeaders = false;
|
private boolean myShowRequestHeaders = false;
|
||||||
private boolean myShowResponseHeaders = true;
|
private boolean myShowResponseHeaders = true;
|
||||||
|
private boolean myShowNarrative = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -427,7 +437,6 @@ public class ResponseHighlighterInterceptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) {
|
private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) {
|
||||||
if (theResourceResponse == null && theGraphqlResponse == null) {
|
if (theResourceResponse == null && theGraphqlResponse == null) {
|
||||||
// this will happen during, for example, a bulk export polling request
|
// this will happen during, for example, a bulk export polling request
|
||||||
|
@ -582,90 +591,7 @@ public class ResponseHighlighterInterceptor {
|
||||||
outputBuffer.append(" <head>\n");
|
outputBuffer.append(" <head>\n");
|
||||||
outputBuffer.append(" <meta charset=\"utf-8\" />\n");
|
outputBuffer.append(" <meta charset=\"utf-8\" />\n");
|
||||||
outputBuffer.append(" <style>\n");
|
outputBuffer.append(" <style>\n");
|
||||||
outputBuffer.append(".httpStatusDiv {");
|
outputBuffer.append(ClasspathUtil.loadResource("ca/uhn/fhir/rest/server/interceptor/ResponseHighlighter.css"));
|
||||||
outputBuffer.append(" font-size: 1.2em;");
|
|
||||||
outputBuffer.append(" font-weight: bold;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".hlQuot { color: #88F; }\n");
|
|
||||||
outputBuffer.append(".hlQuot a { text-decoration: underline; text-decoration-color: #CCC; }\n");
|
|
||||||
outputBuffer.append(".hlQuot a:HOVER { text-decoration: underline; text-decoration-color: #008; }\n");
|
|
||||||
outputBuffer.append(".hlQuot .uuid, .hlQuot .dateTime {\n");
|
|
||||||
outputBuffer.append(" user-select: all;\n");
|
|
||||||
outputBuffer.append(" -moz-user-select: all;\n");
|
|
||||||
outputBuffer.append(" -webkit-user-select: all;\n");
|
|
||||||
outputBuffer.append(" -ms-user-select: element;\n");
|
|
||||||
outputBuffer.append("}\n");
|
|
||||||
outputBuffer.append(".hlAttr {\n");
|
|
||||||
outputBuffer.append(" color: #888;\n");
|
|
||||||
outputBuffer.append("}\n");
|
|
||||||
outputBuffer.append(".hlTagName {\n");
|
|
||||||
outputBuffer.append(" color: #006699;\n");
|
|
||||||
outputBuffer.append("}\n");
|
|
||||||
outputBuffer.append(".hlControl {\n");
|
|
||||||
outputBuffer.append(" color: #660000;\n");
|
|
||||||
outputBuffer.append("}\n");
|
|
||||||
outputBuffer.append(".hlText {\n");
|
|
||||||
outputBuffer.append(" color: #000000;\n");
|
|
||||||
outputBuffer.append("}\n");
|
|
||||||
outputBuffer.append(".hlUrlBase {\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".headersDiv {\n");
|
|
||||||
outputBuffer.append(" padding: 10px;");
|
|
||||||
outputBuffer.append(" margin-left: 10px;");
|
|
||||||
outputBuffer.append(" border: 1px solid #CCC;");
|
|
||||||
outputBuffer.append(" border-radius: 10px;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".headersRow {\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".headerName {\n");
|
|
||||||
outputBuffer.append(" color: #888;\n");
|
|
||||||
outputBuffer.append(" font-family: monospace;\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".headerValue {\n");
|
|
||||||
outputBuffer.append(" color: #88F;\n");
|
|
||||||
outputBuffer.append(" font-family: monospace;\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".responseBodyTable {");
|
|
||||||
outputBuffer.append(" width: 100%;\n");
|
|
||||||
outputBuffer.append(" margin-left: 0px;\n");
|
|
||||||
outputBuffer.append(" margin-top: -10px;\n");
|
|
||||||
outputBuffer.append(" position: relative;\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".responseBodyTableFirstColumn {");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".responseBodyTableSecondColumn {");
|
|
||||||
outputBuffer.append(" position: absolute;\n");
|
|
||||||
outputBuffer.append(" margin-left: 70px;\n");
|
|
||||||
outputBuffer.append(" vertical-align: top;\n");
|
|
||||||
outputBuffer.append(" left: 0px;\n");
|
|
||||||
outputBuffer.append(" right: 0px;\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".responseBodyTableSecondColumn PRE {");
|
|
||||||
outputBuffer.append(" margin: 0px;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".sizeInfo {");
|
|
||||||
outputBuffer.append(" margin-top: 20px;");
|
|
||||||
outputBuffer.append(" font-size: 0.8em;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".lineAnchor A {");
|
|
||||||
outputBuffer.append(" text-decoration: none;");
|
|
||||||
outputBuffer.append(" padding-left: 20px;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".lineAnchor {");
|
|
||||||
outputBuffer.append(" display: block;");
|
|
||||||
outputBuffer.append(" padding-right: 20px;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(".selectedLine {");
|
|
||||||
outputBuffer.append(" background-color: #EEF;");
|
|
||||||
outputBuffer.append(" font-weight: bold;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append("H1 {");
|
|
||||||
outputBuffer.append(" font-size: 1.1em;");
|
|
||||||
outputBuffer.append(" color: #666;");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append("BODY {\n");
|
|
||||||
outputBuffer.append(" font-family: Arial;\n");
|
|
||||||
outputBuffer.append("}");
|
|
||||||
outputBuffer.append(" </style>\n");
|
outputBuffer.append(" </style>\n");
|
||||||
outputBuffer.append(" </head>\n");
|
outputBuffer.append(" </head>\n");
|
||||||
outputBuffer.append("\n");
|
outputBuffer.append("\n");
|
||||||
|
@ -756,6 +682,16 @@ public class ResponseHighlighterInterceptor {
|
||||||
// ignore (this will hit if we're running in a servlet 2.5 environment)
|
// ignore (this will hit if we're running in a servlet 2.5 environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (myShowNarrative) {
|
||||||
|
String narrativeHtml = extractNarrativeHtml(theRequestDetails, theResource);
|
||||||
|
if (isNotBlank(narrativeHtml)) {
|
||||||
|
outputBuffer.append("<h1>Narrative</h1>");
|
||||||
|
outputBuffer.append("<div class=\"narrativeBody\">");
|
||||||
|
outputBuffer.append(narrativeHtml);
|
||||||
|
outputBuffer.append("</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
outputBuffer.append("<h1>Response Body</h1>");
|
outputBuffer.append("<h1>Response Body</h1>");
|
||||||
|
|
||||||
outputBuffer.append("<div class=\"responseBodyTable\">");
|
outputBuffer.append("<div class=\"responseBodyTable\">");
|
||||||
|
@ -827,6 +763,71 @@ public class ResponseHighlighterInterceptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
String extractNarrativeHtml(@Nonnull RequestDetails theRequestDetails, @Nullable IBaseResource theResource) {
|
||||||
|
if (theResource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FhirContext ctx = theRequestDetails.getFhirContext();
|
||||||
|
|
||||||
|
// Try to extract the narrative from the resource. First, just see if there
|
||||||
|
// is a narrative in the normal spot.
|
||||||
|
XhtmlNode xhtmlNode = extractNarrativeFromDomainResource(theResource, ctx);
|
||||||
|
|
||||||
|
// If the resource is a document, see if the Composition has a narrative
|
||||||
|
if (xhtmlNode == null && "Bundle".equals(ctx.getResourceType(theResource))) {
|
||||||
|
if ("document".equals(ctx.newTerser().getSinglePrimitiveValueOrNull(theResource, "type"))) {
|
||||||
|
IBaseResource firstResource = ctx.newTerser().getSingleValueOrNull(theResource, "entry.resource", IBaseResource.class);
|
||||||
|
if (firstResource != null && "Composition".equals(ctx.getResourceType(firstResource))) {
|
||||||
|
xhtmlNode = extractNarrativeFromDomainResource(firstResource, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the resource is a Parameters, see if it has a narrative in the first
|
||||||
|
// parameter
|
||||||
|
if (xhtmlNode == null && "Parameters".equals(ctx.getResourceType(theResource))) {
|
||||||
|
String firstParameterName = ctx.newTerser().getSinglePrimitiveValueOrNull(theResource, "parameter.name");
|
||||||
|
if ("Narrative".equals(firstParameterName)) {
|
||||||
|
String firstParameterValue = ctx.newTerser().getSinglePrimitiveValueOrNull(theResource, "parameter.value[x]");
|
||||||
|
if (defaultString(firstParameterValue).startsWith("<div")) {
|
||||||
|
xhtmlNode = new XhtmlNode();
|
||||||
|
xhtmlNode.setValueAsString(firstParameterValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FHIR only allows a pretty restricted set of HTML tags and attributes, in order
|
||||||
|
* to avoid any risk of injection attacks. If anything that isn't explicitly allowed
|
||||||
|
* by FHIR is present in the narrative we won't render it and instead we'll explain
|
||||||
|
* what validation problems we found.
|
||||||
|
*/
|
||||||
|
if (xhtmlNode != null) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
Validate.isTrue(xhtmlNode.getName() == null);
|
||||||
|
xhtmlNode.getFirstElement().validate(errors, "", true, false, false);
|
||||||
|
if (errors.size() > 0) {
|
||||||
|
StringBuilder errorNarrative = new StringBuilder();
|
||||||
|
errorNarrative.append("Can not render narrative due to validation errors:");
|
||||||
|
errorNarrative.append("<ul>");
|
||||||
|
errors.forEach(next -> {
|
||||||
|
errorNarrative.append("<li>");
|
||||||
|
errorNarrative.append(sanitizeUrlPart(next));
|
||||||
|
errorNarrative.append("</li>");
|
||||||
|
});
|
||||||
|
errorNarrative.append("</ul>");
|
||||||
|
return errorNarrative.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return xhtmlNode.getValueAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void writeLength(HttpServletResponse theServletResponse, int theLength) throws IOException {
|
private void writeLength(HttpServletResponse theServletResponse, int theLength) throws IOException {
|
||||||
double kb = ((double) theLength) / FileUtils.ONE_KB;
|
double kb = ((double) theLength) / FileUtils.ONE_KB;
|
||||||
if (kb <= 1000) {
|
if (kb <= 1000) {
|
||||||
|
@ -876,4 +877,75 @@ public class ResponseHighlighterInterceptor {
|
||||||
theBuilder.append("</div>");
|
theBuilder.append("</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to {@literal true} (default is {@literal true}), if the response is a FHIR
|
||||||
|
* resource, and that resource includes a <a href="http://hl7.org/fhir/narrative.html">Narrative</div>,
|
||||||
|
* the narrative will be rendered in the HTML response page as actual rendered HTML.
|
||||||
|
* <p>
|
||||||
|
* The narrative to be rendered will be sourced from one of 3 possible locations,
|
||||||
|
* depending on the resource being returned by the server:
|
||||||
|
* <ul>
|
||||||
|
* <li>if the resource is a DomainResource, the narrative in Resource.text will be rendered.</li>
|
||||||
|
* <li>If the resource is a document bundle, the narrative in the document Composition will be rendered.</li>
|
||||||
|
* <li>If the resource is a Parameters resource, and the first parameter has the name "Narrative" and a value consisting of a string starting with "<div", that will be rendered.</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* In all cases, the narrative is scanned to ensure that it does not contain any tags
|
||||||
|
* or attributes that are not explicitly allowed by the FHIR specification in order
|
||||||
|
* to <a href="http://hl7.org/fhir/narrative.html#xhtml">prevent active content</a>.
|
||||||
|
* If any such tags or attributes are found, the narrative is not rendered and
|
||||||
|
* instead a warning is displayed. Note that while this scanning is helpful, it does
|
||||||
|
* not completely mitigate the security risks associated with narratives. See
|
||||||
|
* <a href="http://hl7.org/fhir/security.html#narrative">FHIR Security: Narrative</a>
|
||||||
|
* for more information.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return Should the narrative be rendered?
|
||||||
|
* @since 6.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
public boolean isShowNarrative() {
|
||||||
|
return myShowNarrative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to {@literal true} (default is {@literal true}), if the response is a FHIR
|
||||||
|
* resource, and that resource includes a <a href="http://hl7.org/fhir/narrative.html">Narrative</div>,
|
||||||
|
* the narrative will be rendered in the HTML response page as actual rendered HTML.
|
||||||
|
* <p>
|
||||||
|
* The narrative to be rendered will be sourced from one of 3 possible locations,
|
||||||
|
* depending on the resource being returned by the server:
|
||||||
|
* <ul>
|
||||||
|
* <li>if the resource is a DomainResource, the narrative in Resource.text will be rendered.</li>
|
||||||
|
* <li>If the resource is a document bundle, the narrative in the document Composition will be rendered.</li>
|
||||||
|
* <li>If the resource is a Parameters resource, and the first parameter has the name "Narrative" and a value consisting of a string starting with "<div", that will be rendered.</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* In all cases, the narrative is scanned to ensure that it does not contain any tags
|
||||||
|
* or attributes that are not explicitly allowed by the FHIR specification in order
|
||||||
|
* to <a href="http://hl7.org/fhir/narrative.html#xhtml">prevent active content</a>.
|
||||||
|
* If any such tags or attributes are found, the narrative is not rendered and
|
||||||
|
* instead a warning is displayed. Note that while this scanning is helpful, it does
|
||||||
|
* not completely mitigate the security risks associated with narratives. See
|
||||||
|
* <a href="http://hl7.org/fhir/security.html#narrative">FHIR Security: Narrative</a>
|
||||||
|
* for more information.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theShowNarrative Should the narrative be rendered?
|
||||||
|
* @since 6.6.0
|
||||||
|
*/
|
||||||
|
public void setShowNarrative(boolean theShowNarrative) {
|
||||||
|
myShowNarrative = theShowNarrative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static XhtmlNode extractNarrativeFromDomainResource(@Nonnull IBaseResource theResource, FhirContext ctx) {
|
||||||
|
if (ctx.getResourceDefinition(theResource).getChildByName("text") != null) {
|
||||||
|
return ctx.newTerser().getSingleValue(theResource, "text.div", XhtmlNode.class).orElse(null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
.httpStatusDiv {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlQuot {
|
||||||
|
color: #88F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlQuot a {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlQuot a:HOVER {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: #008;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlQuot .uuid, .hlQuot .dateTime {
|
||||||
|
user-select: all;
|
||||||
|
-moz-user-select: all;
|
||||||
|
-webkit-user-select: all;
|
||||||
|
-ms-user-select: element;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlAttr {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlTagName {
|
||||||
|
color: #006699;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlControl {
|
||||||
|
color: #660000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlText {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hlUrlBase {
|
||||||
|
}
|
||||||
|
|
||||||
|
.headersDiv {
|
||||||
|
padding: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headersRow {
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerName {
|
||||||
|
color: #888;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerValue {
|
||||||
|
color: #88F;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody {
|
||||||
|
padding: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody DIV,
|
||||||
|
.narrativeBody TABLE {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody TABLE > THEAD {
|
||||||
|
background: #AAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody TABLE > TBODY > TR:nth-child(odd) > TD {
|
||||||
|
background: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody TABLE > TBODY > TR:nth-child(even) > TD {
|
||||||
|
background: #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrativeBody TABLE TD, .narrativeBody TABLE TH {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responseBodyTable {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-top: -10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responseBodyTableFirstColumn {
|
||||||
|
}
|
||||||
|
|
||||||
|
.responseBodyTableSecondColumn {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 70px;
|
||||||
|
vertical-align: top;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responseBodyTableSecondColumn PRE {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sizeInfo {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineAnchor A {
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineAnchor {
|
||||||
|
display: block;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedLine {
|
||||||
|
background-color: #EEF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
H1 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
BODY {
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.ResponseDetails;
|
import ca.uhn.fhir.rest.api.server.ResponseDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
|
@ -40,20 +41,28 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Binary;
|
import org.hl7.fhir.r4.model.Binary;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
import org.hl7.fhir.r4.model.Composition;
|
||||||
import org.hl7.fhir.r4.model.HumanName;
|
import org.hl7.fhir.r4.model.HumanName;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
import org.hl7.fhir.r4.model.OperationOutcome;
|
||||||
import org.hl7.fhir.r4.model.Organization;
|
import org.hl7.fhir.r4.model.Organization;
|
||||||
|
import org.hl7.fhir.r4.model.Parameters;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Quantity;
|
||||||
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
|
import org.hl7.fhir.r4.model.Type;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -82,17 +91,20 @@ import static org.mockito.Mockito.when;
|
||||||
public class ResponseHighlightingInterceptorTest {
|
public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlightingInterceptorTest.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlightingInterceptorTest.class);
|
||||||
private static ResponseHighlighterInterceptor ourInterceptor = new ResponseHighlighterInterceptor();
|
private static final ResponseHighlighterInterceptor ourInterceptor = new ResponseHighlighterInterceptor();
|
||||||
private static Server ourServer;
|
private static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||||
|
private static Server ourServer;
|
||||||
private static CloseableHttpClient ourClient;
|
private static CloseableHttpClient ourClient;
|
||||||
private static FhirContext ourCtx = FhirContext.forR4();
|
|
||||||
private static int ourPort;
|
private static int ourPort;
|
||||||
private static RestfulServer ourServlet;
|
private static RestfulServer ourServlet;
|
||||||
|
private static DummyPatientResourceProvider ourPatientProvider = new DummyPatientResourceProvider();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
ourInterceptor.setShowRequestHeaders(new ResponseHighlighterInterceptor().isShowRequestHeaders());
|
ResponseHighlighterInterceptor defaults = new ResponseHighlighterInterceptor();
|
||||||
ourInterceptor.setShowResponseHeaders(new ResponseHighlighterInterceptor().isShowResponseHeaders());
|
ourInterceptor.setShowRequestHeaders(defaults.isShowRequestHeaders());
|
||||||
|
ourInterceptor.setShowResponseHeaders(defaults.isShowResponseHeaders());
|
||||||
|
ourInterceptor.setShowNarrative(defaults.isShowNarrative());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -189,8 +201,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDontHighlightWhenOriginHeaderPresent() throws Exception {
|
public void testDontHighlightWhenOriginHeaderPresent() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
when(req.getHeader(Constants.HEADER_ORIGIN)).thenAnswer(theInvocation -> "http://example.com");
|
when(req.getHeader(Constants.HEADER_ORIGIN)).thenAnswer(theInvocation -> "http://example.com");
|
||||||
|
@ -210,10 +220,86 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
// true means it decided to not handle the request..
|
// true means it decided to not handle the request..
|
||||||
assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertTrue(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_DomainResource() {
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addName().setFamily("Simpson");
|
||||||
|
patient.getText().setDivAsString("<div>HELLO</div>");
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), patient);
|
||||||
|
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">HELLO</div>", outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_NonDomainResource() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.TRANSACTION);
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), bundle);
|
||||||
|
assertNull(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_DocumentWithCompositionNarrative() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||||
|
Composition composition = new Composition();
|
||||||
|
composition.getText().setDivAsString("<div>HELLO</div>");
|
||||||
|
bundle.addEntry().setResource(composition);
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), bundle);
|
||||||
|
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">HELLO</div>", outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_ParametersWithNarrativeAsFirstParameter() {
|
||||||
|
Parameters parameters = new Parameters();
|
||||||
|
parameters.addParameter("Narrative", new StringType("<div>HELLO</div>"));
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), parameters);
|
||||||
|
assertEquals("<div xmlns=\"http://www.w3.org/1999/xhtml\">HELLO</div>", outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_Parameters() {
|
||||||
|
Parameters parameters = new Parameters();
|
||||||
|
parameters.addParameter("Foo", new StringType("<div>HELLO</div>"));
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), parameters);
|
||||||
|
assertNull(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_ParametersWithNonNarrativeFirstParameter_1() {
|
||||||
|
Parameters parameters = new Parameters();
|
||||||
|
parameters.addParameter("Narrative", new Quantity(123L));
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), parameters);
|
||||||
|
assertNull(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_ParametersWithNonNarrativeFirstParameter_2() {
|
||||||
|
Parameters parameters = new Parameters();
|
||||||
|
parameters.addParameter("Narrative", (Type)null);
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), parameters);
|
||||||
|
assertNull(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtractNarrativeHtml_ParametersWithNonNarrativeFirstParameter_3() {
|
||||||
|
Parameters parameters = new Parameters();
|
||||||
|
parameters.addParameter("Narrative", new StringType("hello"));
|
||||||
|
|
||||||
|
String outcome = ourInterceptor.extractNarrativeHtml(newRequest(), parameters);
|
||||||
|
assertNull(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testForceApplicationJson() throws Exception {
|
public void testForceApplicationJson() throws Exception {
|
||||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json");
|
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json");
|
||||||
|
@ -467,8 +553,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightException() throws Exception {
|
public void testHighlightException() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
|
||||||
|
@ -492,7 +576,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
ResourceNotFoundException exception = new ResourceNotFoundException("Not found");
|
ResourceNotFoundException exception = new ResourceNotFoundException("Not found");
|
||||||
exception.setOperationOutcome(new OperationOutcome().addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setDiagnostics("Hello")));
|
exception.setOperationOutcome(new OperationOutcome().addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setDiagnostics("Hello")));
|
||||||
|
|
||||||
assertFalse(ic.handleException(reqDetails, exception, req, resp));
|
assertFalse(ourInterceptor.handleException(reqDetails, exception, req, resp));
|
||||||
|
|
||||||
String output = sw.getBuffer().toString();
|
String output = sw.getBuffer().toString();
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
@ -526,14 +610,11 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See #346
|
* See #346
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightForceHtmlCt() throws Exception {
|
public void testHighlightForceHtmlCt() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir"));
|
||||||
|
|
||||||
|
@ -553,7 +634,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
// false means it decided to handle the request..
|
// false means it decided to handle the request..
|
||||||
assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertFalse(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -561,7 +642,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightForceHtmlFormat() throws Exception {
|
public void testHighlightForceHtmlFormat() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir"));
|
||||||
|
@ -582,13 +662,11 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
// false means it decided to handle the request..
|
// false means it decided to handle the request..
|
||||||
assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertFalse(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightForceRaw() throws Exception {
|
public void testHighlightForceRaw() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
|
||||||
|
@ -610,13 +688,12 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
// true means it decided to not handle the request..
|
// true means it decided to not handle the request..
|
||||||
assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertTrue(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightNormalResponse() throws Exception {
|
public void testHighlightNormalResponse() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
@ -636,7 +713,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServer(server);
|
reqDetails.setServer(server);
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertFalse(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
|
|
||||||
String output = sw.getBuffer().toString();
|
String output = sw.getBuffer().toString();
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
@ -647,8 +724,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightNormalResponseForcePrettyPrint() throws Exception {
|
public void testHighlightNormalResponseForcePrettyPrint() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
|
||||||
|
@ -669,7 +744,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServer(server);
|
reqDetails.setServer(server);
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertFalse(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
|
|
||||||
String output = sw.getBuffer().toString();
|
String output = sw.getBuffer().toString();
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
@ -682,8 +757,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightProducesDefaultJsonWithBrowserRequest() throws Exception {
|
public void testHighlightProducesDefaultJsonWithBrowserRequest() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
|
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
@ -703,7 +776,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServer(server);
|
reqDetails.setServer(server);
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertFalse(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
|
|
||||||
String output = sw.getBuffer().toString();
|
String output = sw.getBuffer().toString();
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
@ -712,8 +785,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHighlightProducesDefaultJsonWithBrowserRequest2() throws Exception {
|
public void testHighlightProducesDefaultJsonWithBrowserRequest2() throws Exception {
|
||||||
ResponseHighlighterInterceptor ic = ourInterceptor;
|
|
||||||
|
|
||||||
HttpServletRequest req = mock(HttpServletRequest.class);
|
HttpServletRequest req = mock(HttpServletRequest.class);
|
||||||
|
|
||||||
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html;q=0.8,application/xhtml+xml,application/xml;q=0.9"));
|
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html;q=0.8,application/xhtml+xml,application/xml;q=0.9"));
|
||||||
|
@ -734,7 +805,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
reqDetails.setServletRequest(req);
|
reqDetails.setServletRequest(req);
|
||||||
|
|
||||||
// True here means the interceptor didn't handle the request, because HTML wasn't the top ranked accept header
|
// True here means the interceptor didn't handle the request, because HTML wasn't the top ranked accept header
|
||||||
assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
assertTrue(ourInterceptor.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -871,6 +942,60 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
assertThat(responseContent, (containsStringIgnoringCase("Content-Type")));
|
assertThat(responseContent, (containsStringIgnoringCase("Content-Type")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNarrative() throws IOException {
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addName().setFamily("Simpson");
|
||||||
|
patient.getText().setDivAsString("<div><table><thead><tr><th>Header1</th><th>Header2</th></tr></thead><tr><td>A cell</td><td>A cell</td></tr><tr><td>A cell 2</td><td>A cell 2</td></tr></table></div>");
|
||||||
|
ourPatientProvider.myNextPatientOpResponse = patient;
|
||||||
|
|
||||||
|
String url = "http://localhost:" + ourPort + "/Patient/1/$patientOp?_format=html/json";
|
||||||
|
HttpGet httpGet = new HttpGet(url);
|
||||||
|
try (CloseableHttpResponse response = ourClient.execute(httpGet)) {
|
||||||
|
String resp = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
|
||||||
|
assertThat(resp, containsString("<h1>Narrative</h1>"));
|
||||||
|
assertThat(resp, containsString("<thead><tr><th>Header1</th><th>Header2</th></tr></thead>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNarrative_Disabled() throws IOException {
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addName().setFamily("Simpson");
|
||||||
|
patient.getText().setDivAsString("<div><table><thead><tr><th>Header1</th><th>Header2</th></tr></thead><tr><td>A cell</td><td>A cell</td></tr><tr><td>A cell 2</td><td>A cell 2</td></tr></table></div>");
|
||||||
|
ourPatientProvider.myNextPatientOpResponse = patient;
|
||||||
|
|
||||||
|
ourInterceptor.setShowNarrative(false);
|
||||||
|
|
||||||
|
String url = "http://localhost:" + ourPort + "/Patient/1/$patientOp?_format=html/json";
|
||||||
|
HttpGet httpGet = new HttpGet(url);
|
||||||
|
try (CloseableHttpResponse response = ourClient.execute(httpGet)) {
|
||||||
|
String resp = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
|
||||||
|
assertThat(resp, not(containsString("<h1>Narrative</h1>")));
|
||||||
|
assertThat(resp, not(containsString("<thead><tr><th>Header1</th><th>Header2</th></tr></thead>")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNarrative_SketchyTagBlocked() throws IOException {
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addName().setFamily("Simpson");
|
||||||
|
patient.getText().setDivAsString("<div><table onclick=\"foo();\"><thead><tr><th>Header1</th><th>Header2</th></tr></thead><tr><td>A cell</td><td>A cell</td></tr><tr><td>A cell 2</td><td>A cell 2</td></tr></table></div>");
|
||||||
|
ourPatientProvider.myNextPatientOpResponse = patient;
|
||||||
|
|
||||||
|
String url = "http://localhost:" + ourPort + "/Patient/1/$patientOp?_format=html/json";
|
||||||
|
HttpGet httpGet = new HttpGet(url);
|
||||||
|
try (CloseableHttpResponse response = ourClient.execute(httpGet)) {
|
||||||
|
String resp = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
|
||||||
|
assertThat(resp, not(containsString("<thead><tr><th>Header1</th><th>Header2</th></tr></thead>")));
|
||||||
|
assertThat(resp, containsString("Error at div/table: Found attribute table.onclick in a resource"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNullResponseResource() {
|
public void testNullResponseResource() {
|
||||||
ourInterceptor.setShowResponseHeaders(true);
|
ourInterceptor.setShowResponseHeaders(true);
|
||||||
|
@ -895,64 +1020,6 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
assertTrue(ourInterceptor.outgoingResponse(requestDetails, responseObject, servletRequest, servletResponse));
|
assertTrue(ourInterceptor.outgoingResponse(requestDetails, responseObject, servletRequest, servletResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
public static void afterClassClearContext() throws Exception {
|
|
||||||
JettyUtil.closeServer(ourServer);
|
|
||||||
TestUtil.randomizeLocaleAndTimezone();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class GraphQLProvider {
|
|
||||||
@GraphQL
|
|
||||||
public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQuery) {
|
|
||||||
return "{\"foo\":\"bar\"}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
public static void beforeClass() throws Exception {
|
|
||||||
ourServer = new Server(0);
|
|
||||||
|
|
||||||
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
|
|
||||||
|
|
||||||
ServletHandler proxyHandler = new ServletHandler();
|
|
||||||
ourServlet = new RestfulServer(ourCtx);
|
|
||||||
ourServlet.setDefaultResponseEncoding(EncodingEnum.XML);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Enable CORS
|
|
||||||
*/
|
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
|
||||||
CorsInterceptor corsInterceptor = new CorsInterceptor(config);
|
|
||||||
config.addAllowedHeader("Origin");
|
|
||||||
config.addAllowedHeader("Accept");
|
|
||||||
config.addAllowedHeader("X-Requested-With");
|
|
||||||
config.addAllowedHeader("Content-Type");
|
|
||||||
config.addAllowedHeader("Access-Control-Request-Method");
|
|
||||||
config.addAllowedHeader("Access-Control-Request-Headers");
|
|
||||||
config.addAllowedOrigin("*");
|
|
||||||
config.addExposedHeader("Location");
|
|
||||||
config.addExposedHeader("Content-Location");
|
|
||||||
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
|
||||||
ourServlet.registerInterceptor(corsInterceptor);
|
|
||||||
|
|
||||||
ourServlet.registerInterceptor(ourInterceptor);
|
|
||||||
ourServlet.registerProviders(patientProvider, new DummyBinaryResourceProvider(), new GraphQLProvider());
|
|
||||||
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
|
|
||||||
ServletHolder servletHolder = new ServletHolder(ourServlet);
|
|
||||||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
|
||||||
|
|
||||||
ourServer.setHandler(proxyHandler);
|
|
||||||
JettyUtil.startServer(ourServer);
|
|
||||||
ourPort = JettyUtil.getPortForStartedServer(ourServer);
|
|
||||||
|
|
||||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
|
||||||
HttpClientBuilder builder = HttpClientBuilder.create();
|
|
||||||
builder.setConnectionManager(connectionManager);
|
|
||||||
ourClient = builder.build();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestServletRequestDetails extends ServletRequestDetails {
|
class TestServletRequestDetails extends ServletRequestDetails {
|
||||||
TestServletRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
TestServletRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
||||||
super(theInterceptorBroadcaster);
|
super(theInterceptorBroadcaster);
|
||||||
|
@ -964,6 +1031,13 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class GraphQLProvider {
|
||||||
|
@GraphQL
|
||||||
|
public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQuery) {
|
||||||
|
return "{\"foo\":\"bar\"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class DummyBinaryResourceProvider implements IResourceProvider {
|
public static class DummyBinaryResourceProvider implements IResourceProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -978,7 +1052,7 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
if (theId.getIdPart().equals("html")) {
|
if (theId.getIdPart().equals("html")) {
|
||||||
retVal.setContent("<html>DATA</html>".getBytes(Charsets.UTF_8));
|
retVal.setContent("<html>DATA</html>".getBytes(Charsets.UTF_8));
|
||||||
retVal.setContentType("text/html");
|
retVal.setContentType("text/html");
|
||||||
}else {
|
} else {
|
||||||
retVal.setContent(new byte[]{1, 2, 3, 4});
|
retVal.setContent(new byte[]{1, 2, 3, 4});
|
||||||
retVal.setContentType(theId.getIdPart());
|
retVal.setContentType(theId.getIdPart());
|
||||||
}
|
}
|
||||||
|
@ -998,6 +1072,8 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
|
|
||||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||||
|
|
||||||
|
private Patient myNextPatientOpResponse;
|
||||||
|
|
||||||
private Patient createPatient1() {
|
private Patient createPatient1() {
|
||||||
Patient patient = new Patient();
|
Patient patient = new Patient();
|
||||||
patient.addIdentifier();
|
patient.addIdentifier();
|
||||||
|
@ -1031,14 +1107,14 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
return Collections.singletonList(p);
|
return Collections.singletonList(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(name="binaryOp", idempotent = true)
|
@Operation(name = "binaryOp", idempotent = true)
|
||||||
public Binary binaryOp(@IdParam IdType theId) {
|
public Binary binaryOp(@IdParam IdType theId) {
|
||||||
Binary retVal = new Binary();
|
Binary retVal = new Binary();
|
||||||
retVal.setId(theId);
|
retVal.setId(theId);
|
||||||
if (theId.getIdPart().equals("html")) {
|
if (theId.getIdPart().equals("html")) {
|
||||||
retVal.setContent("<html>DATA</html>".getBytes(Charsets.UTF_8));
|
retVal.setContent("<html>DATA</html>".getBytes(Charsets.UTF_8));
|
||||||
retVal.setContentType("text/html");
|
retVal.setContentType("text/html");
|
||||||
}else {
|
} else {
|
||||||
retVal.setContent(new byte[]{1, 2, 3, 4});
|
retVal.setContent(new byte[]{1, 2, 3, 4});
|
||||||
retVal.setContentType(theId.getIdPart());
|
retVal.setContentType(theId.getIdPart());
|
||||||
}
|
}
|
||||||
|
@ -1108,6 +1184,67 @@ public class ResponseHighlightingInterceptorTest {
|
||||||
return Collections.singletonList(p);
|
return Collections.singletonList(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(name = "patientOp", idempotent = true)
|
||||||
|
public Patient patientOp(@IdParam IIdType theId) {
|
||||||
|
return myNextPatientOpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void afterClassClearContext() throws Exception {
|
||||||
|
JettyUtil.closeServer(ourServer);
|
||||||
|
TestUtil.randomizeLocaleAndTimezone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeClass() throws Exception {
|
||||||
|
ourServer = new Server(0);
|
||||||
|
|
||||||
|
ServletHandler proxyHandler = new ServletHandler();
|
||||||
|
ourServlet = new RestfulServer(ourCtx);
|
||||||
|
ourServlet.setDefaultResponseEncoding(EncodingEnum.XML);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable CORS
|
||||||
|
*/
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
CorsInterceptor corsInterceptor = new CorsInterceptor(config);
|
||||||
|
config.addAllowedHeader("Origin");
|
||||||
|
config.addAllowedHeader("Accept");
|
||||||
|
config.addAllowedHeader("X-Requested-With");
|
||||||
|
config.addAllowedHeader("Content-Type");
|
||||||
|
config.addAllowedHeader("Access-Control-Request-Method");
|
||||||
|
config.addAllowedHeader("Access-Control-Request-Headers");
|
||||||
|
config.addAllowedOrigin("*");
|
||||||
|
config.addExposedHeader("Location");
|
||||||
|
config.addExposedHeader("Content-Location");
|
||||||
|
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
ourServlet.registerInterceptor(corsInterceptor);
|
||||||
|
|
||||||
|
ourServlet.registerInterceptor(ourInterceptor);
|
||||||
|
ourServlet.registerProviders(ourPatientProvider, new DummyBinaryResourceProvider(), new GraphQLProvider());
|
||||||
|
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
|
||||||
|
ServletHolder servletHolder = new ServletHolder(ourServlet);
|
||||||
|
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||||
|
|
||||||
|
ourServer.setHandler(proxyHandler);
|
||||||
|
JettyUtil.startServer(ourServer);
|
||||||
|
ourPort = JettyUtil.getPortForStartedServer(ourServer);
|
||||||
|
|
||||||
|
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||||
|
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||||
|
builder.setConnectionManager(connectionManager);
|
||||||
|
ourClient = builder.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static SystemRequestDetails newRequest() {
|
||||||
|
SystemRequestDetails retVal = new SystemRequestDetails();
|
||||||
|
retVal.setFhirContext(ourCtx);
|
||||||
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue