diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5115-fix-openapi-for-r5.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5115-fix-openapi-for-r5.yaml new file mode 100644 index 00000000000..fa79f906b1f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5115-fix-openapi-for-r5.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 5115 +title: "OpenAPI definitions were not working for R5 JPA servers. This has been corrected." diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 954277e4aa0..1310d658df0 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -71,6 +71,12 @@ ${project.version} test + + ca.uhn.hapi.fhir + hapi-fhir-structures-r5 + ${project.version} + test + ch.qos.logback logback-classic 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 4686555cbcf..c43684f8dac 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 @@ -908,11 +908,13 @@ public class OpenApiInterceptor { break; case "Resource": if (theResourceType != null) { - IBaseResource resource = FHIR_CONTEXT_CANONICAL - .getResourceDefinition(theResourceType) - .newInstance(); - resource.setId("1"); - param.setResource((Resource) resource); + if (FHIR_CONTEXT_CANONICAL.getResourceTypes().contains(theResourceType)) { + IBaseResource resource = FHIR_CONTEXT_CANONICAL + .getResourceDefinition(theResourceType) + .newInstance(); + resource.setId("1"); + param.setResource((Resource) resource); + } } break; } @@ -1002,7 +1004,7 @@ public class OpenApiInterceptor { } return () -> { IBaseResource example = null; - if (theResourceType != null) { + if (theResourceType != null && theFhirContext.getResourceTypes().contains(theResourceType)) { example = theFhirContext.getResourceDefinition(theResourceType).newInstance(); } return example; 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 f4cffe4d184..2e0b0ac5067 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 @@ -4,20 +4,19 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.model.api.annotation.Description; -import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.annotation.Patch; -import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.test.utilities.HtmlUtil; +import ca.uhn.fhir.test.utilities.HttpClientExtension; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.ExtensionConstants; import com.gargoylesoftware.htmlunit.html.DomElement; @@ -30,24 +29,10 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.hamcrest.Matchers; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseCoding; -import org.hl7.fhir.instance.model.api.IBaseConformance; -import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.CapabilityStatement; -import org.hl7.fhir.r4.model.DecimalType; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; +import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.r5.model.ActorDefinition; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,298 +41,341 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; +import java.util.*; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class OpenApiInterceptorTest { private static final Logger ourLog = LoggerFactory.getLogger(OpenApiInterceptorTest.class); - private final FhirContext myFhirContext = FhirContext.forR4Cached(); - @RegisterExtension - @Order(0) - protected RestfulServerExtension myServer = new RestfulServerExtension(myFhirContext) - .withServletPath("/fhir/*") - .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))) - .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class))) - .withServer(t -> t.registerProvider(new MyLastNProvider())) - .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor())); - private CloseableHttpClient myClient; - @BeforeEach - public void before() { - myClient = HttpClientBuilder.create().build(); - } + @Nested + class R4 extends BaseOpenApiInterceptorTest { - @AfterEach - public void after() throws IOException { - myClient.close(); - myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); - } - - @Test - public void testFetchSwagger() throws IOException { - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - - String resp; - HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/metadata?_pretty=true"); - try (CloseableHttpResponse response = myClient.execute(get)) { - resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info("CapabilityStatement: {}", resp); + @Override + FhirContext getContext() { + return FhirContext.forR4Cached(); } - get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs"); - try (CloseableHttpResponse response = myClient.execute(get)) { - resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info("Response: {}", response.getStatusLine()); - ourLog.debug("Response: {}", resp); + @Test + public void testSwaggerUiWithResourceCounts() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + List buttonTexts = parsePageButtonTexts(resp, url); + assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "Patient 2", "OperationDefinition 1", "Observation 0")); } - OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class); + @Test + public void testSwaggerUiWithResourceCounts_OneResourceOnly() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition")); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - PathItem fooOpPath = parsed.getPaths().get("/$foo-op"); - assertNull(fooOpPath.getGet()); - assertNotNull(fooOpPath.getPost()); - assertEquals("Foo Op Description", fooOpPath.getPost().getDescription()); - assertEquals("Foo Op Short", fooOpPath.getPost().getSummary()); - - PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn"); - assertNotNull(lastNPath.getPost()); - assertEquals("LastN Description", lastNPath.getPost().getDescription()); - assertEquals("LastN Short", lastNPath.getPost().getSummary()); - assertNull(lastNPath.getPost().getParameters()); - assertNotNull(lastNPath.getPost().getRequestBody()); - assertNotNull(lastNPath.getGet()); - assertEquals("LastN Description", lastNPath.getGet().getDescription()); - assertEquals("LastN Short", lastNPath.getGet().getSummary()); - assertEquals(4, lastNPath.getGet().getParameters().size()); - assertEquals("Subject description", lastNPath.getGet().getParameters().get(0).getDescription()); - } - - @Test - public void testRedirectFromBaseUrl() throws IOException { - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - - HttpGet get; - - get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); - try (CloseableHttpResponse response = myClient.execute(get)) { - assertEquals(400, response.getStatusLine().getStatusCode()); - } - - get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); - get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); - try (CloseableHttpResponse response = myClient.execute(get)) { - String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info("Response: {}", response); - ourLog.info("Response: {}", responseString); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertThat(responseString, containsString("Swagger UI")); - } - - get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/?foo=foo"); - get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); - try (CloseableHttpResponse response = myClient.execute(get)) { - assertEquals(400, response.getStatusLine().getStatusCode()); - } - - get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir?foo=foo"); - get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); - try (CloseableHttpResponse response = myClient.execute(get)) { - assertEquals(400, response.getStatusLine().getStatusCode()); + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + List buttonTexts = parsePageButtonTexts(resp, url); + assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient")); } } + @Nested + class R5 extends BaseOpenApiInterceptorTest { - @Test - public void testSwaggerUiWithResourceCounts() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + /** + * A provider that uses a resource type not present in R4 + */ + private MyTypeLevelActorDefinitionProviderR5 myActorDefinitionProvider = new MyTypeLevelActorDefinitionProviderR5(); - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - List buttonTexts = parsePageButtonTexts(resp, url); - assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "Patient 2", "OperationDefinition 1", "Observation 0")); + @BeforeEach + void beforeEach() { + myServer.registerProvider(myActorDefinitionProvider); + ServerCapabilityStatementProvider a = (ServerCapabilityStatementProvider) myServer.getRestfulServer().getServerConformanceProvider(); + myServer.getRestfulServer().getServerConformanceProvider(); + } + + @AfterEach + void afterEach() { + myServer.unregisterProvider(myActorDefinitionProvider); + } + + @Override + FhirContext getContext() { + return FhirContext.forR5Cached(); + } } - @Test - public void testSwaggerUiWithCopyright() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - assertThat(resp, resp, containsString("

This server is copyright Example Org 2021

")); - assertThat(resp, resp, not(containsString("swagger-ui-custom.css"))); - } + @SuppressWarnings("JUnitMalformedDeclaration") + abstract static class BaseOpenApiInterceptorTest { - @Test - public void testSwaggerUiWithNoBannerUrl() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor().setBannerImage("")); + @RegisterExtension + @Order(0) + protected RestfulServerExtension myServer = new RestfulServerExtension(getContext()) + .withServletPath("/fhir/*") + .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(getContext(), getContext().getResourceDefinition("Patient").getImplementingClass()))) + .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(getContext(), getContext().getResourceDefinition("Observation").getImplementingClass()))) + .withServer(t -> t.registerProvider(new MySystemLevelOperationProvider())) + .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor())); + @RegisterExtension + private HttpClientExtension myClient = new HttpClientExtension(); - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - assertThat(resp, resp, not(containsString("img id=\"banner_img\""))); - } + abstract FhirContext getContext(); - @Test - public void testSwaggerUiWithCustomStylesheet() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + @AfterEach + public void after() { + myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); + } - OpenApiInterceptor interceptor = new OpenApiInterceptor(); - interceptor.setCssText("BODY {\nfont-size: 1.1em;\n}"); - myServer.getRestfulServer().registerInterceptor(interceptor); + @Test + public void testFetchSwagger() throws IOException { + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - // Fetch Swagger UI HTML - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - assertThat(resp, resp, containsString("")); - - // Fetch Custom CSS - url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/swagger-ui-custom.css"; - resp = fetchSwaggerUi(url); - String expected = """ - BODY { - font-size: 1.1em; + String resp; + HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/metadata?_pretty=true"); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("CapabilityStatement: {}", resp); } - """; - assertEquals(removeCtrlR(expected), removeCtrlR(resp)); - } - protected String removeCtrlR (String source) { - String result = source; - if (source != null) { - result = StringUtils.remove(source, '\r'); - } - return result; - } - - @Test - public void testSwaggerUiNotPaged() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs"); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response.getStatusLine()); + ourLog.debug("Response: {}", resp); + } - OpenApiInterceptor interceptor = new OpenApiInterceptor(); - interceptor.setUseResourcePages(false); - myServer.getRestfulServer().registerInterceptor(interceptor); + OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class); - // Fetch Swagger UI HTML - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - List buttonTexts = parsePageButtonTexts(resp, url); - assertThat(buttonTexts.toString(), buttonTexts, empty()); - } + PathItem fooOpPath = parsed.getPaths().get("/$foo-op"); + assertNull(fooOpPath.getGet()); + assertNotNull(fooOpPath.getPost()); + assertEquals("Foo Op Description", fooOpPath.getPost().getDescription()); + assertEquals("Foo Op Short", fooOpPath.getPost().getSummary()); - @Test - public void testSwaggerUiWithResourceCounts_OneResourceOnly() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition")); - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - - String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; - String resp = fetchSwaggerUi(url); - List buttonTexts = parsePageButtonTexts(resp, url); - assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient")); - } - - @Test - public void testRemoveTrailingSlash() { - OpenApiInterceptor interceptor = new OpenApiInterceptor(); - String url1 = interceptor.removeTrailingSlash("http://localhost:8000"); - String url2 = interceptor.removeTrailingSlash("http://localhost:8000/"); - String url3 = interceptor.removeTrailingSlash("http://localhost:8000//"); - String expect = "http://localhost:8000"; - assertEquals(expect, url1); - assertEquals(expect, url2); - assertEquals(expect, url3); - } - - @Test - public void testRemoveTrailingSlashWithNullUrl() { - OpenApiInterceptor interceptor = new OpenApiInterceptor(); - String url = interceptor.removeTrailingSlash(null); - assertEquals(null, url); - } - - @Test - public void testStandardRedirectScriptIsAccessible() throws IOException { - myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); - myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - - HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/oauth2-redirect.html"); - try (CloseableHttpResponse response = myClient.execute(get)) { - assertEquals(200, response.getStatusLine().getStatusCode()); - } - } - - private String fetchSwaggerUi(String url) throws IOException { - String resp; - HttpGet get = new HttpGet(url); - try (CloseableHttpResponse response = myClient.execute(get)) { - resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info("Response: {}", response.getStatusLine()); - ourLog.debug("Response: {}", resp); - } - return resp; - } - - private List parsePageButtonTexts(String resp, String url) throws IOException { - HtmlPage html = HtmlUtil.parseAsHtml(resp, new URL(url)); - HtmlDivision pageButtons = (HtmlDivision) html.getElementById("pageButtons"); - if (pageButtons == null) { - return Collections.emptyList(); + PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn"); + assertNotNull(lastNPath.getPost()); + assertEquals("LastN Description", lastNPath.getPost().getDescription()); + assertEquals("LastN Short", lastNPath.getPost().getSummary()); + assertNull(lastNPath.getPost().getParameters()); + assertNotNull(lastNPath.getPost().getRequestBody()); + assertNotNull(lastNPath.getGet()); + assertEquals("LastN Description", lastNPath.getGet().getDescription()); + assertEquals("LastN Short", lastNPath.getGet().getSummary()); + assertEquals(4, lastNPath.getGet().getParameters().size()); + assertEquals("Subject description", lastNPath.getGet().getParameters().get(0).getDescription()); } - List buttonTexts = new ArrayList<>(); - for (DomElement next : pageButtons.getChildElements()) { - buttonTexts.add(next.asNormalizedText()); - } - return buttonTexts; - } + @Test + public void testRedirectFromBaseUrl() throws IOException { + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + HttpGet get; - public static class AddResourceCountsInterceptor { + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); + try (CloseableHttpResponse response = myClient.execute(get)) { + assertEquals(400, response.getStatusLine().getStatusCode()); + } - private final HashSet myResourceNamesToAddTo; + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); + get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); + try (CloseableHttpResponse response = myClient.execute(get)) { + String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response); + ourLog.info("Response: {}", responseString); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(responseString, containsString("Swagger UI")); + } + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/?foo=foo"); + get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); + try (CloseableHttpResponse response = myClient.execute(get)) { + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir?foo=foo"); + get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); + try (CloseableHttpResponse response = myClient.execute(get)) { + assertEquals(400, response.getStatusLine().getStatusCode()); + } - public AddResourceCountsInterceptor(String... theResourceNamesToAddTo) { - myResourceNamesToAddTo = new HashSet<>(Arrays.asList(theResourceNamesToAddTo)); } - @Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED) - public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) { - CapabilityStatement cs = (CapabilityStatement) theCapabilityStatement; - cs.setCopyright("This server is copyright **Example Org** 2021"); + @Test + public void testSwaggerUiWithCopyright() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); - int numResources = cs.getRestFirstRep().getResource().size(); - for (int i = 0; i < numResources; i++) { + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + assertThat(resp, resp, containsString("

This server is copyright Example Org 2021

")); + assertThat(resp, resp, not(containsString("swagger-ui-custom.css"))); + } - CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(i); - if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) { - continue; + @Test + public void testSwaggerUiWithNoBannerUrl() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor().setBannerImage("")); + + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + assertThat(resp, resp, not(containsString("img id=\"banner_img\""))); + } + + @Test + public void testSwaggerUiWithCustomStylesheet() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + + OpenApiInterceptor interceptor = new OpenApiInterceptor(); + interceptor.setCssText("BODY {\nfont-size: 1.1em;\n}"); + myServer.getRestfulServer().registerInterceptor(interceptor); + + // Fetch Swagger UI HTML + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + assertThat(resp, resp, containsString("")); + + // Fetch Custom CSS + url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/swagger-ui-custom.css"; + resp = fetchSwaggerUi(url); + String expected = """ + BODY { + font-size: 1.1em; } + """; + assertEquals(removeCtrlR(expected), removeCtrlR(resp)); + } - restResource.addExtension( - ExtensionConstants.CONF_RESOURCE_COUNT, - new DecimalType(i) // reverse order - ); + protected String removeCtrlR(String source) { + String result = source; + if (source != null) { + result = StringUtils.remove(source, '\r'); + } + return result; + } + @Test + public void testSwaggerUiNotPaged() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + + OpenApiInterceptor interceptor = new OpenApiInterceptor(); + interceptor.setUseResourcePages(false); + myServer.getRestfulServer().registerInterceptor(interceptor); + + // Fetch Swagger UI HTML + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + List buttonTexts = parsePageButtonTexts(resp, url); + assertThat(buttonTexts.toString(), buttonTexts, empty()); + } + + @Test + public void testRemoveTrailingSlash() { + OpenApiInterceptor interceptor = new OpenApiInterceptor(); + String url1 = interceptor.removeTrailingSlash("http://localhost:8000"); + String url2 = interceptor.removeTrailingSlash("http://localhost:8000/"); + String url3 = interceptor.removeTrailingSlash("http://localhost:8000//"); + String expect = "http://localhost:8000"; + assertEquals(expect, url1); + assertEquals(expect, url2); + assertEquals(expect, url3); + } + + @Test + public void testRemoveTrailingSlashWithNullUrl() { + OpenApiInterceptor interceptor = new OpenApiInterceptor(); + String url = interceptor.removeTrailingSlash(null); + assertEquals(null, url); + } + + @Test + public void testStandardRedirectScriptIsAccessible() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/oauth2-redirect.html"); + try (CloseableHttpResponse response = myClient.execute(get)) { + assertEquals(200, response.getStatusLine().getStatusCode()); } } + protected String fetchSwaggerUi(String url) throws IOException { + String resp; + HttpGet get = new HttpGet(url); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response.getStatusLine()); + ourLog.debug("Response: {}", resp); + } + return resp; + } + + protected List parsePageButtonTexts(String resp, String url) throws IOException { + HtmlPage html = HtmlUtil.parseAsHtml(resp, new URL(url)); + HtmlDivision pageButtons = (HtmlDivision) html.getElementById("pageButtons"); + if (pageButtons == null) { + return Collections.emptyList(); + } + + List buttonTexts = new ArrayList<>(); + for (DomElement next : pageButtons.getChildElements()) { + buttonTexts.add(next.asNormalizedText()); + } + return buttonTexts; + } + + + public static class AddResourceCountsInterceptor { + + private final HashSet myResourceNamesToAddTo; + + public AddResourceCountsInterceptor(String... theResourceNamesToAddTo) { + myResourceNamesToAddTo = new HashSet<>(Arrays.asList(theResourceNamesToAddTo)); + } + + @Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED) + public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) { + if (theCapabilityStatement instanceof org.hl7.fhir.r4.model.CapabilityStatement) { + org.hl7.fhir.r4.model.CapabilityStatement cs = (org.hl7.fhir.r4.model.CapabilityStatement) theCapabilityStatement; + cs.setCopyright("This server is copyright **Example Org** 2021"); + int numResources = cs.getRestFirstRep().getResource().size(); + for (int i = 0; i < numResources; i++) { + org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(i); + if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) { + continue; + } + restResource.addExtension( + ExtensionConstants.CONF_RESOURCE_COUNT, + new org.hl7.fhir.r4.model.DecimalType(i) // reverse order + ); + } + } else { + org.hl7.fhir.r5.model.CapabilityStatement cs = (org.hl7.fhir.r5.model.CapabilityStatement) theCapabilityStatement; + cs.setCopyright("This server is copyright **Example Org** 2021"); + int numResources = cs.getRestFirstRep().getResource().size(); + for (int i = 0; i < numResources; i++) { + org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(i); + if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) { + continue; + } + restResource.addExtension( + ExtensionConstants.CONF_RESOURCE_COUNT, + new org.hl7.fhir.r5.model.DecimalType(i) // reverse order + ); + } + } + } + + } } - public static class MyLastNProvider { + public static class MySystemLevelOperationProvider { @Description(value = "LastN Description", shortDefinition = "LastN Short") @@ -375,11 +403,35 @@ public class OpenApiInterceptorTest { throw new IllegalStateException(); } - @Patch(type = Patient.class) + @Patch(typeName = "Patient") public MethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType, @ResourceParam IBaseParameters theRequestBody) { throw new IllegalStateException(); } + } + + + static class MyTypeLevelActorDefinitionProviderR5 extends HashMapResourceProvider { + + /** + * Constructor + */ + public MyTypeLevelActorDefinitionProviderR5() { + super(FhirContext.forR5Cached(), ActorDefinition.class); + } + + + @Validate + public MethodOutcome validate( + @ResourceParam IBaseResource theResource, + @ResourceParam String theRawResource, + @ResourceParam EncodingEnum theEncoding, + @Validate.Mode ValidationModeEnum theMode, + @Validate.Profile String theProfile, + RequestDetails theRequestDetails) { + return null; + } } + } diff --git a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java index 5d2be6224c9..28aa5880c8e 100644 --- a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java +++ b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java @@ -42,7 +42,7 @@ public class OpenApiInterceptorWithAuthorizationInterceptorTest { .withServletPath("/fhir/*") .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))) .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class))) - .withServer(t -> t.registerProvider(new OpenApiInterceptorTest.MyLastNProvider())) + .withServer(t -> t.registerProvider(new OpenApiInterceptorTest.MySystemLevelOperationProvider())) .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor())); private CloseableHttpClient myClient; private AuthorizationInterceptor myAuthorizationInterceptor; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody.java index f1731d96261..19a43078ce3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody.java @@ -28,6 +28,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import java.lang.reflect.Method; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody extends BaseOutcomeReturningMethodBinding { @@ -39,13 +41,16 @@ public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResour FhirContext theContext, Object theProvider, Class theMethodAnnotationType, - Class theResourceTypeFromAnnotation) { + Class theResourceTypeFromAnnotation, + String theResourceTypeNameFromAnnotation) { super(theMethod, theContext, theMethodAnnotationType, theProvider); Class resourceType = theResourceTypeFromAnnotation; if (resourceType != IBaseResource.class) { RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType); myResourceName = def.getName(); + } else if (isNotBlank(theResourceTypeNameFromAnnotation)) { + myResourceName = theResourceTypeNameFromAnnotation; } else { if (theProvider != null && theProvider instanceof IResourceProvider) { RuntimeResourceDefinition def = @@ -56,7 +61,7 @@ public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResour Msg.code(457) + "Can not determine resource type for method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName() + " - Did you forget to include the resourceType() value on the @" - + Delete.class.getSimpleName() + " method annotation?"); + + theMethodAnnotationType.getSimpleName() + " method annotation?"); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java index 334eb8684a7..ecb730951c7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java @@ -38,7 +38,8 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithRe theContext, theProvider, Delete.class, - theMethod.getAnnotation(Delete.class).type()); + theMethod.getAnnotation(Delete.class).type(), + theMethod.getAnnotation(Delete.class).typeName()); } @Nonnull diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java index 587ccf46811..8c2a4318b4f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java @@ -54,7 +54,8 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes theContext, theProvider, Patch.class, - theMethod.getAnnotation(Patch.class).type()); + theMethod.getAnnotation(Patch.class).type(), + theMethod.getAnnotation(Patch.class).typeName()); for (ListIterator> iter = Arrays.asList(theMethod.getParameterTypes()).listIterator(); diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java index f16e3ba8ae7..1f4a427f447 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java @@ -29,12 +29,11 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.server.IPagingProvider; import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; -import ca.uhn.fhir.validation.FhirValidator; -import ca.uhn.fhir.validation.ResultSeverityEnum; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServlet; import java.util.ArrayList; @@ -45,6 +44,7 @@ import java.util.Map; import java.util.function.Consumer; public class RestfulServerExtension extends BaseJettyServerExtension { + private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerExtension.class); private FhirContext myFhirContext; private List myProviders = new ArrayList<>(); private FhirVersionEnum myFhirVersion; @@ -86,6 +86,8 @@ public class RestfulServerExtension extends BaseJettyServerExtension