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.dao.index;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.RuntimeResourceDefinition;
027import ca.uhn.fhir.context.RuntimeSearchParam;
028import ca.uhn.fhir.i18n.Msg;
029import ca.uhn.fhir.interceptor.model.RequestPartitionId;
030import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
031import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
032import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
033import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
034import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
035import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
036import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
037import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
038import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
039import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
040import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
041import ca.uhn.fhir.rest.api.server.RequestDetails;
042import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
043import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
044import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
045import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
046import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
047import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
048import ca.uhn.fhir.util.CanonicalIdentifier;
049import ca.uhn.fhir.util.HapiExtensions;
050import ca.uhn.fhir.util.TerserUtil;
051import jakarta.annotation.Nonnull;
052import jakarta.annotation.Nullable;
053import org.apache.http.NameValuePair;
054import org.apache.http.client.utils.URLEncodedUtils;
055import org.hl7.fhir.instance.model.api.IBase;
056import org.hl7.fhir.instance.model.api.IBaseExtension;
057import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
058import org.hl7.fhir.instance.model.api.IBaseReference;
059import org.hl7.fhir.instance.model.api.IBaseResource;
060import org.hl7.fhir.instance.model.api.IIdType;
061import org.hl7.fhir.instance.model.api.IPrimitiveType;
062import org.springframework.beans.factory.annotation.Autowired;
063
064import java.nio.charset.StandardCharsets;
065import java.util.Date;
066import java.util.List;
067import java.util.Optional;
068
069public class DaoResourceLinkResolver<T extends IResourcePersistentId<?>> implements IResourceLinkResolver {
070        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class);
071
072        @Autowired
073        private JpaStorageSettings myStorageSettings;
074
075        @Autowired
076        private FhirContext myContext;
077
078        @Autowired
079        private IIdHelperService<T> myIdHelperService;
080
081        @Autowired
082        private DaoRegistry myDaoRegistry;
083
084        @Autowired
085        private ISearchParamRegistry mySearchParamRegistry;
086
087        @Autowired
088        private IHapiTransactionService myTransactionService;
089
090        @Override
091        public IResourceLookup findTargetResource(
092                        @Nonnull RequestPartitionId theRequestPartitionId,
093                        String theSourceResourceName,
094                        PathAndRef thePathAndRef,
095                        RequestDetails theRequest,
096                        TransactionDetails theTransactionDetails) {
097
098                IBaseReference targetReference = thePathAndRef.getRef();
099                String sourcePath = thePathAndRef.getPath();
100
101                IIdType targetResourceId = targetReference.getReferenceElement();
102                if (targetResourceId.isEmpty() && targetReference.getResource() != null) {
103                        targetResourceId = targetReference.getResource().getIdElement();
104                }
105
106                String resourceType = targetResourceId.getResourceType();
107                RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(resourceType);
108                Class<? extends IBaseResource> type = resourceDef.getImplementingClass();
109
110                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(
111                                theSourceResourceName,
112                                thePathAndRef.getSearchParamName(),
113                                ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
114
115                T persistentId = null;
116                if (theTransactionDetails != null) {
117                        T resolvedResourceId = (T) theTransactionDetails.getResolvedResourceId(targetResourceId);
118                        if (resolvedResourceId != null
119                                        && resolvedResourceId.getId() != null
120                                        && resolvedResourceId.getAssociatedResourceId() != null) {
121                                persistentId = resolvedResourceId;
122                        }
123                }
124
125                IResourceLookup<?> resolvedResource;
126                String idPart = targetResourceId.getIdPart();
127                try {
128                        if (persistentId == null) {
129
130                                // If we previously looked up the ID, and it was not found, don't bother
131                                // looking it up again
132                                if (theTransactionDetails != null
133                                                && theTransactionDetails.hasNullResolvedResourceId(targetResourceId)) {
134                                        throw new ResourceNotFoundException(Msg.code(2602));
135                                }
136
137                                resolvedResource = myIdHelperService.resolveResourceIdentity(
138                                                theRequestPartitionId,
139                                                resourceType,
140                                                idPart,
141                                                ResolveIdentityMode.excludeDeleted().noCacheUnlessDeletesDisabled());
142                                ourLog.trace("Translated {}/{} to resource PID {}", type, idPart, resolvedResource);
143                        } else {
144                                resolvedResource = new ResourceLookupPersistentIdWrapper<>(persistentId);
145                        }
146                } catch (ResourceNotFoundException e) {
147
148                        Optional<IBasePersistedResource> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(
149                                        type, targetReference, idPart, theRequest, theTransactionDetails);
150                        if (!createdTableOpt.isPresent()) {
151
152                                if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
153                                        return null;
154                                }
155
156                                RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type);
157                                String resName = missingResourceDef.getName();
158
159                                // Check if this was a deleted resource
160                                try {
161                                        resolvedResource = myIdHelperService.resolveResourceIdentity(
162                                                        theRequestPartitionId,
163                                                        resourceType,
164                                                        idPart,
165                                                        ResolveIdentityMode.includeDeleted().noCacheUnlessDeletesDisabled());
166                                        handleDeletedTarget(resourceType, idPart, sourcePath);
167                                } catch (ResourceNotFoundException e2) {
168                                        resolvedResource = null;
169                                }
170
171                                if (resolvedResource == null) {
172                                        throw new InvalidRequestException(Msg.code(1094) + "Resource " + resName + "/" + idPart
173                                                        + " not found, specified in path: " + sourcePath);
174                                }
175                        }
176
177                        resolvedResource = createdTableOpt.get();
178                }
179
180                ourLog.trace(
181                                "Resolved resource of type {} as PID: {}",
182                                resolvedResource.getResourceType(),
183                                resolvedResource.getPersistentId());
184                if (!validateResolvedResourceOrThrow(resourceType, resolvedResource, targetResourceId, idPart, sourcePath)) {
185                        return null;
186                }
187
188                if (persistentId == null) {
189                        Object id = resolvedResource.getPersistentId().getId();
190                        Integer partitionId = null;
191                        if (resolvedResource.getPartitionId() != null) {
192                                partitionId = resolvedResource.getPartitionId().getPartitionId();
193                        }
194                        persistentId = myIdHelperService.newPid(id, partitionId);
195                        persistentId.setAssociatedResourceId(targetResourceId);
196                        if (theTransactionDetails != null) {
197                                theTransactionDetails.addResolvedResourceId(targetResourceId, persistentId);
198                        }
199                }
200
201                if (!searchParam.hasTargets() && searchParam.getTargets().contains(resourceType)) {
202                        return null;
203                }
204
205                return resolvedResource;
206        }
207
208        /**
209         * Validates the resolved resource.
210         * If 'Enforce Referential Integrity on Write' is enabled:
211         * Throws <code>UnprocessableEntityException</code> when resource types do not match
212         * Throws <code>InvalidRequestException</code> when the resolved resource was deleted
213         * <p>
214         * Otherwise, return false when resource types do not match or resource was deleted
215         * and return true if the resolved resource is valid.
216         */
217        private boolean validateResolvedResourceOrThrow(
218                        String resourceType,
219                        IResourceLookup resolvedResource,
220                        IIdType targetResourceId,
221                        String idPart,
222                        String sourcePath) {
223                if (!resourceType.equals(resolvedResource.getResourceType())) {
224                        ourLog.error(
225                                        "Resource with PID {} was of type {} and wanted {}",
226                                        resolvedResource.getPersistentId(),
227                                        resourceType,
228                                        resolvedResource.getResourceType());
229                        if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
230                                return false;
231                        }
232                        throw new UnprocessableEntityException(Msg.code(1095)
233                                        + "Resource contains reference to unknown resource ID " + targetResourceId.getValue());
234                }
235
236                if (resolvedResource.getDeleted() != null) {
237                        return handleDeletedTarget(resolvedResource.getResourceType(), idPart, sourcePath);
238                }
239                return true;
240        }
241
242        private boolean handleDeletedTarget(String resType, String idPart, String sourcePath) {
243                if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) {
244                        return false;
245                }
246                String resName = resType;
247                throw new InvalidRequestException(Msg.code(1096) + "Resource " + resName + "/" + idPart
248                                + " is deleted, specified in path: " + sourcePath);
249        }
250
251        @Nullable
252        @Override
253        public IBaseResource loadTargetResource(
254                        @Nonnull RequestPartitionId theRequestPartitionId,
255                        String theSourceResourceName,
256                        PathAndRef thePathAndRef,
257                        RequestDetails theRequest,
258                        TransactionDetails theTransactionDetails) {
259                return myTransactionService
260                                .withRequest(theRequest)
261                                .withTransactionDetails(theTransactionDetails)
262                                .withRequestPartitionId(theRequestPartitionId)
263                                .execute(() -> {
264                                        IIdType targetId = thePathAndRef.getRef().getReferenceElement();
265                                        IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(targetId.getResourceType());
266                                        return dao.read(targetId, theRequest);
267                                });
268        }
269
270        /**
271         * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
272         */
273        public <T extends IBaseResource> Optional<IBasePersistedResource> createPlaceholderTargetIfConfiguredToDoSo(
274                        Class<T> theType,
275                        IBaseReference theReference,
276                        @Nullable String theIdToAssignToPlaceholder,
277                        RequestDetails theRequest,
278                        TransactionDetails theTransactionDetails) {
279                IBasePersistedResource valueOf = null;
280
281                if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) {
282                        RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
283                        String resName = missingResourceDef.getName();
284
285                        @SuppressWarnings("unchecked")
286                        T newResource = (T) missingResourceDef.newInstance();
287
288                        tryToAddPlaceholderExtensionToResource(newResource);
289
290                        IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType);
291                        ourLog.debug(
292                                        "Automatically creating empty placeholder resource: {}",
293                                        newResource.getIdElement().getValue());
294
295                        if (myStorageSettings.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()) {
296                                tryToCopyIdentifierFromReferenceToTargetResource(theReference, missingResourceDef, newResource);
297                        }
298
299                        if (theIdToAssignToPlaceholder != null) {
300                                if (theTransactionDetails != null) {
301                                        String existingId = newResource.getIdElement().getValue();
302                                        theTransactionDetails.addRollbackUndoAction(() -> newResource.setId(existingId));
303                                }
304                                newResource.setId(resName + "/" + theIdToAssignToPlaceholder);
305                                valueOf = placeholderResourceDao
306                                                .update(newResource, null, true, false, theRequest, theTransactionDetails)
307                                                .getEntity();
308                        } else {
309                                valueOf = placeholderResourceDao.create(newResource, theRequest).getEntity();
310                        }
311
312                        IResourcePersistentId persistentId = valueOf.getPersistentId();
313                        persistentId = myIdHelperService.newPid(persistentId.getId());
314                        persistentId.setAssociatedResourceId(valueOf.getIdDt());
315                        theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
316                        theTransactionDetails.addAutoCreatedPlaceholderResource(newResource.getIdElement());
317                }
318
319                return Optional.ofNullable(valueOf);
320        }
321
322        private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) {
323                if (newResource instanceof IBaseHasExtensions) {
324                        IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
325                        extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
326                        extension.setValue(myContext.newPrimitiveBoolean(true));
327                }
328        }
329
330        private <T extends IBaseResource> void tryToCopyIdentifierFromReferenceToTargetResource(
331                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
332                //              boolean referenceHasIdentifier = theSourceReference.hasIdentifier();
333                CanonicalIdentifier referenceMatchUrlIdentifier = extractIdentifierFromUrl(
334                                theSourceReference.getReferenceElement().getValue());
335                CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference);
336
337                if (referenceIdentifier == null && referenceMatchUrlIdentifier != null) {
338                        addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
339                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier == null) {
340                        addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
341                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier != null) {
342                        if (referenceIdentifier.equals(referenceMatchUrlIdentifier)) {
343                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
344                        } else {
345                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
346                                addMatchUrlIdentifierToTargetResource(
347                                                theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
348                        }
349                }
350        }
351
352        private <T extends IBaseResource> void addSubjectIdentifierToTargetResource(
353                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
354                BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
355                if (targetIdentifier != null) {
356                        BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
357                        String identifierElementName = identifierElement.getName();
358                        boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
359                        if (targetHasIdentifierElement) {
360
361                                BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>)
362                                                myContext.getElementDefinition(theSourceReference.getClass());
363                                BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
364                                Optional<IBase> identifierOpt =
365                                                referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
366                                identifierOpt.ifPresent(
367                                                theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
368                        }
369                }
370        }
371
372        private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource(
373                        RuntimeResourceDefinition theTargetResourceDef,
374                        T theTargetResource,
375                        CanonicalIdentifier referenceMatchUrlIdentifier) {
376                BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier");
377                IBase identifierIBase = identifierDefinition
378                                .getChildByName("identifier")
379                                .newInstance(identifierDefinition.getInstanceConstructorArguments());
380                IBase systemIBase = TerserUtil.newElement(
381                                myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString());
382                IBase valueIBase = TerserUtil.newElement(
383                                myContext,
384                                "string",
385                                referenceMatchUrlIdentifier.getValueElement().getValueAsString());
386                // Set system in the IBase Identifier
387
388                BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass());
389
390                BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system");
391                systemDefinition.getMutator().setValue(identifierIBase, systemIBase);
392
393                BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value");
394                valueDefinition.getMutator().setValue(identifierIBase, valueIBase);
395
396                // Set Value in the IBase identifier
397                identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase);
398        }
399
400        private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) {
401                Optional<IBase> identifier =
402                                myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class);
403                if (!identifier.isPresent()) {
404                        return null;
405                } else {
406                        CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier();
407                        Optional<IPrimitiveType> system =
408                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class);
409                        Optional<IPrimitiveType> value =
410                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class);
411
412                        system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString()));
413                        value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString()));
414                        return canonicalIdentifier;
415                }
416        }
417
418        /**
419         * Extracts the first available identifier from the URL part
420         *
421         * @param theValue Part of the URL to extract identifiers from
422         * @return Returns the first available identifier in the canonical form or null if URL contains no identifier param
423         * @throws IllegalArgumentException IllegalArgumentException is thrown in case identifier parameter can not be split using <code>system|value</code> pattern.
424         */
425        protected CanonicalIdentifier extractIdentifierFromUrl(String theValue) {
426                int identifierIndex = theValue.indexOf("identifier=");
427                if (identifierIndex == -1) {
428                        return null;
429                }
430
431                List<NameValuePair> params =
432                                URLEncodedUtils.parse(theValue.substring(identifierIndex), StandardCharsets.UTF_8, '&', ';');
433                Optional<NameValuePair> idOptional =
434                                params.stream().filter(p -> p.getName().equals("identifier")).findFirst();
435                if (!idOptional.isPresent()) {
436                        return null;
437                }
438
439                NameValuePair id = idOptional.get();
440                String identifierString = id.getValue();
441                String[] split = identifierString.split("\\|");
442                if (split.length != 2) {
443                        throw new IllegalArgumentException(Msg.code(1097) + "Can't create a placeholder reference with identifier "
444                                        + theValue + ". It is not a valid identifier");
445                }
446
447                CanonicalIdentifier identifier = new CanonicalIdentifier();
448                identifier.setSystem(split[0]);
449                identifier.setValue(split[1]);
450                return identifier;
451        }
452
453        @Override
454        public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
455                myDaoRegistry.getDaoOrThrowException(theType);
456        }
457
458        private static class ResourceLookupPersistentIdWrapper<P extends IResourcePersistentId> implements IResourceLookup {
459                private final P myPersistentId;
460
461                public ResourceLookupPersistentIdWrapper(P thePersistentId) {
462                        myPersistentId = thePersistentId;
463                }
464
465                @Override
466                public String getResourceType() {
467                        return myPersistentId.getAssociatedResourceId().getResourceType();
468                }
469
470                @Override
471                public String getFhirId() {
472                        return myPersistentId.getAssociatedResourceId().getIdPart();
473                }
474
475                @Override
476                public Date getDeleted() {
477                        return null;
478                }
479
480                @Override
481                public P getPersistentId() {
482                        return myPersistentId;
483                }
484
485                @Override
486                public PartitionablePartitionId getPartitionId() {
487                        return new PartitionablePartitionId(myPersistentId.getPartitionId(), null);
488                }
489        }
490}