Jd 20241126 fix versioned refs in transaction bundles (#6521)

* tests, fix, minor refactoring

* spotless

* changelog

* changelog update

---------

Co-authored-by: jdar <justin.dar@smiledigitalhealth.com>
This commit is contained in:
jdar8 2024-12-05 13:28:09 -08:00 committed by GitHub
parent e59e7fc29f
commit 6c4aa2c154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 539 additions and 61 deletions

View File

@ -23,12 +23,14 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.util.CollectionUtil; import ca.uhn.fhir.util.CollectionUtil;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import org.apache.commons.lang3.Validate;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
/** /**
* This object supplies default configuration to all {@link IParser parser} instances * This object supplies default configuration to all {@link IParser parser} instances
@ -126,6 +128,17 @@ public class ParserOptions {
return myDontStripVersionsFromReferencesAtPaths; return myDontStripVersionsFromReferencesAtPaths;
} }
/**
* Returns a sub-collection of {@link #getDontStripVersionsFromReferencesAtPaths()} containing only paths
* for the given resource type.
*/
public Set<String> 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 * 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 * resource versions encoded instead of being automatically stripped during the encoding

View File

@ -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."

View File

@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.MessageHeader;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient; 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.Reference;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Task;
@ -42,6 +43,8 @@ import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -65,6 +68,8 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
myStorageSettings.setDeleteEnabled(new JpaStorageSettings().isDeleteEnabled()); myStorageSettings.setDeleteEnabled(new JpaStorageSettings().isDeleteEnabled());
myStorageSettings.setRespectVersionsForSearchIncludes(new JpaStorageSettings().isRespectVersionsForSearchIncludes()); myStorageSettings.setRespectVersionsForSearchIncludes(new JpaStorageSettings().isRespectVersionsForSearchIncludes());
myStorageSettings.setAutoVersionReferenceAtPaths(new JpaStorageSettings().getAutoVersionReferenceAtPaths()); myStorageSettings.setAutoVersionReferenceAtPaths(new JpaStorageSettings().getAutoVersionReferenceAtPaths());
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(new JpaStorageSettings().isAutoCreatePlaceholderReferenceTargets());
myStorageSettings.setResourceClientIdStrategy(new JpaStorageSettings().getResourceClientIdStrategy());
} }
@Nested @Nested
@ -667,6 +672,88 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
assertEquals(patientIdString, observation.getSubject().getReference()); assertEquals(patientIdString, observation.getSubject().getReference());
} }
@Test
public void testDontStripVersionsFromRefsAtPaths_inTransactionBundle_shouldPreserveVersion() {
Set<String> 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<String> 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<Bundle.BundleEntryComponent> 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<String> 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<String> 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 @Test
public void testDontOverwriteExistingVersion() { public void testDontOverwriteExistingVersion() {
myFhirContext.getParserOptions().setStripVersionsFromReferences(false); myFhirContext.getParserOptions().setStripVersionsFromReferences(false);

View File

@ -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"
}
}
]
}

View File

@ -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"
}
}
]
}

View File

@ -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"
}
}
]
}

View File

@ -727,6 +727,27 @@ public abstract class BaseStorageDao {
ourLog.debug(msg); 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<IBaseReference> extractReferencesToAvoidReplacement(
FhirContext theFhirContext, IBaseResource theResource) {
if (!theFhirContext
.getParserOptions()
.getDontStripVersionsFromReferencesAtPaths()
.isEmpty()) {
String theResourceType = theFhirContext.getResourceType(theResource);
Set<String> versionReferencesPaths = theFhirContext
.getParserOptions()
.getDontStripVersionsFromReferencesAtPathsByResourceType(theResourceType);
return getReferencesWithOrWithoutVersionId(versionReferencesPaths, theFhirContext, theResource, false);
}
return Collections.emptySet();
}
/** /**
* Extracts a list of references that should be auto-versioned. * Extracts a list of references that should be auto-versioned.
* *
@ -760,7 +781,7 @@ public abstract class BaseStorageDao {
MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType); MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType);
if (!autoVersionReferencesAtPaths.isEmpty()) { if (!autoVersionReferencesAtPaths.isEmpty()) {
return getReferencesWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource); return getReferencesWithOrWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource, true);
} }
return Collections.emptySet(); return Collections.emptySet();
} }
@ -776,17 +797,30 @@ public abstract class BaseStorageDao {
String resourceName = theFhirContext.getResourceType(theResource); String resourceName = theFhirContext.getResourceType(theResource);
Set<String> autoVersionReferencesPaths = Set<String> autoVersionReferencesPaths =
theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName); theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName);
return getReferencesWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource); return getReferencesWithOrWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource, true);
} }
return Collections.emptySet(); return Collections.emptySet();
} }
private static Set<IBaseReference> getReferencesWithoutVersionId( /**
Set<String> autoVersionReferencesPaths, FhirContext theFhirContext, IBaseResource theResource) { * Extracts references from given resource and filters references by those with versions, or those without versions.
return autoVersionReferencesPaths.stream() *
* @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<IBaseReference> getReferencesWithOrWithoutVersionId(
Set<String> theVersionReferencesPaths,
FhirContext theFhirContext,
IBaseResource theResource,
boolean theShouldFilterByRefsWithoutVersionId) {
return theVersionReferencesPaths.stream()
.map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class)) .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class))
.flatMap(Collection::stream) .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)) .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new))
.keySet(); .keySet();
} }

View File

@ -274,19 +274,20 @@ public abstract class BaseTransactionProcessor {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void handleTransactionCreateOrUpdateOutcome( private void handleTransactionCreateOrUpdateOutcome(
IdSubstitutionMap idSubstitutions, IdSubstitutionMap theIdSubstitutions,
Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
IIdType nextResourceId, IIdType theNextResourceId,
DaoMethodOutcome outcome, DaoMethodOutcome theOutcome,
IBase newEntry, IBase theNewEntry,
String theResourceType, String theResourceType,
IBaseResource theRes, IBaseResource theRes,
RequestDetails theRequestDetails) { RequestDetails theRequestDetails) {
IIdType newId = outcome.getId().toUnqualified(); IIdType newId = theOutcome.getId().toUnqualified();
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); IIdType resourceId =
if (newId.equals(resourceId) == false) { isPlaceholder(theNextResourceId) ? theNextResourceId : theNextResourceId.toUnqualifiedVersionless();
if (!nextResourceId.isEmpty()) { if (!newId.equals(resourceId)) {
idSubstitutions.put(resourceId, newId); if (!theNextResourceId.isEmpty()) {
theIdSubstitutions.put(resourceId, newId);
} }
if (isPlaceholder(resourceId)) { if (isPlaceholder(resourceId)) {
/* /*
@ -294,27 +295,27 @@ public abstract class BaseTransactionProcessor {
*/ */
IIdType id = myContext.getVersion().newIdType(); IIdType id = myContext.getVersion().newIdType();
id.setValue(theResourceType + '/' + resourceId.getValue()); 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)) { if (shouldSwapBinaryToActualResource(theRes, theResourceType, theNextResourceId)) {
theRes = idToPersistedOutcome.get(newId).getResource(); theRes = theIdToPersistedOutcome.get(newId).getResource();
} }
if (outcome.getCreated()) { if (theOutcome.getCreated()) {
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED)); myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED));
} else { } else {
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
} }
Date lastModified = getLastModified(theRes); Date lastModified = getLastModified(theRes);
myVersionAdapter.setResponseLastModified(newEntry, lastModified); myVersionAdapter.setResponseLastModified(theNewEntry, lastModified);
if (outcome.getOperationOutcome() != null) { if (theOutcome.getOperationOutcome() != null) {
myVersionAdapter.setResponseOutcome(newEntry, outcome.getOperationOutcome()); myVersionAdapter.setResponseOutcome(theNewEntry, theOutcome.getOperationOutcome());
} }
if (theRequestDetails != null) { if (theRequestDetails != null) {
@ -323,9 +324,9 @@ public abstract class BaseTransactionProcessor {
RestfulServerUtils.parsePreferHeader(null, prefer).getReturn(); RestfulServerUtils.parsePreferHeader(null, prefer).getReturn();
if (preferReturn != null) { if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) { if (preferReturn == PreferReturnEnum.REPRESENTATION) {
if (outcome.getResource() != null) { if (theOutcome.getResource() != null) {
outcome.fireResourceViewCallbacks(); theOutcome.fireResourceViewCallbacks();
myVersionAdapter.setResource(newEntry, outcome.getResource()); myVersionAdapter.setResource(theNewEntry, theOutcome.getResource());
} }
} }
} }
@ -1685,9 +1686,9 @@ public abstract class BaseTransactionProcessor {
IdSubstitutionMap theIdSubstitutions, IdSubstitutionMap theIdSubstitutions,
Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
StopWatch theTransactionStopWatch, StopWatch theTransactionStopWatch,
EntriesToProcessMap entriesToProcess, EntriesToProcessMap theEntriesToProcess,
Set<IIdType> nonUpdatedEntities, Set<IIdType> theNonUpdatedEntities,
Set<IBasePersistedResource> updatedEntities) { Set<IBasePersistedResource> theUpdatedEntities) {
FhirTerser terser = myContext.newTerser(); FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null; IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null;
@ -1712,6 +1713,9 @@ public abstract class BaseTransactionProcessor {
Set<IBaseReference> referencesToAutoVersion = Set<IBaseReference> referencesToAutoVersion =
BaseStorageDao.extractReferencesToAutoVersion(myContext, myStorageSettings, nextResource); BaseStorageDao.extractReferencesToAutoVersion(myContext, myStorageSettings, nextResource);
Set<IBaseReference> referencesToKeepClientSuppliedVersion =
BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource);
if (referencesToAutoVersion.isEmpty()) { if (referencesToAutoVersion.isEmpty()) {
// no references to autoversion - we can do the resolve and save now // no references to autoversion - we can do the resolve and save now
resolveReferencesThenSaveAndIndexResource( resolveReferencesThenSaveAndIndexResource(
@ -1719,13 +1723,14 @@ public abstract class BaseTransactionProcessor {
theTransactionDetails, theTransactionDetails,
theIdSubstitutions, theIdSubstitutions,
theIdToPersistedOutcome, theIdToPersistedOutcome,
entriesToProcess, theEntriesToProcess,
nonUpdatedEntities, theNonUpdatedEntities,
updatedEntities, theUpdatedEntities,
terser, terser,
nextOutcome, nextOutcome,
nextResource, nextResource,
referencesToAutoVersion); // this is empty referencesToAutoVersion, // this is empty
referencesToKeepClientSuppliedVersion);
} else { } else {
// we have autoversioned things to defer until later // we have autoversioned things to defer until later
if (deferredIndexesForAutoVersioning == null) { if (deferredIndexesForAutoVersioning == null) {
@ -1742,19 +1747,22 @@ public abstract class BaseTransactionProcessor {
DaoMethodOutcome nextOutcome = nextEntry.getKey(); DaoMethodOutcome nextOutcome = nextEntry.getKey();
Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue(); Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue();
IBaseResource nextResource = nextOutcome.getResource(); IBaseResource nextResource = nextOutcome.getResource();
Set<IBaseReference> referencesToKeepClientSuppliedVersion =
BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource);
resolveReferencesThenSaveAndIndexResource( resolveReferencesThenSaveAndIndexResource(
theRequest, theRequest,
theTransactionDetails, theTransactionDetails,
theIdSubstitutions, theIdSubstitutions,
theIdToPersistedOutcome, theIdToPersistedOutcome,
entriesToProcess, theEntriesToProcess,
nonUpdatedEntities, theNonUpdatedEntities,
updatedEntities, theUpdatedEntities,
terser, terser,
nextOutcome, nextOutcome,
nextResource, nextResource,
referencesToAutoVersion); referencesToAutoVersion,
referencesToKeepClientSuppliedVersion);
} }
} }
} }
@ -1764,15 +1772,16 @@ public abstract class BaseTransactionProcessor {
TransactionDetails theTransactionDetails, TransactionDetails theTransactionDetails,
IdSubstitutionMap theIdSubstitutions, IdSubstitutionMap theIdSubstitutions,
Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
EntriesToProcessMap entriesToProcess, EntriesToProcessMap theEntriesToProcess,
Set<IIdType> nonUpdatedEntities, Set<IIdType> theNonUpdatedEntities,
Set<IBasePersistedResource> updatedEntities, Set<IBasePersistedResource> theUpdatedEntities,
FhirTerser terser, FhirTerser theTerser,
DaoMethodOutcome theDaoMethodOutcome, DaoMethodOutcome theDaoMethodOutcome,
IBaseResource theResource, IBaseResource theResource,
Set<IBaseReference> theReferencesToAutoVersion) { Set<IBaseReference> theReferencesToAutoVersion,
Set<IBaseReference> theReferencesToKeepClientSuppliedVersion) {
// References // References
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(theResource); List<ResourceReferenceInfo> allRefs = theTerser.getAllResourceReferences(theResource);
for (ResourceReferenceInfo nextRef : allRefs) { for (ResourceReferenceInfo nextRef : allRefs) {
IBaseReference resourceReference = nextRef.getResourceReference(); IBaseReference resourceReference = nextRef.getResourceReference();
IIdType nextId = resourceReference.getReferenceElement(); IIdType nextId = resourceReference.getReferenceElement();
@ -1794,18 +1803,20 @@ public abstract class BaseTransactionProcessor {
} }
} }
if (newId != null || theIdSubstitutions.containsSource(nextId)) { if (newId != null || theIdSubstitutions.containsSource(nextId)) {
if (shouldReplaceResourceReference(
theReferencesToAutoVersion, theReferencesToKeepClientSuppliedVersion, resourceReference)) {
if (newId == null) { if (newId == null) {
newId = theIdSubstitutions.getForSource(nextId); newId = theIdSubstitutions.getForSource(nextId);
} }
if (newId != null) { if (newId != null) {
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
if (theReferencesToAutoVersion.contains(resourceReference)) { if (theReferencesToAutoVersion.contains(resourceReference)) {
replaceResourceReference(newId, resourceReference, theTransactionDetails); replaceResourceReference(newId, resourceReference, theTransactionDetails);
} else { } else {
replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails); replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails);
} }
} }
}
} else if (nextId.getValue().startsWith("urn:")) { } else if (nextId.getValue().startsWith("urn:")) {
throw new InvalidRequestException( throw new InvalidRequestException(
Msg.code(541) + "Unable to satisfy placeholder ID " + nextId.getValue() Msg.code(541) + "Unable to satisfy placeholder ID " + nextId.getValue()
@ -1858,7 +1869,7 @@ public abstract class BaseTransactionProcessor {
// URIs // URIs
Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>)
myContext.getElementDefinition("uri").getImplementingClass(); myContext.getElementDefinition("uri").getImplementingClass();
List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(theResource, uriType); List<? extends IPrimitiveType<?>> allUris = theTerser.getAllPopulatedChildElementsOfType(theResource, uriType);
for (IPrimitiveType<?> nextRef : allUris) { for (IPrimitiveType<?> nextRef : allUris) {
if (nextRef instanceof IIdType) { if (nextRef instanceof IIdType) {
continue; // No substitution on the resource ID itself! continue; // No substitution on the resource ID itself!
@ -1886,7 +1897,7 @@ public abstract class BaseTransactionProcessor {
IJpaDao jpaDao = (IJpaDao) dao; IJpaDao jpaDao = (IJpaDao) dao;
IBasePersistedResource updateOutcome = null; IBasePersistedResource updateOutcome = null;
if (updatedEntities.contains(theDaoMethodOutcome.getEntity())) { if (theUpdatedEntities.contains(theDaoMethodOutcome.getEntity())) {
boolean forceUpdateVersion = !theReferencesToAutoVersion.isEmpty(); boolean forceUpdateVersion = !theReferencesToAutoVersion.isEmpty();
String matchUrl = theDaoMethodOutcome.getMatchUrl(); String matchUrl = theDaoMethodOutcome.getMatchUrl();
RestOperationTypeEnum operationType = theDaoMethodOutcome.getOperationType(); RestOperationTypeEnum operationType = theDaoMethodOutcome.getOperationType();
@ -1903,7 +1914,7 @@ public abstract class BaseTransactionProcessor {
theTransactionDetails); theTransactionDetails);
updateOutcome = daoMethodOutcome.getEntity(); updateOutcome = daoMethodOutcome.getEntity();
theDaoMethodOutcome = daoMethodOutcome; theDaoMethodOutcome = daoMethodOutcome;
} else if (!nonUpdatedEntities.contains(theDaoMethodOutcome.getId())) { } else if (!theNonUpdatedEntities.contains(theDaoMethodOutcome.getId())) {
updateOutcome = jpaDao.updateEntity( updateOutcome = jpaDao.updateEntity(
theRequest, theRequest,
theResource, theResource,
@ -1920,7 +1931,7 @@ public abstract class BaseTransactionProcessor {
if (updateOutcome != null) { if (updateOutcome != null) {
IIdType newId = updateOutcome.getIdDt(); IIdType newId = updateOutcome.getIdDt();
IIdType entryId = entriesToProcess.getIdWithVersionlessComparison(newId); IIdType entryId = theEntriesToProcess.getIdWithVersionlessComparison(newId);
if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) { if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) {
entryId.setValue(newId.getValue()); entryId.setValue(newId.getValue());
} }
@ -1930,12 +1941,32 @@ public abstract class BaseTransactionProcessor {
theIdSubstitutions.updateTargets(newId); theIdSubstitutions.updateTargets(newId);
if (theDaoMethodOutcome.getOperationOutcome() != null) { if (theDaoMethodOutcome.getOperationOutcome() != null) {
IBase responseEntry = entriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); IBase responseEntry = theEntriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId);
myVersionAdapter.setResponseOutcome(responseEntry, theDaoMethodOutcome.getOperationOutcome()); 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<IBaseReference> theReferencesToAutoVersion,
Set<IBaseReference> theReferencesToKeepClientSuppliedVersion,
IBaseReference theResourceReference) {
return (!theReferencesToKeepClientSuppliedVersion.contains(theResourceReference)
&& myContext.getParserOptions().isStripVersionsFromReferences())
|| theReferencesToAutoVersion.contains(theResourceReference)
|| isPlaceholder(theResourceReference.getReferenceElement());
}
private void replaceResourceReference( private void replaceResourceReference(
IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) { IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) {
addRollbackReferenceRestore(theTransactionDetails, theResourceReference); addRollbackReferenceRestore(theTransactionDetails, theResourceReference);