Merge branch 'rel_5_4'
This commit is contained in:
commit
5c4fcac530
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 2643
|
||||||
|
title: "When using Auto-Version references in the JPA server, if an auto-versioned reference
|
||||||
|
within a FHIR transaction pointed to a resource that did not actually change during the
|
||||||
|
transaction (e.g. an update/PUT where the resource body was unchanged from the existing
|
||||||
|
version), the reference would point to an incremented version number even though none existed.
|
||||||
|
This has been corrected."
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 2647
|
||||||
|
title: "The new Match URL cache suffered from potential cache poisoning if multiple threads performed
|
||||||
|
a condiitonal create operation at the same time."
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
release-date: "2021-05-20"
|
||||||
|
codename: "Pangolin"
|
|
@ -1142,6 +1142,11 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest);
|
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest);
|
||||||
|
|
||||||
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
|
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
|
||||||
|
|
||||||
|
if (theForceUpdate) {
|
||||||
|
changed.setChanged(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (changed.isChanged()) {
|
if (changed.isChanged()) {
|
||||||
entity.setUpdated(theTransactionDetails.getTransactionDate());
|
entity.setUpdated(theTransactionDetails.getTransactionDate());
|
||||||
if (theResource instanceof IResource) {
|
if (theResource instanceof IResource) {
|
||||||
|
|
|
@ -179,6 +179,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
private IRequestPartitionHelperSvc myPartitionHelperSvc;
|
private IRequestPartitionHelperSvc myPartitionHelperSvc;
|
||||||
@Autowired
|
@Autowired
|
||||||
private MemoryCacheService myMemoryCacheService;
|
private MemoryCacheService myMemoryCacheService;
|
||||||
|
private TransactionTemplate myTxTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome create(final T theResource) {
|
public DaoMethodOutcome create(final T theResource) {
|
||||||
|
@ -273,14 +274,22 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
};
|
};
|
||||||
|
|
||||||
Supplier<IIdType> idSupplier = () -> {
|
Supplier<IIdType> idSupplier = () -> {
|
||||||
IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid);
|
return myTxTemplate.execute(tx-> {
|
||||||
if (!retVal.hasVersionIdPart()) {
|
IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid);
|
||||||
return myMemoryCacheService.get(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, retVal, t -> {
|
if (!retVal.hasVersionIdPart()) {
|
||||||
long version = myResourceTableDao.findCurrentVersionByPid(pid.getIdAsLong());
|
IIdType idWithVersion = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong());
|
||||||
return myFhirContext.getVersion().newIdType().setParts(retVal.getBaseUrl(), retVal.getResourceType(), retVal.getIdPart(), Long.toString(version));
|
if (idWithVersion == null) {
|
||||||
});
|
Long version = myResourceTableDao.findCurrentVersionByPid(pid.getIdAsLong());
|
||||||
}
|
if (version != null) {
|
||||||
return retVal;
|
retVal = myFhirContext.getVersion().newIdType().setParts(retVal.getBaseUrl(), retVal.getResourceType(), retVal.getIdPart(), Long.toString(version));
|
||||||
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong(), retVal);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
retVal = idWithVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true);
|
return toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true);
|
||||||
|
@ -1057,6 +1066,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
public void start() {
|
public void start() {
|
||||||
ourLog.debug("Starting resource DAO for type: {}", getResourceName());
|
ourLog.debug("Starting resource DAO for type: {}", getResourceName());
|
||||||
myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class);
|
myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class);
|
||||||
|
myTxTemplate = new TransactionTemplate(myPlatformTransactionManager);
|
||||||
super.start();
|
super.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1537,7 +1547,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
String id = theResource.getIdElement().getValue();
|
String id = theResource.getIdElement().getValue();
|
||||||
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
|
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
|
||||||
|
|
||||||
// Execute the update in a retriable transasction
|
// Execute the update in a retriable transaction
|
||||||
return myTransactionService.execute(theRequest, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
|
return myTransactionService.execute(theRequest, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -922,118 +922,7 @@ public abstract class BaseTransactionProcessor {
|
||||||
* Perform ID substitutions and then index each resource we have saved
|
* Perform ID substitutions and then index each resource we have saved
|
||||||
*/
|
*/
|
||||||
|
|
||||||
FhirTerser terser = myContext.newTerser();
|
resolveReferencesThenSaveAndIndexResources(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, theTransactionStopWatch, entriesToProcess, nonUpdatedEntities, updatedEntities);
|
||||||
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
|
|
||||||
int i = 0;
|
|
||||||
for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
|
|
||||||
|
|
||||||
if (i++ % 250 == 0) {
|
|
||||||
ourLog.debug("Have indexed {} entities out of {} in transaction", i, theIdToPersistedOutcome.values().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextOutcome.isNop()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
IBaseResource nextResource = nextOutcome.getResource();
|
|
||||||
if (nextResource == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// References
|
|
||||||
Set<IBaseReference> referencesToVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
|
|
||||||
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
|
|
||||||
for (ResourceReferenceInfo nextRef : allRefs) {
|
|
||||||
IBaseReference resourceReference = nextRef.getResourceReference();
|
|
||||||
IIdType nextId = resourceReference.getReferenceElement();
|
|
||||||
IIdType newId = null;
|
|
||||||
if (!nextId.hasIdPart()) {
|
|
||||||
if (resourceReference.getResource() != null) {
|
|
||||||
IIdType targetId = resourceReference.getResource().getIdElement();
|
|
||||||
if (targetId.getValue() == null) {
|
|
||||||
// This means it's a contained resource
|
|
||||||
continue;
|
|
||||||
} else if (theIdSubstitutions.containsValue(targetId)) {
|
|
||||||
newId = targetId;
|
|
||||||
} else {
|
|
||||||
throw new InternalErrorException("References by resource with no reference ID are not supported in DAO layer");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newId != null || theIdSubstitutions.containsKey(nextId)) {
|
|
||||||
if (newId == null) {
|
|
||||||
newId = theIdSubstitutions.get(nextId);
|
|
||||||
}
|
|
||||||
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
|
|
||||||
if (referencesToVersion.contains(resourceReference)) {
|
|
||||||
resourceReference.setReference(newId.getValue());
|
|
||||||
} else {
|
|
||||||
resourceReference.setReference(newId.toVersionless().getValue());
|
|
||||||
}
|
|
||||||
} else if (nextId.getValue().startsWith("urn:")) {
|
|
||||||
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
|
|
||||||
} else {
|
|
||||||
if (referencesToVersion.contains(resourceReference)) {
|
|
||||||
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
|
|
||||||
if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
|
|
||||||
resourceReference.setReference(nextId.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// URIs
|
|
||||||
Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) myContext.getElementDefinition("uri").getImplementingClass();
|
|
||||||
List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType);
|
|
||||||
for (IPrimitiveType<?> nextRef : allUris) {
|
|
||||||
if (nextRef instanceof IIdType) {
|
|
||||||
continue; // No substitution on the resource ID itself!
|
|
||||||
}
|
|
||||||
IIdType nextUriString = newIdType(nextRef.getValueAsString());
|
|
||||||
if (theIdSubstitutions.containsKey(nextUriString)) {
|
|
||||||
IIdType newId = theIdSubstitutions.get(nextUriString);
|
|
||||||
ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
|
|
||||||
nextRef.setValueAsString(newId.toVersionless().getValue());
|
|
||||||
} else {
|
|
||||||
ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IPrimitiveType<Date> deletedInstantOrNull;
|
|
||||||
if (nextResource instanceof IAnyResource) {
|
|
||||||
deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource);
|
|
||||||
} else {
|
|
||||||
deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IResource) nextResource);
|
|
||||||
}
|
|
||||||
Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
|
|
||||||
|
|
||||||
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(nextResource.getClass());
|
|
||||||
IJpaDao jpaDao = (IJpaDao) dao;
|
|
||||||
|
|
||||||
IBasePersistedResource updateOutcome = null;
|
|
||||||
if (updatedEntities.contains(nextOutcome.getEntity())) {
|
|
||||||
updateOutcome = jpaDao.updateInternal(theRequest, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource(), theTransactionDetails);
|
|
||||||
} else if (!nonUpdatedEntities.contains(nextOutcome.getId())) {
|
|
||||||
updateOutcome = jpaDao.updateEntity(theRequest, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theTransactionDetails, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we reflect the actual final version for the resource.
|
|
||||||
if (updateOutcome != null) {
|
|
||||||
IIdType newId = updateOutcome.getIdDt();
|
|
||||||
for (IIdType nextEntry : entriesToProcess.values()) {
|
|
||||||
if (nextEntry.getResourceType().equals(newId.getResourceType())) {
|
|
||||||
if (nextEntry.getIdPart().equals(newId.getIdPart())) {
|
|
||||||
if (!nextEntry.hasVersionIdPart() || !nextEntry.getVersionIdPart().equals(newId.getVersionIdPart())) {
|
|
||||||
nextEntry.setParts(nextEntry.getBaseUrl(), nextEntry.getResourceType(), nextEntry.getIdPart(), newId.getVersionIdPart());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
theTransactionStopWatch.endCurrentTask();
|
theTransactionStopWatch.endCurrentTask();
|
||||||
theTransactionStopWatch.startTask("Flush writes to database");
|
theTransactionStopWatch.startTask("Flush writes to database");
|
||||||
|
@ -1042,8 +931,6 @@ public abstract class BaseTransactionProcessor {
|
||||||
|
|
||||||
theTransactionStopWatch.endCurrentTask();
|
theTransactionStopWatch.endCurrentTask();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Double check we didn't allow any duplicates we shouldn't have
|
* Double check we didn't allow any duplicates we shouldn't have
|
||||||
*/
|
*/
|
||||||
|
@ -1093,6 +980,183 @@ public abstract class BaseTransactionProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method replaces any placeholder references in the
|
||||||
|
* source transaction Bundle with their actual targets, then stores the resource contents and indexes
|
||||||
|
* in the database. This is trickier than you'd think because of a couple of possibilities during the
|
||||||
|
* save:
|
||||||
|
* * There may be resources that have not changed (e.g. an update/PUT with a resource body identical
|
||||||
|
* to what is already in the database)
|
||||||
|
* * There may be resources with auto-versioned references, meaning we're replacing certain references
|
||||||
|
* in the resource with a versioned references, referencing the current version at the time of the
|
||||||
|
* transaction processing
|
||||||
|
* * There may by auto-versioned references pointing to these unchanged targets
|
||||||
|
*
|
||||||
|
* If we're not doing any auto-versioned references, we'll just iterate through all resources in the
|
||||||
|
* transaction and save them one at a time.
|
||||||
|
*
|
||||||
|
* However, if we have any auto-versioned references we do this in 2 passes: First the resources from the
|
||||||
|
* transaction that don't have any auto-versioned references are stored. We do them first since there's
|
||||||
|
* a chance they may be a NOP and we'll need to account for their version number not actually changing.
|
||||||
|
* Then we do a second pass for any resources that have auto-versioned references. These happen in a separate
|
||||||
|
* pass because it's too complex to try and insert the auto-versioned references and still
|
||||||
|
* account for NOPs, so we block NOPs in that pass.
|
||||||
|
*/
|
||||||
|
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
|
||||||
|
FhirTerser terser = myContext.newTerser();
|
||||||
|
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
|
||||||
|
IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null;
|
||||||
|
int i = 0;
|
||||||
|
for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
|
||||||
|
|
||||||
|
if (i++ % 250 == 0) {
|
||||||
|
ourLog.debug("Have indexed {} entities out of {} in transaction", i, theIdToPersistedOutcome.values().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextOutcome.isNop()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBaseResource nextResource = nextOutcome.getResource();
|
||||||
|
if (nextResource == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<IBaseReference> referencesToAutoVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
|
||||||
|
if (referencesToAutoVersion.isEmpty()) {
|
||||||
|
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
|
||||||
|
} else {
|
||||||
|
if (deferredIndexesForAutoVersioning == null) {
|
||||||
|
deferredIndexesForAutoVersioning = new IdentityHashMap<>();
|
||||||
|
}
|
||||||
|
deferredIndexesForAutoVersioning.put(nextOutcome, referencesToAutoVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any resources we'll be auto-versioning, index these next
|
||||||
|
if (deferredIndexesForAutoVersioning != null) {
|
||||||
|
for (Map.Entry<DaoMethodOutcome, Set<IBaseReference>> nextEntry : deferredIndexesForAutoVersioning.entrySet()) {
|
||||||
|
DaoMethodOutcome nextOutcome = nextEntry.getKey();
|
||||||
|
Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue();
|
||||||
|
IBaseResource nextResource = nextOutcome.getResource();
|
||||||
|
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities, FhirTerser terser, DaoMethodOutcome nextOutcome, IBaseResource nextResource, Set<IBaseReference> theReferencesToAutoVersion) {
|
||||||
|
// References
|
||||||
|
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
|
||||||
|
for (ResourceReferenceInfo nextRef : allRefs) {
|
||||||
|
IBaseReference resourceReference = nextRef.getResourceReference();
|
||||||
|
IIdType nextId = resourceReference.getReferenceElement();
|
||||||
|
IIdType newId = null;
|
||||||
|
if (!nextId.hasIdPart()) {
|
||||||
|
if (resourceReference.getResource() != null) {
|
||||||
|
IIdType targetId = resourceReference.getResource().getIdElement();
|
||||||
|
if (targetId.getValue() == null) {
|
||||||
|
// This means it's a contained resource
|
||||||
|
continue;
|
||||||
|
} else if (theIdSubstitutions.containsValue(targetId)) {
|
||||||
|
newId = targetId;
|
||||||
|
} else {
|
||||||
|
throw new InternalErrorException("References by resource with no reference ID are not supported in DAO layer");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newId != null || theIdSubstitutions.containsKey(nextId)) {
|
||||||
|
if (newId == null) {
|
||||||
|
newId = theIdSubstitutions.get(nextId);
|
||||||
|
}
|
||||||
|
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
|
||||||
|
if (theReferencesToAutoVersion.contains(resourceReference)) {
|
||||||
|
resourceReference.setReference(newId.getValue());
|
||||||
|
} else {
|
||||||
|
resourceReference.setReference(newId.toVersionless().getValue());
|
||||||
|
}
|
||||||
|
} else if (nextId.getValue().startsWith("urn:")) {
|
||||||
|
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
|
||||||
|
} else {
|
||||||
|
if (theReferencesToAutoVersion.contains(resourceReference)) {
|
||||||
|
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
|
||||||
|
if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
|
||||||
|
resourceReference.setReference(nextId.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// URIs
|
||||||
|
Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) myContext.getElementDefinition("uri").getImplementingClass();
|
||||||
|
List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType);
|
||||||
|
for (IPrimitiveType<?> nextRef : allUris) {
|
||||||
|
if (nextRef instanceof IIdType) {
|
||||||
|
continue; // No substitution on the resource ID itself!
|
||||||
|
}
|
||||||
|
IIdType nextUriString = newIdType(nextRef.getValueAsString());
|
||||||
|
if (theIdSubstitutions.containsKey(nextUriString)) {
|
||||||
|
IIdType newId = theIdSubstitutions.get(nextUriString);
|
||||||
|
ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
|
||||||
|
nextRef.setValueAsString(newId.toVersionless().getValue());
|
||||||
|
} else {
|
||||||
|
ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IPrimitiveType<Date> deletedInstantOrNull;
|
||||||
|
if (nextResource instanceof IAnyResource) {
|
||||||
|
deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource);
|
||||||
|
} else {
|
||||||
|
deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IResource) nextResource);
|
||||||
|
}
|
||||||
|
Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
|
||||||
|
|
||||||
|
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(nextResource.getClass());
|
||||||
|
IJpaDao jpaDao = (IJpaDao) dao;
|
||||||
|
|
||||||
|
IBasePersistedResource updateOutcome = null;
|
||||||
|
if (updatedEntities.contains(nextOutcome.getEntity())) {
|
||||||
|
boolean forceUpdateVersion = false;
|
||||||
|
if (!theReferencesToAutoVersion.isEmpty()) {
|
||||||
|
forceUpdateVersion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutcome = jpaDao.updateInternal(theRequest, nextResource, true, forceUpdateVersion, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource(), theTransactionDetails);
|
||||||
|
} else if (!nonUpdatedEntities.contains(nextOutcome.getId())) {
|
||||||
|
updateOutcome = jpaDao.updateEntity(theRequest, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theTransactionDetails, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we reflect the actual final version for the resource.
|
||||||
|
if (updateOutcome != null) {
|
||||||
|
IIdType newId = updateOutcome.getIdDt();
|
||||||
|
for (IIdType nextEntry : entriesToProcess.values()) {
|
||||||
|
if (nextEntry.getResourceType().equals(newId.getResourceType())) {
|
||||||
|
if (nextEntry.getIdPart().equals(newId.getIdPart())) {
|
||||||
|
if (!nextEntry.hasVersionIdPart() || !nextEntry.getVersionIdPart().equals(newId.getVersionIdPart())) {
|
||||||
|
nextEntry.setParts(nextEntry.getBaseUrl(), nextEntry.getResourceType(), nextEntry.getIdPart(), newId.getVersionIdPart());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOutcome.setId(newId);
|
||||||
|
|
||||||
|
for (IIdType next : theIdSubstitutions.values()) {
|
||||||
|
if (next.getResourceType().equals(newId.getResourceType())) {
|
||||||
|
if (next.getIdPart().equals(newId.getIdPart())) {
|
||||||
|
if (!next.getValue().equals(newId.getValue())) {
|
||||||
|
next.setValue(newId.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateNoDuplicates(RequestDetails theRequest, String theActionName, Map<String, Class<? extends IBaseResource>> conditionalRequestUrls) {
|
private void validateNoDuplicates(RequestDetails theRequest, String theActionName, Map<String, Class<? extends IBaseResource>> conditionalRequestUrls) {
|
||||||
for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
|
for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
|
||||||
String matchUrl = nextEntry.getKey();
|
String matchUrl = nextEntry.getKey();
|
||||||
|
|
|
@ -43,6 +43,8 @@ import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -80,7 +82,8 @@ public class MatchResourceUrlService {
|
||||||
Set<ResourcePersistentId> retVal = search(paramMap, theResourceType, theRequest);
|
Set<ResourcePersistentId> retVal = search(paramMap, theResourceType, theRequest);
|
||||||
|
|
||||||
if (myDaoConfig.getMatchUrlCache() && retVal.size() == 1) {
|
if (myDaoConfig.getMatchUrlCache() && retVal.size() == 1) {
|
||||||
myMemoryCacheService.put(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, retVal.iterator().next());
|
ResourcePersistentId pid = retVal.iterator().next();
|
||||||
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return retVal;
|
return retVal;
|
||||||
|
@ -113,7 +116,7 @@ public class MatchResourceUrlService {
|
||||||
Validate.notBlank(theMatchUrl);
|
Validate.notBlank(theMatchUrl);
|
||||||
Validate.notNull(theResourcePersistentId);
|
Validate.notNull(theResourcePersistentId);
|
||||||
if (myDaoConfig.getMatchUrlCache()) {
|
if (myDaoConfig.getMatchUrlCache()) {
|
||||||
myMemoryCacheService.put(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, theResourcePersistentId);
|
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, theMatchUrl, theResourcePersistentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,5 +93,5 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
|
||||||
Collection<Object[]> findLookupFieldsByResourcePidInPartitionNull(@Param("pid") List<Long> thePids);
|
Collection<Object[]> findLookupFieldsByResourcePidInPartitionNull(@Param("pid") List<Long> thePids);
|
||||||
|
|
||||||
@Query("SELECT t.myVersion FROM ResourceTable t WHERE t.myId = :pid")
|
@Query("SELECT t.myVersion FROM ResourceTable t WHERE t.myId = :pid")
|
||||||
long findCurrentVersionByPid(@Param("pid") Long thePid);
|
Long findCurrentVersionByPid(@Param("pid") Long thePid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,15 @@ package ca.uhn.fhir.jpa.util;
|
||||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.api.model.TranslationQuery;
|
import ca.uhn.fhir.jpa.api.model.TranslationQuery;
|
||||||
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
|
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
|
@ -100,6 +103,19 @@ public class MemoryCacheService {
|
||||||
getCache(theCache).put(theKey, theValue);
|
getCache(theCache).put(theKey, theValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) {
|
||||||
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
put(theCache, theKey, theValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
put(theCache, theKey, theValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Iterable<K> theKeys) {
|
public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Iterable<K> theKeys) {
|
||||||
return (Map<K, V>) getCache(theCache).getAllPresent(theKeys);
|
return (Map<K, V>) getCache(theCache).getAllPresent(theKeys);
|
||||||
}
|
}
|
||||||
|
@ -122,7 +138,7 @@ public class MemoryCacheService {
|
||||||
CONCEPT_TRANSLATION(TranslationQuery.class),
|
CONCEPT_TRANSLATION(TranslationQuery.class),
|
||||||
MATCH_URL(String.class),
|
MATCH_URL(String.class),
|
||||||
CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class),
|
CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class),
|
||||||
RESOURCE_CONDITIONAL_CREATE_VERSION(IIdType.class),
|
RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class),
|
||||||
HISTORY_COUNT(HistoryCountKey.class);
|
HISTORY_COUNT(HistoryCountKey.class);
|
||||||
|
|
||||||
private final Class<?> myKeyType;
|
private final Class<?> myKeyType;
|
||||||
|
|
|
@ -11,15 +11,20 @@ import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import ca.uhn.fhir.util.HapiExtensions;
|
import ca.uhn.fhir.util.HapiExtensions;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
import org.hl7.fhir.r4.model.BooleanType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.CodeType;
|
import org.hl7.fhir.r4.model.CodeType;
|
||||||
|
import org.hl7.fhir.r4.model.Coverage;
|
||||||
import org.hl7.fhir.r4.model.Enumerations;
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
|
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
import org.hl7.fhir.r4.model.Parameters;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Practitioner;
|
||||||
import org.hl7.fhir.r4.model.SearchParameter;
|
import org.hl7.fhir.r4.model.SearchParameter;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
@ -35,6 +40,7 @@ import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
@ -70,6 +76,83 @@ public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test {
|
||||||
myInterceptorRegistry.unregisterInterceptor(myRetryInterceptor);
|
myInterceptorRegistry.unregisterInterceptor(myRetryInterceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConcurrentTransactionCreates() throws ExecutionException, InterruptedException {
|
||||||
|
myDaoConfig.setMatchUrlCache(true);
|
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
Runnable creator = () -> {
|
||||||
|
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||||
|
String patientId = "Patient/PT" + counter.get();
|
||||||
|
IdType practitionerId = IdType.newRandomUuid();
|
||||||
|
IdType practitionerId2 = IdType.newRandomUuid();
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob = new ExplanationOfBenefit();
|
||||||
|
eob.addIdentifier().setSystem("foo").setValue("" + counter.get());
|
||||||
|
eob.getPatient().setReference(patientId);
|
||||||
|
eob.addCareTeam().getProvider().setReference(practitionerId.getValue());
|
||||||
|
eob.addCareTeam().getProvider().setReference(practitionerId2.getValue());
|
||||||
|
bb.addTransactionUpdateEntry(eob).conditional("ExplanationOfBenefit?identifier=foo|" + counter.get());
|
||||||
|
|
||||||
|
Patient pt = new Patient();
|
||||||
|
pt.setId(patientId);
|
||||||
|
pt.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(pt);
|
||||||
|
|
||||||
|
Coverage coverage = new Coverage();
|
||||||
|
coverage.addIdentifier().setSystem("foo").setValue("" + counter.get());
|
||||||
|
coverage.getBeneficiary().setReference(patientId);
|
||||||
|
bb.addTransactionUpdateEntry(coverage).conditional("Coverage?identifier=foo|" + counter.get());
|
||||||
|
|
||||||
|
Practitioner practitioner = new Practitioner();
|
||||||
|
practitioner.setId(practitionerId);
|
||||||
|
practitioner.addIdentifier().setSystem("foo").setValue("" + counter.get());
|
||||||
|
bb.addTransactionCreateEntry(practitioner).conditional("Practitioner?identifier=foo|" + counter.get());
|
||||||
|
|
||||||
|
Practitioner practitioner2 = new Practitioner();
|
||||||
|
practitioner2.setId(practitionerId2);
|
||||||
|
practitioner2.addIdentifier().setSystem("foo2").setValue("" + counter.get());
|
||||||
|
bb.addTransactionCreateEntry(practitioner2).conditional("Practitioner?identifier=foo2|" + counter.get());
|
||||||
|
|
||||||
|
Observation obs = new Observation();
|
||||||
|
obs.setId("Observation/OBS" + counter);
|
||||||
|
obs.getSubject().setReference(pt.getId());
|
||||||
|
bb.addTransactionUpdateEntry(obs);
|
||||||
|
|
||||||
|
Bundle input = (Bundle) bb.getBundle();
|
||||||
|
mySystemDao.transaction(new SystemRequestDetails(), input);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
counter.set(i);
|
||||||
|
ourLog.info("*********************************************************************************");
|
||||||
|
ourLog.info("Starting pass {}", i);
|
||||||
|
ourLog.info("*********************************************************************************");
|
||||||
|
|
||||||
|
List<Future<?>> futures = new ArrayList<>();
|
||||||
|
for (int j = 0; j < 10; j++) {
|
||||||
|
futures.add(myExecutor.submit(creator));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Future<?> next : futures) {
|
||||||
|
try {
|
||||||
|
next.get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
creator.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
runInTransaction(()->{
|
||||||
|
assertEquals(60, myResourceTableDao.count());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateWithClientAssignedId() {
|
public void testCreateWithClientAssignedId() {
|
||||||
myInterceptorRegistry.registerInterceptor(myRetryInterceptor);
|
myInterceptorRegistry.registerInterceptor(myRetryInterceptor);
|
||||||
|
|
|
@ -698,7 +698,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
myCaptureQueriesListener.clear();
|
myCaptureQueriesListener.clear();
|
||||||
mySystemDao.transaction(mySrd, bundleCreator.get());
|
mySystemDao.transaction(mySrd, bundleCreator.get());
|
||||||
assertEquals(1, myCaptureQueriesListener.countSelectQueries());
|
assertEquals(2, myCaptureQueriesListener.countSelectQueries());
|
||||||
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
assertEquals(5, myCaptureQueriesListener.countInsertQueries());
|
||||||
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
|
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
|
@ -11,8 +12,10 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.Condition;
|
import org.hl7.fhir.r4.model.Condition;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
import org.hl7.fhir.r4.model.Encounter;
|
||||||
|
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
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.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.Reference;
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
import org.hl7.fhir.r4.model.Task;
|
import org.hl7.fhir.r4.model.Task;
|
||||||
|
@ -23,8 +26,12 @@ import java.util.Arrays;
|
||||||
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.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.matchesPattern;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@ -40,6 +47,167 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndUpdateVersionedReferencesInTransaction_VersionedReferenceToUpsertWithNop() {
|
||||||
|
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||||
|
myModelConfig.setAutoVersionReferenceAtPaths("ExplanationOfBenefit.patient");
|
||||||
|
|
||||||
|
// We'll submit the same bundle twice. It has an UPSERT (with no changes
|
||||||
|
// the second time) on a Patient, and a CREATE on an ExplanationOfBenefit
|
||||||
|
// referencing that Patient.
|
||||||
|
Supplier<Bundle> supplier = () -> {
|
||||||
|
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setId("Patient/A");
|
||||||
|
patient.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(patient);
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob = new ExplanationOfBenefit();
|
||||||
|
eob.setId(IdType.newRandomUuid());
|
||||||
|
eob.setPatient(new Reference("Patient/A"));
|
||||||
|
bb.addTransactionCreateEntry(eob);
|
||||||
|
|
||||||
|
return (Bundle) bb.getBundle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send it the first time
|
||||||
|
Bundle outcome1 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Patient/A/_history/1", outcome1.getEntry().get(0).getResponse().getLocation());
|
||||||
|
String eobId1 = outcome1.getEntry().get(1).getResponse().getLocation();
|
||||||
|
assertThat(eobId1, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob1 = myExplanationOfBenefitDao.read(new IdType(eobId1), new SystemRequestDetails());
|
||||||
|
assertEquals("Patient/A/_history/1", eob1.getPatient().getReference());
|
||||||
|
|
||||||
|
// Send it again
|
||||||
|
Bundle outcome2 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Patient/A/_history/1", outcome2.getEntry().get(0).getResponse().getLocation());
|
||||||
|
String eobId2 = outcome2.getEntry().get(1).getResponse().getLocation();
|
||||||
|
assertThat(eobId2, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob2 = myExplanationOfBenefitDao.read(new IdType(eobId2), new SystemRequestDetails());
|
||||||
|
assertEquals("Patient/A/_history/1", eob2.getPatient().getReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndUpdateVersionedReferencesInTransaction_VersionedReferenceToVersionedReferenceToUpsertWithNop() {
|
||||||
|
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||||
|
myModelConfig.setAutoVersionReferenceAtPaths(
|
||||||
|
"Patient.managingOrganization",
|
||||||
|
"ExplanationOfBenefit.patient"
|
||||||
|
);
|
||||||
|
|
||||||
|
// We'll submit the same bundle twice. It has an UPSERT (with no changes
|
||||||
|
// the second time) on a Patient, and a CREATE on an ExplanationOfBenefit
|
||||||
|
// referencing that Patient.
|
||||||
|
Supplier<Bundle> supplier = () -> {
|
||||||
|
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||||
|
|
||||||
|
Organization organization = new Organization();
|
||||||
|
organization.setId("Organization/O");
|
||||||
|
organization.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(organization);
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setId("Patient/A");
|
||||||
|
patient.setManagingOrganization(new Reference("Organization/O"));
|
||||||
|
patient.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(patient);
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob = new ExplanationOfBenefit();
|
||||||
|
eob.setId(IdType.newRandomUuid());
|
||||||
|
eob.setPatient(new Reference("Patient/A"));
|
||||||
|
bb.addTransactionCreateEntry(eob);
|
||||||
|
|
||||||
|
return (Bundle) bb.getBundle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send it the first time
|
||||||
|
Bundle outcome1 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Organization/O/_history/1", outcome1.getEntry().get(0).getResponse().getLocation());
|
||||||
|
assertEquals("Patient/A/_history/1", outcome1.getEntry().get(1).getResponse().getLocation());
|
||||||
|
String eobId1 = outcome1.getEntry().get(2).getResponse().getLocation();
|
||||||
|
assertThat(eobId1, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob1 = myExplanationOfBenefitDao.read(new IdType(eobId1), new SystemRequestDetails());
|
||||||
|
assertEquals("Patient/A/_history/1", eob1.getPatient().getReference());
|
||||||
|
|
||||||
|
// Send it again
|
||||||
|
Bundle outcome2 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Organization/O/_history/1", outcome2.getEntry().get(0).getResponse().getLocation());
|
||||||
|
// Technically the patient did not change - If this ever got optimized so that the version here
|
||||||
|
// was 1 that would be even better
|
||||||
|
String patientId = outcome2.getEntry().get(1).getResponse().getLocation();
|
||||||
|
assertEquals("Patient/A/_history/2", patientId);
|
||||||
|
String eobId2 = outcome2.getEntry().get(2).getResponse().getLocation();
|
||||||
|
assertThat(eobId2, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
Patient patient = myPatientDao.read(new IdType("Patient/A"), new SystemRequestDetails());
|
||||||
|
assertEquals(patientId, patient.getId());
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob2 = myExplanationOfBenefitDao.read(new IdType(eobId2), new SystemRequestDetails());
|
||||||
|
assertEquals(patientId, eob2.getPatient().getReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndUpdateVersionedReferencesInTransaction_VersionedReferenceToVersionedReferenceToUpsertWithChange() {
|
||||||
|
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||||
|
myModelConfig.setAutoVersionReferenceAtPaths(
|
||||||
|
"Patient.managingOrganization",
|
||||||
|
"ExplanationOfBenefit.patient"
|
||||||
|
);
|
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Supplier<Bundle> supplier = () -> {
|
||||||
|
BundleBuilder bb = new BundleBuilder(myFhirCtx);
|
||||||
|
|
||||||
|
Organization organization = new Organization();
|
||||||
|
organization.setId("Organization/O");
|
||||||
|
organization.setName("Org " + counter.incrementAndGet()); // change each time
|
||||||
|
organization.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(organization);
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setId("Patient/A");
|
||||||
|
patient.setManagingOrganization(new Reference("Organization/O"));
|
||||||
|
patient.setActive(true);
|
||||||
|
bb.addTransactionUpdateEntry(patient);
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob = new ExplanationOfBenefit();
|
||||||
|
eob.setId(IdType.newRandomUuid());
|
||||||
|
eob.setPatient(new Reference("Patient/A"));
|
||||||
|
bb.addTransactionCreateEntry(eob);
|
||||||
|
|
||||||
|
return (Bundle) bb.getBundle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send it the first time
|
||||||
|
Bundle outcome1 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Organization/O/_history/1", outcome1.getEntry().get(0).getResponse().getLocation());
|
||||||
|
assertEquals("Patient/A/_history/1", outcome1.getEntry().get(1).getResponse().getLocation());
|
||||||
|
String eobId1 = outcome1.getEntry().get(2).getResponse().getLocation();
|
||||||
|
assertThat(eobId1, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob1 = myExplanationOfBenefitDao.read(new IdType(eobId1), new SystemRequestDetails());
|
||||||
|
assertEquals("Patient/A/_history/1", eob1.getPatient().getReference());
|
||||||
|
|
||||||
|
// Send it again
|
||||||
|
Bundle outcome2 = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
|
||||||
|
assertEquals("Organization/O/_history/2", outcome2.getEntry().get(0).getResponse().getLocation());
|
||||||
|
String patientId = outcome2.getEntry().get(1).getResponse().getLocation();
|
||||||
|
assertEquals("Patient/A/_history/2", patientId);
|
||||||
|
String eobId2 = outcome2.getEntry().get(2).getResponse().getLocation();
|
||||||
|
assertThat(eobId2, matchesPattern("ExplanationOfBenefit/[0-9]+/_history/1"));
|
||||||
|
|
||||||
|
Patient patient = myPatientDao.read(new IdType("Patient/A"), new SystemRequestDetails());
|
||||||
|
assertEquals(patientId, patient.getId());
|
||||||
|
|
||||||
|
ExplanationOfBenefit eob2 = myExplanationOfBenefitDao.read(new IdType(eobId2), new SystemRequestDetails());
|
||||||
|
assertEquals(patientId, eob2.getPatient().getReference());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStoreAndRetrieveVersionedReference() {
|
public void testStoreAndRetrieveVersionedReference() {
|
||||||
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
|
||||||
|
@ -185,7 +353,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify Patient Version
|
// Verify Patient Version
|
||||||
assertEquals("2", myPatientDao.search(SearchParameterMap.newSynchronous("active", new TokenParam("false"))).getResources(0,1).get(0).getIdElement().getVersionIdPart());
|
assertEquals("2", myPatientDao.search(SearchParameterMap.newSynchronous("active", new TokenParam("false"))).getResources(0, 1).get(0).getIdElement().getVersionIdPart());
|
||||||
|
|
||||||
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
BundleBuilder builder = new BundleBuilder(myFhirCtx);
|
||||||
|
|
||||||
|
@ -430,9 +598,9 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
||||||
assertEquals(2, outcome.size());
|
assertEquals(2, outcome.size());
|
||||||
List<IBaseResource> resources = outcome.getResources(0, 2);
|
List<IBaseResource> resources = outcome.getResources(0, 2);
|
||||||
assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
assertEquals(2, resources.size(), resources.stream().map(t -> t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
||||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
assertEquals(conditionId.getValue(), ((Task) resources.get(0)).getBasedOn().get(0).getReference());
|
||||||
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
||||||
|
|
||||||
// Now, update the Condition to generate another version of it
|
// Now, update the Condition to generate another version of it
|
||||||
|
@ -445,7 +613,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
resources = outcome.getResources(0, 2);
|
resources = outcome.getResources(0, 2);
|
||||||
assertEquals(2, resources.size());
|
assertEquals(2, resources.size());
|
||||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
assertEquals(conditionId.getValue(), ((Task) resources.get(0)).getBasedOn().get(0).getReference());
|
||||||
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,9 +646,9 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
||||||
assertEquals(2, outcome.size());
|
assertEquals(2, outcome.size());
|
||||||
List<IBaseResource> resources = outcome.getResources(0, 2);
|
List<IBaseResource> resources = outcome.getResources(0, 2);
|
||||||
assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
assertEquals(2, resources.size(), resources.stream().map(t -> t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
||||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
assertEquals(conditionId.getValue(), ((Task) resources.get(0)).getBasedOn().get(0).getReference());
|
||||||
assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue());
|
assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,9 +690,9 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
|
||||||
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON));
|
||||||
assertEquals(2, outcome.size());
|
assertEquals(2, outcome.size());
|
||||||
List<IBaseResource> resources = outcome.getResources(0, 2);
|
List<IBaseResource> resources = outcome.getResources(0, 2);
|
||||||
assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
assertEquals(2, resources.size(), resources.stream().map(t -> t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", ")));
|
||||||
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue());
|
||||||
assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference());
|
assertEquals(conditionId.getValue(), ((Task) resources.get(0)).getBasedOn().get(0).getReference());
|
||||||
assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue());
|
assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
|
@ -122,11 +122,13 @@ public abstract class BaseTask {
|
||||||
return changesCount;
|
return changesCount;
|
||||||
} catch (DataAccessException e) {
|
} catch (DataAccessException e) {
|
||||||
if (myFailureAllowed) {
|
if (myFailureAllowed) {
|
||||||
ourLog.info("Task did not exit successfully, but task is allowed to fail");
|
ourLog.info("Task {} did not exit successfully, but task is allowed to fail", getFlywayVersion());
|
||||||
ourLog.debug("Error was: {}", e.getMessage(), e);
|
ourLog.debug("Error was: {}", e.getMessage(), e);
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw new DataAccessException("Failed during task " + getFlywayVersion() + ": " + e, e) {
|
||||||
|
private static final long serialVersionUID = 8211678931579252166L;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue