
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.searchparam.extractor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 028import ca.uhn.fhir.jpa.dao.BaseStorageDao; 029import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; 030import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver; 031import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 032import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 033import ca.uhn.fhir.jpa.util.MemoryCacheService; 034import ca.uhn.fhir.rest.api.server.RequestDetails; 035import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 036import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 038import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 039import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 040import ca.uhn.fhir.util.FhirTerser; 041import ca.uhn.fhir.util.UrlUtil; 042import org.hl7.fhir.instance.model.api.IBaseReference; 043import org.hl7.fhir.instance.model.api.IBaseResource; 044import org.hl7.fhir.instance.model.api.IIdType; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047import org.springframework.beans.factory.annotation.Autowired; 048 049import java.util.List; 050import java.util.Optional; 051import java.util.Set; 052 053public abstract class BaseSearchParamWithInlineReferencesExtractor<T extends IResourcePersistentId<?>> 054 implements ISearchParamWithInlineReferencesExtractor { 055 private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamWithInlineReferencesExtractor.class); 056 057 protected FhirContext myFhirContext; 058 protected JpaStorageSettings myStorageSettings; 059 060 @Autowired 061 private MatchResourceUrlService<T> myMatchResourceUrlService; 062 063 @Autowired 064 private DaoResourceLinkResolver<T> myDaoResourceLinkResolver; 065 066 @Autowired 067 private MemoryCacheService myMemoryCacheService; 068 069 @Autowired 070 private IIdHelperService<T> myIdHelperService; 071 072 @Autowired 073 private IRequestPartitionHelperSvc myPartitionHelperSvc; 074 075 @Override 076 public void extractInlineReferences( 077 RequestDetails theRequestDetails, IBaseResource theResource, TransactionDetails theTransactionDetails) { 078 FhirTerser terser = myFhirContext.newTerser(); 079 List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); 080 for (IBaseReference nextRef : allRefs) { 081 IIdType nextId = nextRef.getReferenceElement(); 082 String nextIdText = nextId.getValue(); 083 if (nextIdText == null) { 084 continue; 085 } 086 int qmIndex = nextIdText.indexOf('?'); 087 if (qmIndex != -1) { 088 if (!myStorageSettings.isAllowInlineMatchUrlReferences()) { 089 String msg = myFhirContext 090 .getLocalizer() 091 .getMessage( 092 BaseStorageDao.class, 093 "inlineMatchNotSupported", 094 UrlUtil.sanitizeUrlPart( 095 nextRef.getReferenceElement().getValueAsString())); 096 throw new InvalidRequestException(Msg.code(2282) + msg); 097 } 098 nextIdText = truncateReference(nextIdText, qmIndex); 099 String resourceTypeString = 100 nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", ""); 101 RuntimeResourceDefinition matchResourceDef = myFhirContext.getResourceDefinition(resourceTypeString); 102 if (matchResourceDef == null) { 103 String msg = myFhirContext 104 .getLocalizer() 105 .getMessage( 106 BaseStorageDao.class, 107 "invalidMatchUrlInvalidResourceType", 108 nextId.getValue(), 109 resourceTypeString); 110 throw new InvalidRequestException(Msg.code(1090) + msg); 111 } 112 Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass(); 113 114 T resolvedMatch = null; 115 if (theTransactionDetails != null) { 116 resolvedMatch = 117 (T) theTransactionDetails.getResolvedMatchUrls().get(nextIdText); 118 } 119 120 Set<T> matches; 121 if (resolvedMatch != null && !IResourcePersistentId.NOT_FOUND.equals(resolvedMatch)) { 122 matches = Set.of(resolvedMatch); 123 } else { 124 RequestPartitionId theRequestPartitionId = 125 myPartitionHelperSvc.determineReadPartitionForRequestForSearchType( 126 theRequestDetails, resourceTypeString); 127 matches = myMatchResourceUrlService.processMatchUrl( 128 nextIdText, 129 matchResourceType, 130 theTransactionDetails, 131 theRequestDetails, 132 theRequestPartitionId); 133 } 134 135 T match; 136 IIdType newId = null; 137 if (matches.isEmpty()) { 138 Optional<IBasePersistedResource> placeholderOpt = 139 myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo( 140 matchResourceType, nextRef, null, theRequestDetails, theTransactionDetails); 141 if (placeholderOpt.isPresent()) { 142 match = (T) placeholderOpt.get().getPersistentId(); 143 newId = myFhirContext.getVersion().newIdType(); 144 newId.setValue(placeholderOpt.get().getIdDt().getValue()); 145 match.setAssociatedResourceId(newId); 146 theTransactionDetails.addResolvedMatchUrl(myFhirContext, nextIdText, match); 147 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match); 148 } else { 149 String msg = myFhirContext 150 .getLocalizer() 151 .getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue()); 152 throw new ResourceNotFoundException(Msg.code(1091) + msg); 153 } 154 } else if (matches.size() > 1) { 155 String msg = myFhirContext 156 .getLocalizer() 157 .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", nextId.getValue()); 158 throw new PreconditionFailedException(Msg.code(1092) + msg); 159 } else { 160 match = matches.iterator().next(); 161 } 162 163 if (newId == null) { 164 newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match); 165 } 166 ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId); 167 168 if (theTransactionDetails != null) { 169 String previousReference = nextRef.getReferenceElement().getValue(); 170 theTransactionDetails.addRollbackUndoAction(() -> nextRef.setReference(previousReference)); 171 } 172 nextRef.setReference(newId.getValue()); 173 } 174 } 175 } 176 177 // Removes parts of the reference keeping only the valuable parts, the resource type and searchparam 178 private static String truncateReference(String nextIdText, int qmIndex) { 179 for (int i = qmIndex - 1; i >= 0; i--) { 180 if (nextIdText.charAt(i) == '/') { 181 if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') { 182 // Just in case the URL is in the form Patient/?foo=bar 183 continue; 184 } 185 nextIdText = nextIdText.substring(i + 1); 186 break; 187 } 188 } 189 return nextIdText; 190 } 191 192 @Autowired 193 public void setStorageSettings(JpaStorageSettings theStorageSettings) { 194 myStorageSettings = theStorageSettings; 195 } 196 197 @Autowired 198 public void setContext(FhirContext theContext) { 199 myFhirContext = theContext; 200 } 201}