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,12 +908,14 @@ public class OpenApiInterceptor {
break;
case "Resource":
if (theResourceType != null) {
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,42 +41,95 @@ 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;
@Nested
class R4 extends BaseOpenApiInterceptorTest {
@Override
FhirContext getContext() {
return FhirContext.forR4Cached();
}
@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"));
}
@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"));
}
}
@Nested
class R5 extends BaseOpenApiInterceptorTest {
/**
* A provider that uses a resource type not present in R4
*/
private MyTypeLevelActorDefinitionProviderR5 myActorDefinitionProvider = new MyTypeLevelActorDefinitionProviderR5();
@BeforeEach
public void before() {
myClient = HttpClientBuilder.create().build();
void beforeEach() {
myServer.registerProvider(myActorDefinitionProvider);
ServerCapabilityStatementProvider a = (ServerCapabilityStatementProvider) myServer.getRestfulServer().getServerConformanceProvider();
myServer.getRestfulServer().getServerConformanceProvider();
}
@AfterEach
public void after() throws IOException {
myClient.close();
void afterEach() {
myServer.unregisterProvider(myActorDefinitionProvider);
}
@Override
FhirContext getContext() {
return FhirContext.forR5Cached();
}
}
@SuppressWarnings("JUnitMalformedDeclaration")
abstract static class BaseOpenApiInterceptorTest {
@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();
abstract FhirContext getContext();
@AfterEach
public void after() {
myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
}
@ -169,18 +207,6 @@ public class OpenApiInterceptorTest {
}
@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"));
}
@Test
public void testSwaggerUiWithCopyright() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor());
@ -249,17 +275,6 @@ public class OpenApiInterceptorTest {
assertThat(buttonTexts.toString(), buttonTexts, empty());
}
@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();
@ -290,7 +305,7 @@ public class OpenApiInterceptorTest {
}
}
private String fetchSwaggerUi(String url) throws IOException {
protected String fetchSwaggerUi(String url) throws IOException {
String resp;
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse response = myClient.execute(get)) {
@ -301,7 +316,7 @@ public class OpenApiInterceptorTest {
return resp;
}
private List<String> parsePageButtonTexts(String resp, String url) throws IOException {
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) {
@ -326,28 +341,41 @@ public class OpenApiInterceptorTest {
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) {
CapabilityStatement cs = (CapabilityStatement) 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++) {
CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(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 DecimalType(i) // reverse order
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