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}