Merge remote-tracking branch 'origin/master' into ng_20201218_survivorship_poc

This commit is contained in:
Nick Goupinets 2021-01-27 16:47:40 -05:00
commit 3be480c9f0
83 changed files with 1073 additions and 189 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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}

View File

@ -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();

View File

@ -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."

View File

@ -265,9 +265,11 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
*/
DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> 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();
}
}

View File

@ -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() {

View File

@ -260,6 +260,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
StopWatch w = new StopWatch();
preProcessResourceForStorage(theResource);
preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
@ -275,7 +276,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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);
}
}
@ -1130,6 +1131,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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) {
@ -1460,6 +1467,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
T resource = theResource;
preProcessResourceForStorage(resource);
preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
final ResourceTable entity;
@ -1530,6 +1538,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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;
}

View File

@ -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()));
}
}
}
/*
/**
* Verify that we're not storing a Bundle with a disallowed bundle type
*/
private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) {
if (theResource instanceof IBaseBundle) {
Set<String> 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<ResourceReferenceInfo> refs = t.getAllResourceReferences(theResource);
@ -114,17 +172,38 @@ public abstract class BaseStorageDao {
}
}
}
if ("Bundle".equals(type)) {
Set<String> 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<IBaseReference> 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<IBaseReference> extractReferencesToAutoVersion(FhirContext theFhirContext, ModelConfig theModelConfig, IBaseResource theResource) {
Map<IBaseReference, Object> references = Collections.emptyMap();
if (!theModelConfig.getAutoVersionReferenceAtPaths().isEmpty()) {
String resourceName = theFhirContext.getResourceType(theResource);
for (String nextPath : theModelConfig.getAutoVersionReferenceAtPathsByResourceType(resourceName)) {
List<IBaseReference> 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();
}
}

View File

@ -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 extends IBaseBundle> BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
@ -195,7 +199,7 @@ public abstract class BaseTransactionProcessor {
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> 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<IBaseReference> referencesToVersion = BaseStorageDao.extractReferencesToAutoVersion(myContext, myModelConfig, nextResource);
List<ResourceReferenceInfo> 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);
}

View File

@ -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<Bundle> {
@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);

View File

@ -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
*/
@ -578,6 +588,12 @@ public class SearchBuilder implements ISearchBuilder {
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<ResourcePersistentId> thePids, Collection<ResourcePersistentId> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation,
Map<ResourcePersistentId, Integer> thePosition) {
List<Long> myLongPersistentIds;
if (thePids.size() < getMaximumPageSize()) {
myLongPersistentIds = normalizeIdListForLastNInClause(ResourcePersistentId.toLongList(thePids));
} else {
myLongPersistentIds = ResourcePersistentId.toLongList(thePids);
Map<Long, Long> 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<Long> versionlessPids = ResourcePersistentId.toLongList(thePids);
if (versionlessPids.size() < getMaximumPageSize()) {
versionlessPids = normalizeIdListForLastNInClause(versionlessPids);
}
// -- get the resource from the searchView
Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(myLongPersistentIds);
Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(versionlessPids);
//-- preload all tags with tag definition if any
Map<ResourcePersistentId, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
ResourcePersistentId resourceId;
for (ResourceSearchView next : resourceSearchViewList) {
for (IBaseResourceEntity next : resourceSearchViewList) {
if (next.getDeleted() != null) {
continue;
}
Class<? extends IBaseResource> 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<? extends IBaseResource> 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<ResourcePersistentId, Collection<ResourceTag>> getResourceTagMap(Collection<ResourceSearchView> theResourceSearchViewList) {
private Map<Long, Collection<ResourceTag>> getResourceTagMap(Collection<? extends IBaseResourceEntity> theResourceSearchViewList) {
List<Long> 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<ResourcePersistentId, Collection<ResourceTag>> tagMap = new HashMap<>();
Map<Long, Collection<ResourceTag>> 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<ResourcePersistentId> nextRoundMatches = new ArrayList<>(theMatches);
HashSet<ResourcePersistentId> 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<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<ResourcePersistentId> nextPartition : partitions) {
TypedQuery<Long> q = theEntityManager.createQuery(sql, Long.class);
TypedQuery<?> q = theEntityManager.createQuery(sql, Object[].class);
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
List<Long> 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<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
for (Collection<ResourcePersistentId> nextPartition : partitions) {
TypedQuery<Long> 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<Long> 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<SearchQueryExecutor> myQueryList = new ArrayList<>();
private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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<IBaseResource> 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());
}
}
}

View File

@ -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."));
}
}

View File

@ -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."));
}
}

View File

@ -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."));
}
}

View File

@ -123,6 +123,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
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() {

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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<Date> myPeriodIndexEndOfTime;
private NormalizedQuantitySearchLevel myNormalizedQuantitySearchLevel;
private Set<String> myAutoVersionReferenceAtPaths = Collections.emptySet();
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
private boolean myRespectVersionsForSearchIncludes;
/**
* Constructor
@ -268,7 +276,7 @@ public class ModelConfig {
}
HashSet<String> treatBaseUrlsAsLocal = new HashSet<>();
for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) {
for (String next : defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) {
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. <code>"Observation.subject"</code>), 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<String> getAutoVersionReferenceAtPaths() {
return myAutoVersionReferenceAtPaths;
}
/**
* When set with resource paths (e.g. <code>"Observation.subject"</code>), any references found at the given paths
* will automatically have versions appended. The version used will be the current version of the given resource.
* <p>
* 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.
* </p>
* <p>
* Note that for this setting to be useful, the {@link ParserOptions}
* {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths}
* option must also be set.
* </p>
*
* @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<String> paths = Collections.emptySet();
if (thePaths != null) {
paths = new HashSet<>(Arrays.asList(thePaths));
}
setAutoVersionReferenceAtPaths(paths);
}
/**
* When set with resource paths (e.g. <code>"Observation.subject"</code>), any references found at the given paths
* will automatically have versions appended. The version used will be the current version of the given resource.
* <p>
* 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.
* </p>
* <p>
* Note that for this setting to be useful, the {@link ParserOptions}
* {@link ParserOptions#getDontStripVersionsFromReferencesAtPaths() DontStripVersionsFromReferencesAtPaths}
* option must also be set
* </p>
*
* @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<String> thePaths) {
Set<String> paths = defaultIfNull(thePaths, Collections.emptySet());
Map<String, Set<String>> 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<String> getAutoVersionReferenceAtPathsByResourceType(String theResourceType) {
Validate.notEmpty(theResourceType, "theResourceType must not be null or empty");
Set<String> retVal = myTypeToAutoVersionReferenceAtPaths.get(theResourceType);
retVal = defaultIfNull(retVal, Collections.emptySet());
return retVal;
}
/**
* Should searches with <code>_include</code> 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 <code>_include</code> 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");

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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;
}

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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.
* %%

View File

@ -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) {

View File

@ -268,8 +268,8 @@ public class SearchParamExtractorService {
}
Class<? extends IBaseResource> 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);
}

View File

@ -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);

View File

@ -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 <code>null</code>
*/
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 <code>null</code>
*/
public void setVersion(Long theVersion) {
myVersion = theVersion;
}
public static List<Long> toLongList(Collection<ResourcePersistentId> thePids) {
List<Long> retVal = new ArrayList<>(thePids.size());
for (ResourcePersistentId next : thePids) {

View File

@ -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<IIdType, ResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
private Map<String, ResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
private Map<String, Object> myUserData;
private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
@ -72,8 +73,10 @@ public class TransactionDetails {
* "<code>Observation/123</code>") 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<IIdType, ResourcePersistentId> 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> 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> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) {
T retVal = (T) getUserData(theKey);
if (retVal == null) {

View File

@ -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);

View File

@ -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));

View File

@ -738,8 +738,7 @@
<commons_lang3_version>3.9</commons_lang3_version>
<derby_version>10.14.2.0</derby_version>
<!--<derby_version>10.15.1.3</derby_version>-->
<error_prone_annotations_version>2.3.4</error_prone_annotations_version>
<error_prone_core_version>2.3.4</error_prone_core_version>
<error_prone_core_version>2.5.1</error_prone_core_version>
<nullaway_version>0.7.9</nullaway_version>
<guava_version>30.1-jre</guava_version>
<gson_version>2.8.5</gson_version>