Merge pull request #2943 from hapifhir/issue-2901-npe-in-bundle-transaction

Issue 2901 npe in bundle transaction
This commit is contained in:
Tadgh 2021-09-08 09:51:55 -04:00 committed by GitHub
commit e4138d9341
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1330 additions and 285 deletions

View File

@ -961,6 +961,7 @@ public class FhirTerser {
for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) {
List<?> values = nextChild.getAccessor().getValues(theElement);
if (values != null) {
for (Object nextValueObject : values) {
IBase nextValue;

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 2901
jira: SMILE-3004
title: "Processing transactions with AutoversionAtPaths set should create those resources (if AutoCreatePlaceholders is set) and use latest version as expected"

View File

@ -302,6 +302,14 @@ If the server has been configured with a [Resource Server ID Strategy](/apidocs/
Contains the specific version (starting with 1) of the resource that this row corresponds to.
</td>
</tr>
<tr>
<td>RESOURCE_TYPE</td>
<td></td>
<td>String</td>
<td>
Contains the string specifying the type of the resource (Patient, Observation, etc).
</td>
</tr>
</tbody>
</table>
@ -476,7 +484,7 @@ The following columns are common to **all HFJ_SPIDX_xxx tables**.
<tr>
<td>RES_ID</td>
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
<td>String</td>
<td>Long</td>
<td></td>
<td>
Contains the PID of the resource being indexed.

View File

@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.api.dao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.rest.annotation.Offset;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseBundle;

View File

@ -1207,7 +1207,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) {
newParams = new ResourceIndexedSearchParams();
mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing);
changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);

View File

@ -136,9 +136,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;

View File

@ -25,19 +25,17 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
@ -45,12 +43,16 @@ import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.OperationOutcomeUtil;
@ -91,6 +93,10 @@ public abstract class BaseStorageDao {
protected DaoRegistry myDaoRegistry;
@Autowired
protected ModelConfig myModelConfig;
@Autowired
protected IdHelperService myIdHelperService;
@Autowired
protected DaoConfig myDaoConfig;
@VisibleForTesting
public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
@ -204,10 +210,34 @@ public abstract class BaseStorageDao {
for (IBaseReference nextReference : referencesToVersion) {
IIdType referenceElement = nextReference.getReferenceElement();
if (!referenceElement.hasBaseUrl()) {
String resourceType = referenceElement.getResourceType();
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
String targetVersionId = dao.getCurrentVersionId(referenceElement);
String newTargetReference = referenceElement.withVersion(targetVersionId).getValue();
Map<IIdType, ResourcePersistentId> idToPID = myIdHelperService.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
Collections.singletonList(referenceElement)
);
// 3 cases:
// 1) there exists a resource in the db with some version (use this version)
// 2) no resource exists, but we will create one (eventually). The version is 1
// 3) no resource exists, and none will be made -> throw
Long version;
if (idToPID.containsKey(referenceElement)) {
// the resource exists... latest id
// will be the value in the ResourcePersistentId
version = idToPID.get(referenceElement).getVersion();
}
else if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
// if idToPID doesn't contain object
// but autcreateplaceholders is on
// then the version will be 1 (the first version)
version = 1L;
}
else {
// resource not found
// and no autocreateplaceholders set...
// we throw
throw new ResourceNotFoundException(referenceElement);
}
String newTargetReference = referenceElement.withVersion(version.toString()).getValue();
nextReference.setReference(newTargetReference);
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -34,6 +35,7 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
@ -52,6 +54,7 @@ import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
@ -68,11 +71,11 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ServletRequestUtil;
import ca.uhn.fhir.util.AsyncUtil;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.AsyncUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
@ -90,7 +93,6 @@ import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -117,11 +119,11 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static ca.uhn.fhir.util.StringUtil.toUtf8String;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -157,6 +159,9 @@ public abstract class BaseTransactionProcessor {
private TaskExecutor myExecutor ;
@Autowired
private IdHelperService myIdHelperService;
@VisibleForTesting
public void setDaoConfig(DaoConfig theDaoConfig) {
myDaoConfig = theDaoConfig;
@ -252,8 +257,10 @@ public abstract class BaseTransactionProcessor {
myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
}
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
IBase newEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) {
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome,
IIdType nextResourceId, DaoMethodOutcome outcome,
IBase newEntry, String theResourceType,
IBaseResource theRes, RequestDetails theRequestDetails) {
IIdType newId = outcome.getId().toUnqualified();
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
if (newId.equals(resourceId) == false) {
@ -394,7 +401,8 @@ public abstract class BaseTransactionProcessor {
myHapiTransactionService = theHapiTransactionService;
}
private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest, final String theActionName, boolean theNestedMode) {
private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest,
final String theActionName, boolean theNestedMode) {
validateDependencies();
String transactionType = myVersionAdapter.getBundleType(theRequest);
@ -412,7 +420,8 @@ public abstract class BaseTransactionProcessor {
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
}
int numberOfEntries = myVersionAdapter.getEntries(theRequest).size();
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
int numberOfEntries = requestEntries.size();
if (myDaoConfig.getMaximumTransactionBundleSize() != null && numberOfEntries > myDaoConfig.getMaximumTransactionBundleSize()) {
throw new PayloadTooLargeException("Transaction Bundle Too large. Transaction bundle contains " +
@ -425,8 +434,6 @@ public abstract class BaseTransactionProcessor {
final TransactionDetails transactionDetails = new TransactionDetails();
final StopWatch transactionStopWatch = new StopWatch();
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
// Do all entries have a verb?
for (int i = 0; i < numberOfEntries; i++) {
IBase nextReqEntry = requestEntries.get(i);
@ -450,10 +457,11 @@ public abstract class BaseTransactionProcessor {
List<IBase> getEntries = new ArrayList<>();
final IdentityHashMap<IBase, Integer> originalRequestOrder = new IdentityHashMap<>();
for (int i = 0; i < requestEntries.size(); i++) {
originalRequestOrder.put(requestEntries.get(i), i);
IBase requestEntry = requestEntries.get(i);
originalRequestOrder.put(requestEntry, i);
myVersionAdapter.addEntry(response);
if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntries.get(i)).equals("GET")) {
getEntries.add(requestEntries.get(i));
if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntry).equals("GET")) {
getEntries.add(requestEntry);
}
}
@ -472,73 +480,17 @@ public abstract class BaseTransactionProcessor {
}
entries.sort(new TransactionSorter(placeholderIds));
doTransactionWriteOperations(theRequestDetails, theActionName, transactionDetails, transactionStopWatch, response, originalRequestOrder, entries);
// perform all writes
doTransactionWriteOperations(theRequestDetails, theActionName,
transactionDetails, transactionStopWatch,
response, originalRequestOrder, entries);
/*
* Loop through the request and process any entries of type GET
*/
if (getEntries.size() > 0) {
transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries");
}
for (IBase nextReqEntry : getEntries) {
if (theNestedMode) {
throw new InvalidRequestException("Can not invoke read operation on nested transaction");
}
if (!(theRequestDetails instanceof ServletRequestDetails)) {
throw new MethodNotAllowedException("Can not call transaction GET methods from this context");
}
ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails;
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
IBase nextRespEntry = (IBase) myVersionAdapter.getEntries(response).get(originalOrder);
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
String transactionUrl = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
ServletSubRequestDetails requestDetails = ServletRequestUtil.getServletSubRequestDetails(srd, transactionUrl, paramValues);
String url = requestDetails.getRequestPath();
BaseMethodBinding<?> method = srd.getServer().determineResourceMethod(requestDetails, url);
if (method == null) {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
}
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
try {
BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
requestDetails.setRestOperationType(methodBinding.getRestOperationType());
IBaseResource resource = methodBinding.doInvokeServer(srd.getServer(), requestDetails);
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
myVersionAdapter.setResource(nextRespEntry, resource);
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
} catch (BaseServerResponseException e) {
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
populateEntryWithOperationOutcome(e, nextRespEntry);
}
}
transactionStopWatch.endCurrentTask();
// perform all gets
// (we do these last so that the gets happen on the final state of the DB;
// see above note)
doTransactionReadOperations(theRequestDetails, response,
getEntries, originalRequestOrder,
transactionStopWatch, theNestedMode);
// Interceptor broadcast: JPA_PERFTRACE_INFO
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) {
@ -555,6 +507,74 @@ public abstract class BaseTransactionProcessor {
return response;
}
private void doTransactionReadOperations(final RequestDetails theRequestDetails, IBaseBundle theResponse,
List<IBase> theGetEntries, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
StopWatch theTransactionStopWatch, boolean theNestedMode) {
if (theGetEntries.size() > 0) {
theTransactionStopWatch.startTask("Process " + theGetEntries.size() + " GET entries");
/*
* Loop through the request and process any entries of type GET
*/
for (IBase nextReqEntry : theGetEntries) {
if (theNestedMode) {
throw new InvalidRequestException("Can not invoke read operation on nested transaction");
}
if (!(theRequestDetails instanceof ServletRequestDetails)) {
throw new MethodNotAllowedException("Can not call transaction GET methods from this context");
}
ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails;
Integer originalOrder = theOriginalRequestOrder.get(nextReqEntry);
IBase nextRespEntry = (IBase) myVersionAdapter.getEntries(theResponse).get(originalOrder);
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
String transactionUrl = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
ServletSubRequestDetails requestDetails = ServletRequestUtil.getServletSubRequestDetails(srd, transactionUrl, paramValues);
String url = requestDetails.getRequestPath();
BaseMethodBinding<?> method = srd.getServer().determineResourceMethod(requestDetails, url);
if (method == null) {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
}
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
try {
BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method;
requestDetails.setRestOperationType(methodBinding.getRestOperationType());
IBaseResource resource = methodBinding.doInvokeServer(srd.getServer(), requestDetails);
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
myVersionAdapter.setResource(nextRespEntry, resource);
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
} catch (BaseServerResponseException e) {
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
populateEntryWithOperationOutcome(e, nextRespEntry);
}
}
theTransactionStopWatch.endCurrentTask();
}
}
/**
* All of the write operations in the transaction (PUT, POST, etc.. basically anything
* except GET) are performed in their own database transaction before we do the reads.
@ -564,7 +584,10 @@ public abstract class BaseTransactionProcessor {
* heavy load with lots of concurrent transactions using all available
* database connections.
*/
private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName, TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries) {
private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName,
TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch,
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
List<IBase> theEntries) {
TransactionWriteOperationsDetails writeOperationsDetails = null;
if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) ||
CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, myInterceptorBroadcaster, theRequestDetails)) {
@ -593,14 +616,18 @@ public abstract class BaseTransactionProcessor {
.add(TransactionDetails.class, theTransactionDetails)
.add(TransactionWriteOperationsDetails.class, writeOperationsDetails);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, params);
}
TransactionCallback<Map<IBase, IIdType>> txCallback = status -> {
final Set<IIdType> allIds = new LinkedHashSet<>();
final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
Map<IBase, IIdType> retVal = doTransactionWriteOperations(theRequestDetails, theActionName, theTransactionDetails, allIds, idSubstitutions, idToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch);
Map<IBase, IIdType> retVal = doTransactionWriteOperations(theRequestDetails, theActionName,
theTransactionDetails, allIds,
idSubstitutions, idToPersistedOutcome,
theResponse, theOriginalRequestOrder,
theEntries, theTransactionStopWatch);
theTransactionStopWatch.startTask("Commit writes to database");
return retVal;
@ -609,7 +636,8 @@ public abstract class BaseTransactionProcessor {
try {
entriesToProcess = myHapiTransactionService.execute(theRequestDetails, theTransactionDetails, txCallback);
} finally {
}
finally {
if (writeOperationsDetails != null) {
HookParams params = new HookParams()
.add(TransactionDetails.class, theTransactionDetails)
@ -664,8 +692,129 @@ public abstract class BaseTransactionProcessor {
myModelConfig = theModelConfig;
}
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder, List<IBase> theEntries, StopWatch theTransactionStopWatch) {
/**
* Searches for duplicate conditional creates and consolidates them.
*
* @param theEntries
*/
private void consolidateDuplicateConditionals(List<IBase> theEntries) {
final HashMap<String, String> keyToUuid = new HashMap<>();
for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) {
IBase nextReqEntry = theEntries.get(index);
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
if (resource != null) {
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
// Conditional UPDATE
boolean consolidateEntry = false;
if ("PUT".equals(verb)) {
if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
int questionMarkIndex = requestUrl.indexOf('?');
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
consolidateEntry = true;
}
}
}
// Conditional CREATE
if ("POST".equals(verb)) {
if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
if (!entryUrl.equals(requestUrl)) {
consolidateEntry = true;
}
}
}
if (consolidateEntry) {
if (!keyToUuid.containsKey(key)) {
keyToUuid.put(key, entryUrl);
} else {
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
theEntries.remove(index);
index--;
String existingUuid = keyToUuid.get(key);
for (IBase nextEntry : theEntries) {
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
// We're interested in any references directly to the placeholder ID, but also
// references that have a resource target that has the placeholder ID.
String nextReferenceId = nextReference.getReferenceElement().getValue();
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
nextReferenceId = nextReference.getResource().getIdElement().getValue();
}
if (entryUrl.equals(nextReferenceId)) {
nextReference.setReference(existingUuid);
nextReference.setResource(null);
}
}
}
}
}
}
}
}
/**
* Retrieves the next resource id (IIdType) from the base resource and next request entry.
* @param theBaseResource - base resource
* @param theNextReqEntry - next request entry
* @param theAllIds - set of all IIdType values
* @return
*/
private IIdType getNextResourceIdFromBaseResource(IBaseResource theBaseResource,
IBase theNextReqEntry,
Set<IIdType> theAllIds) {
IIdType nextResourceId = null;
if (theBaseResource != null) {
nextResourceId = theBaseResource.getIdElement();
String fullUrl = myVersionAdapter.getFullUrl(theNextReqEntry);
if (isNotBlank(fullUrl)) {
IIdType fullUrlIdType = newIdType(fullUrl);
if (isPlaceholder(fullUrlIdType)) {
nextResourceId = fullUrlIdType;
} else if (!nextResourceId.hasIdPart()) {
nextResourceId = fullUrlIdType;
}
}
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
}
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
nextResourceId = newIdType(toResourceName(theBaseResource.getClass()), nextResourceId.getIdPart());
theBaseResource.setId(nextResourceId);
}
/*
* Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
*/
if (isPlaceholder(nextResourceId)) {
if (!theAllIds.add(nextResourceId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
}
} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
IIdType nextId = nextResourceId.toUnqualifiedVersionless();
if (!theAllIds.add(nextId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
}
}
}
return nextResourceId;
}
protected Map<IBase, IIdType> doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
TransactionDetails theTransactionDetails, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
IBaseBundle theResponse, IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
List<IBase> theEntries, StopWatch theTransactionStopWatch) {
theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts(
Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED,
@ -673,7 +822,6 @@ public abstract class BaseTransactionProcessor {
Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED
);
try {
Set<String> deletedResources = new HashSet<>();
DeleteConflictList deleteConflicts = new DeleteConflictList();
Map<IBase, IIdType> entriesToProcess = new IdentityHashMap<>();
@ -685,117 +833,20 @@ public abstract class BaseTransactionProcessor {
/*
* Look for duplicate conditional creates and consolidate them
*/
final HashMap<String, String> keyToUuid = new HashMap<>();
for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) {
IBase nextReqEntry = theEntries.get(index);
IBaseResource resource = myVersionAdapter.getResource(nextReqEntry);
if (resource != null) {
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
String entryUrl = myVersionAdapter.getFullUrl(nextReqEntry);
String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry);
String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
String key = verb + "|" + requestUrl + "|" + ifNoneExist;
// Conditional UPDATE
boolean consolidateEntry = false;
if ("PUT".equals(verb)) {
if (isNotBlank(entryUrl) && isNotBlank(requestUrl)) {
int questionMarkIndex = requestUrl.indexOf('?');
if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) {
consolidateEntry = true;
}
}
}
// Conditional CREATE
if ("POST".equals(verb)) {
if (isNotBlank(entryUrl) && isNotBlank(requestUrl) && isNotBlank(ifNoneExist)) {
if (!entryUrl.equals(requestUrl)) {
consolidateEntry = true;
}
}
}
if (consolidateEntry) {
if (!keyToUuid.containsKey(key)) {
keyToUuid.put(key, entryUrl);
} else {
ourLog.info("Discarding transaction bundle entry {} as it contained a duplicate conditional {}", originalIndex, verb);
theEntries.remove(index);
index--;
String existingUuid = keyToUuid.get(key);
for (IBase nextEntry : theEntries) {
IBaseResource nextResource = myVersionAdapter.getResource(nextEntry);
for (IBaseReference nextReference : myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) {
// We're interested in any references directly to the placeholder ID, but also
// references that have a resource target that has the placeholder ID.
String nextReferenceId = nextReference.getReferenceElement().getValue();
if (isBlank(nextReferenceId) && nextReference.getResource() != null) {
nextReferenceId = nextReference.getResource().getIdElement().getValue();
}
if (entryUrl.equals(nextReferenceId)) {
nextReference.setReference(existingUuid);
nextReference.setResource(null);
}
}
}
}
}
}
}
consolidateDuplicateConditionals(theEntries);
/*
* Loop through the request and process any entries of type
* PUT, POST or DELETE
*/
for (int i = 0; i < theEntries.size(); i++) {
if (i % 250 == 0) {
ourLog.debug("Processed {} non-GET entries out of {} in transaction", i, theEntries.size());
}
IBase nextReqEntry = theEntries.get(i);
IBaseResource res = myVersionAdapter.getResource(nextReqEntry);
IIdType nextResourceId = null;
if (res != null) {
nextResourceId = res.getIdElement();
String fullUrl = myVersionAdapter.getFullUrl(nextReqEntry);
if (isNotBlank(fullUrl)) {
IIdType fullUrlIdType = newIdType(fullUrl);
if (isPlaceholder(fullUrlIdType)) {
nextResourceId = fullUrlIdType;
} else if (!nextResourceId.hasIdPart()) {
nextResourceId = fullUrlIdType;
}
}
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) {
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
}
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart());
res.setId(nextResourceId);
}
/*
* Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
*/
if (isPlaceholder(nextResourceId)) {
if (!theAllIds.add(nextResourceId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
}
} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
IIdType nextId = nextResourceId.toUnqualifiedVersionless();
if (!theAllIds.add(nextId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
}
}
}
IIdType nextResourceId = getNextResourceIdFromBaseResource(res, nextReqEntry, theAllIds);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
String resourceType = res != null ? myContext.getResourceType(res) : null;
@ -904,7 +955,8 @@ public abstract class BaseTransactionProcessor {
}
}
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequest);
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId,
outcome, nextRespEntry, resourceType, res, theRequest);
entriesToProcess.put(nextRespEntry, outcome.getId());
break;
}
@ -971,52 +1023,24 @@ public abstract class BaseTransactionProcessor {
* was also deleted as a part of this transaction, which is why we check this now at the
* end.
*/
for (Iterator<DeleteConflict> iter = deleteConflicts.iterator(); iter.hasNext(); ) {
DeleteConflict nextDeleteConflict = iter.next();
checkForDeleteConflicts(deleteConflicts, deletedResources, updatedResources);
/*
* If we have a conflict, it means we can't delete Resource/A because
* Resource/B has a reference to it. We'll ignore that conflict though
* if it turns out we're also deleting Resource/B in this transaction.
*/
if (deletedResources.contains(nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) {
iter.remove();
continue;
}
/*
* And then, this is kind of a last ditch check. It's also ok to delete
* Resource/A if Resource/B isn't being deleted, but it is being UPDATED
* in this transaction, and the updated version of it has no references
* to Resource/A any more.
*/
String sourceId = nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue();
String targetId = nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue();
Optional<IBaseResource> updatedSource = updatedResources
.stream()
.filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
.findFirst();
if (updatedSource.isPresent()) {
List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
boolean sourceStillReferencesTarget = referencesInSource
.stream()
.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
if (!sourceStillReferencesTarget) {
iter.remove();
}
}
}
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(myContext, deleteConflicts);
theIdToPersistedOutcome.entrySet().forEach(t -> theTransactionDetails.addResolvedResourceId(t.getKey(), t.getValue().getPersistentId()));
theIdToPersistedOutcome.entrySet().forEach(idAndOutcome -> {
theTransactionDetails.addResolvedResourceId(idAndOutcome.getKey(), idAndOutcome.getValue().getPersistentId());
});
/*
* Perform ID substitutions and then index each resource we have saved
*/
resolveReferencesThenSaveAndIndexResources(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, theTransactionStopWatch, entriesToProcess, nonUpdatedEntities, updatedEntities);
resolveReferencesThenSaveAndIndexResources(theRequest, theTransactionDetails,
theIdSubstitutions, theIdToPersistedOutcome,
theTransactionStopWatch, entriesToProcess,
nonUpdatedEntities, updatedEntities);
theTransactionStopWatch.endCurrentTask();
// flush writes to db
theTransactionStopWatch.startTask("Flush writes to database");
flushSession(theIdToPersistedOutcome);
@ -1070,6 +1094,53 @@ public abstract class BaseTransactionProcessor {
}
}
/**
* Checks for any delete conflicts.
* @param theDeleteConflicts - set of delete conflicts
* @param theDeletedResources - set of deleted resources
* @param theUpdatedResources - list of updated resources
*/
private void checkForDeleteConflicts(DeleteConflictList theDeleteConflicts,
Set<String> theDeletedResources,
List<IBaseResource> theUpdatedResources) {
for (Iterator<DeleteConflict> iter = theDeleteConflicts.iterator(); iter.hasNext(); ) {
DeleteConflict nextDeleteConflict = iter.next();
/*
* If we have a conflict, it means we can't delete Resource/A because
* Resource/B has a reference to it. We'll ignore that conflict though
* if it turns out we're also deleting Resource/B in this transaction.
*/
if (theDeletedResources.contains(nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) {
iter.remove();
continue;
}
/*
* And then, this is kind of a last ditch check. It's also ok to delete
* Resource/A if Resource/B isn't being deleted, but it is being UPDATED
* in this transaction, and the updated version of it has no references
* to Resource/A any more.
*/
String sourceId = nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue();
String targetId = nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue();
Optional<IBaseResource> updatedSource = theUpdatedResources
.stream()
.filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
.findFirst();
if (updatedSource.isPresent()) {
List<ResourceReferenceInfo> referencesInSource = myContext.newTerser().getAllResourceReferences(updatedSource.get());
boolean sourceStillReferencesTarget = referencesInSource
.stream()
.anyMatch(t -> targetId.equals(t.getResourceReference().getReferenceElement().toUnqualifiedVersionless().getValue()));
if (!sourceStillReferencesTarget) {
iter.remove();
}
}
}
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(myContext, theDeleteConflicts);
}
/**
* This method replaces any placeholder references in the
* source transaction Bundle with their actual targets, then stores the resource contents and indexes
@ -1092,7 +1163,10 @@ public abstract class BaseTransactionProcessor {
* pass because it's too complex to try and insert the auto-versioned references and still
* account for NOPs, so we block NOPs in that pass.
*/
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
StopWatch theTransactionStopWatch, Map<IBase, IIdType> entriesToProcess,
Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities) {
FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null;
@ -1114,8 +1188,15 @@ public abstract class BaseTransactionProcessor {
Set<IBaseReference> referencesToAutoVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
if (referencesToAutoVersion.isEmpty()) {
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
// no references to autoversion - we can do the resolve and save now
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails,
theIdSubstitutions, theIdToPersistedOutcome,
entriesToProcess, nonUpdatedEntities,
updatedEntities, terser,
nextOutcome, nextResource,
referencesToAutoVersion); // this is empty
} else {
// we have autoversioned things to defer until later
if (deferredIndexesForAutoVersioning == null) {
deferredIndexesForAutoVersioning = new IdentityHashMap<>();
}
@ -1129,12 +1210,24 @@ public abstract class BaseTransactionProcessor {
DaoMethodOutcome nextOutcome = nextEntry.getKey();
Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue();
IBaseResource nextResource = nextOutcome.getResource();
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails, theIdSubstitutions, theIdToPersistedOutcome, entriesToProcess, nonUpdatedEntities, updatedEntities, terser, nextOutcome, nextResource, referencesToAutoVersion);
resolveReferencesThenSaveAndIndexResource(theRequest, theTransactionDetails,
theIdSubstitutions, theIdToPersistedOutcome,
entriesToProcess, nonUpdatedEntities,
updatedEntities, terser,
nextOutcome, nextResource,
referencesToAutoVersion);
}
}
}
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails, Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities, Set<IBasePersistedResource> updatedEntities, FhirTerser terser, DaoMethodOutcome nextOutcome, IBaseResource nextResource, Set<IBaseReference> theReferencesToAutoVersion) {
private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
Map<IBase, IIdType> entriesToProcess, Set<IIdType> nonUpdatedEntities,
Set<IBasePersistedResource> updatedEntities, FhirTerser terser,
DaoMethodOutcome nextOutcome, IBaseResource nextResource,
Set<IBaseReference> theReferencesToAutoVersion) {
// References
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
for (ResourceReferenceInfo nextRef : allRefs) {
@ -1175,9 +1268,35 @@ public abstract class BaseTransactionProcessor {
} else if (nextId.getValue().startsWith("urn:")) {
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
} else {
// get a map of
// existing ids -> PID (for resources that exist in the DB)
// should this be allPartitions?
Map<IIdType, ResourcePersistentId> idToPID = myIdHelperService.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
theReferencesToAutoVersion.stream()
.map(ref -> ref.getReferenceElement()).collect(Collectors.toList()));
for (IBaseReference baseRef : theReferencesToAutoVersion) {
IIdType id = baseRef.getReferenceElement();
if (!idToPID.containsKey(id)
&& myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
// not in the db, but autocreateplaceholders is true
// so the version we'll set is "1" (since it will be
// created later)
String newRef = id.withVersion("1").getValue();
id.setValue(newRef);
}
else {
// we will add the looked up info to the transaction
// for later
theTransactionDetails.addResolvedResourceId(id,
idToPID.get(id));
}
}
if (theReferencesToAutoVersion.contains(resourceReference)) {
DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId);
if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
if (outcome != null && !outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) {
addRollbackReferenceRestore(theTransactionDetails, resourceReference);
resourceReference.setReference(nextId.getValue());
resourceReference.setResource(null);

View File

@ -100,6 +100,16 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
@Query("SELECT t.myVersion FROM ResourceTable t WHERE t.myId = :pid")
Long findCurrentVersionByPid(@Param("pid") Long thePid);
/**
* This query will return rows with the following values:
* Id (resource pid - long), ResourceType (Patient, etc), version (long)
* Order matters!
* @param pid - list of pids to get versions for
* @return
*/
@Query("SELECT t.myId, t.myResourceType, t.myVersion FROM ResourceTable t WHERE t.myId IN ( :pid )")
Collection<Object[]> getResourceVersionsForPid(@Param("pid") List<Long> pid);
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId IS NULL AND t.myId = :pid")
Optional<ResourceTable> readByPartitionIdNull(@Param("pid") Long theResourceId);

View File

@ -94,7 +94,6 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
}
resolvedResource = createdTableOpt.get();
}

View File

@ -530,6 +530,95 @@ public class IdHelperService {
return retVal;
}
/**
* Helper method to determine if some resources exist in the DB (without throwing).
* Returns a set that contains the IIdType for every resource found.
* If it's not found, it won't be included in the set.
* @param theIds - list of IIdType ids (for the same resource)
* @return
*/
private Map<IIdType, ResourcePersistentId> getIdsOfExistingResources(RequestPartitionId thePartitionId,
Collection<IIdType> theIds) {
// these are the found Ids that were in the db
HashMap<IIdType, ResourcePersistentId> collected = new HashMap<>();
if (theIds == null || theIds.isEmpty()) {
return collected;
}
List<ResourcePersistentId> resourcePersistentIds = resolveResourcePersistentIdsWithCache(thePartitionId,
theIds.stream().collect(Collectors.toList()));
// we'll use this map to fetch pids that require versions
HashMap<Long, ResourcePersistentId> pidsToVersionToResourcePid = new HashMap<>();
// fill in our map
for (ResourcePersistentId pid : resourcePersistentIds) {
if (pid.getVersion() == null) {
pidsToVersionToResourcePid.put(pid.getIdAsLong(), pid);
}
Optional<IIdType> idOp = theIds.stream()
.filter(i -> i.getIdPart().equals(pid.getAssociatedResourceId().getIdPart()))
.findFirst();
// this should always be present
// since it was passed in.
// but land of optionals...
idOp.ifPresent(id -> {
collected.put(id, pid);
});
}
// set any versions we don't already have
if (!pidsToVersionToResourcePid.isEmpty()) {
Collection<Object[]> resourceEntries = myResourceTableDao
.getResourceVersionsForPid(new ArrayList<>(pidsToVersionToResourcePid.keySet()));
for (Object[] record : resourceEntries) {
// order matters!
Long retPid = (Long)record[0];
String resType = (String)record[1];
Long version = (Long)record[2];
pidsToVersionToResourcePid.get(retPid).setVersion(version);
}
}
return collected;
}
/**
* Retrieves the latest versions for any resourceid that are found.
* If they are not found, they will not be contained in the returned map.
* The key should be the same value that was passed in to allow
* consumer to look up the value using the id they already have.
*
* This method should not throw, so it can safely be consumed in
* transactions.
*
* @param theRequestPartitionId - request partition id
* @param theIds - list of IIdTypes for resources of interest.
* @return
*/
public Map<IIdType, ResourcePersistentId> getLatestVersionIdsForResourceIds(RequestPartitionId theRequestPartitionId, Collection<IIdType> theIds) {
HashMap<IIdType, ResourcePersistentId> idToPID = new HashMap<>();
HashMap<String, List<IIdType>> resourceTypeToIds = new HashMap<>();
for (IIdType id : theIds) {
String resourceType = id.getResourceType();
if (!resourceTypeToIds.containsKey(resourceType)) {
resourceTypeToIds.put(resourceType, new ArrayList<>());
}
resourceTypeToIds.get(resourceType).add(id);
}
for (String resourceType : resourceTypeToIds.keySet()) {
Map<IIdType, ResourcePersistentId> idAndPID = getIdsOfExistingResources(theRequestPartitionId,
resourceTypeToIds.get(resourceType));
idToPID.putAll(idAndPID);
}
return idToPID;
}
/**
* @deprecated This method doesn't take a partition ID as input, so it is unsafe. It
* should be reworked to include the partition ID before any new use is incorporated

View File

@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class BaseHapiFhirResourceDaoTest {
TestResourceDao mySvc = new TestResourceDao();
@Test

View File

@ -1,13 +1,207 @@
package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
@ExtendWith(MockitoExtension.class)
public class IdHelperServiceTest {
// helper class to package up data for helper methods
private class ResourceIdPackage {
public IIdType MyResourceId;
public ResourcePersistentId MyPid;
public Long MyVersion;
public ResourceIdPackage(IIdType id,
ResourcePersistentId pid,
Long version) {
MyResourceId = id;
MyPid = pid;
MyVersion = version;
}
}
@Mock
private IResourceTableDao myResourceTableDao;
@Mock
private DaoConfig myDaoConfig;
@Mock
private MemoryCacheService myMemoryCacheService;
@Mock
private EntityManager myEntityManager;
@InjectMocks
private IdHelperService myIdHelperService;
/**
* Gets a ResourceTable record for getResourceVersionsForPid
* Order matters!
* @param resourceType
* @param pid
* @param version
* @return
*/
private Object[] getResourceTableRecordForResourceTypeAndPid(String resourceType, long pid, long version) {
return new Object[] {
pid, // long
resourceType, // string
version // long
};
}
/**
* Helper function to mock out resolveResourcePersistentIdsWithCache
* to return empty lists (as if no resources were found).
*/
private void mock_resolveResourcePersistentIdsWithCache_toReturnNothing() {
CriteriaBuilder cb = Mockito.mock(CriteriaBuilder.class);
CriteriaQuery<ForcedId> criteriaQuery = Mockito.mock(CriteriaQuery.class);
Root<ForcedId> from = Mockito.mock(Root.class);
Path path = Mockito.mock(Path.class);
Mockito.when(cb.createQuery(Mockito.any(Class.class)))
.thenReturn(criteriaQuery);
Mockito.when(criteriaQuery.from(Mockito.any(Class.class)))
.thenReturn(from);
Mockito.when(from.get(Mockito.anyString()))
.thenReturn(path);
TypedQuery<ForcedId> queryMock = Mockito.mock(TypedQuery.class);
Mockito.when(queryMock.getResultList()).thenReturn(new ArrayList<>()); // not found
Mockito.when(myEntityManager.getCriteriaBuilder())
.thenReturn(cb);
Mockito.when(myEntityManager.createQuery(Mockito.any(CriteriaQuery.class)))
.thenReturn(queryMock);
}
/**
* Helper function to mock out getIdsOfExistingResources
* to return the matches and resources matching those provided
* by parameters.
* @param theResourcePacks
*/
private void mockReturnsFor_getIdsOfExistingResources(ResourceIdPackage... theResourcePacks) {
List<ResourcePersistentId> resourcePersistentIds = new ArrayList<>();
List<Object[]> matches = new ArrayList<>();
for (ResourceIdPackage pack : theResourcePacks) {
resourcePersistentIds.add(pack.MyPid);
matches.add(getResourceTableRecordForResourceTypeAndPid(
pack.MyResourceId.getResourceType(),
pack.MyPid.getIdAsLong(),
pack.MyVersion
));
}
ResourcePersistentId first = resourcePersistentIds.remove(0);
if (resourcePersistentIds.isEmpty()) {
Mockito.when(myMemoryCacheService.getIfPresent(Mockito.any(MemoryCacheService.CacheEnum.class), Mockito.anyString()))
.thenReturn(first).thenReturn(null);
}
else {
Mockito.when(myMemoryCacheService.getIfPresent(Mockito.any(MemoryCacheService.CacheEnum.class), Mockito.anyString()))
.thenReturn(first, resourcePersistentIds.toArray());
}
Mockito.when(myResourceTableDao.getResourceVersionsForPid(Mockito.anyList()))
.thenReturn(matches);
}
@Test
public void getLatestVersionIdsForResourceIds_whenResourceExists_returnsMapWithPIDAndVersion() {
IIdType type = new IdDt("Patient/RED");
ResourcePersistentId pid = new ResourcePersistentId(1L);
pid.setAssociatedResourceId(type);
HashMap<IIdType, ResourcePersistentId> map = new HashMap<>();
map.put(type, pid);
ResourceIdPackage pack = new ResourceIdPackage(type, pid, 2L);
// when
mockReturnsFor_getIdsOfExistingResources(pack);
// test
Map<IIdType, ResourcePersistentId> retMap = myIdHelperService.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
Collections.singletonList(type));
Assertions.assertTrue(retMap.containsKey(type));
Assertions.assertEquals(pid.getVersion(), map.get(type).getVersion());
}
@Test
public void getLatestVersionIdsForResourceIds_whenResourceDoesNotExist_returnsEmptyMap() {
IIdType type = new IdDt("Patient/RED");
// when
mock_resolveResourcePersistentIdsWithCache_toReturnNothing();
// test
Map<IIdType, ResourcePersistentId> retMap = myIdHelperService.getLatestVersionIdsForResourceIds(RequestPartitionId.allPartitions(),
Collections.singletonList(type));
Assertions.assertTrue(retMap.isEmpty());
}
@Test
public void getLatestVersionIdsForResourceIds_whenSomeResourcesDoNotExist_returnsOnlyExistingElements() {
// resource to be found
IIdType type = new IdDt("Patient/RED");
ResourcePersistentId pid = new ResourcePersistentId(1L);
pid.setAssociatedResourceId(type);
ResourceIdPackage pack = new ResourceIdPackage(type, pid, 2L);
// resource that won't be found
IIdType type2 = new IdDt("Patient/BLUE");
// when
mock_resolveResourcePersistentIdsWithCache_toReturnNothing();
mockReturnsFor_getIdsOfExistingResources(pack);
// test
Map<IIdType, ResourcePersistentId> retMap = myIdHelperService.getLatestVersionIdsForResourceIds(
RequestPartitionId.allPartitions(),
Arrays.asList(type, type2)
);
// verify
Assertions.assertEquals(1, retMap.size());
Assertions.assertTrue(retMap.containsKey(type));
Assertions.assertFalse(retMap.containsKey(type2));
}
@Test
public void testReplaceDefault_AllPartitions() {

View File

@ -1,17 +1,22 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BundleBuilder;
import ca.uhn.fhir.util.HapiExtensions;
import com.google.common.collect.Sets;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
@ -21,6 +26,7 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Task;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
@ -48,12 +54,11 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new DaoConfig().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets());
myDaoConfig.setBundleTypesAllowedForStorage(new DaoConfig().getBundleTypesAllowedForStorage());
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
}
@Test
public void testCreateWithBadReferenceFails() {
Observation o = new Observation();
o.setStatus(ObservationStatus.FINAL);
o.getSubject().setReference("Patient/FOO");
@ -97,27 +102,23 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
params.add(Task.SP_PART_OF, new ReferenceParam("Task/AAA"));
List<String> found = toUnqualifiedVersionlessIdValues(myTaskDao.search(params));
assertThat(found, contains(id.getValue()));
}
@Test
public void testUpdateWithBadReferenceFails() {
Observation o1 = new Observation();
o1.setStatus(ObservationStatus.FINAL);
IIdType id = myObservationDao.create(o1, mySrd).getId();
Observation o = new Observation();
o.setStatus(ObservationStatus.FINAL);
IIdType id = myObservationDao.create(o, mySrd).getId();
o = new Observation();
o.setId(id);
o.setStatus(ObservationStatus.FINAL);
o.getSubject().setReference("Patient/FOO");
try {
Exception ex = Assertions.assertThrows(InvalidRequestException.class, () -> {
myObservationDao.update(o, mySrd);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), startsWith("Resource Patient/FOO not found, specified in path: Observation.subject"));
}
});
assertThat(ex.getMessage(), startsWith("Resource Patient/FOO not found, specified in path: Observation.subject"));
}
@Test
@ -450,8 +451,6 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
ourLog.info("\nObservation read after Patient update:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
assertEquals(createdObs.getSubject().getReference(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
assertEquals(placeholderPatId.toUnqualifiedVersionless().getValueAsString(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
}
@Test
@ -540,7 +539,6 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
AuditEvent createdEvent = myAuditEventDao.read(id);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdEvent));
}
@Test
@ -560,11 +558,96 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
IIdType id = myObservationDao.create(obsToCreate, mySrd).getId();
Observation createdObs = myObservationDao.read(id);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
assertEquals("Patient/ABC", createdObs.getSubject().getReference());
}
@Test
public void testAutocreatePlaceholderTest() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
Observation obs = new Observation();
obs.setId("Observation/DEF");
Reference patientRef = new Reference("Patient/RED");
obs.setSubject(patientRef);
BundleBuilder builder = new BundleBuilder(myFhirCtx);
builder.addTransactionUpdateEntry(obs);
mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
// verify subresource is created
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
assertNotNull(returned);
}
@Test
public void testAutocreatePlaceholderWithTargetExistingAlreadyTest() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
String patientId = "Patient/RED";
// create
Patient patient = new Patient();
patient.setIdElement(new IdType(patientId));
myPatientDao.update(patient); // use update to use forcedid
// update
patient.setActive(true);
myPatientDao.update(patient);
// observation (with version 2)
Observation obs = new Observation();
obs.setId("Observation/DEF");
Reference patientRef = new Reference(patientId);
obs.setSubject(patientRef);
BundleBuilder builder = new BundleBuilder(myFhirCtx);
builder.addTransactionUpdateEntry(obs);
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
assertNotNull(returned);
Assertions.assertTrue(returned.getActive());
Assertions.assertEquals(2, returned.getIdElement().getVersionIdPartAsLong());
Observation retObservation = myObservationDao.read(obs.getIdElement());
assertNotNull(retObservation);
}
/**
* This test is the same as above, except it uses the serverid (instead of forcedid)
*/
@Test
public void testAutocreatePlaceholderWithExistingTargetWithServerAssignedIdTest() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
// create
Patient patient = new Patient();
patient.setIdElement(new IdType("Patient"));
DaoMethodOutcome ret = myPatientDao.create(patient); // use create to use server id
// update - to update our version
patient.setActive(true);
myPatientDao.update(patient);
// observation (with version 2)
Observation obs = new Observation();
obs.setId("Observation/DEF");
Reference patientRef = new Reference("Patient/" + ret.getId().getIdPart());
obs.setSubject(patientRef);
BundleBuilder builder = new BundleBuilder(myFhirCtx);
builder.addTransactionUpdateEntry(obs);
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), (Bundle) builder.getBundle());
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
assertNotNull(returned);
Assertions.assertEquals(2, returned.getIdElement().getVersionIdPartAsLong());
Observation retObservation = myObservationDao.read(obs.getIdElement());
assertNotNull(retObservation);
}
}

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.BundleBuilder;
@ -20,8 +22,11 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Task;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
@ -30,9 +35,12 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
@ -90,7 +98,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
assertEquals("Patient/A/_history/1", eob2.getPatient().getReference());
}
@Test
public void testCreateAndUpdateVersionedReferencesInTransaction_VersionedReferenceToVersionedReferenceToUpsertWithNop() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
@ -283,7 +290,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
myObservationDao.update(observation);
// Make sure we're not introducing any extra DB operations
assertEquals(5, myCaptureQueriesListener.logSelectQueries().size());
assertEquals(6, myCaptureQueriesListener.logSelectQueries().size());
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
@ -295,7 +302,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
BundleBuilder builder = new BundleBuilder(myFhirCtx);
Patient patient = new Patient();
@ -326,7 +332,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
}
@Test
@ -389,7 +394,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
assertEquals(patientId.getValue(), observation.getSubject().getReference());
assertEquals("2", observation.getSubject().getReferenceElement().getVersionIdPart());
assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference());
}
@ -409,7 +413,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
// Update patient to make a second version
patient.setActive(false);
myPatientDao.update(patient);
}
BundleBuilder builder = new BundleBuilder(myFhirCtx);
@ -458,7 +461,6 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
// Update patient to make a second version
patient.setActive(false);
myPatientDao.update(patient);
}
BundleBuilder builder = new BundleBuilder(myFhirCtx);
@ -491,10 +493,8 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
assertEquals(patientId.getValue(), observation.getSubject().getReference());
}
@Test
public void testSearchAndIncludeVersionedReference_Asynchronous() {
myFhirCtx.getParserOptions().setStripVersionsFromReferences(false);
@ -780,4 +780,120 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue());
}
}
@Test
public void testNoNpeOnEoBBundle() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
List<String> strings = Arrays.asList(
"ExplanationOfBenefit.patient",
"ExplanationOfBenefit.insurer",
"ExplanationOfBenefit.provider",
"ExplanationOfBenefit.careTeam.provider",
"ExplanationOfBenefit.insurance.coverage",
"ExplanationOfBenefit.payee.party"
);
myModelConfig.setAutoVersionReferenceAtPaths(new HashSet<>(strings));
Bundle bundle = myFhirCtx.newJsonParser().parseResource(Bundle.class,
new InputStreamReader(
FhirResourceDaoR4VersionedReferenceTest.class.getResourceAsStream("/npe-causing-bundle.json")));
Bundle transaction = mySystemDao.transaction(new SystemRequestDetails(), bundle);
assertNotNull(transaction);
}
@Test
public void testAutoVersionPathsWithAutoCreatePlaceholders() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
Observation obs = new Observation();
obs.setId("Observation/CDE");
obs.setSubject(new Reference("Patient/ABC"));
DaoMethodOutcome update = myObservationDao.create(obs);
Observation resource = (Observation)update.getResource();
String versionedPatientReference = resource.getSubject().getReference();
assertThat(versionedPatientReference, is(equalTo("Patient/ABC")));
Patient p = myPatientDao.read(new IdDt("Patient/ABC"));
Assertions.assertNotNull(p);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
obs = new Observation();
obs.setId("Observation/DEF");
obs.setSubject(new Reference("Patient/RED"));
update = myObservationDao.create(obs);
resource = (Observation)update.getResource();
versionedPatientReference = resource.getSubject().getReference();
assertThat(versionedPatientReference, is(equalTo("Patient/RED/_history/1")));
}
@Test
@DisplayName("Bundle transaction with AutoVersionReferenceAtPath on and with existing Patient resource should create")
public void bundleTransaction_autoreferenceAtPathWithPreexistingPatientReference_shouldCreate() {
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
String patientId = "Patient/RED";
IIdType idType = new IdDt(patientId);
// create patient ahead of time
Patient patient = new Patient();
patient.setId(patientId);
DaoMethodOutcome outcome = myPatientDao.update(patient);
assertThat(outcome.getResource().getIdElement().getValue(), is(equalTo(patientId + "/_history/1")));
Patient returned = myPatientDao.read(idType);
Assertions.assertNotNull(returned);
assertThat(returned.getId(), is(equalTo(patientId + "/_history/1")));
// update to change version
patient.setActive(true);
myPatientDao.update(patient);
Observation obs = new Observation();
obs.setId("Observation/DEF");
Reference patientRef = new Reference(patientId);
obs.setSubject(patientRef);
BundleBuilder builder = new BundleBuilder(myFhirCtx);
builder.addTransactionUpdateEntry(obs);
Bundle submitted = (Bundle)builder.getBundle();
Bundle returnedTr = mySystemDao.transaction(new SystemRequestDetails(), submitted);
Assertions.assertNotNull(returnedTr);
// some verification
Observation obRet = myObservationDao.read(obs.getIdElement());
Assertions.assertNotNull(obRet);
}
@Test
@DisplayName("GH-2901 Test no NPE is thrown on autoversioned references")
public void testNoNpeMinimal() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject");
Observation obs = new Observation();
obs.setId("Observation/DEF");
Reference patientRef = new Reference("Patient/RED");
obs.setSubject(patientRef);
BundleBuilder builder = new BundleBuilder(myFhirCtx);
builder.addTransactionUpdateEntry(obs);
Bundle submitted = (Bundle)builder.getBundle();
Bundle returnedTr = mySystemDao.transaction(new SystemRequestDetails(), submitted);
Assertions.assertNotNull(returnedTr);
// some verification
Observation obRet = myObservationDao.read(obs.getIdElement());
Assertions.assertNotNull(obRet);
Patient returned = myPatientDao.read(patientRef.getReferenceElement());
Assertions.assertNotNull(returned);
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
@ -119,6 +119,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
myDaoConfig.setBundleBatchPoolSize(new DaoConfig().getBundleBatchPoolSize());
myDaoConfig.setBundleBatchMaxPoolSize(new DaoConfig().getBundleBatchMaxPoolSize());
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets());
myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths());
}
@BeforeEach

View File

@ -0,0 +1,402 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [ {
"resource": {
"resourceType": "ExplanationOfBenefit",
"id": "26d4cebd-95c6-39ea-855c-dc819bc68d08",
"meta": {
"lastUpdated": "2016-01-01T00:56:00.000-05:00",
"profile": [ "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Inpatient-Institutional" ]
},
"identifier": [ {
"type": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
"code": "uc"
} ]
},
"system": "fhir/CodeSystem/sid/eob-inpatient-claim-id",
"value": "20550047"
} ],
"status": "active",
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/claim-type",
"code": "institutional"
} ]
},
"use": "claim",
"patient": {
"reference": "Patient/ABC"
},
"billablePeriod": {
"start": "2015-12-14T00:00:00-05:00"
},
"created": "2021-08-16T13:54:10-04:00",
"insurer": {
"reference": "Organization/A"
},
"provider": {
"reference": "Organization/b9d22776-1ee9-3843-bc48-b4bf67861483"
},
"outcome": "complete",
"careTeam": [ {
"sequence": 1,
"provider": {
"reference": "Practitioner/H"
},
"role": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/claimcareteamrole",
"code": "primary"
} ]
}
}, {
"sequence": 2,
"provider": {
"reference": "Practitioner/I"
},
"role": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
"code": "attending"
} ]
}
}, {
"sequence": 3,
"provider": {
"reference": "Practitioner/J"
},
"role": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
"code": "performing"
} ]
}
} ],
"supportingInfo": [ {
"sequence": 1,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "admissionperiod"
} ]
},
"timingPeriod": {
"start": "2015-12-14T00:00:00-05:00",
"end": "2016-01-11T14:11:00-05:00"
}
}, {
"sequence": 2,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "clmrecvddate"
} ]
},
"timingDate": "2018-12-23"
}, {
"sequence": 3,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "admtype"
} ]
},
"code": {
"coding": [ {
"system": "https://www.nubc.org/CodeSystem/PriorityTypeOfAdmitOrVisit",
"code": "4"
} ]
}
} ],
"diagnosis": [ {
"sequence": 1,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "Z38.00"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
} ],
"insurance": [ {
"focal": true,
"coverage": {
"reference": "Coverage/G"
}
} ],
"total": [ {
"category": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
"code": "benefit"
} ]
},
"amount": {
"value": 1039.28
}
}, {
"category": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
"code": "submitted"
} ]
},
"amount": {
"value": 2011
}
} ]
},
"request": {
"method": "PUT",
"url": "ExplanationOfBenefit/26d4cebd-95c6-39ea-855c-dc819bc68d08"
}
}, {
"resource": {
"resourceType": "ExplanationOfBenefit",
"id": "a25f1c3a-09b9-3f17-8f1b-0fbbf6391fce",
"meta": {
"lastUpdated": "2016-01-01T00:58:00.000-05:00",
"profile": [ "http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-ExplanationOfBenefit-Inpatient-Institutional" ]
},
"identifier": [ {
"type": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
"code": "uc"
} ]
},
"system": "fhir/CodeSystem/sid/eob-inpatient-claim-id",
"value": "20586901"
} ],
"status": "active",
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/claim-type",
"code": "institutional"
} ]
},
"use": "claim",
"patient": {
"reference": "Patient/ABC"
},
"billablePeriod": {
"start": "2015-12-18T00:00:00-05:00"
},
"created": "2021-08-16T13:54:10-04:00",
"insurer": {
"reference": "Organization/A"
},
"provider": {
"reference": "Organization/d10823cf-ee15-3a0e-a12e-1509cd18cda4"
},
"outcome": "complete",
"careTeam": [ {
"sequence": 1,
"provider": {
"reference": "Practitioner/D"
},
"role": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/claimcareteamrole",
"code": "primary"
} ]
}
}, {
"sequence": 2,
"provider": {
"reference": "Practitioner/E"
},
"role": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
"code": "attending"
} ]
}
}, {
"sequence": 3,
"provider": {
"reference": "Practitioner/F"
},
"role": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBClaimCareTeamRole",
"code": "performing"
} ]
}
} ],
"supportingInfo": [ {
"sequence": 1,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "admissionperiod"
} ]
},
"timingPeriod": {
"start": "2015-12-18T00:00:00-05:00",
"end": "2016-01-11T14:12:00-05:00"
}
}, {
"sequence": 2,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "clmrecvddate"
} ]
},
"timingDate": "2018-12-29"
}, {
"sequence": 3,
"category": {
"coding": [ {
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBSupportingInfoType",
"code": "admtype"
} ]
},
"code": {
"coding": [ {
"system": "https://www.nubc.org/CodeSystem/PriorityTypeOfAdmitOrVisit",
"code": "4"
} ]
}
} ],
"diagnosis": [ {
"sequence": 1,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "Z38.00"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 2,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "P96.89"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 3,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "R25.8"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 4,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "P08.21"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 5,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "P92.5"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 6,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "P92.6"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
}, {
"sequence": 7,
"diagnosisCodeableConcept": {
"coding": [ {
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "Z23"
} ]
},
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
"code": "principal"
} ]
} ]
} ],
"insurance": [ {
"focal": true,
"coverage": {
"reference": "Coverage/G"
}
} ],
"total": [ {
"category": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
"code": "benefit"
} ]
},
"amount": {
"value": 1421.31
}
}, {
"category": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/adjudication",
"code": "submitted"
} ]
},
"amount": {
"value": 2336
}
} ]
},
"request": {
"method": "PUT",
"url": "ExplanationOfBenefit/a25f1c3a-09b9-3f17-8f1b-0fbbf6391fce"
}
} ] }

View File

@ -1,19 +1,3 @@
package ca.uhn.fhir.model.dstu2.composite;
/*