Add additional resource docs to generated OpenAPI documentation (#5359)
* Add additional resource docs to generated OpenAPI documentation * Test fix
This commit is contained in:
parent
424f26b897
commit
88e9780004
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
type: add
|
||||
issue: 5355
|
||||
title: "The generated OpenAPI documentation produced by OpenApiInterceptor will now include
|
||||
additional details in the individual resource type documentation, including the values of
|
||||
*CapabilityStatement.rest.resource.documentation*,
|
||||
*CapabilityStatement.rest.resource.profile*, and
|
||||
*CapabilityStatement.rest.resource.supportedProfile*."
|
|
@ -64,6 +64,7 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50;
|
|||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.CanonicalType;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
|
@ -108,10 +109,12 @@ import java.util.Properties;
|
|||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import static ca.uhn.fhir.rest.server.util.NarrativeUtil.sanitizeHtmlFragment;
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
@ -356,7 +359,7 @@ public class OpenApiInterceptor {
|
|||
|
||||
String copyright = cs.getCopyright();
|
||||
if (isNotBlank(copyright)) {
|
||||
copyright = myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
|
||||
copyright = renderMarkdown(copyright);
|
||||
context.setVariable("COPYRIGHT_HTML", copyright);
|
||||
}
|
||||
|
||||
|
@ -411,6 +414,11 @@ public class OpenApiInterceptor {
|
|||
theResponse.getWriter().close();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String renderMarkdown(String copyright) {
|
||||
return myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
|
||||
}
|
||||
|
||||
protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) {
|
||||
theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", "");
|
||||
}
|
||||
|
@ -515,7 +523,7 @@ public class OpenApiInterceptor {
|
|||
|
||||
Tag resourceTag = new Tag();
|
||||
resourceTag.setName(resourceType);
|
||||
resourceTag.setDescription("The " + resourceType + " FHIR resource type");
|
||||
resourceTag.setDescription(createResourceDescription(nextResource));
|
||||
openApi.addTagsItem(resourceTag);
|
||||
|
||||
// Instance Read
|
||||
|
@ -624,6 +632,36 @@ public class OpenApiInterceptor {
|
|||
return openApi;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected String createResourceDescription(
|
||||
CapabilityStatement.CapabilityStatementRestResourceComponent theResource) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append("The ").append(theResource.getType()).append(" FHIR resource type");
|
||||
|
||||
String documentation = theResource.getDocumentation();
|
||||
if (isNotBlank(documentation)) {
|
||||
b.append("<br/>");
|
||||
b.append(sanitizeHtmlFragment(renderMarkdown(documentation)));
|
||||
}
|
||||
|
||||
if (isNotBlank(theResource.getProfile())) {
|
||||
b.append("<br/>");
|
||||
b.append("Base profile: ");
|
||||
b.append(sanitizeHtmlFragment(theResource.getProfile()));
|
||||
}
|
||||
|
||||
for (CanonicalType next : theResource.getSupportedProfile()) {
|
||||
String nextSupportedProfile = next.getValueAsString();
|
||||
if (isNotBlank(nextSupportedProfile)) {
|
||||
b.append("<br/>");
|
||||
b.append("Supported profile: ");
|
||||
b.append(sanitizeHtmlFragment(nextSupportedProfile));
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
protected void addSearchOperation(
|
||||
final OpenAPI openApi,
|
||||
final Operation operation,
|
||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.openapi;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.model.api.annotation.Description;
|
||||
import ca.uhn.fhir.rest.annotation.*;
|
||||
|
@ -32,16 +33,19 @@ import org.apache.http.client.methods.HttpGet;
|
|||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.*;
|
||||
import org.hl7.fhir.r5.model.ActorDefinition;
|
||||
import org.hl7.fhir.r5.model.CapabilityStatement;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
@ -83,6 +87,28 @@ public class OpenApiInterceptorTest {
|
|||
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testResourceDocsCopied() throws IOException {
|
||||
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition"));
|
||||
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
|
||||
myServer.registerInterceptor(new CapabilityStatementEnhancingInterceptor(cs->{
|
||||
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = findPatientResource(cs);
|
||||
patientResource.setProfile("http://baseProfile");
|
||||
patientResource.addSupportedProfile("http://foo");
|
||||
patientResource.addSupportedProfile("http://bar");
|
||||
patientResource.setDocumentation("This is **bolded** documentation");
|
||||
}));
|
||||
|
||||
org.hl7.fhir.r4.model.CapabilityStatement cs = myServer.getFhirClient().capabilities().ofType(org.hl7.fhir.r4.model.CapabilityStatement.class).execute();
|
||||
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = findPatientResource(cs);
|
||||
assertEquals("This is **bolded** documentation", patientResource.getDocumentation());
|
||||
|
||||
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
|
||||
String resp = fetchSwaggerUi(url);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
@ -111,6 +137,35 @@ public class OpenApiInterceptorTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Interceptor
|
||||
private static class CapabilityStatementEnhancingInterceptor {
|
||||
|
||||
private final Consumer<org.hl7.fhir.r4.model.CapabilityStatement> myConsumer;
|
||||
|
||||
public CapabilityStatementEnhancingInterceptor(Consumer<org.hl7.fhir.r4.model.CapabilityStatement> theConsumer) {
|
||||
myConsumer = theConsumer;
|
||||
}
|
||||
|
||||
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
|
||||
public void massageCapabilityStatement(IBaseConformance theCs) {
|
||||
myConsumer.accept((org.hl7.fhir.r4.model.CapabilityStatement) theCs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent findPatientResource(org.hl7.fhir.r4.model.CapabilityStatement theCs) {
|
||||
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = theCs
|
||||
.getRest()
|
||||
.get(0)
|
||||
.getResource()
|
||||
.stream()
|
||||
.filter(t -> "Patient".equals(t.getType()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
return patientResource;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||
abstract static class BaseOpenApiInterceptorTest {
|
||||
|
|
|
@ -47,10 +47,16 @@ public class NarrativeUtil {
|
|||
* <li>All other elements and attributes are removed</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static String sanitize(String theHtml) {
|
||||
XhtmlNode node = new XhtmlNode();
|
||||
node.setValueAsString(theHtml);
|
||||
return sanitize(node).getValueAsString();
|
||||
public static String sanitizeHtmlFragment(String theHtml) {
|
||||
PolicyFactory idPolicy =
|
||||
new HtmlPolicyBuilder().allowAttributes("id").globally().toFactory();
|
||||
|
||||
PolicyFactory policy = Sanitizers.FORMATTING
|
||||
.and(Sanitizers.BLOCKS)
|
||||
.and(Sanitizers.TABLES)
|
||||
.and(Sanitizers.STYLES)
|
||||
.and(idPolicy);
|
||||
return policy.sanitize(theHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,15 +76,7 @@ public class NarrativeUtil {
|
|||
public static XhtmlNode sanitize(XhtmlNode theNode) {
|
||||
String html = theNode.getValueAsString();
|
||||
|
||||
PolicyFactory idPolicy =
|
||||
new HtmlPolicyBuilder().allowAttributes("id").globally().toFactory();
|
||||
|
||||
PolicyFactory policy = Sanitizers.FORMATTING
|
||||
.and(Sanitizers.BLOCKS)
|
||||
.and(Sanitizers.TABLES)
|
||||
.and(Sanitizers.STYLES)
|
||||
.and(idPolicy);
|
||||
String safeHTML = policy.sanitize(html);
|
||||
String safeHTML = sanitizeHtmlFragment(html);
|
||||
|
||||
XhtmlNode retVal = new XhtmlNode();
|
||||
retVal.setValueAsString(safeHTML);
|
||||
|
|
|
@ -9,21 +9,21 @@ public class NarrativeUtilTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"<div><SPAN ID=\"foo\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
|
||||
"<div><span id=\"foo\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
|
||||
"<div><SPAN ONCLICK=\"hello()\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
||||
"<div><span onclick=\"hello()\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
||||
"<div><a href=\"http://goodbye\">hello</a></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
||||
"<div><table><tr><td>hello</td></tr></table></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><table><tbody><tr><td>hello</td></tr></tbody></table></div>",
|
||||
"<div><span style=\"font-size: 100px;\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span style=\"font-size:100px\">hello</span></div>",
|
||||
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
||||
"hello , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
||||
"empty , null",
|
||||
"null , null"
|
||||
"<div><SPAN ID=\"foo\">hello</SPAN></div> , <div><span id=\"foo\">hello</span></div>",
|
||||
"<div><span id=\"foo\">hello</span></div> , <div><span id=\"foo\">hello</span></div>",
|
||||
"<div><SPAN ONCLICK=\"hello()\">hello</SPAN></div> , <div>hello</div>",
|
||||
"<div><span onclick=\"hello()\">hello</span></div> , <div>hello</div>",
|
||||
"<div><a href=\"http://goodbye\">hello</a></div> , <div>hello</div>",
|
||||
"<div><table><tr><td>hello</td></tr></table></div> , <div><table><tbody><tr><td>hello</td></tr></tbody></table></div>",
|
||||
"<div><span style=\"font-size: 100px;\">hello</span></div> , <div><span style=\"font-size:100px\">hello</span></div>",
|
||||
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div>hello</div>",
|
||||
"hello , hello",
|
||||
"empty , empty",
|
||||
"null , empty"
|
||||
})
|
||||
public void testValidateIsCaseInsensitive(String theHtml, String theExpected) {
|
||||
String output = NarrativeUtil.sanitize(fixNull(theHtml));
|
||||
assertEquals(fixNull(theExpected), output);
|
||||
String output = NarrativeUtil.sanitizeHtmlFragment(fixNull(theHtml));
|
||||
assertEquals(fixNull(theExpected), fixNull(output));
|
||||
}
|
||||
|
||||
private String fixNull(String theExpected) {
|
||||
|
|
|
@ -583,7 +583,7 @@ public class BaseController {
|
|||
theModelMap.put("resultBodyIsLong", resultBodyText.length() > 1000);
|
||||
theModelMap.put("requestHeaders", requestHeaders);
|
||||
theModelMap.put("responseHeaders", responseHeaders);
|
||||
theModelMap.put("narrative", NarrativeUtil.sanitize(narrativeString));
|
||||
theModelMap.put("narrative", NarrativeUtil.sanitizeHtmlFragment(narrativeString));
|
||||
theModelMap.put("latencyMs", theLatency);
|
||||
|
||||
theModelMap.put("config", myConfig);
|
||||
|
|
Loading…
Reference in New Issue