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. + + RESOURCE_TYPE + + String + + Contains the string specifying the type of the resource (Patient, Observation, etc). + + @@ -476,7 +484,7 @@ The following columns are common to **all HFJ_SPIDX_xxx tables**. 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; /*