From 909d9f742c0030859a4e995d905c006132cbbb76 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 27 Jan 2021 14:49:10 -0500 Subject: [PATCH] Enable Auto-reference versions in JPA (#2323) * Automatic reference versions * Auto reference versions * resolve fixmes * Add changelog * Test fixes * Test fixes * Resolve fixmes * Work on versioning * Add support for _includes * Update headers * Change visibility * One more tweak * One more API tweak * Fixes * Account for review comments * Fix errorprone reported issue * Test fix * Test fixes * Test fixes --- .../uhn/fhir/interceptor/api/IPointcut.java | 20 + .../java/ca/uhn/fhir/util/BundleBuilder.java | 3 +- .../ca/uhn/fhir/i18n/hapi-messages.properties | 2 + .../hapi/fhir/docs/BundleBuilderExamples.java | 4 +- .../5_3_0/2323-auto-reference-versions.yaml | 8 + .../fhir/jpa/api/dao/IFhirResourceDao.java | 10 +- .../fhir/jpa/api/model/DaoMethodOutcome.java | 3 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 13 +- .../ca/uhn/fhir/jpa/dao/BaseStorageDao.java | 131 ++++- .../jpa/dao/BaseTransactionProcessor.java | 28 +- .../jpa/dao/FhirResourceDaoBundleDstu2.java | 6 +- .../jpa/search/builder/SearchBuilder.java | 192 ++++--- .../jpa/dao/TransactionProcessorTest.java | 4 + .../dao/dstu2/FhirResourceDaoDstu2Test.java | 4 +- .../jpa/dao/dstu3/BaseJpaDstu3SystemTest.java | 2 - .../dao/dstu3/FhirResourceDaoDstu3Test.java | 4 +- .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 2 +- .../jpa/dao/r4/FhirResourceDaoR4Test.java | 4 +- ...irResourceDaoR4VersionedReferenceTest.java | 482 ++++++++++++++++++ .../provider/ResourceProviderDstu2Test.java | 2 +- .../dstu3/ResourceProviderDstu3Test.java | 2 +- .../provider/r4/ResourceProviderR4Test.java | 2 +- .../tasks/HapiFhirJpaMigrationTasks.java | 3 + .../ca/uhn/fhir/jpa/api/IDaoRegistry.java | 2 +- .../jpa/model/config/PartitionSettings.java | 2 +- .../model/cross/IBasePersistedResource.java | 2 +- .../fhir/jpa/model/cross/IResourceLookup.java | 2 +- .../fhir/jpa/model/cross/ResourceLookup.java | 2 +- .../jpa/model/entity/BaseHasResource.java | 2 +- .../jpa/model/entity/BasePartitionable.java | 2 +- .../jpa/model/entity/BaseResourceIndex.java | 2 +- .../BaseResourceIndexedSearchParam.java | 2 +- .../ca/uhn/fhir/jpa/model/entity/BaseTag.java | 2 +- .../jpa/model/entity/BinaryStorageEntity.java | 2 +- .../uhn/fhir/jpa/model/entity/ForcedId.java | 2 +- .../jpa/model/entity/IBaseResourceEntity.java | 2 +- .../fhir/jpa/model/entity/ModelConfig.java | 118 ++++- .../entity/NormalizedQuantitySearchLevel.java | 2 +- .../jpa/model/entity/NpmPackageEntity.java | 2 +- .../model/entity/NpmPackageVersionEntity.java | 2 +- .../NpmPackageVersionResourceEntity.java | 2 +- .../entity/PartitionablePartitionId.java | 2 +- .../model/entity/ResourceEncodingEnum.java | 2 +- .../ResourceHistoryProvenanceEntity.java | 2 +- .../model/entity/ResourceHistoryTable.java | 2 +- .../jpa/model/entity/ResourceHistoryTag.java | 2 +- .../ResourceIndexedCompositeStringUnique.java | 2 +- ...esourceIndexedSearchParamBaseQuantity.java | 2 +- .../ResourceIndexedSearchParamCoords.java | 2 +- .../ResourceIndexedSearchParamDate.java | 2 +- .../ResourceIndexedSearchParamNumber.java | 2 +- .../ResourceIndexedSearchParamQuantity.java | 2 +- ...eIndexedSearchParamQuantityNormalized.java | 2 +- .../ResourceIndexedSearchParamString.java | 2 +- .../ResourceIndexedSearchParamToken.java | 2 +- .../entity/ResourceIndexedSearchParamUri.java | 2 +- .../fhir/jpa/model/entity/ResourceLink.java | 29 +- .../fhir/jpa/model/entity/ResourceTable.java | 2 +- .../fhir/jpa/model/entity/ResourceTag.java | 2 +- .../jpa/model/entity/SearchParamPresent.java | 2 +- .../fhir/jpa/model/entity/TagDefinition.java | 2 +- .../fhir/jpa/model/entity/TagTypeEnum.java | 2 +- .../ca/uhn/fhir/jpa/model/sched/HapiJob.java | 2 +- .../fhir/jpa/model/sched/IHapiScheduler.java | 2 +- .../jpa/model/sched/ISchedulerService.java | 2 +- .../jpa/model/sched/ISmartLifecyclePhase.java | 2 +- .../model/sched/ScheduledJobDefinition.java | 2 +- .../search/ResourceTableRoutingBinder.java | 2 +- .../model/search/SearchRuntimeDetails.java | 2 +- .../jpa/model/search/SearchStatusEnum.java | 2 +- .../search/StorageProcessingMessage.java | 2 +- .../fhir/jpa/model/util/CodeSystemHash.java | 2 +- .../uhn/fhir/jpa/model/util/JpaConstants.java | 2 +- .../fhir/jpa/model/util/UcumServiceUtil.java | 2 +- .../jpa/util/JpaInterceptorBroadcaster.java | 2 +- .../jpa/searchparam/SearchParameterMap.java | 3 +- .../SearchParamExtractorService.java | 17 +- .../ResourceIndexedSearchParamsTest.java | 8 +- .../server/storage/ResourcePersistentId.java | 32 +- .../server/storage/TransactionDetails.java | 15 +- .../fhir/rest/client/GenericClientR4Test.java | 2 +- .../ca/uhn/fhir/util/BundleBuilderTest.java | 4 +- pom.xml | 3 +- 83 files changed, 1073 insertions(+), 189 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2323-auto-reference-versions.yaml create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java index d9abf1de3f9..fc3c9cf0bd7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.interceptor.api; +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import javax.annotation.Nonnull; import java.util.List; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index 3ce27fdc4e0..ca79ffb009b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -173,7 +173,7 @@ public class BundleBuilder { * * @param theResource The resource to create */ - public CreateBuilder addCreateEntry(IBaseResource theResource) { + public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) { setBundleField("type", "transaction"); IBase request = addEntryAndReturnRequest(theResource); @@ -338,7 +338,6 @@ public class BundleBuilder { setBundleField("type", theType); } - public static class UpdateBuilder { private final IPrimitiveType myUrl; diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 411847f5263..fdff9a69951 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -108,6 +108,8 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully delet ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}" for resource type "{1}". Valid search parameters for this search are: {2} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSortParameter=Unknown _sort parameter value "{0}" for resource type "{1}" (Note: sort parameters values must use a valid Search Parameter). Valid values for this search are: {2} +ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidBundleTypeForStorage=Unable to store a Bundle resource on this server with a Bundle.type value of: {0}. Note that if you are trying to perform a FHIR 'transaction' or 'batch' operation you should POST the Bundle resource to the Base URL of the server, not to the '/Bundle' endpoint. + ca.uhn.fhir.rest.api.PatchTypeEnum.missingPatchContentType=Missing or invalid content type for PATCH operation ca.uhn.fhir.rest.api.PatchTypeEnum.invalidPatchContentType=Invalid Content-Type for PATCH operation: {0} ca.uhn.fhir.jpa.dao.BaseTransactionProcessor.unsupportedResourceType=Resource {0} is not supported on this server. Supported resource types: {1} diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java index 757323130d6..6aa3a85bd26 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/BundleBuilderExamples.java @@ -84,7 +84,7 @@ public class BundleBuilderExamples { patient.setActive(true); // Add the patient as a create (aka POST) to the Bundle - builder.addCreateEntry(patient); + builder.addTransactionCreateEntry(patient); // Execute the transaction IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute(); @@ -102,7 +102,7 @@ public class BundleBuilderExamples { patient.addIdentifier().setSystem("http://foo").setValue("bar"); // Add the patient as a create (aka POST) to the Bundle - builder.addCreateEntry(patient).conditional("Patient?identifier=http://foo|bar"); + builder.addTransactionCreateEntry(patient).conditional("Patient?identifier=http://foo|bar"); // Execute the transaction IBaseBundle outcome = myFhirClient.transaction().withBundle(builder.getBundle()).execute(); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2323-auto-reference-versions.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2323-auto-reference-versions.yaml new file mode 100644 index 00000000000..7578b381767 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2323-auto-reference-versions.yaml @@ -0,0 +1,8 @@ +--- +type: add +issue: 2323 +title: "The JPA server has a new setting on the `ModelConfig` bean called \"AutoVersionReferencesAtPaths\". Using + this setting, the server can be configured to add the current target resource version ID to any resource + references found in a resource being stored. In addition, a new setting has been added to the JPA ModelConfig + bean that allows `_include` statements to respect versioned references, and actually include the correct + version for the reference." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java index 94912cea35c..3652f88f561 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java @@ -265,9 +265,11 @@ public interface IFhirResourceDao extends IDao { */ DeleteMethodOutcome deletePidList(String theUrl, Collection theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest); - // /** - // * Invoke the everything operation - // */ - // IBundleProvider everything(IIdType theId); + /** + * Returns the current version ID for the given resource + */ + default String getCurrentVersionId(IIdType theReferenceElement) { + return read(theReferenceElement.toVersionless()).getIdElement().getVersionIdPart(); + } } diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DaoMethodOutcome.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DaoMethodOutcome.java index 3950c06532c..ff0d47d577e 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DaoMethodOutcome.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DaoMethodOutcome.java @@ -47,8 +47,9 @@ public class DaoMethodOutcome extends MethodOutcome { /** * Was this a NO-OP - Typically because of an update to a resource that already matched the contents provided */ - public void setNop(boolean theNop) { + public DaoMethodOutcome setNop(boolean theNop) { myNop = theNop; + return this; } public IBasePersistedResource getEntity() { 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 7cbdd76c2a7..200fefac044 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 @@ -259,6 +259,7 @@ public abstract class BaseHapiFhirResourceDao extends B StopWatch w = new StopWatch(); preProcessResourceForStorage(theResource); + preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); ResourceTable entity = new ResourceTable(); entity.setResourceType(toResourceName(theResource)); @@ -274,7 +275,7 @@ public abstract class BaseHapiFhirResourceDao extends B entity = myEntityManager.find(ResourceTable.class, pid.getId()); IBaseResource resource = toResource(entity, false); theResource.setId(resource.getIdElement().getValue()); - return toMethodOutcome(theRequest, entity, resource).setCreated(false); + return toMethodOutcome(theRequest, entity, resource).setCreated(false).setNop(true); } } @@ -1129,6 +1130,12 @@ public abstract class BaseHapiFhirResourceDao extends B return readEntity(theId, true, theRequest); } + @Override + @Transactional + public String getCurrentVersionId(IIdType theReferenceElement) { + return Long.toString(readEntity(theReferenceElement.toVersionless(), null).getVersion()); + } + @Override @Transactional public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) { @@ -1459,6 +1466,7 @@ public abstract class BaseHapiFhirResourceDao extends B T resource = theResource; preProcessResourceForStorage(resource); + preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); final ResourceTable entity; @@ -1529,6 +1537,9 @@ public abstract class BaseHapiFhirResourceDao extends B resource.setId(entity.getIdDt().getValue()); DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, resource).setCreated(wasDeleted); outcome.setPreviousResource(oldResource); + if (!outcome.isNop()) { + outcome.setId(outcome.getId().withVersion(Long.toString(outcome.getId().getVersionIdPartAsLong() + 1))); + } return outcome; } 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 8208b935dee..9568f15822f 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 @@ -27,8 +27,11 @@ 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.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.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.registry.ISearchParamRegistry; @@ -53,11 +56,10 @@ import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.ResourceReferenceInfo; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +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.r4.model.InstantType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -67,6 +69,7 @@ import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -77,30 +80,85 @@ import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseStorageDao { - private static final Logger ourLog = LoggerFactory.getLogger(BaseStorageDao.class); @Autowired protected ISearchParamRegistry mySearchParamRegistry; + @Autowired + protected FhirContext myFhirContext; + @Autowired + protected DaoRegistry myDaoRegistry; + @Autowired + protected ModelConfig myModelConfig; /** * May be overridden by subclasses to validate resources prior to storage * * @param theResource The resource that is about to be stored + * @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead */ protected void preProcessResourceForStorage(IBaseResource theResource) { + // nothing + } + + /** + * May be overridden by subclasses to validate resources prior to storage + * + * @param theResource The resource that is about to be stored + * @since 5.3.0 + */ + protected void preProcessResourceForStorage(IBaseResource theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean thePerformIndexing) { + + verifyResourceTypeIsAppropriateForDao(theResource); + + verifyResourceIdIsValid(theResource); + + verifyBundleTypeIsAppropriateForStorage(theResource); + + replaceAbsoluteReferencesWithRelative(theResource); + + performAutoVersioning(theResource, thePerformIndexing); + + } + + /** + * Sanity check - Is this resource the right type for this DAO? + */ + private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) { String type = getContext().getResourceType(theResource); if (getResourceName() != null && !getResourceName().equals(type)) { throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName())); } + } + /** + * Verify that the resource ID is actually valid according to FHIR's rules + */ + private void verifyResourceIdIsValid(IBaseResource theResource) { if (theResource.getIdElement().hasIdPart()) { if (!theResource.getIdElement().isIdPartValid()) { throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart())); } } + } - /* - * Replace absolute references with relative ones if configured to do so - */ + /** + * Verify that we're not storing a Bundle with a disallowed bundle type + */ + private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) { + if (theResource instanceof IBaseBundle) { + Set allowedBundleTypes = getConfig().getBundleTypesAllowedForStorage(); + String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource); + bundleType = defaultString(bundleType); + if (!allowedBundleTypes.contains(bundleType)) { + String message = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidBundleTypeForStorage", (isNotBlank(bundleType) ? bundleType : "(missing)")); + throw new UnprocessableEntityException(message); + } + } + } + + /** + * Replace absolute references with relative ones if configured to do so + */ + private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource) { if (getConfig().getTreatBaseUrlsAsLocal().isEmpty() == false) { FhirTerser t = getContext().newTerser(); List refs = t.getAllResourceReferences(theResource); @@ -114,17 +172,38 @@ public abstract class BaseStorageDao { } } } + } - if ("Bundle".equals(type)) { - Set allowedBundleTypes = getConfig().getBundleTypesAllowedForStorage(); - String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource); - bundleType = defaultString(bundleType); - if (!allowedBundleTypes.contains(bundleType)) { - String message = "Unable to store a Bundle resource on this server with a Bundle.type value of: " + (isNotBlank(bundleType) ? bundleType : "(missing)"); - throw new UnprocessableEntityException(message); + /** + * Handle {@link ModelConfig#getAutoVersionReferenceAtPaths() auto-populate-versions} + * + * We only do this if thePerformIndexing is true because if it's false, that means + * we're in a FHIR transaction during the first phase of write operation processing, + * meaning that the versions of other resources may not have need updated yet. For example + * we're about to store an Observation with a reference to a Patient, and that Patient + * is also being updated in the same transaction, during the first "no index" phase, + * the Patient will not yet have its version number incremented, so it would be wrong + * to use that value. During the second phase it is correct. + * + * Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning + * and it is the one that takes care of the placeholder IDs. Look for the other caller of + * {@link #extractReferencesToAutoVersion(FhirContext, ModelConfig, IBaseResource)} + * to find this. + */ + private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) { + if (thePerformIndexing) { + Set referencesToVersion = extractReferencesToAutoVersion(myFhirContext, myModelConfig, theResource); + 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(); + nextReference.setReference(newTargetReference); + } } } - } protected DaoMethodOutcome toMethodOutcome(RequestDetails theRequest, @Nonnull final IBasePersistedResource theEntity, @Nonnull IBaseResource theResource) { @@ -267,4 +346,28 @@ public abstract class BaseStorageDao { } } + /** + * @see ModelConfig#getAutoVersionReferenceAtPaths() + */ + @Nonnull + public static Set extractReferencesToAutoVersion(FhirContext theFhirContext, ModelConfig theModelConfig, IBaseResource theResource) { + Map references = Collections.emptyMap(); + if (!theModelConfig.getAutoVersionReferenceAtPaths().isEmpty()) { + String resourceName = theFhirContext.getResourceType(theResource); + for (String nextPath : theModelConfig.getAutoVersionReferenceAtPathsByResourceType(resourceName)) { + List nextReferences = theFhirContext.newTerser().getValues(theResource, nextPath, IBaseReference.class); + for (IBaseReference next : nextReferences) { + if (next.getReferenceElement().hasVersionIdPart()) { + continue; + } + if (references.isEmpty()) { + references = new IdentityHashMap<>(); + } + references.put(next, null); + } + } + } + return references.keySet(); + } + } 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 6d78f7fce19..ce46f6786c5 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 @@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; 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.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; @@ -78,6 +79,7 @@ import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseParameters; +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; @@ -130,10 +132,12 @@ public abstract class BaseTransactionProcessor { private HapiTransactionService myHapiTransactionService; @Autowired private DaoConfig myDaoConfig; + @Autowired + private ModelConfig myModelConfig; @PostConstruct public void start() { - + ourLog.trace("Starting transaction processor"); } public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { @@ -195,7 +199,7 @@ public abstract class BaseTransactionProcessor { private void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome, IBase newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) { - IIdType newId = outcome.getId().toUnqualifiedVersionless(); + IIdType newId = outcome.getId().toUnqualified(); IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); if (newId.equals(resourceId) == false) { idSubstitutions.put(resourceId, newId); @@ -900,20 +904,32 @@ public abstract class BaseTransactionProcessor { } // References + Set referencesToVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource); List allRefs = terser.getAllResourceReferences(nextResource); for (ResourceReferenceInfo nextRef : allRefs) { - IIdType nextId = nextRef.getResourceReference().getReferenceElement(); + IBaseReference resourceReference = nextRef.getResourceReference(); + IIdType nextId = resourceReference.getReferenceElement(); if (!nextId.hasIdPart()) { continue; } if (theIdSubstitutions.containsKey(nextId)) { IIdType newId = theIdSubstitutions.get(nextId); ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - nextRef.getResourceReference().setReference(newId.getValue()); + if (referencesToVersion.contains(resourceReference)) { + DaoMethodOutcome outcome = theIdToPersistedOutcome.get(newId); + resourceReference.setReference(newId.getValue()); + } else { + resourceReference.setReference(newId.toVersionless().getValue()); + } } 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 { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); + if (referencesToVersion.contains(resourceReference)) { + DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId); + if (!outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) { + resourceReference.setReference(nextId.getValue()); + } + } } } @@ -928,7 +944,7 @@ public abstract class BaseTransactionProcessor { if (theIdSubstitutions.containsKey(nextUriString)) { IIdType newId = theIdSubstitutions.get(nextUriString); ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - nextRef.setValueAsString(newId.getValue()); + nextRef.setValueAsString(newId.toVersionless().getValue()); } else { ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoBundleDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoBundleDstu2.java index d4f53b9e4b2..ed8b2e0a16f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoBundleDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoBundleDstu2.java @@ -22,6 +22,8 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -32,8 +34,8 @@ import static org.apache.commons.lang3.StringUtils.defaultString; public class FhirResourceDaoBundleDstu2 extends BaseHapiFhirResourceDao { @Override - protected void preProcessResourceForStorage(IBaseResource theResource) { - super.preProcessResourceForStorage(theResource); + protected void preProcessResourceForStorage(IBaseResource theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean thePerformIndexing) { + super.preProcessResourceForStorage(theResource, theRequestDetails, theTransactionDetails, thePerformIndexing); for (Entry next : ((Bundle)theResource).getEntry()) { next.setFullUrl((String) null); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index 305edac8b35..4f3be1fecd0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -29,7 +29,9 @@ 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.IDao; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; @@ -42,6 +44,8 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.ResourceSearchView; import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTag; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; @@ -87,6 +91,7 @@ import com.healthmarketscience.sqlbuilder.Condition; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.IdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -147,6 +152,8 @@ public class SearchBuilder implements ISearchBuilder { @Autowired private DaoConfig myDaoConfig; @Autowired + private DaoRegistry myDaoRegistry; + @Autowired private IResourceSearchViewDao myResourceSearchViewDao; @Autowired private FhirContext myContext; @@ -174,8 +181,11 @@ public class SearchBuilder implements ISearchBuilder { private SqlObjectFactory mySqlBuilderFactory; @Autowired private HibernatePropertiesProvider myDialectProvider; - + @Autowired + private ModelConfig myModelConfig; + private boolean hasNextIteratorQuery = false; + /** * Constructor */ @@ -428,7 +438,7 @@ public class SearchBuilder implements ISearchBuilder { //-- exclude the pids already in the previous iterator if (hasNextIteratorQuery) sqlBuilder.excludeResourceIdsPredicate(myPidSet); - + /* * Sort * @@ -532,23 +542,23 @@ public class SearchBuilder implements ISearchBuilder { break; case QUANTITY: theQueryStack.addSortOnQuantity(myResourceName, theSort.getParamName(), ascending); - break; + break; case COMPOSITE: List compositList = param.getCompositeOf(); if (compositList == null) { throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName() + " is not defined by the resource " + myResourceName); } if (compositList.size() != 2) { - throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName() - + " must have 2 composite types declared in parameter annotation, found " - + compositList.size()); + throw new InvalidRequestException("The composite _sort parameter " + theSort.getParamName() + + " must have 2 composite types declared in parameter annotation, found " + + compositList.size()); } RuntimeSearchParam left = compositList.get(0); RuntimeSearchParam right = compositList.get(1); - + createCompositeSort(theQueryStack, myResourceName, left.getParamType(), left.getName(), ascending); createCompositeSort(theQueryStack, myResourceName, right.getParamType(), right.getName(), ascending); - + break; case SPECIAL: case HAS: @@ -562,24 +572,30 @@ public class SearchBuilder implements ISearchBuilder { createSort(theQueryStack, theSort.getChain()); } - + private void createCompositeSort(QueryStack theQueryStack, String theResourceName, RestSearchParameterTypeEnum theParamType, String theParamName, boolean theAscending) { switch (theParamType) { - case STRING: - theQueryStack.addSortOnString(myResourceName, theParamName, theAscending); - break; - case DATE: - theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending); - break; - case TOKEN: - theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending); - break; - case QUANTITY: - theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending); - break; - default: - throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParamType + " on _sort="+ theParamName); + case STRING: + theQueryStack.addSortOnString(myResourceName, theParamName, theAscending); + break; + case DATE: + theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending); + break; + case TOKEN: + theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending); + break; + case QUANTITY: + theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending); + break; + case NUMBER: + case REFERENCE: + case COMPOSITE: + case URI: + case HAS: + case SPECIAL: + default: + throw new InvalidRequestException("Don't know how to handle composite parameter with type of " + theParamType + " on _sort=" + theParamName); } } @@ -587,34 +603,61 @@ public class SearchBuilder implements ISearchBuilder { private void doLoadPids(Collection thePids, Collection theIncludedPids, List theResourceListToPopulate, boolean theForHistoryOperation, Map thePosition) { - List myLongPersistentIds; - if (thePids.size() < getMaximumPageSize()) { - myLongPersistentIds = normalizeIdListForLastNInClause(ResourcePersistentId.toLongList(thePids)); - } else { - myLongPersistentIds = ResourcePersistentId.toLongList(thePids); + Map resourcePidToVersion = null; + for (ResourcePersistentId next : thePids) { + if (next.getVersion() != null && myModelConfig.isRespectVersionsForSearchIncludes()) { + if (resourcePidToVersion == null) { + resourcePidToVersion = new HashMap<>(); + } + resourcePidToVersion.put(next.getIdAsLong(), next.getVersion()); + } + } + + List versionlessPids = ResourcePersistentId.toLongList(thePids); + if (versionlessPids.size() < getMaximumPageSize()) { + versionlessPids = normalizeIdListForLastNInClause(versionlessPids); } // -- get the resource from the searchView - Collection resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(myLongPersistentIds); + Collection resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(versionlessPids); //-- preload all tags with tag definition if any - Map> tagMap = getResourceTagMap(resourceSearchViewList); + Map> tagMap = getResourceTagMap(resourceSearchViewList); - ResourcePersistentId resourceId; - for (ResourceSearchView next : resourceSearchViewList) { + for (IBaseResourceEntity next : resourceSearchViewList) { if (next.getDeleted() != null) { continue; } Class resourceType = myContext.getResourceDefinition(next.getResourceType()).getImplementingClass(); - resourceId = new ResourcePersistentId(next.getId()); + ResourcePersistentId resourceId = new ResourcePersistentId(next.getResourceId()); - IBaseResource resource = myCallingDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation); + /* + * If a specific version is requested via an include, we'll replace the current version + * with the specific desired version. This is not the most efficient thing, given that + * we're loading the current version and then turning around and throwing it away again. + * This could be optimized and probably should be, but it's not critical given that + * this only applies to includes, which don't tend to be massive in numbers. + */ + if (resourcePidToVersion != null) { + Long version = resourcePidToVersion.get(next.getResourceId()); + if (version != null && !version.equals(next.getVersion())) { + resourceId.setVersion(version); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceType); + next = dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null); + } + } + + IBaseResource resource = null; + if (next != null) { + resource = myCallingDao.toResource(resourceType, next, tagMap.get(next.getId()), theForHistoryOperation); + } if (resource == null) { ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion()); continue; } + Integer index = thePosition.get(resourceId); if (index == null) { ourLog.warn("Got back unexpected resource PID {}", resourceId); @@ -639,17 +682,17 @@ public class SearchBuilder implements ISearchBuilder { } } - private Map> getResourceTagMap(Collection theResourceSearchViewList) { + private Map> getResourceTagMap(Collection theResourceSearchViewList) { List idList = new ArrayList<>(theResourceSearchViewList.size()); //-- find all resource has tags - for (ResourceSearchView resource : theResourceSearchViewList) { + for (IBaseResourceEntity resource : theResourceSearchViewList) { if (resource.isHasTags()) idList.add(resource.getId()); } - Map> tagMap = new HashMap<>(); + Map> tagMap = new HashMap<>(); //-- no tags if (idList.size() == 0) @@ -664,11 +707,11 @@ public class SearchBuilder implements ISearchBuilder { for (ResourceTag tag : tagList) { resourceId = new ResourcePersistentId(tag.getResourceId()); - tagCol = tagMap.get(resourceId); + tagCol = tagMap.get(resourceId.getIdAsLong()); if (tagCol == null) { tagCol = new ArrayList<>(); tagCol.add(tag); - tagMap.put(resourceId, tagCol); + tagMap.put(resourceId.getIdAsLong(), tagCol); } else { tagCol.add(tag); } @@ -712,8 +755,12 @@ public class SearchBuilder implements ISearchBuilder { if (theRevIncludes == null || theRevIncludes.isEmpty()) { return new HashSet<>(); } - String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; - String findFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid"; + String searchPidFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; + String findPidFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid"; + String findVersionFieldName = null; + if (!theReverseMode && myModelConfig.isRespectVersionsForSearchIncludes()) { + findVersionFieldName = "myTargetResourceVersion"; + } List nextRoundMatches = new ArrayList<>(theMatches); HashSet allAdded = new HashSet<>(); @@ -737,24 +784,37 @@ public class SearchBuilder implements ISearchBuilder { boolean matchAll = "*".equals(nextInclude.getValue()); if (matchAll) { - String sql; - sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) "; + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT r.").append(findPidFieldName); + if (findVersionFieldName != null) { + sqlBuilder.append(", r." + findVersionFieldName); + } + sqlBuilder.append(" FROM ResourceLink r WHERE r."); + sqlBuilder.append(searchPidFieldName); + sqlBuilder.append(" IN (:target_pids)"); + String sql = sqlBuilder.toString(); List> partitions = partition(nextRoundMatches, getMaximumPageSize()); for (Collection nextPartition : partitions) { - TypedQuery q = theEntityManager.createQuery(sql, Long.class); + TypedQuery q = theEntityManager.createQuery(sql, Object[].class); q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition)); - List results = q.getResultList(); - for (Long resourceLink : results) { - if (resourceLink == null) { + List results = q.getResultList(); + for (Object nextRow : results) { + if (nextRow == null) { // This can happen if there are outgoing references which are canonical or point to // other servers continue; } - if (theReverseMode) { - pidsToInclude.add(new ResourcePersistentId(resourceLink)); + + Long resourceLink; + Long version = null; + if (findVersionFieldName != null) { + resourceLink = (Long) ((Object[]) nextRow)[0]; + version = (Long) ((Object[]) nextRow)[1]; } else { - pidsToInclude.add(new ResourcePersistentId(resourceLink)); + resourceLink = (Long)nextRow; } + + pidsToInclude.add(new ResourcePersistentId(resourceLink, version)); } } } else { @@ -789,17 +849,22 @@ public class SearchBuilder implements ISearchBuilder { String sql; boolean haveTargetTypesDefinedByParam = param.hasTargets(); + String fieldsToLoad = "r." + findPidFieldName; + if (findVersionFieldName != null) { + fieldsToLoad += ", r." + findVersionFieldName; + } + if (targetResourceType != null) { - sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type"; + sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type"; } else if (haveTargetTypesDefinedByParam) { - sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)"; + sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)"; } else { - sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; + sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids)"; } List> partitions = partition(nextRoundMatches, getMaximumPageSize()); for (Collection nextPartition : partitions) { - TypedQuery q = theEntityManager.createQuery(sql, Long.class); + TypedQuery q = theEntityManager.createQuery(sql, Object[].class); q.setParameter("src_path", nextPath); q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition)); if (targetResourceType != null) { @@ -807,10 +872,18 @@ public class SearchBuilder implements ISearchBuilder { } else if (haveTargetTypesDefinedByParam) { q.setParameter("target_resource_types", param.getTargets()); } - List results = q.getResultList(); - for (Long resourceLink : results) { + List results = q.getResultList(); + for (Object resourceLink : results) { if (resourceLink != null) { - pidsToInclude.add(new ResourcePersistentId(resourceLink)); + ResourcePersistentId persistentId; + if (findVersionFieldName != null) { + persistentId = new ResourcePersistentId(((Object[])resourceLink)[0]); + persistentId.setVersion((Long) ((Object[])resourceLink)[1]); + } else { + persistentId = new ResourcePersistentId(resourceLink); + } + assert persistentId.getId() instanceof Long; + pidsToInclude.add(persistentId); } } } @@ -1051,6 +1124,7 @@ public class SearchBuilder implements ISearchBuilder { private final boolean myHaveRawSqlHooks; private final boolean myHavePerfTraceFoundIdHook; private final SortSpec mySort; + private final Integer myOffset; private boolean myFirst = true; private IncludesIterator myIncludesIterator; private ResourcePersistentId myNext; @@ -1059,8 +1133,6 @@ public class SearchBuilder implements ISearchBuilder { private boolean myStillNeedToFetchIncludes; private int mySkipCount = 0; private int myNonSkipCount = 0; - private final Integer myOffset; - private ArrayList myQueryList = new ArrayList<>(); private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) { @@ -1249,7 +1321,7 @@ public class SearchBuilder implements ISearchBuilder { if (myNext == null) { fetchNext(); } - return !NO_MORE.equals(myNext); + return !NO_MORE.equals(myNext); } @Override diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java index fc33a513b13..63f61b740b1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hibernate.Session; import org.hibernate.internal.SessionImpl; @@ -50,6 +51,9 @@ public class TransactionProcessorTest { private MatchResourceUrlService myMatchResourceUrlService; @MockBean private HapiTransactionService myHapiTransactionService; + @MockBean + private ModelConfig myModelConfig; + @MockBean(answer = Answers.RETURNS_DEEP_STUBS) private SessionImpl mySession; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java index 339c23e6d91..26530ca7379 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java @@ -389,7 +389,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); @@ -399,7 +399,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: batch-response", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: batch-response. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3SystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3SystemTest.java index 33c3782f7f7..2f3f34ea987 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3SystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3SystemTest.java @@ -4,8 +4,6 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.rp.dstu3.PatientResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.TestUtil; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import javax.servlet.ServletConfig; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index e69a5d91b55..f062d32a128 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -526,7 +526,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); @@ -536,7 +536,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index d3c227983f9..dab2c282abd 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -2985,7 +2985,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { dr.addPresentedForm(attachment); Attachment attachment2 = new Attachment(); - attachment2.setUrl(IdType.newRandomUuid().getValue()); // this one has no subscitution + attachment2.setUrl(IdType.newRandomUuid().getValue()); // this one has no substitution dr.addPresentedForm(attachment2); Bundle transactionBundle = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index c68a9a705a3..36da81a412f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -842,7 +842,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing)", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: (missing). Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); @@ -852,7 +852,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { myBundleDao.create(bundle, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset", e.getMessage()); + assertEquals("Unable to store a Bundle resource on this server with a Bundle.type value of: searchset. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.", e.getMessage()); } bundle = new Bundle(); 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 new file mode 100644 index 00000000000..b1e52f80bca --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -0,0 +1,482 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.util.BundleBuilder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4VersionedReferenceTest.class); + + @AfterEach + public void afterEach() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(true); + myDaoConfig.setDeleteEnabled(new DaoConfig().isDeleteEnabled()); + myModelConfig.setRespectVersionsForSearchIncludes(new ModelConfig().isRespectVersionsForSearchIncludes()); + myModelConfig.setAutoVersionReferenceAtPaths(new ModelConfig().getAutoVersionReferenceAtPaths()); + } + + @Test + public void testStoreAndRetrieveVersionedReference() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + + Patient p = new Patient(); + p.setActive(true); + IIdType patientId = myPatientDao.create(p).getId().toUnqualified(); + assertEquals("1", patientId.getVersionIdPart()); + assertEquals(null, patientId.getBaseUrl()); + String patientIdString = patientId.getValue(); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientIdString); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Read back + observation = myObservationDao.read(observationId); + assertEquals(patientIdString, observation.getSubject().getReference()); + } + + @Test + public void testDontOverwriteExistingVersion() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + + Patient p = new Patient(); + p.setActive(true); + myPatientDao.create(p); + + // Update the patient + p.setActive(false); + IIdType patientId = myPatientDao.update(p).getId().toUnqualified(); + + assertEquals("2", patientId.getVersionIdPart()); + assertEquals(null, patientId.getBaseUrl()); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.withVersion("1").getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Read back + observation = myObservationDao.read(observationId); + assertEquals(patientId.withVersion("1").getValue(), observation.getSubject().getReference()); + } + + @Test + public void testInsertVersionedReferenceAtPath() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject"); + + Patient p = new Patient(); + p.setActive(true); + IIdType patientId = myPatientDao.create(p).getId().toUnqualified(); + assertEquals("1", patientId.getVersionIdPart()); + assertEquals(null, patientId.getBaseUrl()); + String patientIdString = patientId.getValue(); + + // Create - put an unversioned reference in the subject + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.toVersionless().getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Read back and verify that reference is now versioned + observation = myObservationDao.read(observationId); + assertEquals(patientIdString, observation.getSubject().getReference()); + + myCaptureQueriesListener.clear(); + + // Update - put an unversioned reference in the subject + observation = new Observation(); + observation.setId(observationId); + observation.addIdentifier().setSystem("http://foo").setValue("bar"); + observation.getSubject().setReference(patientId.toVersionless().getValue()); + myObservationDao.update(observation); + + // Make sure we're not introducing any extra DB operations + assertEquals(5, myCaptureQueriesListener.logSelectQueries().size()); + + // Read back and verify that reference is now versioned + observation = myObservationDao.read(observationId); + assertEquals(patientIdString, observation.getSubject().getReference()); + } + + @Test + public void testInsertVersionedReferenceAtPath_InTransaction_SourceAndTargetBothCreated() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject"); + + + BundleBuilder builder = new BundleBuilder(myFhirCtx); + + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.setActive(true); + builder.addTransactionCreateEntry(patient); + + Encounter encounter = new Encounter(); + encounter.setId(IdType.newRandomUuid()); + encounter.addIdentifier().setSystem("http://baz").setValue("baz"); + builder.addTransactionCreateEntry(encounter); + + Observation observation = new Observation(); + observation.getSubject().setReference(patient.getId()); // versioned + observation.getEncounter().setReference(encounter.getId()); // not versioned + builder.addTransactionCreateEntry(observation); + + Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + IdType encounterId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + IdType observationId = new IdType(outcome.getEntry().get(2).getResponse().getLocation()); + assertTrue(patientId.hasVersionIdPart()); + assertTrue(encounterId.hasVersionIdPart()); + assertTrue(observationId.hasVersionIdPart()); + + // Read back and verify that reference is now versioned + observation = myObservationDao.read(observationId); + assertEquals(patientId.getValue(), observation.getSubject().getReference()); + assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference()); + + } + + @Test + public void testInsertVersionedReferenceAtPath_InTransaction_TargetConditionalCreatedNop() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject"); + + { + // Create patient + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.setActive(true); + myPatientDao.create(patient).getId(); + + // Update patient to make a second version + patient.setActive(false); + myPatientDao.update(patient); + + // Create encounter + Encounter encounter = new Encounter(); + encounter.setId(IdType.newRandomUuid()); + encounter.addIdentifier().setSystem("http://baz").setValue("baz"); + myEncounterDao.create(encounter); + } + + BundleBuilder builder = new BundleBuilder(myFhirCtx); + + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.setActive(true); + builder.addTransactionCreateEntry(patient).conditional("Patient?active=false"); + + Encounter encounter = new Encounter(); + encounter.setId(IdType.newRandomUuid()); + encounter.addIdentifier().setSystem("http://baz").setValue("baz"); + builder.addTransactionCreateEntry(encounter).conditional("Encounter?identifier=http://baz|baz"); + + Observation observation = new Observation(); + observation.getSubject().setReference(patient.getId()); // versioned + observation.getEncounter().setReference(encounter.getId()); // not versioned + builder.addTransactionCreateEntry(observation); + + Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); + assertEquals("200 OK", outcome.getEntry().get(1).getResponse().getStatus()); + assertEquals("201 Created", outcome.getEntry().get(2).getResponse().getStatus()); + IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + IdType encounterId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + IdType observationId = new IdType(outcome.getEntry().get(2).getResponse().getLocation()); + assertEquals("2", patientId.getVersionIdPart()); + assertEquals("1", encounterId.getVersionIdPart()); + assertEquals("1", observationId.getVersionIdPart()); + + // Read back and verify that reference is now versioned + observation = myObservationDao.read(observationId); + assertEquals(patientId.getValue(), observation.getSubject().getReference()); + assertEquals(encounterId.toVersionless().getValue(), observation.getEncounter().getReference()); + + } + + + @Test + public void testInsertVersionedReferenceAtPath_InTransaction_TargetUpdate() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myDaoConfig.setDeleteEnabled(false); + myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject"); + + { + // Create patient + Patient patient = new Patient(); + patient.setId("PATIENT"); + patient.setActive(true); + myPatientDao.update(patient).getId(); + + // Update patient to make a second version + patient.setActive(false); + myPatientDao.update(patient); + + } + + BundleBuilder builder = new BundleBuilder(myFhirCtx); + + Patient patient = new Patient(); + patient.setId("Patient/PATIENT"); + patient.setActive(true); + builder.addTransactionUpdateEntry(patient); + + Observation observation = new Observation(); + observation.getSubject().setReference(patient.getId()); // versioned + builder.addTransactionCreateEntry(observation); + + myCaptureQueriesListener.clear(); + Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); + assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus()); + IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + assertEquals("3", patientId.getVersionIdPart()); + assertEquals("1", observationId.getVersionIdPart()); + + // Make sure we're not introducing any extra DB operations + assertEquals(3, myCaptureQueriesListener.logSelectQueries().size()); + + // Read back and verify that reference is now versioned + observation = myObservationDao.read(observationId); + assertEquals(patientId.getValue(), observation.getSubject().getReference()); + + } + + + @Test + public void testInsertVersionedReferenceAtPath_InTransaction_TargetUpdateConditional() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setAutoVersionReferenceAtPaths("Observation.subject"); + + { + // Create patient + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.setActive(true); + myPatientDao.create(patient).getId(); + + // Update patient to make a second version + patient.setActive(false); + myPatientDao.update(patient); + + } + + BundleBuilder builder = new BundleBuilder(myFhirCtx); + + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.setActive(true); + builder + .addTransactionUpdateEntry(patient) + .conditional("Patient?active=false"); + + Observation observation = new Observation(); + observation.getSubject().setReference(patient.getId()); // versioned + builder.addTransactionCreateEntry(observation); + + myCaptureQueriesListener.clear(); + + Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); + assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus()); + IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + assertEquals("3", patientId.getVersionIdPart()); + assertEquals("1", observationId.getVersionIdPart()); + + // Make sure we're not introducing any extra DB operations + assertEquals(4, myCaptureQueriesListener.logSelectQueries().size()); + + // 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); + myModelConfig.setRespectVersionsForSearchIncludes(true); + + // Create the patient + Patient p = new Patient(); + p.addIdentifier().setSystem("http://foo").setValue("1"); + myPatientDao.create(p); + + // Update the patient + p.getIdentifier().get(0).setValue("2"); + IIdType patientId = myPatientDao.update(p).getId().toUnqualified(); + assertEquals("2", patientId.getVersionIdPart()); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.withVersion("1").getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Search - Non Synchronous for * + { + IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(1, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 1); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue()); + } + + // Search - Non Synchronous for named include + { + IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(Observation.INCLUDE_PATIENT)); + assertEquals(1, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 1); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue()); + } + + } + + @Test + public void testSearchAndIncludeVersionedReference_Synchronous() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + myModelConfig.setRespectVersionsForSearchIncludes(true); + + // Create the patient + Patient p = new Patient(); + p.addIdentifier().setSystem("http://foo").setValue("1"); + myPatientDao.create(p); + + // Update the patient + p.getIdentifier().get(0).setValue("2"); + IIdType patientId = myPatientDao.update(p).getId().toUnqualified(); + assertEquals("2", patientId.getVersionIdPart()); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.withVersion("1").getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Search - Non Synchronous for * + { + IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(2, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue()); + } + + // Search - Non Synchronous for named include + { + IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(Observation.INCLUDE_PATIENT)); + assertEquals(2, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue()); + } + + } + + + @Test + public void testSearchAndIncludeUnersionedReference_Asynchronous() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(true); + myModelConfig.setRespectVersionsForSearchIncludes(true); + + // Create the patient + Patient p = new Patient(); + p.addIdentifier().setSystem("http://foo").setValue("1"); + myPatientDao.create(p); + + // Update the patient + p.getIdentifier().get(0).setValue("2"); + IIdType patientId = myPatientDao.update(p).getId().toUnqualified(); + assertEquals("2", patientId.getVersionIdPart()); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.withVersion("1").getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Search - Non Synchronous for * + { + IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(1, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 1); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue()); + } + + // Search - Non Synchronous for named include + { + IBundleProvider outcome = myObservationDao.search(new SearchParameterMap().addInclude(Observation.INCLUDE_PATIENT)); + assertEquals(1, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 1); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue()); + } + + } + + @Test + public void testSearchAndIncludeUnversionedReference_Synchronous() { + myFhirCtx.getParserOptions().setStripVersionsFromReferences(true); + myModelConfig.setRespectVersionsForSearchIncludes(true); + + // Create the patient + Patient p = new Patient(); + p.addIdentifier().setSystem("http://foo").setValue("1"); + myPatientDao.create(p); + + // Update the patient + p.getIdentifier().get(0).setValue("2"); + IIdType patientId = myPatientDao.update(p).getId().toUnqualified(); + assertEquals("2", patientId.getVersionIdPart()); + + Observation observation = new Observation(); + observation.getSubject().setReference(patientId.withVersion("1").getValue()); + IIdType observationId = myObservationDao.create(observation).getId().toUnqualified(); + + // Search - Non Synchronous for * + { + IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(IBaseResource.INCLUDE_ALL)); + assertEquals(2, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue()); + } + + // Search - Non Synchronous for named include + { + IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous().addInclude(Observation.INCLUDE_PATIENT)); + assertEquals(2, outcome.sizeOrThrowNpe()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size()); + assertEquals(observationId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(patientId.withVersion("2").getValue(), resources.get(1).getIdElement().getValue()); + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index bcf63a55729..ea04e937679 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -257,7 +257,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { client.create().resource(resBody).execute().getId(); fail(); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction")); + assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index adedaa93ad3..976b042a7e3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -468,7 +468,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { client.create().resource(resBody).execute().getId(); fail(); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction")); + assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index f4f6e2852eb..44142f3d325 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -798,7 +798,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { client.create().resource(resBody).execute(); fail(); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction")); + assertThat(e.getMessage(), containsString("Unable to store a Bundle resource on this server with a Bundle.type value of: transaction. Note that if you are trying to perform a FHIR transaction or batch operation you should POST the Bundle resource to the Base URL of the server, not to the /Bundle endpoint.")); } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 04f725cc42e..82eed2fefda 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -123,6 +123,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { Builder.BuilderWithTableName quantityTable = version.onTable("HFJ_SPIDX_QUANTITY"); quantityTable.modifyColumn("20210116.1", "SP_VALUE").nullable().failureAllowed().withType(ColumnTypeEnum.DOUBLE); + // HFJ_RES_LINK + version.onTable("HFJ_RES_LINK") + .addColumn("20210126.1", "TARGET_RESOURCE_VERSION").nullable().type(ColumnTypeEnum.LONG); } protected void init520() { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/api/IDaoRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/api/IDaoRegistry.java index fbc109c11ae..a5eb8f7f0b7 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/api/IDaoRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/api/IDaoRegistry.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.api; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/PartitionSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/PartitionSettings.java index d3b5ec014a7..a09ff4f8642 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/PartitionSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/PartitionSettings.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.config; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IBasePersistedResource.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IBasePersistedResource.java index b1105f62388..b6ee61c683f 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IBasePersistedResource.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IBasePersistedResource.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IResourceLookup.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IResourceLookup.java index 03da9283d20..0b32a4f88c8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IResourceLookup.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/IResourceLookup.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/ResourceLookup.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/ResourceLookup.java index d88dd2b37a0..425b0673178 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/ResourceLookup.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/cross/ResourceLookup.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.cross; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java index 4e376cc6be4..0e6dd25f1f0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java index ff925b15725..5d2263f9c26 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java index 48dbef1d1c2..95b5699210f 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index cd87fffc3c4..fc816382abb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java index 9d85f431143..48b0c288ce1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java index a131e67936b..a3988d12323 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java index b966cf0f95e..638b070b1ae 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java index a75ffc24a9d..83b5fd24cf9 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index cb2a4d65466..43f98bdc209 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% @@ -20,20 +20,25 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ +import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.DateTimeType; +import javax.annotation.PostConstruct; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + // TODO: move this to ca.uhn.fhir.jpa.model.config public class ModelConfig { @@ -90,6 +95,9 @@ public class ModelConfig { private IPrimitiveType myPeriodIndexEndOfTime; private NormalizedQuantitySearchLevel myNormalizedQuantitySearchLevel; + private Set myAutoVersionReferenceAtPaths = Collections.emptySet(); + private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); + private boolean myRespectVersionsForSearchIncludes; /** * Constructor @@ -268,7 +276,7 @@ public class ModelConfig { } HashSet treatBaseUrlsAsLocal = new HashSet<>(); - for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { + for (String next : defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { while (next.endsWith("/")) { next = next.substring(0, next.length() - 1); } @@ -618,6 +626,110 @@ public class ModelConfig { myNormalizedQuantitySearchLevel = theNormalizedQuantitySearchLevel; } + /** + * When set with resource paths (e.g. "Observation.subject"), any references found at the given paths + * will automatically have versions appended. The version used will be the current version of the given resource. + * + * @since 5.3.0 + */ + public Set getAutoVersionReferenceAtPaths() { + return myAutoVersionReferenceAtPaths; + } + + /** + * When set with resource paths (e.g. "Observation.subject"), any references found at the given paths + * will automatically have versions appended. The version used will be the current version of the given resource. + *

+ * Versions will only be added if the reference does not already have a version, so any versioned references + * supplied by the client will take precedence over the automatic current version. + *

+ *

+ * Note that for this setting to be useful, the {@link ParserOptions} + * {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths} + * option must also be set. + *

+ * + * @param thePaths A collection of reference paths for which the versions will be appended automatically + * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that + * only resource name and field names with dots separating is allowed here (no repetition + * indicators, FluentPath expressions, etc.) + * @since 5.3.0 + */ + public void setAutoVersionReferenceAtPaths(String... thePaths) { + Set paths = Collections.emptySet(); + if (thePaths != null) { + paths = new HashSet<>(Arrays.asList(thePaths)); + } + setAutoVersionReferenceAtPaths(paths); + } + + /** + * When set with resource paths (e.g. "Observation.subject"), any references found at the given paths + * will automatically have versions appended. The version used will be the current version of the given resource. + *

+ * Versions will only be added if the reference does not already have a version, so any versioned references + * supplied by the client will take precedence over the automatic current version. + *

+ *

+ * Note that for this setting to be useful, the {@link ParserOptions} + * {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths} + * option must also be set + *

+ * + * @param thePaths A collection of reference paths for which the versions will be appended automatically + * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that + * only resource name and field names with dots separating is allowed here (no repetition + * indicators, FluentPath expressions, etc.) + * @since 5.3.0 + */ + public void setAutoVersionReferenceAtPaths(Set thePaths) { + Set paths = defaultIfNull(thePaths, Collections.emptySet()); + Map> byType = new HashMap<>(); + for (String nextPath : paths) { + int doxIdx = nextPath.indexOf('.'); + Validate.isTrue(doxIdx > 0, "Invalid path for auto-version reference at path: %s", nextPath); + String type = nextPath.substring(0, doxIdx); + byType.computeIfAbsent(type, t -> new HashSet<>()).add(nextPath); + } + + + myAutoVersionReferenceAtPaths = paths; + myTypeToAutoVersionReferenceAtPaths = byType; + } + + /** + * Returns a sub-collection of {@link #getAutoVersionReferenceAtPaths()} containing only paths + * for the given resource type. + * + * @since 5.3.0 + */ + public Set getAutoVersionReferenceAtPathsByResourceType(String theResourceType) { + Validate.notEmpty(theResourceType, "theResourceType must not be null or empty"); + Set retVal = myTypeToAutoVersionReferenceAtPaths.get(theResourceType); + retVal = defaultIfNull(retVal, Collections.emptySet()); + return retVal; + } + + /** + * Should searches with _include respect versioned references, and pull the specific requested version. + * This may have performance impacts on heavily loaded systems. + * + * @since 5.3.0 + */ + public boolean isRespectVersionsForSearchIncludes() { + return myRespectVersionsForSearchIncludes; + } + + /** + * Should searches with _include respect versioned references, and pull the specific requested version. + * This may have performance impacts on heavily loaded systems. + * + * @since 5.3.0 + */ + public void setRespectVersionsForSearchIncludes(boolean theRespectVersionsForSearchIncludes) { + myRespectVersionsForSearchIncludes = theRespectVersionsForSearchIncludes; + } + private static void validateTreatBaseUrlsAsLocal(String theUrl) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java index 56b9dee8b31..5e0d0d5348a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java index e6fe8ad90f1..b9e428be54f 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java index 8d5ba9c2273..6d67cc0e288 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java index 40151df76fd..8b8e7ebc551 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java index eca8d8019e7..1301be50a16 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionablePartitionId.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java index 50fddbab268..6e37c22583e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java index 90ef11e5354..6de81cedb89 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java index 70d421321e0..4d4d8388ca1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java index 8c49fe15a0c..fd7be6b6733 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java index 2a16475642f..50b6161c55b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java index 9e35c2c5bff..c1844576f48 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java index b81b79366c8..33f65c9e4e6 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java index 70c1579cb92..17adca2a4d9 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java index b9d3c047f80..ce227863778 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java index 51e587fa29d..058ad6d964e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java index 03a89138db1..7a49ade0214 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index 5cbc85278d2..7c3219ce9b3 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java index 63473f75e2a..2394b51f929 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java index db99cecb3f8..2f46fd0b676 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java index a722898a109..98cf2561bfd 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% @@ -26,6 +26,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; import org.hl7.fhir.instance.model.api.IIdType; +import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -88,12 +89,12 @@ public class ResourceLink extends BaseResourceIndex { @Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true) @FullTextField private String myTargetResourceUrl; - + @Column(name = "TARGET_RESOURCE_VERSION", nullable = true) + private Long myTargetResourceVersion; @FullTextField @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3 @Temporal(TemporalType.TIMESTAMP) private Date myUpdated; - @Transient private transient String myTargetResourceId; @@ -101,6 +102,14 @@ public class ResourceLink extends BaseResourceIndex { super(); } + public Long getTargetResourceVersion() { + return myTargetResourceVersion; + } + + public void setTargetResourceVersion(Long theTargetResourceVersion) { + myTargetResourceVersion = theTargetResourceVersion; + } + public String getTargetResourceId() { if (myTargetResourceId == null && myTargetResource != null) { myTargetResourceId = getTargetResource().getIdDt().getIdPart(); @@ -178,10 +187,6 @@ public class ResourceLink extends BaseResourceIndex { return myTargetResourceUrl; } - public Long getTargetResourcePid() { - return myTargetResourcePid; - } - public void setTargetResourceUrl(IIdType theTargetResourceUrl) { Validate.isTrue(theTargetResourceUrl.hasBaseUrl()); Validate.isTrue(theTargetResourceUrl.hasResourceType()); @@ -199,6 +204,10 @@ public class ResourceLink extends BaseResourceIndex { myTargetResourceUrl = theTargetResourceUrl.getValue(); } + public Long getTargetResourcePid() { + return myTargetResourcePid; + } + public void setTargetResourceUrlCanonical(String theTargetResourceUrl) { Validate.notBlank(theTargetResourceUrl); @@ -279,11 +288,15 @@ public class ResourceLink extends BaseResourceIndex { return retVal; } - public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceType, Long theTargetResourcePid, String theTargetResourceId, Date theUpdated) { + /** + * @param theTargetResourceVersion This should only be populated if the reference actually had a version + */ + public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceType, Long theTargetResourcePid, String theTargetResourceId, Date theUpdated, @Nullable Long theTargetResourceVersion) { ResourceLink retVal = new ResourceLink(); retVal.setSourcePath(theSourcePath); retVal.setSourceResource(theSourceResource); retVal.setTargetResource(theTargetResourceType, theTargetResourcePid, theTargetResourceId); + retVal.setTargetResourceVersion(theTargetResourceVersion); retVal.setUpdated(theUpdated); return retVal; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 0b6ac43c384..b6371a50478 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java index 16d53ac0803..388ae6ae29c 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java index 9baef0f1201..2afefc324dc 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java index 93b28325647..de8543c0fc8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java index aa1869e8f0a..49778769551 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/HapiJob.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/HapiJob.java index 0b4d3129a68..54b21244673 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/HapiJob.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/HapiJob.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/IHapiScheduler.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/IHapiScheduler.java index 3a28afb1867..28490808079 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/IHapiScheduler.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/IHapiScheduler.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISchedulerService.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISchedulerService.java index c5cd4394fba..a9e60ddacdb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISchedulerService.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISchedulerService.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISmartLifecyclePhase.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISmartLifecyclePhase.java index c104d263407..203f0d7039a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISmartLifecyclePhase.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ISmartLifecyclePhase.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ScheduledJobDefinition.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ScheduledJobDefinition.java index b91fefb2580..6abfe39b8e5 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ScheduledJobDefinition.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/sched/ScheduledJobDefinition.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.sched; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ResourceTableRoutingBinder.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ResourceTableRoutingBinder.java index ceb7f9b3a4e..a47d14a9629 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ResourceTableRoutingBinder.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ResourceTableRoutingBinder.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchRuntimeDetails.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchRuntimeDetails.java index 90bc2cb72bd..ff7dcaf0413 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchRuntimeDetails.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchRuntimeDetails.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchStatusEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchStatusEnum.java index dbbcea21fcd..d27b7a300c6 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchStatusEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/SearchStatusEnum.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/StorageProcessingMessage.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/StorageProcessingMessage.java index d2cff96d827..446b683deac 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/StorageProcessingMessage.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/StorageProcessingMessage.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.search; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/CodeSystemHash.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/CodeSystemHash.java index cc8376fb574..7caa80b4697 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/CodeSystemHash.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/CodeSystemHash.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index b24a677c985..f08e9775eac 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java index d718eaaedb6..ede0504046b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.model.util; /* * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/util/JpaInterceptorBroadcaster.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/util/JpaInterceptorBroadcaster.java index 7f52ba6a071..8198d63ef99 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/util/JpaInterceptorBroadcaster.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/util/JpaInterceptorBroadcaster.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.util; /*- * #%L - * HAPI FHIR Model + * HAPI FHIR JPA Model * %% * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index b421de3b1df..6a2dbbc3ce0 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -162,8 +162,9 @@ public class SearchParameterMap implements Serializable { return this; } - public void addInclude(Include theInclude) { + public SearchParameterMap addInclude(Include theInclude) { getIncludes().add(theInclude); + return this; } private void addLastUpdateParam(StringBuilder b, DateParam date) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index 43d0945b341..a3247d7e74b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -268,8 +268,8 @@ public class SearchParamExtractorService { } Class type = resourceDefinition.getImplementingClass(); - String id = nextId.getIdPart(); - if (StringUtils.isBlank(id)) { + String targetId = nextId.getIdPart(); + if (StringUtils.isBlank(targetId)) { String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " + nextId.getValue(); if (theFailOnInvalidReference) { throw new InvalidRequestException(msg); @@ -279,9 +279,11 @@ public class SearchParamExtractorService { } } - ResourcePersistentId resolvedTargetId = theTransactionDetails.getResolvedResourceIds().get(thePathAndRef.getRef().getReferenceElement()); + IIdType referenceElement = thePathAndRef.getRef().getReferenceElement(); + ResourcePersistentId resolvedTargetId = theTransactionDetails.getResolvedResourceId(referenceElement); ResourceLink resourceLink; + Long targetVersionId = nextId.getVersionIdPartAsLong(); if (resolvedTargetId != null) { /* @@ -289,7 +291,7 @@ public class SearchParamExtractorService { * need to resolve it again */ myResourceLinkResolver.validateTypeOrThrowException(type); - resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, resolvedTargetId.getIdAsLong(), nextId.getIdPart(), transactionDate); + resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, resolvedTargetId.getIdAsLong(), targetId, transactionDate, targetVersionId); } else if (theFailOnInvalidReference) { @@ -305,7 +307,7 @@ public class SearchParamExtractorService { } else { // Cache the outcome in the current transaction in case there are more references ResourcePersistentId persistentId = new ResourcePersistentId(resourceLink.getTargetResourcePid()); - theTransactionDetails.addResolvedResourceId(thePathAndRef.getRef().getReferenceElement(), persistentId); + theTransactionDetails.addResolvedResourceId(referenceElement, persistentId); } } else { @@ -317,7 +319,7 @@ public class SearchParamExtractorService { ResourceTable target; target = new ResourceTable(); target.setResourceType(typeString); - resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, null, nextId.getIdPart(), transactionDate); + resourceLink = ResourceLink.forLocalReference(thePathAndRef.getPath(), theEntity, typeString, null, targetId, transactionDate, targetVersionId); } @@ -346,7 +348,8 @@ public class SearchParamExtractorService { String targetResourceType = targetResource.getResourceType(); Long targetResourcePid = targetResource.getResourceId(); String targetResourceIdPart = theNextId.getIdPart(); - return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime); + Long targetVersion = theNextId.getVersionIdPartAsLong(); + return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime, targetVersion); } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParamsTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParamsTest.java index 3688f523317..b7b1ab7c08f 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParamsTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParamsTest.java @@ -36,7 +36,7 @@ public class ResourceIndexedSearchParamsTest { @Test public void matchResourceLinksStringCompareToLong() { - ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date()); + ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date(), null); myParams.getResourceLinks().add(link); ReferenceParam referenceParam = getReferenceParam(STRING_ID); @@ -46,7 +46,7 @@ public class ResourceIndexedSearchParamsTest { @Test public void matchResourceLinksStringCompareToString() { - ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date()); + ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date(), null); myParams.getResourceLinks().add(link); ReferenceParam referenceParam = getReferenceParam(STRING_ID); @@ -56,7 +56,7 @@ public class ResourceIndexedSearchParamsTest { @Test public void matchResourceLinksLongCompareToString() { - ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date()); + ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date(), null); myParams.getResourceLinks().add(link); ReferenceParam referenceParam = getReferenceParam(LONG_ID); @@ -66,7 +66,7 @@ public class ResourceIndexedSearchParamsTest { @Test public void matchResourceLinksLongCompareToLong() { - ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date()); + ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date(), null); myParams.getResourceLinks().add(link); ReferenceParam referenceParam = getReferenceParam(LONG_ID); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java index 713655bff67..0aee3537bd6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/ResourcePersistentId.java @@ -34,10 +34,20 @@ import java.util.Optional; public class ResourcePersistentId { private Object myId; + private Long myVersion; public ResourcePersistentId(Object theId) { + this(theId, null); + } + + /** + * @param theVersion This should only be populated if a specific version is needed. If you want the current version, + * leave this as null + */ + public ResourcePersistentId(Object theId, Long theVersion) { assert !(theId instanceof Optional); myId = theId; + myVersion = theVersion; } @Override @@ -47,12 +57,18 @@ public class ResourcePersistentId { } ResourcePersistentId that = (ResourcePersistentId) theO; - return ObjectUtil.equals(myId, that.myId); + boolean retVal = ObjectUtil.equals(myId, that.myId); + retVal &= ObjectUtil.equals(myVersion, that.myVersion); + return retVal; } @Override public int hashCode() { - return myId.hashCode(); + int retVal = myId.hashCode(); + if (myVersion != null) { + retVal += myVersion.hashCode(); + } + return retVal; } public Object getId() { @@ -72,6 +88,18 @@ public class ResourcePersistentId { return myId.toString(); } + public Long getVersion() { + return myVersion; + } + + /** + * @param theVersion This should only be populated if a specific version is needed. If you want the current version, + * leave this as null + */ + public void setVersion(Long theVersion) { + myVersion = theVersion; + } + public static List toLongList(Collection thePids) { List retVal = new ArrayList<>(thePids.size()); for (ResourcePersistentId next : thePids) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java index 2f5101242d0..57520bc2570 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/storage/TransactionDetails.java @@ -27,6 +27,7 @@ import com.google.common.collect.ListMultimap; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IIdType; +import javax.annotation.Nullable; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -48,7 +49,7 @@ import java.util.function.Supplier; public class TransactionDetails { private final Date myTransactionDate; - private Map myResolvedResourceIds = Collections.emptyMap(); + private Map myResolvedResourceIds = Collections.emptyMap(); private Map myUserData; private ListMultimap myDeferredInterceptorBroadcasts; private EnumSet myDeferredInterceptorBroadcastPointcuts; @@ -72,8 +73,10 @@ public class TransactionDetails { * "Observation/123") and a storage ID for that resource. Resources should only be placed within * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. */ - public Map getResolvedResourceIds() { - return myResolvedResourceIds; + @Nullable + public ResourcePersistentId getResolvedResourceId(IIdType theId) { + String idValue = theId.toVersionless().getValue(); + return myResolvedResourceIds.get(idValue); } /** @@ -88,7 +91,7 @@ public class TransactionDetails { if (myResolvedResourceIds.isEmpty()) { myResolvedResourceIds = new HashMap<>(); } - myResolvedResourceIds.put(theResourceId, thePersistentId); + myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId); } /** @@ -111,10 +114,11 @@ public class TransactionDetails { } /** - * Gets an arbitraty object that will last the lifetime of the current transaction + * Gets an arbitrary object that will last the lifetime of the current transaction * * @see #putUserData(String, Object) */ + @SuppressWarnings("unchecked") public T getUserData(String theKey) { if (myUserData != null) { return (T) myUserData.get(theKey); @@ -126,6 +130,7 @@ public class TransactionDetails { * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and * puts that in the map, and returns it */ + @SuppressWarnings("unchecked") public T getOrCreateUserData(String theKey, Supplier theSupplier) { T retVal = (T) getUserData(theKey); if (retVal == null) { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java index 15fb56f4699..ae333029a1e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java @@ -2124,7 +2124,7 @@ public class GenericClientR4Test extends BaseGenericClientR4Test { }); BundleBuilder builder = new BundleBuilder(ourCtx); - builder.addCreateEntry(new Patient().setActive(true)); + builder.addTransactionCreateEntry(new Patient().setActive(true)); IBaseBundle outcome = client.transaction().withBundle(builder.getBundle()).execute(); assertNull(outcome); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java index 16d8a1f4a99..22bf634d525 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java @@ -143,7 +143,7 @@ public class BundleBuilderTest { Patient patient = new Patient(); patient.setActive(true); - builder.addCreateEntry(patient); + builder.addTransactionCreateEntry(patient); Bundle bundle = (Bundle) builder.getBundle(); ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); @@ -162,7 +162,7 @@ public class BundleBuilderTest { Patient patient = new Patient(); patient.setActive(true); - builder.addCreateEntry(patient).conditional("Patient?active=true"); + builder.addTransactionCreateEntry(patient).conditional("Patient?active=true"); Bundle bundle = (Bundle) builder.getBundle(); ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); diff --git a/pom.xml b/pom.xml index efc9b20b587..62f952961a8 100644 --- a/pom.xml +++ b/pom.xml @@ -738,8 +738,7 @@ 3.9 10.14.2.0 - 2.3.4 - 2.3.4 + 2.5.1 0.7.9 30.1-jre 2.8.5