diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 49739e62b1e..a1efd061a7c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -183,6 +183,24 @@ public interface IValidationSupport { return null; } + /** + * Validates that the given code exists and if possible returns a display + * name. This method is called to check codes which are found in "example" + * binding fields (e.g. Observation.code in the default profile. + * + * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theOptions Provides options controlling the validation + * @param theCodeSystem The code system, e.g. "http://loinc.org" + * @param theCode The code, e.g. "1234-5" + * @param theDisplay The display name, if it should also be validated + * @param theSystemVersion The code system version. + * @return Returns a validation result object + */ + default CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, String theSystemVersion) { + return null; + } + /** * Validates that the given code exists and if possible returns a display * name. This method is called to check codes which are found in "example" @@ -212,6 +230,19 @@ public interface IValidationSupport { return null; } + /** + * Look up a code using the system, system version and code value + * + * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theSystem The CodeSystem URL + * @param theCode The code + * @param theSystemVersion The CodeSystem version + */ + default LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theSystemVersion) { + return null; + } + /** * Returns true if the given valueset can be validated by the given * validation support module diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index 98f49f6ff2d..85b6bb6b8ca 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -227,6 +227,11 @@ public class DaoConfig { */ private boolean myPreloadBlobFromInputStream = false; + /** + * @since 5.1.0 + */ + private boolean myMultipleCodeSystemVersionsEnabled = false; + /** * Constructor */ @@ -2126,4 +2131,34 @@ public class DaoConfig { myPreloadBlobFromInputStream = thePreloadBlobFromInputStream; } + /** + *

+ * This determines whether multiple code system versions will be enabled. If not enabled, existing code systems will be + * deleted when a new version is uploaded. + *

+ *

+ * The default value for this setting is {@code false}. + *

+ * + * @since 5.1.0 + */ + public boolean isMultipleCodeSystemVersionsEnabled() { + return myMultipleCodeSystemVersionsEnabled; + } + + /** + *

+ * This determines whether multiple code system versions will be enabled. If not enabled, existing code systems will be + * deleted when a new version is uploaded. + *

+ *

+ * The default value for this setting is {@code false}. + *

+ * + * @since 5.1.0 + */ + public void setMultipleCodeSystemVersionsEnabled(Boolean theMultipleCodeSystemVersionsEnabled) { + myMultipleCodeSystemVersionsEnabled = theMultipleCodeSystemVersionsEnabled; + } + } diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoCodeSystem.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoCodeSystem.java index ba957e00542..2c00c792b49 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoCodeSystem.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoCodeSystem.java @@ -42,6 +42,11 @@ public interface IFhirResourceDaoCodeSystem ext SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, CD theCodingA, CD theCodingB, RequestDetails theRequestDetails); + @Nonnull + IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType theCode, IPrimitiveType theSystem, CD theCoding, IPrimitiveType theVersion, RequestDetails theRequestDetails); + + SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, CD theCodingA, CD theCodingB, IPrimitiveType theVersion, RequestDetails theRequestDetails); + class SubsumesResult { private final ConceptSubsumptionOutcome myOutcome; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java index ec2b7bd27e6..c43d3e3d9c2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -48,6 +48,7 @@ import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -235,6 +236,12 @@ public class FhirResourceDaoValueSetDstu2 extends BaseHapiFhirResourceDao theCode, IPrimitiveType theSystem, CodingDt theCoding, IPrimitiveType theVersion, RequestDetails theRequestDetails) { + throw new UnsupportedOperationException(); + } + @Override public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType theCode, IPrimitiveType theSystem, CodingDt theCoding, RequestDetails theRequest) { boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode()); @@ -280,6 +287,11 @@ public class FhirResourceDaoValueSetDstu2 extends BaseHapiFhirResourceDao theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, CodingDt theCodingA, CodingDt theCodingB, IPrimitiveType theVersion, RequestDetails theRequestDetails) { + throw new UnsupportedOperationException(); + } + @Override @PostConstruct public void postConstruct() { 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 92af07eaef9..fc55636836f 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,9 +35,19 @@ public interface ITermCodeSystemVersionDao extends JpaRepository findByCodeSystemPid(@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); + + @Query("SELECT cs FROM TermCodeSystemVersion cs WHERE cs.myCodeSystemPid = :codesystem_pid AND cs.myCodeSystemVersionId IS NULL") + TermCodeSystemVersion findByCodeSystemPidVersionIsNull(@Param("codesystem_pid") Long theCodeSystemPid); + @Query("SELECT cs FROM TermCodeSystemVersion cs WHERE cs.myResourcePid = :resource_id") List findByCodeSystemResourcePid(@Param("resource_id") Long theCodeSystemResourcePid); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java index 0fbd43ac94f..6d2cd77aa99 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java @@ -44,6 +44,7 @@ import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; @@ -86,9 +87,15 @@ public class FhirResourceDaoCodeSystemDstu3 extends BaseHapiFhirResourceDao theCode, IPrimitiveType theSystem, Coding theCoding, RequestDetails theRequestDetails) { + return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails); + } + + @Nonnull + @Override + public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType theCode, IPrimitiveType theSystem, Coding theCoding, IPrimitiveType theVersion, RequestDetails theRequestDetails) { boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode()); boolean haveCode = theCode != null && theCode.isEmpty() == false; boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; @@ -109,12 +116,16 @@ public class FhirResourceDaoCodeSystemDstu3 extends BaseHapiFhirResourceDao theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, IPrimitiveType theVersion, RequestDetails theRequestDetails) { + return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB, theVersion); + } + @Override public SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, RequestDetails theRequestDetails) { return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java index f2c7f221e30..a7f91e3c1b0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java @@ -44,6 +44,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; @@ -81,9 +82,15 @@ public class FhirResourceDaoCodeSystemR4 extends BaseHapiFhirResourceDao theCode, IPrimitiveType theSystem, Coding theCoding, RequestDetails theRequestDetails) { + return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails); + } + + @Nonnull + @Override + public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType theCode, IPrimitiveType theSystem, Coding theCoding, IPrimitiveType theVersion, RequestDetails theRequestDetails) { boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode()); boolean haveCode = theCode != null && theCode.isEmpty() == false; boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; @@ -104,13 +111,17 @@ public class FhirResourceDaoCodeSystemR4 extends BaseHapiFhirResourceDao theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, IPrimitiveType theVersion, RequestDetails theRequestDetails) { + return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB, theVersion); + } + @Override public SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, RequestDetails theRequestDetails) { return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoValueSetR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoValueSetR4.java index c2170f8ac7a..7a104f1ba97 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoValueSetR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoValueSetR4.java @@ -113,14 +113,17 @@ public class FhirResourceDaoValueSetR4 extends BaseHapiFhirResourceDao ValueSet source = new ValueSet(); source.setUrl(theUri); - source.getCompose().addInclude().addValueSet(theUri); +// source.getCompose().addInclude().addValueSet(theUri); if (isNotBlank(theFilter)) { - ConceptSetComponent include = source.getCompose().addInclude(); - ConceptSetFilterComponent filter = include.addFilter(); +// ConceptSetComponent include = source.getCompose().addInclude(); +// ConceptSetFilterComponent filter = include.addFilter(); + ConceptSetFilterComponent filter = source.getCompose().addInclude().addValueSet(theUri).addFilter(); filter.setProperty("display"); filter.setOp(FilterOperator.EQUAL); filter.setValue(theFilter); + } else { + source.getCompose().addInclude().addValueSet(theUri); } ValueSet retVal = doExpand(source); @@ -148,14 +151,17 @@ public class FhirResourceDaoValueSetR4 extends BaseHapiFhirResourceDao ValueSet source = new ValueSet(); source.setUrl(theUri); - source.getCompose().addInclude().addValueSet(theUri); +// source.getCompose().addInclude().addValueSet(theUri); if (isNotBlank(theFilter)) { - ConceptSetComponent include = source.getCompose().addInclude(); - ConceptSetFilterComponent filter = include.addFilter(); +// ConceptSetComponent include = source.getCompose().addInclude(); +// ConceptSetFilterComponent filter = include.addFilter(); + ConceptSetFilterComponent filter = source.getCompose().addInclude().addValueSet(theUri).addFilter(); filter.setProperty("display"); filter.setOp(FilterOperator.EQUAL); filter.setValue(theFilter); + } else { + source.getCompose().addInclude().addValueSet(theUri); } ValueSet retVal = doExpand(source, theOffset, theCount); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoCodeSystemR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoCodeSystemR5.java index 0cc481ae32c..329cf00eb40 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoCodeSystemR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoCodeSystemR5.java @@ -45,6 +45,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; @@ -83,9 +84,15 @@ public class FhirResourceDaoCodeSystemR5 extends BaseHapiFhirResourceDao theCode, IPrimitiveType theSystem, Coding theCoding, RequestDetails theRequestDetails) { + return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails); + } + + @Nonnull + @Override + public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType theCode, IPrimitiveType theSystem, Coding theCoding, IPrimitiveType theVersion, RequestDetails theRequestDetails) { boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode()); boolean haveCode = theCode != null && theCode.isEmpty() == false; boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; @@ -106,13 +113,17 @@ public class FhirResourceDaoCodeSystemR5 extends BaseHapiFhirResourceDao theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, IPrimitiveType theVersion, RequestDetails theRequestDetails) { + return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB, theVersion); + } + @Override public SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, Coding theCodingA, Coding theCodingB, RequestDetails theRequestDetails) { return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java index 5b16f5811e9..04444c47f3f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java @@ -40,17 +40,21 @@ import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.SequenceGenerator; import javax.persistence.Table; +import javax.persistence.UniqueConstraint; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import static org.apache.commons.lang3.StringUtils.length; -@Table(name = "TRM_CODESYSTEM_VER" +@Table(name = "TRM_CODESYSTEM_VER", // Note, we used to have a constraint named IDX_CSV_RESOURCEPID_AND_VER (don't reuse this) -) + uniqueConstraints = { + @UniqueConstraint(name = TermCodeSystemVersion.IDX_CODESYSTEM_AND_VER, columnNames = {"CODESYSTEM_PID", "CS_VERSION_ID"}) +}) @Entity() public class TermCodeSystemVersion implements Serializable { + public static final String IDX_CODESYSTEM_AND_VER = "IDX_CODESYSTEM_AND_VER"; public static final int MAX_VERSION_LENGTH = 200; private static final long serialVersionUID = 1L; @OneToMany(fetch = FetchType.LAZY, mappedBy = "myCodeSystem") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java index 2fcee77131e..a6d061c3e06 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderCodeSystemDstu3.java @@ -53,6 +53,7 @@ public class BaseJpaResourceProviderCodeSystemDstu3 extends JpaResourceProviderD @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 = "property", min = 0, max = OperationParam.MAX_UNLIMITED) List theProperties, RequestDetails theRequestDetails ) { @@ -60,7 +61,7 @@ public class BaseJpaResourceProviderCodeSystemDstu3 extends JpaResourceProviderD startRequest(theServletRequest); try { IFhirResourceDaoCodeSystem dao = (IFhirResourceDaoCodeSystem) getDao(); - IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theRequestDetails); + IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theVersion, theRequestDetails); result.throwNotFoundIfAppropriate(); return (Parameters) result.toParameters(theRequestDetails.getFhirContext(), theProperties); } catch (FHIRException e) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java index 2c69bdbf54c..dec2d1adbcc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderCodeSystemR4.java @@ -55,6 +55,7 @@ public class BaseJpaResourceProviderCodeSystemR4 extends JpaResourceProviderR4 theProperties, RequestDetails theRequestDetails ) { @@ -62,7 +63,7 @@ public class BaseJpaResourceProviderCodeSystemR4 extends JpaResourceProviderR4 dao = (IFhirResourceDaoCodeSystem) getDao(); - IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theRequestDetails); + IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theVersion, theRequestDetails); result.throwNotFoundIfAppropriate(); return (Parameters) result.toParameters(theRequestDetails.getFhirContext(), theProperties); } finally { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderCodeSystemR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderCodeSystemR5.java index 38545e7d5d7..45d80b0228f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderCodeSystemR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderCodeSystemR5.java @@ -55,6 +55,7 @@ public class BaseJpaResourceProviderCodeSystemR5 extends JpaResourceProviderR5 theProperties, RequestDetails theRequestDetails ) { @@ -62,7 +63,7 @@ public class BaseJpaResourceProviderCodeSystemR5 extends JpaResourceProviderR5 dao = (IFhirResourceDaoCodeSystem) getDao(); - IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theRequestDetails); + IValidationSupport.LookupCodeResult result = dao.lookupCode(theCode, theSystem, theCoding, theVersion, theRequestDetails); result.throwNotFoundIfAppropriate(); return (Parameters) result.toParameters(theRequestDetails.getFhirContext(), theProperties); } finally { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index b03dc4aec20..69e1113fb09 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -244,7 +244,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - TermCodeSystemVersion cs = getCurrentCodeSystemVersion(theSystem); + TermCodeSystemVersion cs = getCurrentCodeSystemVersionForVersion(theSystem,null); return cs != null; } @@ -616,6 +616,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { private Boolean expandValueSetHandleIncludeOrExclude(@Nullable ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, VersionIndependentConcept theWantConceptOrNull) { String system = theIncludeOrExclude.getSystem(); + String systemVersion = theIncludeOrExclude.getVersion(); boolean hasSystem = isNotBlank(system); boolean hasValueSet = theIncludeOrExclude.getValueSet().size() > 0; @@ -630,7 +631,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system); if (cs != null) { - return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theCodeCounter, theQueryIndex, theWantConceptOrNull, system, cs); + return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theCodeCounter, theQueryIndex, theWantConceptOrNull, system, cs, systemVersion); } else { @@ -671,7 +672,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { nextSystem = system; } - LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(provideValidationSupport()), nextSystem, next.getCode()); + LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(provideValidationSupport()), nextSystem, next.getCode(), systemVersion); if (lookup != null && lookup.isFound()) { addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, nextSystem, next.getCode(), lookup.getCodeDisplay()); foundCount++; @@ -765,7 +766,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } @Nonnull - private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, VersionIndependentConcept theWantConceptOrNull, String theSystem, TermCodeSystem theCs) { + private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, AtomicInteger theCodeCounter, int theQueryIndex, VersionIndependentConcept theWantConceptOrNull, String theSystem, TermCodeSystem theCs, String theSystemVersion) { TermCodeSystemVersion csv = theCs.getCurrentVersion(); FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); @@ -793,7 +794,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { /* * Filters */ - handleFilters(bool, theSystem, qb, theIncludeOrExclude); + handleFilters(bool, theSystem, qb, theIncludeOrExclude, theSystemVersion); Query luceneQuery = bool.createQuery(); @@ -902,15 +903,15 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } - private void handleFilters(BooleanJunction theBool, String theSystem, QueryBuilder theQb, ValueSet.ConceptSetComponent theIncludeOrExclude) { + private void handleFilters(BooleanJunction theBool, String theSystem, QueryBuilder theQb, ValueSet.ConceptSetComponent theIncludeOrExclude, String theSystemVersion) { if (theIncludeOrExclude.getFilter().size() > 0) { for (ValueSet.ConceptSetFilterComponent nextFilter : theIncludeOrExclude.getFilter()) { - handleFilter(theSystem, theQb, theBool, nextFilter); + handleFilter(theSystem, theQb, theBool, nextFilter, theSystemVersion); } } } - private void handleFilter(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { + private void handleFilter(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { if (isBlank(theFilter.getValue()) && theFilter.getOp() == null && isBlank(theFilter.getProperty())) { return; } @@ -926,7 +927,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { break; case "concept": case "code": - handleFilterConceptAndCode(theSystem, theQb, theBool, theFilter); + handleFilterConceptAndCode(theSystem, theQb, theBool, theFilter, theSystemVersion); break; case "parent": case "child": @@ -935,11 +936,11 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { break; case "ancestor": isCodeSystemLoingOrThrowInvalidRequestException(theSystem, theFilter.getProperty()); - handleFilterLoincAncestor(theSystem, theQb, theBool, theFilter); + handleFilterLoincAncestor(theSystem, theQb, theBool, theFilter, theSystemVersion); break; case "descendant": isCodeSystemLoingOrThrowInvalidRequestException(theSystem, theFilter.getProperty()); - handleFilterLoincDescendant(theSystem, theQb, theBool, theFilter); + handleFilterLoincDescendant(theSystem, theQb, theBool, theFilter, theSystemVersion); break; case "copyright": isCodeSystemLoingOrThrowInvalidRequestException(theSystem, theFilter.getProperty()); @@ -990,8 +991,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { bool.must(textQuery); } - private void handleFilterConceptAndCode(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { - TermConcept code = findCode(theSystem, theFilter.getValue()) + private void handleFilterConceptAndCode(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { + TermConcept code = findCode(theSystem, theFilter.getValue(), theSystemVersion) .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {" + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theFilter.getValue())); if (theFilter.getOp() == ValueSet.FilterOperator.ISA) { @@ -1036,41 +1037,41 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } @SuppressWarnings("EnumSwitchStatementWhichMissesCases") - private void handleFilterLoincAncestor(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { + private void handleFilterLoincAncestor(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { switch (theFilter.getOp()) { case EQUAL: - addLoincFilterAncestorEqual(theSystem, theQb, theBool, theFilter); + addLoincFilterAncestorEqual(theSystem, theQb, theBool, theFilter, theSystemVersion); break; case IN: - addLoincFilterAncestorIn(theSystem, theQb, theBool, theFilter); + addLoincFilterAncestorIn(theSystem, theQb, theBool, theFilter, theSystemVersion); break; default: throw new InvalidRequestException("Don't know how to handle op=" + theFilter.getOp() + " on property " + theFilter.getProperty()); } } - private void addLoincFilterAncestorEqual(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { - addLoincFilterAncestorEqual(theSystem, theQb, theBool, theFilter.getProperty(), theFilter.getValue()); + private void addLoincFilterAncestorEqual(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { + addLoincFilterAncestorEqual(theSystem, theQb, theBool, theFilter.getProperty(), theFilter.getValue(), theSystemVersion); } - private void addLoincFilterAncestorEqual(String theSystem, QueryBuilder theQb, BooleanJunction theBool, String theProperty, String theValue) { - List terms = getAncestorTerms(theSystem, theProperty, theValue); + private void addLoincFilterAncestorEqual(String theSystem, QueryBuilder theQb, BooleanJunction theBool, String theProperty, String theValue, String theSystemVersion) { + List terms = getAncestorTerms(theSystem, theProperty, theValue, theSystemVersion); theBool.must(new TermsQuery(terms)); } - private void addLoincFilterAncestorIn(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { + private void addLoincFilterAncestorIn(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { String[] values = theFilter.getValue().split(","); List terms = new ArrayList<>(); for (String value : values) { - terms.addAll(getAncestorTerms(theSystem, theFilter.getProperty(), value)); + terms.addAll(getAncestorTerms(theSystem, theFilter.getProperty(), value, theSystemVersion)); } theBool.must(new TermsQuery(terms)); } - private List getAncestorTerms(String theSystem, String theProperty, String theValue) { + private List getAncestorTerms(String theSystem, String theProperty, String theValue, String theSystemVersion) { List retVal = new ArrayList<>(); - TermConcept code = findCode(theSystem, theValue) + TermConcept code = findCode(theSystem, theValue, theSystemVersion) .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {" + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue)); retVal.add(new Term("myParentPids", "" + code.getId())); @@ -1080,41 +1081,41 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } @SuppressWarnings("EnumSwitchStatementWhichMissesCases") - private void handleFilterLoincDescendant(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { + private void handleFilterLoincDescendant(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { switch (theFilter.getOp()) { case EQUAL: - addLoincFilterDescendantEqual(theSystem, theBool, theFilter); + addLoincFilterDescendantEqual(theSystem, theBool, theFilter, theSystemVersion); break; case IN: - addLoincFilterDescendantIn(theSystem, theQb, theBool, theFilter); + addLoincFilterDescendantIn(theSystem, theQb, theBool, theFilter, theSystemVersion); break; default: throw new InvalidRequestException("Don't know how to handle op=" + theFilter.getOp() + " on property " + theFilter.getProperty()); } } - private void addLoincFilterDescendantEqual(String theSystem, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { - addLoincFilterDescendantEqual(theSystem, theBool, theFilter.getProperty(), theFilter.getValue()); + private void addLoincFilterDescendantEqual(String theSystem, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { + addLoincFilterDescendantEqual(theSystem, theBool, theFilter.getProperty(), theFilter.getValue(), theSystemVersion); } - private void addLoincFilterDescendantEqual(String theSystem, BooleanJunction theBool, String theProperty, String theValue) { - List terms = getDescendantTerms(theSystem, theProperty, theValue); + private void addLoincFilterDescendantEqual(String theSystem, BooleanJunction theBool, String theProperty, String theValue, String theSystemVersion) { + List terms = getDescendantTerms(theSystem, theProperty, theValue, theSystemVersion); theBool.must(new TermsQuery(terms)); } - private void addLoincFilterDescendantIn(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter) { + private void addLoincFilterDescendantIn(String theSystem, QueryBuilder theQb, BooleanJunction theBool, ValueSet.ConceptSetFilterComponent theFilter, String theSystemVersion) { String[] values = theFilter.getValue().split(","); List terms = new ArrayList<>(); for (String value : values) { - terms.addAll(getDescendantTerms(theSystem, theFilter.getProperty(), value)); + terms.addAll(getDescendantTerms(theSystem, theFilter.getProperty(), value, theSystemVersion)); } theBool.must(new TermsQuery(terms)); } - private List getDescendantTerms(String theSystem, String theProperty, String theValue) { + private List getDescendantTerms(String theSystem, String theProperty, String theValue, String theSystemVersion) { List retVal = new ArrayList<>(); - TermConcept code = findCode(theSystem, theValue) + TermConcept code = findCode(theSystem, theValue, theSystemVersion) .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {" + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue)); String[] parentPids = code.getParentPidsAsString().split(" "); @@ -1363,6 +1364,12 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { @Override public Optional findCode(String theCodeSystem, String theCode) { + return findCode(theCodeSystem, theCode, null); + + } + + @Override + public Optional findCode(String theCodeSystem, String theCode, String theVersion) { /* * Loading concepts without a transaction causes issues later on some * platforms (e.g. PSQL) so this transactiontemplate is here to make @@ -1371,7 +1378,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); return txTemplate.execute(t -> { - TermCodeSystemVersion csv = getCurrentCodeSystemVersion(theCodeSystem); + TermCodeSystemVersion csv = getCurrentCodeSystemVersionForVersion(theCodeSystem, theVersion); if (csv == null) { return null; } @@ -1380,12 +1387,20 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } @Nullable - private TermCodeSystemVersion getCurrentCodeSystemVersion(String theUri) { - TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(theUri, uri -> myTxTemplate.execute(tx -> { + private TermCodeSystemVersion getCurrentCodeSystemVersionForVersion(String theUri, String theVersion) { + StringBuffer key = new StringBuffer(theUri); + if (theVersion != null) { + key.append("_").append(theVersion); + } + TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(key.toString(), uri -> myTxTemplate.execute(tx -> { TermCodeSystemVersion csv = null; TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(uri); - if (cs != null && cs.getCurrentVersion() != null) { - csv = cs.getCurrentVersion(); + if (cs != null) { + if (theVersion != null) { + csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), theVersion); + } else if (cs.getCurrentVersion() != null) { + csv = cs.getCurrentVersion(); + } } if (csv != null) { return csv; @@ -1804,19 +1819,28 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } @Override - @Transactional public IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, IBaseCoding theCodingA, IBaseCoding theCodingB) { + return subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB, null); + } + + @Override + @Transactional + public IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, IBaseCoding theCodingA, IBaseCoding theCodingB, IPrimitiveType theSystemVersion) { VersionIndependentConcept conceptA = toConcept(theCodeA, theSystem, theCodingA); VersionIndependentConcept conceptB = toConcept(theCodeB, theSystem, theCodingB); + String systemVersion = null; + if (theSystemVersion != null) { + systemVersion = theSystemVersion.getValue(); + } if (!StringUtils.equals(conceptA.getSystem(), conceptB.getSystem())) { throw new InvalidRequestException("Unable to test subsumption across different code systems"); } - TermConcept codeA = findCode(conceptA.getSystem(), conceptA.getCode()) + TermConcept codeA = findCode(conceptA.getSystem(), conceptA.getCode(), systemVersion) .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptA)); - TermConcept codeB = findCode(conceptB.getSystem(), conceptB.getCode()) + TermConcept codeB = findCode(conceptB.getSystem(), conceptB.getCode(), systemVersion) .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptB)); FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); @@ -1835,10 +1859,10 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { protected abstract ValueSet toCanonicalValueSet(IBaseResource theValueSet); - protected IValidationSupport.LookupCodeResult lookupCode(String theSystem, String theCode) { + protected IValidationSupport.LookupCodeResult lookupCode(String theSystem, String theCode, String theVersion) { TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); return txTemplate.execute(t -> { - Optional codeOpt = findCode(theSystem, theCode); + Optional codeOpt = findCode(theSystem, theCode, theVersion); if (codeOpt.isPresent()) { TermConcept code = codeOpt.get(); @@ -2107,10 +2131,14 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return null; } - @CoverageIgnore @Override public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { + return validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl, null); + } + + @Override + public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, String theSystemVersion) { invokeRunnableForUnitTest(); if (isNotBlank(theValueSetUrl)) { @@ -2119,7 +2147,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - Optional codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); + Optional codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode, theSystemVersion).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); if (codeOpt != null && codeOpt.isPresent()) { VersionIndependentConcept code = codeOpt.get(); 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 6775fd6f2f0..13683722d6b 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 @@ -284,16 +284,19 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { ResourcePersistentId codeSystemResourcePid = getCodeSystemResourcePid(theCodeSystem.getIdElement()); /* - * If this is a not-present codesystem, we don't want to store a new version if one - * already exists, since that will wipe out the existing concepts. We do create or update - * the TermCodeSystem table though, since that allows the DB to reject changes - * that would result in duplicate CodeSysten.url values. + * If this is a not-present codesystem and codesystem version already exists, we don't want to + * overwrite the existing version since that will wipe out the existing concepts. We do create + * or update the TermCodeSystem table though, since that allows the DB to reject changes that would + * result in duplicate CodeSystem.url values. */ if (theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { - TermCodeSystem codeSystem = myCodeSystemDao.findByCodeSystemUri(theCodeSystem.getUrl()); - if (codeSystem != null) { - getOrCreateTermCodeSystem(codeSystemResourcePid, theCodeSystem.getUrl(), theCodeSystem.getUrl(), theResourceEntity); - return; + TermCodeSystem termCodeSystem = myCodeSystemDao.findByCodeSystemUri(theCodeSystem.getUrl()); + if (termCodeSystem != null) { + TermCodeSystemVersion codeSystemVersion = getExistingTermCodeSystemVersion(termCodeSystem.getPid(), theCodeSystem.getVersion()); + if (codeSystemVersion != null) { + getOrCreateTermCodeSystem(codeSystemResourcePid, theCodeSystem.getUrl(), theCodeSystem.getUrl(), theResourceEntity); + return; + } } } @@ -338,22 +341,23 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { ValidateUtil.isTrueOrThrowInvalidRequest(theCodeSystemVersion.getResource() != null, "No resource supplied"); ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystemUri, "No system URI supplied"); - // Grab the existing versions so we can delete them later - List existing = myCodeSystemVersionDao.findByCodeSystemResourcePid(theCodeSystemResourcePid.getIdAsLong()); - - /* - * For now we always delete old versions. At some point it would be nice to allow configuration to keep old versions. - */ - - for (TermCodeSystemVersion next : existing) { - ourLog.info("Deleting old code system version {}", next.getPid()); - Long codeSystemVersionPid = next.getPid(); - deleteCodeSystemVersion(codeSystemVersionPid); + // Grab the existing version so we can delete it + TermCodeSystem existingCodeSystem = myCodeSystemDao.findByCodeSystemUri(theSystemUri); + TermCodeSystemVersion existing = null; + if (existingCodeSystem != null) { + existing = getExistingTermCodeSystemVersion(existingCodeSystem.getPid(), theSystemVersionId); } - ourLog.debug("Flushing..."); - myConceptDao.flush(); - ourLog.debug("Done flushing"); + /* + * Delete version being replaced. + */ + + if(existing != null) { + ourLog.info("Deleting old code system version {}", existing.getPid()); + Long codeSystemVersionPid = existing.getPid(); + deleteCodeSystemVersion(codeSystemVersionPid); + + } /* * Do the upload @@ -408,6 +412,17 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { } } + private TermCodeSystemVersion getExistingTermCodeSystemVersion(Long theCodeSystemVersionPid, String theCodeSystemVersion) { + TermCodeSystemVersion existing; + if (theCodeSystemVersion == null) { + existing = myCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(theCodeSystemVersionPid); + } else { + existing = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(theCodeSystemVersionPid, theCodeSystemVersion); + } + + return existing; + } + private void deleteCodeSystemVersion(final Long theCodeSystemVersionPid) { ourLog.info(" * Deleting code system version {}", theCodeSystemVersionPid); @@ -457,8 +472,11 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { myCodeSystemDao.save(codeSystem); } + myConceptDao.flush(); + ourLog.info(" * Deleting code system version"); - myCodeSystemVersionDao.deleteById(theCodeSystemVersionPid); + myCodeSystemVersionDao.delete(theCodeSystemVersionPid); + myCodeSystemVersionDao.flush(); }); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java index 11401f845a1..3fe945458a5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermLoaderSvcImpl.java @@ -378,6 +378,10 @@ public class TermLoaderSvcImpl implements ITermLoaderSvc { try { String loincCsString = IOUtils.toString(BaseTermReadSvcImpl.class.getResourceAsStream("/ca/uhn/fhir/jpa/term/loinc/loinc.xml"), Charsets.UTF_8); loincCs = FhirContext.forR4().newXmlParser().parseResource(CodeSystem.class, loincCsString); + String codeSystemVersionId = theUploadProperties.getProperty(LOINC_CODESYSTEM_VERSION.getCode()); + if (codeSystemVersionId != null) { + loincCs.setVersion(codeSystemVersionId); + } } catch (IOException e) { throw new InternalErrorException("Failed to load loinc.xml", e); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java index 6b266ae52f3..0056824d586 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java @@ -149,7 +149,12 @@ public class TermReadSvcDstu3 extends BaseTermReadSvcImpl implements IValidation @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { - return super.lookupCode(theSystem, theCode); + return super.lookupCode(theSystem, theCode, null); + } + + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + return super.lookupCode(theSystem, theCode, theVersion); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java index 2075725dc2f..96243af965a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java @@ -89,9 +89,14 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4 return (CodeSystem) theCodeSystem; } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + return super.lookupCode(theSystem, theCode, theVersion); + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { - return super.lookupCode(theSystem, theCode); + return super.lookupCode(theSystem, theCode, null); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java index bfac6ae33d9..9e6e340e5bd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java @@ -73,6 +73,8 @@ public interface ITermReadSvc extends IValidationSupport { List expandValueSet(ValueSetExpansionOptions theExpansionOptions, String theValueSet); + Optional findCode(String theCodeSystem, String theCode, String theVersion); + Optional findCode(String theCodeSystem, String theCode); Set findCodesAbove(Long theCodeSystemResourcePid, Long theCodeSystemResourceVersionPid, String theCode); @@ -103,6 +105,8 @@ public interface ITermReadSvc extends IValidationSupport { IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, IBaseCoding theCodingA, IBaseCoding theCodingB); + IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType theCodeA, IPrimitiveType theCodeB, IPrimitiveType theSystem, IBaseCoding theCodingA, IBaseCoding theCodingB, IPrimitiveType theSystemVersion); + void preExpandDeferredValueSetsToTerminologyTables(); /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUploadPropertiesEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUploadPropertiesEnum.java index d1d1295c9e2..225601034b7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUploadPropertiesEnum.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUploadPropertiesEnum.java @@ -96,6 +96,9 @@ public enum LoincUploadPropertiesEnum { /* * OPTIONAL */ + // This is the version identifier for the answer list file + LOINC_CODESYSTEM_VERSION("loinc.codesystem.version"), + // This is the version identifier for the answer list file LOINC_ANSWERLIST_VERSION("loinc.answerlist.version"), diff --git a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/term/loinc/loincupload.properties b/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/term/loinc/loincupload.properties index 986d2afdaa0..cb7d1eb0345 100644 --- a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/term/loinc/loincupload.properties +++ b/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/term/loinc/loincupload.properties @@ -61,6 +61,10 @@ loinc.universal.lab.order.valueset.file=AccessoryFiles/LoincUniversalLabOrdersVa ### OPTIONAL ### ################ +# This is the version identifier for the LOINC code system +## Key may be omitted if only a single version of LOINC is being kept. +loinc.codesystem.version=2.68 + # This is the version identifier for the answer list file ## Key may be omitted loinc.answerlist.version=Beta.1 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index f8e15870a2f..d34393b7b61 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -521,7 +521,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { myTerminologyDeferredStorageSvc.saveDeferred(); myTerminologyDeferredStorageSvc.saveDeferred(); - IValidationSupport.LookupCodeResult lookupResults = myCodeSystemDao.lookupCode(new StringType("childAA"), new StringType(URL_MY_CODE_SYSTEM),null, mySrd); + IValidationSupport.LookupCodeResult lookupResults = myCodeSystemDao.lookupCode(new StringType("childAA"), new StringType(URL_MY_CODE_SYSTEM), null,null, mySrd); assertEquals(true, lookupResults.isFound()); ValueSet vs = new ValueSet(); @@ -711,7 +711,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { StringType code = new StringType("ParentA"); StringType system = new StringType("http://snomed.info/sct"); - IValidationSupport.LookupCodeResult outcome = myCodeSystemDao.lookupCode(code, system, null, mySrd); + IValidationSupport.LookupCodeResult outcome = myCodeSystemDao.lookupCode(code, system, null,null, mySrd); assertEquals(true, outcome.isFound()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index b73ced860d1..63b88299e1b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -828,7 +828,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { StringType code = new StringType("ParentA"); StringType system = new StringType("http://snomed.info/sct"); - IValidationSupport.LookupCodeResult outcome = myCodeSystemDao.lookupCode(code, system, null, mySrd); + IValidationSupport.LookupCodeResult outcome = myCodeSystemDao.lookupCode(code, system, null,null, mySrd); assertEquals(true, outcome.isFound()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java index 7d6767f1aa4..6196d2d226f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; @@ -415,6 +416,51 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } + @Test + public void testExpandByValueSetWithFilter() throws IOException { + loadAndPersistCodeSystem(); + + ValueSet toExpand = loadResourceFromClasspath(ValueSet.class, "/extensional-case-3-vs.xml"); + + Parameters respParam = myClient + .operation() + .onType(ValueSet.class) + .named("expand") + .withParameter(Parameters.class, "valueSet", toExpand) + .andParameter("filter", new StringType("blood")) + .execute(); + ValueSet expanded = (ValueSet) respParam.getParameter().get(0).getResource(); + + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded); + ourLog.info(resp); + assertThat(resp, stringContainsInOrder( + "", + "")); + + } + + @Test + public void testExpandByUrlWithFilter() throws Exception { + loadAndPersistCodeSystemAndValueSet(); + + Parameters respParam = myClient + .operation() + .onType(ValueSet.class) + .named("expand") + .withParameter(Parameters.class, "url", new UriType("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2")) + .andParameter("filter", new StringType("first")) + .execute(); + ValueSet expanded = (ValueSet) respParam.getParameter().get(0).getResource(); + + String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded); + ourLog.info(resp); + assertThat(resp, stringContainsInOrder( + "", + "")); + + } + + @Test public void testExpandByValueSetWithPreExpansion() throws IOException { myDaoConfig.setPreExpandValueSets(true); 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 8c6f1fc8805..cd7ddf2265f 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 @@ -13,7 +13,7 @@ public class TermCodeSystemStorageSvcTest extends BaseJpaR4Test { public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system"; @Test - public void testStoreNewCodeSystemVersionForExistingCodeSystem() { + public void testStoreNewCodeSystemVersionForExistingCodeSystemNoVersionId() { CodeSystem upload = createCodeSystemWithMoreThan100Concepts(); // Create CodeSystem resource @@ -34,15 +34,38 @@ public class TermCodeSystemStorageSvcTest extends BaseJpaR4Test { assertEquals(125, myTermConceptDao.count()); } + @Test + public void testStoreNewCodeSystemVersionForExistingCodeSystemVersionId() { + CodeSystem upload = createCodeSystemWithMoreThan100Concepts(); + upload.setVersion("1"); + + // Create CodeSystem resource + ResourceTable codeSystemResourceEntity = (ResourceTable) myCodeSystemDao.create(upload, mySrd).getEntity(); + + // Update the CodeSystem resource + runInTransaction(() -> myTermCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded(upload, codeSystemResourceEntity)); + + /* + Because there are more than 100 concepts in the code system, the first 100 will be persisted immediately and + the remaining 25 concepts will be queued up for "deferred save". + + As the CodeSystem was persisted twice, the extra 25 term concepts will be queued twice, each with a different + CodeSystem version PID. Only one set of the term concepts should be persisted (i.e. 125 term concepts in total). + */ + myTerminologyDeferredStorageSvc.setProcessDeferred(true); + myTerminologyDeferredStorageSvc.saveDeferred(); + assertEquals(125, myTermConceptDao.count()); + } + private CodeSystem createCodeSystemWithMoreThan100Concepts() { CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(URL_MY_CODE_SYSTEM); - codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); for (int i = 0; i < 125; i++) { codeSystem.addConcept(new CodeSystem.ConceptDefinitionComponent(new CodeType("codeA " + i))); } + codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); return codeSystem; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java index c41a29eddb6..df83c976ec7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java @@ -17,12 +17,10 @@ import ca.uhn.fhir.jpa.term.loinc.LoincTop2000LabResultsUsHandler; import ca.uhn.fhir.jpa.term.loinc.LoincUniversalOrderSetHandler; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.ValueSet; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -342,6 +340,28 @@ public class TerminologyLoaderSvcLoincTest extends BaseLoaderTest { assertEquals("13006-2", vs.getCompose().getInclude().get(0).getConcept().get(1).getCode()); } + @Test + public void testLoadLoincMultipleVersions() throws IOException { + + // Load LOINC marked as version 2.67 + addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v267_loincupload.properties"); + mySvc.loadLoinc(myFiles.getFiles(), mySrd); + + verify(myTermCodeSystemStorageSvc, times(1)).storeNewCodeSystemVersion(mySystemCaptor.capture(), myCsvCaptor.capture(), any(RequestDetails.class), myValueSetsCaptor.capture(), myConceptMapCaptor.capture()); + CodeSystem loincCS = mySystemCaptor.getValue(); + assertEquals("2.67", loincCS.getVersion()); + + // Load LOINC marked as version 2.68 + myFiles = new ZipCollectionBuilder(); + addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v268_loincupload.properties"); + mySvc.loadLoinc(myFiles.getFiles(), mySrd); + + verify(myTermCodeSystemStorageSvc, times(2)).storeNewCodeSystemVersion(mySystemCaptor.capture(), myCsvCaptor.capture(), any(RequestDetails.class), myValueSetsCaptor.capture(), myConceptMapCaptor.capture()); + loincCS = mySystemCaptor.getValue(); + assertEquals("2.68", loincCS.getVersion()); + + } + @Test @Disabled public void testLoadLoincMandatoryFilesOnly() throws IOException { @@ -382,7 +402,11 @@ public class TerminologyLoaderSvcLoincTest extends BaseLoaderTest { } public static void addLoincMandatoryFilesToZip(ZipCollectionBuilder theFiles) throws IOException { - theFiles.addFileZip("/loinc/", LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + addLoincMandatoryFilesWithPropertiesFileToZip(theFiles, LOINC_UPLOAD_PROPERTIES_FILE.getCode()); + } + + public static void addLoincMandatoryFilesWithPropertiesFileToZip(ZipCollectionBuilder theFiles, String thePropertiesFile) throws IOException { + theFiles.addFileZip("/loinc/", thePropertiesFile); theFiles.addFileZip("/loinc/", LOINC_GROUP_FILE_DEFAULT.getCode()); theFiles.addFileZip("/loinc/", LOINC_GROUP_TERMS_FILE_DEFAULT.getCode()); theFiles.addFileZip("/loinc/", LOINC_PARENT_GROUP_FILE_DEFAULT.getCode()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcDeltaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcDeltaR4Test.java index 265e2064761..db601c7a330 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcDeltaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcDeltaR4Test.java @@ -184,7 +184,7 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test { assertEquals(2, outcome.getUpdatedConceptCount()); runInTransaction(() -> { - TermConcept concept = myTermSvc.findCode("http://foo/cs", "ChildAA").orElseThrow(() -> new IllegalStateException()); + TermConcept concept = myTermSvc.findCode("http://foo/cs", "ChildAA", null).orElseThrow(() -> new IllegalStateException()); assertEquals(2, concept.getParents().size()); assertThat(concept.getParentPidsAsString(), matchesPattern("^[0-9]+ [0-9]+$")); }); 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 d0ff1cebb37..51b817bbb34 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 @@ -4,11 +4,15 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.jpa.api.model.TranslationRequest; +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.TermConceptMap; import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; import ca.uhn.fhir.jpa.entity.TermValueSet; +import ca.uhn.fhir.model.dstu2.valueset.ContentTypeEnum; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.CanonicalType; @@ -30,9 +34,14 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -1880,4 +1889,109 @@ public class TerminologySvcImplR4Test extends BaseTermR4Test { assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } + + @Test + public void testCreateCodeSystemTwoVersions() { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(CS_URL); + codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + codeSystem + .addConcept().setCode("A").setDisplay("Code A"); + codeSystem + .addConcept().setCode("B").setDisplay("Code A"); + + codeSystem.setVersion("1"); + + IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + + Set codes = myTermSvc.findCodesBelow(id.getIdPartAsLong(), id.getVersionIdPartAsLong(), "A"); + assertThat(toCodes(codes), containsInAnyOrder("A")); + + codes = myTermSvc.findCodesBelow(id.getIdPartAsLong(), id.getVersionIdPartAsLong(), "B"); + assertThat(toCodes(codes), containsInAnyOrder("B")); + + runInTransaction(() -> { + List termCodeSystemVersions = myTermCodeSystemVersionDao.findAll(); + assertEquals(termCodeSystemVersions.size(), 1); + TermCodeSystemVersion termCodeSystemVersion_1 = termCodeSystemVersions.get(0); + assertEquals(termCodeSystemVersion_1.getConcepts().size(), 2); + Set termConcepts = new HashSet<>(termCodeSystemVersion_1.getConcepts()); + assertThat(toCodes(termConcepts), containsInAnyOrder("A", "B")); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByResourcePid(id.getIdPartAsLong()); + assertEquals("1", termCodeSystem.getCurrentVersion().getCodeSystemVersionId()); + + }); + + codeSystem.setVersion("2"); + codeSystem + .addConcept().setCode("C").setDisplay("Code C"); + + myCodeSystemDao.update(codeSystem, mySrd).getId().toUnqualified(); + codes = myTermSvc.findCodesBelow(id.getIdPartAsLong(), id.getVersionIdPartAsLong(), "C"); + assertThat(toCodes(codes), containsInAnyOrder("C")); + + runInTransaction(() -> { + List termCodeSystemVersions_updated = myTermCodeSystemVersionDao.findAll(); + assertEquals(termCodeSystemVersions_updated.size(), 2); + TermCodeSystemVersion termCodeSystemVersion_2 = termCodeSystemVersions_updated.get(1); + assertEquals(termCodeSystemVersion_2.getConcepts().size(), 3); + Set termConcepts_updated = new HashSet<>(termCodeSystemVersion_2.getConcepts()); + assertThat(toCodes(termConcepts_updated), containsInAnyOrder("A", "B", "C")); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByResourcePid(id.getIdPartAsLong()); + assertEquals("2", termCodeSystem.getCurrentVersion().getCodeSystemVersionId()); + }); + } + + @Test + public void testUpdateCodeSystemContentModeNotPresent() { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(CS_URL); + codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + codeSystem + .addConcept().setCode("A").setDisplay("Code A"); + codeSystem + .addConcept().setCode("B").setDisplay("Code A"); + + codeSystem.setVersion("1"); + + IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + + Set codes = myTermSvc.findCodesBelow(id.getIdPartAsLong(), id.getVersionIdPartAsLong(), "A"); + assertThat(toCodes(codes), containsInAnyOrder("A")); + + codes = myTermSvc.findCodesBelow(id.getIdPartAsLong(), id.getVersionIdPartAsLong(), "B"); + assertThat(toCodes(codes), containsInAnyOrder("B")); + + runInTransaction(() -> { + List termCodeSystemVersions = myTermCodeSystemVersionDao.findAll(); + assertEquals(termCodeSystemVersions.size(), 1); + TermCodeSystemVersion termCodeSystemVersion_1 = termCodeSystemVersions.get(0); + assertEquals(termCodeSystemVersion_1.getConcepts().size(), 2); + Set termConcepts = new HashSet<>(termCodeSystemVersion_1.getConcepts()); + assertThat(toCodes(termConcepts), containsInAnyOrder("A", "B")); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByResourcePid(id.getIdPartAsLong()); + assertEquals("1", termCodeSystem.getCurrentVersion().getCodeSystemVersionId()); + + }); + + // Remove concepts and changed ContentMode to NOTPRESENT + codeSystem.setConcept(new ArrayList<>()); + codeSystem.setContent((CodeSystem.CodeSystemContentMode.NOTPRESENT)); + + myCodeSystemDao.update(codeSystem, mySrd).getId().toUnqualified(); + runInTransaction(() -> { + List termCodeSystemVersions_updated = myTermCodeSystemVersionDao.findAll(); + assertEquals(termCodeSystemVersions_updated.size(), 1); + TermCodeSystemVersion termCodeSystemVersion_2 = termCodeSystemVersions_updated.get(0); + assertEquals(termCodeSystemVersion_2.getConcepts().size(), 2); + Set termConcepts_updated = new HashSet<>(termCodeSystemVersion_2.getConcepts()); + assertThat(toCodes(termConcepts_updated), containsInAnyOrder("A", "B")); + + TermCodeSystem termCodeSystem = myTermCodeSystemDao.findByResourcePid(id.getIdPartAsLong()); + assertEquals("1", termCodeSystem.getCurrentVersion().getCodeSystemVersionId()); + }); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/v267_loincupload.properties b/hapi-fhir-jpaserver-base/src/test/resources/loinc/v267_loincupload.properties new file mode 100644 index 00000000000..f9a049fdf06 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/v267_loincupload.properties @@ -0,0 +1,87 @@ +################# +### MANDATORY ### +################# + +# Answer lists (ValueSets of potential answers/values for LOINC "questions") +## File must be present +loinc.answerlist.file=AccessoryFiles/AnswerFile/AnswerList.csv +# Answer list links (connects LOINC observation codes to answer list codes) +## File must be present +loinc.answerlist.link.file=AccessoryFiles/AnswerFile/LoincAnswerListLink.csv + +# Document ontology +## File must be present +loinc.document.ontology.file=AccessoryFiles/DocumentOntology/DocumentOntology.csv + +# LOINC codes +## File must be present +loinc.file=LoincTable/Loinc.csv + +# LOINC hierarchy +## File must be present +loinc.hierarchy.file=AccessoryFiles/MultiAxialHierarchy/MultiAxialHierarchy.csv + +# IEEE medical device codes +## File must be present +loinc.ieee.medical.device.code.mapping.table.file=AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv + +# Imaging document codes +## File must be present +loinc.imaging.document.codes.file=AccessoryFiles/ImagingDocuments/ImagingDocumentCodes.csv + +# Part +## File must be present +loinc.part.file=AccessoryFiles/PartFile/Part.csv + +# Part link +## File must be present +loinc.part.link.primary.file=AccessoryFiles/PartFile/LoincPartLink_Primary.csv +loinc.part.link.supplementary.file=AccessoryFiles/PartFile/LoincPartLink_Supplementary.csv + +# Part related code mapping +## File must be present +loinc.part.related.code.mapping.file=AccessoryFiles/PartFile/PartRelatedCodeMapping.csv + +# RSNA playbook +## File must be present +loinc.rsna.playbook.file=AccessoryFiles/LoincRsnaRadiologyPlaybook/LoincRsnaRadiologyPlaybook.csv + +# Top 2000 codes - SI +## File must be present +loinc.top2000.common.lab.results.si.file=AccessoryFiles/Top2000Results/SI/Top2000CommonLabResultsSi.csv +# Top 2000 codes - US +## File must be present +loinc.top2000.common.lab.results.us.file=AccessoryFiles/Top2000Results/US/Top2000CommonLabResultsUs.csv + +# Universal lab order ValueSet +## File must be present +loinc.universal.lab.order.valueset.file=AccessoryFiles/LoincUniversalLabOrdersValueSet/LoincUniversalLabOrdersValueSet.csv + +################ +### OPTIONAL ### +################ + +# This is the version identifier for the LOINC code system +## Key may be omitted if only a single version of LOINC is being kept. +loinc.codesystem.version=2.67 + +# This is the version identifier for the answer list file +## Key may be omitted +loinc.answerlist.version=Beta.1 + +# This is the version identifier for uploaded ConceptMap resources +## Key may be omitted +loinc.conceptmap.version=Beta.1 + +# Group +## Default value if key not provided: AccessoryFiles/GroupFile/Group.csv +## File may be omitted +loinc.group.file=AccessoryFiles/GroupFile/Group.csv +# Group terms +## Default value if key not provided: AccessoryFiles/GroupFile/GroupLoincTerms.csv +## File may be omitted +loinc.group.terms.file=AccessoryFiles/GroupFile/GroupLoincTerms.csv +# Parent group +## Default value if key not provided: AccessoryFiles/GroupFile/ParentGroup.csv +## File may be omitted +loinc.parent.group.file=AccessoryFiles/GroupFile/ParentGroup.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/v268_loincupload.properties b/hapi-fhir-jpaserver-base/src/test/resources/loinc/v268_loincupload.properties new file mode 100644 index 00000000000..7f4854293de --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/v268_loincupload.properties @@ -0,0 +1,87 @@ +################# +### MANDATORY ### +################# + +# This is the version identifier for the LOINC code system +## Key may be omitted if only a single version of LOINC is being kept. +loinc.codesystem.version=2.68 + +# Answer lists (ValueSets of potential answers/values for LOINC "questions") +## File must be present +loinc.answerlist.file=AccessoryFiles/AnswerFile/AnswerList.csv +# Answer list links (connects LOINC observation codes to answer list codes) +## File must be present +loinc.answerlist.link.file=AccessoryFiles/AnswerFile/LoincAnswerListLink.csv + +# Document ontology +## File must be present +loinc.document.ontology.file=AccessoryFiles/DocumentOntology/DocumentOntology.csv + +# LOINC codes +## File must be present +loinc.file=LoincTable/Loinc.csv + +# LOINC hierarchy +## File must be present +loinc.hierarchy.file=AccessoryFiles/MultiAxialHierarchy/MultiAxialHierarchy.csv + +# IEEE medical device codes +## File must be present +loinc.ieee.medical.device.code.mapping.table.file=AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv + +# Imaging document codes +## File must be present +loinc.imaging.document.codes.file=AccessoryFiles/ImagingDocuments/ImagingDocumentCodes.csv + +# Part +## File must be present +loinc.part.file=AccessoryFiles/PartFile/Part.csv + +# Part link +## File must be present +loinc.part.link.primary.file=AccessoryFiles/PartFile/LoincPartLink_Primary.csv +loinc.part.link.supplementary.file=AccessoryFiles/PartFile/LoincPartLink_Supplementary.csv + +# Part related code mapping +## File must be present +loinc.part.related.code.mapping.file=AccessoryFiles/PartFile/PartRelatedCodeMapping.csv + +# RSNA playbook +## File must be present +loinc.rsna.playbook.file=AccessoryFiles/LoincRsnaRadiologyPlaybook/LoincRsnaRadiologyPlaybook.csv + +# Top 2000 codes - SI +## File must be present +loinc.top2000.common.lab.results.si.file=AccessoryFiles/Top2000Results/SI/Top2000CommonLabResultsSi.csv +# Top 2000 codes - US +## File must be present +loinc.top2000.common.lab.results.us.file=AccessoryFiles/Top2000Results/US/Top2000CommonLabResultsUs.csv + +# Universal lab order ValueSet +## File must be present +loinc.universal.lab.order.valueset.file=AccessoryFiles/LoincUniversalLabOrdersValueSet/LoincUniversalLabOrdersValueSet.csv + +################ +### OPTIONAL ### +################ + +# This is the version identifier for the answer list file +## Key may be omitted +loinc.answerlist.version=Beta.1 + +# This is the version identifier for uploaded ConceptMap resources +## Key may be omitted +loinc.conceptmap.version=Beta.1 + +# Group +## Default value if key not provided: AccessoryFiles/GroupFile/Group.csv +## File may be omitted +loinc.group.file=AccessoryFiles/GroupFile/Group.csv +# Group terms +## Default value if key not provided: AccessoryFiles/GroupFile/GroupLoincTerms.csv +## File may be omitted +loinc.group.terms.file=AccessoryFiles/GroupFile/GroupLoincTerms.csv +# Parent group +## Default value if key not provided: AccessoryFiles/GroupFile/ParentGroup.csv +## File may be omitted +loinc.parent.group.file=AccessoryFiles/GroupFile/ParentGroup.csv diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java index cc04a447d10..3c6c10f9fb3 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/BaseValidationSupportWrapper.java @@ -58,6 +58,11 @@ public class BaseValidationSupportWrapper extends BaseValidationSupport { return myWrap.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + return myWrap.lookupCode(theValidationSupportContext, theSystem, theCode, theVersion); + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { return myWrap.lookupCode(theValidationSupportContext, theSystem, theCode); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 829b9977ad7..53c15009f11 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -83,6 +83,12 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple return loadFromCache(myValidateCodeCache, key, t -> super.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl)); } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + String key = "lookupCode " + theSystem + " " + theCode + " " + theVersion; + return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode, theVersion)); + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { String key = "lookupCode " + theSystem + " " + theCode; 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 b447c39e160..51dfe5dd86b 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 @@ -145,6 +145,12 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + // For now Common Code Systems will not be versioned. + return lookupCode(theValidationSupportContext, theSystem, theCode); + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 3fca6d034ac..dc1c3576c14 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -308,6 +308,12 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu .setMessage(message); } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + // TODO: Add support for validating versioned codes as well. + return lookupCode(theValidationSupportContext, theSystem, theCode); + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { return validateCode(theValidationSupportContext, new ConceptValidationOptions(), theSystem, theCode, null, null).asLookupCodeResult(theSystem, theCode); @@ -470,6 +476,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu List nextCodeList = new ArrayList<>(); String system = nextInclude.getSystem(); + String systemVersion = nextInclude.getVersion(); if (isNotBlank(system)) { if (theWantSystem != null && !theWantSystem.equals(system)) { @@ -492,7 +499,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu if (theWantCode != null) { if (theValidationSupportContext.getRootValidationSupport().isCodeSystemSupported(theValidationSupportContext, system)) { - LookupCodeResult lookup = theValidationSupportContext.getRootValidationSupport().lookupCode(theValidationSupportContext, system, theWantCode); + LookupCodeResult lookup = theValidationSupportContext.getRootValidationSupport().lookupCode(theValidationSupportContext, system, theWantCode, systemVersion); if (lookup != null && lookup.isFound()) { CodeSystem.ConceptDefinitionComponent conceptDefinition = new CodeSystem.ConceptDefinitionComponent() .addConcept() diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 841038fd4b7..ac7da03cd07 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -257,6 +257,16 @@ public class ValidationSupportChain implements IValidationSupport { return null; } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theVersion) { + for (IValidationSupport next : myChain) { + if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) { + return next.lookupCode(theValidationSupportContext, theSystem, theCode, theVersion); + } + } + return null; + } + @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { for (IValidationSupport next : myChain) {