From 5cefa7a6ccb9a606e6cbd86f4d2228083838569a Mon Sep 17 00:00:00 2001 From: TipzCM Date: Mon, 15 Nov 2021 15:30:41 -0500 Subject: [PATCH 1/8] 3168 added some nullcheck (#3169) Co-authored-by: leif stawnyczy --- .../interceptor/BaseValidatingInterceptor.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BaseValidatingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BaseValidatingInterceptor.java index 5c4083e7529..3925d7faa10 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BaseValidatingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BaseValidatingInterceptor.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.server.interceptor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -310,11 +311,14 @@ public abstract class BaseValidatingInterceptor extends ValidationResultEnric return null; } - switch (theRequestDetails.getRestOperationType()) { - case GRAPHQL_REQUEST: - return null; - default: - break; + RestOperationTypeEnum opType = theRequestDetails.getRestOperationType(); + if (opType != null) { + switch (opType) { + case GRAPHQL_REQUEST: + return null; + default: + break; + } } FhirValidator validator; From 843517f7ba30656f2a30dae7ee50375768780995 Mon Sep 17 00:00:00 2001 From: IanMMarshall <49525404+IanMMarshall@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:36:30 -0500 Subject: [PATCH 2/8] Avoid creating ResourcePersistentId for placeholder resources with null ID (#3158) * Add check before mapping storage ID to resource ID in TransactionDetails. * Add change log. * Changed to instead prevent creation of ResourcePersistentId with null ID value. * Changed to instead prevent ResourcePersistentId being created with null resource ID. Co-authored-by: ianmarshall --- ...58-creation-of-versioned-placeholders.yaml | 5 ++ .../fhir/jpa/dao/index/IdHelperService.java | 13 +++-- ...irResourceDaoCreatePlaceholdersR4Test.java | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3158-creation-of-versioned-placeholders.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3158-creation-of-versioned-placeholders.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3158-creation-of-versioned-placeholders.yaml new file mode 100644 index 00000000000..20d2fd864af --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3158-creation-of-versioned-placeholders.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 3158 +title: "Resource links were previously not being consistently created in cases where references were versioned and + pointing to recently auto-created placeholder resources." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index 2efcbc28aca..aedce78f880 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -316,12 +316,15 @@ public class IdHelperService { TypedQuery query = myEntityManager.createQuery(criteriaQuery); List results = query.getResultList(); for (ForcedId nextId : results) { - ResourcePersistentId persistentId = new ResourcePersistentId(nextId.getResourceId()); - populateAssociatedResourceId(nextId.getResourceType(), nextId.getForcedId(), persistentId); - retVal.add(persistentId); + // Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending. + if (nextId.getResourceId() != null) { + ResourcePersistentId persistentId = new ResourcePersistentId(nextId.getResourceId()); + populateAssociatedResourceId(nextId.getResourceType(), nextId.getForcedId(), persistentId); + retVal.add(persistentId); - String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getForcedId()); - myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, persistentId); + String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getForcedId()); + myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, persistentId); + } } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java index 1588c81ba80..8a9ec107422 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.HapiExtensions; import com.google.common.collect.Sets; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.AuditEvent; import org.hl7.fhir.r4.model.BooleanType; @@ -29,6 +30,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.HashSet; import java.util.List; import static org.hamcrest.CoreMatchers.equalTo; @@ -650,4 +652,52 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { assertNotNull(retObservation); } + @Test + public void testMultipleVersionedReferencesToAutocreatedPlaceholder() { + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + HashSet refPaths = new HashSet<>(); + refPaths.add("Observation.subject"); + myModelConfig.setAutoVersionReferenceAtPaths(refPaths); + + + Observation obs1 = new Observation(); + obs1.setId("Observation/DEF1"); + Reference patientRef = new Reference("Patient/RED"); + obs1.setSubject(patientRef); + BundleBuilder builder = new BundleBuilder(myFhirCtx); + Observation obs2 = new Observation(); + obs2.setId("Observation/DEF2"); + obs2.setSubject(patientRef); + builder.addTransactionUpdateEntry(obs1); + builder.addTransactionUpdateEntry(obs2); + + mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle()); + + // verify links created to Patient placeholder from both Observations + IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous().addRevInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(3, outcome.getAllResources().size()); + } + + @Test + public void testMultipleReferencesToAutocreatedPlaceholder() { + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + + Observation obs1 = new Observation(); + obs1.setId("Observation/DEF1"); + Reference patientRef = new Reference("Patient/RED"); + obs1.setSubject(patientRef); + BundleBuilder builder = new BundleBuilder(myFhirCtx); + Observation obs2 = new Observation(); + obs2.setId("Observation/DEF2"); + obs2.setSubject(patientRef); + builder.addTransactionUpdateEntry(obs1); + builder.addTransactionUpdateEntry(obs2); + + mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle()); + + // verify links created to Patient placeholder from both Observations + IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous().addRevInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(3, outcome.getAllResources().size()); + } + } From 7e291bb469645010e9f5a64e3adc11b4bf597bc5 Mon Sep 17 00:00:00 2001 From: TipzCM Date: Tue, 16 Nov 2021 10:47:44 -0500 Subject: [PATCH 3/8] 3163 refactor some base mdm stuff (#3166) Co-authored-by: leif stawnyczy --- .../fhir/instance/model/api/IAnyResource.java | 2 +- .../jpa/mdm/config/MdmConsumerConfig.java | 12 ++- .../jpa/mdm/provider/BaseProviderR4Test.java | 7 +- .../mdm/provider/MdmControllerHelper.java | 78 ++++++++++++++++++- .../mdm/provider/MdmProviderDstu3Plus.java | 69 +++------------- .../fhir/mdm/provider/MdmProviderLoader.java | 8 +- 6 files changed, 107 insertions(+), 69 deletions(-) diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java index 9fcacdc4ebd..3d1cca7479d 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java @@ -31,7 +31,7 @@ public interface IAnyResource extends IBaseResource { /** * Search parameter constant for _id */ - @SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token" ) + @SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token") String SP_RES_ID = "_id"; /** diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java index 48c29f97a0c..f61885118a5 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java @@ -257,8 +257,16 @@ public class MdmConsumerConfig { } @Bean - MdmControllerHelper mdmProviderHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader, IMdmSettings theMdmSettings, MessageHelper messageHelper) { - return new MdmControllerHelper(theFhirContext, theResourceLoader, theMdmSettings, messageHelper); + MdmControllerHelper mdmProviderHelper(FhirContext theFhirContext, + IResourceLoader theResourceLoader, + IMdmSettings theMdmSettings, + IMdmMatchFinderSvc theMdmMatchFinderSvc, + MessageHelper messageHelper) { + return new MdmControllerHelper(theFhirContext, + theResourceLoader, + theMdmMatchFinderSvc, + theMdmSettings, + messageHelper); } @Bean diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java index 86cd94f783a..29a5856e53d 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.mdm.api.IMdmClearJobSubmitter; import ca.uhn.fhir.mdm.api.IMdmControllerSvc; import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; +import ca.uhn.fhir.mdm.provider.MdmControllerHelper; import ca.uhn.fhir.mdm.provider.MdmProviderDstu3Plus; import ca.uhn.fhir.mdm.rules.config.MdmSettings; import ca.uhn.fhir.mdm.util.MessageHelper; @@ -29,8 +30,6 @@ import java.util.List; public abstract class BaseProviderR4Test extends BaseMdmR4Test { MdmProviderDstu3Plus myMdmProvider; @Autowired - private IMdmMatchFinderSvc myMdmMatchFinderSvc; - @Autowired private IMdmControllerSvc myMdmControllerSvc; @Autowired private IMdmClearJobSubmitter myMdmClearJobSubmitter; @@ -39,6 +38,8 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test { @Autowired private MdmSettings myMdmSettings; @Autowired + private MdmControllerHelper myMdmHelper; + @Autowired BatchJobHelper myBatchJobHelper; @Autowired MessageHelper myMessageHelper; @@ -55,7 +56,7 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test { @BeforeEach public void before() { - myMdmProvider = new MdmProviderDstu3Plus(myFhirContext, myMdmControllerSvc, myMdmMatchFinderSvc, myMdmSubmitSvc, myMdmSettings); + myMdmProvider = new MdmProviderDstu3Plus(myFhirContext, myMdmControllerSvc, myMdmHelper, myMdmSubmitSvc, myMdmSettings); defaultScript = myMdmSettings.getScriptText(); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java index b5d79b22115..e61b780a5f5 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java @@ -21,20 +21,36 @@ package ca.uhn.fhir.mdm.provider; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; import ca.uhn.fhir.mdm.api.IMdmSettings; +import ca.uhn.fhir.mdm.api.MatchedTarget; +import ca.uhn.fhir.mdm.api.MdmConstants; import ca.uhn.fhir.mdm.util.MdmResourceUtil; import ca.uhn.fhir.mdm.util.MessageHelper; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.validation.IResourceLoader; import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBackboneElement; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.annotation.Nonnull; +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.UUID; + @Service public class MdmControllerHelper { @@ -42,12 +58,18 @@ public class MdmControllerHelper { private final IResourceLoader myResourceLoader; private final IMdmSettings myMdmSettings; private final MessageHelper myMessageHelper; + private final IMdmMatchFinderSvc myMdmMatchFinderSvc; @Autowired - public MdmControllerHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader, IMdmSettings theMdmSettings, MessageHelper theMessageHelper) { + public MdmControllerHelper(FhirContext theFhirContext, + IResourceLoader theResourceLoader, + IMdmMatchFinderSvc theMdmMatchFinderSvc, + IMdmSettings theMdmSettings, + MessageHelper theMessageHelper) { myFhirContext = theFhirContext; myResourceLoader = theResourceLoader; myMdmSettings = theMdmSettings; + myMdmMatchFinderSvc = theMdmMatchFinderSvc; myMessageHelper = theMessageHelper; } @@ -104,4 +126,58 @@ public class MdmControllerHelper { throw new InvalidRequestException(myMessageHelper.getMessageForUnmanagedResource()); } } + + /** + * Helper method which will return a bundle of all Matches and Possible Matches. + */ + public IBaseBundle getMatchesAndPossibleMatchesForResource(IAnyResource theResource, String theResourceType) { + List matches = myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource); + matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); + + BundleBuilder builder = new BundleBuilder(myFhirContext); + builder.setBundleField("type", "searchset"); + builder.setBundleField("id", UUID.randomUUID().toString()); + builder.setMetaField("lastUpdated", builder.newPrimitive("instant", new Date())); + + IBaseBundle retVal = builder.getBundle(); + for (MatchedTarget next : matches) { + boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); + if (!shouldKeepThisEntry) { + continue; + } + + IBase entry = builder.addEntry(); + builder.addToEntry(entry, "resource", next.getTarget()); + + IBaseBackboneElement search = builder.addSearch(entry); + toBundleEntrySearchComponent(builder, search, next); + } + return retVal; + } + + public IBaseBackboneElement toBundleEntrySearchComponent(BundleBuilder theBuilder, IBaseBackboneElement theSearch, MatchedTarget theMatchedTarget) { + theBuilder.setSearchField(theSearch, "mode", "match"); + double score = theMatchedTarget.getMatchResult().getNormalizedScore(); + theBuilder.setSearchField(theSearch, "score", + theBuilder.newPrimitive("decimal", BigDecimal.valueOf(score))); + + String matchGrade = getMatchGrade(theMatchedTarget); + IBaseDatatype codeType = (IBaseDatatype) myFhirContext.getElementDefinition("code").newInstance(matchGrade); + IBaseExtension searchExtension = theSearch.addExtension(); + searchExtension.setUrl(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE); + searchExtension.setValue(codeType); + + return theSearch; + } + + @Nonnull + protected String getMatchGrade(MatchedTarget theTheMatchedTarget) { + String retVal = "probable"; + if (theTheMatchedTarget.isMatch()) { + retVal = "certain"; + } else if (theTheMatchedTarget.isPossibleMatch()) { + retVal = "possible"; + } + return retVal; + } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3Plus.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3Plus.java index d8d1ff84960..05cd9f12608 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3Plus.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3Plus.java @@ -72,9 +72,9 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider { private static final Logger ourLog = getLogger(MdmProviderDstu3Plus.class); private final IMdmControllerSvc myMdmControllerSvc; - private final IMdmMatchFinderSvc myMdmMatchFinderSvc; private final IMdmSubmitSvc myMdmSubmitSvc; private final IMdmSettings myMdmSettings; + private final MdmControllerHelper myMdmControllerHelper; public static final int DEFAULT_PAGE_SIZE = 20; public static final int MAX_PAGE_SIZE = 100; @@ -85,10 +85,14 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider { * Note that this is not a spring bean. Any necessary injections should * happen in the constructor */ - public MdmProviderDstu3Plus(FhirContext theFhirContext, IMdmControllerSvc theMdmControllerSvc, IMdmMatchFinderSvc theMdmMatchFinderSvc, IMdmSubmitSvc theMdmSubmitSvc, IMdmSettings theIMdmSettings) { + public MdmProviderDstu3Plus(FhirContext theFhirContext, + IMdmControllerSvc theMdmControllerSvc, + MdmControllerHelper theMdmHelper, + IMdmSubmitSvc theMdmSubmitSvc, + IMdmSettings theIMdmSettings) { super(theFhirContext); myMdmControllerSvc = theMdmControllerSvc; - myMdmMatchFinderSvc = theMdmMatchFinderSvc; + myMdmControllerHelper = theMdmHelper; myMdmSubmitSvc = theMdmSubmitSvc; myMdmSettings = theIMdmSettings; } @@ -98,7 +102,7 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider { if (thePatient == null) { throw new InvalidRequestException("resource may not be null"); } - return getMatchesAndPossibleMatchesForResource(thePatient, "Patient"); + return myMdmControllerHelper.getMatchesAndPossibleMatchesForResource(thePatient, "Patient"); } @Operation(name = ProviderConstants.MDM_MATCH) @@ -108,51 +112,7 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider { if (theResource == null) { throw new InvalidRequestException("resource may not be null"); } - return getMatchesAndPossibleMatchesForResource(theResource, theResourceType.getValueAsString()); - } - - /** - * Helper method which will return a bundle of all Matches and Possible Matches. - */ - private IBaseBundle getMatchesAndPossibleMatchesForResource(IAnyResource theResource, String theResourceType) { - List matches = myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource); - matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); - - BundleBuilder builder = new BundleBuilder(myFhirContext); - builder.setBundleField("type", "searchset"); - builder.setBundleField("id", UUID.randomUUID().toString()); - builder.setMetaField("lastUpdated", builder.newPrimitive("instant", new Date())); - - IBaseBundle retVal = builder.getBundle(); - for (MatchedTarget next : matches) { - boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); - if (!shouldKeepThisEntry) { - continue; - } - - IBase entry = builder.addEntry(); - builder.addToEntry(entry, "resource", next.getTarget()); - - IBaseBackboneElement search = builder.addSearch(entry); - toBundleEntrySearchComponent(builder, search, next); - } - return retVal; - } - - - public IBaseBackboneElement toBundleEntrySearchComponent(BundleBuilder theBuilder, IBaseBackboneElement theSearch, MatchedTarget theMatchedTarget) { - theBuilder.setSearchField(theSearch, "mode", "match"); - double score = theMatchedTarget.getMatchResult().getNormalizedScore(); - theBuilder.setSearchField(theSearch, "score", - theBuilder.newPrimitive("decimal", BigDecimal.valueOf(score))); - - String matchGrade = getMatchGrade(theMatchedTarget); - IBaseDatatype codeType = (IBaseDatatype) myFhirContext.getElementDefinition("code").newInstance(matchGrade); - IBaseExtension searchExtension = theSearch.addExtension(); - searchExtension.setUrl(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE); - searchExtension.setValue(codeType); - - return theSearch; + return myMdmControllerHelper.getMatchesAndPossibleMatchesForResource(theResource, theResourceType.getValueAsString()); } @Operation(name = ProviderConstants.MDM_MERGE_GOLDEN_RESOURCES) @@ -353,17 +313,6 @@ public class MdmProviderDstu3Plus extends BaseMdmProvider { return retval; } - @Nonnull - protected String getMatchGrade(MatchedTarget theTheMatchedTarget) { - String retVal = "probable"; - if (theTheMatchedTarget.isMatch()) { - retVal = "certain"; - } else if (theTheMatchedTarget.isPossibleMatch()) { - retVal = "possible"; - } - return retVal; - } - private String getResourceType(String theParamName, IPrimitiveType theResourceId) { if (theResourceId != null) { return getResourceType(theParamName, theResourceId.getValueAsString()); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java index 4765f045ea6..9494312645d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java @@ -39,7 +39,7 @@ public class MdmProviderLoader { @Autowired private ResourceProviderFactory myResourceProviderFactory; @Autowired - private IMdmMatchFinderSvc myMdmMatchFinderSvc; + private MdmControllerHelper myMdmControllerHelper; @Autowired private IMdmControllerSvc myMdmControllerSvc; @Autowired @@ -54,7 +54,11 @@ public class MdmProviderLoader { case DSTU3: case R4: myResourceProviderFactory.addSupplier(() -> { - myMdmProvider = new MdmProviderDstu3Plus(myFhirContext, myMdmControllerSvc, myMdmMatchFinderSvc, myMdmSubmitSvc, myMdmSettings); + myMdmProvider = new MdmProviderDstu3Plus(myFhirContext, + myMdmControllerSvc, + myMdmControllerHelper, + myMdmSubmitSvc, + myMdmSettings); return myMdmProvider; }); break; From 0c3fb775df3ffa276b639a2ae805a859495322cf Mon Sep 17 00:00:00 2001 From: TipzCM Date: Tue, 16 Nov 2021 10:58:58 -0500 Subject: [PATCH 4/8] 3170 language portion of language code is case insensitive (#3171) * 3170 language portion of language code is case insensitive * 3170 adding changelog * 3170 house keeping Co-authored-by: leif stawnyczy --- ...code-validation-to-be-case-insensitive.yml | 4 ++++ .../CommonCodeSystemsTerminologyService.java | 14 ++++++------- ...mmonCodeSystemsTerminologyServiceTest.java | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3170-allow-language-code-validation-to-be-case-insensitive.yml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3170-allow-language-code-validation-to-be-case-insensitive.yml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3170-allow-language-code-validation-to-be-case-insensitive.yml new file mode 100644 index 00000000000..a1caa2d72b6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3170-allow-language-code-validation-to-be-case-insensitive.yml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 3170 +title: "Fixed language code validation so that it is case insensitive (eg, en-US, en-us, EN-US, EN-us should all work)" diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index f4891ed7398..a196cf85863 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -212,7 +212,6 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) { - Map map; switch (theSystem) { case LANGUAGES_CODESYSTEM_URL: @@ -254,9 +253,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } private LookupCodeResult lookupLanguageCode(String theCode) { - Map languagesMap = myLanguagesLanugageMap; - Map regionsMap = myLanguagesRegionMap; - if (languagesMap == null || regionsMap == null) { + if (myLanguagesLanugageMap == null || myLanguagesRegionMap == null) { initializeBcp47LanguageMap(); } @@ -266,8 +263,10 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { String region; if (hasRegionAndCodeSegments) { - language = myLanguagesLanugageMap.get(theCode.substring(0, langRegionSeparatorIndex)); - region = myLanguagesRegionMap.get(theCode.substring(langRegionSeparatorIndex + 1)); + // we look for languages in lowercase only + // this will allow case insensitivity for language portion of code + language = myLanguagesLanugageMap.get(theCode.substring(0, langRegionSeparatorIndex).toLowerCase()); + region = myLanguagesRegionMap.get(theCode.substring(langRegionSeparatorIndex + 1).toUpperCase()); if (language == null || region == null) { //In case the user provides both a language and a region, they must both be valid for the lookup to succeed. @@ -278,7 +277,8 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } } else { //In case user has only provided a language, we build the lookup from only that. - language = myLanguagesLanugageMap.get(theCode); + //NB: we only use the lowercase version of the language + language = myLanguagesLanugageMap.get(theCode.toLowerCase()); if (language == null) { ourLog.warn("Couldn't find a valid bcp47 language from code: {}", theCode); return buildNotFoundLookupCodeResult(theCode); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java index a099a68ac93..d3d73fbdefe 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -54,6 +55,26 @@ public class CommonCodeSystemsTerminologyServiceTest { assertNull(outcome); } + @Test + public void lookupCode_languageOnlyLookup_isCaseInsensitive() { + IValidationSupport.LookupCodeResult outcomeUpper = mySvc.lookupCode(newSupport(), "urn:ietf:bcp:47", "SGN", "Sign Languages"); + IValidationSupport.LookupCodeResult outcomeLower = mySvc.lookupCode(newSupport(), "urn:ietf:bcp:47", "sgn", "Sign Languages"); + assertNotNull(outcomeUpper); + assertNotNull(outcomeLower); + assertTrue(outcomeLower.isFound()); + assertTrue(outcomeUpper.isFound()); + } + + @Test + public void lookupCode_languageAndRegionLookup_isCaseInsensitive() { + IValidationSupport.LookupCodeResult outcomeUpper = mySvc.lookupCode(newSupport(), "urn:ietf:bcp:47", "EN-US", "English"); + IValidationSupport.LookupCodeResult outcomeLower = mySvc.lookupCode(newSupport(), "urn:ietf:bcp:47", "en-us", "English"); + assertNotNull(outcomeUpper); + assertNotNull(outcomeLower); + assertTrue(outcomeLower.isFound()); + assertTrue(outcomeUpper.isFound()); + } + @Test public void testUcum_ValidateCode_Good() { ValueSet vs = new ValueSet(); From 175958de5e80192fb1aa7a88f8e60e68d3dd0a46 Mon Sep 17 00:00:00 2001 From: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com> Date: Thu, 18 Nov 2021 09:49:16 -0500 Subject: [PATCH 5/8] Issue 3108 terminology large codesystem deletion takes days to complete (#3160) * Add indexes to foreign keys * Add new version enum * Add TermCodeSystem and TermCodeSystemVersion deletion job artifacts * Add Propagation.NOT_SUPPORTED to allow starting jobs from transactional code * New jobs artifacts * Improve comments * Restructure job configurations to avoid bean name collisions * Restructure job configurations to avoid bean name collisions * Use new offline deletion by job * Test using multiple versions * Retrieve only Pids * Revert to master * Maintain deferred functions synchronization as similar as possible to the way it was * Bypass validations to allow calling an index same as a foreign key to make SchemaMigrationTest happy as H2 adds FK indexes automatically * Adjust tests * Add copyrights * Add test * Revert to delete CodeSystemVersions using deferred ITermDeferredStorageSvc * Allow to wait for jobs tentatively * Organize imports * Fix tests * Increase timeout to cover for possible additional heavy DB activity * Add indexes to two remaining non-indexed foreign key fields to avoid much longer transactions which produce locks on heavy DB load * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3108-terminology-large-code-system-deletion-performance.yaml Co-authored-by: James Agnew * Remove opening stars from logging messages * Relax index name checking logic to allow for indexes on FK columns to be called same as the FKs, to make SchemaMigrationTest happy even when H2 adds indexes to FKs automatically. * Use static array to enumerate platforms which doesn't add indexes to foreign keys automatically. * Cleanup also job executions when performing test artifact cleanup * Adjust to property name change * Add ABANDONED status to the list of waited job statuses to avoid test failures due to wait timeouts * Add also UNKNOWN status to the list of waited job statuses to avoid test failures due to wait timeouts * Filter job executions to be stopped * Set trace to debug test problem * Add STOPPED status to the list of waited job statuses to avoid test failures due to wait timeouts Co-authored-by: juan.marchionatto Co-authored-by: James Agnew --- .../fhir/jpa/batch/config/BatchConstants.java | 25 ++ .../jpa/batch/svc/BatchJobSubmitterImpl.java | 3 + ...arge-code-system-deletion-performance.yaml | 5 + .../java/ca/uhn/fhir/jpa/util/TestUtil.java | 19 +- .../uhn/fhir/jpa/batch/BatchJobsConfig.java | 6 +- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 20 ++ .../dao/data/ITermCodeSystemVersionDao.java | 4 +- .../fhir/jpa/dao/data/ITermConceptDao.java | 7 +- .../dao/data/ITermConceptDesignationDao.java | 11 +- .../data/ITermConceptParentChildLinkDao.java | 11 +- .../jpa/dao/data/ITermConceptPropertyDao.java | 8 +- .../jpa/entity/TermConceptDesignation.java | 5 +- .../entity/TermConceptParentChildLink.java | 3 + .../fhir/jpa/entity/TermConceptProperty.java | 5 +- .../tasks/HapiFhirJpaMigrationTasks.java | 46 +++ .../term/TermCodeSystemStorageSvcImpl.java | 72 +---- .../jpa/term/TermDeferredStorageSvcImpl.java | 187 ++++++------ .../term/api/ITermCodeSystemStorageSvc.java | 5 - .../jpa/term/api/ITermDeferredStorageSvc.java | 2 - .../BaseTermCodeSystemDeleteJobConfig.java | 62 ++++ .../BatchConceptRelationsDeleteWriter.java | 66 +++++ ...rmCodeSystemUniqueVersionDeleteReader.java | 54 ++++ ...atchTermCodeSystemVersionDeleteReader.java | 75 +++++ ...atchTermCodeSystemVersionDeleteWriter.java | 77 +++++ .../job/BatchTermConceptsDeleteWriter.java | 51 ++++ .../job/TermCodeSystemDeleteJobConfig.java | 122 ++++++++ ...CodeSystemDeleteJobParameterValidator.java | 53 ++++ .../term/job/TermCodeSystemDeleteTasklet.java | 60 ++++ .../TermCodeSystemVersionDeleteJobConfig.java | 105 +++++++ ...temVersionDeleteJobParameterValidator.java | 54 ++++ .../term/job/TermConceptDeleteTasklet.java | 62 ++++ .../FhirResourceDaoDstu3CodeSystemTest.java | 10 + .../r4/FhirResourceDaoR4CodeSystemTest.java | 9 + .../jpa/dao/r4/FhirResourceDaoR4Test.java | 8 + .../r5/FhirResourceDaoR5CodeSystemTest.java | 12 +- .../ResourceProviderDstu3CodeSystemTest.java | 7 + .../term/TermCodeSystemStorageSvcTest.java | 8 + .../term/TermDeferredStorageSvcImplTest.java | 21 ++ .../TerminologyLoaderSvcLoincJpaTest.java | 10 +- ...erminologySvcImplCurrentVersionR4Test.java | 36 ++- .../jpa/term/TerminologySvcImplR4Test.java | 7 + .../fhir/jpa/term/ZipCollectionBuilder.java | 2 +- .../jpa/term/job/DynamicJobFlowSandbox.java | 123 ++++++++ .../term/job/TermCodeSystemDeleteJobTest.java | 266 +++++++++++++++++ .../TermCodeSystemVersionDeleteJobTest.java | 272 ++++++++++++++++++ .../fhir/test/utilities/BatchJobHelper.java | 27 +- 46 files changed, 1891 insertions(+), 212 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3108-terminology-large-code-system-deletion-performance.yaml create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BaseTermCodeSystemDeleteJobConfig.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchConceptRelationsDeleteWriter.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemUniqueVersionDeleteReader.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteReader.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteWriter.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermConceptsDeleteWriter.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobConfig.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobParameterValidator.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteTasklet.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobConfig.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobParameterValidator.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermConceptDeleteTasklet.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/DynamicJobFlowSandbox.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobTest.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobTest.java diff --git a/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/BatchConstants.java b/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/BatchConstants.java index f5b77e53211..b5dbd4c0c7d 100644 --- a/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/BatchConstants.java +++ b/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/BatchConstants.java @@ -54,6 +54,31 @@ public final class BatchConstants { * MDM Clear */ public static final String MDM_CLEAR_JOB_NAME = "mdmClearJob"; + + /** + * TermCodeSystem delete + */ + public static final String TERM_CODE_SYSTEM_DELETE_JOB_NAME = "termCodeSystemDeleteJob"; + public static final String TERM_CONCEPT_RELATIONS_DELETE_STEP_NAME = "termConceptRelationsDeleteStep"; + public static final String TERM_CONCEPTS_DELETE_STEP_NAME = "termConceptsDeleteStep"; + public static final String TERM_CODE_SYSTEM_VERSION_DELETE_STEP_NAME = "termCodeSystemVersionDeleteStep"; + public static final String TERM_CODE_SYSTEM_DELETE_STEP_NAME = "termCodeSystemDeleteStep"; + public static final String JOB_PARAM_CODE_SYSTEM_ID = "termCodeSystemPid"; + + /** + * TermCodeSystemVersion delete + */ + public static final String TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME = "termCodeSystemVersionDeleteJob"; + public static final String TERM_CONCEPT_RELATIONS_UNIQUE_VERSION_DELETE_STEP_NAME = "termConceptRelationsUniqueVersionDeleteStep"; + public static final String TERM_CONCEPTS_UNIQUE_VERSION_DELETE_STEP_NAME = "termConceptsUniqueVersionDeleteStep"; + public static final String TERM_CODE_SYSTEM_UNIQUE_VERSION_DELETE_STEP_NAME = "termCodeSystemUniqueVersionDeleteStep"; + + /** + * Both: TermCodeSystem delete and TermCodeSystemVersion delete + */ + public static final String JOB_PARAM_CODE_SYSTEM_VERSION_ID = "termCodeSystemVersionPid"; + + public static final String BULK_EXPORT_READ_CHUNK_PARAMETER = "readChunkSize"; public static final String BULK_EXPORT_GROUP_ID_PARAMETER = "groupId"; /** diff --git a/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/svc/BatchJobSubmitterImpl.java b/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/svc/BatchJobSubmitterImpl.java index 52625ec2c12..189870fa5c0 100644 --- a/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/svc/BatchJobSubmitterImpl.java +++ b/hapi-fhir-batch/src/main/java/ca/uhn/fhir/jpa/batch/svc/BatchJobSubmitterImpl.java @@ -32,6 +32,8 @@ import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteExcep import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import static org.slf4j.LoggerFactory.getLogger; @@ -46,6 +48,7 @@ public class BatchJobSubmitterImpl implements IBatchJobSubmitter { private JobRepository myJobRepository; @Override + @Transactional(propagation = Propagation.NOT_SUPPORTED) public JobExecution runJob(Job theJob, JobParameters theJobParameters) throws JobParametersInvalidException { try { return myJobLauncher.run(theJob, theJobParameters); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3108-terminology-large-code-system-deletion-performance.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3108-terminology-large-code-system-deletion-performance.yaml new file mode 100644 index 00000000000..14f9d905b29 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3108-terminology-large-code-system-deletion-performance.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 3108 +title: "Code System deletion background tasks were taking over a day to complete on very large CodeSystems for PostgreSQL, SQL Server and Oracle + databases. That was improved now taking less than an hour in all three platforms" diff --git a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index 9c8a4795c7c..d2a54c0b224 100644 --- a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import com.google.common.reflect.ClassPath; import com.google.common.reflect.ClassPath.ClassInfo; import org.apache.commons.io.IOUtils; @@ -74,6 +75,17 @@ public class TestUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class); private static Set ourReservedWords; + + // Exceptions set because H2 sets indexes for FKs automatically so this index had to be called as the target FK field + // it is indexing to avoid SchemaMigrationTest to complain about the extra index (which doesn't exist in H2) + private static final Set duplicateNameValidationExceptionList = Sets.newHashSet( + "FK_CONCEPTPROP_CONCEPT", + "FK_CONCEPTDESIG_CONCEPT", + "FK_TERM_CONCEPTPC_CHILD", + "FK_TERM_CONCEPTPC_PARENT" + ); + + /** * non instantiable */ @@ -252,7 +264,8 @@ public class TestUtil { } for (Index nextConstraint : table.indexes()) { assertNotADuplicateName(nextConstraint.name(), theNames); - Validate.isTrue(nextConstraint.name().startsWith("IDX_"), nextConstraint.name() + " must start with IDX_"); + Validate.isTrue(nextConstraint.name().startsWith("IDX_") || nextConstraint.name().startsWith("FK_"), + nextConstraint.name() + " must start with IDX_ or FK_ (last one when indexing a FK column)"); } } @@ -269,7 +282,9 @@ public class TestUtil { Validate.notNull(fk); Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + theAnnotatedElement + " has no name()"); Validate.isTrue(fk.name().startsWith("FK_")); - assertNotADuplicateName(fk.name(), theNames); + if ( ! duplicateNameValidationExceptionList.contains(fk.name())) { + assertNotADuplicateName(fk.name(), theNames); + } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java index f95d515ad18..d4effba64c5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java @@ -26,6 +26,8 @@ import ca.uhn.fhir.jpa.bulk.imprt.job.BulkImportJobConfig; import ca.uhn.fhir.jpa.delete.job.DeleteExpungeJobConfig; import ca.uhn.fhir.jpa.reindex.job.ReindexEverythingJobConfig; import ca.uhn.fhir.jpa.reindex.job.ReindexJobConfig; +import ca.uhn.fhir.jpa.term.job.TermCodeSystemDeleteJobConfig; +import ca.uhn.fhir.jpa.term.job.TermCodeSystemVersionDeleteJobConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -38,7 +40,9 @@ import org.springframework.context.annotation.Import; DeleteExpungeJobConfig.class, ReindexJobConfig.class, ReindexEverythingJobConfig.class, - MdmClearJobConfig.class + MdmClearJobConfig.class, + TermCodeSystemDeleteJobConfig.class, + TermCodeSystemVersionDeleteJobConfig.class }) public class BatchJobsConfig { } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index a7e26212c3e..2438613eb34 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -155,8 +155,13 @@ import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValid import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; +import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.BatchConfigurer; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.support.SimpleJobOperator; +import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; @@ -229,6 +234,9 @@ public abstract class BaseConfig { private Integer searchCoordMaxPoolSize = 100; private Integer searchCoordQueueCapacity = 200; + @Autowired + private JobLauncher myJobLauncher; + /** * Subclasses may override this method to provide settings such as search coordinator pool sizes. */ @@ -279,6 +287,18 @@ public abstract class BaseConfig { return new CascadingDeleteInterceptor(theFhirContext, theDaoRegistry, theInterceptorBroadcaster); } + @Bean + public SimpleJobOperator jobOperator(JobExplorer jobExplorer, JobRepository jobRepository, JobRegistry jobRegistry) { + SimpleJobOperator jobOperator = new SimpleJobOperator(); + + jobOperator.setJobExplorer(jobExplorer); + jobOperator.setJobRepository(jobRepository); + jobOperator.setJobRegistry(jobRegistry); + jobOperator.setJobLauncher(myJobLauncher); + + return jobOperator; + } + @Lazy @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemVersionDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemVersionDao.java index e622d84daa5..404b985d5ca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemVersionDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemVersionDao.java @@ -35,8 +35,8 @@ public interface ITermCodeSystemVersionDao extends JpaRepository findByCodeSystemPid(@Param("codesystem_pid") Long theCodeSystemPid); + @Query("SELECT myId FROM TermCodeSystemVersion WHERE myCodeSystemPid = :codesystem_pid order by myId") + List findSortedPidsByCodeSystemPid(@Param("codesystem_pid") Long theCodeSystemPid); @Query("SELECT cs FROM TermCodeSystemVersion cs WHERE cs.myCodeSystemPid = :codesystem_pid AND cs.myCodeSystemVersionId = :codesystem_version_id") TermCodeSystemVersion findByCodeSystemPidAndVersion(@Param("codesystem_pid") Long theCodeSystemPid, @Param("codesystem_version_id") String theCodeSystemVersionId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java index c45b79fbb87..bd15c79484b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java @@ -4,8 +4,8 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -40,8 +40,9 @@ public interface ITermConceptDao extends JpaRepository, IHapi @Query("SELECT c FROM TermConcept c WHERE c.myCodeSystem = :code_system AND c.myCode = :code") Optional findByCodeSystemAndCode(@Param("code_system") TermCodeSystemVersion theCodeSystem, @Param("code") String theCode); - @Query("SELECT t.myId FROM TermConcept t WHERE t.myCodeSystem.myId = :cs_pid") - Slice findIdsByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + @Modifying + @Query("DELETE FROM TermConcept WHERE myCodeSystem.myId = :cs_pid") + int deleteByCodeSystemVersion(@Param("cs_pid") Long thePid); @Query("SELECT c FROM TermConcept c WHERE c.myCodeSystem = :code_system") List findByCodeSystemVersion(@Param("code_system") TermCodeSystemVersion theCodeSystem); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java index 9d3db62f3d8..8c68741334e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java @@ -1,9 +1,8 @@ package ca.uhn.fhir.jpa.dao.data; import ca.uhn.fhir.jpa.entity.TermConceptDesignation; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -29,10 +28,8 @@ import org.springframework.data.repository.query.Param; public interface ITermConceptDesignationDao extends JpaRepository, IHapiFhirJpaRepository { - @Query("SELECT t.myId FROM TermConceptDesignation t WHERE t.myCodeSystemVersion.myId = :csv_pid") - Slice findIdsByCodeSystemVersion(Pageable thePage, @Param("csv_pid") Long thePid); - - @Query("SELECT COUNT(t) FROM TermConceptDesignation t WHERE t.myCodeSystemVersion.myId = :csv_pid") - Integer countByCodeSystemVersion(@Param("csv_pid") Long thePid); + @Modifying + @Query("DELETE FROM TermConceptDesignation WHERE myCodeSystemVersion.myId = :csv_pid") + int deleteByCodeSystemVersion(@Param("csv_pid") Long thePid); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java index 4a089d9207e..a88e7f4cc23 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java @@ -1,9 +1,8 @@ package ca.uhn.fhir.jpa.dao.data; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -31,13 +30,11 @@ import java.util.Collection; public interface ITermConceptParentChildLinkDao extends JpaRepository, IHapiFhirJpaRepository { - @Query("SELECT COUNT(t) FROM TermConceptParentChildLink t WHERE t.myCodeSystem.myId = :cs_pid") - Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); - @Query("SELECT t.myParentPid FROM TermConceptParentChildLink t WHERE t.myChildPid = :child_pid") Collection findAllWithChild(@Param("child_pid") Long theConceptPid); - @Query("SELECT t.myPid FROM TermConceptParentChildLink t WHERE t.myCodeSystem.myId = :cs_pid") - Slice findIdsByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + @Modifying + @Query("DELETE FROM TermConceptParentChildLink WHERE myCodeSystemVersionPid = :cs_pid") + int deleteByCodeSystemVersion(@Param("cs_pid") Long thePid); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java index a734ea83c69..b03b39428f2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java @@ -1,9 +1,8 @@ package ca.uhn.fhir.jpa.dao.data; import ca.uhn.fhir.jpa.entity.TermConceptProperty; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -29,8 +28,9 @@ import org.springframework.data.repository.query.Param; public interface ITermConceptPropertyDao extends JpaRepository, IHapiFhirJpaRepository { - @Query("SELECT t.myId FROM TermConceptProperty t WHERE t.myCodeSystemVersion.myId = :cs_pid") - Slice findIdsByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + @Modifying + @Query("DELETE FROM TermConceptProperty WHERE myCodeSystemVersion.myId = :cs_pid") + int deleteByCodeSystemVersion(@Param("cs_pid") Long thePid); @Query("SELECT COUNT(t) FROM TermConceptProperty t WHERE t.myCodeSystemVersion.myId = :cs_pid") Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java index 1ca7ad136fb..a2998fab590 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java @@ -30,8 +30,9 @@ import static org.apache.commons.lang3.StringUtils.left; import static org.apache.commons.lang3.StringUtils.length; @Entity -@Table(name = "TRM_CONCEPT_DESIG", uniqueConstraints = { -}, indexes = { +@Table(name = "TRM_CONCEPT_DESIG", uniqueConstraints = { }, indexes = { + // must have same name that indexed FK or SchemaMigrationTest complains because H2 sets this index automatically + @Index(name = "FK_CONCEPTDESIG_CONCEPT", columnList = "CONCEPT_PID", unique = false) }) public class TermConceptDesignation implements Serializable { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java index 24af4d17256..8b75913bd4a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java @@ -27,6 +27,9 @@ import java.io.Serializable; @Entity @Table(name = "TRM_CONCEPT_PC_LINK", indexes = { + // must have same name that indexed FK or SchemaMigrationTest complains because H2 sets this index automatically + @Index(name = "FK_TERM_CONCEPTPC_CHILD", columnList = "CHILD_PID", unique = false), + @Index(name = "FK_TERM_CONCEPTPC_PARENT", columnList = "PARENT_PID", unique = false) }) public class TermConceptParentChildLink implements Serializable { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java index a736542f1bf..d9f808b55aa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java @@ -36,6 +36,7 @@ import javax.persistence.ForeignKey; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Index; import javax.persistence.JoinColumn; import javax.persistence.Lob; import javax.persistence.ManyToOne; @@ -48,7 +49,9 @@ import static org.apache.commons.lang3.StringUtils.left; import static org.apache.commons.lang3.StringUtils.length; @Entity -@Table(name = "TRM_CONCEPT_PROPERTY", uniqueConstraints = { +@Table(name = "TRM_CONCEPT_PROPERTY", uniqueConstraints = { }, indexes = { + // must have same name that indexed FK or SchemaMigrationTest complains because H2 sets this index automatically + @Index(name = "FK_CONCEPTPROP_CONCEPT", columnList = "CONCEPT_PID", unique = false) }) public class TermConceptProperty implements Serializable { public static final int MAX_PROPTYPE_ENUM_LENGTH = 6; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index cc14926567f..ea8b21df6c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -41,16 +41,26 @@ import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import ca.uhn.fhir.util.VersionEnum; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; @SuppressWarnings({"SqlNoDataSourceInspection", "SpellCheckingInspection"}) public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { private final Set myFlags; + // H2, Derby, MariaDB, and MySql automatically add indexes to foreign keys + public static final DriverTypeEnum[] NON_AUTOMATIC_FK_INDEX_PLATFORMS = new DriverTypeEnum[] { + DriverTypeEnum.POSTGRES_9_4, DriverTypeEnum.ORACLE_12C, DriverTypeEnum.MSSQL_2012 }; + + /** * Constructor */ @@ -76,8 +86,44 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init540(); // 20210218 - 20210520 init550(); // 20210520 - init560(); // 20211027 - + init570(); // 20211102 - } + + private void init570() { + Builder version = forVersion(VersionEnum.V5_7_0); + + // both indexes must have same name that indexed FK or SchemaMigrationTest complains because H2 sets this index automatically + + version.onTable("TRM_CONCEPT_PROPERTY") + .addIndex("20211102.1", "FK_CONCEPTPROP_CONCEPT") + .unique(false) + .withColumns("CONCEPT_PID") + .onlyAppliesToPlatforms(NON_AUTOMATIC_FK_INDEX_PLATFORMS); + + version.onTable("TRM_CONCEPT_DESIG") + .addIndex("20211102.2", "FK_CONCEPTDESIG_CONCEPT") + .unique(false) + .withColumns("CONCEPT_PID") + // H2, Derby, MariaDB, and MySql automatically add indexes to foreign keys + .onlyAppliesToPlatforms(NON_AUTOMATIC_FK_INDEX_PLATFORMS); + + version.onTable("TRM_CONCEPT_PC_LINK") + .addIndex("20211102.3", "FK_TERM_CONCEPTPC_CHILD") + .unique(false) + .withColumns("CHILD_PID") + // H2, Derby, MariaDB, and MySql automatically add indexes to foreign keys + .onlyAppliesToPlatforms(NON_AUTOMATIC_FK_INDEX_PLATFORMS); + + version.onTable("TRM_CONCEPT_PC_LINK") + .addIndex("20211102.4", "FK_TERM_CONCEPTPC_PARENT") + .unique(false) + .withColumns("PARENT_PID") + // H2, Derby, MariaDB, and MySql automatically add indexes to foreign keys + .onlyAppliesToPlatforms(NON_AUTOMATIC_FK_INDEX_PLATFORMS); + } + + private void init560() { init560_20211027(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java index 10b50bccab5..bedd92327c1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; @@ -59,8 +60,9 @@ import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.ValueSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.batch.core.Job; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -86,6 +88,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW; @@ -108,8 +111,6 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { @Autowired protected IdHelperService myIdHelperService; @Autowired - private PlatformTransactionManager myTransactionManager; - @Autowired private ITermConceptParentChildLinkDao myConceptParentChildLinkDao; @Autowired private ITermVersionAdapterSvc myTerminologyVersionAdapterSvc; @@ -124,6 +125,13 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { @Autowired private IResourceTableDao myResourceTableDao; + @Autowired + private IBatchJobSubmitter myJobSubmitter; + + @Autowired @Qualifier(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME) + private Job myTermCodeSystemVersionDeleteJob; + + @Override public ResourcePersistentId getValueSetResourcePid(IIdType theIdType) { return myIdHelperService.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), theIdType.getResourceType(), theIdType.getIdPart()); @@ -262,44 +270,6 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { return childTermConcepts; } - @Override - @Transactional - public void deleteCodeSystem(TermCodeSystem theCodeSystem) { - assert TransactionSynchronizationManager.isActualTransactionActive(); - - ourLog.info(" * Deleting code system {}", theCodeSystem.getPid()); - - myEntityManager.flush(); - TermCodeSystem cs = myCodeSystemDao.findById(theCodeSystem.getPid()).orElseThrow(IllegalStateException::new); - cs.setCurrentVersion(null); - myCodeSystemDao.save(cs); - myCodeSystemDao.flush(); - - List codeSystemVersions = myCodeSystemVersionDao.findByCodeSystemPid(theCodeSystem.getPid()); - List codeSystemVersionPids = codeSystemVersions - .stream() - .map(TermCodeSystemVersion::getPid) - .collect(Collectors.toList()); - for (Long next : codeSystemVersionPids) { - deleteCodeSystemVersion(next); - } - - myCodeSystemVersionDao.deleteForCodeSystem(theCodeSystem); - myCodeSystemDao.delete(theCodeSystem); - myEntityManager.flush(); - } - - @Override - @Transactional(propagation = Propagation.NEVER) - public void deleteCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion) { - assert !TransactionSynchronizationManager.isActualTransactionActive(); - - // Delete TermCodeSystemVersion - ourLog.info(" * Deleting TermCodeSystemVersion {}", theCodeSystemVersion.getCodeSystemVersionId()); - deleteCodeSystemVersion(theCodeSystemVersion.getPid()); - - } - /** * Returns the number of saved concepts */ @@ -512,26 +482,6 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { return existing; } - private void deleteCodeSystemVersion(final Long theCodeSystemVersionPid) { - assert TransactionSynchronizationManager.isActualTransactionActive(); - ourLog.info(" * Marking code system version {} for deletion", theCodeSystemVersionPid); - - Optional codeSystemOpt = myCodeSystemDao.findWithCodeSystemVersionAsCurrentVersion(theCodeSystemVersionPid); - if (codeSystemOpt.isPresent()) { - TermCodeSystem codeSystem = codeSystemOpt.get(); - if (codeSystem.getCurrentVersion() != null && codeSystem.getCurrentVersion().getPid().equals(theCodeSystemVersionPid)) { - ourLog.info(" * Removing code system version {} as current version of code system {}", theCodeSystemVersionPid, codeSystem.getPid()); - codeSystem.setCurrentVersion(null); - myCodeSystemDao.save(codeSystem); - } - } - - TermCodeSystemVersion codeSystemVersion = myCodeSystemVersionDao.findById(theCodeSystemVersionPid).orElseThrow(() -> new IllegalStateException()); - codeSystemVersion.setCodeSystemVersionId("DELETED_" + UUID.randomUUID().toString()); - myCodeSystemVersionDao.save(codeSystemVersion); - - myDeferredStorageSvc.deleteCodeSystemVersion(codeSystemVersion); - } private void validateDstu3OrNewer() { Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "Terminology operations only supported in DSTU3+ mode"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java index 6e36e2fb331..fb3da576fda 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java @@ -20,12 +20,11 @@ package ca.uhn.fhir.jpa.term; * #L% */ +import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; -import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; -import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao; import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; @@ -37,6 +36,7 @@ import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; @@ -45,10 +45,15 @@ import org.hl7.fhir.r4.model.ValueSet; import org.quartz.JobExecutionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.launch.JobExecutionNotRunningException; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.launch.NoSuchJobExecutionException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -59,13 +64,17 @@ import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Queue; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_VERSION_ID; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; + public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { private static final Logger ourLog = LoggerFactory.getLogger(TermDeferredStorageSvcImpl.class); @@ -75,6 +84,9 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { final private List myDeferredValueSets = Collections.synchronizedList(new ArrayList<>()); final private List myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>()); final private List myConceptLinksToSaveLater = Collections.synchronizedList(new ArrayList<>()); + final private List myCurrentJobExecutions = Collections.synchronizedList(new ArrayList<>()); + + @Autowired protected ITermConceptDao myConceptDao; @Autowired @@ -83,10 +95,6 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { protected ITermCodeSystemVersionDao myCodeSystemVersionDao; @Autowired protected PlatformTransactionManager myTransactionMgr; - @Autowired - protected ITermConceptPropertyDao myConceptPropertyDao; - @Autowired - protected ITermConceptDesignationDao myConceptDesignationDao; private boolean myProcessDeferred = true; @Autowired private ITermConceptParentChildLinkDao myConceptParentChildLinkDao; @@ -97,6 +105,19 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { @Autowired private ITermCodeSystemStorageSvc myCodeSystemStorageSvc; + @Autowired + private IBatchJobSubmitter myJobSubmitter; + + @Autowired + private JobOperator myJobOperator; + + @Autowired @Qualifier(TERM_CODE_SYSTEM_DELETE_JOB_NAME) + private org.springframework.batch.core.Job myTermCodeSystemDeleteJob; + + @Autowired @Qualifier(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME) + private org.springframework.batch.core.Job myTermCodeSystemVersionDeleteJob; + + @Override public void addConceptToStorageQueue(TermConcept theConcept) { Validate.notNull(theConcept); @@ -122,28 +143,26 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { } @Override - @Transactional - public void deleteCodeSystem(TermCodeSystem theCodeSystem) { - theCodeSystem.setCodeSystemUri("urn:uuid:" + UUID.randomUUID().toString()); - myCodeSystemDao.save(theCodeSystem); - myDeferredCodeSystemsDeletions.add(theCodeSystem); - } - - @Override - @Transactional public void deleteCodeSystemForResource(ResourceTable theCodeSystemToDelete) { + // there are use cases (at least in tests) where the code system is not present for the resource but versions are, + // so, as code system deletion also deletes versions, we try the system first but if not present we also try versions + TermCodeSystem termCodeSystemToDelete = myCodeSystemDao.findByResourcePid(theCodeSystemToDelete.getResourceId()); + if (termCodeSystemToDelete != null) { + termCodeSystemToDelete.setCodeSystemUri("urn:uuid:" + UUID.randomUUID()); + myCodeSystemDao.save(termCodeSystemToDelete); + myDeferredCodeSystemsDeletions.add(termCodeSystemToDelete); + return; + } + List codeSystemVersionsToDelete = myCodeSystemVersionDao.findByCodeSystemResourcePid(theCodeSystemToDelete.getResourceId()); for (TermCodeSystemVersion codeSystemVersionToDelete : codeSystemVersionsToDelete) { if (codeSystemVersionToDelete != null) { myDeferredCodeSystemVersionsDeletions.add(codeSystemVersionToDelete); } } - TermCodeSystem codeSystemToDelete = myCodeSystemDao.findByResourcePid(theCodeSystemToDelete.getResourceId()); - if (codeSystemToDelete != null) { - deleteCodeSystem(codeSystemToDelete); - } } + @Override public void setProcessDeferred(boolean theProcessDeferred) { myProcessDeferred = theProcessDeferred; @@ -236,14 +255,26 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { myDeferredCodeSystemsDeletions.clear(); myConceptLinksToSaveLater.clear(); myDeferredCodeSystemVersionsDeletions.clear(); + clearJobExecutions(); } - private void runInTransaction(Runnable theRunnable) { - assert !TransactionSynchronizationManager.isActualTransactionActive(); - new TransactionTemplate(myTransactionMgr).executeWithoutResult(tx -> theRunnable.run()); + private void clearJobExecutions() { + for (JobExecution jobExecution : myCurrentJobExecutions) { + if (! jobExecution.isRunning()) { continue; } + + try { + myJobOperator.stop(jobExecution.getId()); + + } catch (NoSuchJobExecutionException | JobExecutionNotRunningException theE) { + ourLog.error("Couldn't stop job execution {}: {}", jobExecution.getId(), theE); + } + } + + myCurrentJobExecutions.clear(); } + private T runInTransaction(Supplier theRunnable) { assert !TransactionSynchronizationManager.isActualTransactionActive(); @@ -315,98 +346,53 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { return !myDeferredCodeSystemVersionsDeletions.isEmpty(); } + private void processDeferredCodeSystemDeletions() { for (TermCodeSystem next : myDeferredCodeSystemsDeletions) { - myCodeSystemStorageSvc.deleteCodeSystem(next); + deleteTermCodeSystemOffline(next.getPid()); } myDeferredCodeSystemsDeletions.clear(); } + private void processDeferredCodeSystemVersionDeletions() { for (TermCodeSystemVersion next : myDeferredCodeSystemVersionsDeletions) { - processDeferredCodeSystemVersionDeletions(next.getPid()); + deleteTermCodeSystemVersionOffline(next.getPid()); } - myDeferredCodeSystemVersionsDeletions.clear(); } - private void processDeferredCodeSystemVersionDeletions(long theCodeSystemVersionPid) { - assert !TransactionSynchronizationManager.isActualTransactionActive(); - ourLog.info(" * Deleting CodeSystemVersion[id={}]", theCodeSystemVersionPid); - PageRequest page1000 = PageRequest.of(0, 1000); + private void deleteTermCodeSystemVersionOffline(Long theCodeSystemVersionPid) { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_VERSION_ID, new JobParameter(theCodeSystemVersionPid, true) )); - // Parent/Child links - { - String descriptor = "parent/child links"; - Supplier> loader = () -> myConceptParentChildLinkDao.findIdsByCodeSystemVersion(page1000, theCodeSystemVersionPid); - Supplier counter = () -> myConceptParentChildLinkDao.countByCodeSystemVersion(theCodeSystemVersionPid); - doDelete(descriptor, loader, counter, myConceptParentChildLinkDao); + try { + + JobExecution jobExecution = myJobSubmitter.runJob(myTermCodeSystemVersionDeleteJob, jobParameters); + myCurrentJobExecutions.add(jobExecution); + + } catch (JobParametersInvalidException theE) { + throw new InternalErrorException("Offline job submission for TermCodeSystemVersion: " + + theCodeSystemVersionPid + " failed: " + theE); } - - // Properties - { - String descriptor = "concept properties"; - Supplier> loader = () -> myConceptPropertyDao.findIdsByCodeSystemVersion(page1000, theCodeSystemVersionPid); - Supplier counter = () -> myConceptPropertyDao.countByCodeSystemVersion(theCodeSystemVersionPid); - doDelete(descriptor, loader, counter, myConceptPropertyDao); - } - - // Designations - { - String descriptor = "concept designations"; - Supplier> loader = () -> myConceptDesignationDao.findIdsByCodeSystemVersion(page1000, theCodeSystemVersionPid); - Supplier counter = () -> myConceptDesignationDao.countByCodeSystemVersion(theCodeSystemVersionPid); - doDelete(descriptor, loader, counter, myConceptDesignationDao); - } - - // Concepts - { - String descriptor = "concepts"; - // For some reason, concepts are much slower to delete, so use a smaller batch size - PageRequest page100 = PageRequest.of(0, 100); - Supplier> loader = () -> myConceptDao.findIdsByCodeSystemVersion(page100, theCodeSystemVersionPid); - Supplier counter = () -> myConceptDao.countByCodeSystemVersion(theCodeSystemVersionPid); - doDelete(descriptor, loader, counter, myConceptDao); - } - - runInTransaction(() -> { - Optional codeSystemOpt = myCodeSystemDao.findWithCodeSystemVersionAsCurrentVersion(theCodeSystemVersionPid); - if (codeSystemOpt.isPresent()) { - TermCodeSystem codeSystem = codeSystemOpt.get(); - ourLog.info(" * Removing code system version {} as current version of code system {}", theCodeSystemVersionPid, codeSystem.getPid()); - codeSystem.setCurrentVersion(null); - myCodeSystemDao.save(codeSystem); - } - - ourLog.info(" * Deleting code system version"); - Optional csv = myCodeSystemVersionDao.findById(theCodeSystemVersionPid); - if (csv.isPresent()) { - myCodeSystemVersionDao.delete(csv.get()); - } - }); - - } - private void doDelete(String theDescriptor, Supplier> theLoader, Supplier theCounter, JpaRepository theDao) { - assert !TransactionSynchronizationManager.isActualTransactionActive(); - int count; - ourLog.info(" * Deleting {}", theDescriptor); - int totalCount = runInTransaction(theCounter); - StopWatch sw = new StopWatch(); - count = 0; - while (true) { - Slice link = runInTransaction(theLoader); - if (!link.hasContent()) { - break; - } + private void deleteTermCodeSystemOffline(Long theCodeSystemPid) { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_ID, new JobParameter(theCodeSystemPid, true) )); - runInTransaction(() -> link.forEach(theDao::deleteById)); + try { - count += link.getNumberOfElements(); - ourLog.info(" * {} {} deleted ({}/{}) remaining - {}/sec - ETA: {}", count, theDescriptor, count, totalCount, sw.formatThroughput(count, TimeUnit.SECONDS), sw.getEstimatedTimeRemaining(count, totalCount)); + JobExecution jobExecution = myJobSubmitter.runJob(myTermCodeSystemDeleteJob, jobParameters); + myCurrentJobExecutions.add(jobExecution); + + } catch (JobParametersInvalidException theE) { + throw new InternalErrorException("Offline job submission for TermCodeSystem: " + + theCodeSystemPid + " failed: " + theE); } } @@ -419,9 +405,14 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { retVal &= !isDeferredValueSets(); retVal &= !isDeferredConceptMaps(); retVal &= !isDeferredCodeSystemDeletions(); + retVal &= !isJobsExecuting(); return retVal; } + private boolean isJobsExecuting() { + return myCurrentJobExecutions.stream().anyMatch(JobExecution::isRunning); + } + private void saveConceptLink(TermConceptParentChildLink next) { if (next.getId() == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermCodeSystemStorageSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermCodeSystemStorageSvc.java index 1f85e823720..e5b0b4c131b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermCodeSystemStorageSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermCodeSystemStorageSvc.java @@ -51,11 +51,6 @@ public interface ITermCodeSystemStorageSvc { (boolean) theRequestDetails.getUserData().getOrDefault(MAKE_LOADING_VERSION_CURRENT, Boolean.TRUE); } - void deleteCodeSystem(TermCodeSystem theCodeSystem); - - void deleteCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion); - - void storeNewCodeSystemVersion(ResourcePersistentId theCodeSystemResourcePid, String theSystemUri, String theSystemName, String theSystemVersionId, TermCodeSystemVersion theCodeSystemVersion, ResourceTable theCodeSystemResourceTable, RequestDetails theRequestDetails); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermDeferredStorageSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermDeferredStorageSvc.java index 91cdb11b7f6..944a9c8bd76 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermDeferredStorageSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermDeferredStorageSvc.java @@ -54,8 +54,6 @@ public interface ITermDeferredStorageSvc { void addValueSetsToStorageQueue(List theValueSets); - void deleteCodeSystem(TermCodeSystem theCodeSystem); - void deleteCodeSystemForResource(ResourceTable theCodeSystemResourceToDelete); void deleteCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BaseTermCodeSystemDeleteJobConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BaseTermCodeSystemDeleteJobConfig.java new file mode 100644 index 00000000000..fd9a45c7cff --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BaseTermCodeSystemDeleteJobConfig.java @@ -0,0 +1,62 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration artifacts common to TermCodeSystemDeleteJobConfig and TermCodeSystemVersionDeleteJobConfig + **/ +@Configuration +public class BaseTermCodeSystemDeleteJobConfig { + + protected static final int TERM_CONCEPT_DELETE_TIMEOUT = 60 * 2; // two minutes + + @Autowired + protected JobBuilderFactory myJobBuilderFactory; + + @Autowired + protected StepBuilderFactory myStepBuilderFactory; + + + @Bean + public BatchTermCodeSystemVersionDeleteWriter batchTermCodeSystemVersionDeleteWriter() { + return new BatchTermCodeSystemVersionDeleteWriter(); + } + + @Bean + public BatchConceptRelationsDeleteWriter batchConceptRelationsDeleteWriter() { + return new BatchConceptRelationsDeleteWriter(); + } + + @Bean + public BatchTermConceptsDeleteWriter batchTermConceptsDeleteWriter() { + return new BatchTermConceptsDeleteWriter(); + } + + + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchConceptRelationsDeleteWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchConceptRelationsDeleteWriter.java new file mode 100644 index 00000000000..dbcfa3e2aca --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchConceptRelationsDeleteWriter.java @@ -0,0 +1,66 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Autowired; + +import java.text.DecimalFormat; +import java.util.List; + +public class BatchConceptRelationsDeleteWriter implements ItemWriter { + private static final Logger ourLog = LoggerFactory.getLogger(BatchConceptRelationsDeleteWriter.class); + + private static final DecimalFormat ourDecimalFormat = new DecimalFormat("#,###"); + + @Autowired + private ITermConceptParentChildLinkDao myConceptParentChildLinkDao; + + @Autowired + private ITermConceptPropertyDao myConceptPropertyDao; + + @Autowired + private ITermConceptDesignationDao myConceptDesignationDao; + + + @Override + public void write(List theTermCodeSystemVersionPidList) throws Exception { + // receives input in chunks of size one + long codeSystemVersionId = theTermCodeSystemVersionPidList.get(0); + + ourLog.info("Deleting term code links"); + int deletedLinks = myConceptParentChildLinkDao.deleteByCodeSystemVersion(codeSystemVersionId); + ourLog.info("Deleted {} term code links", ourDecimalFormat.format(deletedLinks)); + + ourLog.info("Deleting term code properties"); + int deletedProperties = myConceptPropertyDao.deleteByCodeSystemVersion(codeSystemVersionId); + ourLog.info("Deleted {} term code properties", ourDecimalFormat.format(deletedProperties)); + + ourLog.info("Deleting concept designations"); + int deletedDesignations = myConceptDesignationDao.deleteByCodeSystemVersion(codeSystemVersionId); + ourLog.info("Deleted {} concept designations", ourDecimalFormat.format(deletedDesignations)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemUniqueVersionDeleteReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemUniqueVersionDeleteReader.java new file mode 100644 index 00000000000..bf110c9c657 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemUniqueVersionDeleteReader.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.jpa.term.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_VERSION_ID; + +/** + * This reader works as a pass-through by passing the received parameter once to the writer, + * in order to share the writer functionality between two jobs + */ +public class BatchTermCodeSystemUniqueVersionDeleteReader implements ItemReader { + private static final Logger ourLog = LoggerFactory.getLogger(BatchTermCodeSystemUniqueVersionDeleteReader.class); + + @Value("#{jobParameters['" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "']}") + private Long myTermCodeSystemVersionPid; + + // indicates if the parameter was already passed once to the writer, which indicates end of task + private boolean myParameterPassed; + + + @Override + public Long read() throws Exception { + if ( ! myParameterPassed) { + myParameterPassed = true; + return myTermCodeSystemVersionPid; + } + + return null; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteReader.java new file mode 100644 index 00000000000..fd5683f13e8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteReader.java @@ -0,0 +1,75 @@ +package ca.uhn.fhir.jpa.term.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.util.List; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; + +/** + * + */ +public class BatchTermCodeSystemVersionDeleteReader implements ItemReader { + private static final Logger ourLog = LoggerFactory.getLogger(BatchTermCodeSystemVersionDeleteReader.class); + + @Autowired + private ITermCodeSystemVersionDao myTermCodeSystemVersionDao; + + @Value("#{jobParameters['" + JOB_PARAM_CODE_SYSTEM_ID + "']}") + private Long myTermCodeSystemPid; + + private List myTermCodeSystemVersionPidList; + private int myCurrentIdx = 0; + + + @Override + public Long read() throws Exception { + if (myTermCodeSystemVersionPidList == null) { + myTermCodeSystemVersionPidList = myTermCodeSystemVersionDao.findSortedPidsByCodeSystemPid(myTermCodeSystemPid); + } + + if (myTermCodeSystemVersionPidList.isEmpty()) { + // nothing to process + ourLog.info("Nothing to process"); + return null; + } + + if (myCurrentIdx >= myTermCodeSystemVersionPidList.size()) { + // nothing else to process + ourLog.info("No more versions to process"); + return null; + } + + // still processing elements + long TermCodeSystemVersionPid = myTermCodeSystemVersionPidList.get(myCurrentIdx++); + ourLog.info("Passing termCodeSystemVersionPid: {} to writer", TermCodeSystemVersionPid); + return TermCodeSystemVersionPid; + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteWriter.java new file mode 100644 index 00000000000..03dbdf882c3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermCodeSystemVersionDeleteWriter.java @@ -0,0 +1,77 @@ +package ca.uhn.fhir.jpa.term.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao; +import ca.uhn.fhir.jpa.entity.TermCodeSystem; +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.Optional; + +@Component +public class BatchTermCodeSystemVersionDeleteWriter implements ItemWriter { + private static final Logger ourLog = LoggerFactory.getLogger(BatchTermCodeSystemVersionDeleteWriter.class); + + @Autowired + private ITermCodeSystemDao myCodeSystemDao; + + @Autowired + private ITermCodeSystemVersionDao myTermCodeSystemVersionDao; + + + + @Override + public void write(List theTermCodeSystemVersionPidList) throws Exception { + // receives input in chunks of size one + long codeSystemVersionId = theTermCodeSystemVersionPidList.get(0); + + ourLog.debug("Executing for codeSystemVersionId: {}", codeSystemVersionId); + + // if TermCodeSystemVersion being deleted is current, disconnect it form TermCodeSystem + Optional codeSystemOpt = myCodeSystemDao.findWithCodeSystemVersionAsCurrentVersion(codeSystemVersionId); + if (codeSystemOpt.isPresent()) { + TermCodeSystem codeSystem = codeSystemOpt.get(); + ourLog.info("Removing code system version: {} as current version of code system: {}", codeSystemVersionId, codeSystem.getPid()); + codeSystem.setCurrentVersion(null); + myCodeSystemDao.save(codeSystem); + } + + ourLog.info("Deleting code system version: {}", codeSystemVersionId); + Optional csv = myTermCodeSystemVersionDao.findById(codeSystemVersionId); + csv.ifPresent(theTermCodeSystemVersion -> { + myTermCodeSystemVersionDao.delete(theTermCodeSystemVersion); + ourLog.info("Code system version: {} deleted", codeSystemVersionId); + }); + + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermConceptsDeleteWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermConceptsDeleteWriter.java new file mode 100644 index 00000000000..43297915c2a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/BatchTermConceptsDeleteWriter.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Autowired; + +import java.text.DecimalFormat; +import java.util.List; + + +public class BatchTermConceptsDeleteWriter implements ItemWriter { + private static final Logger ourLog = LoggerFactory.getLogger(BatchTermConceptsDeleteWriter.class); + + private static final DecimalFormat ourDecimalFormat = new DecimalFormat("#,###"); + + @Autowired + private ITermConceptDao myConceptDao; + + + @Override + public void write(List theTermCodeSystemVersionPidList) throws Exception { + // receives input in chunks of size one + long codeSystemVersionId = theTermCodeSystemVersionPidList.get(0); + + ourLog.info("Deleting concepts"); + int deletedConcepts = myConceptDao.deleteByCodeSystemVersion(codeSystemVersionId); + ourLog.info("Deleted {} concepts", ourDecimalFormat.format(deletedConcepts)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobConfig.java new file mode 100644 index 00000000000..8b31608acd5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobConfig.java @@ -0,0 +1,122 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersValidator; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_STEP_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_STEP_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CONCEPTS_DELETE_STEP_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CONCEPT_RELATIONS_DELETE_STEP_NAME; + +/** + * Configuration for batch job which deletes a TermCodeSystem and its related TermCodeSystemVersion(s), + * TermConceptProperty(es), TermConceptDesignation(s), and TermConceptParentChildLink(s) + **/ +@Configuration +public class TermCodeSystemDeleteJobConfig extends BaseTermCodeSystemDeleteJobConfig { + + + @Bean(name = TERM_CODE_SYSTEM_DELETE_JOB_NAME) + @Lazy + public Job termCodeSystemDeleteJob() { + return myJobBuilderFactory.get(TERM_CODE_SYSTEM_DELETE_JOB_NAME) + .validator(termCodeSystemDeleteJobParameterValidator()) + .start(termConceptRelationsDeleteStep()) + .next(termConceptsDeleteStep()) + .next(termCodeSystemVersionDeleteStep()) + .next(termCodeSystemDeleteStep()) + .build(); + } + + @Bean + public JobParametersValidator termCodeSystemDeleteJobParameterValidator() { + return new TermCodeSystemDeleteJobParameterValidator(); + } + + /** + * This steps deletes TermConceptParentChildLink(s), TermConceptProperty(es) and TermConceptDesignation(s) + * related to TermConcept(s) of the TermCodeSystemVersion being deleted + */ + @Bean(name = TERM_CONCEPT_RELATIONS_DELETE_STEP_NAME) + public Step termConceptRelationsDeleteStep() { + return myStepBuilderFactory.get(TERM_CONCEPT_RELATIONS_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemVersionDeleteReader()) + .writer(batchConceptRelationsDeleteWriter()) + .build(); + } + + /** + * This steps deletes TermConcept(s) of the TermCodeSystemVersion being deleted + */ + @Bean(name = TERM_CONCEPTS_DELETE_STEP_NAME) + public Step termConceptsDeleteStep() { + DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); + attribute.setTimeout(TERM_CONCEPT_DELETE_TIMEOUT); + + return myStepBuilderFactory.get(TERM_CONCEPTS_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemVersionDeleteReader()) + .writer(batchTermConceptsDeleteWriter()) + .transactionAttribute(attribute) + .build(); + } + + /** + * This steps deletes the TermCodeSystemVersion + */ + @Bean(name = TERM_CODE_SYSTEM_VERSION_DELETE_STEP_NAME) + public Step termCodeSystemVersionDeleteStep() { + return myStepBuilderFactory.get(TERM_CODE_SYSTEM_VERSION_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemVersionDeleteReader()) + .writer(batchTermCodeSystemVersionDeleteWriter()) + .build(); + } + + @Bean(name = TERM_CODE_SYSTEM_DELETE_STEP_NAME) + public Step termCodeSystemDeleteStep() { + return myStepBuilderFactory.get(TERM_CODE_SYSTEM_DELETE_STEP_NAME) + .tasklet(termCodeSystemDeleteTasklet()) + .build(); + } + + @Bean + @StepScope + public BatchTermCodeSystemVersionDeleteReader batchTermCodeSystemVersionDeleteReader() { + return new BatchTermCodeSystemVersionDeleteReader(); + } + + @Bean + public TermCodeSystemDeleteTasklet termCodeSystemDeleteTasklet() { + return new TermCodeSystemDeleteTasklet(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobParameterValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobParameterValidator.java new file mode 100644 index 00000000000..6e8d44b4650 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobParameterValidator.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.term.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.JobParametersValidator; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; + +/** + * Validates that a TermCodeSystem parameter is present + */ +public class TermCodeSystemDeleteJobParameterValidator implements JobParametersValidator { + + @Override + public void validate(JobParameters theJobParameters) throws JobParametersInvalidException { + if (theJobParameters == null) { + throw new JobParametersInvalidException("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_ID + "'"); + } + + if ( ! theJobParameters.getParameters().containsKey(JOB_PARAM_CODE_SYSTEM_ID)) { + throw new JobParametersInvalidException("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_ID + "'"); + } + + Long termCodeSystemPid = theJobParameters.getLong(JOB_PARAM_CODE_SYSTEM_ID); + if (termCodeSystemPid == null) { + throw new JobParametersInvalidException("'" + JOB_PARAM_CODE_SYSTEM_ID + "' parameter is null"); + } + + if (termCodeSystemPid <= 0) { + throw new JobParametersInvalidException("Invalid parameter '" + JOB_PARAM_CODE_SYSTEM_ID + "' value: " + termCodeSystemPid); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteTasklet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteTasklet.java new file mode 100644 index 00000000000..17a0bee5335 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteTasklet.java @@ -0,0 +1,60 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; +import ca.uhn.fhir.jpa.entity.TermCodeSystem; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; + +@Component +public class TermCodeSystemDeleteTasklet implements Tasklet { + private static final Logger ourLog = LoggerFactory.getLogger(TermCodeSystemDeleteTasklet.class); + + @Autowired + private ITermCodeSystemDao myTermCodeSystemDao; + + @Autowired + private ITermCodeSystemVersionDao myCodeSystemVersionDao; + + @Override + public RepeatStatus execute(@NotNull StepContribution contribution, ChunkContext context) throws Exception { + long codeSystemPid = (Long) context.getStepContext().getJobParameters().get(JOB_PARAM_CODE_SYSTEM_ID); + ourLog.info("Deleting code system {}", codeSystemPid); + + myTermCodeSystemDao.findById(codeSystemPid).orElseThrow(IllegalStateException::new); + myTermCodeSystemDao.deleteById(codeSystemPid); + ourLog.info("Code system {} deleted", codeSystemPid); + + return RepeatStatus.FINISHED; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobConfig.java new file mode 100644 index 00000000000..a21d912d491 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobConfig.java @@ -0,0 +1,105 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersValidator; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_UNIQUE_VERSION_DELETE_STEP_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CONCEPTS_UNIQUE_VERSION_DELETE_STEP_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CONCEPT_RELATIONS_UNIQUE_VERSION_DELETE_STEP_NAME; + +/** + * Configuration for batch job which deletes a specific TermCodeSystemVersion and its related, + * TermConceptProperty(es), TermConceptDesignation(s), and TermConceptParentChildLink(s) + **/ +@Configuration +public class TermCodeSystemVersionDeleteJobConfig extends BaseTermCodeSystemDeleteJobConfig { + + + @Bean(name = TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME) + @Lazy + public Job termCodeSystemVersionDeleteJob() { + return myJobBuilderFactory.get(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME) + .validator(termCodeSystemVersionDeleteJobParameterValidator()) + .start(termConceptRelationsUniqueVersionDeleteStep()) + .next(termConceptsUniqueVersionDeleteStep()) + .next(termCodeSystemUniqueVersionDeleteStep()) + .build(); + } + + + @Bean + public JobParametersValidator termCodeSystemVersionDeleteJobParameterValidator() { + return new TermCodeSystemVersionDeleteJobParameterValidator(); + } + + + @Bean(name = TERM_CONCEPT_RELATIONS_UNIQUE_VERSION_DELETE_STEP_NAME) + public Step termConceptRelationsUniqueVersionDeleteStep() { + return myStepBuilderFactory.get(TERM_CONCEPT_RELATIONS_UNIQUE_VERSION_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemUniqueVersionDeleteReader()) + .writer(batchConceptRelationsDeleteWriter()) + .build(); + } + + + @Bean(name = TERM_CONCEPTS_UNIQUE_VERSION_DELETE_STEP_NAME) + public Step termConceptsUniqueVersionDeleteStep() { + DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); + attribute.setTimeout(TERM_CONCEPT_DELETE_TIMEOUT); + + return myStepBuilderFactory.get(TERM_CONCEPTS_UNIQUE_VERSION_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemUniqueVersionDeleteReader()) + .writer(batchTermConceptsDeleteWriter()) + .transactionAttribute(attribute) + .build(); + } + + + @Bean(name = TERM_CODE_SYSTEM_UNIQUE_VERSION_DELETE_STEP_NAME) + public Step termCodeSystemUniqueVersionDeleteStep() { + return myStepBuilderFactory.get(TERM_CODE_SYSTEM_UNIQUE_VERSION_DELETE_STEP_NAME) + .chunk(1) + .reader(batchTermCodeSystemUniqueVersionDeleteReader()) + .writer(batchTermCodeSystemVersionDeleteWriter()) + .build(); + } + + + @Bean + @StepScope + public BatchTermCodeSystemUniqueVersionDeleteReader batchTermCodeSystemUniqueVersionDeleteReader() { + return new BatchTermCodeSystemUniqueVersionDeleteReader(); + } + + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobParameterValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobParameterValidator.java new file mode 100644 index 00000000000..269cd7e5535 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobParameterValidator.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.JobParametersValidator; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_VERSION_ID; + +/** + * Validates that a TermCodeSystem parameter is present + */ +public class TermCodeSystemVersionDeleteJobParameterValidator implements JobParametersValidator { + + @Override + public void validate(JobParameters theJobParameters) throws JobParametersInvalidException { + if (theJobParameters == null) { + throw new JobParametersInvalidException("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "'"); + } + + if ( ! theJobParameters.getParameters().containsKey(JOB_PARAM_CODE_SYSTEM_VERSION_ID)) { + throw new JobParametersInvalidException("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "'"); + } + + Long termCodeSystemPid = theJobParameters.getLong(JOB_PARAM_CODE_SYSTEM_VERSION_ID); + if (termCodeSystemPid == null) { + throw new JobParametersInvalidException("'" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "' parameter is null"); + } + + if (termCodeSystemPid <= 0) { + throw new JobParametersInvalidException( + "Invalid parameter '" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "' value: " + termCodeSystemPid); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermConceptDeleteTasklet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermConceptDeleteTasklet.java new file mode 100644 index 00000000000..9052fca2583 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/job/TermConceptDeleteTasklet.java @@ -0,0 +1,62 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; + +/** + * Deletes the TermConcept(s) related to the TermCodeSystemVersion being deleted + * Executes in its own step to be in own transaction because it is a DB-heavy operation + */ +@Component +public class TermConceptDeleteTasklet implements Tasklet { + private static final Logger ourLog = LoggerFactory.getLogger(TermConceptDeleteTasklet.class); + + @Autowired + private ITermCodeSystemDao myTermCodeSystemDao; + + @Autowired + private ITermCodeSystemVersionDao myCodeSystemVersionDao; + + @Override + public RepeatStatus execute(@NotNull StepContribution contribution, ChunkContext context) throws Exception { + long codeSystemPid = (Long) context.getStepContext().getJobParameters().get(JOB_PARAM_CODE_SYSTEM_ID); + ourLog.info("Deleting code system {}", codeSystemPid); + + myTermCodeSystemDao.findById(codeSystemPid).orElseThrow(IllegalStateException::new); + myTermCodeSystemDao.deleteById(codeSystemPid); + + return RepeatStatus.FINISHED; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java index 2e82fe6b11a..187ad7baa7c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java @@ -1,21 +1,28 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.term.TermReindexingSvcImpl; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.apache.commons.io.IOUtils; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.Enumerations; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import java.nio.charset.StandardCharsets; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { + + @Autowired private BatchJobHelper myBatchJobHelper; + @AfterAll public static void afterClassClearContext() { TermReindexingSvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); @@ -64,6 +71,7 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { cs.addConcept().setCode("B"); myCodeSystemDao.update(cs, mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runInTransaction(()->{ assertEquals(2, myConceptDao.count()); }); @@ -77,6 +85,7 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { cs.addConcept().setCode("C"); myCodeSystemDao.update(cs, mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runInTransaction(()->{ assertEquals(1, myConceptDao.count()); }); @@ -86,6 +95,7 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { myCodeSystemDao.delete(id); }); myTerminologyDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); runInTransaction(()->{ assertEquals(0L, myConceptDao.count()); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java index 9b271eaed33..1db04635e74 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java @@ -2,15 +2,19 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.TermReindexingSvcImpl; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.apache.commons.io.IOUtils; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.CodeSystem; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import java.nio.charset.StandardCharsets; import java.util.List; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -18,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; public class FhirResourceDaoR4CodeSystemTest extends BaseJpaR4Test { + @Autowired private BatchJobHelper myBatchJobHelper; + @Test public void testIndexContained() throws Exception { TermReindexingSvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); @@ -58,6 +64,7 @@ public class FhirResourceDaoR4CodeSystemTest extends BaseJpaR4Test { // Now the background scheduler will do its thing myTerminologyDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); runInTransaction(() -> { assertEquals(0, myTermCodeSystemDao.count()); assertEquals(0, myTermCodeSystemVersionDao.count()); @@ -116,6 +123,7 @@ public class FhirResourceDaoR4CodeSystemTest extends BaseJpaR4Test { // Now the background scheduler will do its thing myTerminologyDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); // Entities for first resource should be gone now. runInTransaction(() -> { @@ -150,6 +158,7 @@ public class FhirResourceDaoR4CodeSystemTest extends BaseJpaR4Test { // Now the background scheduler will do its thing myTerminologyDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); // The remaining versions and Code System entities should be gone now. runInTransaction(() -> { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index 458576d30d7..a912f932fd2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -45,6 +45,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; @@ -109,6 +110,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; @@ -128,6 +130,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.MatcherAssert.assertThat; @@ -155,6 +159,9 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4Test.class); + @Autowired + private BatchJobHelper myBatchJobHelper; + @AfterEach public final void after() { myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); @@ -356,6 +363,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runInTransaction(() -> { assertEquals(3L, myTermConceptDao.count()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5CodeSystemTest.java index e7bcd8dd016..5345fa7c14c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5CodeSystemTest.java @@ -2,22 +2,25 @@ package ca.uhn.fhir.jpa.dao.r5; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.TermReindexingSvcImpl; -import org.apache.commons.io.IOUtils; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r5.model.CodeSystem; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; -import java.nio.charset.StandardCharsets; import java.util.List; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; public class FhirResourceDaoR5CodeSystemTest extends BaseJpaR5Test { + @Autowired private BatchJobHelper myBatchJobHelper; + @Test public void testDeleteLargeCompleteCodeSystem() { @@ -42,6 +45,7 @@ public class FhirResourceDaoR5CodeSystemTest extends BaseJpaR5Test { // Now the background scheduler will do its thing myTermDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); runInTransaction(() -> { assertEquals(0, myTermCodeSystemDao.count()); assertEquals(0, myTermCodeSystemVersionDao.count()); @@ -100,6 +104,7 @@ public class FhirResourceDaoR5CodeSystemTest extends BaseJpaR5Test { // Now the background scheduler will do its thing myTermDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); // Entities for first resource should be gone now. runInTransaction(() -> { @@ -134,6 +139,7 @@ public class FhirResourceDaoR5CodeSystemTest extends BaseJpaR5Test { // Now the background scheduler will do its thing myTermDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); // The remaining versions and Code System entities should be gone now. runInTransaction(() -> { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java index 75977b406b4..04aa24142a5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoDstu3TerminologyTest; import ca.uhn.fhir.jpa.term.TermReindexingSvcImpl; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.hl7.fhir.dstu3.model.BooleanType; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeType; @@ -19,16 +20,20 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; public class ResourceProviderDstu3CodeSystemTest extends BaseResourceProviderDstu3Test { + @Autowired private BatchJobHelper myBatchJobHelper; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderDstu3CodeSystemTest.class); public static FhirContext ourCtx = FhirContext.forDstu3Cached(); @@ -133,6 +138,8 @@ public class ResourceProviderDstu3CodeSystemTest extends BaseResourceProviderDst runInTransaction(() -> assertEquals(26L, myConceptDao.count())); myTerminologyDeferredStorageSvc.saveDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_DELETE_JOB_NAME); + runInTransaction(() -> assertEquals(24L, myConceptDao.count())); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcTest.java index e5b1b6cc27d..ea18d984d67 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcTest.java @@ -5,12 +5,15 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -18,6 +21,10 @@ public class TermCodeSystemStorageSvcTest extends BaseJpaR4Test { public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system"; + @Autowired + private BatchJobHelper myBatchJobHelper; + + @Test public void testStoreNewCodeSystemVersionForExistingCodeSystemNoVersionId() { CodeSystem firstUpload = createCodeSystemWithMoreThan100Concepts(); @@ -126,6 +133,7 @@ public class TermCodeSystemStorageSvcTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.setProcessDeferred(true); myTerminologyDeferredStorageSvc.saveDeferred(); myTerminologyDeferredStorageSvc.setProcessDeferred(false); + myBatchJobHelper.awaitAllBulkJobCompletions(false, TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); assertEquals(theExpectedConceptCount, runInTransaction(() -> myTermConceptDao.count())); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImplTest.java index 0291e76e7dd..a63cb8e21b4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImplTest.java @@ -10,10 +10,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.batch.core.JobExecution; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.times; @@ -33,6 +40,9 @@ public class TermDeferredStorageSvcImplTest { @Mock private ITermCodeSystemVersionDao myTermCodeSystemVersionDao; + @Mock + private JobExecution myJobExecution; + @Test public void testSaveDeferredWithExecutionSuspended() { TermDeferredStorageSvcImpl svc = new TermDeferredStorageSvcImpl(); @@ -41,6 +51,17 @@ public class TermDeferredStorageSvcImplTest { } + @Test + public void testStorageNotEmptyWhileJobsExecuting() { + TermDeferredStorageSvcImpl svc = new TermDeferredStorageSvcImpl(); + ReflectionTestUtils.setField(svc, "myCurrentJobExecutions", Collections.singletonList(myJobExecution)); + + when(myJobExecution.isRunning()).thenReturn(true, false); + assertFalse(svc.isStorageQueueEmpty()); + assertTrue(svc.isStorageQueueEmpty()); + } + + @Test public void testSaveDeferred_Concept() { TermConcept concept = new TermConcept(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java index c29b39c7c20..fe9a4d9ba0a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java @@ -5,20 +5,22 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.hl7.fhir.r4.model.CodeSystem; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; -import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_PRIMARY_DEFAULT; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.fail; public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { + @Autowired private BatchJobHelper myBatchJobHelper; private TermLoaderSvcImpl mySvc; - private ZipCollectionBuilder myFiles; @BeforeEach @@ -62,6 +64,7 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { mySvc.loadLoinc(myFiles.getFiles(), mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(false, TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME ); runInTransaction(() -> { assertEquals(1, myTermCodeSystemDao.count()); @@ -87,6 +90,7 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v268_loincupload.properties"); mySvc.loadLoinc(myFiles.getFiles(), mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(false, TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME ); runInTransaction(() -> { assertEquals(1, myTermCodeSystemDao.count()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplCurrentVersionR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplCurrentVersionR4Test.java index 90fa4501bca..b07e1c48610 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplCurrentVersionR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplCurrentVersionR4Test.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermValueSet; @@ -14,6 +15,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; @@ -25,6 +27,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; @@ -33,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.util.ResourceUtils; import javax.persistence.EntityManager; @@ -50,6 +54,7 @@ import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_DUPLICATE_FILE_DEFAULT; import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_FILE_DEFAULT; import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_DUPLICATE_FILE_DEFAULT; @@ -75,6 +80,7 @@ import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000 import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT; import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UPLOAD_PROPERTIES_FILE; import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_XML_FILE; +import static java.util.stream.Collectors.joining; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_ALL_VALUESET_ID; @@ -130,9 +136,11 @@ public class TerminologySvcImplCurrentVersionR4Test extends BaseJpaR4Test { @Autowired private ITermReadSvc myITermReadSvc; - @Autowired - @Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT) - private IValidationSupport myJpaPersistedResourceValidationSupport; + @Autowired @Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT) + private IValidationSupport myJpaPersistedResourceValidationSupport; + + @Autowired private BatchJobHelper myBatchJobHelper; + private ZipCollectionBuilder myFiles; private ServletRequestDetails myRequestDetails = new ServletRequestDetails(); @@ -690,6 +698,7 @@ public class TerminologySvcImplCurrentVersionR4Test extends BaseJpaR4Test { String currentVer = "2.68"; uploadLoincCodeSystem(currentVer, true); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runCommonValidations(Lists.newArrayList(nonCurrentVer, currentVer)); @@ -711,6 +720,7 @@ public class TerminologySvcImplCurrentVersionR4Test extends BaseJpaR4Test { String lastCurrentVer = "2.69"; uploadLoincCodeSystem(lastCurrentVer, true); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runCommonValidations(Lists.newArrayList(firstCurrentVer, noCurrentVer, lastCurrentVer)); @@ -775,11 +785,27 @@ public class TerminologySvcImplCurrentVersionR4Test extends BaseJpaR4Test { } private TermCodeSystemVersion fetchCurrentCodeSystemVersion() { + runInTransaction(() -> { + List tcsList = myEntityManager.createQuery("from TermCodeSystem").getResultList(); + List tcsvList = myEntityManager.createQuery("from TermCodeSystemVersion").getResultList(); + ourLog.error("tcslist: {}", tcsList.stream().map(tcs -> tcs.toString()).collect(joining("\n", "\n", ""))); + ourLog.error("tcsvlist: {}", tcsvList.stream().map(v -> v.toString()).collect(joining("\n", "\n", ""))); + + if (tcsList.size() != 1) { + throw new IllegalStateException("More than one TCS: " + + tcsList.stream().map(tcs -> String.valueOf(tcs.getPid())).collect(joining())); + } + if (tcsList.get(0).getCurrentVersion() == null) { + throw new IllegalStateException("Current version is null in TCS: " + tcsList.get(0).getPid()); + } + }); + return runInTransaction(() -> (TermCodeSystemVersion) myEntityManager.createQuery( - "select tcsv from TermCodeSystemVersion tcsv join fetch tcsv.myCodeSystem tcs " + - "where tcs.myCurrentVersion = tcsv").getSingleResult()); + "select tcsv from TermCodeSystemVersion tcsv join fetch tcsv.myCodeSystem tcs " + + "where tcs.myCurrentVersion = tcsv").getSingleResult()); } + private static void addBaseLoincMandatoryFilesToZip( ZipCollectionBuilder theFiles, Boolean theIncludeTop2000, String theClassPathPrefix) throws IOException { theFiles.addFileZip(theClassPathPrefix, LOINC_XML_FILE.getCode()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java index 4b0f0d73ef9..1c0ddeb5e17 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.test.utilities.BatchJobHelper; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeableConcept; @@ -18,6 +19,7 @@ import org.hl7.fhir.r4.model.codesystems.HttpVerb; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; @@ -29,6 +31,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,6 +42,9 @@ import static org.junit.jupiter.api.Assertions.fail; public class TerminologySvcImplR4Test extends BaseTermR4Test { private static final Logger ourLog = LoggerFactory.getLogger(TerminologySvcImplR4Test.class); + + @Autowired private BatchJobHelper myBatchJobHelper; + ConceptValidationOptions optsNoGuess = new ConceptValidationOptions(); ConceptValidationOptions optsGuess = new ConceptValidationOptions().setInferSystem(true); @@ -424,6 +430,7 @@ public class TerminologySvcImplR4Test extends BaseTermR4Test { IIdType id_v2 = myCodeSystemDao.update(codeSystem, mySrd).getId().toUnqualified(); myTerminologyDeferredStorageSvc.saveAllDeferred(); + myBatchJobHelper.awaitAllBulkJobCompletions(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME); runInTransaction(() -> { List termCodeSystemVersions_updated = myTermCodeSystemVersionDao.findAll(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ZipCollectionBuilder.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ZipCollectionBuilder.java index 1b887b37234..efbbfc57882 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ZipCollectionBuilder.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ZipCollectionBuilder.java @@ -28,7 +28,7 @@ public class ZipCollectionBuilder { /** * Constructor */ - ZipCollectionBuilder() { + public ZipCollectionBuilder() { myFiles = new ArrayList<>(); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/DynamicJobFlowSandbox.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/DynamicJobFlowSandbox.java new file mode 100644 index 00000000000..d91962ced9d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/DynamicJobFlowSandbox.java @@ -0,0 +1,123 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.job.builder.FlowBuilder; +import org.springframework.batch.core.job.builder.SimpleJobBuilder; +import org.springframework.batch.core.job.flow.Flow; +import org.springframework.batch.core.job.flow.support.SimpleFlow; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Not intended to ever run. Used as a sandbox for "interesting" jobs + */ +public class DynamicJobFlowSandbox { + protected static final Logger ourLog = LoggerFactory.getLogger(TermCodeSystemDeleteJobTest.class); + + + @Autowired + private JobBuilderFactory myJobBuilderFactory; + + @Autowired + private StepBuilderFactory myStepBuilderFactory; + + private List versionPidList = Lists.newArrayList(3L, 5L); + + @Bean + public Job testJob() { + SimpleJobBuilder jobBuilder = myJobBuilderFactory.get("job") + .start(stepPreFlow()); + + // add a flow for each Pid + List flowForEachPidList = versionPidList.stream().map(this::getFlowForPid).collect(Collectors.toList()); + flowForEachPidList.forEach( flowForPid -> jobBuilder.on("COMPLETED").to(flowForPid) ); + + return jobBuilder.next(stepPostFlow()).build(); + } + + + private Flow getFlowForPid(Long theLong) { + return new FlowBuilder("flow-for-Pid-" + theLong) + .start(flowStep1(theLong)) + .next(fllowStep2(theLong)) + .build(); + } + + + + public Step flowStep1(long theLong) { + String name = "flow-step-1-for-Pid-" + theLong; + return myStepBuilderFactory.get(name) + .tasklet((contribution, chunkContext) -> { + ourLog.info("\n\n" + name + " executed\n\n"); + return RepeatStatus.FINISHED; + }) + .build(); + } + + + public Step fllowStep2(long theLong) { + String name = "flow-step-2-for-Pid-" + theLong; + return myStepBuilderFactory.get(name) + .tasklet((contribution, chunkContext) -> { + ourLog.info("\n\n" + name + " executed\n\n"); + return RepeatStatus.FINISHED; + }) + .build(); + } + + + public Step stepPreFlow() { + return myStepBuilderFactory.get("step-pre-flow") + .tasklet((contribution, chunkContext) -> { + ourLog.info("\n\nstep-pre-flow executed\n\n"); + return RepeatStatus.FINISHED; + }) + .build(); + } + + + public Step stepPostFlow() { + return myStepBuilderFactory.get("step-post-flow") + .tasklet((contribution, chunkContext) -> { + ourLog.info("\n\nstep-post-flow executed\n\n"); + return RepeatStatus.FINISHED; + }) + .build(); + } + + +} + + + diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobTest.java new file mode 100644 index 00000000000..bf9d6b15c78 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemDeleteJobTest.java @@ -0,0 +1,266 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.entity.TermCodeSystem; +import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; +import ca.uhn.fhir.jpa.term.UploadStatistics; +import ca.uhn.fhir.jpa.term.ZipCollectionBuilder; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.BatchJobHelper; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.ResourceUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Properties; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_ID; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_MAKE_CURRENT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_VERSION; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_TERMS_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_HIERARCHY_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PARENT_GROUP_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_PRIMARY_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_RSNA_PLAYBOOK_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UPLOAD_PROPERTIES_FILE; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_XML_FILE; +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.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class TermCodeSystemDeleteJobTest extends BaseJpaR4Test { + + private final ServletRequestDetails myRequestDetails = new ServletRequestDetails(); + private Properties uploadProperties; + + @Autowired private TermLoaderSvcImpl myTermLoaderSvc; + @Autowired private IBatchJobSubmitter myJobSubmitter; + @Autowired private BatchJobHelper myBatchJobHelper; + + @Autowired @Qualifier(TERM_CODE_SYSTEM_DELETE_JOB_NAME) + private Job myTermCodeSystemDeleteJob; + + + private void initMultipleVersionLoad() throws Exception { + File file = ResourceUtils.getFile("classpath:loinc-ver/" + LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + uploadProperties = new Properties(); + uploadProperties.load(new FileInputStream(file)); + + IFhirResourceDao valueSetIFhirResourceDao = myDaoRegistry.getResourceDao(ValueSet.class); + } + + @Test + public void runDeleteJobMultipleVersions() throws Exception { + initMultipleVersionLoad(); + + // loading a loinc CS with version loads two versions (second one with null version) + String firstCurrentVer = "2.67"; + uploadLoincCodeSystem(firstCurrentVer, true); + + long[] termCodeSystemPidVect = new long[1]; //bypass final restriction + runInTransaction(() -> { + assertEquals(1, myTermCodeSystemDao.count()); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByCodeSystemUri("http://loinc.org"); + assertNotNull(termCodeSystem); + termCodeSystemPidVect[0] = termCodeSystem.getPid(); + + assertEquals(2, myTermCodeSystemVersionDao.count()); + assertEquals(162, myTermConceptDao.count()); + }); + + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_ID, new JobParameter(termCodeSystemPidVect[0], true) )); + + + JobExecution jobExecution = myJobSubmitter.runJob(myTermCodeSystemDeleteJob, jobParameters); + + + myBatchJobHelper.awaitJobCompletion(jobExecution); + assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode()); + + runInTransaction(() -> { + assertEquals(0, myTermCodeSystemDao.count()); + assertNull(myTermCodeSystemDao.findByCodeSystemUri("http://loinc.org")); + assertEquals(0, myTermCodeSystemVersionDao.count()); + assertEquals(0, myTermConceptDao.count()); + }); + } + + + @Test + public void runWithNoParameterFailsValidation() { + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemDeleteJob, new JobParameters()) + ); + assertEquals("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_ID + "'", thrown.getMessage()); + } + + + @Test + public void runWithNullParameterFailsValidation() { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_ID, new JobParameter((Long) null, true) )); + + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemDeleteJob, jobParameters) + ); + assertEquals("'" + JOB_PARAM_CODE_SYSTEM_ID + "' parameter is null", thrown.getMessage()); + } + + + @Test + public void runWithParameterZeroFailsValidation() { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_ID, new JobParameter(0L, true) )); + + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemDeleteJob, jobParameters) + ); + assertEquals("Invalid parameter '" + JOB_PARAM_CODE_SYSTEM_ID + "' value: 0", thrown.getMessage()); + } + + + private IIdType uploadLoincCodeSystem(String theVersion, boolean theMakeItCurrent) throws Exception { + ZipCollectionBuilder files = new ZipCollectionBuilder(); + + myRequestDetails.getUserData().put(LOINC_CODESYSTEM_MAKE_CURRENT, theMakeItCurrent); + uploadProperties.put(LOINC_CODESYSTEM_MAKE_CURRENT.getCode(), Boolean.toString(theMakeItCurrent)); + + assertTrue( + theVersion == null || theVersion.equals("2.67") || theVersion.equals("2.68") || theVersion.equals("2.69"), + "Version supported are: 2.67, 2.68, 2.69 and null" ); + + if (StringUtils.isBlank(theVersion)) { + uploadProperties.remove(LOINC_CODESYSTEM_VERSION.getCode()); + } else { + uploadProperties.put(LOINC_CODESYSTEM_VERSION.getCode(), theVersion); + } + + addLoincMandatoryFilesToZip(files, theVersion); + + UploadStatistics stats = myTermLoaderSvc.loadLoinc(files.getFiles(), mySrd); + myTerminologyDeferredStorageSvc.saveAllDeferred(); + + return stats.getTarget(); + } + + + public void addLoincMandatoryFilesToZip(ZipCollectionBuilder theFiles, String theVersion) throws IOException { + String theClassPathPrefix = getClassPathPrefix(theVersion); + addBaseLoincMandatoryFilesToZip(theFiles, true, theClassPathPrefix); + theFiles.addPropertiesZip(uploadProperties, LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_LINK_FILE_PRIMARY_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT.getCode()); + } + + + private static void addBaseLoincMandatoryFilesToZip( + ZipCollectionBuilder theFiles, Boolean theIncludeTop2000, String theClassPathPrefix) throws IOException { + theFiles.addFileZip(theClassPathPrefix, LOINC_XML_FILE.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_GROUP_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_GROUP_TERMS_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PARENT_GROUP_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_HIERARCHY_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_LINK_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_LINK_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_RSNA_PLAYBOOK_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT.getCode()); + if (theIncludeTop2000) { + theFiles.addFileZip(theClassPathPrefix, LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT.getCode()); + } + } + + + private String getClassPathPrefix(String theVersion) { + String theClassPathPrefix = "/loinc-ver/v-no-version/"; + + if (StringUtils.isBlank(theVersion)) return theClassPathPrefix; + + switch(theVersion) { + case "2.67": + return "/loinc-ver/v267/"; + case "2.68": + return "/loinc-ver/v268/"; + case "2.69": + return "/loinc-ver/v269/"; + } + + fail("Setup failed. Unexpected version: " + theVersion); + return null; + } + + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobTest.java new file mode 100644 index 00000000000..f6bbdbe7d81 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/job/TermCodeSystemVersionDeleteJobTest.java @@ -0,0 +1,272 @@ +package ca.uhn.fhir.jpa.term.job; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.entity.TermCodeSystem; +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; +import ca.uhn.fhir.jpa.term.UploadStatistics; +import ca.uhn.fhir.jpa.term.ZipCollectionBuilder; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.BatchJobHelper; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.ResourceUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Properties; + +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.JOB_PARAM_CODE_SYSTEM_VERSION_ID; +import static ca.uhn.fhir.jpa.batch.config.BatchConstants.TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_ANSWERLIST_LINK_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_MAKE_CURRENT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_CODESYSTEM_VERSION; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_DUPLICATE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_GROUP_TERMS_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_HIERARCHY_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PARENT_GROUP_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_PRIMARY_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_RSNA_PLAYBOOK_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_UPLOAD_PROPERTIES_FILE; +import static ca.uhn.fhir.jpa.term.loinc.LoincUploadPropertiesEnum.LOINC_XML_FILE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class TermCodeSystemVersionDeleteJobTest extends BaseJpaR4Test { + + private final ServletRequestDetails myRequestDetails = new ServletRequestDetails(); + private Properties uploadProperties; + + @Autowired private TermLoaderSvcImpl myTermLoaderSvc; + @Autowired private IBatchJobSubmitter myJobSubmitter; + @Autowired private BatchJobHelper myBatchJobHelper; + + @Autowired @Qualifier(TERM_CODE_SYSTEM_VERSION_DELETE_JOB_NAME) + private Job myTermCodeSystemVersionDeleteJob; + + + private void initMultipleVersionLoad() throws Exception { + File file = ResourceUtils.getFile("classpath:loinc-ver/" + LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + uploadProperties = new Properties(); + uploadProperties.load(new FileInputStream(file)); + + IFhirResourceDao valueSetIFhirResourceDao = myDaoRegistry.getResourceDao(ValueSet.class); + } + + @Test + public void runDeleteJobDeleteOneVersion() throws Exception { + initMultipleVersionLoad(); + + String firstCurrentVer = "2.67"; + uploadLoincCodeSystem(firstCurrentVer, true); + + long[] termCodeSystemVersionPidVect = new long[1]; //bypass final restriction + runInTransaction(() -> { + assertEquals(1, myTermCodeSystemDao.count()); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByCodeSystemUri("http://loinc.org"); + assertNotNull(termCodeSystem); + + TermCodeSystemVersion termCodeSystemVersion = myTermCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(termCodeSystem.getPid()); + assertNotNull(termCodeSystemVersion); + termCodeSystemVersionPidVect[0] = termCodeSystemVersion.getPid(); + + assertEquals(2, myTermCodeSystemVersionDao.count()); + assertEquals(81 * 2, myTermConceptDao.count()); + }); + + + JobParameters jobParameters = new JobParameters(Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_VERSION_ID, new JobParameter(termCodeSystemVersionPidVect[0], true) )); + + + JobExecution jobExecution = myJobSubmitter.runJob(myTermCodeSystemVersionDeleteJob, jobParameters); + + + myBatchJobHelper.awaitJobCompletion(jobExecution); + assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode()); + + runInTransaction(() -> { + assertEquals(1, myTermCodeSystemDao.count()); + assertNotNull(myTermCodeSystemDao.findByCodeSystemUri("http://loinc.org")); + assertEquals(1, myTermCodeSystemVersionDao.count()); + assertEquals(81, myTermConceptDao.count()); + }); + } + + + + + @Test + public void runWithNoParameterFailsValidation() { + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemVersionDeleteJob, new JobParameters()) + ); + assertEquals("This job needs Parameter: '" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "'", thrown.getMessage()); + } + + + @Test + public void runWithNullParameterFailsValidation() { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_VERSION_ID, new JobParameter((Long) null, true) )); + + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemVersionDeleteJob, jobParameters) + ); + assertEquals("'" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "' parameter is null", thrown.getMessage()); + } + + + @Test + public void runWithParameterZeroFailsValidation() { + JobParameters jobParameters = new JobParameters( + Collections.singletonMap( + JOB_PARAM_CODE_SYSTEM_VERSION_ID, new JobParameter(0L, true) )); + + JobParametersInvalidException thrown = Assertions.assertThrows( + JobParametersInvalidException.class, + () -> myJobSubmitter.runJob(myTermCodeSystemVersionDeleteJob, jobParameters) + ); + assertEquals("Invalid parameter '" + JOB_PARAM_CODE_SYSTEM_VERSION_ID + "' value: 0", thrown.getMessage()); + } + + + + private IIdType uploadLoincCodeSystem(String theVersion, boolean theMakeItCurrent) throws Exception { + ZipCollectionBuilder files = new ZipCollectionBuilder(); + + myRequestDetails.getUserData().put(LOINC_CODESYSTEM_MAKE_CURRENT, theMakeItCurrent); + uploadProperties.put(LOINC_CODESYSTEM_MAKE_CURRENT.getCode(), Boolean.toString(theMakeItCurrent)); + + assertTrue( + theVersion == null || theVersion.equals("2.67") || theVersion.equals("2.68") || theVersion.equals("2.69"), + "Version supported are: 2.67, 2.68, 2.69 and null" ); + + if (StringUtils.isBlank(theVersion)) { + uploadProperties.remove(LOINC_CODESYSTEM_VERSION.getCode()); + } else { + uploadProperties.put(LOINC_CODESYSTEM_VERSION.getCode(), theVersion); + } + + addLoincMandatoryFilesToZip(files, theVersion); + + UploadStatistics stats = myTermLoaderSvc.loadLoinc(files.getFiles(), mySrd); + myTerminologyDeferredStorageSvc.saveAllDeferred(); + + return stats.getTarget(); + } + + + public void addLoincMandatoryFilesToZip(ZipCollectionBuilder theFiles, String theVersion) throws IOException { + String theClassPathPrefix = getClassPathPrefix(theVersion); + addBaseLoincMandatoryFilesToZip(theFiles, true, theClassPathPrefix); + theFiles.addPropertiesZip(uploadProperties, LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_LINK_FILE_PRIMARY_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_LINK_FILE_SUPPLEMENTARY_DEFAULT.getCode()); + } + + + private static void addBaseLoincMandatoryFilesToZip( + ZipCollectionBuilder theFiles, Boolean theIncludeTop2000, String theClassPathPrefix) throws IOException { + theFiles.addFileZip(theClassPathPrefix, LOINC_XML_FILE.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_GROUP_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_GROUP_TERMS_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PARENT_GROUP_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_HIERARCHY_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_LINK_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_ANSWERLIST_LINK_DUPLICATE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_PART_RELATED_CODE_MAPPING_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_DOCUMENT_ONTOLOGY_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_RSNA_PLAYBOOK_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_IMAGING_DOCUMENT_CODES_FILE_DEFAULT.getCode()); + if (theIncludeTop2000) { + theFiles.addFileZip(theClassPathPrefix, LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE_DEFAULT.getCode()); + theFiles.addFileZip(theClassPathPrefix, LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE_DEFAULT.getCode()); + } + } + + + private String getClassPathPrefix(String theVersion) { + String theClassPathPrefix = "/loinc-ver/v-no-version/"; + + if (StringUtils.isBlank(theVersion)) return theClassPathPrefix; + + switch(theVersion) { + case "2.67": + return "/loinc-ver/v267/"; + case "2.68": + return "/loinc-ver/v268/"; + case "2.69": + return "/loinc-ver/v269/"; + } + + fail("Setup failed. Unexpected version: " + theVersion); + return null; + } + + + + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/BatchJobHelper.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/BatchJobHelper.java index c9633a50920..80dc101e54e 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/BatchJobHelper.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/BatchJobHelper.java @@ -48,17 +48,30 @@ public class BatchJobHelper { myJobExplorer = theJobExplorer; } + public List awaitAllBulkJobCompletions(String... theJobNames) { + return awaitAllBulkJobCompletions(true, theJobNames); + } + + /** + * Await and report for job completions + * @param theFailIfNotJobsFound indicate if must fail in case no matching jobs are found + * @param theJobNames The job names to match + * @return the matched JobExecution(s) + */ + public List awaitAllBulkJobCompletions(boolean theFailIfNotJobsFound, String... theJobNames) { assert theJobNames.length > 0; List matchingJobInstances = new ArrayList<>(); for (String nextName : theJobNames) { matchingJobInstances.addAll(myJobExplorer.findJobInstancesByJobName(nextName, 0, 100)); } - if (matchingJobInstances.isEmpty()) { - List wantNames = Arrays.asList(theJobNames); - List haveNames = myJobExplorer.getJobNames(); - fail("There are no jobs running - Want names " + wantNames + " and have names " + haveNames); + if (theFailIfNotJobsFound) { + if (matchingJobInstances.isEmpty()) { + List wantNames = Arrays.asList(theJobNames); + List haveNames = myJobExplorer.getJobNames(); + fail("There are no jobs running - Want names " + wantNames + " and have names " + haveNames); + } } List matchingExecutions = matchingJobInstances.stream().flatMap(jobInstance -> myJobExplorer.getJobExecutions(jobInstance).stream()).collect(Collectors.toList()); awaitJobCompletions(matchingExecutions); @@ -82,7 +95,11 @@ public class BatchJobHelper { await().atMost(120, TimeUnit.SECONDS).until(() -> { JobExecution jobExecution = myJobExplorer.getJobExecution(theJobExecution.getId()); ourLog.info("JobExecution {} currently has status: {}- Failures if any: {}", theJobExecution.getId(), jobExecution.getStatus(), jobExecution.getFailureExceptions()); - return jobExecution.getStatus() == BatchStatus.COMPLETED || jobExecution.getStatus() == BatchStatus.FAILED; + // JM: Adding ABANDONED status because given the description, it s similar to FAILURE, and we need to avoid tests failing because + // of wait timeouts caused by unmatched statuses. Also adding STOPPED because tests were found where this wait timed out + // with jobs keeping that status during the whole wait + return jobExecution.getStatus() == BatchStatus.COMPLETED || jobExecution.getStatus() == BatchStatus.FAILED + || jobExecution.getStatus() == BatchStatus.ABANDONED || jobExecution.getStatus() == BatchStatus.STOPPED; }); } From e3a5aaf298ff80d93412b9cd911de1b60c42dbb7 Mon Sep 17 00:00:00 2001 From: JP Date: Fri, 19 Nov 2021 07:02:23 -0700 Subject: [PATCH 6/8] Updated cql docs (#3175) --- .../images/measure_evaluation_sequence.png | Bin 0 -> 50765 bytes .../ref.measure.architecture.drawio.svg | 528 ++++++++++++++++++ ...f.measure.measure-evaluation.sequence.puml | 43 ++ .../ca/uhn/hapi/fhir/docs/images/styling.puml | 126 +++++ .../uhn/hapi/fhir/docs/server_jpa_cql/cql.md | 17 +- .../fhir/docs/server_jpa_cql/cql_measure.md | 406 ++++++++++++++ 6 files changed, 1114 insertions(+), 6 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/measure_evaluation_sequence.png create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.architecture.drawio.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.measure-evaluation.sequence.puml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/styling.puml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/measure_evaluation_sequence.png b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/measure_evaluation_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d5ace8c2a4499d3397814759b437c8c21104c7 GIT binary patch literal 50765 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 + + + + + + +

+
+
+ Docker Container +
+
+
+ + + Docker Container + + + + + + + +
+
+
+ Measure / CQL Components +
+
+
+
+ + Measure / CQL Components + +
+
+ + + + + +
+
+
+ H2 +
+ (Resource Tables, Cache Tables) +
+
+
+
+ + H2... + +
+
+ + + + +
+
+
+ Tomcat Server +
+
+
+
+ + Tomcat Server + +
+
+ + + + +
+
+
+ Spring Web App Context +
+
+
+
+ + Spring Web App Context + +
+
+ + + + +
+
+
+ BaseServlet +
+
+
+
+ + BaseServlet + +
+
+ + + + +
+
+
+ Operation Providers ($evaluate-measure) +
+
+
+
+ + Operation Providers ($evaluate-measure) + +
+
+ + + + +
+
+
+ CQL Evaluator +
+
+
+
+ + CQL Evaluator + +
+
+ + + + +
+
+
+ FHIR Resource Daos +
+ (Resource +
+ CRUD + Searches) +
+
+
+
+ + FHIR Resource Daos... + +
+
+ + + + +
+
+
+ System Dao +
+ (Transactions) +
+
+
+
+ + System Dao... + +
+
+ + + + +
+
+
+ Terminology Service +
+ (Hierarchical Code Searches, Expansion) +
+
+
+
+ + Terminology Service... + +
+
+ + + + + +
+
+
+ Lucene +
+ (Termnology Index) +
+
+
+
+ + Lucene... + +
+
+ + + + +
+
+
+ JPA (Hibernate) +
+
+
+
+ + JPA (Hibernate) + +
+
+ + + + + + + + +
+
+
+ CQL Engine +
+
+
+
+ + CQL Engine + +
+
+ + + + +
+
+
+ CQL Translator +
+
+
+
+ + CQL Translator + +
+
+ + + + +
+
+
+ JPA +
+ TerminologyProvider +
+
+
+
+ + JPA... + +
+
+ + + + +
+
+
+ JPA +
+ FhirRetrieve +
+ Provider +
+
+
+
+ + JPA... + +
+
+ + + + +
+
+
+ LibraryContent +
+ Provider +
+
+
+
+ + LibraryContent... + +
+
+ + + + +
+
+
+ HAPI +
+ ELM +
+ Cache +
+
+
+
+ + HAPI... + +
+
+ + + + + +
+
+
+ REST API +
+
+
+
+ + REST API + +
+
+ + + + +
+
+
+ HAPI +
+ FhirDal +
+
+
+
+ + HAPI... + +
+
+ + + + + +
+
+
+ HAPI Data Access Layer +
+
+
+
+ + HAPI Data Access Lay... + +
+
+ + + + + +
+
+
+ HAPI CQL Interface Implementations +
+
+
+
+ + HAPI CQL Interface Impleme... + +
+
+ + + + + + +
+
+
+ Server +
+
+
+
+ + Server + +
+
+ + + + +
+
+
+ CQL Interfaces +
+
+
+
+ + CQL Interfaces + +
+
+ + + + +
+
+
+ Core CQL modules +
+
+
+
+ + Core CQL modules + +
+
+ + + + + + +
+
+
+ FHIR Operation Implementations +
+
+
+
+ + FHIR Operation Implemen... + +
+
+ + + + +
+
+
+ CQL Interfaces +
+
+
+
+ + CQL Interfaces + +
+
+ + + + + + Viewer does not support full SVG 1.1 + + + + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.measure-evaluation.sequence.puml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.measure-evaluation.sequence.puml new file mode 100644 index 00000000000..219fa5f1025 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.measure-evaluation.sequence.puml @@ -0,0 +1,43 @@ +@startuml measure_evaluation_sequence +!include styling.puml +title Measure $evaluate-measure + +actor User as User +participant OperationProvider as "HAPI Measure Operation Provider" +participant CQLEngine as "CQL Engine" +participant HAPI as "HAPI CQL Adapters" +participant JPA as "HAPI JPA / Terminology Providers" + +User -> OperationProvider: invoke $evaluate-measure +OperationProvider -> JPA: read Measure +JPA --> OperationProvider: return Measure +OperationProvider -> JPA: read Libraries +JPA --> OperationProvider: return Libraries +OperationProvider -> OperationProvider: convert FHIR Libraries to ELM libraries +OperationProvider-> CQLEngine **: create with ELM Libraries + +OperationProvider -> JPA: get Subjects +JPA --> OperationProvider: return Subjects +loop each Subject +OperationProvider -> CQLEngine: set current Subject context +loop each SDE/Population +OperationProvider -> CQLEngine: evaluate SDE/Population criteria +CQLEngine -> CQLEngine: evaluate Definition +alt terminology required +CQLEngine -> HAPI: retrieve terminology +HAPI -> JPA: request terminology +JPA --> HAPI: return terminology +HAPI --> CQLEngine: return terminology +end +CQLEngine -> HAPI: retrieve data +HAPI -> JPA: request data +JPA --> HAPI: return data +HAPI --> CQLEngine: return data +CQLEngine --> OperationProvider: return SDE/Population criteria result +end +end +OperationProvider -> CQLEngine !!: destroy +OperationProvider -> OperationProvider: build MeasureReport +OperationProvider -> OperationProvider: score MeasureReport +OperationProvider -> User: return MeasureReport +@enduml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/styling.puml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/styling.puml new file mode 100644 index 00000000000..3d4971d5d4d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/styling.puml @@ -0,0 +1,126 @@ +@startuml styling + +'These are Alphora colors, they need +' to be updated to reflect the HAPI style guide +!$white = "#fff" + +!$greylt000 = "#F4F6F5" +!$greylt100 = "#DDE3E0" +!$greylt200 = "#C7D1CC" +!$greylt300 = "#B0BFB8" +!$grey = $greylt000 + +!$greydk000 = "#404F47" +!$greydk100 = "#2E3833" +!$greydk200 = "#1C221F" +!$greydk250 = "#121614" +!$greydk300 = "#090B0A" +!$black = $greydk300 + +!$blue000 = "#4B7DD2" +!$blue100 = "#3064BF" +!$blue200 = "#2956A3" +!$blue300 = "#214583" +!$blue = $blue100 + +!$purple000 = "#645FAB" +!$purple100 = "#524D93" +!$purple200 = "#433F78" +!$purple300 = "#34315E" +!$purple = $purple100 + +!$green000 = "#568A67" +!$green100 = "#477154" +!$green200 = "#375841" +!$green300 = "#273F2F" +!$green = $green100 + +!$yellow000 = "#FBB337" +!$yellow100 = "#FAA40F" +!$yellow200 = "#DC8D04" +!$yellow300 = "#B47304" +!$yellow = $yellow100 + +!$red000 = "#FF6633" +!$red100 = "#FF4000" +!$red200 = "#E03800" +!$red300 = "#B82E00" +!$red = $red100 + +skinparam { + defaultFontName Source Sans Pro + + TitleFontStyle bold + + BackgroundColor $white + Shadowing false + + ArrowColor $greydk000 + ArrowFontColor $black + ArrowFontSize 12 + ArrowFont Open Sans + + DelayFontColor $black + DelayFontSize 12 + + ActorBorderColor $black + ActorBackgroundColor $white + ActorFontColor $black + ActorFontSize 14 + ActorFontStyle bold + + ParticipantBorderColor $black + ParticipantBackgroundColor $yellow + ParticipantFontColor $black + ParticipantFontSize 14 + ParticipantFontStyle bold + + DatabaseBorderColor $black + DatabaseBackgroundColor $yellow + DatabaseFontColor $black + DatabaseFontSize 14 + DatabaseFontStyle bold + +} + + +skinparam Sequence { + MessageAlign center + + LifeLineBorderColor $black + ' loop, alt, ref + GroupBodyBackgroundColor $white + GroupBackgroundColor $blue + GroupHeaderFontColor $white + GroupHeaderFontSize 12 + GroupFontSize 12 + + BoxBackgroundColor $greylt000 + BoxBorderColor $black + BoxFontColor $black + BoxFontSize 12 + + + ReferenceBorderColor $black + ReferenceFontColor $black + ReferenceFontSize 12 + ReferenceHeaderBackgroundColor $blue + + DividerBackgroundColor $blue + DividerBorderColor $black + DividerFontColor $white + DividerFontSize 12 +} + +skinparam Note { + BackgroundColor $green + BorderColor $black + FontColor $white + FontStyle bold + FontSize 12 + Font Open Sans +} + +hide footbox + +@enduml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md index ac114e709cc..a069e590584 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md @@ -25,14 +25,19 @@ There are two Spring beans available that add CQL processing to HAPI. You can en * `ca.uhn.fhir.cql.config.CqlDstu3Config` * `ca.uhn.fhir.cql.config.CqlR4Config` -## Operations +## Clinical Reasoning Operations -HAPI provides implementations for some Measure operations for DSTU3 and R4 +HAPI provides implementations for some operations in DSTU3 and R4: -### $evaluate-measure +[Measure Operations](cql_measure) -The [$evaluate-measure](http://hl7.org/fhir/measure-operation-evaluate-measure.html) operation allows the evaluation of a clinical quality measure. This operation is invoked on an instance of a Measure resource: +## Roadmap -`http://base/Measure/measureId/$evaluate-measure?subject=124&periodStart=2014-01&periodend=2014-03` +Further development of the CQL capabilities in HAPI is planned: -The Measure will be evaluated, including any CQL that is referenced. The CQL evaluation requires that all the supporting knowledge artifacts for a given Measure be loaded on the HAPI server, including `Libaries` and `ValueSets`. +* Additional features and performance enhancements for Measure evaluation +* Additional FHIR Clinical Reasoning Module operations: + * Library $evaluate + * PlanDefinition $apply +* Support for the CPG IG Operations + * $cql diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md new file mode 100644 index 00000000000..208b6b3f41a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md @@ -0,0 +1,406 @@ +# Measures + +## Introduction + +The FHIR Clinical Reasoning Module defines the [Measure resource](https://www.hl7.org/fhir/measure.html) and several [associated operations](https://www.hl7.org/fhir/measure-operations.html). The Measure Resource represents a structured, computable definition of a health-related measure such as a clinical quality measure, public health indicator, or population analytics measure. These Measures can then be used for reporting, analytics, and data-exchange purposes. + +Electronic Clinical Quality Measures (eCQMs) in FHIR are represented as a FHIR Measure resource containing metadata and terminology, a population criteria section, and at least one FHIR Library resource containing a data criteria section as well as the logic used to define the population criteria. The population criteria section typically contains initial population criteria, denominator criteria, and numerator criteria sub-components, among others. This is elaborated upon in greater detail in the [CQF Measures IG](http://hl7.org/fhir/us/cqfmeasures). An example of an eCQM as defined in FHIR looks like: + +```json +{ + "resourceType" : "Measure", + "library" : [ + "http://hl7.org/fhir/us/cqfmeasures/Library/EXMLogic" + ], + "group" : [ + { + "population" : [ + { + "code" : { + "coding" : [ + { + "code" : "initial-population" + } + ] + }, + "criteria" : { + "language" : "text/cql.identifier", + "expression" : "Initial Population" + } + }, + { + "code" : { + "coding" : [ + { + "code" : "numerator" + } + ] + }, + "criteria" : { + "language" : "text/cql.identifier", + "expression" : "Numerator" + } + }, + { + "code" : { + "coding" : [ + { + "code" : "denominator" + } + ] + }, + "criteria" : { + "language" : "text/cql.identifier", + "expression" : "Denominator" + } + } + ] + } + ] +} + +``` + +Measures are then scored according the whether a subjects (or subjects) are members of the various populations. + +For example, a Measure for Breast Cancer screening might define an Initial Population (via CQL expressions) of "all women", a Denominator of "women over 35", and a Numerator of "women over 35 who have had breast cancer screenings in the past year". If the Measure is evaluated against a population of 100 women, 50 are over 35, and of those 25 have had breast cancer screenings in the past year, the final score would be 50%1 (total number in numerator / total number in the denominator). + +1. There are several methods for scoring Measures, this is meant only as an example. + +## Operations + +HAPI implements the [$evaluate-measure](https://www.hl7.org/fhir/operation-measure-evaluate-measure.html) operation. Support for additional operations is planned. + +## Evaluate Measure + +The `$evaluate-measure` operation is used to execute a Measure as specified by the relevant FHIR Resources against a subject or set of subjects. This implementation currently focuses primarily on supporting the narrower evaluation requirements defined by the [CQF Measures IG](http://hl7.org/fhir/us/cqfmeasures). Some support for extensions defined by other IGs is included as well, and the implementation aims to support a wider range of functionality in the future. + +### Example Measure + +Several example Measures are available in the [ecqm-content-r4](https://github.com/cqframework/ecqm-content-r4) IG. Full Bundles with all the required supporting resources are available [here](https://github.com/cqframework/ecqm-content-r4/tree/master/bundles/measure). You can download a Bundle and load it on your server as a transaction: + +```bash +POST http://your-server-base/fhir BreastCancerScreeningFHIR-bundle.json +``` + +These Bundles also include example Patient clinical data so once posted Measure evaluation can be invoked with: + +```bash +GET http://your-server-base/fhir/Measure/BreastCancerScreeningFHIR/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31&subject=numerator&reportType=subject +``` + +### Measure Features + +The FHIR Measure specification defines several different types of Measures and various parameters for controlling the Measure evaluation. This section describes the features supported by HAPI. + +#### Reporting Period + +The `periodStart` and `periodEnd` parameters are used to control the Reporting Period for which a report is generated. This corresponds to `Measurement Period` defined in the CQL logic, as defined by the conformance requirements in the CQF Measures IG. Both `periodStart` and `periodEnd` must be used or neither must be used. + +If neither are used the default reporting period specified in the CQL logic is used, as shown here + +```cql +parameter "Measurement Period" Interval + default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0) +``` + +If neither are used and there is no default reporting period in the CQL logic an error is thrown. + +A request using `periodStart` and `periodEnd` looks like: + +```bash +GET fhir/Measure//$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31 +``` + +`periodStart` and `periodEnd` support Dates (YYYY, YYYY-MM, or YYYY-MM-DD) and DateTimes (YYYY-MM-DDThh:mm:ss+zz:zz) + +#### Report Types + +Measure report types determine what data is returned from the evaluation. This is controlled with the `reportType` parameter on the $evaluate-measure Operation + +| Report Type | Supported | Description | +| ------------ | :----------------: | -------------------------------------------------------------------------------------------------------------- | +| subject | :white_check_mark: | Measure report for a single subject (e.g. one patient). Includes additional detail, such as evaluatedResources | +| subject-list | :white_check_mark: | Measure report including the list of subjects in each population (e.g. all the patients in the "numerator") | +| population | :white_check_mark: | Summary measure report for a population | + +NOTE: There's an open issue on the FHIR specification to align these names to the MeasureReportType value set. + +A request using `reportType` looks like: + +```bash +GET fhir/Measure//$evaluate-measure?reportType=subject-list +``` + +#### Subject Types + +The subject of a measure evaluation is controlled with the `subject` (R4+) and `patient` (DSTU3) operation parameters. Currently the only subject type supported by HAPI is Patient. This means that all Measure evaluation and reporting happens with respect to a Patient or set of Patient resources. + +| Subject Type | Supported | Description | +| ----------------- | :------------------: | ----------------- | +| Patient | :white_check_mark: | A Patient | +| Practitioner | :white_large_square: | A Practitioner | +| Organization | :white_large_square: | An Organization | +| Location | :white_large_square: | A Location | +| Device | :white_large_square: | A Device | +| Group1 | :white_large_square: | A set of subjects | + +1. See next section + +A request using `subject` looks like: + +```bash +GET fhir/Measure//$evaluate-measure?subject=Patient/123 +``` + +##### Selecting a set of Patients + +The set of Patients used for Measure evaluation is controlled with the `subject` (R4+) or `patient` (DSTU3), and `practitioner` parameters. The two parameters are mutually exclusive. + +| Parameter | Supported | Description | +| ----------------------------------------------------- | :------------------: | ----------------------------------------------------------------------- | +| Not specified | :white_check_mark: | All Patients on the server | +| `subject=XXX` or `subject=Patient/XXX` | :white_check_mark: | A single Patient | +| `practitioner=XXX` or `practitioner=Practitioner/XXX` | :white_check_mark: | All Patients whose `generalPractitioner` is the referenced Practitioner | +| `subject=Group/XXX`1 | :white_large_square: | A Group containing subjects | +| `subject=XXX` AND `practitioner=XXX` | :x: | Not a valid combination | + +1. Referencing a Group of Patients as the subject is defined in the ATR IG and is on the roadmap. This will allow much more control over which Patients are included in the evaluated set. + +A request using `practitioner` looks like: + +```bash +GET fhir/Measure//$evaluate-measure?practitioner=Practitioner/XYZ +``` + +#### ReportType, Subject, Practitioner Matrix + +The following table shows the combinations of the `subject` (or `patient`), `practitioner` and `reportType` parameters that are valid + +| | subject reportType | subject-list reportType | population reportType | +| ---------------- | :----------------: | :-------------------------------: | :-------------------------------: | +| subject parameter | :white_check_mark: | :white_check_mark: 1,2 | :white_check_mark: 1,2 | +| practitioner parameter | :x:3 | :white_check_mark: | :white_check_mark: | + +1. Including the subject parameter restricts the Measure evaluation to a single Patient. Omit the `subject` (or `patient`) parameter to get report for multiple Patients. The subject-list and population report types have less detail than a subject report. +2. A Group `subject` with a subject-list or population `reportType` will be a valid combination once Group support is implemented. +3. A practitioner have may zero, one, or many patients so a practitioner report always assumes a set. + +#### Scoring Methods + +The Measure scoring method determines how a Measure score is calculated. It is set with the [scoring](https://www.hl7.org/fhir/measure-definitions.html#Measure.scoring) element on the Measure resource. + +The HAPI implementation conforms to the requirements defined by the CQF Measures IG. A more detailed description of each scoring method is linked in the table below. + +| Scoring Method | Supported | Description | +| ------------------- | :------------------: | ---------------------------------------------------------------------------------------------------------------------- | +| proportion | :white_check_mark: | [Proportion Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#proportion-measures) | +| ratio | :white_check_mark: | [Ratio Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#ratio-measures) | +| continuous-variable | :white_check_mark: | [Continuous Variable](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#continuous-variable-measure) | +| cohort | :white_check_mark:* | [Cohort](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#cohort-definitions) | +| composite | :white_large_square: | See below | + +* The cohort Measure scoring support is partial. The HAPI implementation does not yet return the required Measure observations + +An example Measure resource with `scoring` defined looks like: + +```json +{ + "resourceType": "Measure", + "scoring": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "proportion", + "display": "Proportion" + } ] + } +} +``` + +##### Composite Scoring + +A composite Measure is scored by combining and/or aggregating the results of other Measures. The [compositeScoring](https://www.hl7.org/fhir/measure-definitions.html#Measure.compositeScoring) element is used to control how composite Measures are scored. HAPI does not currently support any composite scoring method. + +| Composite Scoring Method | Supported | Description | +| ------------------------ | :------------------: | ---------------------------------------------------------------------------------------------- | +| opportunity | :white_large_square: | Combines Numerators and Denominators for each component Measure | +| all-or-nothing | :white_large_square: | Includes individuals that are in the numerator for all component Measures | +| linear | :white_large_square: | Gives an individual score based on the number of numerators in which they appear | +| weighted | :white_large_square: | Gives an individual a cored based on a weighted factor for each numerator in which they appear | + +#### Populations + +The HAPI implementation uses the populations defined by the CQF Measures IG for each scoring type. A matrix of the supported populations is shown in the [Criteria Names](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#criteria-names) section of the CQF Measures IG. + +#### Population Criteria + +The logical criteria used for determining each Measure population is defined by the [Measure.group.population.criteria element](). The Measure specification allows population criteria to be defined using FHIR Path, CQL, or other languages as appropriate. The HAPI implementation currently only supports using CQL. The relationship between a Measure Population and CQL is illustrated in the [Population Criteria](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#population-criteria) section of the CQF Measures IG. + +An example Measure resource with a population criteria referencing a CQL identifier looks like: + +```json +{ + "resourceType": "Measure", + "group": [ { + "population": [ { + "code": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } ] + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Initial Population" + } + }] + }] +} +``` + +##### Criteria Expression Type + +| Expression Type | Supported | +| --------------- | :------------------: | +| CQL | :white_check_mark: | +| FHIR Path | :white_large_square: | + +#### Supplemental Data Elements + +Supplemental Data Elements are used to report additional information about the subjects that may not be included in the in the Population criteria definitions. For example, it may be of interest to report the gender of all subjects for informational purposes. Supplemental data elements are defined by the [Measure.supplementalData](http://www.hl7.org/fhir/measure-definitions.html#Measure.supplementalData) element, and are reported as Observations in the evaluatedResources of the MeasureReport. + +Supplemental Data Elements can be specified as either CQL definitions or FHIR Path expressions. + +| Expression Type | Supported | +| --------------- | :------------------: | +| CQL | :white_check_mark: | +| FHIR Path | :white_large_square: | + +An example Measure resource with some supplemental data elements set looks like: + +```json +{ +"resourceType": "Measure", + "supplementalData": [ { + "code": { + "text": "sde-ethnicity" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "SDE Ethnicity" + } + }] +} +``` + +#### Stratifiers + +Stratifiers are used divide Measure populations into segments of interest. For example, it may be of interest to compare the Measure score between different age groups or genders. Each stratum within a stratification is scored the same way as the overall population. Stratifiers are defined using the [Measure.group.stratifier](http://hl7.org/fhir/R4/measure-definitions.html#Measure.group.stratifier) element. + +HAPI does not implement stratifier support but it's on the roadmap. + +An example Measure resource with a stratifier set looks like: + +```json +{ + "resourceType": "Measure", + "group": [ { + "stratifier": [ { + "code": { + "text": "Stratum 1" + }, + "criteria": { + "language": "text/cql.identifier", + "expression": "Stratification 1" + } + }] + }] +} +``` + +##### Stratifier Expression Support + +As with Populations and Supplemental Data Elements the criteria used for Stratification may be defined with CQL or FHIR Path. + +| Expression Type | Supported | +| --------------- | :------------------: | +| CQL | :white_large_square: | +| FHIR Path | :white_large_square: | + +##### Stratifier Component Support + +The Measure specification also supports multi-dimensional stratification, for cases where more than one data element is needed. + +| Stratifier Type | Supported | +| ---------------- | :------------------: | +| Single Component | :white_large_square: | +| Multi Component | :white_large_square: | + +#### Evaluated Resources + +A FHIR MeasureReport permits referencing the Resources used when evaluating in the [MeasureReport.evaluatedResource](https://www.hl7.org/fhir/measurereport-definitions.html#MeasureReport.evaluatedResource) element. HAPI includes these resources when generating `subject` reports for a single Patient. Evaluated resources for `population` or `subject-list` reports are not included. For large populations this could quickly become an extremely large number of resources. + +The evaluated resources will not include every resource on the HAPI server for a given subject. Rather, it includes only the resources that were retrieved from the server by the CQL logic that was evaluated. This corresponds to the data-requirements for a given Measure. As an example, consider the following CQL: + +```cql +valueset "Example Value Set" : 'http://fhir.org/example-value-set' + +define "Example Observations": + [Observation : "Example Value Set"] +``` + +That CQL will only select Observation Resources that have a code in the "Example Value Set". Those Observations will be reported in the Evaluated Resources while any others will not. + +#### Last Received On + +The `lastReceivedOn` parameter is the date the Measure was evaluated and reported. It is used to limit the number of resources reported in the Measure report for individual reports. It is currently not supported by HAPI. + +#### Extensions + +A number of extensions to Measure evaluation defined by various IGs are supported. They are described briefly in the table below. + +| Extension | Description | +| --------- | ----------- | +| http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine | Used to evaluate different product lines (e.g. Medicare, Private, etc.) | +| http://hl7.org/fhir/StructureDefinition/cqf-measureInfo | Used to demark a Measure Observation | +| http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-populationReference | Used to specify the population that triggered a particular `evaluatedResource`| + +There's not currently a way to configure which extensions are enabled. All supported extensions are always enabled. + +## Architecture + +Below are a few diagrams that show the overall architecture of Measure evaluation and how it fits into the HAPI FHIR Server. + +### Component Diagram + +This is a simplified component diagram of the Measure evaluation architecture + +![Measure Evaluation Architecture](images/../../images/ref.measure.architecture.drawio.svg) + +### Sequence Chart + +This sequence chart approximates the Measure evaluation logic implemented by HAPI. + +![Measure Evaluation Sequence Chart](images/../../images/measure_evaluation_sequence.png) + +## FAQs + +Q: I get an error saying HAPI can't locate my library, and I've verified it's on the server. + +A: HAPI follows the [Library conformance requirements](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#conformance-requirement-3-1) defined by the CQF Measures IG, meaning the Library must have a `logic-library` type, the name and versions of the FHIR Library and CQL Library must match, and the url of the Library must end in the name of the Library. + +FHIR Libraries generated from CQL via the IG Publisher follow these requirements automatically. + +Q: Does HAPI support partitions for evaluation? + +A: Yes, though the Measure and associated Resources must be in the same partition as the clinical data being used. + +## Roadmap + +* Complete cohort implementation +* Support for stratifiers +* Support for Group subjects +* Support for FHIRPath expressions in Stratifiers, Supplemental Data Elements, and Population Criteria +* $data-requirements, $collect-data, $submit-data, and $care-gaps operations +* Support for more extensions defined in the CQF Measures, CPG, and ATR IGs From 0a64294467a4696b7d8fa52a9ae034caab248d4b Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 19 Nov 2021 10:24:44 -0500 Subject: [PATCH 7/8] Release 5.6.0 (#3174) * 3138 externalized binary packages (#3139) * Add test and impl * Add changelog * Fix test * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml * add beans to test configs * Typo * Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java Co-authored-by: michaelabuckley * Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java Co-authored-by: michaelabuckley Co-authored-by: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Co-authored-by: michaelabuckley * 3131 - Added support for the lookup operation in the Remote Terminology code (#3134) * Remove leading underscores from identifiers (#3146) * Version bump * License files * version.yaml Co-authored-by: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Co-authored-by: michaelabuckley --- ...on-lookup-to-remoteterminologyservice.yaml | 4 + .../3138-support-externalized-binaries.yaml | 6 + .../hapi/fhir/changelog/5_6_0/version.yaml | 3 + .../server_plain/rest_operations_search.md | 10 +- .../binstore/BaseBinaryStorageSvcImpl.java | 42 +++- .../fhir/jpa/binstore/IBinaryStorageSvc.java | 11 + .../binstore/NullBinaryStorageSvcImpl.java | 7 + .../fhir/jpa/packages/JpaPackageCache.java | 54 +++-- .../uhn/fhir/jpa/config/TestDstu2Config.java | 2 - .../ca/uhn/fhir/jpa/config/TestJPAConfig.java | 8 + .../ca/uhn/fhir/jpa/packages/NpmR4Test.java | 72 ++++++ .../jpa/model/entity/BinaryStorageEntity.java | 1 + .../ca/uhn/fhir/jpa/config/TestJpaConfig.java | 9 + .../fhir/jpa/config/TestJpaDstu3Config.java | 4 - ...teTerminologyServiceValidationSupport.java | 169 +++++++++++++- ...rminologyServiceValidationSupportTest.java | 172 +++++++++++++- ...logyServiceValidationSupportDstu3Test.java | 214 ++++++++++++++++++ ...inologyServiceValidationSupportR5Test.java | 35 +++ 18 files changed, 785 insertions(+), 38 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml new file mode 100644 index 00000000000..338009e842b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3131-add-operation-lookup-to-remoteterminologyservice.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 3131 +title: "Provided a Remote Terminology Service implementation for the $lookup Operation." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml new file mode 100644 index 00000000000..82fdd9c5792 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3138-support-externalized-binaries.yaml @@ -0,0 +1,6 @@ +--- +type: fix +jira: SMILE-3152 +issue: 3138 +title: "Previously, the package registry would not work correctly when externalized binary storage was enabled. This has been corrected." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml new file mode 100644 index 00000000000..c86c3b6785f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-11-18" +codename: "Raccoon" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md index 4f770485f44..cfca23c55d4 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/rest_operations_search.md @@ -346,7 +346,7 @@ Sort specifications can be passed into handler methods by adding a parameter of Example URL to invoke this method: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_sort=given +http://fhir.example.com/Patient?identifier=urn:foo|123&_sort=given ``` @@ -364,7 +364,7 @@ of resources fetched from the database. Example URL to invoke this method: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10 ``` # Paging @@ -388,17 +388,17 @@ for more information. Example URL to invoke this method for the first page: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10&_offset=0 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10&_offset=0 ``` or just ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10 ``` Example URL to invoke this method for the second page: ```url -http://fhir.example.com/Patient?_identifier=urn:foo|123&_count=10&_offset=10 +http://fhir.example.com/Patient?identifier=urn:foo|123&_count=10&_offset=10 ``` Note that if the paging provider is configured to be database backed, `_offset=0` behaves differently than no `_offset`. This diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java index e8f4da57e90..d2f000fea58 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/BaseBinaryStorageSvcImpl.java @@ -20,20 +20,31 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; +import ca.uhn.fhir.util.BinaryUtil; +import ca.uhn.fhir.util.HapiExtensions; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.hash.HashingInputStream; import com.google.common.io.ByteStreams; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; +import java.io.IOException; import java.io.InputStream; import java.security.SecureRandom; +import java.util.Optional; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { private final SecureRandom myRandom; @@ -41,6 +52,8 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { private final int ID_LENGTH = 100; private int myMaximumBinarySize = Integer.MAX_VALUE; private int myMinimumBinarySize; + @Autowired + private FhirContext myFhirContext; BaseBinaryStorageSvcImpl() { myRandom = new SecureRandom(); @@ -104,7 +117,6 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { }; } - String provideIdForNewBlob(String theBlobIdOrNull) { String id = theBlobIdOrNull; if (isBlank(theBlobIdOrNull)) { @@ -112,4 +124,32 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { } return id; } + + @Override + public byte[] fetchDataBlobFromBinary(IBaseBinary theBaseBinary) throws IOException { + IPrimitiveType dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); + byte[] value = dataElement.getValue(); + if (value == null) { + Optional attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); + if (attachmentId.isPresent()) { + value = fetchBlob(theBaseBinary.getIdElement(), attachmentId.get()); + } else { + throw new InternalErrorException("Unable to load binary blob data for " + theBaseBinary.getIdElement()); + } + } + return value; + } + + @SuppressWarnings("unchecked") + private Optional getAttachmentId(IBaseHasExtensions theBaseBinary) { + return theBaseBinary + .getExtension() + .stream() + .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) + .filter(t -> t.getValue() instanceof IPrimitiveType) + .map(t -> (IPrimitiveType) t.getValue()) + .map(t -> t.getValue()) + .filter(t -> isNotBlank(t)) + .findFirst(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java index b283d46b918..9100f144d21 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/IBinaryStorageSvc.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IIdType; import javax.annotation.Nonnull; @@ -101,4 +103,13 @@ public interface IBinaryStorageSvc { * @return The payload as a byte array */ byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException; + + /** + * Fetch the byte[] contents of a given Binary resource's `data` element. If the data is a standard base64encoded string that is embedded, return it. + * Otherwise, attempt to load the externalized binary blob via the the externalized binary storage service. + * + * @param theResourceId The resource ID The ID of the Binary resource you want to extract data bytes from + * @return The binary data blob as a byte array + */ + byte[] fetchDataBlobFromBinary(IBaseBinary theResource) throws IOException; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java index 1d798cd7b26..4cdd7c38f94 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/NullBinaryStorageSvcImpl.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.binstore; * #L% */ +import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IIdType; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -81,4 +83,9 @@ public class NullBinaryStorageSvcImpl implements IBinaryStorageSvc { public byte[] fetchBlob(IIdType theResourceId, String theBlobId) { throw new UnsupportedOperationException(); } + + @Override + public byte[] fetchDataBlobFromBinary(IBaseBinary theResource) throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 6a8db13d73d..59790c4197c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; @@ -123,6 +124,9 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac @Autowired private PartitionSettings myPartitionSettings; + @Autowired(required = false)//It is possible that some implementers will not create such a bean. + private IBinaryStorageSvc myBinaryStorageSvc; + @Override @Transactional public NpmPackage loadPackageFromCacheOnly(String theId, @Nullable String theVersion) { @@ -172,13 +176,37 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac private IHapiPackageCacheManager.PackageContents loadPackageContents(NpmPackageVersionEntity thePackageVersion) { IFhirResourceDao binaryDao = getBinaryDao(); IBaseBinary binary = binaryDao.readByPid(new ResourcePersistentId(thePackageVersion.getPackageBinary().getId())); + try { + byte[] content = fetchBlobFromBinary(binary); + PackageContents retVal = new PackageContents() + .setBytes(content) + .setPackageId(thePackageVersion.getPackageId()) + .setVersion(thePackageVersion.getVersionId()) + .setLastModified(thePackageVersion.getUpdatedTime()); + return retVal; + } catch (IOException e) { + throw new InternalErrorException("Failed to load package. There was a problem reading binaries", e); + } + } - PackageContents retVal = new PackageContents() - .setBytes(binary.getContent()) - .setPackageId(thePackageVersion.getPackageId()) - .setVersion(thePackageVersion.getVersionId()) - .setLastModified(thePackageVersion.getUpdatedTime()); - return retVal; + /** + * Helper method which will attempt to use the IBinaryStorageSvc to resolve the binary blob if available. If + * the bean is unavailable, fallback to assuming we are using an embedded base64 in the data element. + * @param theBinary the Binary who's `data` blob you want to retrieve + * @return a byte array containing the blob. + * + * @throws IOException + */ + private byte[] fetchBlobFromBinary(IBaseBinary theBinary) throws IOException { + if (myBinaryStorageSvc != null) { + return myBinaryStorageSvc.fetchDataBlobFromBinary(theBinary); + } else { + byte[] value = BinaryUtil.getOrCreateData(myCtx, theBinary).getValue(); + if (value == null) { + throw new InternalErrorException("Failed to fetch blob from Binary/" + theBinary.getIdElement()); + } + return value; + } } @SuppressWarnings("unchecked") @@ -487,14 +515,12 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac private IBaseResource loadPackageEntity(NpmPackageVersionResourceEntity contents) { try { - - ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); - IBaseBinary binary = getBinaryDao().readByPid(binaryPid); - byte[] resourceContentsBytes = BinaryUtil.getOrCreateData(myCtx, binary).getValue(); - String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); - - FhirContext packageContext = getFhirContext(contents.getFhirVersion()); - return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); + ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); + IBaseBinary binary = getBinaryDao().readByPid(binaryPid); + byte[] resourceContentsBytes= fetchBlobFromBinary(binary); + String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); + FhirContext packageContext = getFhirContext(contents.getFhirVersion()); + return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); } catch (Exception e) { throw new RuntimeException("Failed to load package resource " + contents, e); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java index 8fa1fac4348..fa6f055c222 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java @@ -172,6 +172,4 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return requestValidator; } - - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java index 4f98f112a5c..02f484b19de 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; +import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; @@ -70,4 +72,10 @@ public class TestJPAConfig { public BatchJobHelper batchJobHelper(JobExplorer theJobExplorer) { return new BatchJobHelper(theJobExplorer); } + + @Bean + @Lazy + public IBinaryStorageSvc binaryStorage() { + return new MemoryBinaryStorageSvcImpl(); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java index ed387fed101..eada70a648e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java @@ -270,6 +270,78 @@ public class NpmR4Test extends BaseJpaR4Test { }); } + @Test + public void testInstallR4PackageWithExternalizedBinaries() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myInterceptorService.registerInterceptor(myBinaryStorageInterceptor); + byte[] bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); + assertEquals(1, outcome.getResourcesInstalled().get("CodeSystem")); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + // Make sure we can fetch the package by ID and Version + NpmPackage pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", "0.12.0"); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure we can fetch the package by ID + pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", null); + assertEquals("0.12.0", pkg.version()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure DB rows were saved + runInTransaction(() -> { + NpmPackageEntity pkgEntity = myPackageDao.findByPackageId("hl7.fhir.uv.shorthand").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", pkgEntity.getPackageId()); + + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("0.12.0", versionEntity.getVersionId()); + assertEquals(3001, versionEntity.getPackageSizeBytes()); + assertEquals(true, versionEntity.isCurrentVersion()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("4.0.1", versionEntity.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, versionEntity.getFhirVersion()); + + NpmPackageVersionResourceEntity resource = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand").getContent().get(0); + assertEquals("http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand", resource.getCanonicalUrl()); + assertEquals("0.12.0", resource.getCanonicalVersion()); + assertEquals("ImplementationGuide-hl7.fhir.uv.shorthand.json", resource.getFilename()); + assertEquals("4.0.1", resource.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, resource.getFhirVersion()); + assertEquals(6155, resource.getResSizeBytes()); + }); + + // Fetch resource by URL + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Fetch resource by URL with version + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand|0.12.0"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Search for the installed resource + runInTransaction(() -> { + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.add(StructureDefinition.SP_URL, new UriParam("http://hl7.org/fhir/uv/shorthand/CodeSystem/shorthand-code-system")); + IBundleProvider result = myCodeSystemDao.search(map); + assertEquals(1, result.sizeOrThrowNpe()); + IBaseResource resource = result.getResources(0, 1).get(0); + assertEquals("CodeSystem/shorthand-code-system/_history/1", resource.getIdElement().toString()); + }); + + myInterceptorService.unregisterInterceptor(myBinaryStorageInterceptor); + } + @Test public void testNumericIdsInstalledWithNpmPrefix() throws Exception { myDaoConfig.setAllowExternalReferences(true); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java index a3988d12323..d3c9becccbf 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java @@ -30,6 +30,7 @@ public class BinaryStorageEntity { @Id @Column(name = "BLOB_ID", length = 200, nullable = false) + //N.B GGG: Note that the `blob id` is the same as the `externalized binary id`. private String myBlobId; @Column(name = "RESOURCE_ID", length = 100, nullable = false) private String myResourceId; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java index 1bbc78645a9..621a9ddcd0e 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaConfig.java @@ -21,10 +21,13 @@ package ca.uhn.fhir.jpa.config; */ import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc; +import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.orm.jpa.JpaTransactionManager; @@ -43,6 +46,12 @@ public class TestJpaConfig { return daoConfig().getModelConfig(); } + @Bean + @Lazy + public IBinaryStorageSvc binaryStorage() { + return new MemoryBinaryStorageSvcImpl(); + } + @Bean @Primary public JpaTransactionManager hapiTransactionManager(EntityManagerFactory entityManagerFactory) { diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java index 3ca64e792b3..05db35085e5 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaDstu3Config.java @@ -135,10 +135,6 @@ public class TestJpaDstu3Config extends BaseJavaConfigDstu3 { return requestValidator; } - @Bean - public IBinaryStorageSvc binaryStorage() { - return new MemoryBinaryStorageSvcImpl(); - } @Bean public DefaultProfileValidationSupport validationSupportChainDstu3() { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java index cd5b7197a51..dd804cc16f5 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java @@ -1,6 +1,7 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; @@ -9,8 +10,8 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.util.ParametersUtil; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.checkerframework.framework.qual.InvisibleQualifier; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -82,7 +83,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup */ private String extractCodeSystemForCode(ValueSet theValueSet, String theCode) { if (theValueSet.getCompose() == null || theValueSet.getCompose().getInclude() == null - || theValueSet.getCompose().getInclude().isEmpty()) { + || theValueSet.getCompose().getInclude().isEmpty()) { return null; } @@ -111,11 +112,9 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup } catch (IOException theE) { ourLog.error("IOException trying to serialize ValueSet to json: " + theE); } - return null; } - private String getVersionedCodeSystem(ValueSet.ConceptSetComponent theComponent) { String codeSystem = theComponent.getSystem(); if ( ! codeSystem.contains("|") && theComponent.hasVersion()) { @@ -124,7 +123,6 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup return codeSystem; } - @Override public IBaseResource fetchCodeSystem(String theSystem) { IGenericClient client = provideClient(); @@ -143,6 +141,167 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup return null; } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) { + Validate.notBlank(theCode, "theCode must be provided"); + + IGenericClient client = provideClient(); + FhirContext fhirContext = client.getFhirContext(); + FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion(); + + switch (fhirVersion) { + case DSTU3: + case R4: + IBaseParameters params = ParametersUtil.newInstance(fhirContext); + ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode); + if (!StringUtils.isEmpty(theSystem)) { + ParametersUtil.addParameterToParametersString(fhirContext, params, "system", theSystem); + } + if (!StringUtils.isEmpty(theDisplayLanguage)) { + ParametersUtil.addParameterToParametersString(fhirContext, params, "language", theDisplayLanguage); + } + Class codeSystemClass = myCtx.getResourceDefinition("CodeSystem").getImplementingClass(); + IBaseParameters outcome = client + .operation() + .onType((Class) codeSystemClass) + .named("$lookup") + .withParameters(params) + .useHttpGet() + .execute(); + if (outcome != null && !outcome.isEmpty()) { + switch (fhirVersion) { + case DSTU3: + return generateLookupCodeResultDSTU3(theCode, theSystem, (org.hl7.fhir.dstu3.model.Parameters)outcome); + case R4: + return generateLookupCodeResultR4(theCode, theSystem, (org.hl7.fhir.r4.model.Parameters)outcome); + } + } + break; + default: + throw new UnsupportedOperationException("Unsupported FHIR version '" + fhirVersion.getFhirVersionString() + + "'. Only DSTU3 and R4 are supported."); + } + return null; + } + + private LookupCodeResult generateLookupCodeResultDSTU3(String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) { + // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding + // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). + LookupCodeResult result = new LookupCodeResult(); + result.setSearchedForCode(theCode); + result.setSearchedForSystem(theSystem); + result.setFound(true); + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : outcomeDSTU3.getParameter()) { + switch (parameterComponent.getName()) { + case "property": + org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part"); + // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved + if (part != null && part.hasValues() && part.getValues().size() >= 2) { + String key = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); + String value = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); + if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { + result.getProperties().add(new StringConceptProperty(key, value)); + } + } + break; + case "designation": + ConceptDesignation conceptDesignation = new ConceptDesignation(); + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { + switch(designationComponent.getName()) { + case "language": + conceptDesignation.setLanguage(designationComponent.getValue().toString()); + break; + case "use": + org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding)designationComponent.getValue(); + if (coding != null) { + conceptDesignation.setUseSystem(coding.getSystem()); + conceptDesignation.setUseCode(coding.getCode()); + conceptDesignation.setUseDisplay(coding.getDisplay()); + } + break; + case "value": + conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); + break; + } + } + result.getDesignations().add(conceptDesignation); + break; + case "name": + result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "version": + result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "display": + result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "abstract": + result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); + break; + } + } + return result; + } + + private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, org.hl7.fhir.r4.model.Parameters outcomeR4) { + // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding + // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM). + LookupCodeResult result = new LookupCodeResult(); + result.setSearchedForCode(theCode); + result.setSearchedForSystem(theSystem); + result.setFound(true); + for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) { + switch (parameterComponent.getName()) { + case "property": + org.hl7.fhir.r4.model.Property part = parameterComponent.getChildByName("part"); + // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved + if (part != null && part.hasValues() && part.getValues().size() >= 2) { + String key = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString(); + String value = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString(); + if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) { + result.getProperties().add(new StringConceptProperty(key, value)); + } + } + break; + case "designation": + ConceptDesignation conceptDesignation = new ConceptDesignation(); + for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) { + switch(designationComponent.getName()) { + case "language": + conceptDesignation.setLanguage(designationComponent.getValue().toString()); + break; + case "use": + org.hl7.fhir.r4.model.Coding coding = (org.hl7.fhir.r4.model.Coding)designationComponent.getValue(); + if (coding != null) { + conceptDesignation.setUseSystem(coding.getSystem()); + conceptDesignation.setUseCode(coding.getCode()); + conceptDesignation.setUseDisplay(coding.getDisplay()); + } + break; + case "value": + conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString())); + break; + } + } + result.getDesignations().add(conceptDesignation); + break; + case "name": + result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "version": + result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "display": + result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString())); + break; + case "abstract": + result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString()))); + break; + } + } + return result; + } + @Override public IBaseResource fetchValueSet(String theValueSetUrl) { IGenericClient client = provideClient(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java index 68b743c3183..74b2568f20c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupportTest.java @@ -3,12 +3,14 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.parser.IJsonLikeParser; 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.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; @@ -22,12 +24,14 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -44,14 +48,18 @@ import static org.hamcrest.Matchers.lessThan; 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.assertTrue; public class RemoteTerminologyServiceValidationSupportTest { - private static final String DISPLAY = "DISPLAY"; + private static final String LANGUAGE = "en"; private static final String CODE_SYSTEM = "CODE_SYS"; + private static final String CODE_SYSTEM_VERSION = "2.1"; + private static final String CODE_SYSTEM_VERSION_AS_TEXT = "v2.1.12"; private static final String CODE = "CODE"; private static final String VALUE_SET_URL = "http://value.set/url"; private static final String ERROR_MESSAGE = "This is an error message"; + private static FhirContext ourCtx = FhirContext.forR4(); @RegisterExtension @@ -88,7 +96,50 @@ public class RemoteTerminologyServiceValidationSupportTest { } @Test - public void testValidateCode_SystemCodeDisplayUrl_Success() { + public void testLookupOperation_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, + DISPLAY, null); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, myCodeSystemProvider.myNextReturnParams.getParameter("name").toString()); + assertTrue(Boolean.parseBoolean(myCodeSystemProvider.myNextReturnParams.getParameter("result").primitiveValue())); + } + + @Test + public void testLookupOperationWithAllParams_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, + DISPLAY, null); + addAdditionalReturnParameters(); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, myCodeSystemProvider.myNextReturnParams.getParameter("name").toString()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + assertTrue(Boolean.parseBoolean(myCodeSystemProvider.myNextReturnParams.getParameter("result").primitiveValue())); + + validateExtraCodeSystemParams(); + } + + @Test + public void testLookupCode_BlankCode_ThrowsException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, + "", null); + }); + } + + @Test + public void testValidateCode_ValueSet_Success() { createNextValueSetReturnParameters(true, DISPLAY, null); IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL); @@ -104,6 +155,62 @@ public class RemoteTerminologyServiceValidationSupportTest { assertEquals(null, myValueSetProvider.myLastValueSet); } + @Test + public void testValidateCodeWithAllParams_CodeSystem_Success() { + createNextCodeSystemReturnParameters(true, DISPLAY, null); + addAdditionalReturnParameters(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, null); + assertEquals(CODE, outcome.getCode()); + assertEquals(DISPLAY, outcome.getDisplay()); + assertEquals(null, outcome.getSeverity()); + assertEquals(null, outcome.getMessage()); + + validateExtraCodeSystemParams(); + } + + private void validateExtraCodeSystemParams() { + assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("property")) { + for (Parameters.ParametersParameterComponent propertyComponent : param.getPart()) { + switch(propertyComponent.getName()) { + case "name": + assertEquals("birthDate", propertyComponent.getValue().toString()); + break; + case "value": + assertEquals("1930-01-01", propertyComponent.getValue().toString()); + break; + } + } + } else if (paramName.equals("designation")) { + for (Parameters.ParametersParameterComponent designationComponent : param.getPart()) { + switch(designationComponent.getName()) { + case "language": + assertEquals(LANGUAGE, designationComponent.getValue().toString()); + break; + case "use": + Coding coding = (Coding)designationComponent.getValue(); + assertNotNull(coding, "Coding value returned via designation use should NOT be NULL!"); + assertEquals("code", coding.getCode()); + assertEquals("system", coding.getSystem()); + assertEquals("display", coding.getDisplay()); + break; + case "value": + assertEquals("some value", designationComponent.getValue().toString()); + break; + } + } + } + } + } + @Test public void testValidateCode_SystemCodeDisplayUrl_Error() { createNextValueSetReturnParameters(false, null, ERROR_MESSAGE); @@ -132,7 +239,6 @@ public class RemoteTerminologyServiceValidationSupportTest { assertEquals(null, outcome.getMessage()); assertEquals(CODE, myCodeSystemProvider.myLastCode.getCode()); - assertEquals(DISPLAY, myCodeSystemProvider.myLastDisplay.getValue()); assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); } @@ -375,6 +481,18 @@ public class RemoteTerminologyServiceValidationSupportTest { } } + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay, String theMessage) { + myCodeSystemProvider.myNextReturnParams = new Parameters(); + myCodeSystemProvider.myNextReturnParams.addParameter("result", theResult); + myCodeSystemProvider.myNextReturnParams.addParameter("version", theVersion); + myCodeSystemProvider.myNextReturnParams.addParameter("name", theVersionAsText); + myCodeSystemProvider.myNextReturnParams.addParameter("display", theDisplay); + if (theMessage != null) { + myCodeSystemProvider.myNextReturnParams.addParameter("message", theMessage); + } + } + private void createNextValueSetReturnParameters(boolean theResult, String theDisplay, String theMessage) { myValueSetProvider.myNextReturnParams = new Parameters(); myValueSetProvider.myNextReturnParams.addParameter("result", theResult); @@ -384,6 +502,23 @@ public class RemoteTerminologyServiceValidationSupportTest { } } + private void addAdditionalReturnParameters() { + // property + Parameters.ParametersParameterComponent param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("property"); + param.addPart().setName("name").setValue(new StringType("birthDate")); + param.addPart().setName("value").setValue(new StringType("1930-01-01")); + // designation + param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("designation"); + param.addPart().setName("language").setValue(new CodeType("en")); + Parameters.ParametersParameterComponent codingParam = param.addPart().setName("use"); + Coding coding = new Coding(); + coding.setCode("code"); + coding.setSystem("system"); + coding.setDisplay("display"); + codingParam.setValue(coding); + param.addPart().setName("value").setValue(new StringType("some value")); + } + private static class MyCodeSystemProvider implements IResourceProvider { private UriParam myLastUrlParam; @@ -391,8 +526,10 @@ public class RemoteTerminologyServiceValidationSupportTest { private int myInvocationCount; private UriType myLastUrl; private CodeType myLastCode; - private StringType myLastDisplay; + private Coding myLastCoding; + private StringType myLastVersion; private Parameters myNextReturnParams; + private IValidationSupport.LookupCodeResult myNextLookupCodeResult; @Operation(name = "validate-code", idempotent = true, returnParameters = { @OperationParam(name = "result", type = BooleanType.class, min = 1), @@ -409,11 +546,34 @@ public class RemoteTerminologyServiceValidationSupportTest { myInvocationCount++; myLastUrl = theCodeSystemUrl; myLastCode = theCode; - myLastDisplay = theDisplay; return myNextReturnParams; } + @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { + @OperationParam(name="name", type=StringType.class, min=1), + @OperationParam(name="version", type=StringType.class, min=0), + @OperationParam(name="display", type=StringType.class, min=1), + @OperationParam(name="abstract", type=BooleanType.class, min=1), + }) + public Parameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name="code", min=0, max=1) CodeType theCode, + @OperationParam(name="system", min=0, max=1) UriType theSystem, + @OperationParam(name="coding", min=0, max=1) Coding theCoding, + @OperationParam(name="version", min=0, max=1) StringType theVersion, + @OperationParam(name="displayLanguage", min=0, max=1) CodeType theDisplayLanguage, + @OperationParam(name="property", min = 0, max = OperationParam.MAX_UNLIMITED) List theProperties, + RequestDetails theRequestDetails + ) { + myInvocationCount++; + myLastCode = theCode; + myLastUrl = theSystem; + myLastCoding = theCoding; + myLastVersion = theVersion; + return myNextReturnParams; + } + @Search public List find(@RequiredParam(name = "url") UriParam theUrlParam) { myLastUrlParam = theUrlParam; @@ -429,8 +589,6 @@ public class RemoteTerminologyServiceValidationSupportTest { private static class MyValueSetProvider implements IResourceProvider { - - private Parameters myNextReturnParams; private List myNextReturnValueSets; private UriType myLastUrl; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java new file mode 100644 index 00000000000..9d435406dd8 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyServiceValidationSupportDstu3Test.java @@ -0,0 +1,214 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.DateType; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class RemoteTerminologyServiceValidationSupportDstu3Test { + private static final String DISPLAY = "DISPLAY"; + private static final String LANGUAGE = "en"; + private static final String CODE_SYSTEM = "CODE_SYS"; + private static final String CODE_SYSTEM_VERSION = "2.1"; + private static final String CODE_SYSTEM_VERSION_AS_TEXT = "v2.1.12"; + private static final String CODE = "CODE"; + + private static FhirContext ourCtx = FhirContext.forDstu3(); + + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx); + + private RemoteTerminologyServiceValidationSupport mySvc; + private MyCodeSystemProvider myCodeSystemProvider; + + @BeforeEach + public void before() { + myCodeSystemProvider = new MyCodeSystemProvider(); + myRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider); + String baseUrl = "http://localhost:" + myRestfulServerExtension.getPort(); + mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); + mySvc.setBaseUrl(baseUrl); + mySvc.addClientInterceptor(new LoggingInterceptor(true)); + } + + @Test + public void testLookupOperation_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, DISPLAY); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.asStringValue()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("version")) { + assertEquals(CODE_SYSTEM_VERSION, param.getValue().toString()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("name")) { + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, param.getValue().toString()); + } + } + } + + @Test + public void testLookupOperationWithAllParams_CodeSystem_Success() { + createNextCodeSystemLookupReturnParameters(true, CODE_SYSTEM_VERSION, CODE_SYSTEM_VERSION_AS_TEXT, DISPLAY, LANGUAGE); + addAdditionalCodeSystemLookupReturnParameters(); + + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, CODE_SYSTEM, CODE, LANGUAGE); + assertNotNull(outcome, "Call to lookupCode() should return a non-NULL result!"); + assertEquals(DISPLAY, outcome.getCodeDisplay()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + + assertEquals(CODE, myCodeSystemProvider.myLastCode.asStringValue()); + assertEquals(CODE_SYSTEM, myCodeSystemProvider.myLastUrl.getValueAsString()); + for (Parameters.ParametersParameterComponent param : myCodeSystemProvider.myNextReturnParams.getParameter()) { + String paramName = param.getName(); + if (paramName.equals("result")) { + assertEquals(true, ((BooleanType)param.getValue()).booleanValue()); + } else if (paramName.equals("version")) { + assertEquals(CODE_SYSTEM_VERSION, param.getValue().toString()); + } else if (paramName.equals("display")) { + assertEquals(DISPLAY, param.getValue().toString()); + } else if (paramName.equals("name")) { + assertEquals(CODE_SYSTEM_VERSION_AS_TEXT, param.getValue().toString()); + } else if (paramName.equals("language")) { + assertEquals(LANGUAGE, param.getValue().toString()); + } else if (paramName.equals("property")) { + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent propertyComponent : param.getPart()) { + switch(propertyComponent.getName()) { + case "name": + assertEquals("birthDate", propertyComponent.getValue().toString()); + break; + case "value": + assertEquals("1930-01-01", ((DateType)propertyComponent.getValue()).asStringValue()); + break; + } + } + } else if (paramName.equals("designation")) { + for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : param.getPart()) { + switch(designationComponent.getName()) { + case "language": + assertEquals(LANGUAGE, designationComponent.getValue().toString()); + break; + case "use": + Coding coding = (Coding)designationComponent.getValue(); + assertNotNull(coding, "Coding value returned via designation use should NOT be NULL!"); + assertEquals("code", coding.getCode()); + assertEquals("system", coding.getSystem()); + assertEquals("display", coding.getDisplay()); + break; + case "value": + assertEquals("some value", designationComponent.getValue().toString()); + break; + } + } + } + } + } + + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay) { + createNextCodeSystemLookupReturnParameters(theResult, theVersion, theVersionAsText, theDisplay, null); + } + + private void createNextCodeSystemLookupReturnParameters(boolean theResult, String theVersion, String theVersionAsText, + String theDisplay, String theLanguage) { + myCodeSystemProvider.myNextReturnParams = new Parameters(); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("result").setValue(new BooleanType(theResult)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("version").setValue(new StringType(theVersion)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("name").setValue(new StringType(theVersionAsText)); + myCodeSystemProvider.myNextReturnParams.addParameter().setName("display").setValue(new StringType(theDisplay)); + if (!StringUtils.isBlank(theLanguage)) { + myCodeSystemProvider.myNextReturnParams.addParameter().setName("language").setValue(new StringType(theLanguage)); + } + } + + private void addAdditionalCodeSystemLookupReturnParameters() { + // property + Parameters.ParametersParameterComponent param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("property"); + param.addPart().setName("name").setValue(new StringType("birthDate")); + param.addPart().setName("value").setValue(new DateType("1930-01-01")); + // designation + param = myCodeSystemProvider.myNextReturnParams.addParameter().setName("designation"); + param.addPart().setName("language").setValue(new CodeType("en")); + Parameters.ParametersParameterComponent codingParam = param.addPart().setName("use"); + Coding coding = new Coding(); + coding.setCode("code"); + coding.setSystem("system"); + coding.setDisplay("display"); + codingParam.setValue(coding); + param.addPart().setName("value").setValue(new StringType("some value")); + } + + private static class MyCodeSystemProvider implements IResourceProvider { + private int myInvocationCount; + private UriType myLastUrl; + private CodeType myLastCode; + private Coding myLastCoding; + private StringType myLastVersion; + private CodeType myLastDisplayLanguage; + private Parameters myNextReturnParams; + + @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { + @OperationParam(name="name", type=StringType.class, min=1), + @OperationParam(name="version", type=StringType.class, min=0), + @OperationParam(name="display", type=StringType.class, min=1), + @OperationParam(name="abstract", type=BooleanType.class, min=1), + }) + public Parameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name="code", min=0, max=1) CodeType theCode, + @OperationParam(name="system", min=0, max=1) UriType theSystem, + @OperationParam(name="coding", min=0, max=1) Coding theCoding, + @OperationParam(name="version", min=0, max=1) StringType theVersion, + @OperationParam(name="displayLanguage", min=0, max=1) CodeType theDisplayLanguage, + @OperationParam(name="property", min = 0, max = OperationParam.MAX_UNLIMITED) List theProperties, + RequestDetails theRequestDetails + ) { + myInvocationCount++; + myLastCode = theCode; + myLastUrl = theSystem; + myLastCoding = theCoding; + myLastVersion = theVersion; + myLastDisplayLanguage = theDisplayLanguage; + return myNextReturnParams; + } + + @Override + public Class getResourceType() { + return CodeSystem.class; + } + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java new file mode 100644 index 00000000000..78362aaabb1 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/RemoteTerminologyServiceValidationSupportR5Test.java @@ -0,0 +1,35 @@ +package org.hl7.fhir.r5.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RemoteTerminologyServiceValidationSupportR5Test { + private static final String ANY_NONBLANK_VALUE = "anything"; + private static FhirContext ourCtx = FhirContext.forR5(); + @RegisterExtension + public RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx); + + private RemoteTerminologyServiceValidationSupport mySvc; + + @BeforeEach + public void before() { + String baseUrl = "http://localhost:" + myRestfulServerExtension.getPort(); + mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); + mySvc.setBaseUrl(baseUrl); + } + + @Test + public void testLookupCode_R5_ThrowsException() { + Assertions.assertThrows(UnsupportedOperationException.class, () -> { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode( + new ValidationSupportContext(FhirContext.forR5().getValidationSupport()), ANY_NONBLANK_VALUE, ANY_NONBLANK_VALUE); + }); + } +} From 35dedb86289a30d8d8fe7404c9248a1e9f159446 Mon Sep 17 00:00:00 2001 From: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Date: Fri, 19 Nov 2021 11:54:24 -0500 Subject: [PATCH 8/8] 3176 - CQL doc fixes. (#3177) --- .../resources/ca/uhn/hapi/fhir/docs/files.properties | 1 + ....drawio.svg => ref_measure_architecture_drawio.svg} | 0 .../ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md | 2 +- .../uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md | 10 +++++----- 4 files changed, 7 insertions(+), 6 deletions(-) rename hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/{ref.measure.architecture.drawio.svg => ref_measure_architecture_drawio.svg} (100%) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 27d50aa20ab..fcaf49fd2d0 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -73,6 +73,7 @@ page.server_jpa_mdm.mdm_expansion=MDM Search Expansion section.server_jpa_cql.title=JPA Server: CQL page.server_jpa_cql.cql=CQL Getting Started +page.server_jpa_cql.cql_measure=CQL Measure section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.architecture.drawio.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref_measure_architecture_drawio.svg similarity index 100% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref.measure.architecture.drawio.svg rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/ref_measure_architecture_drawio.svg diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md index a069e590584..e6429927237 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql.md @@ -29,7 +29,7 @@ There are two Spring beans available that add CQL processing to HAPI. You can en HAPI provides implementations for some operations in DSTU3 and R4: -[Measure Operations](cql_measure) +[CQL Measure](cql_measure.html) ## Roadmap diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md index 208b6b3f41a..33b66e17bd5 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_cql/cql_measure.md @@ -1,4 +1,4 @@ -# Measures +# CQL Measure ## Introduction @@ -234,7 +234,7 @@ The HAPI implementation uses the populations defined by the CQF Measures IG for #### Population Criteria -The logical criteria used for determining each Measure population is defined by the [Measure.group.population.criteria element](). The Measure specification allows population criteria to be defined using FHIR Path, CQL, or other languages as appropriate. The HAPI implementation currently only supports using CQL. The relationship between a Measure Population and CQL is illustrated in the [Population Criteria](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#population-criteria) section of the CQF Measures IG. +The logical criteria used for determining each Measure population is defined by the [Measure.group.population.criteria](https://hl7.org/fhir/R4/measure-definitions.html#Measure.group.population.criteria) element. The Measure specification allows population criteria to be defined using FHIR Path, CQL, or other languages as appropriate. The HAPI implementation currently only supports using CQL. The relationship between a Measure Population and CQL is illustrated in the [Population Criteria](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#population-criteria) section of the CQF Measures IG. An example Measure resource with a population criteria referencing a CQL identifier looks like: @@ -376,13 +376,13 @@ Below are a few diagrams that show the overall architecture of Measure evaluatio This is a simplified component diagram of the Measure evaluation architecture -![Measure Evaluation Architecture](images/../../images/ref.measure.architecture.drawio.svg) +![Measure Evaluation Architecture](/hapi-fhir/docs/images/ref_measure_architecture_drawio.svg) ### Sequence Chart This sequence chart approximates the Measure evaluation logic implemented by HAPI. -![Measure Evaluation Sequence Chart](images/../../images/measure_evaluation_sequence.png) +![Measure Evaluation Sequence Chart](/hapi-fhir/docs/images/measure_evaluation_sequence.png) ## FAQs @@ -402,5 +402,5 @@ A: Yes, though the Measure and associated Resources must be in the same partitio * Support for stratifiers * Support for Group subjects * Support for FHIRPath expressions in Stratifiers, Supplemental Data Elements, and Population Criteria -* $data-requirements, $collect-data, $submit-data, and $care-gaps operations +* `$data-requirements`, `$collect-data`, `$submit-data`, and `$care-gaps` operations * Support for more extensions defined in the CQF Measures, CPG, and ATR IGs