001package ca.uhn.fhir.jpa.dao; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.context.RuntimeResourceDefinition; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.jpa.api.config.DaoConfig; 030import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 031import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 032import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 033import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 034import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 035import ca.uhn.fhir.jpa.util.MemoryCacheService; 036import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 037import ca.uhn.fhir.rest.api.server.RequestDetails; 038import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 039import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 040import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 041import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; 042import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 043import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 044import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 045import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 046import ca.uhn.fhir.util.StopWatch; 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 javax.annotation.Nullable; 053import java.util.Collections; 054import java.util.HashMap; 055import java.util.HashSet; 056import java.util.Map; 057import java.util.Objects; 058import java.util.Set; 059import java.util.stream.Collectors; 060 061@Service 062public class MatchResourceUrlService { 063 064 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MatchResourceUrlService.class); 065 066 @Autowired 067 private DaoRegistry myDaoRegistry; 068 @Autowired 069 private FhirContext myContext; 070 @Autowired 071 private MatchUrlService myMatchUrlService; 072 @Autowired 073 private DaoConfig myDaoConfig; 074 @Autowired 075 private IInterceptorBroadcaster myInterceptorBroadcaster; 076 @Autowired 077 private MemoryCacheService myMemoryCacheService; 078 079 /** 080 * Note that this will only return a maximum of 2 results!! 081 */ 082 public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, TransactionDetails theTransactionDetails, RequestDetails theRequest) { 083 return processMatchUrl(theMatchUrl, theResourceType, theTransactionDetails, theRequest, null); 084 } 085 086 /** 087 * Note that this will only return a maximum of 2 results!! 088 */ 089 public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, TransactionDetails theTransactionDetails, RequestDetails theRequest, IBaseResource theConditionalOperationTargetOrNull) { 090 Set<ResourcePersistentId> retVal = null; 091 092 String resourceType = myContext.getResourceType(theResourceType); 093 String matchUrl = massageForStorage(resourceType, theMatchUrl); 094 095 ResourcePersistentId resolvedInTransaction = theTransactionDetails.getResolvedMatchUrls().get(matchUrl); 096 if (resolvedInTransaction != null) { 097 // If the resource has previously been looked up within the transaction, there's no need to re-authorize it. 098 if (resolvedInTransaction == TransactionDetails.NOT_FOUND) { 099 return Collections.emptySet(); 100 } else { 101 return Collections.singleton(resolvedInTransaction); 102 } 103 } 104 105 ResourcePersistentId resolvedInCache = processMatchUrlUsingCacheOnly(resourceType, matchUrl); 106 if (resolvedInCache != null) { 107 retVal = Collections.singleton(resolvedInCache); 108 } 109 110 if (retVal == null) { 111 RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType); 112 SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(matchUrl, resourceDef); 113 if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { 114 throw new InvalidRequestException(Msg.code(518) + "Invalid match URL[" + matchUrl + "] - URL has no search parameters"); 115 } 116 paramMap.setLoadSynchronousUpTo(2); 117 118 retVal = search(paramMap, theResourceType, theRequest, theConditionalOperationTargetOrNull); 119 } 120 121 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 122 if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, myInterceptorBroadcaster, theRequest)) { 123 Map<IBaseResource, ResourcePersistentId> resourceToPidMap = new HashMap<>(); 124 125 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 126 127 for (ResourcePersistentId pid : retVal) { 128 resourceToPidMap.put(dao.readByPid(pid), pid); 129 } 130 131 SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(resourceToPidMap.keySet()); 132 HookParams params = new HookParams() 133 .add(IPreResourceShowDetails.class, accessDetails) 134 .add(RequestDetails.class, theRequest) 135 .addIfMatchesType(ServletRequestDetails.class, theRequest); 136 137 try { 138 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 139 140 retVal = accessDetails.toList() 141 .stream() 142 .map(resourceToPidMap::get) 143 .filter(Objects::nonNull) 144 .collect(Collectors.toSet()); 145 } catch (ForbiddenOperationException e) { 146 // If the search matches a resource that the user does not have authorization for, 147 // we want to treat it the same as if the search matched no resources, in order not to leak information. 148 ourLog.warn("Inline match URL [" + matchUrl + "] specified a resource the user is not authorized to access.", e); 149 retVal = new HashSet<>(); 150 } 151 } 152 153 if (retVal.size() == 1) { 154 ResourcePersistentId pid = retVal.iterator().next(); 155 theTransactionDetails.addResolvedMatchUrl(matchUrl, pid); 156 if (myDaoConfig.isMatchUrlCacheEnabled()) { 157 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, pid); 158 } 159 } 160 161 return retVal; 162 } 163 164 private <R extends IBaseResource> IFhirResourceDao<R> getResourceDao(Class<R> theResourceType) { 165 IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType); 166 if (dao == null) { 167 throw new InternalErrorException(Msg.code(519) + "No DAO for resource type: " + theResourceType.getName()); 168 } 169 return dao; 170 } 171 172 private String massageForStorage(String theResourceType, String theMatchUrl) { 173 Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); 174 int questionMarkIdx = theMatchUrl.indexOf("?"); 175 if (questionMarkIdx > 0) { 176 return theMatchUrl; 177 } 178 if (questionMarkIdx == 0) { 179 return theResourceType + theMatchUrl; 180 } 181 return theResourceType + "?" + theMatchUrl; 182 } 183 184 @Nullable 185 public ResourcePersistentId processMatchUrlUsingCacheOnly(String theResourceType, String theMatchUrl) { 186 ResourcePersistentId existing = null; 187 if (myDaoConfig.isMatchUrlCacheEnabled()) { 188 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 189 existing = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl); 190 } 191 return existing; 192 } 193 194 public <R extends IBaseResource> Set<ResourcePersistentId> search(SearchParameterMap theParamMap, Class<R> theResourceType, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) { 195 StopWatch sw = new StopWatch(); 196 IFhirResourceDao<R> dao = getResourceDao(theResourceType); 197 198 Set<ResourcePersistentId> retVal = dao.searchForIds(theParamMap, theRequest, theConditionalOperationTargetOrNull); 199 200 // Interceptor broadcast: JPA_PERFTRACE_INFO 201 if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) { 202 StorageProcessingMessage message = new StorageProcessingMessage(); 203 message.setMessage("Processed conditional resource URL with " + retVal.size() + " result(s) in " + sw); 204 HookParams params = new HookParams() 205 .add(RequestDetails.class, theRequest) 206 .addIfMatchesType(ServletRequestDetails.class, theRequest) 207 .add(StorageProcessingMessage.class, message); 208 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); 209 } 210 return retVal; 211 } 212 213 214 public void matchUrlResolved(TransactionDetails theTransactionDetails, String theResourceType, String theMatchUrl, ResourcePersistentId theResourcePersistentId) { 215 Validate.notBlank(theMatchUrl); 216 Validate.notNull(theResourcePersistentId); 217 String matchUrl = massageForStorage(theResourceType, theMatchUrl); 218 theTransactionDetails.addResolvedMatchUrl(matchUrl, theResourcePersistentId); 219 if (myDaoConfig.isMatchUrlCacheEnabled()) { 220 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, matchUrl, theResourcePersistentId); 221 } 222 } 223 224}