From 88e97800041fa0f9873c7272c7375c632d8cbc3c Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 10 Oct 2023 20:36:33 -0400 Subject: [PATCH] Add additional resource docs to generated OpenAPI documentation (#5359) * Add additional resource docs to generated OpenAPI documentation * Test fix --- .../5355-add-openapi-resource-details.yaml | 8 +++ .../fhir/rest/openapi/OpenApiInterceptor.java | 42 +++++++++++++- .../rest/openapi/OpenApiInterceptorTest.java | 55 +++++++++++++++++++ .../fhir/rest/server/util/NarrativeUtil.java | 24 ++++---- .../rest/server/util/NarrativeUtilTest.java | 26 ++++----- .../java/ca/uhn/fhir/to/BaseController.java | 2 +- 6 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5355-add-openapi-resource-details.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5355-add-openapi-resource-details.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5355-add-openapi-resource-details.yaml new file mode 100644 index 00000000000..3e784c3b80f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5355-add-openapi-resource-details.yaml @@ -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*." diff --git a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java index 783e082a795..b49b65f39bd 100644 --- a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java +++ b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java @@ -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("
"); + b.append(sanitizeHtmlFragment(renderMarkdown(documentation))); + } + + if (isNotBlank(theResource.getProfile())) { + b.append("
"); + b.append("Base profile: "); + b.append(sanitizeHtmlFragment(theResource.getProfile())); + } + + for (CanonicalType next : theResource.getSupportedProfile()) { + String nextSupportedProfile = next.getValueAsString(); + if (isNotBlank(nextSupportedProfile)) { + b.append("
"); + b.append("Supported profile: "); + b.append(sanitizeHtmlFragment(nextSupportedProfile)); + } + } + + return b.toString(); + } + protected void addSearchOperation( final OpenAPI openApi, final Operation operation, diff --git a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java index 2e0b0ac5067..441a656b826 100644 --- a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java +++ b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java @@ -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 myConsumer; + + public CapabilityStatementEnhancingInterceptor(Consumer 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 { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/NarrativeUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/NarrativeUtil.java index bb7b89c0516..eb7ab4d9bfb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/NarrativeUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/NarrativeUtil.java @@ -47,10 +47,16 @@ public class NarrativeUtil { *
  • All other elements and attributes are removed
  • * */ - 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); diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/NarrativeUtilTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/NarrativeUtilTest.java index 7278c25f8af..a8bc5d34a46 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/NarrativeUtilTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/NarrativeUtilTest.java @@ -9,21 +9,21 @@ public class NarrativeUtilTest { @ParameterizedTest @CsvSource({ - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "
    hello
    ,
    hello
    ", - "hello ,
    hello
    ", - "empty , null", - "null , null" + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "
    hello
    ,
    hello
    ", + "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) { diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java index 0b9d14f3162..49c2f4c4ea5 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java @@ -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);