Optmize forced ID resolution (#1754)

* Add forced ID caching

* Work on id optimization

* Test fixes

* More optimization work

* Add some docs

* Work on docs

* Test fixes

* Test fixes

* Test fixes

* Address review comments

* Compile fix
This commit is contained in:
James Agnew 2020-03-12 17:12:50 -04:00 committed by GitHub
parent fe51c08168
commit 1dc5d89013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 895 additions and 315 deletions

View File

@ -83,6 +83,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionMissingUrl=Unable to perfor
ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionInvalidUrl=Unable to perform {0}, URL provided is invalid: {1}
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.cantValidateWithNoResource=No resource supplied for $validate operation (resource is required unless mode is \"delete\")
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.deleteBlockedBecauseDisabled=Resource deletion is not permitted on this server
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.duplicateCreateForcedId=Can not create entity with ID[{0}], a resource with this ID already exists
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithInvalidId=Can not process entity with ID[{0}], this is not a valid FHIR ID
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.incorrectResourceType=Incorrect resource type detected for endpoint, found {0} but expected {1}

View File

@ -43,6 +43,7 @@ page.server_jpa.get_started=Get Started ⚡
page.server_jpa.architecture=Architecture
page.server_jpa.configuration=Configuration
page.server_jpa.search=Search
page.server_jpa.performance=Performance
page.server_jpa.upgrading=Upgrade Guide
section.interceptors.title=Interceptors

View File

@ -0,0 +1,11 @@
# Performance
This page contains information for performance optimization.
# Bulk Loading
On servers where a large amount of data will be ingested, the following considerations may be helpful:
* Optimize your database thread pool count and HTTP client thread count: Every environment will have a different optimal setting for the number of concurrent writes that are permitted, and the maximum number of database connections allowed.
* Disable deletes: If the JPA server is configured to have the FHIR delete operation disabled, it is able to skip some resource reference deletion checks during resource creation, which can have a measurable improvement to performance over large datasets.

View File

@ -1146,7 +1146,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
}
}
// Syncrhonize composite params
// Synchronize composite params
mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, entity, existingParams);
}
}

View File

@ -181,8 +181,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) {
validateIdPresentForDelete(theId);
validateDeleteEnabled();
final ResourceTable entity = readEntityLatestVersion(theId, theRequest);
final ResourceTable entity = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
}
@ -258,6 +259,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
validateIdPresentForDelete(theId);
validateDeleteEnabled();
DeleteConflictList deleteConflicts = new DeleteConflictList();
if (isNotBlank(theId.getValue())) {
@ -280,6 +282,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
*/
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) {
validateDeleteEnabled();
StopWatch w = new StopWatch();
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType, theRequest);
@ -355,6 +359,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) {
validateDeleteEnabled();
DeleteConflictList deleteConflicts = new DeleteConflictList();
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails);
@ -364,6 +370,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return outcome;
}
private void validateDeleteEnabled() {
if (!myDaoConfig.isDeleteEnabled()) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "deleteBlockedBecauseDisabled");
throw new PreconditionFailedException(msg);
}
}
private void validateIdPresentForDelete(IIdType theId) {
if (theId == null || !theId.hasIdPart()) {
throw new InvalidRequestException("Can not perform delete, no ID provided");
@ -683,7 +696,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaAdd(theMetaAdd, entity);
} else {
@ -715,7 +728,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theResourceId);
}
ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest);
ResourceTable latestVersion = readEntityLatestVersion(theResourceId);
if (latestVersion.getVersion() != entity.getVersion()) {
doMetaDelete(theMetaDel, entity);
} else {
@ -791,7 +804,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
} else {
entityToUpdate = readEntityLatestVersion(theId, theRequest);
entityToUpdate = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart()) {
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
@ -926,7 +939,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
validateResourceTypeAndThrowInvalidRequestException(theId);
ResourcePersistentId pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart(), theRequest);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
if (entity == null) {
@ -964,8 +977,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
return entity;
}
protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequest) {
ResourcePersistentId persistentId = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart(), theRequest);
protected ResourceTable readEntityLatestVersion(IIdType theId) {
ResourcePersistentId persistentId = myIdHelperService.resolveResourcePersistentIds(getResourceName(), theId.getIdPart());
ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId.getId());
if (entity == null) {
throw new ResourceNotFoundException(theId);
@ -1206,7 +1219,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
resourceId = theResource.getIdElement();
try {
entity = readEntityLatestVersion(resourceId, theRequest);
entity = readEntityLatestVersion(resourceId);
} catch (ResourceNotFoundException e) {
return doCreate(theResource, null, thePerformIndexing, new Date(), theRequest);
}
@ -1278,7 +1291,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (theId == null || theId.hasIdPart() == false) {
throw new InvalidRequestException("No ID supplied. ID is required when validating with mode=DELETE");
}
final ResourceTable entity = readEntityLatestVersion(theId, theRequest);
final ResourceTable entity = readEntityLatestVersion(theId);
// Validate that there are no resources pointing to the candidate that
// would prevent deletion

View File

@ -183,6 +183,11 @@ public class DaoConfig {
*/
private boolean myPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets;
/**
* @since 5.0.0
*/
private boolean myDeleteEnabled = true;
/**
* Constructor
*/
@ -1907,7 +1912,33 @@ public class DaoConfig {
setPreExpandValueSetsDefaultCount(Math.min(getPreExpandValueSetsDefaultCount(), getPreExpandValueSetsMaxCount()));
}
public enum StoreMetaSourceInformationEnum {
/**
* This setting should be disabled (set to <code>false</code>) on servers that are not allowing
* deletes. Default is <code>true</code>. If deletes are disabled, some checks for resource
* deletion can be skipped, which improves performance. This is particularly helpful when large
* amounts of data containing client-assigned IDs are being loaded, but it can also improve
* search performance.
*
* @since 5.0.0
*/
public void setDeleteEnabled(boolean theDeleteEnabled) {
myDeleteEnabled = theDeleteEnabled;
}
/**
* This setting should be disabled (set to <code>false</code>) on servers that are not allowing
* deletes. Default is <code>true</code>. If deletes are disabled, some checks for resource
* deletion can be skipped, which improves performance. This is particularly helpful when large
* amounts of data containing client-assigned IDs are being loaded, but it can also improve
* search performance.
*
* @since 5.0.0
*/
public boolean isDeleteEnabled() {
return myDeleteEnabled;
}
public enum StoreMetaSourceInformationEnum {
NONE(false, false),
SOURCE_URI(true, false),
REQUEST_ID(false, true),

View File

@ -20,16 +20,12 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
@ -37,8 +33,6 @@ import org.springframework.transaction.PlatformTransactionManager;
import java.util.Date;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class FhirResourceDaoSubscriptionDstu2 extends BaseHapiFhirResourceDao<Subscription> implements IFhirResourceDaoSubscription<Subscription> {
@Autowired
@ -56,7 +50,7 @@ public class FhirResourceDaoSubscriptionDstu2 extends BaseHapiFhirResourceDao<Su
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
ResourceTable entity = readEntityLatestVersion(theId);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -229,7 +229,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
StringParam idParm = (StringParam) idParam;
idParamValue = idParm.getValue();
}
pid = myIdHelperService.translateForcedIdToPid(theResourceName, idParamValue, theRequest);
// pid = myIdHelperService.translateForcedIdToPid_(theResourceName, idParamValue, theRequest);
}
ResourcePersistentId referencingPid = pid;
@ -282,7 +282,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (contextParts.length != 3 || "Patient".equals(contextParts[0]) == false || "$everything".equals(contextParts[2]) == false) {
throw new InvalidRequestException("Invalid context: " + theContext);
}
ResourcePersistentId pid = myIdHelperService.translateForcedIdToPid(contextParts[0], contextParts[1], theRequest);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(contextParts[0], contextParts[1]);
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);

View File

@ -266,7 +266,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myParams.get(IAnyResource.SP_RES_ID) != null) {
StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
ResourcePersistentId pid = myIdHelperService.translateForcedIdToPid(myResourceName, idParm.getValue(), theRequest);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(myResourceName, idParm.getValue());
if (myAlsoIncludePids == null) {
myAlsoIncludePids = new ArrayList<>(1);
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.data;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/*
* #%L
@ -35,8 +36,8 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)")
List<Long> findByForcedId(@Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN (:forced_id)")
List<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
@ -44,4 +45,37 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Modifying
@Query("DELETE FROM ForcedId t WHERE t.myId = :pid")
void deleteByPid(@Param("pid") Long theId);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("SELECT f.myForcedId, f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN ( :forced_id )")
Collection<Object[]> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
/**
* Warning: No DB index exists for this particular query, so it may not perform well
*
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("" +
"SELECT " +
" f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " +
"FROM ForcedId f " +
"JOIN ResourceTable t ON t.myId = f.myResourcePid " +
"WHERE f.myForcedId IN ( :forced_id )")
Collection<Object[]> findAndResolveByForcedIdWithNoType(@Param("forced_id") Collection<String> theForcedIds);
/**
* This method returns a Collection where each row is an element in the collection. Each element in the collection
* is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way.
*/
@Query("" +
"SELECT " +
" f.myResourceType, f.myResourcePid, f.myForcedId, t.myDeleted " +
"FROM ForcedId f " +
"JOIN ResourceTable t ON t.myId = f.myResourcePid " +
"WHERE f.myResourceType = :resource_type AND f.myForcedId IN ( :forced_id )")
Collection<Object[]> findAndResolveByForcedIdWithNoType(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedIds);
}

View File

@ -8,9 +8,11 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/*
* #%L
@ -63,4 +65,6 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
@Query("DELETE FROM ResourceTable t WHERE t.myId = :pid")
void deleteByPid(@Param("pid") Long theId);
@Query("SELECT t.myResourceType, t.myId, t.myDeleted FROM ResourceTable t WHERE t.myId IN (:pid)")
Collection<Object[]> findLookupFieldsByResourcePid(@Param("pid") List<Long> thePids);
}

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoSubscriptionDstu3 extends BaseHapiFhirResourceDao<Su
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
ResourceTable entity = readEntityLatestVersion(theId);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -29,7 +29,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -64,17 +64,16 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
private DaoRegistry myDaoRegistry;
@Override
public ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
ResourceTable target;
ResourcePersistentId valueOf;
String idPart = theNextId.getIdPart();
public IResourceLookup findTargetResource(RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
IResourceLookup resolvedResource;
String idPart = theSourceResourceId.getIdPart();
try {
valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, idPart, theRequest);
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, valueOf);
resolvedResource = myIdHelperService.resolveResourceIdentity(theTypeString, idPart, theRequest);
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
} catch (ResourceNotFoundException e) {
Optional<ResourcePersistentId> pidOpt = createPlaceholderTargetIfConfiguredToDoSo(theType, theReference, idPart);
if (!pidOpt.isPresent()) {
Optional<ResourceTable> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(theType, theReference, idPart);
if (!createdTableOpt.isPresent()) {
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
return null;
@ -82,43 +81,36 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
String resName = missingResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theNextPathsUnsplit);
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theSourcePath);
}
valueOf = pidOpt.get();
resolvedResource = createdTableOpt.get();
}
target = myEntityManager.find(ResourceTable.class, valueOf.getIdAsLong());
RuntimeResourceDefinition targetResourceDef = myContext.getResourceDefinition(theType);
if (target == null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " not found, specified in path: " + theNextPathsUnsplit);
ourLog.trace("Resolved resource of type {} as PID: {}", resolvedResource.getResourceType(), resolvedResource.getResourceId());
if (!theTypeString.equals(resolvedResource.getResourceType())) {
ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theTypeString, resolvedResource.getResourceType());
throw new UnprocessableEntityException("Resource contains reference to unknown resource ID " + theSourceResourceId.getValue());
}
ourLog.trace("Resource PID {} is of type {}", valueOf, target.getResourceType());
if (!theTypeString.equals(target.getResourceType())) {
ourLog.error("Resource {} with PID {} was not of type {}", target.getIdDt().getValue(), target.getId(), theTypeString);
throw new UnprocessableEntityException(
"Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType());
if (resolvedResource.getDeleted() != null) {
String resName = resolvedResource.getResourceType();
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " is deleted, specified in path: " + theSourcePath);
}
if (target.getDeleted() != null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " is deleted, specified in path: " + theNextPathsUnsplit);
}
if (!theNextSpDef.hasTargets() && theNextSpDef.getTargets().contains(theTypeString)) {
if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theTypeString)) {
return null;
}
return target;
return resolvedResource;
}
/**
* @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
*/
public <T extends IBaseResource> Optional<ResourcePersistentId> createPlaceholderTargetIfConfiguredToDoSo(Class<T> theType, IBaseReference theReference, @Nullable String theIdToAssignToPlaceholder) {
ResourcePersistentId valueOf = null;
public <T extends IBaseResource> Optional<ResourceTable> createPlaceholderTargetIfConfiguredToDoSo(Class<T> theType, IBaseReference theReference, @Nullable String theIdToAssignToPlaceholder) {
ResourceTable valueOf = null;
if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
@ -136,9 +128,9 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
if (theIdToAssignToPlaceholder != null) {
newResource.setId(resName + "/" + theIdToAssignToPlaceholder);
valueOf = placeholderResourceDao.update(newResource).getEntity().getPersistentId();
valueOf = ((ResourceTable) placeholderResourceDao.update(newResource).getEntity());
} else {
valueOf = placeholderResourceDao.create(newResource).getEntity().getPersistentId();
valueOf = ((ResourceTable) placeholderResourceDao.create(newResource).getEntity());
}
}

View File

@ -21,7 +21,8 @@ package ca.uhn.fhir.jpa.dao.index;
*/
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.util.AddRemoveCount;
import org.springframework.beans.factory.annotation.Autowired;
@ -36,23 +37,22 @@ import java.util.List;
@Service
public class DaoSearchParamSynchronizer {
@Autowired
private DaoConfig myDaoConfig;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private DaoConfig myDaoConfig;
public AddRemoveCount synchronizeSearchParamsToDatabase(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
AddRemoveCount retVal = new AddRemoveCount();
synchronize(theParams, theEntity, retVal, theParams.myStringParams, existingParams.myStringParams);
synchronize(theParams, theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams);
synchronize(theParams, theEntity,retVal, theParams.myNumberParams, existingParams.myNumberParams);
synchronize(theParams, theEntity,retVal, theParams.myQuantityParams, existingParams.myQuantityParams);
synchronize(theParams, theEntity,retVal, theParams.myDateParams, existingParams.myDateParams);
synchronize(theParams, theEntity,retVal, theParams.myUriParams, existingParams.myUriParams);
synchronize(theParams, theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams);
synchronize(theParams, theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams);
synchronize(theParams, theEntity, retVal, theParams.myDateParams, existingParams.myDateParams);
synchronize(theParams, theEntity, retVal, theParams.myUriParams, existingParams.myUriParams);
synchronize(theParams, theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams);
synchronize(theParams, theEntity,retVal, theParams.myLinks, existingParams.myLinks);
synchronize(theParams, theEntity, retVal, theParams.myLinks, existingParams.myLinks);
// make sure links are indexed
theEntity.setResourceLinks(theParams.myLinks);
@ -85,7 +85,7 @@ public class DaoSearchParamSynchronizer {
* "one delete + one insert" with "one update"
*
* @param theIndexesToRemove The rows that would be removed
* @param theIndexesToAdd The rows that would be added
* @param theIndexesToAdd The rows that would be added
*/
private <T extends BaseResourceIndex> void tryToReuseIndexEntities(List<T> theIndexesToRemove, List<T> theIndexesToAdd) {
for (int addIndex = 0; addIndex < theIndexesToAdd.size(); addIndex++) {
@ -107,8 +107,6 @@ public class DaoSearchParamSynchronizer {
}
<T> List<T> subtract(Collection<T> theSubtractFrom, Collection<T> theToSubtract) {
assert theSubtractFrom != theToSubtract;

View File

@ -22,132 +22,212 @@ package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import java.util.*;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* This class is used to convert between PIDs (the internal primary key for a particular resource as
* stored in the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE} table), and the
* public ID that a resource has.
* <p>
* These IDs are sometimes one and the same (by default, a resource that the server assigns the ID of
* <code>Patient/1</code> will simply use a PID of 1 and and ID of 1. However, they may also be different
* in cases where a forced ID is used (an arbitrary client-assigned ID).
* </p>
* <p>
* This service is highly optimized in order to minimize the number of DB calls as much as possible,
* since ID resolution is fundamental to many basic operations. This service returns either
* {@link IResourceLookup} or {@link ResourcePersistentId} depending on the method being called.
* The former involves an extra database join that the latter does not require, so selecting the
* right method here is important.
* </p>
*/
@Service
public class IdHelperService {
private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class);
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
protected IResourceTableDao myResourceTableDao;
@Autowired(required = true)
private DaoConfig myDaoConfig;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
private Cache<String, Long> myPersistentIdCache;
private Cache<String, IResourceLookup> myResourceLookupCache;
@PostConstruct
public void start() {
myPersistentIdCache = newCache();
myResourceLookupCache = newCache();
}
public void delete(ForcedId forcedId) {
myForcedIdDao.deleteByPid(forcedId.getId());
}
/**
* Given a resource type and ID, looks up the resource and returns a {@link IResourceLookup}. This
* object contains the internal PID for the resource and the resource deletion status, making it sufficient
* for persisting resource links between resources without adding any further database calls after the
* single one performed by this call.
*
* @throws ResourceNotFoundException If the ID can not be found
*/
@Nonnull
public ResourcePersistentId translateForcedIdToPid(IIdType theId, RequestDetails theRequestDetails) {
return translateForcedIdToPid(theId.getResourceType(), theId.getIdPart(), theRequestDetails);
}
/**
* @throws ResourceNotFoundException If the ID can not be found
*/
@Nonnull
public ResourcePersistentId translateForcedIdToPid(String theResourceName, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException {
public IResourceLookup resolveResourceIdentity(String theResourceName, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException {
// We only pass 1 input in so only 0..1 will come back
IdDt id = new IdDt(theResourceName, theResourceId);
List<ResourcePersistentId> matches = translateForcedIdToPids(myDaoConfig, myInterceptorBroadcaster, theRequestDetails, myForcedIdDao, Collections.singletonList(id));
Collection<IResourceLookup> matches = translateForcedIdToPids(theRequestDetails, Collections.singletonList(id));
assert matches.size() <= 1;
if (matches.isEmpty()) {
throw new ResourceNotFoundException(id);
}
return matches.get(0);
return matches.iterator().next();
}
public List<ResourcePersistentId> translateForcedIdToPids(Collection<IIdType> theId, RequestDetails theRequestDetails) {
return IdHelperService.translateForcedIdToPids(myDaoConfig, myInterceptorBroadcaster, theRequestDetails, myForcedIdDao, theId);
/**
* Given a resource type and ID, determines the internal persistent ID for the resource.
*
* @throws ResourceNotFoundException If the ID can not be found
*/
@Nonnull
public ResourcePersistentId resolveResourcePersistentIds(String theResourceType, String theId) {
Long retVal;
if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId)) {
if (myDaoConfig.isDeleteEnabled()) {
retVal = resolveResourceIdentity(theResourceType, theId);
} else {
String key = theResourceType + "/" + theId;
retVal = myPersistentIdCache.get(key, t -> resolveResourceIdentity(theResourceType, theId));
}
} else {
retVal = Long.parseLong(theId);
}
return new ResourcePersistentId(retVal);
}
private static List<ResourcePersistentId> translateForcedIdToPids(DaoConfig theDaoConfig, IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequest, IForcedIdDao theForcedIdDao, Collection<IIdType> theId) {
theId.forEach(id -> Validate.isTrue(id.hasIdPart()));
/**
* Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs
*/
@Nonnull
public List<ResourcePersistentId> resolveResourcePersistentIds(List<IIdType> theIds, RequestDetails theRequest) {
theIds.forEach(id -> Validate.isTrue(id.hasIdPart()));
if (theId.isEmpty()) {
if (theIds.isEmpty()) {
return Collections.emptyList();
}
List<ResourcePersistentId> retVal = new ArrayList<>();
ListMultimap<String, String> typeToIds = MultimapBuilder.hashKeys().arrayListValues().build();
for (IIdType nextId : theId) {
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(nextId)) {
retVal.add(new ResourcePersistentId(nextId.getIdPartAsLong()));
} else {
if (nextId.hasResourceType()) {
typeToIds.put(nextId.getResourceType(), nextId.getIdPart());
} else {
typeToIds.put("", nextId.getIdPart());
}
}
if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) {
theIds
.stream()
.filter(t -> isValidPid(t))
.map(t -> t.getIdPartAsLong())
.map(t -> new ResourcePersistentId(t))
.forEach(t -> retVal.add(t));
}
ListMultimap<String, String> typeToIds = organizeIdsByResourceType(theIds);
for (Map.Entry<String, Collection<String>> nextEntry : typeToIds.asMap().entrySet()) {
String nextResourceType = nextEntry.getKey();
Collection<String> nextIds = nextEntry.getValue();
if (isBlank(nextResourceType)) {
StorageProcessingMessage msg = new StorageProcessingMessage()
.setMessage("This search uses unqualified resource IDs (an ID without a resource type). This is less efficient than using a qualified type.");
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, msg);
JpaInterceptorBroadcaster.doCallHooks(theInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
theForcedIdDao
.findByForcedId(nextIds)
.stream()
.map(t->new ResourcePersistentId(t))
.forEach(t->retVal.add(t));
List<Long> views = myForcedIdDao.findByForcedId(nextIds);
views.forEach(t -> retVal.add(new ResourcePersistentId(t)));
} else {
theForcedIdDao
.findByTypeAndForcedId(nextResourceType, nextIds)
.stream()
.map(t->new ResourcePersistentId(t))
.forEach(t->retVal.add(t));
if (!myDaoConfig.isDeleteEnabled()) {
for (Iterator<String> idIterator = nextIds.iterator(); idIterator.hasNext(); ) {
String nextId = idIterator.next();
String key = nextResourceType + "/" + nextId;
Long nextCachedPid = myPersistentIdCache.getIfPresent(key);
if (nextCachedPid != null) {
idIterator.remove();
retVal.add(new ResourcePersistentId(nextCachedPid));
}
}
}
if (nextIds.size() > 0) {
Collection<Object[]> views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds);
for (Object[] nextView : views) {
String forcedId = (String) nextView[0];
Long pid = (Long) nextView[1];
retVal.add(new ResourcePersistentId(pid));
if (!myDaoConfig.isDeleteEnabled()) {
String key = nextResourceType + "/" + forcedId;
myPersistentIdCache.put(key, pid);
}
}
}
}
}
return retVal;
return retVal;
}
/**
* Given a persistent ID, returns the associated resource ID
*/
@Nonnull
public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, ResourcePersistentId theId) {
IIdType retVal = theCtx.getVersion().newIdType();
retVal.setValue(translatePidIdToForcedId(theResourceType, theId));
return retVal;
}
public String translatePidIdToForcedId(String theResourceType, ResourcePersistentId theId) {
private String translatePidIdToForcedId(String theResourceType, ResourcePersistentId theId) {
ForcedId forcedId = myForcedIdDao.findByResourcePid(theId.getIdAsLong());
if (forcedId != null) {
return forcedId.getResourceType() + '/' + forcedId.getForcedId();
@ -156,17 +236,139 @@ public class IdHelperService {
}
}
public static boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);
if (nextChar < '0' || nextChar > '9') {
return false;
private ListMultimap<String, String> organizeIdsByResourceType(Collection<IIdType> theIds) {
ListMultimap<String, String> typeToIds = MultimapBuilder.hashKeys().arrayListValues().build();
for (IIdType nextId : theIds) {
if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(nextId)) {
if (nextId.hasResourceType()) {
typeToIds.put(nextId.getResourceType(), nextId.getIdPart());
} else {
typeToIds.put("", nextId.getIdPart());
}
}
}
return true;
return typeToIds;
}
private Long resolveResourceIdentity(String theResourceType, String theId) {
Long retVal;
retVal = myForcedIdDao
.findByTypeAndForcedId(theResourceType, theId)
.orElseThrow(() -> new ResourceNotFoundException(new IdDt(theResourceType, theId)));
return retVal;
}
private Collection<IResourceLookup> translateForcedIdToPids(RequestDetails theRequest, Collection<IIdType> theId) {
theId.forEach(id -> Validate.isTrue(id.hasIdPart()));
if (theId.isEmpty()) {
return Collections.emptyList();
}
List<IResourceLookup> retVal = new ArrayList<>();
if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) {
List<Long> pids = theId
.stream()
.filter(t -> isValidPid(t))
.map(t -> t.getIdPartAsLong())
.collect(Collectors.toList());
if (!pids.isEmpty()) {
Collection<Object[]> lookups = myResourceTableDao.findLookupFieldsByResourcePid(pids);
for (Object[] next : lookups) {
String resourceType = (String) next[0];
Long resourcePid = (Long) next[1];
Date deletedAt = (Date) next[2];
retVal.add(new ResourceLookup(resourceType, resourcePid, deletedAt));
}
}
}
ListMultimap<String, String> typeToIds = organizeIdsByResourceType(theId);
for (Map.Entry<String, Collection<String>> nextEntry : typeToIds.asMap().entrySet()) {
String nextResourceType = nextEntry.getKey();
Collection<String> nextIds = nextEntry.getValue();
if (!myDaoConfig.isDeleteEnabled()) {
for (Iterator<String> forcedIdIterator = nextIds.iterator(); forcedIdIterator.hasNext(); ) {
String nextForcedId = forcedIdIterator.next();
String nextKey = nextResourceType + "/" + nextForcedId;
IResourceLookup cachedLookup = myResourceLookupCache.getIfPresent(nextKey);
if (cachedLookup != null) {
forcedIdIterator.remove();
retVal.add(cachedLookup);
}
}
}
if (nextIds.size() > 0) {
Collection<Object[]> views;
if (isBlank(nextResourceType)) {
warnAboutUnqualifiedForcedIdResolution(theRequest);
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextIds);
} else {
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds);
}
for (Object[] next : views) {
String resourceType = (String) next[0];
Long resourcePid = (Long) next[1];
String forcedId = (String) next[2];
Date deletedAt = (Date) next[3];
ResourceLookup lookup = new ResourceLookup(resourceType, resourcePid, deletedAt);
retVal.add(lookup);
if (!myDaoConfig.isDeleteEnabled()) {
String key = resourceType + "/" + forcedId;
myResourceLookupCache.put(key, lookup);
}
}
}
}
return retVal;
}
private void warnAboutUnqualifiedForcedIdResolution(RequestDetails theRequest) {
StorageProcessingMessage msg = new StorageProcessingMessage()
.setMessage("This search uses unqualified resource IDs (an ID without a resource type). This is less efficient than using a qualified type.");
ourLog.debug(msg.getMessage());
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(StorageProcessingMessage.class, msg);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
}
public void clearCache() {
myPersistentIdCache.invalidateAll();
myResourceLookupCache.invalidateAll();
}
private <T, V> @NonNull Cache<T, V> newCache() {
return Caffeine
.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
public static boolean isValidPid(IIdType theId) {
if (theId == null) {
return false;
}
String idPart = theId.getIdPart();
return isValidPid(idPart);
}
public static boolean isValidPid(String theIdPart) {
return StringUtils.isNumeric(theIdPart);
}
}

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
@ -187,7 +188,9 @@ public class SearchParamWithInlineReferencesExtractor {
if (linksForCompositePart != null) {
for (ResourceLink nextLink : linksForCompositePart) {
if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
assert isNotBlank(nextLink.getTargetResourceType());
assert isNotBlank(nextLink.getTargetResourceId());
String value = nextLink.getTargetResourceType() + "/" + nextLink.getTargetResourceId();
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
@ -250,13 +253,14 @@ public class SearchParamWithInlineReferencesExtractor {
ResourcePersistentId match;
if (matches.isEmpty()) {
Optional<ResourcePersistentId> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null);
Optional<ResourceTable> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null);
if (placeholderOpt.isPresent()) {
match = placeholderOpt.get();
match = new ResourcePersistentId(placeholderOpt.get().getResourceId());
} else {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(msg);
}
} else if (matches.size() > 1) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(msg);
@ -264,9 +268,9 @@ public class SearchParamWithInlineReferencesExtractor {
match = matches.iterator().next();
}
String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, match);
IIdType newId = myIdHelperService.translatePidIdToForcedId(myContext, resourceTypeString, match);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
nextRef.setReference(newId);
nextRef.setReference(newId.getValue());
}
}
}

View File

@ -149,7 +149,7 @@ class PredicateBuilderReference extends BasePredicateBuilder {
List<Predicate> codePredicates = new ArrayList<>();
// Resources by ID
List<ResourcePersistentId> targetPids = myIdHelperService.translateForcedIdToPids(targetIds, theRequest);
List<ResourcePersistentId> targetPids = myIdHelperService.resolveResourcePersistentIds(targetIds, theRequest);
if (!targetPids.isEmpty()) {
ourLog.debug("Searching for resource link with target PIDs: {}", targetPids);
Predicate pathPredicate;

View File

@ -60,7 +60,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder {
@Nullable
Predicate addPredicateResourceId(List<List<IQueryParameterType>> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest) {
Predicate nextPredicate = createPredicate(myQueryRoot.getRoot(), theResourceName, theValues, theOperation, theRequest);
Predicate nextPredicate = createPredicate(myQueryRoot.getRoot(), theResourceName, theValues, theOperation);
if (nextPredicate != null) {
myQueryRoot.addPredicate(nextPredicate);
@ -71,7 +71,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder {
}
@Nullable
private Predicate createPredicate(Root<ResourceTable> theRoot, String theResourceName, List<List<IQueryParameterType>> theValues, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest) {
private Predicate createPredicate(Root<ResourceTable> theRoot, String theResourceName, List<List<IQueryParameterType>> theValues, SearchFilterParser.CompareOperation theOperation) {
Predicate nextPredicate = null;
Set<ResourcePersistentId> allOrPids = null;
@ -89,7 +89,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder {
if (isNotBlank(value)) {
haveValue = true;
try {
ResourcePersistentId pid = myIdHelperService.translateForcedIdToPid(theResourceName, valueAsId.getIdPart(), theRequest);
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(theResourceName, valueAsId.getIdPart());
orPids.add(pid);
} catch (ResourceNotFoundException e) {
// This is not an error in a search, it just results in no matchesFhirResourceDaoR4InterceptorTest

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoSubscriptionR4 extends BaseHapiFhirResourceDao<Subsc
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
ResourceTable entity = readEntityLatestVersion(theId);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoSubscriptionR5 extends BaseHapiFhirResourceDao<Subsc
@Override
public Long getSubscriptionTablePidForSubscriptionResource(IIdType theId, RequestDetails theRequest) {
ResourceTable entity = readEntityLatestVersion(theId, theRequest);
ResourceTable entity = readEntityLatestVersion(theId);
SubscriptionTable table = mySubscriptionTableDao.findOneByResourcePid(entity.getId());
if (table == null) {
return null;

View File

@ -117,11 +117,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
@Override
public ResourcePersistentId getValueSetResourcePid(IIdType theIdType) {
return getValueSetResourcePid(theIdType, null);
}
private ResourcePersistentId getValueSetResourcePid(IIdType theIdType, RequestDetails theRequestDetails) {
return myIdHelperService.translateForcedIdToPid(theIdType, theRequestDetails);
return myIdHelperService.resolveResourcePersistentIds(theIdType.getResourceType(), theIdType.getIdPart());
}
@Transactional
@ -295,7 +291,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
Validate.notBlank(theCodeSystemResource.getUrl(), "theCodeSystemResource must have a URL");
IIdType csId = myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(theCodeSystemResource);
ResourcePersistentId codeSystemResourcePid = myIdHelperService.translateForcedIdToPid(csId, theRequest);
ResourcePersistentId codeSystemResourcePid = myIdHelperService.resolveResourcePersistentIds(csId.getResourceType(), csId.getIdPart());
ResourceTable resource = myResourceTableDao.getOne(codeSystemResourcePid.getIdAsLong());
ourLog.info("CodeSystem resource has ID: {}", csId.getValue());
@ -555,11 +551,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
}
private ResourcePersistentId getCodeSystemResourcePid(IIdType theIdType) {
return getCodeSystemResourcePid(theIdType, null);
}
private ResourcePersistentId getCodeSystemResourcePid(IIdType theIdType, RequestDetails theRequestDetails) {
return myIdHelperService.translateForcedIdToPid(theIdType, theRequestDetails);
return myIdHelperService.resolveResourcePersistentIds(theIdType.getResourceType(), theIdType.getIdPart());
}
private void persistChildren(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap<TermConcept, Object> theConceptsStack, int theTotalConcepts) {

View File

@ -28,6 +28,7 @@ import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.hibernate.annotations.Subselect;
import org.hibernate.validator.constraints.Length;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.InstantType;
@ -39,7 +40,11 @@ import java.io.InputStream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Ascii.toUpperCase;
@ -96,7 +101,10 @@ public class TestUtil {
private static void scanClass(Set<String> theNames, Class<?> theClazz, boolean theIsSuperClass) {
ourLog.info("Scanning: {}", theClazz.getSimpleName());
scan(theClazz, theNames, theIsSuperClass);
Subselect subselect = theClazz.getAnnotation(Subselect.class);
boolean isView = (subselect != null);
scan(theClazz, theNames, theIsSuperClass, isView);
for (Field nextField : theClazz.getDeclaredFields()) {
if (Modifier.isStatic(nextField.getModifiers())) {
@ -104,7 +112,7 @@ public class TestUtil {
}
ourLog.info(" * Scanning field: {}", nextField.getName());
scan(nextField, theNames, theIsSuperClass);
scan(nextField, theNames, theIsSuperClass, isView);
Lob lobClass = nextField.getAnnotation(Lob.class);
if (lobClass != null) {
@ -140,7 +148,7 @@ public class TestUtil {
scanClass(theNames, theClazz.getSuperclass(), true);
}
private static void scan(AnnotatedElement theAnnotatedElement, Set<String> theNames, boolean theIsSuperClass) {
private static void scan(AnnotatedElement theAnnotatedElement, Set<String> theNames, boolean theIsSuperClass, boolean theIsView) {
Table table = theAnnotatedElement.getAnnotation(Table.class);
if (table != null) {
@ -198,7 +206,7 @@ public class TestUtil {
*/
if (field.getType().equals(String.class)) {
if (!hasLob) {
if (column.length() == 255) {
if (!theIsView && column.length() == 255) {
throw new IllegalStateException("Field does not have an explicit maximum length specified: " + field);
}
if (column.length() > MAX_COL_LENGTH) {

View File

@ -426,7 +426,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
myPatientDao.create(p, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Resource contains reference to Organization/" + id1.getIdPart() + " but resource with ID " + id1.getIdPart() + " is actually of type Observation", e.getMessage());
assertEquals("Resource contains reference to unknown resource ID Organization/" + id1.getIdPart(), e.getMessage());
}
// Now with a forced ID

View File

@ -34,6 +34,7 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
@ -488,7 +489,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
assertEquals(2, results.size());
List<IIdType> actual = toUnqualifiedVersionlessIds(
mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam((ParamPrefixEnum) null, 123, "http://foo", "UNIT"))));
mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, "http://foo", "UNIT"))));
assertThat(actual, contains(id));
}
@ -1585,7 +1586,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
dr01.setSubject(new Reference(patientId01));
IIdType drId01 = myDiagnosticReportDao.create(dr01, mySrd).getId();
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", new Object[]{patientId01, patientId02, obsId01, obsId02, drId01});
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01);
List<Observation> result = toList(myObservationDao
.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "urn:system|testSearchResourceLinkWithChain01"))));
@ -1690,7 +1691,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
Date after = new Date();
ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick();
ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", new Object[]{patientId01, locId01, obsId01, obsId02});
ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", patientId01, locId01, obsId01, obsId02);
List<IIdType> result;
SearchParameterMap params;
@ -1753,7 +1754,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
dr01.setSubject(new Reference(patientId01));
IIdType drId01 = myDiagnosticReportDao.create(dr01, mySrd).getId();
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", new Object[]{patientId01, patientId02, obsId01, obsId02, drId01});
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01);
List<Observation> result = toList(
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01"))));
@ -2844,7 +2845,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
ourLog.info("Initial size: " + value.size());
for (IBaseResource next : value.getResources(0, value.size())) {
ourLog.info("Deleting: {}", next.getIdElement());
myDeviceDao.delete((IIdType) next.getIdElement(), mySrd);
myDeviceDao.delete(next.getIdElement(), mySrd);
}
value = myDeviceDao.search(new SearchParameterMap());
@ -2863,7 +2864,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
public void testSearchWithRevIncludes() {
final String methodName = "testSearchWithRevIncludes";
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionMgr);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
IIdType pid = txTemplate.execute(new TransactionCallback<IIdType>() {
@Override
@ -2881,7 +2882,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
});
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_RES_ID, new StringParam(pid.getIdPart()));
map.add(IAnyResource.SP_RES_ID, new StringParam(pid.getIdPart()));
map.addRevInclude(Condition.INCLUDE_PATIENT);
IBundleProvider results = myPatientDao.search(map);
List<IBaseResource> foundResources = results.getResources(0, results.size());

View File

@ -700,7 +700,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
myPatientDao.create(p, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Resource contains reference to Organization/" + id1.getIdPart() + " but resource with ID " + id1.getIdPart() + " is actually of type Observation", e.getMessage());
assertEquals("Resource contains reference to unknown resource ID Organization/" + id1.getIdPart(), e.getMessage());
}
// Now with a forced ID

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
@ -352,6 +353,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
private DaoRegistry myDaoRegistry;
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Autowired
private IdHelperService myIdHelperService;
@After()
public void afterCleanupDao() {
@ -380,6 +383,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc);
termDeferredStorageSvc.clearDeferred();
myIdHelperService.clearCache();
}
@After()

View File

@ -1,12 +1,18 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.slf4j.Logger;
@ -14,11 +20,20 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4DeleteTest.class);
@After
public void after() {
myDaoConfig.setDeleteEnabled(new DaoConfig().isDeleteEnabled());
}
@Test
public void testDeleteMarksResourceAndVersionAsDeleted() {
@ -29,19 +44,19 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
myPatientDao.delete(id);
// Table should be marked as deleted
runInTransaction(()->{
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(id.getIdPartAsLong()).get();
assertNotNull(resourceTable.getDeleted());
assertTrue(resourceTable.isDeleted());
});
// Current version should be marked as deleted
runInTransaction(()->{
runInTransaction(() -> {
ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 1);
assertNull(resourceTable.getDeleted());
assertNotNull(resourceTable.getPersistentId());
});
runInTransaction(()->{
runInTransaction(() -> {
ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 2);
assertNotNull(resourceTable.getDeleted());
});
@ -66,6 +81,23 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
}
@Test
public void testDeleteDisabled() {
myDaoConfig.setDeleteEnabled(false);
Patient p = new Patient();
p.setActive(true);
IIdType pId = myPatientDao.create(p).getId().toUnqualifiedVersionless();
try {
myPatientDao.delete(pId);
fail();
} catch (PreconditionFailedException e) {
assertEquals("Resource deletion is not permitted on this server", e.getMessage());
}
}
@Test
public void testDeleteCircularReferenceInTransaction() throws IOException {
@ -157,14 +189,14 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
myPatientDao.delete(id);
// Table should be marked as deleted
runInTransaction(()->{
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(id.getIdPartAsLong()).get();
assertNotNull(resourceTable.getDeleted());
});
// Mark the current history version as not-deleted even though the actual resource
// table entry is marked deleted
runInTransaction(()->{
runInTransaction(() -> {
ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 2);
resourceTable.setDeleted(null);
myResourceHistoryTableDao.save(resourceTable);

View File

@ -1,29 +1,20 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.*;
import org.springframework.test.context.TestPropertySource;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.reset;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@ -32,6 +23,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
public void afterResetDao() {
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
myDaoConfig.setDeleteEnabled(new DaoConfig().isDeleteEnabled());
}
@Before
@ -53,7 +45,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
Patient p = new Patient();
p.setId(id.getIdPart());
p.addIdentifier().setSystem("urn:system").setValue("2");
myPatientDao.update(p).getResource();
myPatientDao.update(p);
});
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(5, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
@ -164,6 +156,177 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
}
@Test
public void testReferenceToForcedId() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myCaptureQueriesListener.clear();
myPatientDao.update(patient);
/*
* Add a resource with a forced ID target link
*/
myCaptureQueriesListener.clear();
Observation observation = new Observation();
observation.getSubject().setReference("Patient/P");
myObservationDao.create(observation);
myCaptureQueriesListener.logAllQueriesForCurrentThread();
// select: lookup forced ID
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// insert to: HFJ_RESOURCE, HFJ_RES_VER, HFJ_RES_LINK
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
/*
* Add another
*/
myCaptureQueriesListener.clear();
observation = new Observation();
observation.getSubject().setReference("Patient/P");
myObservationDao.create(observation);
// select: lookup forced ID
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// insert to: HFJ_RESOURCE, HFJ_RES_VER, HFJ_RES_LINK
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
}
@Test
public void testReferenceToForcedId_DeletesDisabled() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myDaoConfig.setDeleteEnabled(false);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myCaptureQueriesListener.clear();
myPatientDao.update(patient);
/*
* Add a resource with a forced ID target link
*/
myCaptureQueriesListener.clear();
Observation observation = new Observation();
observation.getSubject().setReference("Patient/P");
myObservationDao.create(observation);
myCaptureQueriesListener.logAllQueriesForCurrentThread();
// select: lookup forced ID
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// insert to: HFJ_RESOURCE, HFJ_RES_VER, HFJ_RES_LINK
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
/*
* Add another
*/
myCaptureQueriesListener.clear();
observation = new Observation();
observation.getSubject().setReference("Patient/P");
myObservationDao.create(observation);
// select: no lookups needed because of cache
assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// insert to: HFJ_RESOURCE, HFJ_RES_VER, HFJ_RES_LINK
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
}
@Test
public void testSearchUsingForcedIdReference() {
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
Observation obs = new Observation();
obs.getSubject().setReference("Patient/P");
myObservationDao.update(obs);
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("subject", new ReferenceParam("Patient/P"));
myCaptureQueriesListener.clear();
assertEquals(1, myObservationDao.search(map).size().intValue());
// myCaptureQueriesListener.logAllQueriesForCurrentThread();
// Resolve forced ID, Perform search, load result
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
/*
* Again
*/
myCaptureQueriesListener.clear();
assertEquals(1, myObservationDao.search(map).size().intValue());
myCaptureQueriesListener.logAllQueriesForCurrentThread();
// Resolve forced ID, Perform search, load result
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@Test
public void testSearchUsingForcedIdReference_DeletedDisabled() {
myDaoConfig.setDeleteEnabled(false);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
Observation obs = new Observation();
obs.getSubject().setReference("Patient/P");
myObservationDao.update(obs);
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("subject", new ReferenceParam("Patient/P"));
myCaptureQueriesListener.clear();
assertEquals(1, myObservationDao.search(map).size().intValue());
myCaptureQueriesListener.logAllQueriesForCurrentThread();
// Resolve forced ID, Perform search, load result
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
/*
* Again
*/
myCaptureQueriesListener.clear();
assertEquals(1, myObservationDao.search(map).size().intValue());
myCaptureQueriesListener.logAllQueriesForCurrentThread();
// (NO resolve forced ID), Perform search, load result
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@AfterClass
public static void afterClassClearContext() {

View File

@ -910,7 +910,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
myPatientDao.create(p, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Resource contains reference to Organization/" + id1.getIdPart() + " but resource with ID " + id1.getIdPart() + " is actually of type Observation", e.getMessage());
assertEquals("Resource contains reference to unknown resource ID Organization/" + id1.getIdPart(), e.getMessage());
}
// Now with a forced ID

View File

@ -0,0 +1,11 @@
package ca.uhn.fhir.jpa.model.cross;
import java.util.Date;
public interface IResourceLookup {
String getResourceType();
Long getResourceId();
Date getDeleted();
}

View File

@ -0,0 +1,30 @@
package ca.uhn.fhir.jpa.model.cross;
import java.util.Date;
public class ResourceLookup implements IResourceLookup {
private final String myResourceType;
private final Long myResourcePid;
private final Date myDeletedAt;
public ResourceLookup(String theResourceType, Long theResourcePid, Date theDeletedAt) {
myResourceType = theResourceType;
myResourcePid = theResourcePid;
myDeletedAt = theDeletedAt;
}
@Override
public String getResourceType() {
return myResourceType;
}
@Override
public Long getResourceId() {
return myResourcePid;
}
@Override
public Date getDeleted() {
return myDeletedAt;
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.util.ObjectUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* This class is an abstraction for however primary keys are stored in the underlying storage engine. This might be
@ -35,6 +36,7 @@ public class ResourcePersistentId {
private Object myId;
public ResourcePersistentId(Object theId) {
assert !(theId instanceof Optional);
myId = theId;
}

View File

@ -60,30 +60,40 @@ public class ResourceLink extends BaseResourceIndex {
private String mySourceResourceType;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = true, foreignKey = @ForeignKey(name = "FK_RESLINK_TARGET"))
@JoinColumn(name = "TARGET_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = true, insertable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_RESLINK_TARGET"))
private ResourceTable myTargetResource;
@Column(name = "TARGET_RESOURCE_ID", insertable = false, updatable = false, nullable = true)
@Column(name = "TARGET_RESOURCE_ID", insertable = true, updatable = true, nullable = true)
@Field()
private Long myTargetResourcePid;
@Column(name = "TARGET_RESOURCE_TYPE", nullable = false, length = ResourceTable.RESTYPE_LEN)
@Field()
private String myTargetResourceType;
@Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true)
@Field()
private String myTargetResourceUrl;
@Field()
@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;
public ResourceLink() {
super();
}
public String getTargetResourceId() {
if (myTargetResourceId == null && myTargetResource != null) {
myTargetResourceId = myTargetResource.getIdDt().getIdPart();
}
return myTargetResourceId;
}
public String getTargetResourceType() {
return myTargetResourceType;
}
@Override
public boolean equals(Object theObj) {
if (this == theObj) {
@ -99,8 +109,9 @@ public class ResourceLink extends BaseResourceIndex {
EqualsBuilder b = new EqualsBuilder();
b.append(mySourcePath, obj.mySourcePath);
b.append(mySourceResource, obj.mySourceResource);
b.append(myTargetResourcePid, obj.myTargetResourcePid);
b.append(myTargetResourceUrl, obj.myTargetResourceUrl);
b.append(myTargetResourceType, obj.myTargetResourceType);
b.append(getTargetResourceId(), obj.getTargetResourceId());
return b.isEquals();
}
@ -122,15 +133,12 @@ public class ResourceLink extends BaseResourceIndex {
mySourceResourceType = theSourceResource.getResourceType();
}
public ResourceTable getTargetResource() {
return myTargetResource;
}
public void setTargetResource(String theResourceType, Long theResourcePid, String theTargetResourceId) {
Validate.notBlank(theResourceType);
public void setTargetResource(ResourceTable theTargetResource) {
Validate.notNull(theTargetResource);
myTargetResource = theTargetResource;
myTargetResourcePid = theTargetResource.getId();
myTargetResourceType = theTargetResource.getResourceType();
myTargetResourceType = theResourceType;
myTargetResourcePid = theResourcePid;
myTargetResourceId = theTargetResourceId;
}
public Long getTargetResourcePid() {
@ -189,8 +197,9 @@ public class ResourceLink extends BaseResourceIndex {
HashCodeBuilder b = new HashCodeBuilder();
b.append(mySourcePath);
b.append(mySourceResource);
b.append(myTargetResourcePid);
b.append(myTargetResourceUrl);
b.append(getTargetResourceType());
b.append(getTargetResourceId());
return b.toHashCode();
}
@ -228,11 +237,11 @@ public class ResourceLink extends BaseResourceIndex {
return retVal;
}
public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, ResourceTable theTargetResource, Date theUpdated) {
public static ResourceLink forLocalReference(String theSourcePath, ResourceTable theSourceResource, String theTargetResourceType, Long theTargetResourcePid, String theTargetResourceId, Date theUpdated) {
ResourceLink retVal = new ResourceLink();
retVal.setSourcePath(theSourcePath);
retVal.setSourceResource(theSourceResource);
retVal.setTargetResource(theTargetResource);
retVal.setTargetResource(theTargetResourceType, theTargetResourcePid, theTargetResourceId);
retVal.setUpdated(theUpdated);
return retVal;
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.model.entity;
*/
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.search.IndexNonDeletedInterceptor;
import ca.uhn.fhir.model.primitive.IdDt;
@ -29,12 +30,20 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hibernate.annotations.OptimisticLock;
import org.hibernate.search.annotations.*;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Analyzer;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Fields;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
import javax.persistence.Index;
import javax.persistence.*;
import java.io.Serializable;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -48,7 +57,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
@Index(name = "IDX_RES_TYPE", columnList = "RES_TYPE"),
@Index(name = "IDX_INDEXSTATUS", columnList = "SP_INDEX_STATUS")
})
public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource {
public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource, IResourceLookup {
public static final int RESTYPE_LEN = 40;
private static final int MAX_LANGUAGE_LENGTH = 20;
private static final int MAX_PROFILE_LENGTH = 200;
@ -626,7 +635,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
@Override
public IdDt getIdDt() {
if (getForcedId() == null) {
Long id = getResourceId();
Long id = this.getResourceId();
return new IdDt(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
} else {
// Avoid a join query if possible

View File

@ -21,14 +21,31 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
*/
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public interface IResourceLinkResolver {
ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest);
/**
* This method resolves the target of a reference found within a resource that is being created/updated. We do this
* so that we can create indexed links between resources, and so that we can validate that the target actually
* exists in cases where we need to check that.
* <p>
* This method returns an {@link IResourceLookup} so as to avoid needing to resolve the entire resource.
*
* @param theSearchParam The param that is being indexed
* @param theSourcePath The path within the resource where this reference was found
* @param theSourceResourceId The ID of the resource containing the reference to the target being resolved
* @param theTypeString The type of the resource being resolved
* @param theType The resource type of the target
* @param theReference The reference being resolved
* @param theRequest The incoming request, if any
*/
IResourceLookup findTargetResource(RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest);
void validateTypeOrThrowException(Class<? extends IBaseResource> theType);
}

View File

@ -23,16 +23,17 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.DependsOn;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import static org.apache.commons.lang3.StringUtils.compare;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public final class ResourceIndexedSearchParams {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceIndexedSearchParams.class);
@ -217,7 +218,7 @@ public final class ResourceIndexedSearchParams {
return myPopulatedResourceLinkParameters;
}
public boolean matchParam(String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
if (theParamDef == null) {
return false;
}
@ -242,7 +243,7 @@ public final class ResourceIndexedSearchParams {
resourceParams = myDateParams;
break;
case REFERENCE:
return matchResourceLinks(theResourceName, theParamName, theParam, theParamDef.getPath());
return matchResourceLinks(theModelConfig, theResourceName, theParamName, theParam, theParamDef.getPath());
case COMPOSITE:
case HAS:
case SPECIAL:
@ -259,39 +260,56 @@ public final class ResourceIndexedSearchParams {
return resourceParams.stream().anyMatch(namedParamPredicate);
}
/**
* @deprecated Replace with the method below
*/
// KHS This needs to be public as libraries outside of hapi call it directly
@Deprecated
public boolean matchResourceLinks(String theResourceName, String theParamName, IQueryParameterType theParam, String theParamPath) {
return matchResourceLinks(new ModelConfig(), theResourceName, theParamName, theParam, theParamPath);
}
// KHS This needs to be public as libraries outside of hapi call it directly
public boolean matchResourceLinks(ModelConfig theModelConfig, String theResourceName, String theParamName, IQueryParameterType theParam, String theParamPath) {
ReferenceParam reference = (ReferenceParam)theParam;
Predicate<ResourceLink> namedParamPredicate = resourceLink ->
resourceLinkMatches(theResourceName, resourceLink, theParamName, theParamPath)
&& resourceIdMatches(resourceLink, reference);
searchParameterPathMatches(theResourceName, resourceLink, theParamName, theParamPath)
&& resourceIdMatches(theModelConfig, resourceLink, reference);
return myLinks.stream().anyMatch(namedParamPredicate);
}
private boolean resourceIdMatches(ResourceLink theResourceLink, ReferenceParam theReference) {
ResourceTable target = theResourceLink.getTargetResource();
IdDt idDt = target.getIdDt();
if (idDt.isIdPartValidLong()) {
if (theReference.isIdPartValidLong()) {
return theReference.getIdPartAsLong().equals(idDt.getIdPartAsLong());
} else {
return false;
}
} else {
ForcedId forcedId = target.getForcedId();
if (forcedId != null) {
return forcedId.getForcedId().equals(theReference.getValue());
} else {
private boolean resourceIdMatches(ModelConfig theModelConfig, ResourceLink theResourceLink, ReferenceParam theReference) {
String baseUrl = theReference.getBaseUrl();
if (isNotBlank(baseUrl)) {
if (!theModelConfig.getTreatBaseUrlsAsLocal().contains(baseUrl)) {
return false;
}
}
String targetType = theResourceLink.getTargetResourceType();
String targetId = theResourceLink.getTargetResourceId();
assert isNotBlank(targetType);
assert isNotBlank(targetId);
if (theReference.hasResourceType()) {
if (!theReference.getResourceType().equals(targetType)) {
return false;
}
}
if (!targetId.equals(theReference.getIdPart())) {
return false;
}
return true;
}
private boolean resourceLinkMatches(String theResourceName, ResourceLink theResourceLink, String theParamName, String theParamPath) {
return theResourceLink.getTargetResource().getResourceType().equalsIgnoreCase(theParamName) ||
theResourceLink.getSourcePath().equalsIgnoreCase(theParamPath);
private boolean searchParameterPathMatches(String theResourceName, ResourceLink theResourceLink, String theParamName, String theParamPath) {
String sourcePath = theResourceLink.getSourcePath();
return sourcePath.equalsIgnoreCase(theParamPath);
}
@Override

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
@ -39,6 +40,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -63,15 +66,17 @@ public class ResourceLinkExtractor {
ISearchParamExtractor.SearchParamSet<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource);
SearchParamExtractorService.handleWarnings(theRequest, myInterceptorBroadcaster, refs);
Map<String, IResourceLookup> resourceIdToResolvedTarget = new HashMap<>();
for (PathAndRef nextPathAndRef : refs) {
RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(resourceName, nextPathAndRef.getSearchParamName());
extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest);
extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, searchParam, nextPathAndRef, theFailOnInvalidReference, theRequest, resourceIdToResolvedTarget);
}
theEntity.setHasLinks(theParams.myLinks.size() > 0);
}
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest) {
private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
IBaseReference nextReference = thePathAndRef.getRef();
IIdType nextId = nextReference.getReferenceElement();
String path = thePathAndRef.getPath();
@ -153,21 +158,36 @@ public class ResourceLinkExtractor {
}
theResourceLinkResolver.validateTypeOrThrowException(type);
ResourceLink resourceLink = createResourceLink(theEntity, theUpdateTime, theResourceLinkResolver, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest);
ResourceLink resourceLink = createResourceLink(theEntity, theUpdateTime, theResourceLinkResolver, theRuntimeSearchParam, path, thePathAndRef, nextId, typeString, type, nextReference, theRequest, theResourceIdToResolvedTarget);
if (resourceLink == null) {
return;
}
theParams.myLinks.add(resourceLink);
}
private ResourceLink createResourceLink(ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
ResourceTable targetResource = theResourceLinkResolver.findTargetResource(nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest);
private ResourceLink createResourceLink(ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, Map<String, IResourceLookup> theResourceIdToResolvedTarget) {
/*
* We keep a cache of resolved target resources. This is good since for some resource types, there
* are multiple search parameters that map to the same element path within a resource (e.g.
* Observation:patient and Observation.subject and we don't want to force a resolution of the
* target any more times than we have to.
*/
IResourceLookup targetResource = theResourceIdToResolvedTarget.get(theNextId.getValue());
if (targetResource == null) {
targetResource = theResourceLinkResolver.findTargetResource(nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theReference, theRequest);
}
if (targetResource == null) {
return null;
}
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResource, theUpdateTime);
theResourceIdToResolvedTarget.put(theNextId.getValue(), targetResource);
String targetResourceType = targetResource.getResourceType();
Long targetResourcePid = targetResource.getResourceId();
String targetResourceIdPart = theNextId.getIdPart();
return ResourceLink.forLocalReference(nextPathAndRef.getPath(), theEntity, targetResourceType, targetResourcePid, targetResourceIdPart, theUpdateTime);
}
}

View File

@ -30,7 +30,6 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
@ -146,7 +145,7 @@ public class InMemoryResourceMatcher {
case Constants.PARAM_SOURCE:
return InMemoryMatchResult.fromBoolean(matchSourcesAndOr(theAndOrParams, theResource));
default:
return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
return matchResourceParam(myModelConfig, theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
}
}
@ -189,7 +188,7 @@ public class InMemoryResourceMatcher {
return theValue.equals(theId.getValue()) || theValue.equals(theId.getIdPart());
}
private InMemoryMatchResult matchResourceParam(String theParamName, List<List<IQueryParameterType>> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) {
private InMemoryMatchResult matchResourceParam(ModelConfig theModelConfig, String theParamName, List<List<IQueryParameterType>> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) {
if (theParamDef != null) {
switch (theParamDef.getParamType()) {
case QUANTITY:
@ -202,7 +201,7 @@ public class InMemoryResourceMatcher {
if (theSearchParams == null) {
return InMemoryMatchResult.successfulMatch();
} else {
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
}
case COMPOSITE:
case HAS:
@ -219,28 +218,8 @@ public class InMemoryResourceMatcher {
}
}
private boolean matchParams(String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
if (paramDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
stripBaseUrlsFromReferenceParams(theNextAnd);
}
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theResourceName, theParamName, paramDef, token));
}
private void stripBaseUrlsFromReferenceParams(List<? extends IQueryParameterType> theNextAnd) {
if (myModelConfig.getTreatBaseUrlsAsLocal().isEmpty()) {
return;
}
for (IQueryParameterType param : theNextAnd) {
ReferenceParam ref = (ReferenceParam) param;
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
if (dt.hasBaseUrl()) {
if (myModelConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
ref.setValue(dt.toUnqualified().getValue());
}
}
}
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token));
}
private boolean hasChain(List<List<IQueryParameterType>> theAndOrParams) {
@ -256,7 +235,7 @@ public class InMemoryResourceMatcher {
for (List<IQueryParameterType> theAndOrParam : theAndOrParams) {
for (IQueryParameterType param : theAndOrParam) {
if (param instanceof BaseParamWithPrefix) {
ParamPrefixEnum prefix = ((BaseParamWithPrefix) param).getPrefix();
ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) param).getPrefix();
RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
if (!supportedPrefix(prefix, paramType)) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, String.format("The prefix %s is not supported for param type %s", prefix, paramType));
@ -268,6 +247,7 @@ public class InMemoryResourceMatcher {
return InMemoryMatchResult.successfulMatch();
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
private boolean supportedPrefix(ParamPrefixEnum theParam, RestSearchParameterTypeEnum theParamType) {
if (theParam == null || theParamType == null) {
return true;

View File

@ -21,7 +21,8 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
*/
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourceLookup;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -34,18 +35,17 @@ import org.springframework.stereotype.Service;
public class InlineResourceLinkResolver implements IResourceLinkResolver {
@Override
public ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
public IResourceLookup findTargetResource(RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
/*
* TODO: JA - This gets used during runtime in-memory matching for subscription. It's not
* really clear if it's useful or not.
*/
ResourceTable target;
target = new ResourceTable();
target.setResourceType(theTypeString);
if (theNextId.isIdPartValidLong()) {
target.setId(theNextId.getIdPartAsLong());
} else {
ForcedId forcedId = new ForcedId();
forcedId.setForcedId(theNextId.getIdPart());
target.setForcedId(forcedId);
}
return target;
return new ResourceLookup(theTypeString, null, null);
}
@Override

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.searchparam.extractor;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -14,72 +14,70 @@ import java.util.Set;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class ResourceIndexedSearchParamsTest {
public static final String STRING_ID = "StringId";
public static final String LONG_ID = "123";
private ResourceIndexedSearchParams myParams;
private ResourceTable myTarget;
private ResourceTable mySource;
private ModelConfig myModelConfig = new ModelConfig();
@Before
public void before() {
ResourceTable source = new ResourceTable();
source.setResourceType("Patient");
mySource = new ResourceTable();
mySource.setResourceType("Patient");
myTarget = new ResourceTable();
myTarget.setResourceType("Organization");
myParams = new ResourceIndexedSearchParams(source);
ResourceLink link = ResourceLink.forLocalReference("organization", source, myTarget, new Date());
myParams.getResourceLinks().add(link);
myParams = new ResourceIndexedSearchParams(mySource);
}
@Test
public void matchResourceLinksStringCompareToLong() {
ReferenceParam referenceParam = getReferenceParam(STRING_ID);
myTarget.setId(123L);
ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date());
myParams.getResourceLinks().add(link);
boolean result = myParams.matchResourceLinks("Patient", "organization", referenceParam, "organization");
ReferenceParam referenceParam = getReferenceParam(STRING_ID);
boolean result = myParams.matchResourceLinks(myModelConfig, "Patient", "organization", referenceParam, "organization");
assertFalse(result);
}
@Test
public void matchResourceLinksStringCompareToString() {
ReferenceParam referenceParam = getReferenceParam(STRING_ID);
ForcedId forcedid = new ForcedId();
forcedid.setForcedId(STRING_ID);
myTarget.setForcedId(forcedid);
ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date());
myParams.getResourceLinks().add(link);
boolean result = myParams.matchResourceLinks("Patient", "organization", referenceParam, "organization");
ReferenceParam referenceParam = getReferenceParam(STRING_ID);
boolean result = myParams.matchResourceLinks(myModelConfig, "Patient", "organization", referenceParam, "organization");
assertTrue(result);
}
@Test
public void matchResourceLinksLongCompareToString() {
ReferenceParam referenceParam = getReferenceParam(LONG_ID);
ForcedId forcedid = new ForcedId();
forcedid.setForcedId(STRING_ID);
myTarget.setForcedId(forcedid);
ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, STRING_ID, new Date());
myParams.getResourceLinks().add(link);
boolean result = myParams.matchResourceLinks("Patient", "organization", referenceParam, "organization");
ReferenceParam referenceParam = getReferenceParam(LONG_ID);
boolean result = myParams.matchResourceLinks(myModelConfig, "Patient", "organization", referenceParam, "organization");
assertFalse(result);
}
@Test
public void matchResourceLinksLongCompareToLong() {
ReferenceParam referenceParam = getReferenceParam(LONG_ID);
myTarget.setId(123L);
ResourceLink link = ResourceLink.forLocalReference("organization", mySource, "Organization", 123L, LONG_ID, new Date());
myParams.getResourceLinks().add(link);
boolean result = myParams.matchResourceLinks("Patient", "organization", referenceParam, "organization");
ReferenceParam referenceParam = getReferenceParam(LONG_ID);
boolean result = myParams.matchResourceLinks(myModelConfig, "Patient", "organization", referenceParam, "organization");
assertTrue(result);
}
private ReferenceParam getReferenceParam(String theId) {
ReferenceParam retval = new ReferenceParam();
retval.setValue(theId);
return retval;
ReferenceParam retVal = new ReferenceParam();
retVal.setValue(theId);
return retVal;
}
@ -93,14 +91,14 @@ public class ResourceIndexedSearchParamsTest {
Lists.newArrayList("name=SMITH", "name=JOHN")
);
values = ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains("Patient", partsChoices);
assertThat(values.toString(), values, containsInAnyOrder("Patient?gender=male&name=JOHN","Patient?gender=male&name=SMITH"));
assertThat(values.toString(), values, containsInAnyOrder("Patient?gender=male&name=JOHN", "Patient?gender=male&name=SMITH"));
partsChoices = Lists.newArrayList(
Lists.newArrayList("gender=male", ""),
Lists.newArrayList("name=SMITH", "name=JOHN", "")
);
values = ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains("Patient", partsChoices);
assertThat(values.toString(), values, containsInAnyOrder("Patient?gender=male&name=JOHN","Patient?gender=male&name=SMITH"));
assertThat(values.toString(), values, containsInAnyOrder("Patient?gender=male&name=JOHN", "Patient?gender=male&name=SMITH"));
partsChoices = Lists.newArrayList(
);