001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 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.jpa.api.config.JpaStorageSettings; 026import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 027import ca.uhn.fhir.jpa.dao.BaseStorageDao; 028import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; 029import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver; 030import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 031import ca.uhn.fhir.jpa.util.MemoryCacheService; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 034import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 036import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 037import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 038import ca.uhn.fhir.util.FhirTerser; 039import ca.uhn.fhir.util.UrlUtil; 040import org.hl7.fhir.instance.model.api.IBaseReference; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042import org.hl7.fhir.instance.model.api.IIdType; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045import org.springframework.beans.factory.annotation.Autowired; 046 047import java.util.List; 048import java.util.Optional; 049import java.util.Set; 050 051public abstract class BaseSearchParamWithInlineReferencesExtractor<T extends IResourcePersistentId<?>> 052 implements ISearchParamWithInlineReferencesExtractor { 053 private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamWithInlineReferencesExtractor.class); 054 055 protected FhirContext myFhirContext; 056 protected JpaStorageSettings myStorageSettings; 057 058 @Autowired 059 private MatchResourceUrlService<T> myMatchResourceUrlService; 060 061 @Autowired 062 private DaoResourceLinkResolver<T> myDaoResourceLinkResolver; 063 064 @Autowired 065 private MemoryCacheService myMemoryCacheService; 066 067 @Autowired 068 private IIdHelperService<T> myIdHelperService; 069 070 @Override 071 public void extractInlineReferences( 072 RequestDetails theRequestDetails, IBaseResource theResource, TransactionDetails theTransactionDetails) { 073 FhirTerser terser = myFhirContext.newTerser(); 074 List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); 075 for (IBaseReference nextRef : allRefs) { 076 IIdType nextId = nextRef.getReferenceElement(); 077 String nextIdText = nextId.getValue(); 078 if (nextIdText == null) { 079 continue; 080 } 081 int qmIndex = nextIdText.indexOf('?'); 082 if (qmIndex != -1) { 083 if (!myStorageSettings.isAllowInlineMatchUrlReferences()) { 084 String msg = myFhirContext 085 .getLocalizer() 086 .getMessage( 087 BaseStorageDao.class, 088 "inlineMatchNotSupported", 089 UrlUtil.sanitizeUrlPart( 090 nextRef.getReferenceElement().getValueAsString())); 091 throw new InvalidRequestException(Msg.code(2282) + msg); 092 } 093 nextIdText = truncateReference(nextIdText, qmIndex); 094 String resourceTypeString = 095 nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", ""); 096 RuntimeResourceDefinition matchResourceDef = myFhirContext.getResourceDefinition(resourceTypeString); 097 if (matchResourceDef == null) { 098 String msg = myFhirContext 099 .getLocalizer() 100 .getMessage( 101 BaseStorageDao.class, 102 "invalidMatchUrlInvalidResourceType", 103 nextId.getValue(), 104 resourceTypeString); 105 throw new InvalidRequestException(Msg.code(1090) + msg); 106 } 107 Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass(); 108 109 T resolvedMatch = null; 110 if (theTransactionDetails != null) { 111 resolvedMatch = 112 (T) theTransactionDetails.getResolvedMatchUrls().get(nextIdText); 113 } 114 115 Set<T> matches; 116 if (resolvedMatch != null && !IResourcePersistentId.NOT_FOUND.equals(resolvedMatch)) { 117 matches = Set.of(resolvedMatch); 118 } else { 119 matches = myMatchResourceUrlService.processMatchUrl( 120 nextIdText, matchResourceType, theTransactionDetails, theRequestDetails); 121 } 122 123 T match; 124 IIdType newId = null; 125 if (matches.isEmpty()) { 126 Optional<IBasePersistedResource> placeholderOpt = 127 myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo( 128 matchResourceType, nextRef, null, theRequestDetails, theTransactionDetails); 129 if (placeholderOpt.isPresent()) { 130 match = (T) placeholderOpt.get().getPersistentId(); 131 newId = myFhirContext.getVersion().newIdType(); 132 newId.setValue(placeholderOpt.get().getIdDt().getValue()); 133 match.setAssociatedResourceId(newId); 134 theTransactionDetails.addResolvedMatchUrl(myFhirContext, nextIdText, match); 135 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match); 136 } else { 137 String msg = myFhirContext 138 .getLocalizer() 139 .getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue()); 140 throw new ResourceNotFoundException(Msg.code(1091) + msg); 141 } 142 } else if (matches.size() > 1) { 143 String msg = myFhirContext 144 .getLocalizer() 145 .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", nextId.getValue()); 146 throw new PreconditionFailedException(Msg.code(1092) + msg); 147 } else { 148 match = matches.iterator().next(); 149 } 150 151 if (newId == null) { 152 newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match); 153 } 154 ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId); 155 156 if (theTransactionDetails != null) { 157 String previousReference = nextRef.getReferenceElement().getValue(); 158 theTransactionDetails.addRollbackUndoAction(() -> nextRef.setReference(previousReference)); 159 } 160 nextRef.setReference(newId.getValue()); 161 } 162 } 163 } 164 165 // Removes parts of the reference keeping only the valuable parts, the resource type and searchparam 166 private static String truncateReference(String nextIdText, int qmIndex) { 167 for (int i = qmIndex - 1; i >= 0; i--) { 168 if (nextIdText.charAt(i) == '/') { 169 if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') { 170 // Just in case the URL is in the form Patient/?foo=bar 171 continue; 172 } 173 nextIdText = nextIdText.substring(i + 1); 174 break; 175 } 176 } 177 return nextIdText; 178 } 179 180 @Autowired 181 public void setStorageSettings(JpaStorageSettings theStorageSettings) { 182 myStorageSettings = theStorageSettings; 183 } 184 185 @Autowired 186 public void setContext(FhirContext theContext) { 187 myFhirContext = theContext; 188 } 189}