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.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 if (resolvedInCache != null) { 122 retVal = Collections.singleton(resolvedInCache); 123 } 124 125 if (retVal == null) { 126 RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType); 127 SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(matchUrl, resourceDef); 128 if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { 129 throw new InvalidRequestException( 130 Msg.code(518) + "Invalid match URL[" + matchUrl + "] - URL has no search parameters"); 131 } 132 paramMap.setLoadSynchronousUpTo(2); 133 134 retVal = search(paramMap, theResourceType, theRequest, theConditionalOperationTargetOrNull); 135 } 136 137 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 138 if (CompositeInterceptorBroadcaster.hasHooks( 139 Pointcut.STORAGE_PRESHOW_RESOURCES, myInterceptorBroadcaster, theRequest)) { 140 Map<IBaseResource, T> resourceToPidMap = new HashMap<>(); 141 142 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 143 144 for (T pid : retVal) { 145 resourceToPidMap.put(dao.readByPid(pid), pid); 146 } 147 148 SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(resourceToPidMap.keySet()); 149 HookParams params = new HookParams() 150 .add(IPreResourceShowDetails.class, accessDetails) 151 .add(RequestDetails.class, theRequest) 152 .addIfMatchesType(ServletRequestDetails.class, theRequest); 153 154 try { 155 CompositeInterceptorBroadcaster.doCallHooks( 156 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 157 158 retVal = accessDetails.toList().stream() 159 .map(resourceToPidMap::get) 160 .filter(Objects::nonNull) 161 .collect(Collectors.toSet()); 162 } catch (ForbiddenOperationException e) { 163 // If the search matches a resource that the user does not have authorization for, 164 // we want to treat it the same as if the search matched no resources, in order not to leak information. 165 ourLog.warn( 166 "Inline match URL [" + matchUrl 167 + "] specified a resource the user is not authorized to access.", 168 e); 169 retVal = new HashSet<>(); 170 } 171 } 172 173 if (retVal.size() == 1) { 174 T pid = retVal.iterator().next(); 175 theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, pid); 176 if (myStorageSettings.isMatchUrlCacheEnabled()) { 177 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, pid); 178 } 179 } 180 181 return retVal; 182 } 183 184 private <R extends IBaseResource> IFhirResourceDao<R> getResourceDao(Class<R> theResourceType) { 185 IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType); 186 if (dao == null) { 187 throw new InternalErrorException(Msg.code(519) + "No DAO for resource type: " + theResourceType.getName()); 188 } 189 return dao; 190 } 191 192 private String massageForStorage(String theResourceType, String theMatchUrl) { 193 Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); 194 int questionMarkIdx = theMatchUrl.indexOf("?"); 195 if (questionMarkIdx > 0) { 196 return theMatchUrl; 197 } 198 if (questionMarkIdx == 0) { 199 return theResourceType + theMatchUrl; 200 } 201 return theResourceType + "?" + theMatchUrl; 202 } 203 204 @Nullable 205 public T processMatchUrlUsingCacheOnly(String theResourceType, String theMatchUrl) { 206 T existing = null; 207 if (myStorageSettings.isMatchUrlCacheEnabled()) { 208 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 209 existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl); 210 } 211 return existing; 212 } 213 214 public <R extends IBaseResource> Set<T> search( 215 SearchParameterMap theParamMap, 216 Class<R> theResourceType, 217 RequestDetails theRequest, 218 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 219 StopWatch sw = new StopWatch(); 220 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 221 222 List<T> retVal = dao.searchForIds(theParamMap, theRequest, theConditionalOperationTargetOrNull); 223 224 // Interceptor broadcast: JPA_PERFTRACE_INFO 225 if (CompositeInterceptorBroadcaster.hasHooks( 226 Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) { 227 StorageProcessingMessage message = new StorageProcessingMessage(); 228 message.setMessage("Processed conditional resource URL with " + retVal.size() + " result(s) in " + sw); 229 HookParams params = new HookParams() 230 .add(RequestDetails.class, theRequest) 231 .addIfMatchesType(ServletRequestDetails.class, theRequest) 232 .add(StorageProcessingMessage.class, message); 233 CompositeInterceptorBroadcaster.doCallHooks( 234 myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); 235 } 236 237 return new HashSet<>(retVal); 238 } 239 240 public void matchUrlResolved( 241 TransactionDetails theTransactionDetails, 242 String theResourceType, 243 String theMatchUrl, 244 T theResourcePersistentId) { 245 Validate.notBlank(theMatchUrl); 246 Validate.notNull(theResourcePersistentId); 247 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 248 theTransactionDetails.addResolvedMatchUrl(myContext, matchUrl, theResourcePersistentId); 249 if (myStorageSettings.isMatchUrlCacheEnabled()) { 250 myMemoryCacheService.putAfterCommit( 251 MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, theResourcePersistentId); 252 } 253 } 254 255 public void unresolveMatchUrl( 256 TransactionDetails theTransactionDetails, String theResourceType, String theMatchUrl) { 257 Validate.notBlank(theMatchUrl); 258 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 259 theTransactionDetails.removeResolvedMatchUrl(matchUrl); 260 } 261}