Transaction with delete then update should not fail (#4831)

* Fixed

* Test fixes

* Add test

* Ongoing work

* Work on xactx

* Cleanup

* Changelog cleanup

* Resolve fixme

* Rework broken APIs

* Version bump

* Add license headers

* License header update

* License

* rk on fixes

* Test fixes

* Address review comments

* Test fixes

* Add license headers

* License header
This commit is contained in:
James Agnew 2023-05-15 07:41:40 -04:00 committed by GitHub
parent 8cf14c8c0e
commit 483ddca3be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 1056 additions and 253 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -123,7 +123,7 @@ public abstract class BasePrimitive<T> extends BaseIdentifiableElement implement
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
myStringValue = encode(myCoercedValue);
}
}

View File

@ -249,6 +249,22 @@ public class BundleBuilder {
return new CreateBuilder(request);
}
/**
* Adds an entry containing a delete (DELETE) request.
* Also sets the Bundle.type value to "transaction" if it is not already set.
* <p>
* Note that the resource is only used to extract its ID and type, and the body of the resource is not included in the entry,
*
* @param theCondition The conditional URL, e.g. "Patient?identifier=foo|bar"
* @since 6.8.0
*/
public DeleteBuilder addTransactionDeleteConditionalEntry(String theCondition) {
Validate.notBlank(theCondition, "theCondition must not be blank");
setBundleField("type", "transaction");
return addDeleteEntry(theCondition);
}
/**
* Adds an entry containing a delete (DELETE) request.
* Also sets the Bundle.type value to "transaction" if it is not already set.

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Docs
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.docs;
import ca.uhn.fhir.context.FhirContext;

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 4831
title: "When performing a FHIR transaction containing both a conditional delete as well as a
conditional create/update for the same resource, the resource was left in an inconsistent
state. This has been corrected. Thanks to Laxman Singh for raising this issue."

View File

@ -0,0 +1,5 @@
---
type: perf
issue: 4831
title: "Conditional deletes that delete multiple resources at once have been optimized to perform
fewer SQL select statements, which should improve performance on large deletes."

View File

@ -0,0 +1,8 @@
---
- item:
type: "add"
title: "The version of a few dependencies have been bumped to the latest versions
(dependent HAPI modules listed in brackets):
<ul>
<li>Hibernate ORM (JPA): 5.6.12.Final -&gt; 5.6.15.Final</li>
</ul>"

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -536,20 +536,16 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
String newVersion;
long newVersionLong;
if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
newVersion = "1";
newVersionLong = 1;
theSavedEntity.initializeVersion();
} else {
newVersionLong = theResourceId.getVersionIdPartAsLong() + 1;
newVersion = Long.toString(newVersionLong);
theSavedEntity.markVersionUpdatedInCurrentTransaction();
}
assert theResourceId != null;
String newVersion = Long.toString(theSavedEntity.getVersion());
IIdType newId = theResourceId.withVersion(newVersion);
theResource.getIdElement().setValue(newId.getValue());
theSavedEntity.setVersion(newVersionLong);
}
public boolean isLogicalReference(IIdType theId) {
@ -1090,9 +1086,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
return entity;
}
if (theUpdateVersion) {
long newVersion = entity.getVersion() + 1;
entity.setVersion(newVersion);
if (entity.getId() != null && theUpdateVersion) {
entity.markVersionUpdatedInCurrentTransaction();
}
/*
@ -1295,6 +1290,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
private void createHistoryEntry(RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) {
boolean versionedTags = getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED;
final ResourceHistoryTable historyEntry = theEntity.toHistory(versionedTags);
historyEntry.setEncoding(theChanged.getEncoding());
historyEntry.setResource(theChanged.getResourceBinary());

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.dao.ReindexOutcome;
import ca.uhn.fhir.jpa.api.dao.ReindexParameters;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
@ -199,9 +200,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private TransactionTemplate myTxTemplate;
@Autowired
private UrlPartitioner myUrlPartitioner;
@Autowired
private ResourceSearchUrlSvc myResourceSearchUrlSvc;
@Autowired
private IFhirSystemDao<?, ?> mySystemDao;
public static <T extends IBaseResource> T invokeStoragePreShowResources(IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequest, T retVal) {
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, theInterceptorBroadcaster, theRequest)) {
@ -263,12 +265,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
*/
@Override
public DaoMethodOutcome create(final T theResource) {
return create(theResource, null, true, new TransactionDetails(), null);
return create(theResource, null, true, null, new TransactionDetails());
}
@Override
public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) {
return create(theResource, null, true, new TransactionDetails(), theRequestDetails);
return create(theResource, null, true, theRequestDetails, new TransactionDetails());
}
/**
@ -281,11 +283,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) {
return create(theResource, theIfNoneExist, true, new TransactionDetails(), theRequestDetails);
return create(theResource, theIfNoneExist, true, theRequestDetails, new TransactionDetails());
}
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails) {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName());
return myTransactionService
.withRequest(theRequestDetails)
@ -340,7 +342,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity.setResourceType(toResourceName(theResource));
entity.setPartitionId(PartitionablePartitionId.toStoragePartition(theRequestPartitionId, myPartitionSettings));
entity.setCreatedByMatchUrl(theMatchUrl);
entity.setVersion(1);
entity.initializeVersion();
if (isNotBlank(theMatchUrl) && theProcessMatchUrl) {
Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest);
@ -348,19 +350,51 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theMatchUrl, match.size());
throw new PreconditionFailedException(Msg.code(958) + msg);
} else if (match.size() == 1) {
JpaPid pid = match.iterator().next();
Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> {
return myTxTemplate.execute(tx -> {
/*
* Ok, so we've found a single PID that matches the conditional URL.
* That's good, there are two possibilities below.
*/
JpaPid pid = match.iterator().next();
if (theTransactionDetails.getDeletedResourceIds().contains(pid)) {
/*
* If the resource matching the given match URL has already been
* deleted within this transaction. This is a really rare case, since
* it means the client has performed a FHIR transaction with both
* a delete and a create on the same conditional URL. This is rare
* but allowed, and means that it's now ok to create a new one resource
* matching the conditional URL since we'll be deleting any existing
* index rows on the existing resource as a part of this transaction.
* We can also un-resolve the previous match URL in the TransactionDetails
* since we'll resolve it to the new resource ID below
*/
myMatchResourceUrlService.unresolveMatchUrl(theTransactionDetails, getResourceName(), theMatchUrl);
} else {
/*
* This is the normal path where the conditional URL matched exactly
* one resource, so we won't be creating anything but instead
* just returning the existing ID. We now have a PID for the matching
* resource, but we haven't loaded anything else (e.g. the forced ID
* or the resource body aren't yet loaded from the DB). We're going to
* return a LazyDaoOutcome with two lazy loaded providers for loading the
* entity and the forced ID since we can avoid these extra SQL loads
* unless we know we're actually going to use them. For example, if
* the client has specified "Prefer: return=minimal" then we won't be
* needing the load the body.
*/
Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> myTxTemplate.execute(tx -> {
ResourceTable foundEntity = myEntityManager.find(ResourceTable.class, pid.getId());
IBaseResource resource = myJpaStorageResourceParser.toResource(foundEntity, false);
theResource.setId(resource.getIdElement().getValue());
return new LazyDaoMethodOutcome.EntityAndResource(foundEntity, resource);
});
};
Supplier<IIdType> idSupplier = () -> {
return myTxTemplate.execute(tx -> {
Supplier<IIdType> idSupplier = () -> myTxTemplate.execute(tx -> {
IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid);
if (!retVal.hasVersionIdPart()) {
Long version = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getId());
@ -376,13 +410,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
return retVal;
});
};
DaoMethodOutcome outcome = toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true);
StorageResponseCodeEnum responseCode = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH;
String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulCreateConditionalWithMatch", w.getMillisAndRestart(), UrlUtil.sanitizeUrlPart(theMatchUrl));
outcome.setOperationOutcome(createInfoOperationOutcome(msg, responseCode));
return outcome;
DaoMethodOutcome outcome = toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true);
StorageResponseCodeEnum responseCode = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH;
String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulCreateConditionalWithMatch", w.getMillisAndRestart(), UrlUtil.sanitizeUrlPart(theMatchUrl));
outcome.setOperationOutcome(createInfoOperationOutcome(msg, responseCode));
return outcome;
}
}
}
@ -617,12 +651,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceVersionConflictException(Msg.code(961) + "Trying to delete " + theId + " but this is not the current version");
}
JpaPid persistentId = JpaPid.fromId(entity.getResourceId());
theTransactionDetails.addDeletedResourceId(persistentId);
// Don't delete again if it's already deleted
if (isDeleted(entity)) {
DaoMethodOutcome outcome = createMethodOutcomeForResourceId(entity.getIdDt().getValue(), MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED, StorageResponseCodeEnum.SUCCESSFUL_DELETE_ALREADY_DELETED);
// used to exist, so we'll set the persistent id
outcome.setPersistentId(JpaPid.fromId(entity.getResourceId()));
outcome.setPersistentId(persistentId);
outcome.setEntity(entity);
return outcome;
@ -681,7 +718,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return myTransactionService.execute(theRequest, transactionDetails, tx -> {
DeleteConflictList deleteConflicts = new DeleteConflictList();
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest);
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest, transactionDetails);
DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
return outcome;
});
@ -692,20 +729,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
* transaction processors
*/
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails) {
public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails) {
validateDeleteEnabled();
TransactionDetails transactionDetails = new TransactionDetails();
return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails));
return myTransactionService.execute(theRequestDetails, theTransactionDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theTransactionDetails, theRequestDetails));
}
@Nonnull
private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) {
private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
SearchParameterMap paramMap = resourceSearch.getSearchParameterMap();
paramMap.setLoadSynchronous(true);
Set<JpaPid> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest, null);
Set<JpaPid> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequestDetails, null);
if (resourceIds.size() > 1) {
if (!getStorageSettings().isAllowMultipleDelete()) {
@ -713,7 +749,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest);
return deletePidList(theUrl, resourceIds, deleteConflicts, theRequestDetails, theTransactionDetails);
}
@Override
@ -733,15 +769,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
}
@Nonnull
@Override
public <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(String theUrl, Collection<P> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) {
public <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(String theUrl, Collection<P> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
StopWatch w = new StopWatch();
TransactionDetails transactionDetails = new TransactionDetails();
List<ResourceTable> deletedResources = new ArrayList<>();
List<IResourcePersistentId<?>> resolvedIds = theResourceIds
.stream()
.map(t -> (IResourcePersistentId<?>) t)
.collect(Collectors.toList());
mySystemDao.preFetchResources(resolvedIds, false);
for (P pid : theResourceIds) {
JpaPid jpaPid = (JpaPid) pid;
// This shouldn't actually need to hit the DB because we pre-fetch above
ResourceTable entity = myEntityManager.find(ResourceTable.class, jpaPid.getId());
deletedResources.add(entity);
@ -750,18 +794,18 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify IServerOperationInterceptors about pre-action call
HookParams hooks = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, transactionDetails);
doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
doCallHooks(transactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequest, transactionDetails);
myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, transactionDetails);
// Perform delete
preDelete(resourceToDelete, entity, theRequest);
preDelete(resourceToDelete, entity, theRequestDetails);
updateEntityForDelete(theRequest, transactionDetails, entity);
updateEntityForDelete(theRequestDetails, transactionDetails, entity);
resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors
@ -770,11 +814,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(TransactionDetails.class, transactionDetails)
.add(InterceptorInvocationTimingEnum.class, transactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED));
doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
doCallHooks(transactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
}
@ -791,6 +835,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis());
theTransactionDetails.addDeletedResourceIds(theResourceIds);
DeleteMethodOutcome retVal = new DeleteMethodOutcome();
retVal.setDeletedEntities(deletedResources);
retVal.setOperationOutcome(oo);
@ -825,10 +871,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
boolean hasTag = false;
for (BaseTag next : new ArrayList<>(theEntity.getTags())) {
if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
Objects.equals(next.getTag().getCode(), nextDef.getCode()) &&
Objects.equals(next.getTag().getVersion(), nextDef.getVersion()) &&
Objects.equals(next.getTag().getUserSelected(), nextDef.getUserSelected())) {
Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
Objects.equals(next.getTag().getCode(), nextDef.getCode()) &&
Objects.equals(next.getTag().getVersion(), nextDef.getVersion()) &&
Objects.equals(next.getTag().getUserSelected(), nextDef.getUserSelected())) {
hasTag = true;
break;
}
@ -1367,7 +1413,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
reindexOptimizeStorageHistoryEntity(entity, historyEntity);
if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) {
int pageSize = 100;
for (int page = 0; ((long)page * pageSize) < entity.getVersion(); page++) {
for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) {
Slice<ResourceHistoryTable> historyEntities = myResourceHistoryTableDao.findForResourceIdAndReturnEntities(PageRequest.of(page, pageSize), entity.getId(), historyEntity.getVersion());
for (ResourceHistoryTable next : historyEntities) {
reindexOptimizeStorageHistoryEntity(entity, next);

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.i18n.Msg;

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -127,7 +127,7 @@ public class PersistObservationIndexedSearchParamLastNR4IT {
ResourceTable entity = new ResourceTable();
entity.setId(55L);
entity.setResourceType("Observation");
entity.setVersion(0L);
entity.setVersionForUnitTest(0L);
testObservationPersist.deleteObservationIndex(entity);
elasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -153,10 +153,6 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase
myUpdated = theUpdated;
}
public void setUpdated(InstantDt theUpdated) {
myUpdated = theUpdated.getValue();
}
@Override
public abstract long getVersion();

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.model.search.ResourceTableRoutingBinder;
import ca.uhn.fhir.jpa.model.search.SearchParamTextPropertyBinder;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hibernate.Session;
@ -59,6 +60,7 @@ import javax.persistence.Index;
import javax.persistence.NamedEntityGraph;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PostPersist;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
@ -67,12 +69,13 @@ import javax.persistence.Version;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Indexed(routingBinder= @RoutingBinderRef(type = ResourceTableRoutingBinder.class))
@Indexed(routingBinder = @RoutingBinderRef(type = ResourceTableRoutingBinder.class))
@Entity
@Table(name = ResourceTable.HFJ_RESOURCE, uniqueConstraints = {}, indexes = {
// Do not reuse previously used index name: IDX_INDEXSTATUS, IDX_RES_TYPE
@ -83,23 +86,22 @@ import java.util.stream.Collectors;
@NamedEntityGraph(name = "Resource.noJoins")
public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource<JpaPid> {
public static final int RESTYPE_LEN = 40;
private static final int MAX_LANGUAGE_LENGTH = 20;
private static final long serialVersionUID = 1L;
public static final String HFJ_RESOURCE = "HFJ_RESOURCE";
public static final String RES_TYPE = "RES_TYPE";
private static final int MAX_LANGUAGE_LENGTH = 20;
private static final long serialVersionUID = 1L;
/**
* Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB
* Note the extra config needed in HS6 for indexing transient props:
* https://docs.jboss.org/hibernate/search/6.0/migration/html_single/#indexed-transient-requires-configuration
*
* <p>
* Note that we depend on `myVersion` updated for this field to be indexed.
*/
@Transient
@FullTextField(name = "myContentText", searchable = Searchable.YES, projectable = Projectable.YES, analyzer = "standardAnalyzer")
@FullTextField(name = "myContentTextEdgeNGram", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompleteEdgeAnalyzer")
@FullTextField(name = "myContentTextNGram", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompleteNGramAnalyzer")
@FullTextField(name = "myContentTextPhonetic", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompletePhoneticAnalyzer")
@FullTextField(name = "myContentTextEdgeNGram", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompleteEdgeAnalyzer")
@FullTextField(name = "myContentTextNGram", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompleteNGramAnalyzer")
@FullTextField(name = "myContentTextPhonetic", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompletePhoneticAnalyzer")
@OptimisticLock(excluded = true)
@IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion")))
private String myContentText;
@ -133,9 +135,9 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
*/
@Transient()
@FullTextField(name = "myNarrativeText", searchable = Searchable.YES, projectable = Projectable.YES, analyzer = "standardAnalyzer")
@FullTextField(name = "myNarrativeTextEdgeNGram", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompleteEdgeAnalyzer")
@FullTextField(name = "myNarrativeTextNGram", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompleteNGramAnalyzer")
@FullTextField(name = "myNarrativeTextPhonetic", searchable= Searchable.YES, projectable= Projectable.NO, analyzer = "autocompletePhoneticAnalyzer")
@FullTextField(name = "myNarrativeTextEdgeNGram", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompleteEdgeAnalyzer")
@FullTextField(name = "myNarrativeTextNGram", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompleteNGramAnalyzer")
@FullTextField(name = "myNarrativeTextPhonetic", searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "autocompletePhoneticAnalyzer")
@OptimisticLock(excluded = true)
@IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion")))
private String myNarrativeText;
@ -176,7 +178,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@Column(name = "SP_QUANTITY_PRESENT")
@OptimisticLock(excluded = true)
private boolean myParamsQuantityPopulated;
/**
* Added to support UCUM conversion
* since 5.3.0
@ -184,9 +186,9 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false)
@OptimisticLock(excluded = true)
private Collection<ResourceIndexedSearchParamQuantityNormalized> myParamsQuantityNormalized;
/**
* Added to support UCUM conversion,
* Added to support UCUM conversion,
* NOTE : use Boolean class instead of boolean primitive, in order to set the existing rows to null
* since 5.3.0
*/
@ -278,18 +280,17 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@Transient
private transient boolean myUnchangedInCurrentOperation;
/**
* The id of the Resource.
* Will contain either the client-assigned id, or the sequence value.
* Will be null during insert time until the first read.
*
*/
@Column(name= "FHIR_ID",
@Column(name = "FHIR_ID",
// [A-Za-z0-9\-\.]{1,64} - https://www.hl7.org/fhir/datatypes.html#id
length = 64,
// we never update this after insert, and the Generator will otherwise "dirty" the object.
updatable = false)
// inject the pk for server-assigned sequence ids.
@GeneratorType(when = GenerationTime.INSERT, type = FhirIdGenerator.class)
// Make sure the generator doesn't bump the history version.
@ -305,30 +306,21 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@Column(name = "SEARCH_URL_PRESENT", nullable = true)
private Boolean mySearchUrlPresent = false;
/**
* Populate myFhirId with server-assigned sequence id when no client-id provided.
* We eat this complexity during insert to simplify query time with a uniform column.
* Server-assigned sequence ids aren't available until just before insertion.
* Hibernate calls insert Generators after the pk has been assigned, so we can use myId safely here.
*/
public static final class FhirIdGenerator implements ValueGenerator<String> {
@Override
public String generateValue(Session session, Object owner) {
ResourceTable that = (ResourceTable) owner;
return that.myFhirId != null ? that.myFhirId : that.myId.toString();
}
}
@Version
@Column(name = "RES_VER")
private long myVersion;
@OneToMany(mappedBy = "myResourceTable", fetch = FetchType.LAZY)
private Collection<ResourceHistoryProvenanceEntity> myProvenance;
@Transient
private transient ResourceHistoryTable myCurrentVersionEntity;
@Transient
private transient ResourceHistoryTable myNewVersionEntity;
@Transient
private transient boolean myVersionUpdatedInCurrentTransaction;
@OneToOne(optional = true, fetch = FetchType.EAGER, cascade = {}, orphanRemoval = false, mappedBy = "myResource")
@OptimisticLock(excluded = true)
private ForcedId myForcedId;
@ -343,6 +335,39 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
super();
}
/**
* Setting this flag is an indication that we're making changes and the version number will
* be incremented in the current transaction. When this is set, calls to {@link #getVersion()}
* will be incremented by one.
* This flag is cleared in {@link #postPersist()} since at that time the new version number
* should be reflected.
*/
public void markVersionUpdatedInCurrentTransaction() {
if (!myVersionUpdatedInCurrentTransaction) {
/*
* Note that modifying this number doesn't actually directly affect what
* gets stored in the database since this is a @Version field and the
* value is therefore managed by Hibernate. So in other words, if the
* row in the database is updated, it doesn't matter what we set
* this field to, hibernate will increment it by one. However, we still
* increment it for two reasons:
* 1. The value gets used for the version attribute in the ResourceHistoryTable
* entity we create for each new version.
* 2. For updates to existing resources, there may actually not be any other
* changes to this entity so incrementing this is a signal to
* Hibernate that something changed and we need to force an entity
* update.
*/
myVersion++;
this.myVersionUpdatedInCurrentTransaction = true;
}
}
@PostPersist
public void postPersist() {
myVersionUpdatedInCurrentTransaction = false;
}
@Override
public ResourceTag addTag(TagDefinition theTag) {
for (ResourceTag next : getTags()) {
@ -355,7 +380,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
return tag;
}
public String getHashSha256() {
return myHashSha256;
}
@ -558,6 +582,26 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
return myVersion;
}
/**
* Sets the version on this entity to {@literal 1}. This should only be called
* on resources that are not yet persisted. After that time the version number
* is managed by hibernate.
*/
public void initializeVersion() {
assert myId == null;
myVersion = 1;
}
/**
* Don't call this in any JPA environments, the version will be ignored
* since this field is managed by hibernate
*/
@VisibleForTesting
public void setVersionForUnitTest(long theVersion) {
myVersion = theVersion;
}
@Override
public boolean isDeleted() {
return getDeleted() != null;
@ -568,10 +612,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
setDeleted(null);
}
public void setVersion(long theVersion) {
myVersion = theVersion;
}
public boolean isHasLinks() {
return myHasLinks;
}
@ -633,7 +673,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) {
myParamsQuantityPopulated = theParamsQuantityPopulated;
}
public Boolean isParamsQuantityNormalizedPopulated() {
if (myParamsQuantityNormalizedPopulated == null)
return Boolean.FALSE;
@ -689,14 +729,14 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
myUnchangedInCurrentOperation = theUnchangedInCurrentOperation;
}
public void setContentText(String theContentText) {
myContentText = theContentText;
}
public String getContentText() {
return myContentText;
}
public void setContentText(String theContentText) {
myContentText = theContentText;
}
public void setNarrativeText(String theNarrativeText) {
myNarrativeText = theNarrativeText;
}
@ -709,12 +749,27 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
mySearchUrlPresent = theSearchUrlPresent;
}
/**
* This method creates a new history entity, or might reuse the current one if we've
* already created one in the current transaction. This is because we can only increment
* the version once in a DB transaction (since hibernate manages that number) so creating
* multiple {@link ResourceHistoryTable} entities will result in a constraint error.
*/
public ResourceHistoryTable toHistory(boolean theCreateVersionTags) {
ResourceHistoryTable retVal = new ResourceHistoryTable();
boolean createVersionTags = theCreateVersionTags;
ResourceHistoryTable retVal = myNewVersionEntity;
if (retVal == null) {
retVal = new ResourceHistoryTable();
myNewVersionEntity = retVal;
} else {
// Tags should already be set
createVersionTags = false;
}
retVal.setResourceId(myId);
retVal.setResourceType(myResourceType);
retVal.setVersion(myVersion);
retVal.setVersion(getVersion());
retVal.setTransientForcedId(getTransientForcedId());
retVal.setPublished(getPublishedDate());
@ -725,10 +780,8 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
retVal.setForcedId(getForcedId());
retVal.setPartitionId(getPartitionId());
retVal.getTags().clear();
retVal.setHasTags(isHasTags());
if (isHasTags() && theCreateVersionTags) {
if (isHasTags() && createVersionTags) {
for (ResourceTag next : getTags()) {
retVal.addTag(next);
}
@ -772,16 +825,16 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
* This is a convenience to avoid loading the version a second time within a single transaction. It is
* not persisted.
*/
public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) {
myCurrentVersionEntity = theCurrentVersionEntity;
public ResourceHistoryTable getCurrentVersionEntity() {
return myCurrentVersionEntity;
}
/**
* This is a convenience to avoid loading the version a second time within a single transaction. It is
* not persisted.
*/
public ResourceHistoryTable getCurrentVersionEntity() {
return myCurrentVersionEntity;
public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) {
myCurrentVersionEntity = theCurrentVersionEntity;
}
@Override
@ -799,8 +852,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
myForcedId = theForcedId;
}
@Override
public IdDt getIdDt() {
IdDt retVal = new IdDt();
@ -808,7 +859,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
return retVal;
}
public IIdType getIdType(FhirContext theContext) {
IIdType retVal = theContext.getVersion().newIdType();
populateId(retVal);
@ -830,14 +880,14 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
}
}
public void setCreatedByMatchUrl(String theCreatedByMatchUrl) {
myCreatedByMatchUrl = theCreatedByMatchUrl;
}
public String getCreatedByMatchUrl() {
return myCreatedByMatchUrl;
}
public void setCreatedByMatchUrl(String theCreatedByMatchUrl) {
myCreatedByMatchUrl = theCreatedByMatchUrl;
}
public void setLuceneIndexData(ExtendedHSearchIndexData theLuceneIndexData) {
myLuceneIndexData = theLuceneIndexData;
}
@ -862,4 +912,18 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
public void setFhirId(String theFhirId) {
myFhirId = theFhirId;
}
/**
* Populate myFhirId with server-assigned sequence id when no client-id provided.
* We eat this complexity during insert to simplify query time with a uniform column.
* Server-assigned sequence ids aren't available until just before insertion.
* Hibernate calls insert Generators after the pk has been assigned, so we can use myId safely here.
*/
public static final class FhirIdGenerator implements ValueGenerator<String> {
@Override
public String generateValue(Session session, Object owner) {
ResourceTable that = (ResourceTable) owner;
return that.myFhirId != null ? that.myFhirId : that.myId.toString();
}
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -94,7 +94,7 @@ public class SearchParamRegistryImplTest {
ResourceTable searchParamEntity = new ResourceTable();
searchParamEntity.setResourceType("SearchParameter");
searchParamEntity.setId(theId);
searchParamEntity.setVersion(theVersion);
searchParamEntity.setVersionForUnitTest(theVersion);
return searchParamEntity;
}
@ -199,7 +199,7 @@ public class SearchParamRegistryImplTest {
// Update the resource without changing anything that would affect our cache
ResourceTable lastEntity = newEntities.get(newEntities.size() - 1);
lastEntity.setVersion(2);
lastEntity.setVersionForUnitTest(2);
resetMock(Enumerations.PublicationStatus.ACTIVE, newEntities);
mySearchParamRegistry.requestRefresh();
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 0, 1, 0);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -2,7 +2,10 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.batch2.api.IJobDataSink;
import ca.uhn.fhir.batch2.api.RunOutcome;
import ca.uhn.fhir.batch2.api.VoidModel;
import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson;
import ca.uhn.fhir.batch2.jobs.chunk.TypedPidJson;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeStep;
import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters;
import ca.uhn.fhir.batch2.jobs.reindex.ReindexStep;
import ca.uhn.fhir.context.FhirContext;
@ -10,12 +13,12 @@ import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.ReindexParameters;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum;
import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
@ -98,8 +101,6 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@ -137,26 +138,28 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
@Autowired
private ReindexStep myReindexStep;
@Autowired
private DeleteExpungeStep myDeleteExpungeStep;
@AfterEach
public void afterResetDao() {
myStorageSettings.setResourceMetaCountHardLimit(new JpaStorageSettings().getResourceMetaCountHardLimit());
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
myStorageSettings.setDeleteEnabled(new JpaStorageSettings().isDeleteEnabled());
myStorageSettings.setMatchUrlCacheEnabled(new JpaStorageSettings().isMatchUrlCacheEnabled());
myStorageSettings.setHistoryCountMode(JpaStorageSettings.DEFAULT_HISTORY_COUNT_MODE);
myStorageSettings.setMassIngestionMode(new JpaStorageSettings().isMassIngestionMode());
myStorageSettings.setAutoVersionReferenceAtPaths(new JpaStorageSettings().getAutoVersionReferenceAtPaths());
myStorageSettings.setRespectVersionsForSearchIncludes(new JpaStorageSettings().isRespectVersionsForSearchIncludes());
myFhirContext.getParserOptions().setStripVersionsFromReferences(true);
myStorageSettings.setTagStorageMode(new JpaStorageSettings().getTagStorageMode());
myStorageSettings.clearSupportedSubscriptionTypesForUnitTest();
myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete());
myStorageSettings.setAutoCreatePlaceholderReferenceTargets(new JpaStorageSettings().isAutoCreatePlaceholderReferenceTargets());
myStorageSettings.setAutoVersionReferenceAtPaths(new JpaStorageSettings().getAutoVersionReferenceAtPaths());
myStorageSettings.setDeleteEnabled(new JpaStorageSettings().isDeleteEnabled());
myStorageSettings.setHistoryCountMode(JpaStorageSettings.DEFAULT_HISTORY_COUNT_MODE);
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
myStorageSettings.setInlineResourceTextBelowSize(new JpaStorageSettings().getInlineResourceTextBelowSize());
myStorageSettings.setMassIngestionMode(new JpaStorageSettings().isMassIngestionMode());
myStorageSettings.setMatchUrlCacheEnabled(new JpaStorageSettings().isMatchUrlCacheEnabled());
myStorageSettings.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new JpaStorageSettings().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
myStorageSettings.setResourceClientIdStrategy(new JpaStorageSettings().getResourceClientIdStrategy());
myStorageSettings.setResourceMetaCountHardLimit(new JpaStorageSettings().getResourceMetaCountHardLimit());
myStorageSettings.setRespectVersionsForSearchIncludes(new JpaStorageSettings().isRespectVersionsForSearchIncludes());
myStorageSettings.setTagStorageMode(new JpaStorageSettings().getTagStorageMode());
myStorageSettings.setInlineResourceTextBelowSize(new JpaStorageSettings().getInlineResourceTextBelowSize());
myStorageSettings.clearSupportedSubscriptionTypesForUnitTest();
myFhirContext.getParserOptions().setStripVersionsFromReferences(true);
TermReadSvcImpl.setForceDisableHibernateSearchForUnitTest(false);
}
@ -418,7 +421,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -452,7 +454,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -478,7 +479,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -533,7 +533,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(0, myCaptureQueriesListener.getCommitCount());
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -559,7 +558,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -650,7 +648,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -740,6 +737,62 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@Test
public void testDeleteMultiple() {
for (int i = 0; i < 10; i++) {
createPatient(withId("PT" + i), withActiveTrue(), withIdentifier("http://foo", "id" + i), withFamily("Family" + i));
}
myStorageSettings.setAllowMultipleDelete(true);
// Test
myCaptureQueriesListener.clear();
DeleteMethodOutcome outcome = myPatientDao.deleteByUrl("Patient?active=true", new SystemRequestDetails());
// Validate
assertEquals(13, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(10, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(10, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(30, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(10, outcome.getDeletedEntities().size());
}
@Test
public void testDeleteExpungeStep() {
// Setup
for (int i = 0; i < 10; i++) {
createPatient(
withId("PT" + i),
withActiveTrue(),
withIdentifier("http://foo", "id" + i),
withFamily("Family" + i),
withTag("http://foo", "blah"));
}
List<TypedPidJson> pids = runInTransaction(() -> myForcedIdDao
.findAll()
.stream()
.map(t -> new TypedPidJson(t.getResourceType(), Long.toString(t.getResourceId())))
.collect(Collectors.toList()));
runInTransaction(()-> assertEquals(10, myResourceTableDao.count()));
IJobDataSink<VoidModel> sink = mock(IJobDataSink.class);
// Test
myCaptureQueriesListener.clear();
RunOutcome outcome = myDeleteExpungeStep.doDeleteExpunge(new ResourceIdListWorkChunkJson(pids), sink, "instance-id", "chunk-id");
// Verify
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(29, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(10, outcome.getRecordsProcessed());
runInTransaction(()-> assertEquals(0, myResourceTableDao.count()));
}
/**
* See the class javadoc before changing the counts in this test!
*/
@ -3185,6 +3238,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
return (Bundle) bb.getBundle();
};
// Pass 1
myCaptureQueriesListener.clear();
mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
@ -3192,13 +3247,55 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// Pass 2
myCaptureQueriesListener.clear();
mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
Bundle outcome = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
myCaptureQueriesListener.logInsertQueries();
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(7, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
IdType patientId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("2", patientId.getVersionIdPart());
Patient patient = myPatientDao.read(patientId, mySrd);
assertEquals(1, patient.getMeta().getProfile().size());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("SMITH", patient.getNameFirstRep().getFamily());
patient = myPatientDao.read(patientId.withVersion("1"), mySrd);
assertEquals(1, patient.getMeta().getProfile().size());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("SMITH", patient.getNameFirstRep().getFamily());
// Pass 3
myCaptureQueriesListener.clear();
outcome = mySystemDao.transaction(new SystemRequestDetails(), supplier.get());
assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
myCaptureQueriesListener.logInsertQueries();
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
patientId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("3", patientId.getVersionIdPart());
patient = myPatientDao.read(patientId, mySrd);
assertEquals(1, patient.getMeta().getProfile().size());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("SMITH", patient.getNameFirstRep().getFamily());
patient = myPatientDao.read(patientId.withVersion("2"), mySrd);
assertEquals(1, patient.getMeta().getProfile().size());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("SMITH", patient.getNameFirstRep().getFamily());
patient = myPatientDao.read(patientId.withVersion("1"), mySrd);
assertEquals(1, patient.getMeta().getProfile().size());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("SMITH", patient.getNameFirstRep().getFamily());
}
@ -3206,7 +3303,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
* See the class javadoc before changing the counts in this test!
*/
@Test
public void testMassIngestionMode_TransactionWithChanges_2() throws IOException {
public void testMassIngestionMode_TransactionWithChanges_NonVersionedTags() throws IOException {
myStorageSettings.setDeleteEnabled(false);
myStorageSettings.setMatchUrlCacheEnabled(true);
myStorageSettings.setMassIngestionMode(true);

View File

@ -100,6 +100,9 @@ public class FhirResourceDaoR4TagsInlineTest extends BaseResourceProviderR4Test
mySearchParameterDao.update(searchParameter, mySrd);
mySearchParamRegistry.forceRefresh();
logAllResources();
logAllResourceVersions();
createPatientsForInlineSearchTests();
logAllTokenIndexes();

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.delete.job;
import ca.uhn.fhir.batch2.api.IJobCoordinator;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeAppCtx;
import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeJobParameters;
import ca.uhn.fhir.batch2.model.JobInstance;
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;

View File

@ -254,7 +254,7 @@ public class ReindexJobTest extends BaseJpaR4Test {
resource.setDeleted(currentDate);
resource.setUpdated(currentDate);
resource.setHashSha256(null);
resource.setVersion(2L);
resource.setVersionForUnitTest(2L);
myResourceTableDao.save(resource);
});

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.dao.r5;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.Bundle;
@ -12,15 +14,22 @@ import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.model.Quantity;
import org.hl7.fhir.r5.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import javax.annotation.Nonnull;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class FhirSystemDaoTransactionR5Test extends BaseJpaR5Test {
@ -30,6 +39,7 @@ public class FhirSystemDaoTransactionR5Test extends BaseJpaR5Test {
myStorageSettings.setIndexMissingFields(defaults.getIndexMissingFields());
myStorageSettings.setMatchUrlCacheEnabled(defaults.isMatchUrlCacheEnabled());
myStorageSettings.setDeleteEnabled(defaults.isDeleteEnabled());
myStorageSettings.setInlineResourceTextBelowSize(defaults.getInlineResourceTextBelowSize());
}
@ -435,6 +445,248 @@ public class FhirSystemDaoTransactionR5Test extends BaseJpaR5Test {
}
/**
* If a conditional delete and conditional update are both used on the same condition,
* the update should win.
*/
@Test
public void testConditionalDeleteAndConditionalUpdateOnSameResource() {
Bundle outcome;
Patient actual;
// First pass (resource doesn't already exist)
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(myFhirContext));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType resourceId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless();
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("1", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Second pass (resource already exists)
myCaptureQueriesListener.clear();
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(myFhirContext));
myCaptureQueriesListener.logUpdateQueries();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/2"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
logAllResources();
logAllResourceVersions();
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("2", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Third pass (resource already exists)
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(myFhirContext));
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/3"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("3", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
}
@Test
public void testConditionalDeleteAndConditionalUpdateOnSameResource_MultipleMatchesAlreadyExist() {
// Setup
myPatientDao.create(createPatientWithIdentifierAndTag(), mySrd);
myPatientDao.create(createPatientWithIdentifierAndTag(), mySrd);
// Test
try {
mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(myFhirContext));
fail();
} catch (PreconditionFailedException e) {
// Verify
assertThat(e.getMessage(), containsString("Multiple resources match this search"));
}
}
/**
* If a conditional delete and conditional update are both used on the same condition,
* the update should win.
*/
@Test
public void testConditionalDeleteAndConditionalCreateOnSameResource() {
Bundle outcome;
Patient actual;
// First pass (resource doesn't already exist)
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalCreateOnSameResource(myFhirContext));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType resourceId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless();
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("1", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Second pass (resource already exists)
myCaptureQueriesListener.clear();
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalCreateOnSameResource(myFhirContext));
myCaptureQueriesListener.logUpdateQueries();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
logAllResources();
logAllResourceVersions();
IdType resourceId2 = new IdType(outcome.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless();
assertNotEquals(resourceId.getIdPart(), resourceId2.getIdPart());
actual = myPatientDao.read(resourceId2, mySrd);
assertEquals("1", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Third pass (resource already exists)
outcome = mySystemDao.transaction(mySrd, createBundleWithConditionalDeleteAndConditionalCreateOnSameResource(myFhirContext));
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertThat(outcome.getEntry().get(1).getResponse().getLocation(), endsWith("_history/1"));
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType resourceId3 = new IdType(outcome.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless();
assertNotEquals(resourceId2.getIdPart(), resourceId3.getIdPart());
actual = myPatientDao.read(resourceId3, mySrd);
assertEquals("1", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
}
/**
* There's not much point to deleting and updating the same resource in a
* transaction, but let's make sure we at least don't end up in a bad state
*/
@Test
public void testDeleteAndUpdateOnSameResource() {
Bundle outcome;
Patient actual;
// First pass (resource doesn't already exist)
outcome = mySystemDao.transaction(mySrd, createBundleWithDeleteAndUpdateOnSameResource(myFhirContext));
assertEquals(null, outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("Patient/P/_history/1", outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
IdType resourceId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()).toUnqualifiedVersionless();
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("1", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Second pass (resource already exists)
myCaptureQueriesListener.clear();
outcome = mySystemDao.transaction(mySrd, createBundleWithDeleteAndUpdateOnSameResource(myFhirContext));
myCaptureQueriesListener.logUpdateQueries();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("Patient/P/_history/2", outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("Patient/P/_history/2", outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
logAllResources();
logAllResourceVersions();
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("2", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
// Third pass (resource already exists)
outcome = mySystemDao.transaction(mySrd, createBundleWithDeleteAndUpdateOnSameResource(myFhirContext));
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
assertEquals("Patient/P/_history/3", outcome.getEntry().get(0).getResponse().getLocation());
assertEquals("204 No Content", outcome.getEntry().get(0).getResponse().getStatus());
assertEquals("Patient/P/_history/3", outcome.getEntry().get(1).getResponse().getLocation());
assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus());
actual = myPatientDao.read(resourceId, mySrd);
assertEquals("3", actual.getIdElement().getVersionIdPart());
assertEquals("http://foo", actual.getIdentifierFirstRep().getSystem());
assertEquals("http://tag", actual.getMeta().getTagFirstRep().getSystem());
}
@Nonnull
private static Bundle createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(FhirContext theFhirContext) {
// Build a new bundle each time we need it
BundleBuilder bb = new BundleBuilder(theFhirContext);
bb.addTransactionDeleteConditionalEntry("Patient?identifier=http://foo|bar");
Patient patient = createPatientWithIdentifierAndTag();
bb.addTransactionUpdateEntry(patient).conditional("Patient?identifier=http://foo|bar");
return bb.getBundleTyped();
}
@Nonnull
private static Bundle createBundleWithConditionalDeleteAndConditionalCreateOnSameResource(FhirContext theFhirContext) {
// Build a new bundle each time we need it
BundleBuilder bb = new BundleBuilder(theFhirContext);
bb.addTransactionDeleteConditionalEntry("Patient?identifier=http://foo|bar");
Patient patient = createPatientWithIdentifierAndTag();
patient.setId((String)null);
bb.addTransactionCreateEntry(patient).conditional("Patient?identifier=http://foo|bar");
return bb.getBundleTyped();
}
@Nonnull
private static Bundle createBundleWithDeleteAndUpdateOnSameResource(FhirContext theFhirContext) {
// Build a new bundle each time we need it
BundleBuilder bb = new BundleBuilder(theFhirContext);
bb.addTransactionDeleteEntry(new IdType("Patient/P"));
bb.addTransactionUpdateEntry(createPatientWithIdentifierAndTag());
return bb.getBundleTyped();
}
@Nonnull
private static Patient createPatientWithIdentifierAndTag() {
Patient patient = new Patient();
patient.setId("Patient/P");
patient.getMeta().addTag("http://tag", "tag-code", "tag-display");
patient.addIdentifier().setSystem("http://foo").setValue("bar");
return patient;
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -33,13 +33,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Supplier;
/**
@ -62,6 +56,7 @@ public class TransactionDetails {
private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap();
private Map<String, Supplier<IBaseResource>> myResolvedResources = Collections.emptyMap();
private Set<IResourcePersistentId> myDeletedResourceIds = Collections.emptySet();
private Map<String, Object> myUserData;
private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
@ -114,6 +109,34 @@ public class TransactionDetails {
}
}
/**
* @since 6.8.0
*/
public void addDeletedResourceId(@Nonnull IResourcePersistentId theResourceId) {
Validate.notNull(theResourceId, "theResourceId must not be null");
if (myDeletedResourceIds.isEmpty()) {
myDeletedResourceIds = new HashSet<>();
}
myDeletedResourceIds.add(theResourceId);
}
/**
* @since 6.8.0
*/
public void addDeletedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) {
for (IResourcePersistentId<?> next : theResourceIds) {
addDeletedResourceId(next);
}
}
/**
* @since 6.8.0
*/
@Nonnull
public Set<IResourcePersistentId> getDeletedResourceIds() {
return Collections.unmodifiableSet(myDeletedResourceIds);
}
/**
* A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
* "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
@ -223,6 +246,15 @@ public class TransactionDetails {
myResolvedMatchUrls.put(theConditionalUrl, thePersistentId);
}
/**
* @since 6.8.0
* @see #addResolvedMatchUrl(FhirContext, String, IResourcePersistentId)
*/
public void removeResolvedMatchUrl(String theMatchUrl) {
myResolvedMatchUrls.remove(theMatchUrl);
}
private boolean matchUrlWithDiffIdExists(String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
if (myResolvedMatchUrls.containsKey(theConditionalUrl) && myResolvedMatchUrls.get(theConditionalUrl) != NOT_FOUND) {
return myResolvedMatchUrls.get(theConditionalUrl).getId() != thePersistentId.getId();
@ -359,13 +391,13 @@ public class TransactionDetails {
return !myResolvedResourceIds.isEmpty();
}
public void setFhirTransaction(boolean theFhirTransaction) {
myFhirTransaction = theFhirTransaction;
}
public boolean isFhirTransaction() {
return myFhirTransaction;
}
public void setFhirTransaction(boolean theFhirTransaction) {
myFhirTransaction = theFhirTransaction;
}
}

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -96,7 +96,7 @@ public class DeleteExpungeStep implements IJobStepWorker<ReindexJobParameters, R
List<JpaPid> persistentIds = myData.getResourcePersistentIds(myIdHelperService);
if (persistentIds.isEmpty()) {
ourLog.info("Starting delete expunge work chunk. Ther are no resources to delete expunge - Instance[{}] Chunk[{}]", myInstanceId, myChunkId);
ourLog.info("Starting delete expunge work chunk. There are no resources to delete expunge - Instance[{}] Chunk[{}]", myInstanceId, myChunkId);
return null;
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -85,7 +85,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
* won't be indexed and searches won't work.
* @param theRequestDetails The request details including permissions and partitioning information
*/
DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails);
DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails);
DaoMethodOutcome create(T theResource, String theIfNoneExist, RequestDetails theRequestDetails);
@ -111,14 +111,40 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
/**
* This method does not throw an exception if there are delete conflicts, but populates them
* in the provided list
*
* @since 6.8.0
*/
DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList theDeleteConflictsListToPopulate, RequestDetails theRequestDetails);
DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList theDeleteConflictsListToPopulate, RequestDetails theRequestDetails, @Nonnull TransactionDetails theTransactionDetails);
/**
* This method throws an exception if there are delete conflicts
*/
DeleteMethodOutcome deleteByUrl(String theString, RequestDetails theRequestDetails);
/**
* @deprecated Deprecated in 6.8.0 - Use and implement {@link #deletePidList(String, Collection, DeleteConflictList, RequestDetails, TransactionDetails)}
*/
default <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(String theUrl, Collection<P> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) {
return deletePidList(theUrl, theResourceIds, theDeleteConflicts, theRequest, new TransactionDetails());
}
/**
* Delete a list of resource Pids
* <p>
* CAUTION: This list does not throw an exception if there are delete conflicts. It should always be followed by
* a call to DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(fhirContext, conflicts);
* to actually throw the exception. The reason this method doesn't do that itself is that it is expected to be
* called repeatedly where an earlier conflict can be removed in a subsequent pass.
*
* @param theUrl the original URL that triggered the deletion
* @param theResourceIds the ids of the resources to be deleted
* @param theDeleteConflicts out parameter of conflicts preventing deletion
* @param theRequestDetails the request that initiated the request
* @return response back to the client
* @since 6.8.0
*/
<P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(String theUrl, Collection<P> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails);
ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails);
ExpungeOutcome expunge(IIdType theIIdType, ExpungeOptions theExpungeOptions, RequestDetails theRequest);
@ -331,22 +357,6 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria);
/**
* Delete a list of resource Pids
* <p>
* CAUTION: This list does not throw an exception if there are delete conflicts. It should always be followed by
* a call to DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(fhirContext, conflicts);
* to actually throw the exception. The reason this method doesn't do that itself is that it is expected to be
* called repeatedly where an earlier conflict can be removed in a subsequent pass.
*
* @param theUrl the original URL that triggered the delete
* @param theResourceIds the ids of the resources to be deleted
* @param theDeleteConflicts out parameter of conflicts preventing deletion
* @param theRequest the request that initiated the request
* @return response back to the client
*/
<P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(String theUrl, Collection<P> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest);
/**
* @deprecated use #read(IIdType, RequestDetails) instead
*/

View File

@ -961,7 +961,7 @@ public abstract class BaseTransactionProcessor {
String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
// create individual resource
outcome = resourceDao.create(res, matchUrl, false, theTransactionDetails, theRequest);
outcome = resourceDao.create(res, matchUrl, false, theRequest, theTransactionDetails);
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId());
res.setId(outcome.getId());
@ -999,7 +999,7 @@ public abstract class BaseTransactionProcessor {
} else {
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequest, theTransactionDetails);
setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, deleteOutcome.getId());
List<? extends IBasePersistedResource> allDeleted = deleteOutcome.getDeletedEntities();
for (IBasePersistedResource deleted : allDeleted) {

View File

@ -222,4 +222,9 @@ public class MatchResourceUrlService<T extends IResourcePersistentId> {
}
}
public void unresolveMatchUrl(TransactionDetails theTransactionDetails, String theResourceType, String theMatchUrl) {
Validate.notBlank(theMatchUrl);
String matchUrl = massageForStorage(theResourceType, theMatchUrl);
theTransactionDetails.removeResolvedMatchUrl(matchUrl);
}
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import org.hl7.fhir.r4.model.AuditEvent;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import org.hl7.fhir.r4.model.AuditEvent;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import ca.uhn.fhir.rest.api.server.RequestDetails;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.storage.interceptor.balp;
import org.hl7.fhir.r4.model.AuditEvent;

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.parser;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.parser;
import static org.junit.jupiter.api.Assertions.assertThrows;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.parser;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.test.concurrency;
import ca.uhn.fhir.interceptor.api.HookParams;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.test.concurrency;
import ca.uhn.fhir.i18n.Msg;

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.0-SNAPSHOT</version>
<version>6.7.1-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

Some files were not shown because too many files have changed in this diff Show More