Fix openapi for R5 (#5115)

* Fix openapi for R5

* Add changelog
This commit is contained in:
James Agnew 2023-07-21 11:17:13 -04:00 committed by GitHub
parent 66f428d356
commit d31da211b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 350 additions and 277 deletions

View File

@ -0,0 +1,4 @@
---
type: add
issue: 5115
title: "OpenAPI definitions were not working for R5 JPA servers. This has been corrected."

View File

@ -71,6 +71,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>

View File

@ -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;

View File

@ -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<String> 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("<title>Swagger UI</title>"));
}
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<String> 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<String> 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("<p>This server is copyright <strong>Example Org</strong> 2021</p>"));
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("<link rel=\"stylesheet\" type=\"text/css\" href=\"./swagger-ui-custom.css\"/>"));
// 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<String> 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<String> 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<String> 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<String> 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<String> 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("<title>Swagger UI</title>"));
}
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("<p>This server is copyright <strong>Example Org</strong> 2021</p>"));
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("<link rel=\"stylesheet\" type=\"text/css\" href=\"./swagger-ui-custom.css\"/>"));
// 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<String> 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<String> 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<String> buttonTexts = new ArrayList<>();
for (DomElement next : pageButtons.getChildElements()) {
buttonTexts.add(next.asNormalizedText());
}
return buttonTexts;
}
public static class AddResourceCountsInterceptor {
private final HashSet<String> 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<ActorDefinition> {
/**
* 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;
}
}
}

View File

@ -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;

View File

@ -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<? extends IBaseResource> theResourceTypeFromAnnotation) {
Class<? extends IBaseResource> theResourceTypeFromAnnotation,
String theResourceTypeNameFromAnnotation) {
super(theMethod, theContext, theMethodAnnotationType, theProvider);
Class<? extends IBaseResource> 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?");
}
}

View File

@ -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

View File

@ -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<Class<?>> iter =
Arrays.asList(theMethod.getParameterTypes()).listIterator();

View File

@ -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<RestfulServerExtension> {
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerExtension.class);
private FhirContext myFhirContext;
private List<Object> myProviders = new ArrayList<>();
private FhirVersionEnum myFhirVersion;
@ -86,6 +86,8 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
myFhirContext.getRestfulClientFactory().setSocketTimeout((int) (500 * DateUtils.MILLIS_PER_SECOND));
myFhirContext.getRestfulClientFactory().setServerValidationMode(myServerValidationMode);
ourLog.info("FHIR server has been started with base URL: {}", getBaseUrl());
}
@Override