From 2e1f5f52769cf5f38883f3ac27e20f1fe1bdd84a Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Sat, 12 Nov 2022 18:39:38 +0100 Subject: [PATCH] fixes for support of R4B / 6.2.0 (#455) --- pom.xml | 11 +- .../starter/annotations/OnEitherVersion.java | 4 + .../starter/annotations/OnR4BCondition.java | 18 +++ .../starter/common/FhirServerConfigR4B.java | 17 +++ .../jpa/starter/common/StarterJpaConfig.java | 7 +- ...sitoryValidationInterceptorFactoryR4B.java | 77 +++++++++++ src/main/resources/application.yaml | 4 +- .../fhir/jpa/starter/ExampleServerR4BIT.java | 120 ++++++++++++++++++ 8 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnR4BCondition.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/common/validation/RepositoryValidationInterceptorFactoryR4B.java create mode 100644 src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java diff --git a/pom.xml b/pom.xml index 7293b30..35ae0de 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.2.0-PRE18-SNAPSHOT + 6.2.0 hapi-fhir-jpaserver-starter @@ -316,11 +316,18 @@ ${spring_boot_version} + + + io.micrometer + micrometer-core + 1.9.4 + + io.micrometer micrometer-registry-prometheus - 1.8.5 + 1.9.4 diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnEitherVersion.java b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnEitherVersion.java index abf564f..463f779 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnEitherVersion.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnEitherVersion.java @@ -28,6 +28,10 @@ public class OnEitherVersion extends AnyNestedCondition { static class OnR4 { } + @Conditional(OnR4BCondition.class) + static class OnR4B { + } + @Conditional(OnR5Condition.class) static class OnR5 { } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnR4BCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnR4BCondition.java new file mode 100644 index 0000000..e323a22 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/annotations/OnR4BCondition.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.jpa.starter.annotations; + +import ca.uhn.fhir.context.FhirVersionEnum; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class OnR4BCondition implements Condition { + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { + String version = conditionContext. + getEnvironment() + .getProperty("hapi.fhir.fhir_version") + .toUpperCase(); + + return FhirVersionEnum.R4B.name().equals(version); + } +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java new file mode 100644 index 0000000..d99d102 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/FhirServerConfigR4B.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.starter.common; + +import ca.uhn.fhir.jpa.config.r4b.JpaR4BConfig; +import ca.uhn.fhir.jpa.starter.annotations.OnR4BCondition; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Conditional(OnR4BCondition.class) +@Import({ + JpaR4BConfig.class, + StarterJpaConfig.class, + ElasticsearchConfig.class +}) +public class FhirServerConfigR4B { +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java index ed15015..3f481bd 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDaoJpaImpl; import ca.uhn.fhir.jpa.dao.search.HSearchSortHelperImpl; import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper; -import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; @@ -423,7 +422,6 @@ public class StarterJpaConfig { public static IServerConformanceProvider calculateConformanceProvider(IFhirSystemDao fhirSystemDao, RestfulServer fhirServer, DaoConfig daoConfig, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport) { FhirVersionEnum fhirVersion = fhirSystemDao.getContext().getVersion().getVersion(); if (fhirVersion == FhirVersionEnum.DSTU2) { - JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(fhirServer, fhirSystemDao, daoConfig); confProvider.setImplementationDescription("HAPI FHIR DSTU2 Server"); return confProvider; @@ -437,6 +435,11 @@ public class StarterJpaConfig { JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(fhirServer, fhirSystemDao, daoConfig, searchParamRegistry, theValidationSupport); confProvider.setImplementationDescription("HAPI FHIR R4 Server"); return confProvider; + } else if (fhirVersion == FhirVersionEnum.R4B) { + + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(fhirServer, fhirSystemDao, daoConfig, searchParamRegistry, theValidationSupport); + confProvider.setImplementationDescription("HAPI FHIR R4B Server"); + return confProvider; } else if (fhirVersion == FhirVersionEnum.R5) { JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(fhirServer, fhirSystemDao, daoConfig, searchParamRegistry, theValidationSupport); diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/RepositoryValidationInterceptorFactoryR4B.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/RepositoryValidationInterceptorFactoryR4B.java new file mode 100644 index 0000000..277af2f --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/validation/RepositoryValidationInterceptorFactoryR4B.java @@ -0,0 +1,77 @@ +package ca.uhn.fhir.jpa.starter.common.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.interceptor.validation.IRepositoryValidatingRule; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.starter.annotations.OnR4BCondition; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenParam; +import org.hl7.fhir.r4b.model.StructureDefinition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory.ENABLE_REPOSITORY_VALIDATING_INTERCEPTOR; + +/** + * This class can be customized to enable the {@link RepositoryValidatingInterceptor} + * on this server. + *

+ * The enable_repository_validating_interceptor property must be enabled in application.yaml + * in order to use this class. + */ +@ConditionalOnProperty(prefix = "hapi.fhir", name = ENABLE_REPOSITORY_VALIDATING_INTERCEPTOR, havingValue = "true") +@Configuration +@Conditional(OnR4BCondition.class) +public class RepositoryValidationInterceptorFactoryR4B implements IRepositoryValidationInterceptorFactory { + + private final FhirContext fhirContext; + private final RepositoryValidatingRuleBuilder repositoryValidatingRuleBuilder; + private final IFhirResourceDao structureDefinitionResourceProvider; + + public RepositoryValidationInterceptorFactoryR4B(RepositoryValidatingRuleBuilder repositoryValidatingRuleBuilder, DaoRegistry daoRegistry) { + this.repositoryValidatingRuleBuilder = repositoryValidatingRuleBuilder; + this.fhirContext = daoRegistry.getSystemDao().getContext(); + structureDefinitionResourceProvider = daoRegistry.getResourceDao("StructureDefinition"); + + } + + @Override + public RepositoryValidatingInterceptor buildUsingStoredStructureDefinitions() { + + IBundleProvider results = structureDefinitionResourceProvider.search(new SearchParameterMap().add(StructureDefinition.SP_KIND, new TokenParam("resource"))); + Map> structureDefintions = results.getResources(0, results.size()) + .stream() + .map(StructureDefinition.class::cast) + .collect(Collectors.groupingBy(StructureDefinition::getType)); + + structureDefintions.forEach((key, value) -> { + String[] urls = value.stream().map(StructureDefinition::getUrl).toArray(String[]::new); + repositoryValidatingRuleBuilder.forResourcesOfType(key).requireAtLeastOneProfileOf(urls).and().requireValidationToDeclaredProfiles(); + }); + + List rules = repositoryValidatingRuleBuilder.build(); + return new RepositoryValidatingInterceptor(fhirContext, rules); + } + + @Override + public RepositoryValidatingInterceptor build() { + + // Customize the ruleBuilder here to have the rules you want! We will give a simple example + // of enabling validation for all Patient resources + repositoryValidatingRuleBuilder.forResourcesOfType("Patient").requireAtLeastProfile("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient").and().requireValidationToDeclaredProfiles(); + + // Do not customize below this line + List rules = repositoryValidatingRuleBuilder.build(); + return new RepositoryValidatingInterceptor(fhirContext, rules); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 19a1666..67a652f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -48,8 +48,8 @@ spring: # hibernate.search.backend.directory.root: target/lucenefiles # hibernate.search.backend.lucene_version: lucene_current ### elastic parameters ===> see also elasticsearch section below <=== - hibernate.search.backend.type: elasticsearch - hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer +# hibernate.search.backend.type: elasticsearch +# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer hapi: fhir: ### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html) diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java new file mode 100644 index 0000000..0092722 --- /dev/null +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ExampleServerR4BIT.java @@ -0,0 +1,120 @@ +package ca.uhn.fhir.jpa.starter; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4b.model.Bundle; +import org.hl7.fhir.r4b.model.Patient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties = { + "spring.datasource.url=jdbc:h2:mem:dbr4b", + "hapi.fhir.enable_repository_validating_interceptor=true", + "hapi.fhir.fhir_version=r4b", + "hapi.fhir.subscription.websocket_enabled=false", + "hapi.fhir.mdm_enabled=false", + "hapi.fhir.implementationguides.dk-core.name=hl7.fhir.dk.core", + "hapi.fhir.implementationguides.dk-core.version=1.1.0", + // Override is currently required when using MDM as the construction of the MDM + // beans are ambiguous as they are constructed multiple places. This is evident + // when running in a spring boot environment + "spring.main.allow-bean-definition-overriding=true"}) +class ExampleServerR4BIT { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleServerR4BIT.class); + private IGenericClient ourClient; + private FhirContext ourCtx; + + @LocalServerPort + private int port; + + @Test + @Order(0) + void testCreateAndRead() { + String methodName = "testCreateAndRead"; + ourLog.info("Entering " + methodName + "()..."); + + Patient pt = new Patient(); + pt.setActive(true); + pt.getBirthDateElement().setValueAsString("2020-01-01"); + pt.addIdentifier().setSystem("http://foo").setValue("12345"); + pt.addName().setFamily(methodName); + IIdType id = ourClient.create().resource(pt).execute().getId(); + + Patient pt2 = ourClient.read().resource(Patient.class).withId(id).execute(); + assertEquals(methodName, pt2.getName().get(0).getFamily()); + + } + + + @Test + public void testBatchPutWithIdenticalTags() { + String batchPuts = "{\n" + + "\t\"resourceType\": \"Bundle\",\n" + + "\t\"id\": \"patients\",\n" + + "\t\"type\": \"batch\",\n" + + "\t\"entry\": [\n" + + "\t\t{\n" + + "\t\t\t\"request\": {\n" + + "\t\t\t\t\"method\": \"PUT\",\n" + + "\t\t\t\t\"url\": \"Patient/pat-1\"\n" + + "\t\t\t},\n" + + "\t\t\t\"resource\": {\n" + + "\t\t\t\t\"resourceType\": \"Patient\",\n" + + "\t\t\t\t\"id\": \"pat-1\",\n" + + "\t\t\t\t\"meta\": {\n" + + "\t\t\t\t\t\"tag\": [\n" + + "\t\t\t\t\t\t{\n" + + "\t\t\t\t\t\t\t\"system\": \"http://mysystem.org\",\n" + + "\t\t\t\t\t\t\t\"code\": \"value2\"\n" + + "\t\t\t\t\t\t}\n" + + "\t\t\t\t\t]\n" + + "\t\t\t\t}\n" + + "\t\t\t},\n" + + "\t\t\t\"fullUrl\": \"/Patient/pat-1\"\n" + + "\t\t},\n" + + "\t\t{\n" + + "\t\t\t\"request\": {\n" + + "\t\t\t\t\"method\": \"PUT\",\n" + + "\t\t\t\t\"url\": \"Patient/pat-2\"\n" + + "\t\t\t},\n" + + "\t\t\t\"resource\": {\n" + + "\t\t\t\t\"resourceType\": \"Patient\",\n" + + "\t\t\t\t\"id\": \"pat-2\",\n" + + "\t\t\t\t\"meta\": {\n" + + "\t\t\t\t\t\"tag\": [\n" + + "\t\t\t\t\t\t{\n" + + "\t\t\t\t\t\t\t\"system\": \"http://mysystem.org\",\n" + + "\t\t\t\t\t\t\t\"code\": \"value2\"\n" + + "\t\t\t\t\t\t}\n" + + "\t\t\t\t\t]\n" + + "\t\t\t\t}\n" + + "\t\t\t},\n" + + "\t\t\t\"fullUrl\": \"/Patient/pat-2\"\n" + + "\t\t}\n" + + "\t]\n" + + "}"; + Bundle bundle = FhirContext.forR4B().newJsonParser().parseResource(Bundle.class, batchPuts); + ourClient.transaction().withBundle(bundle).execute(); + } + + + + @BeforeEach + void beforeEach() { + + ourCtx = FhirContext.forR4B(); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); + String ourServerBase = "http://localhost:" + port + "/fhir/"; + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); + + + } +}