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