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.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.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
028import ca.uhn.fhir.jpa.dao.BaseStorageDao;
029import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
030import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
031import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
032import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
033import ca.uhn.fhir.jpa.util.MemoryCacheService;
034import ca.uhn.fhir.rest.api.server.RequestDetails;
035import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
036import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
038import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
039import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
040import ca.uhn.fhir.util.FhirTerser;
041import ca.uhn.fhir.util.UrlUtil;
042import org.hl7.fhir.instance.model.api.IBaseReference;
043import org.hl7.fhir.instance.model.api.IBaseResource;
044import org.hl7.fhir.instance.model.api.IIdType;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047import org.springframework.beans.factory.annotation.Autowired;
048
049import java.util.List;
050import java.util.Optional;
051import java.util.Set;
052
053public abstract class BaseSearchParamWithInlineReferencesExtractor<T extends IResourcePersistentId<?>>
054                implements ISearchParamWithInlineReferencesExtractor {
055        private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamWithInlineReferencesExtractor.class);
056
057        protected FhirContext myFhirContext;
058        protected JpaStorageSettings myStorageSettings;
059
060        @Autowired
061        private MatchResourceUrlService<T> myMatchResourceUrlService;
062
063        @Autowired
064        private DaoResourceLinkResolver<T> myDaoResourceLinkResolver;
065
066        @Autowired
067        private MemoryCacheService myMemoryCacheService;
068
069        @Autowired
070        private IIdHelperService<T> myIdHelperService;
071
072        @Autowired
073        private IRequestPartitionHelperSvc myPartitionHelperSvc;
074
075        @Override
076        public void extractInlineReferences(
077                        RequestDetails theRequestDetails, IBaseResource theResource, TransactionDetails theTransactionDetails) {
078                FhirTerser terser = myFhirContext.newTerser();
079                List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
080                for (IBaseReference nextRef : allRefs) {
081                        IIdType nextId = nextRef.getReferenceElement();
082                        String nextIdText = nextId.getValue();
083                        if (nextIdText == null) {
084                                continue;
085                        }
086                        int qmIndex = nextIdText.indexOf('?');
087                        if (qmIndex != -1) {
088                                if (!myStorageSettings.isAllowInlineMatchUrlReferences()) {
089                                        String msg = myFhirContext
090                                                        .getLocalizer()
091                                                        .getMessage(
092                                                                        BaseStorageDao.class,
093                                                                        "inlineMatchNotSupported",
094                                                                        UrlUtil.sanitizeUrlPart(
095                                                                                        nextRef.getReferenceElement().getValueAsString()));
096                                        throw new InvalidRequestException(Msg.code(2282) + msg);
097                                }
098                                nextIdText = truncateReference(nextIdText, qmIndex);
099                                String resourceTypeString =
100                                                nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
101                                RuntimeResourceDefinition matchResourceDef = myFhirContext.getResourceDefinition(resourceTypeString);
102                                if (matchResourceDef == null) {
103                                        String msg = myFhirContext
104                                                        .getLocalizer()
105                                                        .getMessage(
106                                                                        BaseStorageDao.class,
107                                                                        "invalidMatchUrlInvalidResourceType",
108                                                                        nextId.getValue(),
109                                                                        resourceTypeString);
110                                        throw new InvalidRequestException(Msg.code(1090) + msg);
111                                }
112                                Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
113
114                                T resolvedMatch = null;
115                                if (theTransactionDetails != null) {
116                                        resolvedMatch =
117                                                        (T) theTransactionDetails.getResolvedMatchUrls().get(nextIdText);
118                                }
119
120                                Set<T> matches;
121                                if (resolvedMatch != null && !IResourcePersistentId.NOT_FOUND.equals(resolvedMatch)) {
122                                        matches = Set.of(resolvedMatch);
123                                } else {
124                                        RequestPartitionId theRequestPartitionId =
125                                                        myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
126                                                                        theRequestDetails, resourceTypeString);
127                                        matches = myMatchResourceUrlService.processMatchUrl(
128                                                        nextIdText,
129                                                        matchResourceType,
130                                                        theTransactionDetails,
131                                                        theRequestDetails,
132                                                        theRequestPartitionId);
133                                }
134
135                                T match;
136                                IIdType newId = null;
137                                if (matches.isEmpty()) {
138                                        Optional<IBasePersistedResource> placeholderOpt =
139                                                        myDaoResourceLinkResolver.createPlaceholderTargetIfConfiguredToDoSo(
140                                                                        matchResourceType, nextRef, null, theRequestDetails, theTransactionDetails);
141                                        if (placeholderOpt.isPresent()) {
142                                                match = (T) placeholderOpt.get().getPersistentId();
143                                                newId = myFhirContext.getVersion().newIdType();
144                                                newId.setValue(placeholderOpt.get().getIdDt().getValue());
145                                                match.setAssociatedResourceId(newId);
146                                                theTransactionDetails.addResolvedMatchUrl(myFhirContext, nextIdText, match);
147                                                myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.MATCH_URL, nextIdText, match);
148                                        } else {
149                                                String msg = myFhirContext
150                                                                .getLocalizer()
151                                                                .getMessage(BaseStorageDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
152                                                throw new ResourceNotFoundException(Msg.code(1091) + msg);
153                                        }
154                                } else if (matches.size() > 1) {
155                                        String msg = myFhirContext
156                                                        .getLocalizer()
157                                                        .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
158                                        throw new PreconditionFailedException(Msg.code(1092) + msg);
159                                } else {
160                                        match = matches.iterator().next();
161                                }
162
163                                if (newId == null) {
164                                        newId = myIdHelperService.translatePidIdToForcedId(myFhirContext, resourceTypeString, match);
165                                }
166                                ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
167
168                                if (theTransactionDetails != null) {
169                                        String previousReference = nextRef.getReferenceElement().getValue();
170                                        theTransactionDetails.addRollbackUndoAction(() -> nextRef.setReference(previousReference));
171                                }
172                                nextRef.setReference(newId.getValue());
173                        }
174                }
175        }
176
177        // Removes parts of the reference keeping only the valuable parts, the resource type and searchparam
178        private static String truncateReference(String nextIdText, int qmIndex) {
179                for (int i = qmIndex - 1; i >= 0; i--) {
180                        if (nextIdText.charAt(i) == '/') {
181                                if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
182                                        // Just in case the URL is in the form Patient/?foo=bar
183                                        continue;
184                                }
185                                nextIdText = nextIdText.substring(i + 1);
186                                break;
187                        }
188                }
189                return nextIdText;
190        }
191
192        @Autowired
193        public void setStorageSettings(JpaStorageSettings theStorageSettings) {
194                myStorageSettings = theStorageSettings;
195        }
196
197        @Autowired
198        public void setContext(FhirContext theContext) {
199                myFhirContext = theContext;
200        }
201}