4618 refactor searchparamwithinlinereferencesextractor to create a interface and a base class (#4619)

* Introduced new interface, refactored SearchParamWithInlineReferencesExtractor, added new test

* clean up, remove unused param

* added change log

* moved error message to properties file, made base class abstract

* fixed issue with test

* code review changes

---------

Co-authored-by: Steven Li <steven@smilecdr.com>
This commit is contained in:
StevenXLi 2023-03-15 12:27:52 -04:00 committed by GitHub
parent e4bb254754
commit 733259a13b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 226 additions and 205 deletions

View File

@ -78,7 +78,8 @@ ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor.externalizedBinarySt
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}" ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}"
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlMultipleMatches=Invalid match URL "{0}" - Multiple resources match this search
ca.uhn.fhir.jpa.dao.BaseStorageDao.inlineMatchNotSupported=Inline match URLs are not supported on this server. Cannot process reference: "{0}"
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedNoId=Failed to {0} resource in transaction because no ID was provided ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedNoId=Failed to {0} resource in transaction because no ID was provided
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedUnknownId=Failed to {0} resource in transaction because no resource could be found with ID {1} ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedUnknownId=Failed to {0} resource in transaction because no resource could be found with ID {1}

View File

@ -0,0 +1,6 @@
---
type: change
issue: 4618
title: "A new interface has been created for SearchParamWithInlineReferencesExtractor with a base implementation.
This allows us to encapsulate logic for resolving and replacing inline resources to the same place
is also cuts down on code duplication and allow for easier sharing of the same logic"

View File

@ -1,66 +0,0 @@
package ca.uhn.fhir.jpa.dao.index;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
public class ExtractInlineReferenceParams {
private IBaseResource myResource;
private TransactionDetails myTransactionDetails;
private RequestDetails myRequestDetails;
public ExtractInlineReferenceParams(
IBaseResource theResource,
TransactionDetails theTransactionDetails,
RequestDetails theRequest
) {
myResource = theResource;
myTransactionDetails = theTransactionDetails;
myRequestDetails = theRequest;
}
public IBaseResource getResource() {
return myResource;
}
public void setResource(IBaseResource theResource) {
myResource = theResource;
}
public TransactionDetails getTransactionDetails() {
return myTransactionDetails;
}
public void setTransactionDetails(TransactionDetails theTransactionDetails) {
myTransactionDetails = theTransactionDetails;
}
public RequestDetails getRequestDetails() {
return myRequestDetails;
}
public void setRequestDetails(RequestDetails theRequestDetails) {
myRequestDetails = theRequestDetails;
}
}

View File

@ -20,41 +20,31 @@ package ca.uhn.fhir.jpa.dao.index;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.BaseStorageDao;
import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.NotFoundPid; import ca.uhn.fhir.rest.api.server.storage.NotFoundPid;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams; import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.FhirTerser;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -65,39 +55,24 @@ import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType; import javax.persistence.PersistenceContextType;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@Lazy @Lazy
public class SearchParamWithInlineReferencesExtractor { public class SearchParamWithInlineReferencesExtractor extends BaseSearchParamWithInlineReferencesExtractor<JpaPid> implements ISearchParamWithInlineReferencesExtractor {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class);
@PersistenceContext(type = PersistenceContextType.TRANSACTION) @PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager; protected EntityManager myEntityManager;
@Autowired @Autowired
private MatchResourceUrlService<JpaPid> myMatchResourceUrlService;
@Autowired
private JpaStorageSettings myStorageSettings;
@Autowired
private FhirContext myContext;
@Autowired
private IIdHelperService<JpaPid> myIdHelperService;
@Autowired
private ISearchParamRegistry mySearchParamRegistry; private ISearchParamRegistry mySearchParamRegistry;
@Autowired @Autowired
private SearchParamExtractorService mySearchParamExtractorService; private SearchParamExtractorService mySearchParamExtractorService;
@Autowired @Autowired
private DaoResourceLinkResolver<JpaPid> myDaoResourceLinkResolver;
@Autowired
private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
@Autowired @Autowired
private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao; private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@Autowired @Autowired
private PartitionSettings myPartitionSettings; private PartitionSettings myPartitionSettings;
@Autowired
private MemoryCacheService myMemoryCacheService;
@VisibleForTesting @VisibleForTesting
public void setPartitionSettings(PartitionSettings thePartitionSettings) { public void setPartitionSettings(PartitionSettings thePartitionSettings) {
@ -116,8 +91,8 @@ public class SearchParamWithInlineReferencesExtractor {
public void populateFromResource(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, TransactionDetails theTransactionDetails, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest, boolean thePerformIndexing) { public void populateFromResource(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, TransactionDetails theTransactionDetails, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest, boolean thePerformIndexing) {
if (thePerformIndexing) { if (thePerformIndexing) {
ExtractInlineReferenceParams extractParams = new ExtractInlineReferenceParams(theResource, theTransactionDetails, theRequest); // Perform inline match URL substitution
extractInlineReferences(extractParams); extractInlineReferences(theRequest, theResource, theTransactionDetails);
} }
mySearchParamExtractorService.extractFromResource(theRequestPartitionId, theRequest, theParams, theExistingParams, theEntity, theResource, theTransactionDetails, thePerformIndexing); mySearchParamExtractorService.extractFromResource(theRequestPartitionId, theRequest, theParams, theExistingParams, theEntity, theResource, theTransactionDetails, thePerformIndexing);
@ -183,113 +158,6 @@ public class SearchParamWithInlineReferencesExtractor {
return paramsListForCompositePart; return paramsListForCompositePart;
} }
@VisibleForTesting
public void setStorageSettings(JpaStorageSettings theStorageSettings) {
myStorageSettings = theStorageSettings;
}
@VisibleForTesting
public void setContext(FhirContext theContext) {
myContext = theContext;
}
@Deprecated
public void extractInlineReferences(
IBaseResource theResource,
TransactionDetails theTransactionDetails,
RequestDetails theRequest
) {
extractInlineReferences(new ExtractInlineReferenceParams(theResource, theTransactionDetails, theRequest));
}
/**
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo".
* These match URLs are resolved and replaced with the ID of the
* matching resource.
* <p>
* This method is *only* called from UPDATE path
*/
public void extractInlineReferences(ExtractInlineReferenceParams theParams) {
if (!myStorageSettings.isAllowInlineMatchUrlReferences()) {
return;
}
IBaseResource resource = theParams.getResource();
RequestDetails theRequest = theParams.getRequestDetails();
TransactionDetails theTransactionDetails = theParams.getTransactionDetails();
FhirTerser terser = myContext.newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(resource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(Msg.code(1090) + msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
JpaPid resolvedMatch = null;
if (theTransactionDetails != null) {
resolvedMatch = (JpaPid) theTransactionDetails.getResolvedMatchUrls().get(nextIdText);
}
//Attempt to find the target reference before creating a placeholder
Set<JpaPid> matches;
if (resolvedMatch != null && !IResourcePersistentId.NOT_FOUND.equals(resolvedMatch)) {
matches = Set.of(resolvedMatch);
} else {
matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theTransactionDetails, theRequest);
}
JpaPid match;
if (matches.isEmpty()) {
Optional<IBasePersistedResource> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null, theRequest, theTransactionDetails);
if (placeholderOpt.isPresent()) {
match = (JpaPid) placeholderOpt.get().getPersistentId();
match.setAssociatedResourceId(placeholderOpt.get().getIdDt());
theTransactionDetails.addResolvedMatchUrl(nextIdText, match);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match);
} else {
String msg = myContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(Msg.code(1091) + msg);
}
} else if (matches.size() > 1) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(Msg.code(1092) + msg);
} else {
match = matches.iterator().next();
}
IIdType newId = myIdHelperService.translatePidIdToForcedId(myContext, resourceTypeString, match);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
if (theTransactionDetails != null) {
String previousReference = nextRef.getReferenceElement().getValue();
theTransactionDetails.addRollbackUndoAction(()->nextRef.setReference(previousReference));
}
nextRef.setReference(newId.getValue());
}
}
}
@VisibleForTesting @VisibleForTesting
public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) { public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) {
myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer; myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer;
@ -317,7 +185,7 @@ public class SearchParamWithInlineReferencesExtractor {
searchParameterId = next.getSearchParameterId().toUnqualifiedVersionless().getValue(); searchParameterId = next.getSearchParameterId().toUnqualifiedVersionless().getValue();
} }
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue(), searchParameterId); String msg = myFhirContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue(), searchParameterId);
throw new PreconditionFailedException(Msg.code(1093) + msg); throw new PreconditionFailedException(Msg.code(1093) + msg);
} }
} }

View File

@ -1108,6 +1108,25 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
} }
} }
@Test
public void testTransactionCreateInlineMatchUrlWithAllowInlineMatchUrlReferencesSettingNotEnabled() {
Bundle request = new Bundle();
myStorageSettings.setAllowInlineMatchUrlReferences(false);
Observation o = new Observation();
o.getCode().setText("Some Observation");
o.getSubject().setReference("Patient?identifier=urn%3Asystem%7C");
request.addEntry().setResource(o).getRequest().setMethod(HTTPVerb.POST);
try {
mySystemDao.transaction(mySrd, request);
fail();
} catch (InvalidRequestException e) {
assertEquals(Msg.code(2282) + "Inline match URLs are not supported on this server. Cannot process reference: \"Patient?identifier=urn%3Asystem%7C\"", e.getMessage());
}
}
@Test @Test
public void testTransactionMissingResourceForPost() { public void testTransactionMissingResourceForPost() {
Bundle request = new Bundle(); Bundle request = new Bundle();

View File

@ -0,0 +1,158 @@
package ca.uhn.fhir.jpa.searchparam.extractor;
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.BaseStorageDao;
import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public abstract class BaseSearchParamWithInlineReferencesExtractor<T extends IResourcePersistentId> implements ISearchParamWithInlineReferencesExtractor {
private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamWithInlineReferencesExtractor.class);
protected FhirContext myFhirContext;
protected JpaStorageSettings myStorageSettings;
@Autowired
private MatchResourceUrlService<T> myMatchResourceUrlService;
@Autowired
private DaoResourceLinkResolver<T> myDaoResourceLinkResolver;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Autowired
private IIdHelperService<T> myIdHelperService;
@Override
public void extractInlineReferences(RequestDetails theRequestDetails, IBaseResource theResource, TransactionDetails theTransactionDetails) {
FhirTerser terser = myFhirContext.newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
if (!myStorageSettings.isAllowInlineMatchUrlReferences()) {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "inlineMatchNotSupported", UrlUtil.sanitizeUrlPart(nextRef.getReferenceElement().getValueAsString()));
throw new InvalidRequestException(Msg.code(2282) + msg);
}
nextIdText = truncateReference(nextIdText, qmIndex);
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myFhirContext.getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(Msg.code(1090) + msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
T resolvedMatch = null;
if (theTransactionDetails != null) {
resolvedMatch = (T) theTransactionDetails.getResolvedMatchUrls().get(nextIdText);
}
Set<T> matches;
if (resolvedMatch != null && !IResourcePersistentId.NOT_FOUND.equals(resolvedMatch)) {
matches = Set.of(resolvedMatch);
} else {
matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theTransactionDetails, theRequestDetails);
}
T match;
if (matches.isEmpty()) {
Optional<IBasePersistedResource> placeholderOpt = myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(matchResourceType, nextRef, null, theRequestDetails, theTransactionDetails);
if (placeholderOpt.isPresent()) {
match = (T) placeholderOpt.get().getPersistentId();
match.setAssociatedResourceId(placeholderOpt.get().getIdDt());
theTransactionDetails.addResolvedMatchUrl(nextIdText, match);
myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match);
} else {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(Msg.code(1091) + msg);
}
} else if (matches.size() > 1) {
String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(Msg.code(1092) + msg);
} else {
match = matches.iterator().next();
}
IIdType newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
if (theTransactionDetails != null) {
String previousReference = nextRef.getReferenceElement().getValue();
theTransactionDetails.addRollbackUndoAction(() -> nextRef.setReference(previousReference));
}
nextRef.setReference(newId.getValue());
}
}
}
// Removes parts of the reference keeping only the valuable parts, the resource type and searchparam
private static String truncateReference(String nextIdText, int qmIndex) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
return nextIdText;
}
@Autowired
public void setStorageSettings(JpaStorageSettings theStorageSettings) {
myStorageSettings = theStorageSettings;
}
@Autowired
public void setContext(FhirContext theContext) {
myFhirContext = theContext;
}
}

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.jpa.searchparam.extractor;
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface ISearchParamWithInlineReferencesExtractor {
/**
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo".
* These match URLs are resolved and replaced with the ID of the
* matching resource.
*/
void extractInlineReferences(RequestDetails theRequestDetails, IBaseResource theResource, TransactionDetails theTransactionDetails);
}