Merge pull request #2943 from hapifhir/issue-2901-npe-in-bundle-transaction
Issue 2901 npe in bundle transaction
This commit is contained in:
commit
e4138d9341
|
@ -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;
|
||||
|
|
|
@ -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"
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -94,7 +94,6 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
|
||||
|
||||
}
|
||||
|
||||
resolvedResource = createdTableOpt.get();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
} ] }
|
|
@ -1,19 +1,3 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
package ca.uhn.fhir.model.dstu2.composite;
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue