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}