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 36df7a9ddd8b..939c83dc0ceb 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 @@ -774,6 +774,7 @@ public String getPropertyName() { // Some of the types in the spec are not yet implemented as well. // @see https://github.com/hapifhir/hapi-fhir/issues/5700 String TYPE_STRING = "string"; + String TYPE_BOOLEAN = "boolean"; String TYPE_CODING = "Coding"; String TYPE_GROUP = "group"; @@ -800,6 +801,29 @@ public String getType() { } } + class BooleanConceptProperty extends BaseConceptProperty { + private final boolean myValue; + + /** + * Constructor + * + * @param theName The name + */ + public BooleanConceptProperty(String theName, boolean theValue) { + super(theName); + myValue = theValue; + } + + public boolean getValue() { + return myValue; + } + + @Override + public String getType() { + return TYPE_BOOLEAN; + } + } + class CodingConceptProperty extends BaseConceptProperty { private final String myCode; private final String myCodeSystem; @@ -1073,7 +1097,7 @@ class ValueSetExpansionOutcome { private final IBaseResource myValueSet; private final String myError; - private boolean myErrorIsFromServer; + private final boolean myErrorIsFromServer; public ValueSetExpansionOutcome(String theError, boolean theErrorIsFromServer) { myValueSet = null; @@ -1199,7 +1223,7 @@ public LookupCodeResult setFound(boolean theFound) { } public void throwNotFoundIfAppropriate() { - if (isFound() == false) { + if (!isFound()) { throw new ResourceNotFoundException(Msg.code(1738) + "Unable to find code[" + getSearchedForCode() + "] in system[" + getSearchedForSystem() + "]"); } @@ -1270,6 +1294,10 @@ private void populateProperty( StringConceptProperty stringConceptProperty = (StringConceptProperty) theConceptProperty; ParametersUtil.addPartString(theContext, theProperty, "value", stringConceptProperty.getValue()); break; + case TYPE_BOOLEAN: + BooleanConceptProperty booleanConceptProperty = (BooleanConceptProperty) theConceptProperty; + ParametersUtil.addPartBoolean(theContext, theProperty, "value", booleanConceptProperty.getValue()); + break; case TYPE_CODING: CodingConceptProperty codingConceptProperty = (CodingConceptProperty) theConceptProperty; ParametersUtil.addPartCoding( @@ -1321,7 +1349,7 @@ class TranslateCodeRequest { private final String myTargetValueSetUrl; private final IIdType myResourceId; private final boolean myReverse; - private List myCodings; + private final List myCodings; public TranslateCodeRequest(List theCodings, String theTargetSystemUrl) { myCodings = theCodings; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java index 4c0d5b134cd8..77c6e5fab4ee 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java @@ -220,14 +220,36 @@ public String getFirstPartitionNameOrNull() { /** * Returns true if this request partition contains only one partition ID and it is the DEFAULT partition ID (null) + * + * @deprecated use {@link #isDefaultPartition(Integer)} or {@link IRequestPartitionHelperSvc.isDefaultPartition} + * instead + * . */ + @Deprecated(since = "2025.02.R01") public boolean isDefaultPartition() { + return isDefaultPartition(null); + } + + /** + * Test whether this request partition is for a given default partition ID. + * + * This method can be directly invoked on a requestPartition object providing that theDefaultPartitionId + * is known or through {@link IRequestPartitionHelperSvc.isDefaultPartition} where the implementer of the interface + * will provide the default partition id (see {@link IRequestPartitionHelperSvc.getDefaultPartition}). + * + * @param theDefaultPartitionId is the ID that was given to the default partition. The default partition ID can be + * NULL as per default or specifically assigned another value. + * See PartitionSettings#setDefaultPartitionId. + * @return true if the request partition contains only one partition ID and the partition ID is + * theDefaultPartitionId. + */ + public boolean isDefaultPartition(@Nullable Integer theDefaultPartitionId) { if (isAllPartitions()) { return false; } return hasPartitionIds() && getPartitionIds().size() == 1 - && getPartitionIds().get(0) == null; + && Objects.equals(getPartitionIds().get(0), theDefaultPartitionId); } public boolean hasPartitionId(Integer thePartitionId) { @@ -243,8 +265,34 @@ public boolean hasPartitionNames() { return myPartitionNames != null; } + /** + * Verifies that one of the requested partition is the default partition which is assumed to have a default value of + * null. + * + * @return true if one of the requested partition is the default partition(null). + * + * @deprecated use {@link #hasDefaultPartitionId(Integer)} or {@link IRequestPartitionHelperSvc.hasDefaultPartitionId} + * instead + */ + @Deprecated(since = "2025.02.R01") public boolean hasDefaultPartitionId() { - return getPartitionIds().contains(null); + return hasDefaultPartitionId(null); + } + + /** + * Test whether this request partition has the default partition as one of its targeted partitions. + * + * This method can be directly invoked on a requestPartition object providing that theDefaultPartitionId + * is known or through {@link IRequestPartitionHelperSvc.hasDefaultPartitionId} where the implementer of the interface + * will provide the default partition id (see {@link IRequestPartitionHelperSvc.getDefaultPartition}). + * + * @param theDefaultPartitionId is the ID that was given to the default partition. The default partition ID can be + * NULL as per default or specifically assigned another value. + * See PartitionSettings#setDefaultPartitionId. + * @return true if the request partition has the default partition as one of the targeted partition. + */ + public boolean hasDefaultPartitionId(@Nullable Integer theDefaultPartitionId) { + return getPartitionIds().contains(theDefaultPartitionId); } public List getPartitionIdsWithoutDefault() { @@ -285,11 +333,13 @@ public static RequestPartitionId allPartitions() { } @Nonnull + // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. public static RequestPartitionId defaultPartition() { return fromPartitionIds(Collections.singletonList(null)); } @Nonnull + // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) { return fromPartitionIds(Collections.singletonList(null), thePartitionDate); } @@ -360,14 +410,6 @@ public static RequestPartitionId forPartitionIdsAndNames( return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate); } - public static boolean isDefaultPartition(@Nullable RequestPartitionId thePartitionId) { - if (thePartitionId == null) { - return false; - } - - return thePartitionId.isDefaultPartition(); - } - /** * Create a string representation suitable for use as a cache key. Null aware. *

diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/LenientErrorHandler.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/LenientErrorHandler.java index 20e368a38e89..abc21e57958b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/LenientErrorHandler.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/LenientErrorHandler.java @@ -87,9 +87,9 @@ public void incorrectJsonType( ScalarType theFoundScalarType) { if (myLogErrors) { if (ourLog.isWarnEnabled()) { - String message = describeLocation(theLocation) + - createIncorrectJsonTypeMessage( - theElementName, theExpected, theExpectedScalarType, theFound, theFoundScalarType); + String message = describeLocation(theLocation) + + createIncorrectJsonTypeMessage( + theElementName, theExpected, theExpectedScalarType, theFound, theFoundScalarType); ourLog.warn(message); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/system/HapiSystemProperties.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/system/HapiSystemProperties.java index 0f1dd621e161..f7b9296e5ca9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/system/HapiSystemProperties.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/system/HapiSystemProperties.java @@ -37,6 +37,8 @@ public final class HapiSystemProperties { static final long DEFAULT_VALIDATION_RESOURCE_CACHE_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10); static final String PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA = "hapi.storage.prevent_invalidating_conditional_match_criteria"; + static final String DISABLE_DATABASE_PARTITION_MODE_SCHEMA_CHECK = + "hapi.storage.disable_database_partition_mode_schema_check"; private HapiSystemProperties() {} @@ -164,4 +166,9 @@ public static boolean isPreventInvalidatingConditionalMatchCriteria() { return Boolean.parseBoolean(System.getProperty( HapiSystemProperties.PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA, Boolean.FALSE.toString())); } + + public static boolean isDisableDatabasePartitionModeSchemaCheck() { + return Boolean.parseBoolean(System.getProperty( + HapiSystemProperties.DISABLE_DATABASE_PARTITION_MODE_SCHEMA_CHECK, Boolean.FALSE.toString())); + } } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 5beb605e03c1..5164fb2ba8e4 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -151,6 +151,7 @@ ca.uhn.fhir.jpa.dao.BaseTransactionProcessor.fhirPatchShouldNotUseBinaryResource ca.uhn.fhir.jpa.patch.FhirPatch.invalidInsertIndex=Invalid insert index {0} for path {1} - Only have {2} existing entries ca.uhn.fhir.jpa.patch.FhirPatch.invalidMoveSourceIndex=Invalid move source index {0} for path {1} - Only have {2} existing entries ca.uhn.fhir.jpa.patch.FhirPatch.invalidMoveDestinationIndex=Invalid move destination index {0} for path {1} - Only have {2} existing entries +ca.uhn.fhir.jpa.patch.FhirPatch.noMatchingElementForPath=No element matches the specified path: {0} ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1} ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.invalidInclude=Invalid {0} parameter value: "{1}". {2} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java index 8d6e934ba73c..108436fbc827 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java @@ -17,6 +17,8 @@ public class RequestPartitionIdTest { private static final Logger ourLog = LoggerFactory.getLogger(RequestPartitionIdTest.class); + private static final Integer ourDefaultPartitionId = 0; + @Test public void testHashCode() { assertEquals(31860737, RequestPartitionId.allPartitions().hashCode()); @@ -41,6 +43,29 @@ public void testPartition() { assertFalse(RequestPartitionId.forPartitionIdsAndNames(null, Lists.newArrayList(1, 2), null).isDefaultPartition()); } + @Test + public void testIsDefaultPartition_withDefaultPartitionAsParameter() { + + assertThat(RequestPartitionId.defaultPartition().isDefaultPartition(null)).isTrue(); + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId).isDefaultPartition(ourDefaultPartitionId)).isTrue(); + + assertThat(RequestPartitionId.defaultPartition().isDefaultPartition(ourDefaultPartitionId)).isFalse(); + assertThat(RequestPartitionId.allPartitions().isDefaultPartition(ourDefaultPartitionId)).isFalse(); + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId, 2).isDefaultPartition(ourDefaultPartitionId)).isFalse(); + } + + @Test + public void testHasDefaultPartition_withDefaultPartitionAsParameter() { + + assertThat(RequestPartitionId.defaultPartition().hasDefaultPartitionId(null)).isTrue(); + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId).hasDefaultPartitionId(ourDefaultPartitionId)).isTrue(); + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId, null).hasDefaultPartitionId(null)).isTrue(); + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId, null).hasDefaultPartitionId(ourDefaultPartitionId)).isTrue(); + + assertThat(RequestPartitionId.fromPartitionIds(ourDefaultPartitionId).hasDefaultPartitionId(null)).isFalse(); + assertThat(RequestPartitionId.defaultPartition().hasDefaultPartitionId(ourDefaultPartitionId)).isFalse(); + } + @Test public void testMergeIds() { RequestPartitionId input0 = RequestPartitionId.fromPartitionIds(1, 2, 3); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md new file mode 100644 index 000000000000..1a5757852b7b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md @@ -0,0 +1 @@ +This was an interim release which was never made public, as it was decided that this release would have a major bump, to 8.0.0 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/version.yaml new file mode 100644 index 000000000000..25d98f998b65 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2025-02-17" +codename: "Transfiguration" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6599-fix-NPE-in-storage-module.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6599-fix-NPE-in-storage-module.yaml new file mode 100644 index 000000000000..41992b841d61 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6599-fix-NPE-in-storage-module.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 6599 +jira: SMILE-9237 +title: "Previously, attempting to restart the Storage module would result in a `NullPointerException` when partition +selection mode was set to `PATIENT_ID`, mass ingestion was enabled, and at least one Search Parameter was disabled. This +has now been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6615-fixing-validation-on-sp.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6615-fixing-validation-on-sp.yaml new file mode 100644 index 000000000000..abe777766853 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6615-fixing-validation-on-sp.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 6615 +jira: SMILE-9569 +title: "SearchParameter validation was not being skipped on updates, even if requested. This has been fixed" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6644-allow-deleting-resources-if-only-references-are-versioned.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6644-allow-deleting-resources-if-only-references-are-versioned.yaml new file mode 100644 index 000000000000..229c34650779 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6644-allow-deleting-resources-if-only-references-are-versioned.yaml @@ -0,0 +1,11 @@ +--- +type: add +issue: 6644 +jira: SMILE-9604 +title: "By default, referential integrity is enforced on deletes. + This change introduces support for an exceptional case such + that deletion of a given resource will not be blocked if all + references to that resource are versioned. If there is at + least one unversioned reference to the resource, deletion will + still be blocked. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6656-fhir-patch-replace-fix-for-collection-elements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6656-fhir-patch-replace-fix-for-collection-elements.yaml new file mode 100644 index 000000000000..f74d1109289d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6656-fhir-patch-replace-fix-for-collection-elements.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 6656 +title: "Previously, FHIR patch 'replace' would fail when trying to replace a sub-element of a high cardinality element +using the the FHIR patch syntax. This has been fixed." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6662-remote-terminology-boolean-values-as-string.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6662-remote-terminology-boolean-values-as-string.yaml new file mode 100644 index 000000000000..469cd7d40f14 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6662-remote-terminology-boolean-values-as-string.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 6662 +title: "Fixed remote terminology lookup results showing boolean properties as strings." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6673-search-with-fulltextsearch-enabled-should-not-return-nulls.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6673-search-with-fulltextsearch-enabled-should-not-return-nulls.yaml new file mode 100644 index 000000000000..8549474e9d76 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6673-search-with-fulltextsearch-enabled-should-not-return-nulls.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 6673 +title: "When attempting to search for resources while both `AdvancedHSearchIndexing` + and `StoreResourcesInHibernateSearchIndex` are enabled, returned lists + could sometimes contain null entries. + This has been fixed. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6686-failure-creating-cross-partition-subscription.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6686-failure-creating-cross-partition-subscription.yaml new file mode 100644 index 000000000000..bde4c46f1aaf --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6686-failure-creating-cross-partition-subscription.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 6686 +title: "Previously, attempting to create a cross-partition subscription would fail if the default partition ID was +assigned a value different than the default value(null). This issue is fixed." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6689-replace-references-operation-skip-versioned-refs.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6689-replace-references-operation-skip-versioned-refs.yaml new file mode 100644 index 000000000000..4499b28faf0d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6689-replace-references-operation-skip-versioned-refs.yaml @@ -0,0 +1,4 @@ +--- +type: change +issue: 6689 +title: "$hapi.fhir.replace-references operation has been changed to not replace versioned references" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6692-fix-security-label-filtering-with-not-operator.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6692-fix-security-label-filtering-with-not-operator.yaml new file mode 100644 index 000000000000..db6fa8202600 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6692-fix-security-label-filtering-with-not-operator.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 6692 +jira: SMILE-9736 +title: "Previously, when the in-memory matcher was used to match resources with a `_security` label filter +and a `:not` operator (i.e. `_security:not=http://terminology.hl7.org/CodeSystem/v3-ActCode|NODSCLCD`), +resources with no security labels at all were not matched. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6697-missing-search-params-with-hsearch-enabled.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6697-missing-search-params-with-hsearch-enabled.yaml new file mode 100644 index 000000000000..6275acddcb4a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6697-missing-search-params-with-hsearch-enabled.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 6697 +title: "Previously, operation $apply-codesystem-delta-add issued with Hibernate Search enabled and default search params option turned off resulted in an invalid sort specification error. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6700-cross-partition-checker.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6700-cross-partition-checker.yaml new file mode 100644 index 000000000000..9a54e2f55fd1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6700-cross-partition-checker.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 6700 +title: "Previously if a non-null default partition ID was selected when partitioning is enabled, the subscription matcher would fail to find cross-partition subscriptions, causing subscriptions to appear to not be working. This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/version.yaml index 3ce650fd9b30..25d98f998b65 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/version.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/version.yaml @@ -1,3 +1,3 @@ --- release-date: "2025-02-17" -codename: "TBD" +codename: "Transfiguration" diff --git a/hapi-fhir-jpa-hibernate-services/pom.xml b/hapi-fhir-jpa-hibernate-services/pom.xml index beb445c58983..1136dcf735de 100644 --- a/hapi-fhir-jpa-hibernate-services/pom.xml +++ b/hapi-fhir-jpa-hibernate-services/pom.xml @@ -3,9 +3,9 @@ 4.0.0 ca.uhn.hapi.fhir - hapi-fhir + hapi-deployable-pom 8.1.0-SNAPSHOT - ../pom.xml + ../hapi-deployable-pom/pom.xml hapi-fhir-jpa-hibernate-services diff --git a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/DatabasePartitionModeIdFilteringMappingContributor.java b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/DatabasePartitionModeIdFilteringMappingContributor.java index 76e9c4a47606..1c947d3a9cb5 100644 --- a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/DatabasePartitionModeIdFilteringMappingContributor.java +++ b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/DatabasePartitionModeIdFilteringMappingContributor.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Hibernate Services + * %% + * Copyright (C) 2014 - 2025 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% + */ package ca.uhn.hapi.fhir.sql.hibernatesvc; import ca.uhn.fhir.context.ConfigurationException; @@ -42,8 +61,11 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -109,7 +131,7 @@ public class DatabasePartitionModeIdFilteringMappingContributor implements org.hibernate.boot.spi.AdditionalMappingContributor { - private final Set myQualifiedIdRemovedColumnNames = new HashSet<>(); + private final Set myQualifiedIdRemovedColumnNames = new HashSet<>(); /** * Constructor @@ -182,14 +204,15 @@ private void removePartitionedIdColumnsFromMetadata( filterPartitionedIdsFromCompositeComponents(c); } - for (Map.Entry nextEntry : - theMetadata.getEntityBindingMap().entrySet()) { - PersistentClass entityPersistentClass = nextEntry.getValue(); + for (String nextEntityName : + new TreeSet<>(theMetadata.getEntityBindingMap().keySet())) { + + PersistentClass entityPersistentClass = + theMetadata.getEntityBindingMap().get(nextEntityName); Table table = entityPersistentClass.getTable(); for (ForeignKey foreignKey : table.getForeignKeys().values()) { // Adjust relations with local filtered columns (e.g. ManyToOne) - filterPartitionedIdsFromLocalFks( - theClassLoaderService, theMetadata, foreignKey, table, nextEntry.getKey()); + filterPartitionedIdsFromLocalFks(theClassLoaderService, theMetadata, foreignKey, table, nextEntityName); } for (Property property : entityPersistentClass.getProperties()) { @@ -249,15 +272,16 @@ private void filterPartitionedIdsFromIdClassPks( if (remove != null) { iter.remove(); idRemovedColumns.addAll(property.getColumns()); - idRemovedColumnNames.addAll( - property.getColumns().stream().map(Column::getName).collect(Collectors.toSet())); + idRemovedColumnNames.addAll(property.getColumns().stream() + .map(c -> c.getName().toUpperCase(Locale.ROOT)) + .collect(Collectors.toSet())); property.getColumns().stream() - .map(theColumn -> table.getName() + "#" + theColumn.getName()) + .map(theColumn -> new TableAndColumnName(table.getName(), theColumn.getName())) .forEach(myQualifiedIdRemovedColumnNames::add); idRemovedProperties.add(property.getName()); for (Column next : entityPersistentClass.getTable().getColumns()) { - if (idRemovedColumnNames.contains(next.getName())) { + if (idRemovedColumnNames.contains(next.getName().toUpperCase(Locale.ROOT))) { next.setNullable(true); } } @@ -302,15 +326,17 @@ private void filterPartitionedIdsFromCompositeComponents(Component c) { jakarta.persistence.Column column = field.getAnnotation(jakarta.persistence.Column.class); String columnName = column.name(); - myQualifiedIdRemovedColumnNames.add(tableName + "#" + columnName); + myQualifiedIdRemovedColumnNames.add(new TableAndColumnName(tableName, columnName)); PrimaryKey primaryKey = c.getTable().getPrimaryKey(); primaryKey .getColumns() - .removeIf(t -> myQualifiedIdRemovedColumnNames.contains(tableName + "#" + t.getName())); + .removeIf(t -> myQualifiedIdRemovedColumnNames.contains( + new TableAndColumnName(tableName, t.getName()))); for (Column nextColumn : c.getTable().getColumns()) { - if (myQualifiedIdRemovedColumnNames.contains(tableName + "#" + nextColumn.getName())) { + if (myQualifiedIdRemovedColumnNames.contains( + new TableAndColumnName(tableName, nextColumn.getName()))) { nextColumn.setNullable(true); } } @@ -357,13 +383,15 @@ private void filterPartitionedIdsFromLocalFks( ToOne manyToOne = (ToOne) value; Set columnNamesToRemoveFromFks = filterPropertiesFromToOneRelationship( theClassLoaderService, theMetadata, theTable, theEntityTypeName, manyToOne); - removeColumns(theForeignKey.getColumns(), t1 -> columnNamesToRemoveFromFks.contains(t1.getName())); + removeColumns( + theForeignKey.getColumns(), + t1 -> columnNamesToRemoveFromFks.contains(t1.getName().toUpperCase(Locale.ROOT))); } else { theForeignKey .getColumns() - .removeIf(t -> myQualifiedIdRemovedColumnNames.contains( - theForeignKey.getReferencedTable().getName() + "#" + t.getName())); + .removeIf(t -> myQualifiedIdRemovedColumnNames.contains(new TableAndColumnName( + theForeignKey.getReferencedTable().getName(), t.getName()))); } } @@ -384,16 +412,20 @@ private Set filterPropertiesFromToOneRelationship( Set columnNamesToRemoveFromFks = determineFilteredColumnNamesInForeignKey(entityType, propertyName, targetTableName); - removeColumns(manyToOne.getColumns(), t1 -> columnNamesToRemoveFromFks.contains(t1.getName())); + removeColumns( + manyToOne.getColumns(), + t1 -> columnNamesToRemoveFromFks.contains(t1.getName().toUpperCase(Locale.ROOT))); - columnNamesToRemoveFromFks.forEach(t -> myQualifiedIdRemovedColumnNames.add(theTable.getName() + "#" + t)); + columnNamesToRemoveFromFks.forEach( + t -> myQualifiedIdRemovedColumnNames.add(new TableAndColumnName(theTable.getName(), t))); return columnNamesToRemoveFromFks; } private void filterPartitionedIdsFromUniqueConstraints(UniqueKey uniqueKey, Table table) { uniqueKey .getColumns() - .removeIf(t -> myQualifiedIdRemovedColumnNames.contains(table.getName() + "#" + t.getName())); + .removeIf(t -> + myQualifiedIdRemovedColumnNames.contains(new TableAndColumnName(table.getName(), t.getName()))); } private void filterPartitionedIdsFromRemoteFks(PersistentClass entityPersistentClass) { @@ -407,8 +439,8 @@ private void filterPartitionedIdsFromRemoteFks(PersistentClass entityPersistentC dependantValue .getColumns() - .removeIf(t -> myQualifiedIdRemovedColumnNames.contains( - propertyValueBag.getCollectionTable().getName() + "#" + t.getName())); + .removeIf(t -> myQualifiedIdRemovedColumnNames.contains(new TableAndColumnName( + propertyValueBag.getCollectionTable().getName(), t.getName()))); } } } @@ -429,8 +461,9 @@ private Set determineFilteredColumnNamesInForeignKey( if (isBlank(targetColumnName)) { targetColumnName = sourceColumnName; } - if (myQualifiedIdRemovedColumnNames.contains(theTargetTableName + "#" + targetColumnName)) { - columnNamesToRemoveFromFks.add(sourceColumnName); + if (myQualifiedIdRemovedColumnNames.contains( + new TableAndColumnName(theTargetTableName, targetColumnName))) { + columnNamesToRemoveFromFks.add(sourceColumnName.toUpperCase(Locale.ROOT)); } } } @@ -450,13 +483,15 @@ private static void removeColumnsFromIndexes( for (PartitionedIndex partitionedIndex : partitionedIndexes.value()) { String indexName = partitionedIndex.name(); Set columnNames = Set.of(partitionedIndex.columns()); + assert columnNames.stream().allMatch(t -> t.equals(t.toUpperCase(Locale.ROOT))); + Index index = table.getIndex(indexName); if (index != null) { List selectables = getFieldValue(index, "selectables"); for (Iterator iter = selectables.iterator(); iter.hasNext(); ) { Column next = (Column) iter.next(); - if (!columnNames.contains(next.getName())) { + if (!columnNames.contains(next.getName().toUpperCase(Locale.ROOT))) { iter.remove(); } } @@ -552,4 +587,33 @@ private static Class getType(ClassLoaderService theClassLoaderService, String Validate.notNull(entityType, "Could not load type: %s", theEntityTypeName); return entityType; } + + private static class TableAndColumnName { + private final String myTableName; + private final String myColumnName; + private final int myHashCode; + + private TableAndColumnName(String theTableName, String theColumnName) { + myTableName = theTableName.toUpperCase(Locale.ROOT); + myColumnName = theColumnName.toUpperCase(Locale.ROOT); + myHashCode = Objects.hash(myTableName, myColumnName); + } + + @Override + public boolean equals(Object theO) { + if (!(theO instanceof TableAndColumnName)) return false; + TableAndColumnName that = (TableAndColumnName) theO; + return Objects.equals(myTableName, that.myTableName) && Objects.equals(myColumnName, that.myColumnName); + } + + @Override + public int hashCode() { + return myHashCode; + } + + @Override + public String toString() { + return "[" + myTableName + "#" + myColumnName + "]"; + } + } } diff --git a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/HapiHibernateDialectSettingsService.java b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/HapiHibernateDialectSettingsService.java index 1bf94920c66b..6f5e7c970fa3 100644 --- a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/HapiHibernateDialectSettingsService.java +++ b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/HapiHibernateDialectSettingsService.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Hibernate Services + * %% + * Copyright (C) 2014 - 2025 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% + */ package ca.uhn.hapi.fhir.sql.hibernatesvc; import org.hibernate.service.Service; diff --git a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIdProperty.java b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIdProperty.java index 6f8092dfe2fd..1c54a4bb3cce 100644 --- a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIdProperty.java +++ b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIdProperty.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Hibernate Services + * %% + * Copyright (C) 2014 - 2025 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% + */ package ca.uhn.hapi.fhir.sql.hibernatesvc; import java.lang.annotation.Retention; diff --git a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndex.java b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndex.java index 6f784a1de6f3..4d75fb858b74 100644 --- a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndex.java +++ b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndex.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Hibernate Services + * %% + * Copyright (C) 2014 - 2025 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% + */ package ca.uhn.hapi.fhir.sql.hibernatesvc; import java.lang.annotation.Retention; diff --git a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndexes.java b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndexes.java index b4f1e05bbfdb..f610199da7c0 100644 --- a/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndexes.java +++ b/hapi-fhir-jpa-hibernate-services/src/main/java/ca/uhn/hapi/fhir/sql/hibernatesvc/PartitionedIndexes.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Hibernate Services + * %% + * Copyright (C) 2014 - 2025 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% + */ package ca.uhn.hapi.fhir.sql.hibernatesvc; import java.lang.annotation.ElementType; diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index 3a339d93fbf8..06ca538b9b5e 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -6,7 +6,6 @@ ca.uhn.hapi.fhir hapi-deployable-pom 8.1.0-SNAPSHOT - ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java index c096722ee9e8..1d820797b743 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java @@ -27,8 +27,10 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.model.config.PartitionSettings; @@ -127,6 +129,12 @@ public class SearchConfig { @Autowired private HapiTransactionService myHapiTransactionService; + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; + + @Autowired + private IJpaStorageResourceParser myJpaStorageResourceParser; + @Bean public ISearchCoordinatorSvc searchCoordinatorSvc() { return new SearchCoordinatorSvcImpl( @@ -167,6 +175,8 @@ public ISearchBuilder newSearchBuilder(String theResourceName, Class pidsToResource( .flatMap(pidChunk -> searchBuilder.loadResourcesByPid(pidChunk, theRequest).stream()); // apply interceptors return resourceStream - .flatMap(resource -> invokeStoragePreAccessResources(theRequest, resource).stream()) + .flatMap(resource -> resource == null + ? Stream.empty() + : invokeStoragePreAccessResources(theRequest, resource).stream()) .flatMap(resource -> Optional.ofNullable(invokeStoragePreShowResources(theRequest, resource)).stream()); } @@ -2492,16 +2497,19 @@ && getStorageSettings().getResourceServerIdStrategy() // Start if (outcome == null) { - outcome = doUpdateForUpdateOrPatch( - theRequest, - resourceId, - theMatchUrl, - thePerformIndexing, - theForceUpdateVersion, - theResource, - entity, - update, - theTransactionDetails); + UpdateParameters updateParameters = new UpdateParameters<>() + .setRequestDetails(theRequest) + .setResourceIdToUpdate(resourceId) + .setMatchUrl(theMatchUrl) + .setShouldPerformIndexing(thePerformIndexing) + .setShouldForceUpdateVersion(theForceUpdateVersion) + .setResource(theResource) + .setEntity(entity) + .setOperationType(update) + .setTransactionDetails(theTransactionDetails) + .setShouldForcePopulateOldResourceForProcessing(false); + + outcome = doUpdateForUpdateOrPatch(updateParameters); } postUpdateTransaction(theTransactionDetails); @@ -2523,23 +2531,14 @@ protected void postUpdateTransaction(TransactionDetails theTransactionDetails) { } @Override - protected DaoMethodOutcome doUpdateForUpdateOrPatch( - RequestDetails theRequest, - IIdType theResourceId, - String theMatchUrl, - boolean thePerformIndexing, - boolean theForceUpdateVersion, - T theResource, - IBasePersistedResource theEntity, - RestOperationTypeEnum theOperationType, - TransactionDetails theTransactionDetails) { + protected DaoMethodOutcome doUpdateForUpdateOrPatch(UpdateParameters theUpdateParameters) { /* * We stored a resource searchUrl at creation time to prevent resource duplication. * We'll clear any currently existing urls from the db, otherwise we could hit * duplicate index violations if we try to add another (after this create/update) */ - ResourceTable entity = (ResourceTable) theEntity; + ResourceTable entity = (ResourceTable) theUpdateParameters.getEntity(); /* * If we read back the entity from hibernate, and it's already saying @@ -2555,7 +2554,7 @@ protected DaoMethodOutcome doUpdateForUpdateOrPatch( if (entity.isSearchUrlPresent()) { JpaPid persistentId = entity.getResourceId(); - theTransactionDetails.addUpdatedResourceId(persistentId); + theUpdateParameters.getTransactionDetails().addUpdatedResourceId(persistentId); entity.setSearchUrlPresent(false); // it will be removed at the end } @@ -2572,16 +2571,11 @@ protected DaoMethodOutcome doUpdateForUpdateOrPatch( null); } - return super.doUpdateForUpdateOrPatch( - theRequest, - theResourceId, - theMatchUrl, - thePerformIndexing, - theForceUpdateVersion, - theResource, - theEntity, - theOperationType, - theTransactionDetails); + boolean shouldForcePopulateOldResourceForProcessing = myInterceptorBroadcaster instanceof InterceptorService + && ((InterceptorService) myInterceptorBroadcaster) + .hasRegisteredInterceptor(PatientCompartmentEnforcingInterceptor.class); + theUpdateParameters.setShouldForcePopulateOldResourceForProcessing(shouldForcePopulateOldResourceForProcessing); + return super.doUpdateForUpdateOrPatch(theUpdateParameters); } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImpl.java index 4fbcd65c927c..232b8ab1ccc1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImpl.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; @@ -46,12 +47,19 @@ import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE_NORM; import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.SEARCH_PARAM_ROOT; import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.URI_VALUE; +import static java.util.Objects.isNull; /** * Used to build HSearch sort clauses. */ public class HSearchSortHelperImpl implements IHSearchSortHelper { private static final Logger ourLog = LoggerFactory.getLogger(HSearchSortHelperImpl.class); + public static final Map ourSortingParamNameToParamType = Map.of( + Constants.PARAM_LASTUPDATED, RestSearchParameterTypeEnum.DATE, + Constants.PARAM_ID, RestSearchParameterTypeEnum.TOKEN, + Constants.PARAM_TAG, RestSearchParameterTypeEnum.TOKEN, + Constants.PARAM_SECURITY, RestSearchParameterTypeEnum.TOKEN, + Constants.PARAM_SOURCE, RestSearchParameterTypeEnum.TOKEN); /** Indicates which HSearch properties must be sorted for each RestSearchParameterTypeEnum **/ private Map> mySortPropertyListMap = Map.of( @@ -151,14 +159,20 @@ Optional getSortClause(SearchSortFactory theF, SortSpec theSortSp */ @VisibleForTesting Optional getParamType(String theResourceTypeName, String theParamName) { + RestSearchParameterTypeEnum value; + ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( theResourceTypeName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); RuntimeSearchParam searchParam = activeSearchParams.get(theParamName); + if (searchParam == null) { - return Optional.empty(); + value = isNull(theParamName) ? null : ourSortingParamNameToParamType.get(theParamName); + + } else { + value = searchParam.getParamType(); } - return Optional.of(searchParam.getParamType()); + return Optional.ofNullable(value); } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictFinderService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictFinderService.java index 9816f786e513..97058e29d753 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictFinderService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictFinderService.java @@ -35,10 +35,12 @@ public class DeleteConflictFinderService { protected EntityManager myEntityManager; List findConflicts(ResourceTable theEntity, int maxResults) { - TypedQuery query = myEntityManager.createQuery( - "SELECT l FROM ResourceLink l WHERE l.myTargetResource.myPid = :target_pid", ResourceLink.class); + String queryStr = + "SELECT l FROM ResourceLink l WHERE l.myTargetResource.myPid = :target_pid AND (l.myTargetResourceVersion IS NULL)"; + TypedQuery query = myEntityManager.createQuery(queryStr, ResourceLink.class); query.setParameter("target_pid", theEntity.getId()); query.setMaxResults(maxResults); + return query.getResultList(); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroup.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroup.java index 49baa68aa23d..6c3e0ba0e322 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroup.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroup.java @@ -37,6 +37,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -65,20 +66,23 @@ public class TermConceptMapGroup extends BasePartitionable implements Serializab value = { @JoinColumn( name = "CONCEPT_MAP_PID", - insertable = true, + insertable = false, updatable = false, nullable = false, referencedColumnName = "PID"), @JoinColumn( name = "PARTITION_ID", referencedColumnName = "PARTITION_ID", - insertable = true, + insertable = false, updatable = false, nullable = false) }, foreignKey = @ForeignKey(name = "FK_TCMGROUP_CONCEPTMAP")) private TermConceptMap myConceptMap; + @Column(name = "CONCEPT_MAP_PID", nullable = false) + private Long myConceptMapPid; + @Column(name = "SOURCE_URL", nullable = false, length = TermCodeSystem.MAX_URL_LENGTH) private String mySource; @@ -109,6 +113,8 @@ public TermConceptMap getConceptMap() { public TermConceptMapGroup setConceptMap(TermConceptMap theTermConceptMap) { myConceptMap = theTermConceptMap; + myConceptMapPid = theTermConceptMap.getId(); + Validate.notNull(myConceptMapPid, "Concept map pid must not be null"); setPartitionId(theTermConceptMap.getPartitionId()); return this; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElement.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElement.java index 4f48e7e071a8..65063b0b70d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElement.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElement.java @@ -37,6 +37,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -71,20 +72,23 @@ public class TermConceptMapGroupElement extends BasePartitionable implements Ser value = { @JoinColumn( name = "CONCEPT_MAP_GROUP_PID", - insertable = true, + insertable = false, updatable = false, nullable = false, referencedColumnName = "PID"), @JoinColumn( name = "PARTITION_ID", referencedColumnName = "PARTITION_ID", - insertable = true, + insertable = false, updatable = false, nullable = false) }, foreignKey = @ForeignKey(name = "FK_TCMGELEMENT_GROUP")) private TermConceptMapGroup myConceptMapGroup; + @Column(name = "CONCEPT_MAP_GROUP_PID", nullable = false) + private Long myConceptMapGroupPid; + @Column(name = "SOURCE_CODE", nullable = false, length = TermConcept.MAX_CODE_LENGTH) private String myCode; @@ -126,6 +130,8 @@ public TermConceptMapGroup getConceptMapGroup() { public TermConceptMapGroupElement setConceptMapGroup(TermConceptMapGroup theTermConceptMapGroup) { myConceptMapGroup = theTermConceptMapGroup; + myConceptMapGroupPid = theTermConceptMapGroup.getId(); + Validate.notNull(myConceptMapGroupPid, "ConceptMapGroupPid must not be null"); setPartitionId(theTermConceptMapGroup.getPartitionId()); return this; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElementTarget.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElementTarget.java index 44aa5f644cba..10cc50510d4f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElementTarget.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMapGroupElementTarget.java @@ -38,6 +38,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -74,20 +75,23 @@ public class TermConceptMapGroupElementTarget extends BasePartitionable implemen value = { @JoinColumn( name = "CONCEPT_MAP_GRP_ELM_PID", - insertable = true, + insertable = false, updatable = false, nullable = false, referencedColumnName = "PID"), @JoinColumn( name = "PARTITION_ID", referencedColumnName = "PARTITION_ID", - insertable = true, + insertable = false, updatable = false, nullable = false) }, foreignKey = @ForeignKey(name = "FK_TCMGETARGET_ELEMENT")) private TermConceptMapGroupElement myConceptMapGroupElement; + @Column(name = "CONCEPT_MAP_GRP_ELM_PID", nullable = false) + private Long myConceptMapGroupElementPid; + @Column(name = "TARGET_CODE", nullable = true, length = TermConcept.MAX_CODE_LENGTH) private String myCode; @@ -131,6 +135,8 @@ public TermConceptMapGroupElement getConceptMapGroupElement() { public void setConceptMapGroupElement(TermConceptMapGroupElement theTermConceptMapGroupElement) { myConceptMapGroupElement = theTermConceptMapGroupElement; + myConceptMapGroupElementPid = theTermConceptMapGroupElement.getId(); + Validate.notNull(myConceptMapGroupElementPid, "ConceptMapGroupElement must not be null"); setPartitionId(theTermConceptMapGroupElement.getPartitionId()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index f49d72b1a021..9042a482c3c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -22,7 +22,6 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.PartitionEntity; -import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import org.apache.commons.lang3.Validate; @@ -37,11 +36,6 @@ public class RequestPartitionHelperSvc extends BaseRequestPartitionHelperSvc { @Autowired IPartitionLookupSvc myPartitionConfigSvc; - @Autowired - PartitionSettings myPartitionSettings; - - public RequestPartitionHelperSvc() {} - @Override public RequestPartitionId validateAndNormalizePartitionIds(RequestPartitionId theRequestPartitionId) { List names = null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 6b917aa531eb..b8de1d4462e2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -182,7 +182,7 @@ public IBaseParameters replaceReferences( startRequest(theServletRequest); try { - validateReplaceReferencesParams(theSourceId.getValue(), theTargetId.getValue()); + validateReplaceReferencesParams(theSourceId, theTargetId); int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit); @@ -205,13 +205,14 @@ public IBaseParameters replaceReferences( } } - private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { - if (isBlank(theSourceId)) { + private static void validateReplaceReferencesParams( + IPrimitiveType theSourceId, IPrimitiveType theTargetId) { + if (theSourceId == null || isBlank(theSourceId.getValue())) { throw new InvalidRequestException(Msg.code(2583) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); } - if (isBlank(theTargetId)) { + if (theTargetId == null || isBlank(theTargetId.getValue())) { throw new InvalidRequestException(Msg.code(2584) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index c4b8e33d9512..17b4eb0f6428 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -224,17 +224,19 @@ public class SearchBuilder implements ISearchBuilder { private SearchQueryProperties mySearchProperties; - @Autowired(required = false) private IFulltextSearchSvc myFulltextSearchSvc; @Autowired(required = false) - private IElasticsearchSvc myIElasticsearchSvc; + public void setFullTextSearch(IFulltextSearchSvc theFulltextSearchSvc) { + myFulltextSearchSvc = theFulltextSearchSvc; + } + + private IResourceHistoryTableDao myResourceHistoryTableDao; - @Autowired private IJpaStorageResourceParser myJpaStorageResourceParser; - @Autowired - private IResourceHistoryTableDao myResourceHistoryTableDao; + @Autowired(required = false) + private IElasticsearchSvc myIElasticsearchSvc; @Autowired private IResourceHistoryTagDao myResourceHistoryTagDao; @@ -259,6 +261,8 @@ public SearchBuilder( DaoRegistry theDaoRegistry, FhirContext theContext, IIdHelperService theIdHelperService, + IResourceHistoryTableDao theResourceHistoryTagDao, + IJpaStorageResourceParser theIJpaStorageResourceParser, Class theResourceType) { myResourceName = theResourceName; myResourceType = theResourceType; @@ -274,6 +278,8 @@ public SearchBuilder( myDaoRegistry = theDaoRegistry; myContext = theContext; myIdHelperService = theIdHelperService; + myResourceHistoryTableDao = theResourceHistoryTagDao; + myJpaStorageResourceParser = theIJpaStorageResourceParser; mySearchProperties = new SearchQueryProperties(); } @@ -1266,20 +1272,14 @@ private void doLoadPids( } IBaseResource resource = null; - if (next != null) { - resource = myJpaStorageResourceParser.toResource( - resourceType, next, tagMap.get(next.getResourceId()), theForHistoryOperation); - } + resource = myJpaStorageResourceParser.toResource( + resourceType, next, tagMap.get(next.getResourceId()), theForHistoryOperation); if (resource == null) { - if (next != null) { - ourLog.warn( - "Unable to find resource {}/{}/_history/{} in database", - next.getResourceType(), - next.getIdDt().getIdPart(), - next.getVersion()); - } else { - ourLog.warn("Unable to find resource in database."); - } + ourLog.warn( + "Unable to find resource {}/{}/_history/{} in database", + next.getResourceType(), + next.getIdDt().getIdPart(), + next.getVersion()); continue; } @@ -1295,12 +1295,15 @@ private void doLoadPids( ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.MATCH); } + // ensure there's enough space; "<=" because of 0-indexing + while (theResourceListToPopulate.size() <= index) { + theResourceListToPopulate.add(null); + } theResourceListToPopulate.set(index, resource); } } private Map> getResourceTagMap(Collection theHistoryTables) { - switch (myStorageSettings.getTagStorageMode()) { case VERSIONED: return getPidToTagMapVersioned(theHistoryTables); @@ -1410,13 +1413,14 @@ public void loadResourcesByPid( assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids; Map position = new HashMap<>(); + int index = 0; for (JpaPid next : thePids) { - position.put(next.getId(), theResourceListToPopulate.size()); - theResourceListToPopulate.add(null); + position.put(next.getId(), index++); } // Can we fast track this loading by checking elastic search? - if (isLoadingFromElasticSearchSupported(thePids)) { + boolean isUsingElasticSearch = isLoadingFromElasticSearchSupported(thePids); + if (isUsingElasticSearch) { try { theResourceListToPopulate.addAll(loadResourcesFromElasticSearch(thePids)); return; 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 567d92e324a1..c0e46faa16c2 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 @@ -165,7 +165,7 @@ public UploadStatistics applyDeltaCodeSystemsAdd(String theSystem, CustomTermino Validate.notNull(csv); CodeSystem codeSystem = myTerminologySvc.fetchCanonicalCodeSystemFromCompleteContext(theSystem); - if (codeSystem.getContent() != CodeSystem.CodeSystemContentMode.NOTPRESENT) { + if (codeSystem != null && codeSystem.getContent() != CodeSystem.CodeSystemContentMode.NOTPRESENT) { throw new InvalidRequestException( Msg.code(844) + "CodeSystem with url[" + Constants.codeSystemWithDefaultDescription(theSystem) + "] can not apply a delta - wrong content mode: " + codeSystem.getContent()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PartitionedIdModeVerificationSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PartitionedIdModeVerificationSvc.java index 31f514aabdfc..db1599d0b0d0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PartitionedIdModeVerificationSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/PartitionedIdModeVerificationSvc.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.migrate.JdbcUtils; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dialect.IHapiFhirDialect; +import ca.uhn.fhir.system.HapiSystemProperties; import org.apache.commons.collections4.SetUtils; import org.hibernate.dialect.Dialect; import org.slf4j.Logger; @@ -87,6 +88,11 @@ public void verifyPartitionedIdMode() throws SQLException { public static void verifySchemaIsAppropriateForDatabasePartitionMode( DriverTypeEnum.ConnectionProperties cp, boolean expectDatabasePartitionMode) throws SQLException { + + if (HapiSystemProperties.isDisableDatabasePartitionModeSchemaCheck()) { + return; + } + Set pkColumns = JdbcUtils.getPrimaryKeyColumns(cp, "HFJ_RESOURCE"); if (pkColumns.isEmpty()) { return; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImplTest.java index 313381c6be5f..55a19dbd55fe 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/search/HSearchSortHelperImplTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.search; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; @@ -13,6 +14,9 @@ import org.hibernate.search.engine.search.sort.dsl.SortFinalStep; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -20,6 +24,7 @@ import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -81,6 +86,36 @@ void testGetParamType() { assertFalse(paramType.isEmpty()); } + private static Stream provideArgumentsForGetParamType() { + Stream.Builder retVal = Stream.builder(); + HSearchSortHelperImpl.ourSortingParamNameToParamType.forEach((theSortSpecName, theRestSearchParameterTypeEnum) -> + { + SortSpec sortSpec = new SortSpec(theSortSpecName); + retVal.add(Arguments.of(sortSpec, Optional.of(theRestSearchParameterTypeEnum))); + }); + + return retVal.build(); + } + /** + * Validates that getParamType() returns a param type when _id, _lastUpdated, _tag, _security and _source are absent from + * the search param registry. + */ + @ParameterizedTest + @MethodSource("provideArgumentsForGetParamType") + void testGetParamTypeWhenParamNameIsNotInSearchParamRegistry(SortSpec sortSpec, Optional expectedSearchParamType) { + //Given that we have params absent from the SearchParamsRegistry + String resourceType = "CodeSystem"; + String absentSearchParam = sortSpec.getParamName(); + when(mockSearchParamRegistry.getActiveSearchParams(eq(resourceType), any())).thenReturn(mockResourceSearchParams); + when(mockResourceSearchParams.get(absentSearchParam)).thenReturn(null); + + //Execute + Optional paramType = tested.getParamType(resourceType, absentSearchParam); + + //Validate + assertThat(paramType).isEqualTo(expectedSearchParamType); + } + @Test void testGetSortClause() { SortSpec sortSpec = new SortSpec(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/builder/SearchBuilderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/builder/SearchBuilderTest.java index 1d067a62ed6c..73fc495c294b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/builder/SearchBuilderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/builder/SearchBuilderTest.java @@ -1,32 +1,58 @@ package ca.uhn.fhir.jpa.search.builder; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; +import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dao.JpaPid; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class SearchBuilderTest { public static final FhirContext ourCtx = FhirContext.forR4Cached(); + + @Mock + private IResourceHistoryTableDao myResourceHistoryTableDao; + + @Mock + private IJpaStorageResourceParser myJpaStorageResourceParser; + @Spy private FhirContext myFhirContext = ourCtx; @@ -36,9 +62,19 @@ class SearchBuilderTest { @Spy private PartitionSettings myPartitionSettings = new PartitionSettings(); + @Spy + private JpaStorageSettings myStorageSettings = new JpaStorageSettings(); + + @Mock + private IFulltextSearchSvc myFulltextSearchSvc; + @Mock(strictness = Mock.Strictness.LENIENT) private DaoRegistry myDaoRegistry; + /** + * NB: only the fields that are injected in the constructor will be injected by + * mockito + */ @InjectMocks private SearchBuilder mySearchBuilder; @@ -82,6 +118,72 @@ void testPartitionBySizeAndPartitionId_ReuseIfSmallEnoughAndAllSamePartition() { assertSame(input, actual.iterator().next()); } + @ParameterizedTest + @ValueSource(booleans = { true, false }) + @SuppressWarnings("unchecked") + public void loadResourcesByPid_containsNoNullElements(boolean theUseElasticSearch) { + // setup + List pids = new ArrayList<>(); + List includedPids = new ArrayList<>(); + List resources = new ArrayList<>(); + RequestDetails requestDetails = new SystemRequestDetails(); + + pids.add(JpaPid.fromId(1L)); + Patient patient = new Patient(); + patient.setId("Patient/1"); + + if (theUseElasticSearch) { + mySearchBuilder.setFullTextSearch(myFulltextSearchSvc); + + myStorageSettings.setStoreResourceInHSearchIndex(true); + myStorageSettings.setHibernateSearchIndexSearchParams(true); + myStorageSettings.setHibernateSearchIndexFullText(true); + } + + // when + // (these are just for output values) + if (!theUseElasticSearch) { + ResourceHistoryTable ht = new ResourceHistoryTable(); + ht.setResourceId(1L); + ht.setResourceType("Patient"); + + when(myResourceHistoryTableDao.findCurrentVersionsByResourcePidsAndFetchResourceTable(any())) + .thenReturn(List.of(ht)); + when(myJpaStorageResourceParser.toResource(any(Class.class), any(ResourceHistoryTable.class), any(), anyBoolean())) + .thenReturn(patient); + } else { + when(myFulltextSearchSvc.getResources(any(List.class))) + .thenReturn(List.of(patient)); + } + + // test + mySearchBuilder.loadResourcesByPid( + pids, + includedPids, + resources, + false, + requestDetails + ); + + // verify + assertFalse(resources.contains(null)); + + // validating the returns for completion's sake + assertEquals(1, resources.size()); + if (theUseElasticSearch) { + // if using elastisearch, we want to know the getResources was invoked + // with the pid list we sent in + ArgumentCaptor> pidCapture = ArgumentCaptor.forClass(Collection.class); + verify(myFulltextSearchSvc).getResources(pidCapture.capture()); + assertNotNull(pidCapture.getValue()); + assertEquals(pids.size(), pidCapture.getValue().size()); + assertTrue(pidCapture.getValue().contains(1L)); // the only element + } + + // reset + myStorageSettings = new JpaStorageSettings(); + } + @Test void testPartitionBySizeAndPartitionId_Partitioned() { List input = List.of( diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index 0662fbc1dc83..4f4249d6912e 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.rp.r4.PatientResourceProvider; import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases; import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases; +import ca.uhn.fhir.jpa.search.IIdSearchTestTemplate; import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases; import ca.uhn.fhir.jpa.search.builder.SearchBuilder; import ca.uhn.fhir.jpa.search.lastn.ElasticsearchRestClientFactory; @@ -2616,5 +2617,16 @@ protected void beforeOrAfterTestClass(TestContext testContext, DirtiesContext.Cl } } + @Nested + class IdTestCases implements IIdSearchTestTemplate { + @Override + public TestDaoSearch getSearch() { + return myTestDaoSearch; + } + @Override + public ITestDataBuilder getBuilder() { + return myTestDataBuilder; + } + } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmOperationPointcutsIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmOperationPointcutsIT.java index aff7c6cc6ff6..8bf6a91fa224 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmOperationPointcutsIT.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmOperationPointcutsIT.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper; @@ -42,6 +43,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.ConfigurableApplicationContext; import java.io.IOException; import java.util.ArrayList; @@ -80,7 +82,7 @@ private enum LinkHistoryParameters { * All of them should hit our interceptor. */ private enum MdmSubmitEndpoint { - PATIENT_INSTANCE(false,false), + PATIENT_INSTANCE(false, false), PATIENT_TYPE(true, true, false), PRACTITIONER_INSTANCE(false, false), PRACTITIONER_TYPE(true, true, false), @@ -119,6 +121,11 @@ public boolean canTakeCriteria() { @SpyBean private IMdmSubmitSvc myMdmSubmitSvc; + @Autowired + private JpaStorageSettings myStorageSettings; + + @Autowired + private ConfigurableApplicationContext myApplicationContext; private MdmLinkHistoryProviderDstu3Plus myLinkHistoryProvider; @@ -150,382 +157,382 @@ public void after() throws IOException { myInterceptors.clear(); } - @Test - public void mergeGoldenResources_withInterceptor_firesHook() { - // setup - AtomicBoolean called = new AtomicBoolean(false); - String inputState = """ + @Test + public void mergeGoldenResources_withInterceptor_firesHook() { + // setup + AtomicBoolean called = new AtomicBoolean(false); + String inputState = """ + GP1, AUTO, POSSIBLE_DUPLICATE, GP2 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); + + // we won't use for validation, just setup + myMdmLinkHelper.setup(state); + + Patient gp1 = state.getParameter("GP1"); + Patient gp2 = state.getParameter("GP2"); + + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_MERGE_GOLDEN_RESOURCES) + void onUpdate(RequestDetails theDetails, MdmMergeEvent theEvent) { + called.getAndSet(true); + assertEquals("Patient/" + gp1.getIdPart(), theEvent.getFromResource().getId()); + assertEquals("Patient/" + gp2.getIdPart(), theEvent.getToResource().getId()); + assertTrue(theEvent.getFromResource().isGoldenResource() && theEvent.getToResource().isGoldenResource()); + } + }; + myInterceptors.add(interceptor); + myInterceptorService.registerInterceptor(interceptor); + + // test + myMdmProvider.mergeGoldenResources( + new StringType(gp1.getId()), // from + new StringType(gp2.getId()), // to + null, // merged resource + new SystemRequestDetails() // request details + ); + + // verify + assertTrue(called.get()); + } + + @Test + public void mdmUpdate_withInterceptor_firesHook() { + // setup + Patient p1 = createPatient(); + Patient gp1 = createGoldenPatient(); + MdmLink link = (MdmLink) myMdmLinkDaoSvc.newMdmLink(); + link.setLinkSource(MdmLinkSourceEnum.AUTO); + link.setMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH); + link.setCreated(new Date()); + link.setGoldenResourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), gp1))); + link.setSourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), p1))); + myMdmLinkDaoSvc.save(link); + + MdmMatchResultEnum toSave = MdmMatchResultEnum.MATCH; + AtomicBoolean called = new AtomicBoolean(false); + + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_UPDATE_LINK) + void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) { + called.getAndSet(true); + assertThat(theEvent.getMdmLinks()).hasSize(1); + MdmLinkJson link = theEvent.getMdmLinks().get(0); + assertEquals(toSave, link.getMatchResult()); + assertEquals("Patient/" + p1.getIdPart(), link.getSourceId()); + assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId()); + } + }; + myInterceptors.add(interceptor); + myInterceptorService.registerInterceptor(interceptor); + + // test + myMdmProvider.updateLink( + new StringType(gp1.getId()), // golden resource id + new StringType(p1.getId()), // resource id + new StringType(toSave.name()), // link type + new ServletRequestDetails() // request details + ); + + // verify + assertTrue(called.get()); + } + + @Test + public void createLink_withInterceptor_firesHook() { + // setup + AtomicBoolean called = new AtomicBoolean(false); + Patient patient = createPatient(); + Patient golden = createGoldenPatient(); + MdmMatchResultEnum match = MdmMatchResultEnum.MATCH; + + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_CREATE_LINK) + void onCreate(RequestDetails theDetails, MdmLinkEvent theEvent) { + called.getAndSet(true); + assertThat(theEvent.getMdmLinks()).hasSize(1); + MdmLinkJson link = theEvent.getMdmLinks().get(0); + assertEquals(match, link.getMatchResult()); + assertEquals("Patient/" + patient.getIdPart(), link.getSourceId()); + assertEquals("Patient/" + golden.getIdPart(), link.getGoldenResourceId()); + } + }; + myInterceptors.add(interceptor); + myInterceptorService.registerInterceptor(interceptor); + + // test + myMdmProvider.createLink( + new StringType(golden.getId()), + new StringType(patient.getId()), + new StringType(match.name()), + new ServletRequestDetails() + ); + + // validation + assertTrue(called.get()); + } + + @Test + public void notDuplicate_withInterceptor_firesHook() { + // setup + AtomicBoolean called = new AtomicBoolean(); + String initialState = """ GP1, AUTO, POSSIBLE_DUPLICATE, GP2 - """; - MDMState state = new MDMState<>(); - state.setInputState(inputState); - - // we won't use for validation, just setup - myMdmLinkHelper.setup(state); - - Patient gp1 = state.getParameter("GP1"); - Patient gp2 = state.getParameter("GP2"); - - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_MERGE_GOLDEN_RESOURCES) - void onUpdate(RequestDetails theDetails, MdmMergeEvent theEvent) { - called.getAndSet(true); - assertEquals("Patient/" + gp1.getIdPart(), theEvent.getFromResource().getId()); - assertEquals("Patient/" + gp2.getIdPart(), theEvent.getToResource().getId()); - assertTrue(theEvent.getFromResource().isGoldenResource() && theEvent.getToResource().isGoldenResource()); - } - }; - myInterceptors.add(interceptor); - myInterceptorService.registerInterceptor(interceptor); - - // test - myMdmProvider.mergeGoldenResources( - new StringType(gp1.getId()), // from - new StringType(gp2.getId()), // to - null, // merged resource - new SystemRequestDetails() // request details - ); - - // verify - assertTrue(called.get()); - } + """; + MDMState state = new MDMState<>(); + state.setInputState(initialState); + + // we won't use for validation, just setup + myMdmLinkHelper.setup(state); + + Patient gp1 = state.getParameter("GP1"); + Patient gp2 = state.getParameter("GP2"); + + // interceptor + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_NOT_DUPLICATE) + void call(RequestDetails theRequestDetails, MdmLinkEvent theEvent) { + called.getAndSet(true); + + assertThat(theEvent.getMdmLinks()).hasSize(1); + MdmLinkJson link = theEvent.getMdmLinks().get(0); + assertEquals("Patient/" + gp2.getIdPart(), link.getSourceId()); + assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId()); + assertEquals(MdmMatchResultEnum.NO_MATCH, link.getMatchResult()); + } + }; + myInterceptors.add(interceptor); + myInterceptorRegistry.registerInterceptor(interceptor); + + // test + myMdmProvider.notDuplicate( + new StringType(gp1.getId()), + new StringType(gp2.getId()), + new ServletRequestDetails() + ); - @Test - public void mdmUpdate_withInterceptor_firesHook() { - // setup - Patient p1 = createPatient(); - Patient gp1 = createGoldenPatient(); - MdmLink link = (MdmLink) myMdmLinkDaoSvc.newMdmLink(); - link.setLinkSource(MdmLinkSourceEnum.AUTO); - link.setMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH); - link.setCreated(new Date()); - link.setGoldenResourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), gp1))); - link.setSourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), p1))); - myMdmLinkDaoSvc.save(link); - - MdmMatchResultEnum toSave = MdmMatchResultEnum.MATCH; - AtomicBoolean called = new AtomicBoolean(false); - - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_UPDATE_LINK) - void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) { - called.getAndSet(true); - assertThat(theEvent.getMdmLinks()).hasSize(1); - MdmLinkJson link = theEvent.getMdmLinks().get(0); - assertEquals(toSave, link.getMatchResult()); - assertEquals("Patient/" + p1.getIdPart(), link.getSourceId()); - assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId()); - } - }; - myInterceptors.add(interceptor); - myInterceptorService.registerInterceptor(interceptor); - - // test - myMdmProvider.updateLink( - new StringType(gp1.getId()), // golden resource id - new StringType(p1.getId()), // resource id - new StringType(toSave.name()), // link type - new ServletRequestDetails() // request details - ); - - // verify - assertTrue(called.get()); - } + // verify + assertTrue(called.get()); + } - @Test - public void createLink_withInterceptor_firesHook() { - // setup - AtomicBoolean called = new AtomicBoolean(false); - Patient patient = createPatient(); - Patient golden = createGoldenPatient(); - MdmMatchResultEnum match = MdmMatchResultEnum.MATCH; - - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_CREATE_LINK) - void onCreate(RequestDetails theDetails, MdmLinkEvent theEvent) { - called.getAndSet(true); - assertThat(theEvent.getMdmLinks()).hasSize(1); - MdmLinkJson link = theEvent.getMdmLinks().get(0); - assertEquals(match, link.getMatchResult()); - assertEquals("Patient/" + patient.getIdPart(), link.getSourceId()); - assertEquals("Patient/" + golden.getIdPart(), link.getGoldenResourceId()); - } - }; - myInterceptors.add(interceptor); - myInterceptorService.registerInterceptor(interceptor); - - // test - myMdmProvider.createLink( - new StringType(golden.getId()), - new StringType(patient.getId()), - new StringType(match.name()), - new ServletRequestDetails() - ); - - // validation - assertTrue(called.get()); + @ParameterizedTest + @ValueSource(strings = { + "Patient,Practitioner,Medication", + "Patient", + "" + }) + public void clearMdmLinks_withHook_firesInterceptor(String theResourceTypes) { + // setup + AtomicBoolean called = new AtomicBoolean(); + Batch2JobStartResponse response = new Batch2JobStartResponse(); + response.setInstanceId("test"); + + List> resourceTypes = new ArrayList<>(); + if (isNotBlank(theResourceTypes)) { + String[] rts = theResourceTypes.split(","); + for (String rt : rts) { + resourceTypes.add(new StringType(rt)); + } } - @Test - public void notDuplicate_withInterceptor_firesHook() { - // setup - AtomicBoolean called = new AtomicBoolean(); - String initialState = """ - GP1, AUTO, POSSIBLE_DUPLICATE, GP2 - """; - MDMState state = new MDMState<>(); - state.setInputState(initialState); - - // we won't use for validation, just setup - myMdmLinkHelper.setup(state); - - Patient gp1 = state.getParameter("GP1"); - Patient gp2 = state.getParameter("GP2"); - - // interceptor - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_NOT_DUPLICATE) - void call(RequestDetails theRequestDetails, MdmLinkEvent theEvent) { - called.getAndSet(true); - - assertThat(theEvent.getMdmLinks()).hasSize(1); - MdmLinkJson link = theEvent.getMdmLinks().get(0); - assertEquals("Patient/" + gp2.getIdPart(), link.getSourceId()); - assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId()); - assertEquals(MdmMatchResultEnum.NO_MATCH, link.getMatchResult()); - } - }; - myInterceptors.add(interceptor); - myInterceptorRegistry.registerInterceptor(interceptor); - - // test - myMdmProvider.notDuplicate( - new StringType(gp1.getId()), - new StringType(gp2.getId()), - new ServletRequestDetails() - ); - - // verify - assertTrue(called.get()); - } + // when + // we don't care to actually submit the job, so we'll mock it here + doReturn(response) + .when(myJobCoordinator).startInstance(any(RequestDetails.class), any(JobInstanceStartRequest.class)); - @ParameterizedTest - @ValueSource(strings = { - "Patient,Practitioner,Medication", - "Patient", - "" - }) - public void clearMdmLinks_withHook_firesInterceptor(String theResourceTypes) { - // setup - AtomicBoolean called = new AtomicBoolean(); - Batch2JobStartResponse response = new Batch2JobStartResponse(); - response.setInstanceId("test"); - - List> resourceTypes = new ArrayList<>(); - if (isNotBlank(theResourceTypes)) { - String[] rts = theResourceTypes.split(","); - for (String rt : rts) { - resourceTypes.add(new StringType(rt)); + // interceptor + Object interceptor = new Object() { + @Hook(Pointcut.MDM_CLEAR) + void call(RequestDetails theRequestDetails, MdmClearEvent theEvent) { + called.set(true); + + assertNotNull(theEvent.getResourceTypes()); + if (isNotBlank(theResourceTypes)) { + assertThat(theEvent.getResourceTypes()).hasSize(resourceTypes.size()); + + for (IPrimitiveType resourceName : resourceTypes) { + assertThat(theEvent.getResourceTypes()).contains(resourceName.getValue()); + } + } else { + // null or empty resource types means all + // mdm resource types + myMdmSettings.getMdmRules() + .getMdmTypes().forEach(rtype -> { + assertTrue(theEvent.getResourceTypes().contains(rtype)); + }); } } + }; + myInterceptors.add(interceptor); + myInterceptorRegistry.registerInterceptor(interceptor); + + // test + myMdmProvider.clearMdmLinks( + resourceTypes, // resource type filter + null, // batchsize + new ServletRequestDetails() + ); - // when - // we don't care to actually submit the job, so we'll mock it here - doReturn(response) - .when(myJobCoordinator).startInstance(any(RequestDetails.class), any(JobInstanceStartRequest.class)); - - // interceptor - Object interceptor = new Object() { - @Hook(Pointcut.MDM_CLEAR) - void call(RequestDetails theRequestDetails, MdmClearEvent theEvent) { - called.set(true); + // verify + assertTrue(called.get()); + } - assertNotNull(theEvent.getResourceTypes()); - if (isNotBlank(theResourceTypes)) { - assertThat(theEvent.getResourceTypes()).hasSize(resourceTypes.size()); + @ParameterizedTest + @EnumSource(MdmSubmitEndpoint.class) + public void mdmSubmit_interceptor_differentPaths(MdmSubmitEndpoint theMdmSubmitEndpoint) throws InterruptedException { + // setup + AtomicBoolean called = new AtomicBoolean(); + Batch2JobStartResponse res = new Batch2JobStartResponse(); + res.setInstanceId("test"); + List urls = new ArrayList<>(); + boolean[] asyncValue = new boolean[1]; + PointcutLatch latch = new PointcutLatch(theMdmSubmitEndpoint.name()); + + // when + // we don't actually want to start batch jobs, so we'll mock it + doReturn(res) + .when(myJobCoordinator).startInstance(any(RequestDetails.class), any(JobInstanceStartRequest.class)); + doReturn(1L) + .when(myMdmSubmitSvc).submitSourceResourceTypeToMdm(anyString(), any(), any(RequestDetails.class)); + + // use identifier because it's on almost every resource type + StringType[] criteria = theMdmSubmitEndpoint.canTakeCriteria() ? + new StringType[]{new StringType("identifier=true"), null} + : new StringType[]{null}; + ServletRequestDetails request = new ServletRequestDetails(); + + // register an interceptor + Object interceptor = new Object() { + @Hook(Pointcut.MDM_SUBMIT) + void call(RequestDetails theRequestDetails, MdmSubmitEvent theEvent) { + called.set(true); + + assertEquals(asyncValue[0], theEvent.isBatchJob()); + + String urlStr = String.join(", ", urls); + assertThat(theEvent.getUrls().size()).as(urlStr + " <-> " + String.join(", ", theEvent.getUrls())).isEqualTo(urls.size()); + for (String url : urls) { + assertThat(theEvent.getUrls().contains(url)).as("[" + urlStr + "] does not contain " + url + ".").isTrue(); + } + latch.call(1); + } + }; + myInterceptors.add(interceptor); + myInterceptorRegistry.registerInterceptor(interceptor); + + for (StringType criterion : criteria) { + for (boolean respondAsync : theMdmSubmitEndpoint.getAsyncOptions()) { + ourLog.info("\nRunning test for {}; async: {}", theMdmSubmitEndpoint.name(), respondAsync); + + // reset + asyncValue[0] = respondAsync; + called.set(false); + urls.clear(); + + ServletRequestDetails req = spy(request); + doReturn(respondAsync).when(req).isPreferRespondAsync(); + + // test + latch.setExpectedCount(1); + switch (theMdmSubmitEndpoint) { + case PATIENT_INSTANCE: + // patient must exist to do the mdm submit + Patient p = new Patient(); + p.setActive(true); + p.addName() + .setFamily("Simpson") + .addGiven("Homer"); + long patientId = myPatientDao.create(p) + .getId().getIdPartAsLong(); + + IdType patientIdType = new IdType("Patient/" + patientId); + + urls.add(patientIdType.getValue()); + + myMdmProvider.mdmBatchPatientInstance( + patientIdType, + req + ); + break; + case PATIENT_TYPE: + if (respondAsync) { + urls.add("Patient?"); + } else { + urls.add(createUrl("Patient", criterion)); + } - for (IPrimitiveType resourceName : resourceTypes) { - assertThat(theEvent.getResourceTypes()).contains(resourceName.getValue()); + myMdmProvider.mdmBatchPatientType( + criterion, // criteria + null, // batch size + req // request + ); + break; + case PRACTITIONER_INSTANCE: + // practitioner must exist to do mdm submit + Practitioner practitioner = new Practitioner(); + practitioner.setActive(true); + practitioner.addName() + .setFamily("Hibbert") + .addGiven("Julius"); + long practitionerId = myPractitionerDao.create(practitioner) + .getId().getIdPartAsLong(); + IdType practitionerIdType = new IdType("Practitioner/" + practitionerId); + + urls.add(practitionerIdType.getValue()); + + myMdmProvider.mdmBatchPractitionerInstance( + practitionerIdType, + req + ); + break; + case PRACTITIONER_TYPE: + if (respondAsync) { + urls.add("Practitioner?"); + } else { + urls.add(createUrl("Practitioner", criterion)); } - } else { - // null or empty resource types means all - // mdm resource types + + myMdmProvider.mdmBatchPractitionerType( + criterion, // criteria + null, // batchsize + req // request + ); + break; + case RANDOM_MDM_RESOURCE: + // these tests use the mdm rules in: + // resources/mdm/mdm-rules.json + // Medication is one of the allowable mdm types + String resourceType = "Medication"; + urls.add(createUrl(resourceType, criterion)); + myMdmProvider.mdmBatchOnAllSourceResources( + new StringType(resourceType), + criterion, + null, + req + ); + break; + case ALL_RESOURCES: myMdmSettings.getMdmRules() .getMdmTypes().forEach(rtype -> { - assertTrue(theEvent.getResourceTypes().contains(rtype)); + urls.add(createUrl(rtype, criterion)); }); - } - } - }; - myInterceptors.add(interceptor); - myInterceptorRegistry.registerInterceptor(interceptor); - - // test - myMdmProvider.clearMdmLinks( - resourceTypes, // resource type filter - null, // batchsize - new ServletRequestDetails() - ); - - // verify - assertTrue(called.get()); - } - @ParameterizedTest - @EnumSource(MdmSubmitEndpoint.class) - public void mdmSubmit_interceptor_differentPaths(MdmSubmitEndpoint theMdmSubmitEndpoint) throws InterruptedException { - // setup - AtomicBoolean called = new AtomicBoolean(); - Batch2JobStartResponse res = new Batch2JobStartResponse(); - res.setInstanceId("test"); - List urls = new ArrayList<>(); - boolean[] asyncValue = new boolean[1]; - PointcutLatch latch = new PointcutLatch(theMdmSubmitEndpoint.name()); - - // when - // we don't actually want to start batch jobs, so we'll mock it - doReturn(res) - .when(myJobCoordinator).startInstance(any(RequestDetails.class), any(JobInstanceStartRequest.class)); - doReturn(1L) - .when(myMdmSubmitSvc).submitSourceResourceTypeToMdm(anyString(), any(), any(RequestDetails.class)); - - // use identifier because it's on almost every resource type - StringType[] criteria = theMdmSubmitEndpoint.canTakeCriteria() ? - new StringType[] { new StringType("identifier=true"), null } - : new StringType[] { null }; - ServletRequestDetails request = new ServletRequestDetails(); - - // register an interceptor - Object interceptor = new Object() { - @Hook(Pointcut.MDM_SUBMIT) - void call(RequestDetails theRequestDetails, MdmSubmitEvent theEvent) { - called.set(true); - - assertEquals(asyncValue[0], theEvent.isBatchJob()); - - String urlStr = String.join(", ", urls); - assertThat(theEvent.getUrls().size()).as(urlStr + " <-> " + String.join(", ", theEvent.getUrls())).isEqualTo(urls.size()); - for (String url : urls) { - assertThat(theEvent.getUrls().contains(url)).as("[" + urlStr + "] does not contain " + url + ".").isTrue(); - } - latch.call(1); + myMdmProvider.mdmBatchOnAllSourceResources( + null, // resource type (null is all) + criterion, // criteria + null, // batchsize + req + ); + break; } - }; - myInterceptors.add(interceptor); - myInterceptorRegistry.registerInterceptor(interceptor); - - for (StringType criterion : criteria) { - for (boolean respondAsync : theMdmSubmitEndpoint.getAsyncOptions()) { - ourLog.info("\nRunning test for {}; async: {}", theMdmSubmitEndpoint.name(), respondAsync); - - // reset - asyncValue[0] = respondAsync; - called.set(false); - urls.clear(); - - ServletRequestDetails req = spy(request); - doReturn(respondAsync).when(req).isPreferRespondAsync(); - - // test - latch.setExpectedCount(1); - switch (theMdmSubmitEndpoint) { - case PATIENT_INSTANCE: - // patient must exist to do the mdm submit - Patient p = new Patient(); - p.setActive(true); - p.addName() - .setFamily("Simpson") - .addGiven("Homer"); - long patientId = myPatientDao.create(p) - .getId().getIdPartAsLong(); - - IdType patientIdType = new IdType("Patient/" + patientId); - - urls.add(patientIdType.getValue()); - - myMdmProvider.mdmBatchPatientInstance( - patientIdType, - req - ); - break; - case PATIENT_TYPE: - if (respondAsync) { - urls.add("Patient?"); - } else { - urls.add(createUrl("Patient", criterion)); - } - - myMdmProvider.mdmBatchPatientType( - criterion, // criteria - null, // batch size - req // request - ); - break; - case PRACTITIONER_INSTANCE: - // practitioner must exist to do mdm submit - Practitioner practitioner = new Practitioner(); - practitioner.setActive(true); - practitioner.addName() - .setFamily("Hibbert") - .addGiven("Julius"); - long practitionerId = myPractitionerDao.create(practitioner) - .getId().getIdPartAsLong(); - IdType practitionerIdType = new IdType("Practitioner/" + practitionerId); - - urls.add(practitionerIdType.getValue()); - - myMdmProvider.mdmBatchPractitionerInstance( - practitionerIdType, - req - ); - break; - case PRACTITIONER_TYPE: - if (respondAsync) { - urls.add("Practitioner?"); - } else { - urls.add(createUrl("Practitioner", criterion)); - } - - myMdmProvider.mdmBatchPractitionerType( - criterion, // criteria - null, // batchsize - req // request - ); - break; - case RANDOM_MDM_RESOURCE: - // these tests use the mdm rules in: - // resources/mdm/mdm-rules.json - // Medication is one of the allowable mdm types - String resourceType = "Medication"; - urls.add(createUrl(resourceType, criterion)); - myMdmProvider.mdmBatchOnAllSourceResources( - new StringType(resourceType), - criterion, - null, - req - ); - break; - case ALL_RESOURCES: - myMdmSettings.getMdmRules() - .getMdmTypes().forEach(rtype -> { - urls.add(createUrl(rtype, criterion)); - }); - - myMdmProvider.mdmBatchOnAllSourceResources( - null, // resource type (null is all) - criterion, // criteria - null, // batchsize - req - ); - break; - } - // verify - latch.awaitExpected(); - assertTrue(called.get()); - } + // verify + latch.awaitExpected(); + assertTrue(called.get()); } } + } private String createUrl(String theResourceType, StringType theCriteria) { @@ -537,119 +544,117 @@ private String createUrl(String theResourceType, StringType theCriteria) { } - @ParameterizedTest - @EnumSource(LinkHistoryParameters.class) - public void historyLinks_withPointcut_firesHook(LinkHistoryParameters theParametersToSend) { - // setup - AtomicBoolean called = new AtomicBoolean(); - - List> sourceIds = new ArrayList<>(); - List> goldenResourceIds = new ArrayList<>(); - - Patient p1 = createPatient(); - sourceIds.add(new StringType("Patient/" + p1.getIdPart())); - Patient gp1 = createGoldenPatient(); - goldenResourceIds.add(new StringType("Patient/" + gp1.getIdPart())); - MdmLink link = (MdmLink) myMdmLinkDaoSvc.newMdmLink(); - link.setLinkSource(MdmLinkSourceEnum.AUTO); - link.setMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH); - link.setCreated(new Date()); - link.setGoldenResourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), gp1))); - link.setSourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), p1))); - myMdmLinkDaoSvc.save(link); - - // save a change - link.setMatchResult(MdmMatchResultEnum.MATCH); - myMdmLinkDaoSvc.save(link); - - // interceptor - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_LINK_HISTORY) - void onHistory(RequestDetails theRequestDetails, MdmHistoryEvent theEvent) { - called.getAndSet(true); - - List history = theEvent.getMdmLinkRevisions(); - List gids = theEvent.getGoldenResourceIds(); - List sids = theEvent.getSourceIds(); - - if (theParametersToSend == LinkHistoryParameters.SOURCE_IDS) { - assertThat(gids).isEmpty(); - } else if (theParametersToSend == LinkHistoryParameters.GOLDEN_IDS) { - assertThat(sids).isEmpty(); - } else { - assertFalse(sids.isEmpty() && gids.isEmpty()); - } - - assertThat(history).isNotEmpty(); - assertThat(history).hasSize(2); + @ParameterizedTest + @EnumSource(LinkHistoryParameters.class) + public void historyLinks_withPointcut_firesHook(LinkHistoryParameters theParametersToSend) { + // setup + AtomicBoolean called = new AtomicBoolean(); + + List> sourceIds = new ArrayList<>(); + List> goldenResourceIds = new ArrayList<>(); + + Patient p1 = createPatient(); + sourceIds.add(new StringType("Patient/" + p1.getIdPart())); + Patient gp1 = createGoldenPatient(); + goldenResourceIds.add(new StringType("Patient/" + gp1.getIdPart())); + MdmLink link = (MdmLink) myMdmLinkDaoSvc.newMdmLink(); + link.setLinkSource(MdmLinkSourceEnum.AUTO); + link.setMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH); + link.setCreated(new Date()); + link.setGoldenResourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), gp1))); + link.setSourcePersistenceId(runInTransaction(() -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), p1))); + myMdmLinkDaoSvc.save(link); + + // save a change + link.setMatchResult(MdmMatchResultEnum.MATCH); + myMdmLinkDaoSvc.save(link); + + // interceptor + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_LINK_HISTORY) + void onHistory(RequestDetails theRequestDetails, MdmHistoryEvent theEvent) { + called.getAndSet(true); + + List history = theEvent.getMdmLinkRevisions(); + List gids = theEvent.getGoldenResourceIds(); + List sids = theEvent.getSourceIds(); + + if (theParametersToSend == LinkHistoryParameters.SOURCE_IDS) { + assertThat(gids).isEmpty(); + } else if (theParametersToSend == LinkHistoryParameters.GOLDEN_IDS) { + assertThat(sids).isEmpty(); + } else { + assertFalse(sids.isEmpty() && gids.isEmpty()); } - }; - myInterceptors.add(interceptor); - myInterceptorRegistry.registerInterceptor(interceptor); - - // test - List> sourceIdsToSend = theParametersToSend != LinkHistoryParameters.GOLDEN_IDS ? sourceIds : new ArrayList<>(); - List> goldenIdsToSend = theParametersToSend != LinkHistoryParameters.SOURCE_IDS ? goldenResourceIds : new ArrayList<>(); - IBaseParameters retval = myLinkHistoryProvider.historyLinks( - goldenIdsToSend, - sourceIdsToSend, - new ServletRequestDetails() - ); - - // verify - assertTrue(called.get()); - assertFalse(retval.isEmpty()); - } - @Test - public void updateLink_NoMatch_LinkEvent_allUpdates() { - // When a Link is set to "NO_MATCH", it can cause other link updates. - // If a source record would be left unlinked to any - // golden record, a new link / golden record would be created. + assertThat(history).isNotEmpty(); + assertThat(history).hasSize(2); + } + }; + myInterceptors.add(interceptor); + myInterceptorRegistry.registerInterceptor(interceptor); + + // test + List> sourceIdsToSend = theParametersToSend != LinkHistoryParameters.GOLDEN_IDS ? sourceIds : new ArrayList<>(); + List> goldenIdsToSend = theParametersToSend != LinkHistoryParameters.SOURCE_IDS ? goldenResourceIds : new ArrayList<>(); + IBaseParameters retval = myLinkHistoryProvider.historyLinks( + goldenIdsToSend, + sourceIdsToSend, + new ServletRequestDetails() + ); + + // verify + assertTrue(called.get()); + assertFalse(retval.isEmpty()); + } - // setup - String inputState = """ + @Test + public void updateLink_NoMatch_LinkEvent_allUpdates() { + // When a Link is set to "NO_MATCH", it can cause other link updates. + // If a source record would be left unlinked to any + // golden record, a new link / golden record would be created. + + // setup + String inputState = """ GP1, AUTO, MATCH, P1 """; - MDMState state = new MDMState<>(); - state.setInputState(inputState); - - // we won't use for validation, just setup - myMdmLinkHelper.setup(state); + MDMState state = new MDMState<>(); + state.setInputState(inputState); - Patient patient = state.getParameter("P1"); - Patient originalPatientGolden = state.getParameter("GP1"); + // we won't use for validation, just setup + myMdmLinkHelper.setup(state); - AtomicBoolean called = new AtomicBoolean(false); + Patient patient = state.getParameter("P1"); + Patient originalPatientGolden = state.getParameter("GP1"); - Object interceptor = new Object() { - @Hook(Pointcut.MDM_POST_UPDATE_LINK) - void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) { - called.getAndSet(true); - assertThat(theEvent.getMdmLinks()).hasSize(2); + AtomicBoolean called = new AtomicBoolean(false); - MdmLinkJson originalLink = theEvent.getMdmLinks().get(0); - MdmLinkJson newLink = theEvent.getMdmLinks().get(1); - String original_target = "Patient/" + originalPatientGolden.getIdPart(); + Object interceptor = new Object() { + @Hook(Pointcut.MDM_POST_UPDATE_LINK) + void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) { + called.getAndSet(true); + assertThat(theEvent.getMdmLinks()).hasSize(2); - assertThat(originalLink.getGoldenResourceId()).isEqualTo(original_target); - assertThat(newLink.getGoldenResourceId()).isNotEqualTo(original_target); - } - }; - myInterceptors.add(interceptor); - myInterceptorService.registerInterceptor(interceptor); - - // test - myMdmProvider.updateLink( - new StringType("Patient/" + originalPatientGolden.getIdPart()), - new StringType("Patient/" + patient.getIdPart()), - new StringType("NO_MATCH"), - new ServletRequestDetails() - ); - - // verify - assertTrue(called.get()); - } + MdmLinkJson originalLink = theEvent.getMdmLinks().get(0); + MdmLinkJson newLink = theEvent.getMdmLinks().get(1); + String original_target = "Patient/" + originalPatientGolden.getIdPart(); + assertThat(originalLink.getGoldenResourceId()).isEqualTo(original_target); + assertThat(newLink.getGoldenResourceId()).isNotEqualTo(original_target); + } + }; + myInterceptors.add(interceptor); + myInterceptorService.registerInterceptor(interceptor); + + // test + myMdmProvider.updateLink( + new StringType("Patient/" + originalPatientGolden.getIdPart()), + new StringType("Patient/" + patient.getIdPart()), + new StringType("NO_MATCH"), + new ServletRequestDetails() + ); + // verify + assertTrue(called.get()); + } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java index 7526d745c6ca..051d1b4346fc 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java @@ -53,14 +53,14 @@ public class MdmProviderBatchR4Test extends BaseLinkR4Test { protected StringType myGoldenMedicationId; @RegisterExtension - LogbackTestExtension myLogCapture = new LogbackTestExtension((Logger) Logs.getMdmTroubleshootingLog()); + private LogbackTestExtension myLogCapture = new LogbackTestExtension((Logger) Logs.getMdmTroubleshootingLog()); @Autowired - IInterceptorService myInterceptorService; + private IInterceptorService myInterceptorService; @Autowired - MdmSettings myMdmSettings; + private MdmSettings myMdmSettings; - PointcutLatch afterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED); + private final PointcutLatch afterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED); public static Stream requestTypes() { ServletRequestDetails asyncSrd = mock(ServletRequestDetails.class); @@ -72,6 +72,7 @@ public static Stream requestTypes() { Arguments.of(Named.of("Synchronous Request", syncSrd)) ); } + @Override @BeforeEach public void before() throws Exception { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java index 44f2d9840a7a..757624caecee 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java @@ -182,4 +182,37 @@ RequestPartitionId determineCreatePartitionForRequest( * @return - A {@link RequestPartitionId} with a normalized list of partition ids and partition names. */ RequestPartitionId validateAndNormalizePartitionNames(RequestPartitionId theRequestPartitionId); + + /** + * This method returns the default partition ID. Implementers of this interface should overwrite this method to provide + * a default partition ID that is different than the default value of null. + * + * @return the default partition ID + */ + @Nullable + default Integer getDefaultPartitionId() { + return null; + } + + /** + * Test whether theRequestPartitionId is only targeting the default partition where the ID of the default + * partition is provided by {@link #getDefaultPartitionId()}. + * + * @param theRequestPartitionId to perform the evaluation upon. + * @return true if the theRequestPartitionId is for the default partition only. + */ + default boolean isDefaultPartition(@Nonnull RequestPartitionId theRequestPartitionId) { + return theRequestPartitionId.isDefaultPartition(getDefaultPartitionId()); + } + + /** + * Test whether theRequestPartitionId has one of its targeted partitions matching the default partition + * where the ID of the default partition is provided by {@link #getDefaultPartitionId()}. + * + * @param theRequestPartitionId to perform the evaluation upon. + * @return true if the theRequestPartitionId is targeting the default partition. + */ + default boolean hasDefaultPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) { + return theRequestPartitionId.hasDefaultPartitionId(getDefaultPartitionId()); + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java index 3fbc183b8828..5e0af9af7d93 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java @@ -72,6 +72,7 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams.isMatchSearchParam; +import static org.apache.commons.lang3.ObjectUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -438,6 +439,10 @@ private boolean matchTagOrSecurity(IQueryParameterType theParam, IBaseResource t } if (param.getModifier() == TokenParamModifier.NOT) { + // :not filters for security labels / tags should always match resources with no security labels / tags + if (isEmpty(list)) { + return true; + } haveMatch = !haveMatch; } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java index 164ef8bd2c73..b8ee96e2ad3a 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import jakarta.annotation.Nonnull; import org.hl7.fhir.r5.model.BaseDateTimeType; +import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.DateTimeType; @@ -68,6 +69,9 @@ public class InMemoryResourceMatcherR5Test { private static final String REQUEST_ID = "a_request_id"; private static final String TEST_SOURCE = SOURCE_URI + "#" + REQUEST_ID; + public static final String SECURITY_LABEL_SYSTEM = "http://terminology.hl7.org/CodeSystem/v3-ActCode"; + public static final String SECURITY_LABEL_CODE = "NODSCLCD"; + @MockBean ISearchParamRegistry mySearchParamRegistry; @MockBean @@ -327,6 +331,42 @@ public void testNowPast() { assertTrue(result.matched()); } + @Test + public void testNotSecurityFilter_onBundleWithDisallowedSecurityTag_isNotMatched() { + String filter = "_security:not=%s|%s".formatted(SECURITY_LABEL_SYSTEM, SECURITY_LABEL_CODE); + + Bundle bundle = new Bundle(); + bundle.getMeta().addSecurity().setSystem(SECURITY_LABEL_SYSTEM).setCode(SECURITY_LABEL_CODE); + + InMemoryMatchResult result = myInMemoryResourceMatcher.match(filter, bundle, null, newRequest()); + assertThat(result.supported()).as(result.getUnsupportedReason()).isTrue(); + assertThat(result.matched()).isFalse(); + } + + @Test + public void testNotSecurityFilter_onBundleWithAllowedSecurityTag_isMatched() { + String filter = "_security:not=%s|%s".formatted(SECURITY_LABEL_SYSTEM, SECURITY_LABEL_CODE); + + Bundle bundle = new Bundle(); + bundle.getMeta().addSecurity().setSystem(SECURITY_LABEL_SYSTEM).setCode("ANOTHER_CODE"); + + InMemoryMatchResult result = myInMemoryResourceMatcher.match(filter, bundle, null, newRequest()); + assertThat(result.supported()).as(result.getUnsupportedReason()).isTrue(); + assertThat(result.matched()).isTrue(); + } + + @Test + public void testNotSecurityFilter_onBundleWithNoSecurityTags_isMatched() { + String filter = "_security:not=%s|%s".formatted(SECURITY_LABEL_SYSTEM, SECURITY_LABEL_CODE); + + Bundle bundle = new Bundle(); + assertThat(bundle.getMeta().getSecurity()).isEmpty(); + + InMemoryMatchResult result = myInMemoryResourceMatcher.match(filter, bundle, null, newRequest()); + assertThat(result.supported()).as(result.getUnsupportedReason()).isTrue(); + assertThat(result.matched()).isTrue(); + } + @Test public void testNowNextWeek() { Observation futureObservation = new Observation(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java index c762a2755493..bc40e5bb2500 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java @@ -264,7 +264,7 @@ protected void validatePermissions( ? theRequestPartitionId : determinePartition(theRequestDetails, theSubscription); - if (!toCheckPartitionId.isDefaultPartition()) { + if (!myRequestPartitionHelperSvc.isDefaultPartition(toCheckPartitionId)) { throw new UnprocessableEntityException( Msg.code(2010) + "Cross partition subscription must be created on the default partition"); } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriberTest.java index 9ddfad2d2c47..2c4dcb595b3c 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriberTest.java @@ -4,7 +4,6 @@ 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.model.entity.StorageSettings; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java index 803087b40663..5765e39b952f 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java @@ -1,32 +1,42 @@ package ca.uhn.fhir.jpa.subscription.match.registry; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.primitive.BooleanDt; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.subscription.SubscriptionConstants; import ca.uhn.fhir.subscription.SubscriptionTestDataHelper; import ca.uhn.fhir.util.HapiExtensions; import jakarta.annotation.Nonnull; +import org.assertj.core.api.AssertionsForClassTypes; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Enumerations; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import java.util.Set; import java.util.stream.Stream; import static ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW; import static ca.uhn.fhir.rest.api.Constants.RESOURCE_PARTITION_ID; import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; +import static java.util.Objects.isNull; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -35,6 +45,8 @@ class SubscriptionCanonicalizerTest { + private static final Integer NON_NULL_DEFAULT_PARTITION_ID = 666; + FhirContext r4Context = FhirContext.forR4(); private final SubscriptionCanonicalizer testedSC = new SubscriptionCanonicalizer(r4Context, new SubscriptionSettings()); @@ -170,6 +182,90 @@ private static Stream crossPartitionParams() { return Stream.of(null, RequestPartitionId.fromPartitionId(1), RequestPartitionId.defaultPartition()) ; } + private class FakeNonNullDefaultPartitionIDHelper implements IRequestPartitionHelperSvc { + + @Override + public @Nullable Integer getDefaultPartitionId() { + return NON_NULL_DEFAULT_PARTITION_ID; + } + + @Override + public boolean isDefaultPartition(@NotNull RequestPartitionId theRequestPartitionId) { + return theRequestPartitionId.getPartitionIds().get(0).equals(NON_NULL_DEFAULT_PARTITION_ID); + } + + @Override + public boolean hasDefaultPartitionId(@NotNull RequestPartitionId theRequestPartitionId) { + return theRequestPartitionId.getPartitionIds().stream().anyMatch(part -> part.equals(NON_NULL_DEFAULT_PARTITION_ID)); + } + + @Override + public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, @NotNull ReadPartitionIdRequestDetails theDetails) { + return null; + } + + @Override + public RequestPartitionId determineGenericPartitionForRequest(RequestDetails theRequestDetails) { + return null; + } + + @Override + public @NotNull RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @NotNull IBaseResource theResource, @NotNull String theResourceType) { + return null; + } + + @Override + public @NotNull Set toReadPartitions(@NotNull RequestPartitionId theRequestPartitionId) { + return Set.of(); + } + + @Override + public boolean isResourcePartitionable(String theResourceType) { + return false; + } + + @Override + public RequestPartitionId validateAndNormalizePartitionIds(RequestPartitionId theRequestPartitionId) { + return null; + } + + @Override + public RequestPartitionId validateAndNormalizePartitionNames(RequestPartitionId theRequestPartitionId) { + return null; + } + } + @Test + public void testNonNullDefaultPartitionIDCanonicalizesToCrossPartition() { + IRequestPartitionHelperSvc myHelperSvc = new FakeNonNullDefaultPartitionIDHelper(); + final SubscriptionSettings subscriptionSettings = new SubscriptionSettings(); + + subscriptionSettings.setCrossPartitionSubscriptionEnabled(true); + final SubscriptionCanonicalizer subscriptionCanonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4(), subscriptionSettings); + subscriptionCanonicalizer.setPartitionHelperSvc(myHelperSvc); + Subscription subscription = buildMdmSubscriptionR4("test-subscription", "Patient?"); + CanonicalSubscription canonicalize = subscriptionCanonicalizer.canonicalize(subscription); + + assertThat(canonicalize.isCrossPartitionEnabled()).isTrue(); + + } + private Subscription buildMdmSubscriptionR4(String theId, String theCriteria) { + Subscription retval = new Subscription(); + retval.setId(theId); + retval.setReason("MDM Simulacrum Subscription"); + retval.setStatus(Subscription.SubscriptionStatus.REQUESTED); + retval.setCriteria(theCriteria); + retval.addExtension() + .setUrl(HapiExtensions.EXTENSION_SUBSCRIPTION_CROSS_PARTITION) + .setValue(new BooleanType().setValue(true)); + Subscription.SubscriptionChannelComponent channel = retval.getChannel(); + channel.setType(Subscription.SubscriptionChannelType.MESSAGE); + channel.setEndpoint("channel:test"); + channel.setPayload(Constants.CT_JSON); + RequestPartitionId retPartId = RequestPartitionId.fromPartitionId(NON_NULL_DEFAULT_PARTITION_ID); + retval.setUserData(Constants.RESOURCE_PARTITION_ID, retPartId); + return retval; + } + @ParameterizedTest @MethodSource("crossPartitionParams") void testSubscriptionCrossPartitionEnableProperty_forDstu2WithExtensionAndPartitions(RequestPartitionId theRequestPartitionId) { @@ -207,15 +303,12 @@ void testSubscriptionCrossPartitionEnableProperty_forDstu3WithExtensionAndPartit final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionTrue = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionTrue); final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionFalse = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionFalse); - if(RequestPartitionId.isDefaultPartition(theRequestPartitionId)){ - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isTrue(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } else { - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } + assertCanonicalSubscriptionCrossPropertyValue( + canonicalSubscriptionWithoutExtension, + canonicalSubscriptionWithExtensionCrossPartitionTrue, + canonicalSubscriptionWithExtensionCrossPartitionFalse, + theRequestPartitionId + ); } @ParameterizedTest @@ -242,16 +335,12 @@ void testSubscriptionCrossPartitionEnableProperty_forR4WithExtensionAndPartition final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionTrue = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionTrue); final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionFalse = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionFalse); - if(RequestPartitionId.isDefaultPartition(theRequestPartitionId)){ - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isTrue(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } else { - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } - + assertCanonicalSubscriptionCrossPropertyValue( + canonicalSubscriptionWithoutExtension, + canonicalSubscriptionWithExtensionCrossPartitionTrue, + canonicalSubscriptionWithExtensionCrossPartitionFalse, + theRequestPartitionId + ); } @ParameterizedTest @@ -276,15 +365,12 @@ void testSubscriptionCrossPartitionEnableProperty_forR4BWithExtensionAndPartitio final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionTrue = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionTrue); final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionFalse = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionFalse); - if(RequestPartitionId.isDefaultPartition(theRequestPartitionId)){ - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isTrue(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } else { - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } + assertCanonicalSubscriptionCrossPropertyValue( + canonicalSubscriptionWithoutExtension, + canonicalSubscriptionWithExtensionCrossPartitionTrue, + canonicalSubscriptionWithExtensionCrossPartitionFalse, + theRequestPartitionId + ); } @ParameterizedTest @@ -309,15 +395,12 @@ void testSubscriptionCrossPartitionEnableProperty_forR5WithExtensionAndPartition final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionTrue = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionTrue); final CanonicalSubscription canonicalSubscriptionWithExtensionCrossPartitionFalse = subscriptionCanonicalizer.canonicalize(subscriptionWithExtensionCrossPartitionFalse); - if(RequestPartitionId.isDefaultPartition(theRequestPartitionId)){ - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isTrue(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } else { - assertThat(canonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isFalse(); - assertThat(canonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); - } + assertCanonicalSubscriptionCrossPropertyValue( + canonicalSubscriptionWithoutExtension, + canonicalSubscriptionWithExtensionCrossPartitionTrue, + canonicalSubscriptionWithExtensionCrossPartitionFalse, + theRequestPartitionId + ); } private org.hl7.fhir.r4b.model.Subscription buildR4BSubscription(String thePayloadContent) { @@ -419,4 +502,21 @@ private static org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent filter.setValue(theValue); return filter; } + + private void assertCanonicalSubscriptionCrossPropertyValue(CanonicalSubscription theCanonicalSubscriptionWithoutExtension, + CanonicalSubscription theCanonicalSubscriptionWithExtensionCrossPartitionTrue, + CanonicalSubscription theCanonicalSubscriptionWithExtensionCrossPartitionFalse, + RequestPartitionId theRequestPartitionId) { + + boolean isDefaultPartition = isNull(theRequestPartitionId) ? false : theRequestPartitionId.isDefaultPartition(); + + AssertionsForClassTypes.assertThat(theCanonicalSubscriptionWithoutExtension.isCrossPartitionEnabled()).isFalse(); + AssertionsForClassTypes.assertThat(theCanonicalSubscriptionWithExtensionCrossPartitionFalse.isCrossPartitionEnabled()).isFalse(); + + if(isDefaultPartition){ + AssertionsForClassTypes.assertThat(theCanonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isTrue(); + } else { + AssertionsForClassTypes.assertThat(theCanonicalSubscriptionWithExtensionCrossPartitionTrue.isCrossPartitionEnabled()).isFalse(); + } + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistryTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistryTest.java index 9f00708493a2..cea0decb5f03 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistryTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistryTest.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; -import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.subscription.channel.subscription.ISubscriptionDeliveryChannelNamer; import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 5f47b22ceaf8..869bcba4a949 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser; import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchDeliverer; import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber; @@ -33,6 +34,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; import java.util.List; @@ -55,8 +57,12 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscribableChannelDstu3Test { private final IFhirResourceDao myMockSubscriptionDao = Mockito.mock(IFhirResourceDao.class); + @Autowired + private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + @BeforeEach public void beforeEach() { + when(myRequestPartitionHelperSvc.isDefaultPartition(any(RequestPartitionId.class))).thenReturn(Boolean.TRUE); when(myMockSubscriptionDao.getResourceType()).thenReturn(Subscription.class); myDaoRegistry.register(myMockSubscriptionDao); } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/websocket/WebsocketConnectionValidatorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/websocket/WebsocketConnectionValidatorTest.java index faa22afb775d..79fb1e05a17e 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/websocket/WebsocketConnectionValidatorTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/websocket/WebsocketConnectionValidatorTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; @@ -66,6 +67,8 @@ public class WebsocketConnectionValidatorTest { @MockBean SubscriptionRegistry mySubscriptionRegistry; @MockBean + IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + @MockBean ISearchParamRegistry mySearchParamRegistry; @MockBean SubscriptionSettings mySubscriptionSettings; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java index 87f50e1b56d9..0ebcdb8f86ab 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Pointcut; +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.model.config.SubscriptionSettings; @@ -22,6 +23,8 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.subscription.SubscriptionConstants; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.HapiExtensions; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4b.model.CanonicalType; @@ -51,6 +54,7 @@ import java.util.List; import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -194,18 +198,37 @@ public void testMalformedEndpoint(IBaseResource theSubscription) { mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null); } + @Test + public void testCreateSubscription_whenCreatedOnNonDefaultPartition_willFail() { + final Subscription subscription = createSubscription(); + ExtensionUtil.addExtension(myFhirContext, subscription, HapiExtensions.EXTENSION_SUBSCRIPTION_CROSS_PARTITION, "boolean", Boolean.TRUE); + + when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true); + when(myRequestPartitionHelperSvc.isDefaultPartition(any())).thenReturn(false); + + RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(1); + + try { + mySubscriptionValidatingInterceptor.resourcePreCreate(subscription, null, requestPartitionId); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals(Msg.code(2010) + "Cross partition subscription must be created on the default partition", e.getMessage()); + } + } + @Test public void testSubscriptionUpdate() { final Subscription subscription = createSubscription(); - // Assert there is no Exception thrown here. - mySubscriptionValidatingInterceptor.resourceUpdated(subscription, subscription, null, null); + assertThatNoException().isThrownBy(() -> mySubscriptionValidatingInterceptor.resourceUpdated(subscription, subscription, null, null)); } @Test public void testInvalidPointcut() { + final Subscription subscription = createSubscription(); + try { - mySubscriptionValidatingInterceptor.validateSubmittedSubscription(createSubscription(), null, null, Pointcut.TEST_RB); + mySubscriptionValidatingInterceptor.validateSubmittedSubscription(subscription, null, null, Pointcut.TEST_RB); fail(""); } catch (UnprocessableEntityException e) { assertEquals(Msg.code(2267) + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: " + Pointcut.TEST_RB, e.getMessage()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java index c6111894b4fa..5fff4bc4654d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java @@ -20,6 +20,7 @@ import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; @@ -59,6 +60,8 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; @@ -73,6 +76,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; @@ -160,13 +164,46 @@ public void init() { * To be called for tests that require additional * setup * - * @param clazz + * @param theClazz */ - private void setup(Class clazz) { - mySvc.setResourceType(clazz); + @SuppressWarnings({"rawtypes", "unchecked"}) + private void setup(Class theClazz) { + mySvc.setResourceType(theClazz); mySvc.start(); } + @Test + @SuppressWarnings("unchecked") + public void searchForResources_withNullResourceReturns_doesNotFail() { + // setup + SearchParameterMap map = new SearchParameterMap(); + RequestDetails requestDetails = new SystemRequestDetails(); + + MockHapiTransactionService myTransactionService = new MockHapiTransactionService(); + mySvc.setTransactionService(myTransactionService); + setup(Patient.class); + List resourceList = new ArrayList<>(); + resourceList.add(null); + resourceList.add(new Patient()); + + // when + when(mySearchBuilderFactory.newSearchBuilder(eq("Patient"), eq(Patient.class))) + .thenReturn(myISearchBuilder); + when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(eq(requestDetails), eq("Patient"), eq(map))) + .thenReturn(RequestPartitionId.allPartitions()); + when(myISearchBuilder.createQueryStream(any(SearchParameterMap.class), any(SearchRuntimeDetails.class), any(RequestDetails.class), any(RequestPartitionId.class))) + .thenReturn(Stream.of(JpaPid.fromId(1L), JpaPid.fromId(2L))); + when(myISearchBuilder.loadResourcesByPid(any(Collection.class), any(RequestDetails.class))) + .thenReturn(resourceList); + + // test + List resources = mySvc.searchForResources(map, requestDetails); + + // verify + // list of 2 in (null, resource), list of 1 out (only resource) + assertEquals(1, resources.size()); + } + @Test public void validateResourceIdCreation_asSystem() { Patient patient = new Patient(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index b928c6ecf2bb..3bd0f92daa4d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -126,6 +126,7 @@ import static org.apache.commons.lang3.StringUtils.countMatches; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -216,6 +217,31 @@ public void before() throws Exception { myReindexTestHelper = new ReindexTestHelper(myFhirContext, myDaoRegistry, mySearchParamRegistry); } + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void syncDatabaseToCache_elasticSearchOrJPA_shouldNotFail(boolean theUseElasticSearch) throws Exception { + // setup + if (theUseElasticSearch) { + myStorageSettings.setStoreResourceInHSearchIndex(true); + myStorageSettings.setHibernateSearchIndexSearchParams(true); + } + // only 1 retry so this test doesn't take forever + mySubscriptionLoader.setMaxRetries(1); + + // create a single subscription + String payload = "application/fhir+json"; + Subscription subscription = createSubscription("Patient?", payload, ourServer.getBaseUrl(), null); + mySubscriptionDao.create(subscription, mySrd); + + // test + assertDoesNotThrow(() -> { + mySubscriptionLoader.doSyncResourcesForUnitTest(); + }); + + // reset + mySubscriptionLoader.setMaxRetries(null); + } + /** * See the class javadoc before changing the counts in this test! */ @@ -3448,8 +3474,6 @@ public void testTransaction_ComboParamIndexesInUse_NoPreCheck() { assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); - - } /** diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 64ba2efa2d65..41822721c4a9 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -60,6 +60,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.IdType; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/delete/DeleteConflictServiceR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/delete/DeleteConflictServiceR4Test.java index cbb90a688fd3..c15a7476ffcf 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/delete/DeleteConflictServiceR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/delete/DeleteConflictServiceR4Test.java @@ -1,40 +1,49 @@ package ca.uhn.fhir.jpa.delete; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DeleteConflict; import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; +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.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class DeleteConflictServiceR4Test extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictServiceR4Test.class); - private final DeleteConflictInterceptor myDeleteInterceptor = new DeleteConflictInterceptor(); private int myInterceptorDeleteCount; @@ -204,6 +213,139 @@ public void testBadInterceptorNoInfiniteLoop() { assertEquals(1 + DeleteConflictService.MAX_RETRY_ATTEMPTS, myDeleteInterceptor.myCallCount); } + private void setupResourceReferenceTests() { + // we don't need this + myInterceptorRegistry.unregisterInterceptor(myDeleteInterceptor); + + // we have to allow versioned references; by default we do not + myFhirContext.getParserOptions() + .setStripVersionsFromReferences(false); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void delete_resourceReferencedByVersionedReferenceOnly_succeeds(boolean theUpdateObs) { + // setup + SystemRequestDetails requestDetails = new SystemRequestDetails(); + DaoMethodOutcome outcome; + + setupResourceReferenceTests(); + + Observation observation = new Observation(); + observation.setStatus(Observation.ObservationStatus.FINAL); + outcome = myObservationDao.create(observation, requestDetails); + IIdType obsId = outcome.getId(); + + // create encounter that references Observation by versioned reference only + Encounter encounter = new Encounter(); + encounter.setStatus(Encounter.EncounterStatus.FINISHED); + Coding coding = new Coding(); + coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode"); + coding.setCode("AMB"); + encounter.setClass_(coding); + encounter.addReasonReference() + .setReference(obsId.getValue()); // versioned reference + outcome = myEncounterDao.create(encounter, requestDetails); + assertTrue(outcome.getCreated()); + + if (theUpdateObs) { + observation.setId(obsId.toUnqualifiedVersionless()); + observation.setIssued(new Date()); + myObservationDao.update(observation, requestDetails); + } + + // test + outcome = myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails); + + // validate + assertTrue(outcome.getOperationOutcome() instanceof OperationOutcome); + OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome(); + assertFalse(oo.getIssue().isEmpty()); + assertTrue(oo.getIssue().stream().anyMatch(i -> i.getDiagnostics().contains("Successfully deleted 1 resource(s)"))); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void delete_resourceReferencedByNonVersionReferenceOnly_fails(boolean theUpdateObservation) { + // setup + SystemRequestDetails requestDetails = new SystemRequestDetails(); + DaoMethodOutcome outcome; + + setupResourceReferenceTests(); + + Observation observation = new Observation(); + observation.setStatus(Observation.ObservationStatus.FINAL); + outcome = myObservationDao.create(observation, requestDetails); + IIdType obsId = outcome.getId(); + + // create encounter that references Observation by versioned reference only + Encounter encounter = new Encounter(); + encounter.setStatus(Encounter.EncounterStatus.FINISHED); + Coding coding = new Coding(); + coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode"); + coding.setCode("AMB"); + encounter.setClass_(coding); + encounter.addReasonReference() + .setReference(obsId.toUnqualifiedVersionless().getValue()); // versionless reference + outcome = myEncounterDao.create(encounter, requestDetails); + assertTrue(outcome.getCreated()); + + if (theUpdateObservation) { + observation.setId(obsId.toUnqualifiedVersionless()); + observation.setIssued(new Date()); + myObservationDao.update(observation, requestDetails); + } + + // test + try { + myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails); + fail("Deletion of resource referenced by versionless id should fail."); + } catch (ResourceVersionConflictException ex) { + assertTrue(ex.getLocalizedMessage().contains("Unable to delete") + && ex.getLocalizedMessage().contains("at least one resource has a reference to this resource"), + ex.getLocalizedMessage()); + } + } + + @Test + public void delete_resourceReferencedByNonVersionedAndVersionedReferences_fails() { + // setup + SystemRequestDetails requestDetails = new SystemRequestDetails(); + DaoMethodOutcome outcome; + + setupResourceReferenceTests(); + + Observation observation = new Observation(); + observation.setStatus(Observation.ObservationStatus.FINAL); + outcome = myObservationDao.create(observation, requestDetails); + IIdType obsId = outcome.getId(); + + // create 2 encounters; one referenced with a versionless id, one referenced with a versioned id + for (String id : new String[] { obsId.getValue(), obsId.toUnqualifiedVersionless().getValue() }) { + Encounter encounter = new Encounter(); + encounter.setStatus(Encounter.EncounterStatus.FINISHED); + Coding coding = new Coding(); + coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode"); + coding.setCode("AMB"); + encounter.setClass_(coding); + encounter.addReasonReference() + .setReference(id); + outcome = myEncounterDao.create(encounter, requestDetails); + assertTrue(outcome.getCreated()); + } + + // test + try { + // versionless delete + myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails); + fail("Should not be able to delete observations referenced by versionless id because it is referenced by version AND by versionless."); + } catch (ResourceVersionConflictException ex) { + assertTrue(ex.getLocalizedMessage().contains("Unable to delete") + && ex.getLocalizedMessage().contains("at least one resource has a reference to this resource"), + ex.getLocalizedMessage()); + } + } + @Test public void testNoDuplicateConstraintReferences() { Patient patient = new Patient(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java index 9f65a4f99931..6ce237302d70 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java @@ -1,5 +1,9 @@ package ca.uhn.fhir.jpa.interceptor; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; + +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; + import static org.junit.jupiter.api.Assertions.assertEquals; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; @@ -11,10 +15,10 @@ import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; public class PatientCompartmentEnforcingInterceptorTest extends BaseResourceProviderR4Test { @@ -56,11 +60,14 @@ public void after() throws Exception { myPartitionSettings.setUnnamedPartitionMode(defaultPartitionSettings.isUnnamedPartitionMode()); myPartitionSettings.setDefaultPartitionId(defaultPartitionSettings.getDefaultPartitionId()); myPartitionSettings.setAllowReferencesAcrossPartitions(defaultPartitionSettings.getAllowReferencesAcrossPartitions()); - } + myStorageSettings.setMassIngestionMode(false); + } - @Test - public void testUpdateResource_whenCrossingPatientCompartment_throws() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testUpdateResource_whenCrossingPatientCompartment_throws(boolean theMassIngestionEnabled) { + myStorageSettings.setMassIngestionMode(theMassIngestionEnabled); myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED); createPatientA(); createPatientB(); @@ -76,8 +83,10 @@ public void testUpdateResource_whenCrossingPatientCompartment_throws() { assertEquals("HAPI-2476: Resource compartment changed. Was a referenced Patient changed?", thrown.getMessage()); } - @Test - public void testUpdateResource_whenNotCrossingPatientCompartment_allows() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testUpdateResource_whenNotCrossingPatientCompartment_allows(boolean theMassIngestionEnabled) { + myStorageSettings.setMassIngestionMode(theMassIngestionEnabled); createPatientA(); Observation obs = new Observation(); @@ -87,7 +96,8 @@ public void testUpdateResource_whenNotCrossingPatientCompartment_allows() { obs.getNote().add(new Annotation().setText("some text")); obs.setStatus(Observation.ObservationStatus.CORRECTED); - myObservationDao.update(obs, new SystemRequestDetails()); + DaoMethodOutcome outcome = myObservationDao.update(obs, new SystemRequestDetails()); + assertEquals("Patient/A", ((Observation) outcome.getResource()).getSubject().getReference()); } private void createPatientA() { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvcTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvcTest.java index 4775c50cf512..481370077d1e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvcTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvcTest.java @@ -7,17 +7,19 @@ import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Patient; +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.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -55,6 +57,11 @@ public void before(){ myPatient.setId(new IdType("Patient", "123", "1")); } + @AfterEach + public void afterEach(){ + myPartitionSettings.setDefaultPartitionId(null); + } + @Test public void testDetermineReadPartitionForSystemRequest_withPartitionIdOnly_returnsCorrectPartition() { // setup @@ -182,6 +189,27 @@ public void testValidateAndNormalizePartitionNames_withNameAndInvalidId_throwsEx } } + @ParameterizedTest + @MethodSource + public void testDefaultPartition_whenDefaultPartitionIsNotNull(Integer theRequestPartitionId) { + final Integer defaultPartitionId = 0; + myPartitionSettings.setDefaultPartitionId(defaultPartitionId); + + { + RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(defaultPartitionId); + assertThat(mySvc.isDefaultPartition(requestPartitionId)).isTrue(); + } + + { + RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(theRequestPartitionId); + assertThat(mySvc.isDefaultPartition(requestPartitionId)).isFalse(); + } + } + + private static Stream testDefaultPartition_whenDefaultPartitionIsNotNull(){ + return Stream.of(null, 1,2,3); + } + private PartitionEntity createPartition1() { return myPartitionDao.save(new PartitionEntity().setId(PARTITION_ID_1).setName(PARTITION_NAME_1)); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/patch/FhirPatchApplyR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/patch/FhirPatchApplyR4Test.java index 9941e0f657ad..1ded9b1e3082 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/patch/FhirPatchApplyR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/patch/FhirPatchApplyR4Test.java @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FhirPatchApplyR4Test { @@ -456,6 +457,117 @@ public void testAddToHighCardinalityFieldSetsValueIfEmpty() { assertEquals("third-system", patient.getIdentifier().get(0).getSystem()); assertEquals("third-value", patient.getIdentifier().get(0).getValue()); } + + + @Test + public void testReplaceAnElementInHighCardinalityFieldByIndex() { + FhirPatch svc = new FhirPatch(ourCtx); + Patient patient = new Patient(); + patient.addIdentifier().setSystem("first-system").setValue("first-value"); + patient.addIdentifier().setSystem("second-system").setValue("second-value"); + + //Given: We create a patch to replace the second identifier + Identifier theValue = new Identifier().setSystem("third-system").setValue("third-value"); + Parameters patch = new Parameters(); + patch.addParameter(createPatchReplaceOperation("Patient.identifier[1]", theValue)); + + //When: We apply the patch + svc.apply(patient, patch); + ourLog.debug("Outcome:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + + //Then: it replaces the identifier correctly. + assertThat(patient.getIdentifier()).hasSize(2); + assertThat(patient.getIdentifier().get(0).getSystem()).isEqualTo("first-system"); + assertThat(patient.getIdentifier().get(0).getValue()).isEqualTo("first-value"); + assertThat(patient.getIdentifier().get(1).getSystem()).isEqualTo("third-system"); + assertThat(patient.getIdentifier().get(1).getValue()).isEqualTo("third-value"); + } + + @ParameterizedTest + @CsvSource({ + "Patient.identifier.where(system='not-an-existing-system')", //filter for not existing element + "Patient.identifier[5]" // index out of bounds for the element + }) + public void testReplaceAnElementInHighCardinalityField_NoMatchingElement_InvalidRequest(String thePath) { + FhirPatch svc = new FhirPatch(ourCtx); + Patient patient = new Patient(); + patient.addIdentifier().setSystem("first-system").setValue("first-value"); + patient.addIdentifier().setSystem("second-system").setValue("second-value"); + + //Given: We create a patch to replace the second identifier + Identifier theValue = new Identifier().setSystem("third-system").setValue("third-value"); + Parameters patch = new Parameters(); + patch.addParameter(createPatchReplaceOperation(thePath, theValue)); + + //When: We apply the patch, expect an InvalidRequestException + InvalidRequestException ex = assertThrows(InvalidRequestException.class, () -> svc.apply(patient, patch)); + String expectedMessage = String.format("HAPI-2617: No element matches the specified path: %s", thePath); + assertThat(ex.getMessage()).isEqualTo(expectedMessage); + + + assertThat(patient.getIdentifier()).hasSize(2); + assertThat(patient.getIdentifier().get(0).getSystem()).isEqualTo("first-system"); + assertThat(patient.getIdentifier().get(0).getValue()).isEqualTo("first-value"); + assertThat(patient.getIdentifier().get(1).getSystem()).isEqualTo("second-system"); + assertThat(patient.getIdentifier().get(1).getValue()).isEqualTo("second-value"); + } + + + @Test + public void testReplaceAnElementInHighCardinalityFieldByFilter_SingleMatch() { + FhirPatch svc = new FhirPatch(ourCtx); + Patient patient = new Patient(); + patient.addIdentifier().setSystem("first-system").setValue("first-value"); + patient.addIdentifier().setSystem("second-system").setValue("second-value"); + + //Given: We create a patch to replace the second identifier + Identifier theValue = new Identifier().setSystem("third-system").setValue("third-value"); + Parameters patch = new Parameters(); + patch.addParameter(createPatchReplaceOperation("Patient.identifier.where(system='second-system')", + theValue)); + + //When: We apply the patch + svc.apply(patient, patch); + ourLog.debug("Outcome:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + + //Then: it replaces the identifier correctly. + assertThat(patient.getIdentifier()).hasSize(2); + assertThat(patient.getIdentifier().get(0).getSystem()).isEqualTo("first-system"); + assertThat(patient.getIdentifier().get(0).getValue()).isEqualTo("first-value"); + assertThat(patient.getIdentifier().get(1).getSystem()).isEqualTo("third-system"); + assertThat(patient.getIdentifier().get(1).getValue()).isEqualTo("third-value"); + } + + @Test + public void testReplaceElementsInHighCardinalityFieldByFilter_MultipleMatches() { + FhirPatch svc = new FhirPatch(ourCtx); + Patient patient = new Patient(); + patient.addIdentifier().setSystem("existing-system1").setValue("first-value"); + patient.addIdentifier().setSystem("to-be-replaced-system").setValue("second-value"); + patient.addIdentifier().setSystem("existing-system2").setValue("third-value"); + patient.addIdentifier().setSystem("to-be-replaced-system").setValue("fourth-value"); + //Given: We create a patch to replace the second identifier + Identifier theValue = new Identifier().setSystem("new-system").setValue("new-value"); + Parameters patch = new Parameters(); + patch.addParameter(createPatchReplaceOperation("Patient.identifier.where(system='to-be-replaced-system')", + theValue)); + + //When: We apply the patch + svc.apply(patient, patch); + ourLog.debug("Outcome:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + + //Then: it replaces identifiers correctly. + assertThat(patient.getIdentifier()).hasSize(4); + assertThat(patient.getIdentifier().get(0).getSystem()).isEqualTo("existing-system1"); + assertThat(patient.getIdentifier().get(0).getValue()).isEqualTo("first-value"); + assertThat(patient.getIdentifier().get(1).getSystem()).isEqualTo("new-system"); + assertThat(patient.getIdentifier().get(1).getValue()).isEqualTo("new-value"); + assertThat(patient.getIdentifier().get(2).getSystem()).isEqualTo("existing-system2"); + assertThat(patient.getIdentifier().get(2).getValue()).isEqualTo("third-value"); + assertThat(patient.getIdentifier().get(3).getSystem()).isEqualTo("new-system"); + assertThat(patient.getIdentifier().get(3).getValue()).isEqualTo("new-value"); + } + @Test public void testReplaceToHighCardinalityFieldRemovesAllAndSetsValue() { FhirPatch svc = new FhirPatch(ourCtx); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 92b140ac95cc..701c0135697c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -4,11 +4,16 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import jakarta.servlet.http.HttpServletResponse; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Provenance; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; @@ -16,9 +21,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import java.util.List; +import java.util.Set; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; @@ -29,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class ReplaceReferencesR4Test extends BaseResourceProviderR4Test { @@ -159,7 +167,127 @@ void testReplaceReferencesSmallTransactionEntriesSize() { myTestHelper.assertAllReferencesUpdated(); } - // TODO ED we should add some tests for the invalid request error cases (and assert 4xx status code) + @ParameterizedTest + @ValueSource(strings = {""}) + @NullSource + void testReplaceReferences_MissingSourceId_ThrowsInvalidRequestException(String theSourceId) { + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> { + myTestHelper.callReplaceReferencesWithResourceLimit(myClient, theSourceId, "target-id", false, null); + }); + assertThat(exception.getMessage()).contains("HAPI-2583: Parameter 'source-reference-id' is blank"); + } + + @ParameterizedTest + @ValueSource(strings = {""}) + @NullSource + void testReplaceReferences_MissingTargetId_ThrowsInvalidRequestException(String theTargetId) { + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> { + myTestHelper.callReplaceReferencesWithResourceLimit(myClient, "source-id", theTargetId, false, null); + }); + assertThat(exception.getMessage()).contains("HAPI-2584: Parameter 'target-reference-id' is blank"); + } + + + @Test + void testReplaceReferences_WhenReplacingAHighCardinalityReferenceElement_OnlyReplacesMatchingReferences() { + //This test uses an Observation resource with multiple Practitioner references in the 'performer' element. + // Create Practitioners + IIdType practitionerId1 = myClient.create().resource(new Practitioner()).execute().getId().toUnqualifiedVersionless(); + IIdType practitionerId2 = myClient.create().resource(new Practitioner()).execute().getId().toUnqualifiedVersionless(); + IIdType practitionerId3 = myClient.create().resource(new Practitioner()).execute().getId().toUnqualifiedVersionless(); + + // Create observation with references in the performer field + IIdType observationId = createObservationWithPerformers(practitionerId1, practitionerId2).toUnqualifiedVersionless(); + + // Call $replace-references operation to replace practitionerId1 with practitionerId3 + Parameters outParams = myTestHelper.callReplaceReferencesWithResourceLimit(myClient, + practitionerId1.toString(), + practitionerId3.toString(), + false, + null); + + // Assert operation outcome + Bundle patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); + + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, + 1, List.of( + "Observation")); + + // Fetch and validate updated observation + Observation updatedObservation = myClient + .read() + .resource(Observation.class) + .withId(observationId) + .execute(); + + // Extract the performer references from the updated Observation + List actualPerformerIds = updatedObservation.getPerformer().stream() + .map(ref -> ref.getReferenceElement().toString()) + .toList(); + + // Assert that the performer references match the expected values + assertThat(actualPerformerIds).containsExactly(practitionerId3.toString(), practitionerId2.toString()); + } + + @Test + void testReplaceReferences_ShouldNotReplaceVersionedReferences() { + // this configuration makes preserve versioned references in the Provenance.target + // so that we can test that the versioned reference was not replaced + // but keep a copy of the original configuration to restore it after the test + Set originalNotStrippedPaths = + myFhirContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths(); + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths("Provenance.target"); + try { + + IIdType practitionerId1 = myClient.create().resource(new Practitioner()).execute().getId().toUnqualified(); + IIdType practitionerId2 = myClient.create().resource(new Practitioner()).execute().getId().toUnqualified(); + + Provenance provenance = new Provenance(); + provenance.addTarget(new Reference(practitionerId1)); + IIdType provenanceId = myClient.create().resource(provenance).execute().getId(); + // Call $replace-references operation to replace practitionerId1 with practitionerId3 + myTestHelper.callReplaceReferencesWithResourceLimit(myClient, + practitionerId1.toVersionless().toString(), + practitionerId2.toVersionless().toString(), + false, + null); + + // Fetch and validate the provenance + Provenance provenanceAfterOperation = myClient + .read() + .resource(Provenance.class) + .withId(provenanceId.toUnqualifiedVersionless()) + .execute(); + + // Extract the target references from the Provenance + List actualTargetIds = provenanceAfterOperation.getTarget().stream() + .map(ref -> ref.getReferenceElement().toString()) + .toList(); + + // Assert that the versioned reference in the Provenance was not replaced + assertThat(actualTargetIds).containsExactly(practitionerId1.toString()); + + } finally { + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(originalNotStrippedPaths); + } + } + + private IIdType createObservationWithPerformers(IIdType... performerIds) { + // Create a new Observation resource + Observation observation = new Observation(); + + // Add references to performers + for (IIdType performerId : performerIds) { + observation.addPerformer(new Reference(performerId.toUnqualifiedVersionless())); + } + + // Store the observation resource via the FHIR client + return myClient.create().resource(observation).execute().getId(); + + } + + + @Override protected boolean verboseClientLogging() { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 0dbc0dfc1bec..f6d082a73dc8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.model.dao.JpaPidFk; @@ -19,6 +20,7 @@ import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.submit.interceptor.SearchParamValidatingInterceptor; import ca.uhn.fhir.jpa.term.ZipCollectionBuilder; import ca.uhn.fhir.jpa.test.config.TestR4Config; import ca.uhn.fhir.jpa.util.MemoryCacheService; @@ -62,6 +64,8 @@ import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.test.util.LogbackTestExtension; +import ca.uhn.test.util.LogbackTestExtensionAssert; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import jakarta.annotation.Nonnull; @@ -170,16 +174,21 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import ch. qos. logback. classic.Level; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.AopTestUtils; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.client.apache.ResourceEntity; + import java.io.BufferedReader; import java.io.IOException; @@ -222,6 +231,9 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { @Autowired private ISearchDao mySearchEntityDao; + @RegisterExtension + public LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension(SearchParamValidatingInterceptor.class, Level.WARN); + @Override @AfterEach public void after() throws Exception { @@ -264,7 +276,31 @@ public void before() throws Exception { myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds()); } + @Test + public void testSearchParameterValidation() { + // setup + SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.getUserData().put(SearchParamValidatingInterceptor.SKIP_VALIDATION, true); + + SearchParameter sp = new SearchParameter(); + sp.setUrl("http://example.com/name"); + sp.setId("name"); + sp.setCode("name"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.setStatus(Enumerations.PublicationStatus.RETIRED); + sp.addBase("Patient"); + sp.setExpression("Patient.name"); + + // test + DaoMethodOutcome outcome = mySearchParameterDao.update(sp, requestDetails); + myCaptureQueriesListener.clear(); + sp.setId(outcome.getId()); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.update(sp, requestDetails); + + LogbackTestExtensionAssert.assertThat(myLogbackTestExtension).hasWarnMessage("Skipping validation of submitted SearchParameter because " + SearchParamValidatingInterceptor.SKIP_VALIDATION + " flag is true"); + } @Test public void testParameterWithNoValueThrowsError_InvalidChainOnCustomSearch() throws IOException { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java index 99180105bde9..36a122d3bc64 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java @@ -356,6 +356,37 @@ public void testApplyDeltaAdd_UsingCodeSystem() { ); } + @Test + public void testApplyDeltaAdd_UsingCodeSystemWithElasticSearch() { + //Given: Advance HSearch indexing is enabled + myStorageSettings.setHibernateSearchIndexFullText(true); + myStorageSettings.setHibernateSearchIndexSearchParams(true); + myStorageSettings.setStoreResourceInHSearchIndex(true); + + //Given: We have a non-existent code system + CodeSystem codeSystem = new CodeSystem(); + myClient.create().resource(codeSystem).execute(); + CodeSystem.ConceptDefinitionComponent chem = codeSystem.addConcept().setCode("CHEM").setDisplay("Chemistry"); + chem.addConcept().setCode("HB").setDisplay("Hemoglobin"); + chem.addConcept().setCode("NEUT").setDisplay("Neutrophils"); + CodeSystem.ConceptDefinitionComponent micro = codeSystem.addConcept().setCode("MICRO").setDisplay("Microbiology"); + micro.addConcept().setCode("C&S").setDisplay("Culture And Sensitivity"); + + //Execute + Parameters outcome = myClient + .operation() + .onType(CodeSystem.class) + .named(JpaConstants.OPERATION_APPLY_CODESYSTEM_DELTA_ADD) + .withParameter(Parameters.class, TerminologyUploaderProvider.PARAM_SYSTEM, new UriType("http://example.com/cs")) + .andParameter(TerminologyUploaderProvider.PARAM_CODESYSTEM, codeSystem) + .prettyPrint() + .execute(); + + //Validate + IntegerType conceptCount = (IntegerType) outcome.getParameter("conceptCount").getValue(); + assertThat(conceptCount.getValue()).isEqualTo(5); + } + @Test public void testApplyDeltaAdd_UsingCodeSystemWithConceptProprieties() { CodeSystem codeSystem = new CodeSystem(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java index bc64093cbc5e..cface957c2f8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionValidatingInterceptorTest.java @@ -35,6 +35,7 @@ import static ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator.IEndpointUrlValidationStrategy; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -108,7 +109,7 @@ public void testValidate_RestHook_Populated() { subscription.getChannel().setPayload("application/fhir+json"); subscription.getChannel().setEndpoint("http://foo"); - mySvc.resourcePreCreate(subscription, null, null); + assertThatNoException().isThrownBy(() -> mySvc.resourcePreCreate(subscription, null, null)); } @Test @@ -196,7 +197,7 @@ public void testValidate_RestHook_NoPayload() { subscription.getChannel().setType(Subscription.SubscriptionChannelType.RESTHOOK); subscription.getChannel().setEndpoint("http://foo"); - mySvc.resourcePreCreate(subscription, null, null); + assertThatNoException().isThrownBy(() -> mySvc.resourcePreCreate(subscription, null, null)); } @Test @@ -220,6 +221,7 @@ public void testValidate_Cross_Partition_Subscription() { when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true); when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true); when(myRequestPartitionHelperSvc.determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class), eq("Subscription"))).thenReturn(RequestPartitionId.defaultPartition()); + when(myRequestPartitionHelperSvc.isDefaultPartition(any())).thenReturn(true); Subscription subscription = new Subscription(); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/dbpartitionmode/TestDefinitions.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/dbpartitionmode/TestDefinitions.java index 14a2f6bc765d..7effae6f49e8 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/dbpartitionmode/TestDefinitions.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/dbpartitionmode/TestDefinitions.java @@ -57,9 +57,12 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.update.UpdateSet; import org.assertj.core.api.Assertions; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r5.model.ConceptMap; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.DateTimeType; @@ -102,6 +105,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hl7.fhir.instance.model.api.IAnyResource.SP_RES_ID; 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.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -133,10 +137,12 @@ abstract class TestDefinitions implements ITestDataBuilder { @Autowired private IFhirResourceDaoPatient myPatientDao; @Autowired - private IFhirResourceDaoObservation myObservationDao; - @Autowired private IFhirResourceDao myCodeSystemDao; @Autowired + private IFhirResourceDao myConceptMapDao; + @Autowired + private IFhirResourceDaoObservation myObservationDao; + @Autowired private IFhirResourceDao myValueSetDao; @Autowired private IFhirResourceDao myEncounterDao; @@ -289,6 +295,92 @@ public void testCreate_Conditional() throws JSQLParserException { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); } + + @Test + public void testCreate_ConceptMap() throws JSQLParserException { + ConceptMap cm = new ConceptMap(); + cm.setId("cm"); + cm.setStatus(Enumerations.PublicationStatus.ACTIVE); + cm.setUrl("http://example.com/cm"); + ConceptMap.ConceptMapGroupComponent group = cm.addGroup(); + group.setSource("http://source"); + group.setTarget("http://target"); + ConceptMap.SourceElementComponent code0 = group.addElement().setCode("code0").setDisplay("display0"); + code0.addTarget().setCode("target0").setDisplay("target0display0"); + + // Test + myCaptureQueriesListener.clear(); + myConceptMapDao.update(cm, new SystemRequestDetails()); + + // Verify + myCaptureQueriesListener.logInsertQueries(); + + String expectedPartitionId = "NULL"; + if (myPartitionSettings.isPartitioningEnabled()) { + if (myPartitionSettings.getDefaultPartitionId() != null) { + expectedPartitionId = "'" + myPartitionSettings.getDefaultPartitionId() + "'"; + } + } + + List insertConceptMaps = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CONCEPT_MAP ")); + assertEquals(1, insertConceptMaps.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertConceptMaps.get(0).getSql(true, false)).get("PARTITION_ID")); + + List insertConceptMapGroups = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CONCEPT_MAP_GROUP ")); + assertEquals(1, insertConceptMapGroups.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertConceptMapGroups.get(0).getSql(true, false)).get("PARTITION_ID")); + + List insertConceptMapGroupElements = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CONCEPT_MAP_GRP_ELEMENT ")); + assertEquals(1, insertConceptMapGroupElements.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertConceptMapGroupElements.get(0).getSql(true, false)).get("PARTITION_ID")); + + List insertConceptMapGroupElementTargets = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CONCEPT_MAP_GRP_ELM_TGT ")); + assertEquals(1, insertConceptMapGroupElementTargets.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertConceptMapGroupElementTargets.get(0).getSql(true, false)).get("PARTITION_ID")); + } + + @Test + public void testCreate_CodeSystem() throws JSQLParserException { + CodeSystem cs = new CodeSystem(); + cs.setId("cs"); + cs.setStatus(Enumerations.PublicationStatus.ACTIVE); + cs.setUrl("http://example.com/cs"); + cs.addConcept().setCode("code0").setDisplay("display0"); + + // Test + myCaptureQueriesListener.clear(); + myCodeSystemDao.update(cs, new SystemRequestDetails()); + + // Verify + myCaptureQueriesListener.logInsertQueries(); + + String expectedPartitionId = "NULL"; + if (myPartitionSettings.isPartitioningEnabled()) { + if (myPartitionSettings.getDefaultPartitionId() != null) { + expectedPartitionId = "'" + myPartitionSettings.getDefaultPartitionId() + "'"; + } + } + + List insertTrmCodeSystem = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CODESYSTEM ")); + assertEquals(1, insertTrmCodeSystem.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertTrmCodeSystem.get(0).getSql(true, false)).get("PARTITION_ID")); + assertEquals("NULL", parseInsertStatementParams(insertTrmCodeSystem.get(0).getSql(true, false)).get("CURRENT_VERSION_PID")); + assertEquals("NULL", parseInsertStatementParams(insertTrmCodeSystem.get(0).getSql(true, false)).get("CURRENT_VERSION_PARTITION_ID")); + + List insertTrmConcept = myCaptureQueriesListener.getInsertQueries(t -> t.getSql(true, false).startsWith("insert into TRM_CONCEPT ")); + assertEquals(1, insertTrmConcept.size()); + assertEquals(expectedPartitionId, parseInsertStatementParams(insertTrmConcept.get(0).getSql(true, false)).get("PARTITION_ID")); + + myCaptureQueriesListener.logUpdateQueries(); + List updateCodeSystems = myCaptureQueriesListener.getUpdateQueries(t -> t.getSql(true, false).startsWith("update TRM_CODESYSTEM ")); + assertEquals(1, updateCodeSystems.size()); + assertEquals(expectedPartitionId, parseUpdateStatementParams(updateCodeSystems.get(0).getSql(true, false)).get("CURRENT_VERSION_PARTITION_ID")); + + List updateCodeSystemVersions = myCaptureQueriesListener.getUpdateQueries(t -> t.getSql(true, false).startsWith("update TRM_CODESYSTEM_VER ")); + assertEquals(1, updateCodeSystemVersions.size()); + } + + @ParameterizedTest @EnumSource(PartitionSettings.CrossPartitionReferenceMode.class) public void testCreate_ReferenceToResourceInOtherPartition(PartitionSettings.CrossPartitionReferenceMode theAllowReferencesToCrossPartition) { @@ -1778,12 +1870,30 @@ private static Map parseInsertStatementParams(String theInsertSq for (int i = 0; i < parsedStatement.getColumns().size(); i++) { String columnName = parsedStatement.getColumns().get(i).getColumnName(); String columnValue = parsedStatement.getValues().getExpressions().get(i).toString(); + assertFalse(retVal.containsKey(columnName), ()->"Duplicate column in insert statement: " + columnName); retVal.put(columnName, columnValue); } return retVal; } + private static Map parseUpdateStatementParams(String theUpdateSql) throws JSQLParserException { + Update parsedStatement = (Update) CCJSqlParserUtil.parse(theUpdateSql); + + Map retVal = new HashMap<>(); + + for (UpdateSet updateSet : parsedStatement.getUpdateSets()) { + for (int i = 0; i < updateSet.getColumns().size(); i++) { + String columnName = updateSet.getColumns().get(i).getColumnName(); + String columnValue = updateSet.getValues().getExpressions().get(i).toString(); + assertFalse(retVal.containsKey(columnName), ()->"Duplicate column in insert statement: " + columnName); + retVal.put(columnName, columnValue); + } + } + + return retVal; + } + private static String parseInsertStatementTableName(String theInsertSql) throws JSQLParserException { Insert parsedStatement = (Insert) CCJSqlParserUtil.parse(theInsertSql); return parsedStatement.getTable().getName(); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 856b092af078..49379e456c02 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -234,6 +234,20 @@ public Parameters callReplaceReferences(IGenericClient theFhirClient, boolean th public Parameters callReplaceReferencesWithResourceLimit( IGenericClient theFhirClient, boolean theIsAsync, Integer theResourceLimit) { + return callReplaceReferencesWithResourceLimit( + theFhirClient, + mySourcePatientId.getValue(), + myTargetPatientId.getValue(), + theIsAsync, + theResourceLimit); + } + + public Parameters callReplaceReferencesWithResourceLimit( + IGenericClient theFhirClient, + String theSourceId, + String theTargetId, + boolean theIsAsync, + Integer theResourceLimit) { IOperationUntypedWithInputAndPartialOutput request = theFhirClient .operation() .onServer() @@ -241,10 +255,10 @@ public Parameters callReplaceReferencesWithResourceLimit( .withParameter( Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, - new StringType(mySourcePatientId.getValue())) + new StringType(theSourceId)) .andParameter( ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, - new StringType(myTargetPatientId.getValue())); + new StringType(theTargetId)); if (theResourceLimit != null) { request.andParameter( ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT, new IntegerType(theResourceLimit)); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/H2_EMBEDDED.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/H2_EMBEDDED.sql new file mode 100644 index 000000000000..00ba125cb3e7 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/H2_EMBEDDED.sql @@ -0,0 +1,531 @@ +INSERT INTO HFJ_SEARCH ( + PID, + CREATED, + SEARCH_DELETED, + EXPIRY_OR_NULL, + FAILURE_CODE, + FAILURE_MESSAGE, + LAST_UPDATED_HIGH, + LAST_UPDATED_LOW, + NUM_BLOCKED, + NUM_FOUND, + PARTITION_ID, + PREFERRED_PAGE_SIZE, + RESOURCE_ID, + RESOURCE_TYPE, + SEARCH_PARAM_MAP, + SEARCH_PARAM_MAP_BIN, + SEARCH_QUERY_STRING, + SEARCH_QUERY_STRING_HASH, + SEARCH_QUERY_STRING_VC, + SEARCH_TYPE, + SEARCH_STATUS, + TOTAL_COUNT, + SEARCH_UUID, + OPTLOCK_VERSION +) VALUES ( + 54, + '2025-01-01 16:52:09.149', + FALSE, + '2025-01-05 15:16:26.43', + 500, + 'Failure Message', + '2025-01-05 15:17:26.43', + '2025-01-05 15:18:26.43', + 0, + 1, + 1, + 10, + 1906, + 'Patient', + 5146583, + 7368816, + 8479927, + 1212873581, + '?_lastUpdated=le2024-05-31', + 1, + 'FINISHED', + 1, + '983ace20-3244-4c22-a473-559baa4d9f6d', + 1 +); + +INSERT INTO HFJ_SEARCH_RESULT ( + PID, + SEARCH_ORDER, + RESOURCE_PARTITION_ID, + RESOURCE_PID, + SEARCH_PID +) VALUES ( + 2, + 0, + 1, + 1730, + 54 +); + +INSERT INTO MPI_LINK ( + PID, + CREATED, + EID_MATCH, + TARGET_TYPE, + LINK_SOURCE, + MATCH_RESULT, + NEW_PERSON, + PERSON_PARTITION_ID, + PERSON_PID, + SCORE, + TARGET_PARTITION_ID, + TARGET_PID, + UPDATED, + VECTOR, + VERSION, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + RULE_COUNT, + PARTITION_DATE, + PARTITION_ID +) VALUES ( + 3, + '2025-01-05 15:16:26.43', + 1, + 'Observation', + 0, + 2, + 1, + 1, + 1906, + 1.0, + 1, + 1678, + '2025-01-05 15:16:26.43', + 61, + '1', + 1, + 1906, + 1, + '2025-01-05', + 1 +); + +INSERT INTO MPI_LINK_AUD ( + PID, + REV, + REVTYPE, + PERSON_PARTITION_ID, + PERSON_PID, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + TARGET_TYPE, + RULE_COUNT, + TARGET_PARTITION_ID, + TARGET_PID, + MATCH_RESULT, + LINK_SOURCE, + VERSION, + EID_MATCH, + NEW_PERSON, + SCORE, + CREATED, + UPDATED, + VECTOR, + PARTITION_ID, + PARTITION_DATE +) VALUES ( + 2, + 1, + 0, + 1, + 1906, + 1, + 1906, + 'PATIENT', + 0, + 1, + 1357, + 2, + 0, + 1, + 0, + 1, + 1, + '2025-01-29 10:14:40.69', + '2025-01-29 10:14:41.70', + 3, + 1, + '2025-01-05' +); + +INSERT INTO NPM_PACKAGE_VER ( + PID, + PKG_AUTHOR, + AUTHOR_UPPER, + CURRENT_VERSION, + PKG_DESC, + DESC_UPPER, + FHIR_VERSION, + FHIR_VERSION_ID, + PACKAGE_ID, + PACKAGE_SIZE_BYTES, + SAVED_TIME, + UPDATED_TIME, + VERSION_ID, + PACKAGE_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'HL7 Inc', + 'HL7 INC', + TRUE, + 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', + 'DEFINITIONS (API, STRUCTURES AND TERMINOLOGIES) FOR THE R4 VERSION OF THE FHIR STANDARD', + 'R4', + '4.0.1', + 'COM.EXAMPLE.IG', + 370, + '2024-05-01 15:22:37.957', + '2024-05-01 15:22:37.959', + '1.0.2', + 1, + 1, + 1 +); + +INSERT INTO NPM_PACKAGE_VER_RES ( + PID, + CANONICAL_URL, + CANONICAL_VERSION, + FILE_DIR, + FHIR_VERSION, + FHIR_VERSION_ID, + FILE_NAME, + RES_SIZE_BYTES, + RES_TYPE, + UPDATED_TIME, + PACKVER_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'http://hl7.org/fhir/StructureDefinition/Patient', + '4.0.1', + 'PACKAGE', + 'R4', + '4.0.1', + 'TESTPATIENT.JSON', + 225, + 'PATIENT', + '2024-05-01 15:22:38.057', + 1, + 1, + 2 +); + +INSERT INTO TRM_CODESYSTEM ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODE_SYSTEM_URI, + CURRENT_VERSION_PARTITION_ID, + CURRENT_VERSION_PID, + CS_NAME, + RES_ID +) VALUES ( + 2, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG?VERSION=2.67', + 1, + 55, + 'LOINC', + 1780 +); + +INSERT INTO TRM_CODESYSTEM_VER ( + PID, + PARTITION_ID, + PARTITION_DATE, + CS_DISPLAY, + CODESYSTEM_PID, + CS_VERSION_ID, + RES_ID +) VALUES ( + 55, + 1, + '2024-05-01', + 'LOINC', + 2, + '2.67', + 1780 +); + +INSERT INTO TRM_CONCEPT ( + PID, + PARTITION_ID, + CODEVAL, + CODESYSTEM_PID, + DISPLAY, + INDEX_STATUS, + PARENT_PIDS, + PARENT_PIDS_VC, + CODE_SEQUENCE, + CONCEPT_UPDATED +) VALUES ( + 2, + 1, + 'LL100-9', + 55, + 'Radiation Rx Performed', + 1, + '1415723', + '1415723', + 3, + '2024-05-01 17:02:39.139' + ); + +INSERT INTO TRM_CONCEPT_DESIG ( + PID, + PARTITION_ID, + PARTITION_DATE, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VAL_VC, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 107, + 1, + '2024-05-01', + 'NL', + '900000000000013009', + 'SYNONYM', + 'HTTPS://LOINC.ORG', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 55, + 2 +); + +INSERT INTO TRM_CONCEPT_MAP ( + PID, + PARTITION_ID, + PARTITION_DATE, + RES_ID, + SOURCE_URL, + TARGET_URL, + URL, + VER +) VALUES ( + 55, + 1, + '2024-05-01', + 1796, + 'HTTP://LOINC.ORG', + 'HTTPS://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + '1.1' +); + +INSERT INTO TRM_CONCEPT_MAP_GROUP ( + PID, + PARTITION_ID, + PARTITION_DATE, + CONCEPT_MAP_URL, + SOURCE_URL, + SOURCE_VS, + SOURCE_VERSION, + TARGET_URL, + TARGET_VS, + TARGET_VERSION, + CONCEPT_MAP_PID +) VALUES ( + 55, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'HTTP://LOINC.ORG', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + '2.67', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/ALL-COMPOUNDS', + '1.1', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELEMENT ( + PID, + PARTITION_ID, + PARTITION_DATE, + SOURCE_CODE, + CONCEPT_MAP_URL, + SOURCE_DISPLAY, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GROUP_PID +) VALUES ( + 61, + 1, + '2024-05-01', + 'LP15942-3', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'HTTP://LOINC.ORG', + '2.67', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELM_TGT ( + PID, + PARTITION_ID, + PARTITION_DATE, + TARGET_CODE, + CONCEPT_MAP_URL, + TARGET_DISPLAY, + TARGET_EQUIVALENCE, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GRP_ELM_PID +) VALUES ( + 61, + 1, + '2024-05-01', + '1064', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'EQUAL', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + '1.1', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/PUBCHEM-ALL', + 61 +); + +INSERT INTO TRM_CONCEPT_PC_LINK ( + PID, + PARTITION_ID, + CHILD_PID, + CODESYSTEM_PID, + PARENT_PID, + REL_TYPE +) VALUES ( + 55, + 1, + 2, + 55, + 151, + 0 +); + +INSERT INTO TRM_CONCEPT_PROPERTY ( + PID, + PARTITION_ID, + PARTITION_DATE, + PROP_CODESYSTEM, + PROP_DISPLAY, + PROP_KEY, + PROP_TYPE, + PROP_VAL, + PROP_VAL_BIN, + PROP_VAL_LOB, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 155, + 1, + '2024-05-01', + 'http://loinc.org', + 'code-A', + 'CODING', + 1, + 'LP14082-9', + '\x48656c6c6f20776f726c6422', + 83006307, + 55, + 2 +); + +INSERT INTO TRM_VALUESET ( + PID, + PARTITION_ID, + PARTITION_DATE, + EXPANSION_STATUS, + EXPANDED_AT, + VSNAME, + RES_ID, + TOTAL_CONCEPT_DESIGNATIONS, + TOTAL_CONCEPTS, + URL, + VER +) VALUES ( + 61, + 1, + '2025-05-01', + 'EXPANDED', + '2025-01-04 16:09:14.488', + 'v2.0127', + 1654, + 0, + 8, + 'http://terminology.hl7.org/ValueSet/v2-0127', + '2.0.0' +); + +INSERT INTO TRM_VALUESET_CONCEPT ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODEVAL, + DISPLAY, + INDEX_STATUS, + VALUESET_ORDER, + SOURCE_DIRECT_PARENT_PIDS, + SOURCE_DIRECT_PARENT_PIDS_VC, + SOURCE_PID, + SYSTEM_URL, + SYSTEM_VER, + VALUESET_PID +) VALUES ( + 2, + 1, + '2025-05-01', + 'LA4281-7', + 'Boost Rad at this Facility', + 1, + 3, + '1415722', + '1415722', + 1, + 'HTTP://LOINC.ORG', + 'V2.67', + 59 +); + +INSERT INTO TRM_VALUESET_C_DESIGNATION ( + PID, + PARTITION_ID, + PARTITION_DATE, + VALUESET_CONCEPT_PID, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VALUESET_PID +) VALUES ( + 5, + 1, + '2025-05-01', + 2, + 'EN', + '900000000000013009', + 'Synonym', + 'HTTP://SNOMED.INFO/SCT', + 'NM THYROID STUDY REPORT', + 59 +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/MSSQL_2012.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/MSSQL_2012.sql new file mode 100644 index 000000000000..be535bbcbcfa --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/MSSQL_2012.sql @@ -0,0 +1,531 @@ +INSERT INTO HFJ_SEARCH ( + PID, + CREATED, + SEARCH_DELETED, + EXPIRY_OR_NULL, + FAILURE_CODE, + FAILURE_MESSAGE, + LAST_UPDATED_HIGH, + LAST_UPDATED_LOW, + NUM_BLOCKED, + NUM_FOUND, + PARTITION_ID, + PREFERRED_PAGE_SIZE, + RESOURCE_ID, + RESOURCE_TYPE, + SEARCH_PARAM_MAP, + SEARCH_PARAM_MAP_BIN, + SEARCH_QUERY_STRING, + SEARCH_QUERY_STRING_HASH, + SEARCH_QUERY_STRING_VC, + SEARCH_TYPE, + SEARCH_STATUS, + TOTAL_COUNT, + SEARCH_UUID, + OPTLOCK_VERSION +) VALUES ( + 54, + '2025-01-01 16:52:09.149', + 0, + '2025-01-05 15:16:26.43', + 500, + 'Failure Message', + '2025-01-05 15:17:26.43', + '2025-01-05 15:18:26.43', + 0, + 1, + 1, + 10, + 1906, + 'Patient', + 5146583, + 7368816, + 8479927, + 1212873581, + '?_lastUpdated=le2024-05-31', + 1, + 'FINISHED', + 1, + '983ace20-3244-4c22-a473-559baa4d9f6d', + 1 +); + +INSERT INTO HFJ_SEARCH_RESULT ( + PID, + SEARCH_ORDER, + RESOURCE_PARTITION_ID, + RESOURCE_PID, + SEARCH_PID +) VALUES ( + 2, + 0, + 1, + 1730, + 54 +); + +INSERT INTO MPI_LINK ( + PID, + CREATED, + EID_MATCH, + TARGET_TYPE, + LINK_SOURCE, + MATCH_RESULT, + NEW_PERSON, + PERSON_PARTITION_ID, + PERSON_PID, + SCORE, + TARGET_PARTITION_ID, + TARGET_PID, + UPDATED, + VECTOR, + VERSION, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + RULE_COUNT, + PARTITION_DATE, + PARTITION_ID +) VALUES ( + 3, + '2025-01-05 15:16:26.43', + 'true', + 'Observation', + 0, + 2, + 'true', + 1, + 1906, + 1.0, + 1, + 1678, + '2025-01-05 15:16:26.43', + 61, + '1', + 1, + 1906, + 1, + '2025-01-05', + 1 +); + +INSERT INTO MPI_LINK_AUD ( + PID, + REV, + REVTYPE, + PERSON_PARTITION_ID, + PERSON_PID, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + TARGET_TYPE, + RULE_COUNT, + TARGET_PARTITION_ID, + TARGET_PID, + MATCH_RESULT, + LINK_SOURCE, + VERSION, + EID_MATCH, + NEW_PERSON, + SCORE, + CREATED, + UPDATED, + VECTOR, + PARTITION_ID, + PARTITION_DATE +) VALUES ( + 2, + 1, + 0, + 1, + 1906, + 1, + 1906, + 'PATIENT', + 0, + 1, + 1357, + 2, + 0, + 1, + 0, + 1, + 1, + '2025-01-29 10:14:40.69', + '2025-01-29 10:14:41.70', + 3, + 1, + '2025-01-05' +); + +INSERT INTO NPM_PACKAGE_VER ( + PID, + PKG_AUTHOR, + AUTHOR_UPPER, + CURRENT_VERSION, + PKG_DESC, + DESC_UPPER, + FHIR_VERSION, + FHIR_VERSION_ID, + PACKAGE_ID, + PACKAGE_SIZE_BYTES, + SAVED_TIME, + UPDATED_TIME, + VERSION_ID, + PACKAGE_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'HL7 Inc', + 'HL7 INC', + 1, + 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', + 'DEFINITIONS (API, STRUCTURES AND TERMINOLOGIES) FOR THE R4 VERSION OF THE FHIR STANDARD', + 'R4', + '4.0.1', + 'COM.EXAMPLE.IG', + 370, + '2024-05-01 15:22:37.957', + '2024-05-01 15:22:37.959', + '1.0.2', + 1, + 1, + 1 +); + +INSERT INTO NPM_PACKAGE_VER_RES ( + PID, + CANONICAL_URL, + CANONICAL_VERSION, + FILE_DIR, + FHIR_VERSION, + FHIR_VERSION_ID, + FILE_NAME, + RES_SIZE_BYTES, + RES_TYPE, + UPDATED_TIME, + PACKVER_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'http://hl7.org/fhir/StructureDefinition/Patient', + '4.0.1', + 'PACKAGE', + 'R4', + '4.0.1', + 'TESTPATIENT.JSON', + 225, + 'PATIENT', + '2024-05-01 15:22:38.057', + 1, + 1, + 2 +); + +INSERT INTO TRM_CODESYSTEM ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODE_SYSTEM_URI, + CURRENT_VERSION_PARTITION_ID, + CURRENT_VERSION_PID, + CS_NAME, + RES_ID +) VALUES ( + 2, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG?VERSION=2.67', + 1, + 55, + 'LOINC', + 1780 +); + +INSERT INTO TRM_CODESYSTEM_VER ( + PID, + PARTITION_ID, + PARTITION_DATE, + CS_DISPLAY, + CODESYSTEM_PID, + CS_VERSION_ID, + RES_ID +) VALUES ( + 55, + 1, + '2024-05-01', + 'LOINC', + 2, + '2.67', + 1780 +); + +INSERT INTO TRM_CONCEPT ( + PID, + PARTITION_ID, + CODEVAL, + CODESYSTEM_PID, + DISPLAY, + INDEX_STATUS, + PARENT_PIDS, + PARENT_PIDS_VC, + CODE_SEQUENCE, + CONCEPT_UPDATED +) VALUES ( + 2, + 1, + 'LL100-9', + 55, + 'Radiation Rx Performed', + 1, + '1415723', + '1415723', + 3, + '2024-05-01 17:02:39.139' + ); + +INSERT INTO TRM_CONCEPT_DESIG ( + PID, + PARTITION_ID, + PARTITION_DATE, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VAL_VC, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 107, + 1, + '2024-05-01', + 'NL', + '900000000000013009', + 'SYNONYM', + 'HTTPS://LOINC.ORG', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 55, + 2 +); + +INSERT INTO TRM_CONCEPT_MAP ( + PID, + PARTITION_ID, + PARTITION_DATE, + RES_ID, + SOURCE_URL, + TARGET_URL, + URL, + VER +) VALUES ( + 55, + 1, + '2024-05-01', + 1796, + 'HTTP://LOINC.ORG', + 'HTTPS://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + '1.1' +); + +INSERT INTO TRM_CONCEPT_MAP_GROUP ( + PID, + PARTITION_ID, + PARTITION_DATE, + CONCEPT_MAP_URL, + SOURCE_URL, + SOURCE_VS, + SOURCE_VERSION, + TARGET_URL, + TARGET_VS, + TARGET_VERSION, + CONCEPT_MAP_PID +) VALUES ( + 55, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'HTTP://LOINC.ORG', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + '2.72', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/ALL-COMPOUNDS', + '1.1', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELEMENT ( + PID, + PARTITION_ID, + PARTITION_DATE, + SOURCE_CODE, + CONCEPT_MAP_URL, + SOURCE_DISPLAY, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GROUP_PID +) VALUES ( + 61, + 1, + '2024-05-01', + 'LP15942-3', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'HTTP://LOINC.ORG', + '2.67', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELM_TGT ( + PID, + PARTITION_ID, + PARTITION_DATE, + TARGET_CODE, + CONCEPT_MAP_URL, + TARGET_DISPLAY, + TARGET_EQUIVALENCE, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GRP_ELM_PID +) VALUES ( + 61, + 1, + '2024-05-01', + '1064', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'EQUAL', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + '1.1', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/PUBCHEM-ALL', + 61 +); + +INSERT INTO TRM_CONCEPT_PC_LINK ( + PID, + PARTITION_ID, + CHILD_PID, + CODESYSTEM_PID, + PARENT_PID, + REL_TYPE +) VALUES ( + 55, + 1, + 2, + 55, + 151, + 0 +); + +INSERT INTO TRM_CONCEPT_PROPERTY ( + PID, + PARTITION_ID, + PARTITION_DATE, + PROP_CODESYSTEM, + PROP_DISPLAY, + PROP_KEY, + PROP_TYPE, + PROP_VAL, + PROP_VAL_BIN, + PROP_VAL_LOB, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 155, + 1, + '2024-05-01', + 'http://loinc.org', + 'code-A', + 'CODING', + 1, + 'LP14082-9', + 8479928, + 83006308, + 55, + 2 +); + +INSERT INTO TRM_VALUESET ( + PID, + PARTITION_ID, + PARTITION_DATE, + EXPANSION_STATUS, + EXPANDED_AT, + VSNAME, + RES_ID, + TOTAL_CONCEPT_DESIGNATIONS, + TOTAL_CONCEPTS, + URL, + VER +) VALUES ( + 61, + 1, + '2025-05-01', + 'EXPANDED', + '2025-01-04 16:09:14.488', + 'v2.0127', + 1654, + 0, + 8, + 'http://terminology.hl7.org/ValueSet/v2-0127', + '2.0.0' +); + +INSERT INTO TRM_VALUESET_CONCEPT ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODEVAL, + DISPLAY, + INDEX_STATUS, + VALUESET_ORDER, + SOURCE_DIRECT_PARENT_PIDS, + SOURCE_DIRECT_PARENT_PIDS_VC, + SOURCE_PID, + SYSTEM_URL, + SYSTEM_VER, + VALUESET_PID +) VALUES ( + 2, + 1, + '2025-05-01', + 'LA4281-7', + 'Boost Rad at this Facility', + 1, + 3, + '1415722', + '1415722', + 1, + 'HTTP://LOINC.ORG', + 'V2.67', + 59 +); + +INSERT INTO TRM_VALUESET_C_DESIGNATION ( + PID, + PARTITION_ID, + PARTITION_DATE, + VALUESET_CONCEPT_PID, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VALUESET_PID +) VALUES ( + 5, + 1, + '2025-05-01', + 2, + 'EN', + '900000000000013009', + 'Synonym', + 'HTTP://SNOMED.INFO/SCT', + 'NM THYROID STUDY REPORT', + 59 +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/ORACLE_12C.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/ORACLE_12C.sql new file mode 100644 index 000000000000..8bbcc2864f4a --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/ORACLE_12C.sql @@ -0,0 +1,531 @@ +INSERT INTO HFJ_SEARCH ( + PID, + CREATED, + SEARCH_DELETED, + EXPIRY_OR_NULL, + FAILURE_CODE, + FAILURE_MESSAGE, + LAST_UPDATED_HIGH, + LAST_UPDATED_LOW, + NUM_BLOCKED, + NUM_FOUND, + PARTITION_ID, + PREFERRED_PAGE_SIZE, + RESOURCE_ID, + RESOURCE_TYPE, + SEARCH_PARAM_MAP, + SEARCH_PARAM_MAP_BIN, + SEARCH_QUERY_STRING, + SEARCH_QUERY_STRING_HASH, + SEARCH_QUERY_STRING_VC, + SEARCH_TYPE, + SEARCH_STATUS, + TOTAL_COUNT, + SEARCH_UUID, + OPTLOCK_VERSION +) VALUES ( + 54, + SYSDATE, + 0, + SYSDATE, + 500, + 'Failure Message', + SYSDATE, + SYSDATE, + 0, + 1, + 1, + 10, + 1906, + 'Patient', + HEXTORAW('1B25E293'), + HEXTORAW('453d7a34'), + HEXTORAW('28721FB0'), + 1212873581, + '?_lastUpdated=le2024-05-31', + 1, + 'FINISHED', + 1, + '983ace20-3244-4c22-a473-559baa4d9f6d', + 1 +); + +INSERT INTO HFJ_SEARCH_RESULT ( + PID, + SEARCH_ORDER, + RESOURCE_PARTITION_ID, + RESOURCE_PID, + SEARCH_PID +) VALUES ( + 2, + 0, + 1, + 1730, + 54 +); + +INSERT INTO MPI_LINK ( + PID, + CREATED, + EID_MATCH, + TARGET_TYPE, + LINK_SOURCE, + MATCH_RESULT, + NEW_PERSON, + PERSON_PARTITION_ID, + PERSON_PID, + SCORE, + TARGET_PARTITION_ID, + TARGET_PID, + UPDATED, + VECTOR, + VERSION, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + RULE_COUNT, + PARTITION_DATE, + PARTITION_ID +) VALUES ( + 3, + SYSDATE, + 1, + 'Observation', + 0, + 2, + 1, + 1, + 1906, + 1.0, + 1, + 1678, + SYSDATE, + 61, + '1', + 1, + 1906, + 1, + SYSDATE, + 1 +); + +INSERT INTO MPI_LINK_AUD ( + PID, + REV, + REVTYPE, + PERSON_PARTITION_ID, + PERSON_PID, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + TARGET_TYPE, + RULE_COUNT, + TARGET_PARTITION_ID, + TARGET_PID, + MATCH_RESULT, + LINK_SOURCE, + VERSION, + EID_MATCH, + NEW_PERSON, + SCORE, + CREATED, + UPDATED, + VECTOR, + PARTITION_ID, + PARTITION_DATE +) VALUES ( + 2, + 1, + 0, + 1, + 1906, + 1, + 1906, + 'PATIENT', + 0, + 1, + 1357, + 2, + 0, + 1, + 0, + 1, + 1, + SYSDATE, + SYSDATE, + 3, + 1, + SYSDATE +); + +INSERT INTO NPM_PACKAGE_VER ( + PID, + PKG_AUTHOR, + AUTHOR_UPPER, + CURRENT_VERSION, + PKG_DESC, + DESC_UPPER, + FHIR_VERSION, + FHIR_VERSION_ID, + PACKAGE_ID, + PACKAGE_SIZE_BYTES, + SAVED_TIME, + UPDATED_TIME, + VERSION_ID, + PACKAGE_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'HL7 Inc', + 'HL7 INC', + 1, + 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', + 'DEFINITIONS (API, STRUCTURES AND TERMINOLOGIES) FOR THE R4 VERSION OF THE FHIR STANDARD', + 'R4', + '4.0.1', + 'COM.EXAMPLE.IG', + 370, + SYSDATE, + SYSDATE, + '1.0.2', + 1, + 1, + 1 +); + +INSERT INTO NPM_PACKAGE_VER_RES ( + PID, + CANONICAL_URL, + CANONICAL_VERSION, + FILE_DIR, + FHIR_VERSION, + FHIR_VERSION_ID, + FILE_NAME, + RES_SIZE_BYTES, + RES_TYPE, + UPDATED_TIME, + PACKVER_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'http://hl7.org/fhir/StructureDefinition/Patient', + '4.0.1', + 'PACKAGE', + 'R4', + '4.0.1', + 'TESTPATIENT.JSON', + 225, + 'PATIENT', + SYSDATE, + 1, + 1, + 2 +); + +INSERT INTO TRM_CODESYSTEM ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODE_SYSTEM_URI, + CURRENT_VERSION_PARTITION_ID, + CURRENT_VERSION_PID, + CS_NAME, + RES_ID +) VALUES ( + 2, + 1, + SYSDATE, + 'HTTP://LOINC.ORG?VERSION=2.67', + 1, + 55, + 'LOINC', + 1780 +); + +INSERT INTO TRM_CODESYSTEM_VER ( + PID, + PARTITION_ID, + PARTITION_DATE, + CS_DISPLAY, + CODESYSTEM_PID, + CS_VERSION_ID, + RES_ID +) VALUES ( + 55, + 1, + SYSDATE, + 'LOINC', + 2, + '2.67', + 1780 +); + +INSERT INTO TRM_CONCEPT ( + PID, + PARTITION_ID, + CODEVAL, + CODESYSTEM_PID, + DISPLAY, + INDEX_STATUS, + PARENT_PIDS, + PARENT_PIDS_VC, + CODE_SEQUENCE, + CONCEPT_UPDATED +) VALUES ( + 2, + 1, + 'LL100-9', + 55, + 'Radiation Rx Performed', + 1, + '1415723', + '1415723', + 3, + SYSDATE + ); + +INSERT INTO TRM_CONCEPT_DESIG ( + PID, + PARTITION_ID, + PARTITION_DATE, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VAL_VC, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 107, + 1, + SYSDATE, + 'NL', + '900000000000013009', + 'SYNONYM', + 'HTTPS://LOINC.ORG', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 55, + 2 +); + +INSERT INTO TRM_CONCEPT_MAP ( + PID, + PARTITION_ID, + PARTITION_DATE, + RES_ID, + SOURCE_URL, + TARGET_URL, + URL, + VER +) VALUES ( + 55, + 1, + SYSDATE, + 1796, + 'HTTP://LOINC.ORG', + 'HTTPS://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + '1.1' +); + +INSERT INTO TRM_CONCEPT_MAP_GROUP ( + PID, + PARTITION_ID, + PARTITION_DATE, + CONCEPT_MAP_URL, + SOURCE_URL, + SOURCE_VS, + SOURCE_VERSION, + TARGET_URL, + TARGET_VS, + TARGET_VERSION, + CONCEPT_MAP_PID +) VALUES ( + 55, + 1, + SYSDATE, + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'HTTP://LOINC.ORG', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + '2.72', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/ALL-COMPOUNDS', + '1.1', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELEMENT ( + PID, + PARTITION_ID, + PARTITION_DATE, + SOURCE_CODE, + CONCEPT_MAP_URL, + SOURCE_DISPLAY, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GROUP_PID +) VALUES ( + 61, + 1, + SYSDATE, + 'LP15942-3', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'HTTP://LOINC.ORG', + '2.67', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELM_TGT ( + PID, + PARTITION_ID, + PARTITION_DATE, + TARGET_CODE, + CONCEPT_MAP_URL, + TARGET_DISPLAY, + TARGET_EQUIVALENCE, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GRP_ELM_PID +) VALUES ( + 61, + 1, + SYSDATE, + '1064', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'EQUAL', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + '1.1', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/PUBCHEM-ALL', + 61 +); + +INSERT INTO TRM_CONCEPT_PC_LINK ( + PID, + PARTITION_ID, + CHILD_PID, + CODESYSTEM_PID, + PARENT_PID, + REL_TYPE +) VALUES ( + 55, + 1, + 2, + 55, + 151, + 0 +); + +INSERT INTO TRM_CONCEPT_PROPERTY ( + PID, + PARTITION_ID, + PARTITION_DATE, + PROP_CODESYSTEM, + PROP_DISPLAY, + PROP_KEY, + PROP_TYPE, + PROP_VAL, + PROP_VAL_BIN, + PROP_VAL_LOB, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 155, + 1, + SYSDATE, + 'http://loinc.org', + 'code-A', + 'CODING', + 1, + 'LP14082-9', + HEXTORAW('453d8a34'), + HEXTORAW('8B9D6255'), + 55, + 2 +); + +INSERT INTO TRM_VALUESET ( + PID, + PARTITION_ID, + PARTITION_DATE, + EXPANSION_STATUS, + EXPANDED_AT, + VSNAME, + RES_ID, + TOTAL_CONCEPT_DESIGNATIONS, + TOTAL_CONCEPTS, + URL, + VER +) VALUES ( + 61, + 1, + SYSDATE, + 'EXPANDED', + SYSDATE, + 'v2.0127', + 1654, + 0, + 8, + 'http://terminology.hl7.org/ValueSet/v2-0127', + '2.0.0' +); + +INSERT INTO TRM_VALUESET_CONCEPT ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODEVAL, + DISPLAY, + INDEX_STATUS, + VALUESET_ORDER, + SOURCE_DIRECT_PARENT_PIDS, + SOURCE_DIRECT_PARENT_PIDS_VC, + SOURCE_PID, + SYSTEM_URL, + SYSTEM_VER, + VALUESET_PID +) VALUES ( + 2, + 1, + SYSDATE, + 'LA4281-7', + 'Boost Rad at this Facility', + 1, + 3, + '1415722', + '1415722', + 1, + 'HTTP://LOINC.ORG', + 'V2.67', + 59 +); + +INSERT INTO TRM_VALUESET_C_DESIGNATION ( + PID, + PARTITION_ID, + PARTITION_DATE, + VALUESET_CONCEPT_PID, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VALUESET_PID +) VALUES ( + 5, + 1, + SYSDATE, + 2, + 'EN', + '900000000000013009', + 'Synonym', + 'HTTP://SNOMED.INFO/SCT', + 'NM THYROID STUDY REPORT', + 59 +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/POSTGRES_9_4.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/POSTGRES_9_4.sql new file mode 100644 index 000000000000..719319610331 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_8_0/data/POSTGRES_9_4.sql @@ -0,0 +1,531 @@ +INSERT INTO HFJ_SEARCH ( + PID, + CREATED, + SEARCH_DELETED, + EXPIRY_OR_NULL, + FAILURE_CODE, + FAILURE_MESSAGE, + LAST_UPDATED_HIGH, + LAST_UPDATED_LOW, + NUM_BLOCKED, + NUM_FOUND, + PARTITION_ID, + PREFERRED_PAGE_SIZE, + RESOURCE_ID, + RESOURCE_TYPE, + SEARCH_PARAM_MAP, + SEARCH_PARAM_MAP_BIN, + SEARCH_QUERY_STRING, + SEARCH_QUERY_STRING_HASH, + SEARCH_QUERY_STRING_VC, + SEARCH_TYPE, + SEARCH_STATUS, + TOTAL_COUNT, + SEARCH_UUID, + OPTLOCK_VERSION +) VALUES ( + 54, + '2025-01-01 16:52:09.149', + FALSE, + '2025-01-05 15:16:26.43', + 500, + 'Failure Message', + '2025-01-05 15:17:26.43', + '2025-01-05 15:18:26.43', + 0, + 1, + 1, + 10, + 1906, + 'Patient', + 5146583, + '013d7d16d7ad4fefb61bd95b765c8ceb', + 8479927, + 1212873581, + '?_lastUpdated=le2024-05-31', + 1, + 'FINISHED', + 1, + '983ace20-3244-4c22-a473-559baa4d9f6d', + 1 +); + +INSERT INTO HFJ_SEARCH_RESULT ( + PID, + SEARCH_ORDER, + RESOURCE_PARTITION_ID, + RESOURCE_PID, + SEARCH_PID +) VALUES ( + 2, + 0, + 1, + 1730, + 54 +); + +INSERT INTO MPI_LINK ( + PID, + CREATED, + EID_MATCH, + TARGET_TYPE, + LINK_SOURCE, + MATCH_RESULT, + NEW_PERSON, + PERSON_PARTITION_ID, + PERSON_PID, + SCORE, + TARGET_PARTITION_ID, + TARGET_PID, + UPDATED, + VECTOR, + VERSION, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + RULE_COUNT, + PARTITION_DATE, + PARTITION_ID +) VALUES ( + 3, + '2025-01-05 15:16:26.43', + TRUE, + 'Observation', + 0, + 2, + TRUE, + 1, + 1906, + 1.0, + 1, + 1678, + '2025-01-05 15:16:26.43', + 61, + '1', + 1, + 1906, + 1, + '2025-01-05', + 1 +); + +INSERT INTO MPI_LINK_AUD ( + PID, + REV, + REVTYPE, + PERSON_PARTITION_ID, + PERSON_PID, + GOLDEN_RESOURCE_PARTITION_ID, + GOLDEN_RESOURCE_PID, + TARGET_TYPE, + RULE_COUNT, + TARGET_PARTITION_ID, + TARGET_PID, + MATCH_RESULT, + LINK_SOURCE, + VERSION, + EID_MATCH, + NEW_PERSON, + SCORE, + CREATED, + UPDATED, + VECTOR, + PARTITION_ID, + PARTITION_DATE +) VALUES ( + 2, + 1, + 0, + 1, + 1906, + 1, + 1906, + 'PATIENT', + 0, + 1, + 1357, + 2, + 0, + 1, + false, + true, + 1, + '2025-01-29 10:14:40.69', + '2025-01-29 10:14:41.70', + 3, + 1, + '2025-01-05' +); + +INSERT INTO NPM_PACKAGE_VER ( + PID, + PKG_AUTHOR, + AUTHOR_UPPER, + CURRENT_VERSION, + PKG_DESC, + DESC_UPPER, + FHIR_VERSION, + FHIR_VERSION_ID, + PACKAGE_ID, + PACKAGE_SIZE_BYTES, + SAVED_TIME, + UPDATED_TIME, + VERSION_ID, + PACKAGE_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'HL7 Inc', + 'HL7 INC', + TRUE, + 'Definitions (API, structures and terminologies) for the R4 version of the FHIR standard', + 'DEFINITIONS (API, STRUCTURES AND TERMINOLOGIES) FOR THE R4 VERSION OF THE FHIR STANDARD', + 'R4', + '4.0.1', + 'COM.EXAMPLE.IG', + 370, + '2024-05-01 15:22:37.957', + '2024-05-01 15:22:37.959', + '1.0.2', + 1, + 1, + 1 +); + +INSERT INTO NPM_PACKAGE_VER_RES ( + PID, + CANONICAL_URL, + CANONICAL_VERSION, + FILE_DIR, + FHIR_VERSION, + FHIR_VERSION_ID, + FILE_NAME, + RES_SIZE_BYTES, + RES_TYPE, + UPDATED_TIME, + PACKVER_PID, + PARTITION_ID, + BINARY_RES_ID +) VALUES ( + 2, + 'http://hl7.org/fhir/StructureDefinition/Patient', + '4.0.1', + 'PACKAGE', + 'R4', + '4.0.1', + 'TESTPATIENT.JSON', + 225, + 'PATIENT', + '2024-05-01 15:22:38.057', + 1, + 1, + 2 +); + +INSERT INTO TRM_CODESYSTEM ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODE_SYSTEM_URI, + CURRENT_VERSION_PARTITION_ID, + CURRENT_VERSION_PID, + CS_NAME, + RES_ID +) VALUES ( + 2, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG?VERSION=2.67', + 1, + 55, + 'LOINC', + 1780 +); + +INSERT INTO TRM_CODESYSTEM_VER ( + PID, + PARTITION_ID, + PARTITION_DATE, + CS_DISPLAY, + CODESYSTEM_PID, + CS_VERSION_ID, + RES_ID +) VALUES ( + 55, + 1, + '2024-05-01', + 'LOINC', + 2, + '2.67', + 1780 +); + +INSERT INTO TRM_CONCEPT ( + PID, + PARTITION_ID, + CODEVAL, + CODESYSTEM_PID, + DISPLAY, + INDEX_STATUS, + PARENT_PIDS, + PARENT_PIDS_VC, + CODE_SEQUENCE, + CONCEPT_UPDATED +) VALUES ( + 2, + 1, + 'LL100-9', + 55, + 'Radiation Rx Performed', + 1, + '1415723', + '1415723', + 3, + '2024-05-01 17:02:39.139' + ); + +INSERT INTO TRM_CONCEPT_DESIG ( + PID, + PARTITION_ID, + PARTITION_DATE, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VAL_VC, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 107, + 1, + '2024-05-01', + 'NL', + '900000000000013009', + 'SYNONYM', + 'HTTPS://LOINC.ORG', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 'DIASTOLISCHE BLOEDDRUK - ZITTEND', + 55, + 2 +); + +INSERT INTO TRM_CONCEPT_MAP ( + PID, + PARTITION_ID, + PARTITION_DATE, + RES_ID, + SOURCE_URL, + TARGET_URL, + URL, + VER +) VALUES ( + 55, + 1, + '2024-05-01', + 1796, + 'HTTP://LOINC.ORG', + 'HTTPS://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + '1.1' +); + +INSERT INTO TRM_CONCEPT_MAP_GROUP ( + PID, + PARTITION_ID, + PARTITION_DATE, + CONCEPT_MAP_URL, + SOURCE_URL, + SOURCE_VS, + SOURCE_VERSION, + TARGET_URL, + TARGET_VS, + TARGET_VERSION, + CONCEPT_MAP_PID +) VALUES ( + 55, + 1, + '2024-05-01', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'HTTP://LOINC.ORG', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + '2.72', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/ALL-COMPOUNDS', + '1.1', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELEMENT ( + PID, + PARTITION_ID, + PARTITION_DATE, + SOURCE_CODE, + CONCEPT_MAP_URL, + SOURCE_DISPLAY, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GROUP_PID +) VALUES ( + 61, + 1, + '2024-05-01', + 'LP15942-3', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'HTTP://LOINC.ORG', + '2.67', + 'HTTP://LOINC.ORG/VS/LOINC-ALL', + 55 +); + +INSERT INTO TRM_CONCEPT_MAP_GRP_ELM_TGT ( + PID, + PARTITION_ID, + PARTITION_DATE, + TARGET_CODE, + CONCEPT_MAP_URL, + TARGET_DISPLAY, + TARGET_EQUIVALENCE, + SYSTEM_URL, + SYSTEM_VERSION, + VALUESET_URL, + CONCEPT_MAP_GRP_ELM_PID +) VALUES ( + 61, + 1, + '2024-05-01', + '1064', + 'HTTP://LOINC.ORG/CM/LOINC-PARTS-TO-PUBCHEM', + 'UROBILINOGEN', + 'EQUAL', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV', + '1.1', + 'HTTP://PUBCHEM.NCBI.NLM.NIH.GOV/VS/PUBCHEM-ALL', + 61 +); + +INSERT INTO TRM_CONCEPT_PC_LINK ( + PID, + PARTITION_ID, + CHILD_PID, + CODESYSTEM_PID, + PARENT_PID, + REL_TYPE +) VALUES ( + 55, + 1, + 2, + 55, + 151, + 0 +); + +INSERT INTO TRM_CONCEPT_PROPERTY ( + PID, + PARTITION_ID, + PARTITION_DATE, + PROP_CODESYSTEM, + PROP_DISPLAY, + PROP_KEY, + PROP_TYPE, + PROP_VAL, + PROP_VAL_BIN, + PROP_VAL_LOB, + CS_VER_PID, + CONCEPT_PID +) VALUES ( + 155, + 1, + '2024-05-01', + 'http://loinc.org', + 'code-A', + 'CODING', + 1, + 'LP14082-9', + '\x48656c6c6f20776f726c6422', + 83006407, + 55, + 2 +); + +INSERT INTO TRM_VALUESET ( + PID, + PARTITION_ID, + PARTITION_DATE, + EXPANSION_STATUS, + EXPANDED_AT, + VSNAME, + RES_ID, + TOTAL_CONCEPT_DESIGNATIONS, + TOTAL_CONCEPTS, + URL, + VER +) VALUES ( + 61, + 1, + '2025-05-01', + 'EXPANDED', + '2025-01-04 16:09:14.488', + 'v2.0127', + 1654, + 0, + 8, + 'http://terminology.hl7.org/ValueSet/v2-0127', + '2.0.0' +); + +INSERT INTO TRM_VALUESET_CONCEPT ( + PID, + PARTITION_ID, + PARTITION_DATE, + CODEVAL, + DISPLAY, + INDEX_STATUS, + VALUESET_ORDER, + SOURCE_DIRECT_PARENT_PIDS, + SOURCE_DIRECT_PARENT_PIDS_VC, + SOURCE_PID, + SYSTEM_URL, + SYSTEM_VER, + VALUESET_PID +) VALUES ( + 2, + 1, + '2025-05-01', + 'LA4281-7', + 'Boost Rad at this Facility', + 1, + 3, + '1415722', + '1415722', + 1, + 'HTTP://LOINC.ORG', + 'V2.67', + 59 +); + +INSERT INTO TRM_VALUESET_C_DESIGNATION ( + PID, + PARTITION_ID, + PARTITION_DATE, + VALUESET_CONCEPT_PID, + LANG, + USE_CODE, + USE_DISPLAY, + USE_SYSTEM, + VAL, + VALUESET_PID +) VALUES ( + 5, + 1, + '2025-05-01', + 2, + 'EN', + '900000000000013009', + 'Synonym', + 'HTTP://SNOMED.INFO/SCT', + 'NM THYROID STUDY REPORT', + 59 +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java index daf26965bc85..acac21b0914b 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java @@ -230,7 +230,7 @@ private void verifyTrm_Concept_Desig(JpaEmbeddedDatabase theDatabase, DriverType final Object queryResultValueVal = queryResultValuesVal.iterator().next(); assertThat(queryResultValueVal).isInstanceOf(Number.class); if (queryResultValueVal instanceof Number queryResultNumber) { - assertThat(queryResultNumber.intValue()).isEqualTo(2); + assertThat(queryResultNumber.intValue()).isEqualTo(3); } assertThat(nullValVcCount).hasSize(1); @@ -244,7 +244,7 @@ private void verifyTrm_Concept_Desig(JpaEmbeddedDatabase theDatabase, DriverType final Object allCountValue = allCount.get(0).values().iterator().next(); if (allCountValue instanceof Number allCountNumber) { - assertThat(allCountNumber.intValue()).isEqualTo(2); + assertThat(allCountNumber.intValue()).isEqualTo(3); } try (final Connection connection = theDatabase.getDataSource().getConnection()) { diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java index a2401c9271d0..1285e715c53a 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java +++ b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java @@ -42,7 +42,7 @@ public class ResourceMatcherR4Test extends BaseMdmRulesR4Test { @Mock private Appender myAppender; @Captor - ArgumentCaptor myLoggingEvent; + private ArgumentCaptor myLoggingEvent; @Override @BeforeEach diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/DelegatingConsentService.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/DelegatingConsentService.java index 0ee45783d9e6..97adc4e10942 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/DelegatingConsentService.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/DelegatingConsentService.java @@ -68,6 +68,10 @@ public void completeOperationFailure( myTarget.completeOperationFailure(theRequestDetails, theException, theContextServices); } + public IConsentService getTarget() { + return myTarget; + } + public void setTarget(IConsentService theTarget) { myTarget = theTarget; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/MultiDelegateConsentService.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/MultiDelegateConsentService.java index aec293b96296..d54c47e053dd 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/MultiDelegateConsentService.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/MultiDelegateConsentService.java @@ -90,4 +90,8 @@ public ConsentOutcome willSeeResource( return myVoteCombiner.apply(myDelegates.stream() .map(nextDelegate -> nextDelegate.willSeeResource(theRequestDetails, theResource, theContextServices))); } + + public Collection getDelegates() { + return myDelegates; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java index 4effcb284fea..59cf3c22e92e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/cache/BaseResourceCacheSynchronizer.java @@ -59,7 +59,7 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi private IResourceChangeListenerRegistry myResourceChangeListenerRegistry; @Autowired - DaoRegistry myDaoRegistry; + private DaoRegistry myDaoRegistry; private SearchParameterMap mySearchParameterMap; private SystemRequestDetails mySystemRequestDetails; @@ -67,6 +67,8 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi private final Semaphore mySyncResourcesSemaphore = new Semaphore(1); private final Object mySyncResourcesLock = new Object(); + private Integer myMaxRetryCount = null; + protected BaseResourceCacheSynchronizer(String theResourceName) { myResourceName = theResourceName; } @@ -150,10 +152,22 @@ public int doSyncResourcesForUnitTest() { synchronized int doSyncResourcesWithRetry() { // retry runs MAX_RETRIES times // and if errors result every time, it will fail - Retrier syncResourceRetrier = new Retrier<>(this::doSyncResources, MAX_RETRIES); + Retrier syncResourceRetrier = new Retrier<>(this::doSyncResources, getMaxRetries()); return syncResourceRetrier.runWithRetry(); } + private int getMaxRetries() { + if (myMaxRetryCount != null) { + return myMaxRetryCount; + } + return MAX_RETRIES; + } + + @VisibleForTesting + public void setMaxRetries(Integer theMaxRetries) { + myMaxRetryCount = theMaxRetries; + } + @SuppressWarnings("unchecked") private int doSyncResources() { if (isStopping()) { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageResourceDao.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageResourceDao.java index 9ab51e07dc3a..8eacb2d3820b 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageResourceDao.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageResourceDao.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.patch.FhirPatch; import ca.uhn.fhir.jpa.patch.JsonPatchUtils; import ca.uhn.fhir.jpa.patch.XmlPatchUtils; +import ca.uhn.fhir.jpa.update.UpdateParameters; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum; import ca.uhn.fhir.rest.api.PatchTypeEnum; @@ -188,16 +189,19 @@ public DaoMethodOutcome patchInTransaction( preProcessResourceForStorage(destinationCasted, theRequestDetails, theTransactionDetails, true); - return doUpdateForUpdateOrPatch( - theRequestDetails, - resourceId, - theConditionalUrl, - thePerformIndexing, - false, - destinationCasted, - entityToUpdate, - RestOperationTypeEnum.PATCH, - theTransactionDetails); + UpdateParameters updateParameters = new UpdateParameters<>() + .setRequestDetails(theRequestDetails) + .setResourceIdToUpdate(resourceId) + .setMatchUrl(theConditionalUrl) + .setShouldPerformIndexing(thePerformIndexing) + .setShouldForceUpdateVersion(false) + .setResource(destinationCasted) + .setEntity(entityToUpdate) + .setOperationType(RestOperationTypeEnum.PATCH) + .setTransactionDetails(theTransactionDetails) + .setShouldForcePopulateOldResourceForProcessing(false); + + return doUpdateForUpdateOrPatch(updateParameters); } @Override @@ -216,33 +220,32 @@ protected abstract IBasePersistedResource readEntityLatestVersion( protected abstract IBasePersistedResource readEntityLatestVersion( IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails); - protected DaoMethodOutcome doUpdateForUpdateOrPatch( - RequestDetails theRequest, - IIdType theResourceId, - String theMatchUrl, - boolean thePerformIndexing, - boolean theForceUpdateVersion, - T theResource, - IBasePersistedResource theEntity, - RestOperationTypeEnum theOperationType, - TransactionDetails theTransactionDetails) { - if (theResourceId.hasVersionIdPart() - && Long.parseLong(theResourceId.getVersionIdPart()) != theEntity.getVersion()) { - throw new ResourceVersionConflictException( - Msg.code(989) + "Trying to update " + theResourceId + " but this is not the current version"); + protected DaoMethodOutcome doUpdateForUpdateOrPatch(UpdateParameters theUpdateParameters) { + + if (theUpdateParameters.getResourceIdToUpdate().hasVersionIdPart() + && Long.parseLong(theUpdateParameters.getResourceIdToUpdate().getVersionIdPart()) + != theUpdateParameters.getEntity().getVersion()) { + throw new ResourceVersionConflictException(Msg.code(989) + "Trying to update " + + theUpdateParameters.getResourceIdToUpdate() + " but this is not the current version"); } - if (theResourceId.hasResourceType() && !theResourceId.getResourceType().equals(getResourceName())) { + if (theUpdateParameters.getResourceIdToUpdate().hasResourceType() + && !theUpdateParameters + .getResourceIdToUpdate() + .getResourceType() + .equals(getResourceName())) { throw new UnprocessableEntityException(Msg.code(990) + "Invalid resource ID[" - + theEntity.getIdDt().toUnqualifiedVersionless() + "] of type[" + theEntity.getResourceType() + + theUpdateParameters.getEntity().getIdDt().toUnqualifiedVersionless() + "] of type[" + + theUpdateParameters.getEntity().getResourceType() + "] - Does not match expected [" + getResourceName() + "]"); } IBaseResource oldResource; - if (getStorageSettings().isMassIngestionMode()) { + if (getStorageSettings().isMassIngestionMode() + && !theUpdateParameters.shouldForcePopulateOldResourceForProcessing()) { oldResource = null; } else { - oldResource = getStorageResourceParser().toResource(theEntity, false); + oldResource = getStorageResourceParser().toResource(theUpdateParameters.getEntity(), false); } /* @@ -256,8 +259,8 @@ protected DaoMethodOutcome doUpdateForUpdateOrPatch( * See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources * for a test that needs this. */ - boolean wasDeleted = theEntity.isDeleted(); - theEntity.setNotDeleted(); + boolean wasDeleted = theUpdateParameters.getEntity().isDeleted(); + theUpdateParameters.getEntity().setNotDeleted(); /* * If we aren't indexing, that means we're doing this inside a transaction. @@ -265,10 +268,16 @@ protected DaoMethodOutcome doUpdateForUpdateOrPatch( * after placeholder IDs have been replaced, by calling {@link #updateInternal} * directly. So we just bail now. */ - if (!thePerformIndexing) { - theResource.setId(theEntity.getIdDt().getValue()); + if (!theUpdateParameters.shouldPerformIndexing()) { + theUpdateParameters + .getResource() + .setId(theUpdateParameters.getEntity().getIdDt().getValue()); DaoMethodOutcome outcome = toMethodOutcome( - theRequest, theEntity, theResource, theMatchUrl, theOperationType) + theUpdateParameters.getRequest(), + theUpdateParameters.getEntity(), + theUpdateParameters.getResource(), + theUpdateParameters.getMatchUrl(), + theUpdateParameters.getOperationType()) .setCreated(wasDeleted); outcome.setPreviousResource(oldResource); if (!outcome.isNop()) { @@ -284,16 +293,16 @@ protected DaoMethodOutcome doUpdateForUpdateOrPatch( * Otherwise, we're not in a transaction */ return updateInternal( - theRequest, - theResource, - theMatchUrl, - thePerformIndexing, - theForceUpdateVersion, - theEntity, - theResourceId, + theUpdateParameters.getRequest(), + theUpdateParameters.getResource(), + theUpdateParameters.getMatchUrl(), + theUpdateParameters.shouldPerformIndexing(), + theUpdateParameters.shouldForceUpdateVersion(), + theUpdateParameters.getEntity(), + theUpdateParameters.getResourceIdToUpdate(), oldResource, - theOperationType, - theTransactionDetails); + theUpdateParameters.getOperationType(), + theUpdateParameters.getTransactionDetails()); } public static void validateResourceType(IBasePersistedResource theEntity, String theResourceName) { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java index 6f09bfe9b90b..afa508ea13d9 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java @@ -326,7 +326,7 @@ private RequestPartitionId validateAndNormalizePartition( // Replace null partition ID with non-null default partition ID if one is being used if (myPartitionSettings.getDefaultPartitionId() != null && retVal.hasPartitionIds() - && retVal.hasDefaultPartitionId()) { + && hasDefaultPartitionId(retVal)) { List partitionIds = new ArrayList<>(retVal.getPartitionIds()); for (int i = 0; i < partitionIds.size(); i++) { if (partitionIds.get(i) == null) { @@ -363,6 +363,12 @@ public boolean isResourcePartitionable(String theResourceType) { return theResourceType != null && !myNonPartitionableResourceNames.contains(theResourceType); } + @Override + @Nullable + public Integer getDefaultPartitionId() { + return myPartitionSettings.getDefaultPartitionId(); + } + private boolean isResourceNonPartitionable(String theResourceType) { return theResourceType != null && !isResourcePartitionable(theResourceType); } @@ -374,7 +380,7 @@ private void validatePartitionForCreate(RequestPartitionId theRequestPartitionId validateSinglePartitionIdOrName(theRequestPartitionId.getPartitionNames()); // Make sure we're not using one of the conformance resources in a non-default partition - if (theRequestPartitionId.isDefaultPartition() || theRequestPartitionId.isAllPartitions()) { + if (isDefaultPartition(theRequestPartitionId) || theRequestPartitionId.isAllPartitions()) { return; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/patch/FhirPatch.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/patch/FhirPatch.java index 0e2ab8654095..dd83a7f3813f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/patch/FhirPatch.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/patch/FhirPatch.java @@ -172,34 +172,20 @@ private void handleDeleteOperation(IBaseResource theResource, IBase theParameter String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); path = defaultString(path); - String containingPath; - String elementName; - - if (path.endsWith(")")) { - // This is probably a filter, so we're probably dealing with a list - int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses - int lastDotIndex = path.lastIndexOf( - '.', filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that - int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1); - containingPath = path.substring(0, secondLastDotIndex); - elementName = path.substring(secondLastDotIndex + 1, lastDotIndex); - } else if (path.endsWith("]")) { - // This is almost definitely a list - int openBracketIndex = path.lastIndexOf('['); - int lastDotIndex = path.lastIndexOf('.', openBracketIndex); - containingPath = path.substring(0, lastDotIndex); - elementName = path.substring(lastDotIndex + 1, openBracketIndex); - } else { - containingPath = path; - elementName = null; - } + ParsedPath parsedPath = ParsedPath.parse(path); + List containingElements = myContext + .newFhirPath() + .evaluate( + theResource, + parsedPath.getEndsWithAFilterOrIndex() ? parsedPath.getContainingPath() : path, + IBase.class); - List containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); for (IBase nextElement : containingElements) { - if (elementName == null) { - deleteSingleElement(nextElement); + if (parsedPath.getEndsWithAFilterOrIndex()) { + // if the path ends with a filter or index, we must be dealing with a list + deleteFromList(theResource, nextElement, parsedPath.getLastElementName(), path); } else { - deleteFromList(theResource, nextElement, elementName, path); + deleteSingleElement(nextElement); } } } @@ -227,18 +213,51 @@ private void handleReplaceOperation(IBaseResource theResource, IBase theParamete String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH); path = defaultString(path); - int lastDot = path.lastIndexOf("."); - String containingPath = path.substring(0, lastDot); - String elementName = path.substring(lastDot + 1); + ParsedPath parsedPath = ParsedPath.parse(path); - List containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class); - for (IBase nextElement : containingElements) { + List containingElements = + myContext.newFhirPath().evaluate(theResource, parsedPath.getContainingPath(), IBase.class); + for (IBase containingElement : containingElements) { - ChildDefinition childDefinition = findChildDefinition(nextElement, elementName); + ChildDefinition childDefinition = findChildDefinition(containingElement, parsedPath.getLastElementName()); + IBase newValue = getNewValue(theParameters, containingElement, childDefinition); + if (parsedPath.getEndsWithAFilterOrIndex()) { + // if the path ends with a filter or index, we must be dealing with a list + replaceInList(newValue, theResource, containingElement, childDefinition, path); + } else { + childDefinition.getChildDef().getMutator().setValue(containingElement, newValue); + } + } + } - IBase newValue = getNewValue(theParameters, nextElement, childDefinition); + private void replaceInList( + IBase theNewValue, + IBaseResource theResource, + IBase theContainingElement, + ChildDefinition theChildDefinitionForTheList, + String theFullReplacePath) { + + List existingValues = new ArrayList<>( + theChildDefinitionForTheList.getChildDef().getAccessor().getValues(theContainingElement)); + List valuesToReplace = myContext.newFhirPath().evaluate(theResource, theFullReplacePath, IBase.class); + if (valuesToReplace.isEmpty()) { + String msg = myContext + .getLocalizer() + .getMessage(FhirPatch.class, "noMatchingElementForPath", theFullReplacePath); + throw new InvalidRequestException(Msg.code(2617) + msg); + } - childDefinition.getChildDef().getMutator().setValue(nextElement, newValue); + BaseRuntimeChildDefinition.IMutator listMutator = + theChildDefinitionForTheList.getChildDef().getMutator(); + // clear the whole list first, then reconstruct it in the loop below replacing the values that need to be + // replaced + listMutator.setValue(theContainingElement, null); + for (IBase existingValue : existingValues) { + if (valuesToReplace.contains(existingValue)) { + listMutator.addValue(theContainingElement, theNewValue); + } else { + listMutator.addValue(theContainingElement, existingValue); + } } } @@ -667,4 +686,87 @@ public BaseRuntimeElementDefinition getChildElement() { return myChildElement; } } + + /** + * This class helps parse a FHIR path into its component parts for easier patch operation processing. + * It has 3 components: + * - The last element name, which is the last element in the path (not including any list index or filter) + * - The containing path, which is the prefix of the path up to the last element + * - A flag indicating whether the path has a filter or index on the last element of the path, which indicates + * that the path we are dealing is probably for a list element. + * Examples: + * 1. For path "Patient.identifier[2].system", + * - the lastElementName is "system", + * - the containingPath is "Patient.identifier[2]", + * - and endsWithAFilterOrIndex flag is false + * + * 2. For path "Patient.identifier[2]" or for path "Patient.identifier.where('system'='sys1')" + * - the lastElementName is "identifier", + * - the containingPath is "Patient", + * - and the endsWithAFilterOrIndex is true + */ + protected static class ParsedPath { + private final String myLastElementName; + private final String myContainingPath; + private final boolean myEndsWithAFilterOrIndex; + + public ParsedPath(String theLastElementName, String theContainingPath, boolean theEndsWithAFilterOrIndex) { + myLastElementName = theLastElementName; + myContainingPath = theContainingPath; + myEndsWithAFilterOrIndex = theEndsWithAFilterOrIndex; + } + + /** + * returns the last element of the path + */ + public String getLastElementName() { + return myLastElementName; + } + + /** + * Returns the prefix of the path up to the last FHIR resource element + */ + public String getContainingPath() { + return myContainingPath; + } + + /** + * Returns whether the path has a filter or index on the last element of the path, which indicates + * that the path we are dealing is probably a list element. + */ + public boolean getEndsWithAFilterOrIndex() { + return myEndsWithAFilterOrIndex; + } + + public static ParsedPath parse(String path) { + String containingPath; + String elementName; + boolean endsWithAFilterOrIndex = false; + + if (path.endsWith(")")) { + // This is probably a filter, so we're probably dealing with a list + endsWithAFilterOrIndex = true; + int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses + int lastDotIndex = path.lastIndexOf( + '.', + filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that + int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1); + containingPath = path.substring(0, secondLastDotIndex); + elementName = path.substring(secondLastDotIndex + 1, lastDotIndex); + } else if (path.endsWith("]")) { + // This is almost definitely a list + endsWithAFilterOrIndex = true; + int openBracketIndex = path.lastIndexOf('['); + int lastDotIndex = path.lastIndexOf('.', openBracketIndex); + containingPath = path.substring(0, lastDotIndex); + elementName = path.substring(lastDotIndex + 1, openBracketIndex); + } else { + int lastDot = path.lastIndexOf("."); + containingPath = path.substring(0, lastDot); + elementName = path.substring(lastDot + 1); + } + + return new ParsedPath(elementName, containingPath, endsWithAFilterOrIndex); + } + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java index 6a86f2613634..a21f711dbf2f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java @@ -40,6 +40,8 @@ import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.HashSet; @@ -54,6 +56,8 @@ @Interceptor public class SearchParamValidatingInterceptor { + private static final Logger logger = LoggerFactory.getLogger(SearchParamValidatingInterceptor.class); + public static final String SEARCH_PARAM = "SearchParameter"; public static final String SKIP_VALIDATION = SearchParamValidatingInterceptor.class.getName() + ".SKIP_VALIDATION"; @@ -142,6 +146,19 @@ public void validateSearchParamOnUpdate(IBaseResource theResource, RequestDetail if (isNotSearchParameterResource(theResource)) { return; } + + // avoid a loop when loading our hard-coded core FhirContext SearchParameters + // skip Search Param validation if been set in the request + boolean isStartup = theRequestDetails != null + && Boolean.TRUE == theRequestDetails.getUserData().get(SKIP_VALIDATION); + if (isStartup) { + logger.warn( + "Skipping validation of submitted SearchParameter because {} flag is {}", + SKIP_VALIDATION, + Boolean.TRUE); + return; + } + RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource); if (runtimeSearchParam == null) { return; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java index deb4a165469c..16cee9ba6b52 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.SubscriptionSettings; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; @@ -71,11 +72,20 @@ public class SubscriptionCanonicalizer { final FhirContext myFhirContext; private final SubscriptionSettings mySubscriptionSettings; + private IRequestPartitionHelperSvc myHelperSvc; + @Autowired public SubscriptionCanonicalizer(FhirContext theFhirContext, SubscriptionSettings theSubscriptionSettings) { myFhirContext = theFhirContext; mySubscriptionSettings = theSubscriptionSettings; } + // TODO GGG: Eventually, we will unify autowiring styles. It is this way now as this is the least destrctive method + // to accomplish a minimal MR. I recommend moving all dependencies to setter autowiring, but that is for another + // day. + @Autowired + public void setPartitionHelperSvc(IRequestPartitionHelperSvc thePartitionHelperSvc) { + myHelperSvc = thePartitionHelperSvc; + } // TODO: LD: remove this constructor once all callers call the 2 arg constructor above @@ -787,7 +797,9 @@ private boolean handleCrossPartition(IBaseResource theSubscription) { boolean isSubscriptionCreatedOnDefaultPartition = false; if (nonNull(requestPartitionId)) { - isSubscriptionCreatedOnDefaultPartition = requestPartitionId.isDefaultPartition(); + isSubscriptionCreatedOnDefaultPartition = myHelperSvc == null + ? requestPartitionId.isDefaultPartition() + : myHelperSvc.isDefaultPartition(requestPartitionId); } boolean isSubscriptionDefinededAsCrossPartitionSubscription = diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/update/UpdateParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/update/UpdateParameters.java new file mode 100644 index 000000000000..878acffe45bf --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/update/UpdateParameters.java @@ -0,0 +1,140 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2025 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% + */ +package ca.uhn.fhir.jpa.update; + +import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +public class UpdateParameters { + + private RequestDetails myRequestDetails; + private IIdType myResourceIdToUpdate; + private String myMatchUrl; + private boolean myShouldPerformIndexing; + private boolean myShouldForceUpdateVersion; + private T myExistingResource; + private IBasePersistedResource myExistingEntity; + private RestOperationTypeEnum myOperationType; + private TransactionDetails myTransactionDetails; + + /** + * In the update methods, we have a local variable to keep track of the old resource before performing the update + * This old resource may be useful for processing/performing checks (eg. enforcing Patient compartment changes with {@link ca.uhn.fhir.jpa.interceptor.PatientCompartmentEnforcingInterceptor} + * However, populating this can be expensive, and is skipped in Mass Ingestion mode + * + * If this is set to true, the old resource will be forcefully populated. + */ + private boolean myShouldForcePopulateOldResourceForProcessing; + + public RequestDetails getRequest() { + return myRequestDetails; + } + + public UpdateParameters setRequestDetails(RequestDetails theRequest) { + this.myRequestDetails = theRequest; + return this; + } + + public IIdType getResourceIdToUpdate() { + return myResourceIdToUpdate; + } + + public UpdateParameters setResourceIdToUpdate(IIdType theResourceId) { + this.myResourceIdToUpdate = theResourceId; + return this; + } + + public String getMatchUrl() { + return myMatchUrl; + } + + public UpdateParameters setMatchUrl(String theMatchUrl) { + this.myMatchUrl = theMatchUrl; + return this; + } + + public boolean shouldPerformIndexing() { + return myShouldPerformIndexing; + } + + public UpdateParameters setShouldPerformIndexing(boolean thePerformIndexing) { + this.myShouldPerformIndexing = thePerformIndexing; + return this; + } + + public boolean shouldForceUpdateVersion() { + return myShouldForceUpdateVersion; + } + + public UpdateParameters setShouldForceUpdateVersion(boolean theForceUpdateVersion) { + this.myShouldForceUpdateVersion = theForceUpdateVersion; + return this; + } + + public T getResource() { + return myExistingResource; + } + + public UpdateParameters setResource(T theResource) { + this.myExistingResource = theResource; + return this; + } + + public IBasePersistedResource getEntity() { + return myExistingEntity; + } + + public UpdateParameters setEntity(IBasePersistedResource theEntity) { + this.myExistingEntity = theEntity; + return this; + } + + public RestOperationTypeEnum getOperationType() { + return myOperationType; + } + + public UpdateParameters setOperationType(RestOperationTypeEnum myOperationType) { + this.myOperationType = myOperationType; + return this; + } + + public TransactionDetails getTransactionDetails() { + return myTransactionDetails; + } + + public UpdateParameters setTransactionDetails(TransactionDetails myTransactionDetails) { + this.myTransactionDetails = myTransactionDetails; + return this; + } + + public boolean shouldForcePopulateOldResourceForProcessing() { + return myShouldForcePopulateOldResourceForProcessing; + } + + public UpdateParameters setShouldForcePopulateOldResourceForProcessing( + boolean myShouldForcePopulateOldResourceForProcessing) { + this.myShouldForcePopulateOldResourceForProcessing = myShouldForcePopulateOldResourceForProcessing; + return this; + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index d2727fd792f8..5da8e44e875c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -221,6 +221,13 @@ public List getUpdateQueries() { return getQueriesStartingWith("update"); } + /** + * Returns all UPDATE queries executed on the current thread - Index 0 is oldest + */ + public List getUpdateQueries(Predicate theFilter) { + return getQueriesStartingWith("update").stream().filter(theFilter).collect(Collectors.toList()); + } + /** * Returns all UPDATE queries executed on the current thread - Index 0 is oldest */ diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtil.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtil.java index 2c345cf66276..6f96e5c8a406 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtil.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtil.java @@ -53,6 +53,11 @@ public class ResourceCompartmentUtil { */ public static Optional getPatientCompartmentIdentity( IBaseResource theResource, FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) { + if (theResource == null) { + // The resource may be null in mass ingestion mode + return Optional.empty(); + } + RuntimeResourceDefinition resourceDef = theFhirContext.getResourceDefinition(theResource); List patientCompartmentSps = ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index 47fbb2deaa79..7aed46c369ef 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -81,7 +81,6 @@ private Bundle buildPatchBundle( List theResourceIds, RequestDetails theRequestDetails) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - theResourceIds.forEach(referencingResourceId -> { IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); @@ -101,7 +100,7 @@ private Bundle buildPatchBundle( refInfo, theReplaceReferencesRequest.sourceId)) // We only care about references to our source resource .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), + getFhirPathForPatch(referencingResource, refInfo), new Reference(theReplaceReferencesRequest.targetId.getValueAsString()))) .forEach(params::addParameter); // Add each operation to parameters return params; @@ -110,11 +109,33 @@ private Bundle buildPatchBundle( private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() .getReferenceElement() - .toUnqualifiedVersionless() + .toUnqualified() .getValueAsString() .equals(theSourceId.getValueAsString()); } + private String getFhirPathForPatch(IBaseResource theReferencingResource, ResourceReferenceInfo theRefInfo) { + // construct the path to the element containing the reference in the resource, e.g. "Observation.subject" + String path = theReferencingResource.fhirType() + "." + theRefInfo.getName(); + // check the allowed cardinality of the element containing the reference + int maxCardinality = myFhirContext + .newTerser() + .getDefinition(theReferencingResource.getClass(), path) + .getMax(); + if (maxCardinality != 1) { + // if the element allows high cardinality, specify the exact reference to replace by appending a where + // filter to the path. If we don't do this, all the existing references in the element would be lost as a + // result of getting replaced with the new reference, and that is not the behaviour we want. + // e.g. "Observation.performer.where(reference='Practitioner/123')" + return String.format( + "%s.where(reference='%s')", + path, + theRefInfo.getResourceReference().getReferenceElement().getValueAsString()); + } + // the element allows max cardinality of 1, so the whole element can be safely replaced + return path; + } + @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( String thePath, Type theValue) { diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtilTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtilTest.java index bcd376051642..51faac36e46b 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtilTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/util/ResourceCompartmentUtilTest.java @@ -128,6 +128,13 @@ void whenNoPatientResource_returnsPatientCompartment() { assertThat(result).contains("P01"); // } } + + @Test + void nullResource_shouldNotThrowNPE() { + // The input resource may be null when mass ingestion is enabled. + Optional result = ResourceCompartmentUtil.getPatientCompartmentIdentity(null, myFhirContext, mySearchParamExtractor); + assertThat(result).isEmpty(); + } } private List getMockSearchParams(boolean providePatientCompartment) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java index 73a1f9ba1609..dee950281f5f 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java @@ -139,7 +139,6 @@ public void testOutcomeSuccess() throws IOException { when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED); - doNothing().when(myConsentSvc).completeOperationSuccess(any(), any()); HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient"); 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 f6579bb525fc..530faa86e634 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 @@ -27,6 +27,7 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Base; +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.CodeableConcept; @@ -457,6 +458,10 @@ private static BaseConceptProperty createConceptPropertyR4(final String theName, StringType stringType = (StringType) theValue; conceptProperty = new StringConceptProperty(theName, stringType.getValue()); break; + case IValidationSupport.TYPE_BOOLEAN: + BooleanType booleanType = (BooleanType) theValue; + conceptProperty = new BooleanConceptProperty(theName, booleanType.getValue()); + break; case IValidationSupport.TYPE_CODING: Coding coding = (Coding) theValue; conceptProperty = diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java index eac448ad0ebd..8e7641d89dc9 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.context.support.IValidationSupport.GroupConceptProperty; import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult; import ca.uhn.fhir.context.support.IValidationSupport.StringConceptProperty; +import ca.uhn.fhir.context.support.IValidationSupport.BooleanConceptProperty; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.test.utilities.validation.IValidationProviders; @@ -17,17 +18,18 @@ import java.util.List; import java.util.Optional; +import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_BOOLEAN; import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_CODING; import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_GROUP; import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_STRING; -import static java.util.stream.IntStream.range; -import static org.assertj.core.api.Assertions.assertThat; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_NAME; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY; import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.LANGUAGE; +import static java.util.stream.IntStream.range; +import static org.assertj.core.api.Assertions.assertThat; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createConceptProperty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -198,7 +200,8 @@ default void verifyLookupCodeResult(LookupCodeRequest theRequest, LookupCodeResu assertEquals(theExpectedResult.isCodeIsAbstract(), outcome.isCodeIsAbstract()); assertEquals(theExpectedResult.getProperties().size(), outcome.getProperties().size()); - range(0, outcome.getProperties().size()).forEach(i -> assertEqualConceptProperty(theExpectedResult.getProperties().get(i), outcome.getProperties().get(i))); + range(0, outcome.getProperties().size()).forEach(i -> + assertEqualConceptProperty(theExpectedResult.getProperties().get(i), outcome.getProperties().get(i))); assertEquals(theExpectedResult.getDesignations().size(), outcome.getDesignations().size()); range(0, outcome.getDesignations().size()).forEach(i -> assertEqualConceptDesignation(theExpectedResult.getDesignations().get(i), outcome.getDesignations().get(i))); @@ -225,6 +228,11 @@ private void assertEqualConceptProperty(BaseConceptProperty theProperty, BaseCon StringConceptProperty actual = (StringConceptProperty) theProperty; assertEquals(expected.getValue(), actual.getValue()); } + case TYPE_BOOLEAN -> { + BooleanConceptProperty expected = (BooleanConceptProperty) theExpectedProperty; + IValidationSupport.BooleanConceptProperty actual = (BooleanConceptProperty) theProperty; + assertEquals(expected.getValue(), actual.getValue()); + } case TYPE_CODING -> { CodingConceptProperty expected = (CodingConceptProperty) theExpectedProperty; CodingConceptProperty actual = (CodingConceptProperty) theProperty; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java index 382f621f547e..9b0d4e2ca14c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java @@ -133,7 +133,7 @@ public static Stream getPropertyValueListArguments() { @ParameterizedTest @MethodSource(value = "getPropertyValueArguments") public void lookupCode_forCodeSystemWithProperty_returnsCorrectProperty(IBaseDatatype thePropertyValue) { - verifyLookupWithProperty(List.of(thePropertyValue), List.of()); + verifyLookupWithProperty(List.of(thePropertyValue), List.of(0)); } @ParameterizedTest diff --git a/pom.xml b/pom.xml index 3a2f291d3ac3..19d59dce9aac 100644 --- a/pom.xml +++ b/pom.xml @@ -1037,7 +1037,7 @@ 0.64.8 10.20.1 6.6.4.Final - 1.5.12 + 1.5.16 7.2.1.Final @@ -1089,7 +1089,7 @@ 1.0.1 1.52.0 8.15.3 - 1.0.8 + 1.0.9 3.17.0