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.IBaseConformance;
|
||||||
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.r4.model.CanonicalType;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
import org.hl7.fhir.r4.model.Coding;
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
@ -108,10 +109,12 @@ import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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.ObjectUtils.defaultIfNull;
|
||||||
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;
|
||||||
|
@ -356,7 +359,7 @@ public class OpenApiInterceptor {
|
||||||
|
|
||||||
String copyright = cs.getCopyright();
|
String copyright = cs.getCopyright();
|
||||||
if (isNotBlank(copyright)) {
|
if (isNotBlank(copyright)) {
|
||||||
copyright = myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
|
copyright = renderMarkdown(copyright);
|
||||||
context.setVariable("COPYRIGHT_HTML", copyright);
|
context.setVariable("COPYRIGHT_HTML", copyright);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,6 +414,11 @@ public class OpenApiInterceptor {
|
||||||
theResponse.getWriter().close();
|
theResponse.getWriter().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String renderMarkdown(String copyright) {
|
||||||
|
return myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
|
||||||
|
}
|
||||||
|
|
||||||
protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) {
|
protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) {
|
||||||
theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", "");
|
theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", "");
|
||||||
}
|
}
|
||||||
|
@ -515,7 +523,7 @@ public class OpenApiInterceptor {
|
||||||
|
|
||||||
Tag resourceTag = new Tag();
|
Tag resourceTag = new Tag();
|
||||||
resourceTag.setName(resourceType);
|
resourceTag.setName(resourceType);
|
||||||
resourceTag.setDescription("The " + resourceType + " FHIR resource type");
|
resourceTag.setDescription(createResourceDescription(nextResource));
|
||||||
openApi.addTagsItem(resourceTag);
|
openApi.addTagsItem(resourceTag);
|
||||||
|
|
||||||
// Instance Read
|
// Instance Read
|
||||||
|
@ -624,6 +632,36 @@ public class OpenApiInterceptor {
|
||||||
return openApi;
|
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(
|
protected void addSearchOperation(
|
||||||
final OpenAPI openApi,
|
final OpenAPI openApi,
|
||||||
final Operation operation,
|
final Operation operation,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.openapi;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
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.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.model.api.annotation.Description;
|
import ca.uhn.fhir.model.api.annotation.Description;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import ca.uhn.fhir.rest.annotation.*;
|
||||||
|
@ -32,16 +33,19 @@ import org.apache.http.client.methods.HttpGet;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.hl7.fhir.instance.model.api.*;
|
import org.hl7.fhir.instance.model.api.*;
|
||||||
import org.hl7.fhir.r5.model.ActorDefinition;
|
import org.hl7.fhir.r5.model.ActorDefinition;
|
||||||
|
import org.hl7.fhir.r5.model.CapabilityStatement;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.not;
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
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"));
|
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
|
@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")
|
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||||
abstract static class BaseOpenApiInterceptorTest {
|
abstract static class BaseOpenApiInterceptorTest {
|
||||||
|
|
|
@ -47,10 +47,16 @@ public class NarrativeUtil {
|
||||||
* <li>All other elements and attributes are removed</li>
|
* <li>All other elements and attributes are removed</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public static String sanitize(String theHtml) {
|
public static String sanitizeHtmlFragment(String theHtml) {
|
||||||
XhtmlNode node = new XhtmlNode();
|
PolicyFactory idPolicy =
|
||||||
node.setValueAsString(theHtml);
|
new HtmlPolicyBuilder().allowAttributes("id").globally().toFactory();
|
||||||
return sanitize(node).getValueAsString();
|
|
||||||
|
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) {
|
public static XhtmlNode sanitize(XhtmlNode theNode) {
|
||||||
String html = theNode.getValueAsString();
|
String html = theNode.getValueAsString();
|
||||||
|
|
||||||
PolicyFactory idPolicy =
|
String safeHTML = sanitizeHtmlFragment(html);
|
||||||
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);
|
|
||||||
|
|
||||||
XhtmlNode retVal = new XhtmlNode();
|
XhtmlNode retVal = new XhtmlNode();
|
||||||
retVal.setValueAsString(safeHTML);
|
retVal.setValueAsString(safeHTML);
|
||||||
|
|
|
@ -9,21 +9,21 @@ public class NarrativeUtilTest {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@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><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 id=\"foo\">hello</span></div> , <div><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>hello</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>hello</div>",
|
||||||
"<div><a href=\"http://goodbye\">hello</a></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
"<div><a href=\"http://goodbye\">hello</a></div> , <div>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><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 xmlns=\"http://www.w3.org/1999/xhtml\"><span style=\"font-size:100px\">hello</span></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 xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div>hello</div>",
|
||||||
"hello , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
|
"hello , hello",
|
||||||
"empty , null",
|
"empty , empty",
|
||||||
"null , null"
|
"null , empty"
|
||||||
})
|
})
|
||||||
public void testValidateIsCaseInsensitive(String theHtml, String theExpected) {
|
public void testValidateIsCaseInsensitive(String theHtml, String theExpected) {
|
||||||
String output = NarrativeUtil.sanitize(fixNull(theHtml));
|
String output = NarrativeUtil.sanitizeHtmlFragment(fixNull(theHtml));
|
||||||
assertEquals(fixNull(theExpected), output);
|
assertEquals(fixNull(theExpected), fixNull(output));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fixNull(String theExpected) {
|
private String fixNull(String theExpected) {
|
||||||
|
|
|
@ -583,7 +583,7 @@ public class BaseController {
|
||||||
theModelMap.put("resultBodyIsLong", resultBodyText.length() > 1000);
|
theModelMap.put("resultBodyIsLong", resultBodyText.length() > 1000);
|
||||||
theModelMap.put("requestHeaders", requestHeaders);
|
theModelMap.put("requestHeaders", requestHeaders);
|
||||||
theModelMap.put("responseHeaders", responseHeaders);
|
theModelMap.put("responseHeaders", responseHeaders);
|
||||||
theModelMap.put("narrative", NarrativeUtil.sanitize(narrativeString));
|
theModelMap.put("narrative", NarrativeUtil.sanitizeHtmlFragment(narrativeString));
|
||||||
theModelMap.put("latencyMs", theLatency);
|
theModelMap.put("latencyMs", theLatency);
|
||||||
|
|
||||||
theModelMap.put("config", myConfig);
|
theModelMap.put("config", myConfig);
|
||||||
|
|
Loading…
Reference in New Issue