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.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 029import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 030import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 031import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 032import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 033import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 034import ca.uhn.fhir.jpa.util.MemoryCacheService; 035import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 036import ca.uhn.fhir.rest.api.server.RequestDetails; 037import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 038import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 039import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 040import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; 041import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 043import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 044import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 045import ca.uhn.fhir.util.StopWatch; 046import jakarta.annotation.Nullable; 047import org.apache.commons.lang3.Validate; 048import org.hl7.fhir.instance.model.api.IBaseResource; 049import org.springframework.beans.factory.annotation.Autowired; 050import org.springframework.stereotype.Service; 051 052import java.util.Collections; 053import java.util.HashMap; 054import java.util.HashSet; 055import java.util.List; 056import java.util.Map; 057import java.util.Objects; 058import java.util.Set; 059import java.util.stream.Collectors; 060 061@Service 062public class MatchResourceUrlService<T extends IResourcePersistentId> { 063 064 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MatchResourceUrlService.class); 065 066 @Autowired 067 private DaoRegistry myDaoRegistry; 068 069 @Autowired 070 private FhirContext myContext; 071 072 @Autowired 073 private MatchUrlService myMatchUrlService; 074 075 @Autowired 076 private JpaStorageSettings myStorageSettings; 077 078 @Autowired 079 private IInterceptorBroadcaster myInterceptorBroadcaster; 080 081 @Autowired 082 private MemoryCacheService myMemoryCacheService; 083 084 /** 085 * Note that this will only return a maximum of 2 results!! 086 */ 087 public <R extends IBaseResource> Set<T> processMatchUrl( 088 String theMatchUrl, 089 Class<R> theResourceType, 090 TransactionDetails theTransactionDetails, 091 RequestDetails theRequest) { 092 return processMatchUrl(theMatchUrl, theResourceType, theTransactionDetails, theRequest, null); 093 } 094 095 /** 096 * Note that this will only return a maximum of 2 results!! 097 */ 098 public <R extends IBaseResource> Set<T> processMatchUrl( 099 String theMatchUrl, 100 Class<R> theResourceType, 101 TransactionDetails theTransactionDetails, 102 RequestDetails theRequest, 103 IBaseResource theConditionalOperationTargetOrNull) { 104 Set<T> retVal = null; 105 106 String resourceType = myContext.getResourceType(theResourceType); 107 String matchUrl = massageForStorage(resourceType, theMatchUrl); 108 109 T resolvedInTransaction = 110 (T) theTransactionDetails.getResolvedMatchUrls().get(matchUrl); 111 if (resolvedInTransaction != null) { 112 // If the resource has previously been looked up within the transaction, there's no need to re-authorize it. 113 if (resolvedInTransaction == TransactionDetails.NOT_FOUND) { 114 return Collections.emptySet(); 115 } else { 116 return Collections.singleton(resolvedInTransaction); 117 } 118 } 119 120 T resolvedInCache = processMatchUrlUsingCacheOnly(resourceType, matchUrl); 121 ourLog.debug("Resolving match URL from cache {} found: {}", theMatchUrl, resolvedInCache); 122 if (resolvedInCache != null) { 123 retVal = Collections.singleton(resolvedInCache); 124 } 125 126 if (retVal == null) { 127 RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType); 128 SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(matchUrl, resourceDef); 129 if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { 130 throw new InvalidRequestException( 131 Msg.code(518) + "Invalid match URL[" + matchUrl + "] - URL has no search parameters"); 132 } 133 paramMap.setLoadSynchronousUpTo(2); 134 135 retVal = search(paramMap, theResourceType, theRequest, theConditionalOperationTargetOrNull); 136 } 137 138 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 139 IInterceptorBroadcaster compositeBroadcaster = 140 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 141 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 142 Map<IBaseResource, T> resourceToPidMap = new HashMap<>(); 143 144 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 145 146 for (T pid : retVal) { 147 resourceToPidMap.put(dao.readByPid(pid), pid); 148 } 149 150 SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(resourceToPidMap.keySet()); 151 HookParams params = new HookParams() 152 .add(IPreResourceShowDetails.class, accessDetails) 153 .add(RequestDetails.class, theRequest) 154 .addIfMatchesType(ServletRequestDetails.class, theRequest); 155 156 try { 157 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 158 159 retVal = accessDetails.toList().stream() 160 .map(resourceToPidMap::get) 161 .filter(Objects::nonNull) 162 .collect(Collectors.toSet()); 163 } catch (ForbiddenOperationException e) { 164 // If the search matches a resource that the user does not have authorization for, 165 // we want to treat it the same as if the search matched no resources, in order not to leak information. 166 ourLog.warn( 167 "Inline match URL [" + matchUrl 168 + "] specified a resource the user is not authorized to access.", 169 e); 170 retVal = new HashSet<>(); 171 } 172 } 173 174 if (retVal.size() == 1) { 175 T pid = retVal.iterator().next(); 176 theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, pid); 177 if (myStorageSettings.isMatchUrlCacheEnabled()) { 178 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, pid); 179 } 180 } 181 182 return retVal; 183 } 184 185 private <R extends IBaseResource> IFhirResourceDao<R> getResourceDao(Class<R> theResourceType) { 186 IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType); 187 if (dao == null) { 188 throw new InternalErrorException(Msg.code(519) + "No DAO for resource type: " + theResourceType.getName()); 189 } 190 return dao; 191 } 192 193 private String massageForStorage(String theResourceType, String theMatchUrl) { 194 Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); 195 int questionMarkIdx = theMatchUrl.indexOf("?"); 196 if (questionMarkIdx > 0) { 197 return theMatchUrl; 198 } 199 if (questionMarkIdx == 0) { 200 return theResourceType + theMatchUrl; 201 } 202 return theResourceType + "?" + theMatchUrl; 203 } 204 205 @Nullable 206 public T processMatchUrlUsingCacheOnly(String theResourceType, String theMatchUrl) { 207 T existing = null; 208 if (myStorageSettings.isMatchUrlCacheEnabled()) { 209 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 210 existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl); 211 } 212 return existing; 213 } 214 215 public <R extends IBaseResource> Set<T> search( 216 SearchParameterMap theParamMap, 217 Class<R> theResourceType, 218 RequestDetails theRequest, 219 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 220 StopWatch sw = new StopWatch(); 221 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 222 223 List<T> retVal = dao.searchForIds(theParamMap, theRequest, theConditionalOperationTargetOrNull); 224 225 // Interceptor broadcast: JPA_PERFTRACE_INFO 226 IInterceptorBroadcaster compositeBroadcaster = 227 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 228 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 229 StorageProcessingMessage message = new StorageProcessingMessage(); 230 message.setMessage("Processed conditional resource URL with " + retVal.size() + " result(s) in " + sw); 231 HookParams params = new HookParams() 232 .add(RequestDetails.class, theRequest) 233 .addIfMatchesType(ServletRequestDetails.class, theRequest) 234 .add(StorageProcessingMessage.class, message); 235 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 236 } 237 238 return new HashSet<>(retVal); 239 } 240 241 public void matchUrlResolved( 242 TransactionDetails theTransactionDetails, 243 String theResourceType, 244 String theMatchUrl, 245 T theResourcePersistentId) { 246 Validate.notBlank(theMatchUrl); 247 Validate.notNull(theResourcePersistentId); 248 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 249 theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, theResourcePersistentId); 250 if (myStorageSettings.isMatchUrlCacheEnabled()) { 251 myMemoryCacheService.putAfterCommit( 252 MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, theResourcePersistentId); 253 } 254 } 255 256 public void unresolveMatchUrl( 257 TransactionDetails theTransactionDetails, String theResourceType, String theMatchUrl) { 258 Validate.notBlank(theMatchUrl); 259 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 260 theTransactionDetails.removeResolvedMatchUrl(matchUrl); 261 } 262}