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