From 6c4aa2c154d2a5d7d00982d4b6f77e9df3f7dd78 Mon Sep 17 00:00:00 2001 From: jdar8 <69840459+jdar8@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:28:09 -0800 Subject: [PATCH] Jd 20241126 fix versioned refs in transaction bundles (#6521) * tests, fix, minor refactoring * spotless * changelog * changelog update --------- Co-authored-by: jdar --- .../ca/uhn/fhir/context/ParserOptions.java | 13 ++ ...rsioned-refs-from-transaction-bundles.yaml | 6 + ...irResourceDaoR4VersionedReferenceTest.java | 87 +++++++++++ ...sion-reference-and-auto-version-field.json | 136 +++++++++++++++++ ...sioned-reference-and-pre-existing-ref.json | 56 +++++++ ...h-client-supplied-versioned-reference.json | 115 ++++++++++++++ .../ca/uhn/fhir/jpa/dao/BaseStorageDao.java | 46 +++++- .../jpa/dao/BaseTransactionProcessor.java | 141 +++++++++++------- 8 files changed, 539 insertions(+), 61 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6519-fix-versioned-refs-from-transaction-bundles.yaml create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-assigned-version-reference-and-auto-version-field.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference-and-pre-existing-ref.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference.json diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java index 4115c7e49c8..cd2825f9a2f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ParserOptions.java @@ -23,12 +23,14 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.util.CollectionUtil; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.Validate; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** * This object supplies default configuration to all {@link IParser parser} instances @@ -126,6 +128,17 @@ public class ParserOptions { return myDontStripVersionsFromReferencesAtPaths; } + /** + * Returns a sub-collection of {@link #getDontStripVersionsFromReferencesAtPaths()} containing only paths + * for the given resource type. + */ + public Set getDontStripVersionsFromReferencesAtPathsByResourceType(String theResourceType) { + Validate.notEmpty(theResourceType, "theResourceType must not be null or empty"); + return myDontStripVersionsFromReferencesAtPaths.stream() + .filter(referencePath -> referencePath.startsWith(theResourceType + ".")) + .collect(Collectors.toSet()); + } + /** * If supplied value(s), any resource references at the specified paths will have their * resource versions encoded instead of being automatically stripped during the encoding diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6519-fix-versioned-refs-from-transaction-bundles.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6519-fix-versioned-refs-from-transaction-bundles.yaml new file mode 100644 index 00000000000..7eee7dde4ef --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6519-fix-versioned-refs-from-transaction-bundles.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 6519 +jira: SMILE-8776 +title: "Previously, when posting a transaction bundle with versioned references, the server would strip versioned +references form the resource even when configured not to do so. This has now been fixed." diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java index a4b9be0bcc4..a8187581572 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.MessageHeader; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Provenance; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; @@ -42,6 +43,8 @@ import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -65,6 +68,8 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { myStorageSettings.setDeleteEnabled(new JpaStorageSettings().isDeleteEnabled()); myStorageSettings.setRespectVersionsForSearchIncludes(new JpaStorageSettings().isRespectVersionsForSearchIncludes()); myStorageSettings.setAutoVersionReferenceAtPaths(new JpaStorageSettings().getAutoVersionReferenceAtPaths()); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(new JpaStorageSettings().isAutoCreatePlaceholderReferenceTargets()); + myStorageSettings.setResourceClientIdStrategy(new JpaStorageSettings().getResourceClientIdStrategy()); } @Nested @@ -667,6 +672,88 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { assertEquals(patientIdString, observation.getSubject().getReference()); } + @Test + public void testDontStripVersionsFromRefsAtPaths_inTransactionBundle_shouldPreserveVersion() { + Set referencePaths = Set.of("AuditEvent.entity.what","MessageHeader.focus","Provenance.target","Provenance.entity.what"); + + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(referencePaths); + myStorageSettings.setAutoVersionReferenceAtPaths("Encounter.subject"); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true); + myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); + + postBundleAndAssertProvenanceRefsPreserved("/transaction-bundles/transaction-with-client-supplied-versioned-reference.json"); + } + + @Test + public void testDontStripVersionsFromRefsAtAllPaths_inTransactionBundle_shouldPreserveVersion() { + myFhirContext.getParserOptions().setStripVersionsFromReferences(false); + myStorageSettings.setAutoVersionReferenceAtPaths("Encounter.subject"); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true); + myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); + + postBundleAndAssertProvenanceRefsPreserved("/transaction-bundles/transaction-with-client-supplied-versioned-reference.json"); + } + + @Test + public void testDontStripVersionsFromRefsAtPaths_withTransactionBundleAndAutoVersionSet_shouldPreserveVersion() { + Set referencePaths = Set.of("AuditEvent.entity.what","MessageHeader.focus","Provenance.target","Provenance.entity.what"); + + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(referencePaths); + myStorageSettings.setAutoVersionReferenceAtPaths("Provenance.entity.what"); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true); + myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); + + Encounter encounter = new Encounter(); + encounter.setId("242976"); + myEncounterDao.update(encounter); + + postBundleAndAssertProvenanceRefsPreserved("/transaction-bundles/transaction-with-client-supplied-versioned-reference.json"); + } + + private Provenance postBundleAndAssertProvenanceRefsPreserved(String theBundlePath) { + Bundle bundle = myFhirContext.newJsonParser().parseResource(Bundle.class, + new InputStreamReader( + FhirResourceDaoR4VersionedReferenceTest.class.getResourceAsStream(theBundlePath))); + + Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), bundle); + + Optional provenanceEntry = transaction.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Provenance")).findFirst(); + assertThat(provenanceEntry).isPresent(); + String provenanceLocation = provenanceEntry.get().getResponse().getLocation(); + Provenance provenance = myProvenanceDao.read(new IdDt(provenanceLocation)); + assertThat(provenance.getEntity().get(0).getWhat().getReference()).isEqualTo("Encounter/242976/_history/1"); + return provenance; + } + + @Test + public void testDontStripVersionsFromRefsAtPathsAndAutoVersionSameTransaction_withTransactionBundle_shouldPreserveVersion() { + Set referencePaths = Set.of("AuditEvent.entity.what","MessageHeader.focus","Provenance.target","Provenance.entity.what", "Provenance.agent.who"); + + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(referencePaths); + myStorageSettings.setAutoVersionReferenceAtPaths("Provenance.agent.who"); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true); + myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); + + Provenance provenance = postBundleAndAssertProvenanceRefsPreserved("/transaction-bundles/transaction-with-client-assigned-version-reference-and-auto-version-field.json"); + assertThat(provenance.getAgent().get(0).getWho().getReference()).isEqualTo("Patient/237643/_history/1"); + } + + @Test + public void testDontStripVersionsFromRefsAtPaths_withTransactionBundleAndPreExistingReference_shouldPreserveVersion() { + Set referencePaths = Set.of("AuditEvent.entity.what","MessageHeader.focus","Provenance.target","Provenance.entity.what"); + + myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(referencePaths); + myStorageSettings.setAutoVersionReferenceAtPaths("Provenance.entity.what"); + myStorageSettings.setAutoCreatePlaceholderReferenceTargets(true); + myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); + + Encounter encounter = new Encounter(); + encounter.setId("242976"); + myEncounterDao.update(encounter); + + postBundleAndAssertProvenanceRefsPreserved("/transaction-bundles/transaction-with-client-supplied-versioned-reference-and-pre-existing-ref.json"); + } + @Test public void testDontOverwriteExistingVersion() { myFhirContext.getParserOptions().setStripVersionsFromReferences(false); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-assigned-version-reference-and-auto-version-field.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-assigned-version-reference-and-auto-version-field.json new file mode 100644 index 00000000000..c0ed1aac105 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-assigned-version-reference-and-auto-version-field.json @@ -0,0 +1,136 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "https://smilecdrmock.harrisarc.ca/fhir-system/Encounter/242976", + "resource": { + "resourceType": "Encounter", + "id": "242976", + "meta": { + "versionId": "4", + "lastUpdated": "2024-07-26T10:43:49.287-04:00", + "source": "#d9d63c433c828a4d" + }, + "identifier": [ + { + "use": "official", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "VN" + } + ] + }, + "system": "https://www.ciussscentreouest.ca/ids/visit-number/iclsc/dlm", + "value": "ICLSCRepair1-1" + } + ], + "status": "entered-in-error", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "Ambulatory" + }, + "type": [ + { + "coding": [ + { + "system": "https://www.ciussscentreouest.ca/codesystem/mode/iclsc", + "code": "1", + "display": "1 Rencontre présence usager" + } + ] + } + ], + "subject": { + "reference": "Patient/237643" + }, + "period": { + "start": "2020-07-26T13:36:00-04:00", + "end": "2020-07-26T14:06:00-04:00" + }, + "length": { + "value": 30, + "unit": "min" + } + }, + "request": { + "method": "PUT", + "url": "Encounter/242976" + } + }, + { + "resource": { + "resourceType": "Provenance", + "target": [ + { + "reference": "#" + } + ], + "recorded": "2024-07-26T14:51:28.222+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE", + "display": "Create" + } + ] + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleClass", + "code": "AGNT", + "display": "Agent" + } + ] + } + ], + "who": { + "display": "ICLSC Encounter Repair loop", + "reference": "Patient/237643" + } + } + ], + "entity": [ + { + "role": "source", + "what": { + "reference": "Encounter/242976/_history/1" + } + } + ] + }, + "request": { + "method": "POST", + "url": "Provenance" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "237643", + "identifier" : [{ + "system" : "urn:oid:1.2.36.146.595.217.0.1", + "value" : "abc" + }], + "name": [ { + "family": "smith", + "given": [ "John" ] + } ], + "gender": "male", + "birthDate": "1988-08-10" + }, + "request": { + "method": "PUT", + "url": "Patient/237643" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference-and-pre-existing-ref.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference-and-pre-existing-ref.json new file mode 100644 index 00000000000..a94a568dec4 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference-and-pre-existing-ref.json @@ -0,0 +1,56 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Provenance", + "target": [ + { + "reference": "#" + } + ], + "recorded": "2024-07-26T14:51:28.222+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE", + "display": "Create" + } + ] + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleClass", + "code": "AGNT", + "display": "Agent" + } + ] + } + ], + "who": { + "display": "ICLSC Encounter Repair loop" + } + } + ], + "entity": [ + { + "role": "source", + "what": { + "reference": "Encounter/242976/_history/1" + } + } + ] + }, + "request": { + "method": "POST", + "url": "Provenance" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference.json new file mode 100644 index 00000000000..a2c804f5057 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/transaction-bundles/transaction-with-client-supplied-versioned-reference.json @@ -0,0 +1,115 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "https://smilecdrmock.harrisarc.ca/fhir-system/Encounter/242976", + "resource": { + "resourceType": "Encounter", + "id": "242976", + "meta": { + "versionId": "4", + "lastUpdated": "2024-07-26T10:43:49.287-04:00", + "source": "#d9d63c433c828a4d" + }, + "identifier": [ + { + "use": "official", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "VN" + } + ] + }, + "system": "https://www.ciussscentreouest.ca/ids/visit-number/iclsc/dlm", + "value": "ICLSCRepair1-1" + } + ], + "status": "entered-in-error", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "Ambulatory" + }, + "type": [ + { + "coding": [ + { + "system": "https://www.ciussscentreouest.ca/codesystem/mode/iclsc", + "code": "1", + "display": "1 Rencontre présence usager" + } + ] + } + ], + "subject": { + "reference": "Patient/237643" + }, + "period": { + "start": "2020-07-26T13:36:00-04:00", + "end": "2020-07-26T14:06:00-04:00" + }, + "length": { + "value": 30, + "unit": "min" + } + }, + "request": { + "method": "PUT", + "url": "Encounter/242976" + } + }, + { + "resource": { + "resourceType": "Provenance", + "target": [ + { + "reference": "#" + } + ], + "recorded": "2024-07-26T14:51:28.222+00:00", + "activity": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "CREATE", + "display": "Create" + } + ] + }, + "agent": [ + { + "role": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleClass", + "code": "AGNT", + "display": "Agent" + } + ] + } + ], + "who": { + "display": "ICLSC Encounter Repair loop" + } + } + ], + "entity": [ + { + "role": "source", + "what": { + "reference": "Encounter/242976/_history/1" + } + } + ] + }, + "request": { + "method": "POST", + "url": "Provenance" + } + } + ] +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java index 554051f607c..b087c212a5c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java @@ -727,6 +727,27 @@ public abstract class BaseStorageDao { ourLog.debug(msg); } + /** + * Extracts a list of references that have versions in their ID whose versions should not be stripped + * + * @return A set of references that should not have their client-given versions stripped according to the + * versioned references settings. + */ + public static Set extractReferencesToAvoidReplacement( + FhirContext theFhirContext, IBaseResource theResource) { + if (!theFhirContext + .getParserOptions() + .getDontStripVersionsFromReferencesAtPaths() + .isEmpty()) { + String theResourceType = theFhirContext.getResourceType(theResource); + Set versionReferencesPaths = theFhirContext + .getParserOptions() + .getDontStripVersionsFromReferencesAtPathsByResourceType(theResourceType); + return getReferencesWithOrWithoutVersionId(versionReferencesPaths, theFhirContext, theResource, false); + } + return Collections.emptySet(); + } + /** * Extracts a list of references that should be auto-versioned. * @@ -760,7 +781,7 @@ public abstract class BaseStorageDao { MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType); if (!autoVersionReferencesAtPaths.isEmpty()) { - return getReferencesWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource); + return getReferencesWithOrWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource, true); } return Collections.emptySet(); } @@ -776,17 +797,30 @@ public abstract class BaseStorageDao { String resourceName = theFhirContext.getResourceType(theResource); Set autoVersionReferencesPaths = theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName); - return getReferencesWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource); + return getReferencesWithOrWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource, true); } return Collections.emptySet(); } - private static Set getReferencesWithoutVersionId( - Set autoVersionReferencesPaths, FhirContext theFhirContext, IBaseResource theResource) { - return autoVersionReferencesPaths.stream() + /** + * Extracts references from given resource and filters references by those with versions, or those without versions. + * + * @param theVersionReferencesPaths the paths from which to extract references from + * @param theFhirContext the FHIR context + * @param theResource the resource from which to extract references from + * @param theShouldFilterByRefsWithoutVersionId If true, this method will return only references without a version. If false, this method will return only references with a version. + * @return Set of references contained in the resource with or without versions + */ + private static Set getReferencesWithOrWithoutVersionId( + Set theVersionReferencesPaths, + FhirContext theFhirContext, + IBaseResource theResource, + boolean theShouldFilterByRefsWithoutVersionId) { + return theVersionReferencesPaths.stream() .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class)) .flatMap(Collection::stream) - .filter(reference -> !reference.getReferenceElement().hasVersionIdPart()) + .filter(reference -> + reference.getReferenceElement().hasVersionIdPart() ^ theShouldFilterByRefsWithoutVersionId) .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) .keySet(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index 777f87a456f..e219c574529 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -274,19 +274,20 @@ public abstract class BaseTransactionProcessor { @SuppressWarnings("unchecked") private void handleTransactionCreateOrUpdateOutcome( - IdSubstitutionMap idSubstitutions, - Map idToPersistedOutcome, - IIdType nextResourceId, - DaoMethodOutcome outcome, - IBase newEntry, + IdSubstitutionMap theIdSubstitutions, + Map theIdToPersistedOutcome, + IIdType theNextResourceId, + DaoMethodOutcome theOutcome, + IBase theNewEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) { - IIdType newId = outcome.getId().toUnqualified(); - IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); - if (newId.equals(resourceId) == false) { - if (!nextResourceId.isEmpty()) { - idSubstitutions.put(resourceId, newId); + IIdType newId = theOutcome.getId().toUnqualified(); + IIdType resourceId = + isPlaceholder(theNextResourceId) ? theNextResourceId : theNextResourceId.toUnqualifiedVersionless(); + if (!newId.equals(resourceId)) { + if (!theNextResourceId.isEmpty()) { + theIdSubstitutions.put(resourceId, newId); } if (isPlaceholder(resourceId)) { /* @@ -294,27 +295,27 @@ public abstract class BaseTransactionProcessor { */ IIdType id = myContext.getVersion().newIdType(); id.setValue(theResourceType + '/' + resourceId.getValue()); - idSubstitutions.put(id, newId); + theIdSubstitutions.put(id, newId); } } - populateIdToPersistedOutcomeMap(idToPersistedOutcome, newId, outcome); + populateIdToPersistedOutcomeMap(theIdToPersistedOutcome, newId, theOutcome); - if (shouldSwapBinaryToActualResource(theRes, theResourceType, nextResourceId)) { - theRes = idToPersistedOutcome.get(newId).getResource(); + if (shouldSwapBinaryToActualResource(theRes, theResourceType, theNextResourceId)) { + theRes = theIdToPersistedOutcome.get(newId).getResource(); } - if (outcome.getCreated()) { - myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED)); + if (theOutcome.getCreated()) { + myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED)); } else { - myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); + myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); } Date lastModified = getLastModified(theRes); - myVersionAdapter.setResponseLastModified(newEntry, lastModified); + myVersionAdapter.setResponseLastModified(theNewEntry, lastModified); - if (outcome.getOperationOutcome() != null) { - myVersionAdapter.setResponseOutcome(newEntry, outcome.getOperationOutcome()); + if (theOutcome.getOperationOutcome() != null) { + myVersionAdapter.setResponseOutcome(theNewEntry, theOutcome.getOperationOutcome()); } if (theRequestDetails != null) { @@ -323,9 +324,9 @@ public abstract class BaseTransactionProcessor { RestfulServerUtils.parsePreferHeader(null, prefer).getReturn(); if (preferReturn != null) { if (preferReturn == PreferReturnEnum.REPRESENTATION) { - if (outcome.getResource() != null) { - outcome.fireResourceViewCallbacks(); - myVersionAdapter.setResource(newEntry, outcome.getResource()); + if (theOutcome.getResource() != null) { + theOutcome.fireResourceViewCallbacks(); + myVersionAdapter.setResource(theNewEntry, theOutcome.getResource()); } } } @@ -1685,9 +1686,9 @@ public abstract class BaseTransactionProcessor { IdSubstitutionMap theIdSubstitutions, Map theIdToPersistedOutcome, StopWatch theTransactionStopWatch, - EntriesToProcessMap entriesToProcess, - Set nonUpdatedEntities, - Set updatedEntities) { + EntriesToProcessMap theEntriesToProcess, + Set theNonUpdatedEntities, + Set theUpdatedEntities) { FhirTerser terser = myContext.newTerser(); theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); IdentityHashMap> deferredIndexesForAutoVersioning = null; @@ -1712,6 +1713,9 @@ public abstract class BaseTransactionProcessor { Set referencesToAutoVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myStorageSettings, nextResource); + Set referencesToKeepClientSuppliedVersion = + BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource); + if (referencesToAutoVersion.isEmpty()) { // no references to autoversion - we can do the resolve and save now resolveReferencesThenSaveAndIndexResource( @@ -1719,13 +1723,14 @@ public abstract class BaseTransactionProcessor { theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, - entriesToProcess, - nonUpdatedEntities, - updatedEntities, + theEntriesToProcess, + theNonUpdatedEntities, + theUpdatedEntities, terser, nextOutcome, nextResource, - referencesToAutoVersion); // this is empty + referencesToAutoVersion, // this is empty + referencesToKeepClientSuppliedVersion); } else { // we have autoversioned things to defer until later if (deferredIndexesForAutoVersioning == null) { @@ -1742,19 +1747,22 @@ public abstract class BaseTransactionProcessor { DaoMethodOutcome nextOutcome = nextEntry.getKey(); Set referencesToAutoVersion = nextEntry.getValue(); IBaseResource nextResource = nextOutcome.getResource(); + Set referencesToKeepClientSuppliedVersion = + BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource); resolveReferencesThenSaveAndIndexResource( theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, - entriesToProcess, - nonUpdatedEntities, - updatedEntities, + theEntriesToProcess, + theNonUpdatedEntities, + theUpdatedEntities, terser, nextOutcome, nextResource, - referencesToAutoVersion); + referencesToAutoVersion, + referencesToKeepClientSuppliedVersion); } } } @@ -1764,15 +1772,16 @@ public abstract class BaseTransactionProcessor { TransactionDetails theTransactionDetails, IdSubstitutionMap theIdSubstitutions, Map theIdToPersistedOutcome, - EntriesToProcessMap entriesToProcess, - Set nonUpdatedEntities, - Set updatedEntities, - FhirTerser terser, + EntriesToProcessMap theEntriesToProcess, + Set theNonUpdatedEntities, + Set theUpdatedEntities, + FhirTerser theTerser, DaoMethodOutcome theDaoMethodOutcome, IBaseResource theResource, - Set theReferencesToAutoVersion) { + Set theReferencesToAutoVersion, + Set theReferencesToKeepClientSuppliedVersion) { // References - List allRefs = terser.getAllResourceReferences(theResource); + List allRefs = theTerser.getAllResourceReferences(theResource); for (ResourceReferenceInfo nextRef : allRefs) { IBaseReference resourceReference = nextRef.getResourceReference(); IIdType nextId = resourceReference.getReferenceElement(); @@ -1794,16 +1803,18 @@ public abstract class BaseTransactionProcessor { } } if (newId != null || theIdSubstitutions.containsSource(nextId)) { - if (newId == null) { - newId = theIdSubstitutions.getForSource(nextId); - } - if (newId != null) { - ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - - if (theReferencesToAutoVersion.contains(resourceReference)) { - replaceResourceReference(newId, resourceReference, theTransactionDetails); - } else { - replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails); + if (shouldReplaceResourceReference( + theReferencesToAutoVersion, theReferencesToKeepClientSuppliedVersion, resourceReference)) { + if (newId == null) { + newId = theIdSubstitutions.getForSource(nextId); + } + if (newId != null) { + ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); + if (theReferencesToAutoVersion.contains(resourceReference)) { + replaceResourceReference(newId, resourceReference, theTransactionDetails); + } else { + replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails); + } } } } else if (nextId.getValue().startsWith("urn:")) { @@ -1858,7 +1869,7 @@ public abstract class BaseTransactionProcessor { // URIs Class> uriType = (Class>) myContext.getElementDefinition("uri").getImplementingClass(); - List> allUris = terser.getAllPopulatedChildElementsOfType(theResource, uriType); + List> allUris = theTerser.getAllPopulatedChildElementsOfType(theResource, uriType); for (IPrimitiveType nextRef : allUris) { if (nextRef instanceof IIdType) { continue; // No substitution on the resource ID itself! @@ -1886,7 +1897,7 @@ public abstract class BaseTransactionProcessor { IJpaDao jpaDao = (IJpaDao) dao; IBasePersistedResource updateOutcome = null; - if (updatedEntities.contains(theDaoMethodOutcome.getEntity())) { + if (theUpdatedEntities.contains(theDaoMethodOutcome.getEntity())) { boolean forceUpdateVersion = !theReferencesToAutoVersion.isEmpty(); String matchUrl = theDaoMethodOutcome.getMatchUrl(); RestOperationTypeEnum operationType = theDaoMethodOutcome.getOperationType(); @@ -1903,7 +1914,7 @@ public abstract class BaseTransactionProcessor { theTransactionDetails); updateOutcome = daoMethodOutcome.getEntity(); theDaoMethodOutcome = daoMethodOutcome; - } else if (!nonUpdatedEntities.contains(theDaoMethodOutcome.getId())) { + } else if (!theNonUpdatedEntities.contains(theDaoMethodOutcome.getId())) { updateOutcome = jpaDao.updateEntity( theRequest, theResource, @@ -1920,7 +1931,7 @@ public abstract class BaseTransactionProcessor { if (updateOutcome != null) { IIdType newId = updateOutcome.getIdDt(); - IIdType entryId = entriesToProcess.getIdWithVersionlessComparison(newId); + IIdType entryId = theEntriesToProcess.getIdWithVersionlessComparison(newId); if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) { entryId.setValue(newId.getValue()); } @@ -1930,12 +1941,32 @@ public abstract class BaseTransactionProcessor { theIdSubstitutions.updateTargets(newId); if (theDaoMethodOutcome.getOperationOutcome() != null) { - IBase responseEntry = entriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); + IBase responseEntry = theEntriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); myVersionAdapter.setResponseOutcome(responseEntry, theDaoMethodOutcome.getOperationOutcome()); } } } + /** + * We should replace the references when + * 1. It is not a reference we should keep the client-supplied version for as configured by `DontStripVersionsFromReferences` or + * 2. It is a reference that has been identified for auto versioning or + * 3. Is a placeholder reference + * @param theReferencesToAutoVersion list of references identified for auto versioning + * @param theReferencesToKeepClientSuppliedVersion list of references that we should not strip the version for + * @param theResourceReference the resource reference + * @return true if we should replace the resource reference, false if we should keep the client provided reference + */ + private boolean shouldReplaceResourceReference( + Set theReferencesToAutoVersion, + Set theReferencesToKeepClientSuppliedVersion, + IBaseReference theResourceReference) { + return (!theReferencesToKeepClientSuppliedVersion.contains(theResourceReference) + && myContext.getParserOptions().isStripVersionsFromReferences()) + || theReferencesToAutoVersion.contains(theResourceReference) + || isPlaceholder(theResourceReference.getReferenceElement()); + } + private void replaceResourceReference( IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) { addRollbackReferenceRestore(theTransactionDetails, theResourceReference);