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:
parent
e4bb254754
commit
733259a13b
|
@ -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}
|
||||||
|
|
|
@ -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"
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in New Issue