diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java
index 8cc1c9be1de..b0dca947658 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java
@@ -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;
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2901-autoversionatpaths-with-autocreateplaceholders-no-null-pointer.yml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2901-autoversionatpaths-with-autocreateplaceholders-no-null-pointer.yml
new file mode 100644
index 00000000000..5b5159f2372
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2901-autoversionatpaths-with-autocreateplaceholders-no-null-pointer.yml
@@ -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"
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md
index af83007725e..17499751bb8 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md
@@ -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.
+
RES_ID
FK to HFJ_RESOURCE
- String
+ Long
Contains the PID of the resource being indexed.
diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java
index 049e460294b..ff3a34fc4eb 100644
--- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java
+++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java
@@ -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;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
index 2b244a6e48b..4abd1899274 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
@@ -1207,7 +1207,6 @@ public abstract class BaseHapiFhirDao 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);
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
index 39ef8a38ca4..2afd65245eb 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
@@ -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;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java
index 32d37758dde..909cd1bf481 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java
@@ -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;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java
index f181a5760ed..58aeae432ca 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java
@@ -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 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);
}
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
index 08e6f003a7c..d554b645514 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
@@ -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 idSubstitutions, Map idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
- IBase newEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) {
+ private void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map 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 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 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 getEntries = new ArrayList<>();
final IdentityHashMap 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 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 theGetEntries, IdentityHashMap 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 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 theOriginalRequestOrder, List theEntries) {
+ private void doTransactionWriteOperations(RequestDetails theRequestDetails, String theActionName,
+ TransactionDetails theTransactionDetails, StopWatch theTransactionStopWatch,
+ IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder,
+ List 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> txCallback = status -> {
final Set allIds = new LinkedHashSet<>();
final Map idSubstitutions = new HashMap<>();
final Map idToPersistedOutcome = new HashMap<>();
- Map retVal = doTransactionWriteOperations(theRequestDetails, theActionName, theTransactionDetails, allIds, idSubstitutions, idToPersistedOutcome, theResponse, theOriginalRequestOrder, theEntries, theTransactionStopWatch);
+
+ Map 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 doTransactionWriteOperations(final RequestDetails theRequest, String theActionName, TransactionDetails theTransactionDetails, Set theAllIds,
- Map theIdSubstitutions, Map theIdToPersistedOutcome, IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) {
+ /**
+ * Searches for duplicate conditional creates and consolidates them.
+ *
+ * @param theEntries
+ */
+ private void consolidateDuplicateConditionals(List theEntries) {
+ final HashMap 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 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 doTransactionWriteOperations(final RequestDetails theRequest, String theActionName,
+ TransactionDetails theTransactionDetails, Set theAllIds,
+ Map theIdSubstitutions, Map theIdToPersistedOutcome,
+ IBaseBundle theResponse, IdentityHashMap theOriginalRequestOrder,
+ List 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 deletedResources = new HashSet<>();
DeleteConflictList deleteConflicts = new DeleteConflictList();
Map entriesToProcess = new IdentityHashMap<>();
@@ -685,117 +833,20 @@ public abstract class BaseTransactionProcessor {
/*
* Look for duplicate conditional creates and consolidate them
*/
- final HashMap 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 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 updatedSource = updatedResources
- .stream()
- .filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
- .findFirst();
- if (updatedSource.isPresent()) {
- List 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 theDeletedResources,
+ List theUpdatedResources) {
+ for (Iterator 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 updatedSource = theUpdatedResources
+ .stream()
+ .filter(t -> sourceId.equals(t.getIdElement().toUnqualifiedVersionless().getValue()))
+ .findFirst();
+ if (updatedSource.isPresent()) {
+ List 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 theIdSubstitutions, Map theIdToPersistedOutcome, StopWatch theTransactionStopWatch, Map entriesToProcess, Set nonUpdatedEntities, Set updatedEntities) {
+ private void resolveReferencesThenSaveAndIndexResources(RequestDetails theRequest, TransactionDetails theTransactionDetails,
+ Map theIdSubstitutions, Map theIdToPersistedOutcome,
+ StopWatch theTransactionStopWatch, Map entriesToProcess,
+ Set nonUpdatedEntities, Set updatedEntities) {
FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
IdentityHashMap> deferredIndexesForAutoVersioning = null;
@@ -1114,8 +1188,15 @@ public abstract class BaseTransactionProcessor {
Set 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 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 theIdSubstitutions, Map theIdToPersistedOutcome, Map entriesToProcess, Set nonUpdatedEntities, Set updatedEntities, FhirTerser terser, DaoMethodOutcome nextOutcome, IBaseResource nextResource, Set theReferencesToAutoVersion) {
+ private void resolveReferencesThenSaveAndIndexResource(RequestDetails theRequest, TransactionDetails theTransactionDetails,
+ Map theIdSubstitutions, Map theIdToPersistedOutcome,
+ Map entriesToProcess, Set nonUpdatedEntities,
+ Set updatedEntities, FhirTerser terser,
+ DaoMethodOutcome nextOutcome, IBaseResource nextResource,
+ Set theReferencesToAutoVersion) {
// References
List 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 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);
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java
index 25a3e3d9e1d..6ee6a307187 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java
@@ -100,6 +100,16 @@ public interface IResourceTableDao extends JpaRepository {
@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 getResourceVersionsForPid(@Param("pid") List pid);
+
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId IS NULL AND t.myId = :pid")
Optional readByPartitionIdNull(@Param("pid") Long theResourceId);
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java
index a219f39f292..07daf83c2da 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java
@@ -94,7 +94,6 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
}
-
resolvedResource = createdTableOpt.get();
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
index b667e3a1375..03763d40a0f 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
@@ -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 getIdsOfExistingResources(RequestPartitionId thePartitionId,
+ Collection theIds) {
+ // these are the found Ids that were in the db
+ HashMap collected = new HashMap<>();
+
+ if (theIds == null || theIds.isEmpty()) {
+ return collected;
+ }
+
+ List resourcePersistentIds = resolveResourcePersistentIdsWithCache(thePartitionId,
+ theIds.stream().collect(Collectors.toList()));
+
+ // we'll use this map to fetch pids that require versions
+ HashMap pidsToVersionToResourcePid = new HashMap<>();
+
+ // fill in our map
+ for (ResourcePersistentId pid : resourcePersistentIds) {
+ if (pid.getVersion() == null) {
+ pidsToVersionToResourcePid.put(pid.getIdAsLong(), pid);
+ }
+ Optional 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 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 getLatestVersionIdsForResourceIds(RequestPartitionId theRequestPartitionId, Collection theIds) {
+ HashMap idToPID = new HashMap<>();
+ HashMap> 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 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
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java
index bc984843ff4..67badec3ad5 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java
@@ -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
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java
index 20e100065f1..2ca8fea957c 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/index/IdHelperServiceTest.java
@@ -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 criteriaQuery = Mockito.mock(CriteriaQuery.class);
+ Root 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 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 resourcePersistentIds = new ArrayList<>();
+ List 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 map = new HashMap<>();
+ map.put(type, pid);
+ ResourceIdPackage pack = new ResourceIdPackage(type, pid, 2L);
+
+ // when
+ mockReturnsFor_getIdsOfExistingResources(pack);
+
+ // test
+ Map 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 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 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() {
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java
index c00baf2f536..1588c81ba80 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java
@@ -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 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);
+ }
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java
index 67e43f02edf..0c85f54f0a0 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java
@@ -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 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);
+ }
}
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java
index d5428cc9fd1..b9257a30e0b 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java
@@ -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
diff --git a/hapi-fhir-jpaserver-base/src/test/resources/npe-causing-bundle.json b/hapi-fhir-jpaserver-base/src/test/resources/npe-causing-bundle.json
new file mode 100644
index 00000000000..a645b167d35
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/test/resources/npe-causing-bundle.json
@@ -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"
+ }
+ } ] }
diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/composite/NarrativeDt.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/composite/NarrativeDt.java
index ef74811416d..085008a62b1 100644
--- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/composite/NarrativeDt.java
+++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/composite/NarrativeDt.java
@@ -1,19 +1,3 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
package ca.uhn.fhir.model.dstu2.composite;
/*