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> <version>${project.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>

View File

@ -908,11 +908,13 @@ public class OpenApiInterceptor {
break; break;
case "Resource": case "Resource":
if (theResourceType != null) { if (theResourceType != null) {
IBaseResource resource = FHIR_CONTEXT_CANONICAL if (FHIR_CONTEXT_CANONICAL.getResourceTypes().contains(theResourceType)) {
.getResourceDefinition(theResourceType) IBaseResource resource = FHIR_CONTEXT_CANONICAL
.newInstance(); .getResourceDefinition(theResourceType)
resource.setId("1"); .newInstance();
param.setResource((Resource) resource); resource.setId("1");
param.setResource((Resource) resource);
}
} }
break; break;
} }
@ -1002,7 +1004,7 @@ public class OpenApiInterceptor {
} }
return () -> { return () -> {
IBaseResource example = null; IBaseResource example = null;
if (theResourceType != null) { if (theResourceType != null && theFhirContext.getResourceTypes().contains(theResourceType)) {
example = theFhirContext.getResourceDefinition(theResourceType).newInstance(); example = theFhirContext.getResourceDefinition(theResourceType).newInstance();
} }
return example; 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.Hook;
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.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.*;
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.api.Constants; 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.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum; 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.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; 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.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.HtmlUtil; 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.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.ExtensionConstants;
import com.gargoylesoftware.htmlunit.html.DomElement; 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.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; 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.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.r5.model.ActorDefinition;
import org.hl7.fhir.instance.model.api.IBaseConformance; import org.junit.jupiter.api.*;
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.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;
@ -56,298 +41,341 @@ 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.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
public class OpenApiInterceptorTest { public class OpenApiInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(OpenApiInterceptorTest.class); 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 @Nested
public void before() { class R4 extends BaseOpenApiInterceptorTest {
myClient = HttpClientBuilder.create().build();
}
@AfterEach @Override
public void after() throws IOException { FhirContext getContext() {
myClient.close(); return FhirContext.forR4Cached();
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);
} }
get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs"); @Test
try (CloseableHttpResponse response = myClient.execute(get)) { public void testSwaggerUiWithResourceCounts() throws IOException {
resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
ourLog.info("Response: {}", response.getStatusLine()); myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
ourLog.debug("Response: {}", resp);
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"); String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
assertNull(fooOpPath.getGet()); String resp = fetchSwaggerUi(url);
assertNotNull(fooOpPath.getPost()); List<String> buttonTexts = parsePageButtonTexts(resp, url);
assertEquals("Foo Op Description", fooOpPath.getPost().getDescription()); assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient"));
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());
} }
} }
@Nested
class R5 extends BaseOpenApiInterceptorTest {
@Test /**
public void testSwaggerUiWithResourceCounts() throws IOException { * A provider that uses a resource type not present in R4
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); */
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); private MyTypeLevelActorDefinitionProviderR5 myActorDefinitionProvider = new MyTypeLevelActorDefinitionProviderR5();
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; @BeforeEach
String resp = fetchSwaggerUi(url); void beforeEach() {
List<String> buttonTexts = parsePageButtonTexts(resp, url); myServer.registerProvider(myActorDefinitionProvider);
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "Patient 2", "OperationDefinition 1", "Observation 0")); 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/"; @SuppressWarnings("JUnitMalformedDeclaration")
String resp = fetchSwaggerUi(url); abstract static class BaseOpenApiInterceptorTest {
assertThat(resp, resp, containsString("<p>This server is copyright <strong>Example Org</strong> 2021</p>"));
assertThat(resp, resp, not(containsString("swagger-ui-custom.css")));
}
@Test @RegisterExtension
public void testSwaggerUiWithNoBannerUrl() throws IOException { @Order(0)
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); protected RestfulServerExtension myServer = new RestfulServerExtension(getContext())
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor().setBannerImage("")); .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/"; abstract FhirContext getContext();
String resp = fetchSwaggerUi(url);
assertThat(resp, resp, not(containsString("img id=\"banner_img\"")));
}
@Test @AfterEach
public void testSwaggerUiWithCustomStylesheet() throws IOException { public void after() {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
}
OpenApiInterceptor interceptor = new OpenApiInterceptor(); @Test
interceptor.setCssText("BODY {\nfont-size: 1.1em;\n}"); public void testFetchSwagger() throws IOException {
myServer.getRestfulServer().registerInterceptor(interceptor); myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
// Fetch Swagger UI HTML String resp;
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/metadata?_pretty=true");
String resp = fetchSwaggerUi(url); try (CloseableHttpResponse response = myClient.execute(get)) {
assertThat(resp, resp, containsString("<link rel=\"stylesheet\" type=\"text/css\" href=\"./swagger-ui-custom.css\"/>")); resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("CapabilityStatement: {}", resp);
// 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));
}
protected String removeCtrlR (String source) { get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs");
String result = source; try (CloseableHttpResponse response = myClient.execute(get)) {
if (source != null) { resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
result = StringUtils.remove(source, '\r'); ourLog.info("Response: {}", response.getStatusLine());
} ourLog.debug("Response: {}", resp);
return result; }
}
@Test
public void testSwaggerUiNotPaged() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
OpenApiInterceptor interceptor = new OpenApiInterceptor(); OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class);
interceptor.setUseResourcePages(false);
myServer.getRestfulServer().registerInterceptor(interceptor);
// Fetch Swagger UI HTML PathItem fooOpPath = parsed.getPaths().get("/$foo-op");
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; assertNull(fooOpPath.getGet());
String resp = fetchSwaggerUi(url); assertNotNull(fooOpPath.getPost());
List<String> buttonTexts = parsePageButtonTexts(resp, url); assertEquals("Foo Op Description", fooOpPath.getPost().getDescription());
assertThat(buttonTexts.toString(), buttonTexts, empty()); assertEquals("Foo Op Short", fooOpPath.getPost().getSummary());
}
@Test PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn");
public void testSwaggerUiWithResourceCounts_OneResourceOnly() throws IOException { assertNotNull(lastNPath.getPost());
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition")); assertEquals("LastN Description", lastNPath.getPost().getDescription());
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); assertEquals("LastN Short", lastNPath.getPost().getSummary());
assertNull(lastNPath.getPost().getParameters());
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; assertNotNull(lastNPath.getPost().getRequestBody());
String resp = fetchSwaggerUi(url); assertNotNull(lastNPath.getGet());
List<String> buttonTexts = parsePageButtonTexts(resp, url); assertEquals("LastN Description", lastNPath.getGet().getDescription());
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient")); assertEquals("LastN Short", lastNPath.getGet().getSummary());
} assertEquals(4, lastNPath.getGet().getParameters().size());
assertEquals("Subject description", lastNPath.getGet().getParameters().get(0).getDescription());
@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();
} }
List<String> buttonTexts = new ArrayList<>(); @Test
for (DomElement next : pageButtons.getChildElements()) { public void testRedirectFromBaseUrl() throws IOException {
buttonTexts.add(next.asNormalizedText()); myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
}
return buttonTexts;
}
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) @Test
public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) { public void testSwaggerUiWithCopyright() throws IOException {
CapabilityStatement cs = (CapabilityStatement) theCapabilityStatement; myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
cs.setCopyright("This server is copyright **Example Org** 2021"); myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
int numResources = cs.getRestFirstRep().getResource().size(); String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
for (int i = 0; i < numResources; i++) { 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); @Test
if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) { public void testSwaggerUiWithNoBannerUrl() throws IOException {
continue; 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( protected String removeCtrlR(String source) {
ExtensionConstants.CONF_RESOURCE_COUNT, String result = source;
new DecimalType(i) // reverse order 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") @Description(value = "LastN Description", shortDefinition = "LastN Short")
@ -375,11 +403,35 @@ public class OpenApiInterceptorTest {
throw new IllegalStateException(); 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) { public MethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType, @ResourceParam IBaseParameters theRequestBody) {
throw new IllegalStateException(); 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/*") .withServletPath("/fhir/*")
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))) .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class)))
.withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.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())); .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor()));
private CloseableHttpClient myClient; private CloseableHttpClient myClient;
private AuthorizationInterceptor myAuthorizationInterceptor; private AuthorizationInterceptor myAuthorizationInterceptor;

View File

@ -28,6 +28,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody
extends BaseOutcomeReturningMethodBinding { extends BaseOutcomeReturningMethodBinding {
@ -39,13 +41,16 @@ public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResour
FhirContext theContext, FhirContext theContext,
Object theProvider, Object theProvider,
Class<?> theMethodAnnotationType, Class<?> theMethodAnnotationType,
Class<? extends IBaseResource> theResourceTypeFromAnnotation) { Class<? extends IBaseResource> theResourceTypeFromAnnotation,
String theResourceTypeNameFromAnnotation) {
super(theMethod, theContext, theMethodAnnotationType, theProvider); super(theMethod, theContext, theMethodAnnotationType, theProvider);
Class<? extends IBaseResource> resourceType = theResourceTypeFromAnnotation; Class<? extends IBaseResource> resourceType = theResourceTypeFromAnnotation;
if (resourceType != IBaseResource.class) { if (resourceType != IBaseResource.class) {
RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType); RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType);
myResourceName = def.getName(); myResourceName = def.getName();
} else if (isNotBlank(theResourceTypeNameFromAnnotation)) {
myResourceName = theResourceTypeNameFromAnnotation;
} else { } else {
if (theProvider != null && theProvider instanceof IResourceProvider) { if (theProvider != null && theProvider instanceof IResourceProvider) {
RuntimeResourceDefinition def = RuntimeResourceDefinition def =
@ -56,7 +61,7 @@ public abstract class BaseOutcomeReturningMethodBindingWithResourceIdButNoResour
Msg.code(457) + "Can not determine resource type for method '" + theMethod.getName() Msg.code(457) + "Can not determine resource type for method '" + theMethod.getName()
+ "' on type " + theMethod.getDeclaringClass().getCanonicalName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName()
+ " - Did you forget to include the resourceType() value on the @" + " - 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, theContext,
theProvider, theProvider,
Delete.class, Delete.class,
theMethod.getAnnotation(Delete.class).type()); theMethod.getAnnotation(Delete.class).type(),
theMethod.getAnnotation(Delete.class).typeName());
} }
@Nonnull @Nonnull

View File

@ -54,7 +54,8 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes
theContext, theContext,
theProvider, theProvider,
Patch.class, Patch.class,
theMethod.getAnnotation(Patch.class).type()); theMethod.getAnnotation(Patch.class).type(),
theMethod.getAnnotation(Patch.class).typeName());
for (ListIterator<Class<?>> iter = for (ListIterator<Class<?>> iter =
Arrays.asList(theMethod.getParameterTypes()).listIterator(); 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.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.IPagingProvider; import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer; 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.Validate;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import java.util.ArrayList; import java.util.ArrayList;
@ -45,6 +44,7 @@ import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServerExtension> { public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServerExtension> {
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerExtension.class);
private FhirContext myFhirContext; private FhirContext myFhirContext;
private List<Object> myProviders = new ArrayList<>(); private List<Object> myProviders = new ArrayList<>();
private FhirVersionEnum myFhirVersion; private FhirVersionEnum myFhirVersion;
@ -86,6 +86,8 @@ public class RestfulServerExtension extends BaseJettyServerExtension<RestfulServ
myFhirContext.getRestfulClientFactory().setSocketTimeout((int) (500 * DateUtils.MILLIS_PER_SECOND)); myFhirContext.getRestfulClientFactory().setSocketTimeout((int) (500 * DateUtils.MILLIS_PER_SECOND));
myFhirContext.getRestfulClientFactory().setServerValidationMode(myServerValidationMode); myFhirContext.getRestfulClientFactory().setServerValidationMode(myServerValidationMode);
ourLog.info("FHIR server has been started with base URL: {}", getBaseUrl());
} }
@Override @Override