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 =
109                                mySearchParamRegistry.getActiveSearchParam(theSourceResourceName, thePathAndRef.getSearchParamName());
110
111                T persistentId = null;
112                if (theTransactionDetails != null) {
113                        T resolvedResourceId = (T) theTransactionDetails.getResolvedResourceId(targetResourceId);
114                        if (resolvedResourceId != null
115                                        && resolvedResourceId.getId() != null
116                                        && resolvedResourceId.getAssociatedResourceId() != null) {
117                                persistentId = resolvedResourceId;
118                        }
119                }
120
121                IResourceLookup resolvedResource;
122                String idPart = targetResourceId.getIdPart();
123                try {
124                        if (persistentId == null) {
125                                resolvedResource =
126                                                myIdHelperService.resolveResourceIdentity(theRequestPartitionId, resourceType, idPart);
127                                ourLog.trace("Translated {}/{} to resource PID {}", type, idPart, resolvedResource);
128                        } else {
129                                resolvedResource = new ResourceLookupPersistentIdWrapper(persistentId);
130                        }
131                } catch (ResourceNotFoundException e) {
132
133                        Optional<IBasePersistedResource> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo(
134                                        type, targetReference, idPart, theRequest, theTransactionDetails);
135                        if (!createdTableOpt.isPresent()) {
136
137                                if (myStorageSettings.isEnforceReferentialIntegrityOnWrite() == false) {
138                                        return null;
139                                }
140
141                                RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type);
142                                String resName = missingResourceDef.getName();
143                                throw new InvalidRequestException(Msg.code(1094) + "Resource " + resName + "/" + idPart
144                                                + " not found, specified in path: " + sourcePath);
145                        }
146                        resolvedResource = createdTableOpt.get();
147                }
148
149                ourLog.trace(
150                                "Resolved resource of type {} as PID: {}",
151                                resolvedResource.getResourceType(),
152                                resolvedResource.getPersistentId());
153                if (!resourceType.equals(resolvedResource.getResourceType())) {
154                        ourLog.error(
155                                        "Resource with PID {} was of type {} and wanted {}",
156                                        resolvedResource.getPersistentId(),
157                                        resourceType,
158                                        resolvedResource.getResourceType());
159                        throw new UnprocessableEntityException(Msg.code(1095)
160                                        + "Resource contains reference to unknown resource ID " + targetResourceId.getValue());
161                }
162
163                if (resolvedResource.getDeleted() != null) {
164                        String resName = resolvedResource.getResourceType();
165                        throw new InvalidRequestException(Msg.code(1096) + "Resource " + resName + "/" + idPart
166                                        + " is deleted, specified in path: " + sourcePath);
167                }
168
169                if (persistentId == null) {
170                        persistentId =
171                                        myIdHelperService.newPid(resolvedResource.getPersistentId().getId());
172                        persistentId.setAssociatedResourceId(targetResourceId);
173                        if (theTransactionDetails != null) {
174                                theTransactionDetails.addResolvedResourceId(targetResourceId, persistentId);
175                        }
176                }
177
178                if (!searchParam.hasTargets() && searchParam.getTargets().contains(resourceType)) {
179                        return null;
180                }
181
182                return resolvedResource;
183        }
184
185        @Nullable
186        @Override
187        public IBaseResource loadTargetResource(
188                        @Nonnull RequestPartitionId theRequestPartitionId,
189                        String theSourceResourceName,
190                        PathAndRef thePathAndRef,
191                        RequestDetails theRequest,
192                        TransactionDetails theTransactionDetails) {
193                return myTransactionService
194                                .withRequest(theRequest)
195                                .withTransactionDetails(theTransactionDetails)
196                                .withRequestPartitionId(theRequestPartitionId)
197                                .execute(() -> {
198                                        IIdType targetId = thePathAndRef.getRef().getReferenceElement();
199                                        IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(targetId.getResourceType());
200                                        return dao.read(targetId, theRequest);
201                                });
202        }
203
204        /**
205         * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID
206         */
207        public <T extends IBaseResource> Optional<IBasePersistedResource> createPlaceholderTargetIfConfiguredToDoSo(
208                        Class<T> theType,
209                        IBaseReference theReference,
210                        @Nullable String theIdToAssignToPlaceholder,
211                        RequestDetails theRequest,
212                        TransactionDetails theTransactionDetails) {
213                IBasePersistedResource valueOf = null;
214
215                if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) {
216                        RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType);
217                        String resName = missingResourceDef.getName();
218
219                        @SuppressWarnings("unchecked")
220                        T newResource = (T) missingResourceDef.newInstance();
221
222                        tryToAddPlaceholderExtensionToResource(newResource);
223
224                        IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType);
225                        ourLog.debug(
226                                        "Automatically creating empty placeholder resource: {}",
227                                        newResource.getIdElement().getValue());
228
229                        if (myStorageSettings.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()) {
230                                tryToCopyIdentifierFromReferenceToTargetResource(theReference, missingResourceDef, newResource);
231                        }
232
233                        if (theIdToAssignToPlaceholder != null) {
234                                if (theTransactionDetails != null) {
235                                        String existingId = newResource.getIdElement().getValue();
236                                        theTransactionDetails.addRollbackUndoAction(() -> newResource.setId(existingId));
237                                }
238                                newResource.setId(resName + "/" + theIdToAssignToPlaceholder);
239                                valueOf = placeholderResourceDao.update(newResource, theRequest).getEntity();
240                        } else {
241                                valueOf = placeholderResourceDao.create(newResource, theRequest).getEntity();
242                        }
243
244                        IResourcePersistentId persistentId = valueOf.getPersistentId();
245                        persistentId = myIdHelperService.newPid(persistentId.getId());
246                        persistentId.setAssociatedResourceId(valueOf.getIdDt());
247                        theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
248                }
249
250                return Optional.ofNullable(valueOf);
251        }
252
253        private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) {
254                if (newResource instanceof IBaseHasExtensions) {
255                        IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
256                        extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
257                        extension.setValue(myContext.getPrimitiveBoolean(true));
258                }
259        }
260
261        private <T extends IBaseResource> void tryToCopyIdentifierFromReferenceToTargetResource(
262                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
263                //              boolean referenceHasIdentifier = theSourceReference.hasIdentifier();
264                CanonicalIdentifier referenceMatchUrlIdentifier = extractIdentifierFromUrl(
265                                theSourceReference.getReferenceElement().getValue());
266                CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference);
267
268                if (referenceIdentifier == null && referenceMatchUrlIdentifier != null) {
269                        addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
270                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier == null) {
271                        addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
272                } else if (referenceIdentifier != null && referenceMatchUrlIdentifier != null) {
273                        if (referenceIdentifier.equals(referenceMatchUrlIdentifier)) {
274                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
275                        } else {
276                                addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
277                                addMatchUrlIdentifierToTargetResource(
278                                                theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
279                        }
280                }
281        }
282
283        private <T extends IBaseResource> void addSubjectIdentifierToTargetResource(
284                        IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
285                BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
286                if (targetIdentifier != null) {
287                        BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
288                        String identifierElementName = identifierElement.getName();
289                        boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
290                        if (targetHasIdentifierElement) {
291
292                                BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>)
293                                                myContext.getElementDefinition(theSourceReference.getClass());
294                                BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
295                                Optional<IBase> identifierOpt =
296                                                referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
297                                identifierOpt.ifPresent(
298                                                theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
299                        }
300                }
301        }
302
303        private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource(
304                        RuntimeResourceDefinition theTargetResourceDef,
305                        T theTargetResource,
306                        CanonicalIdentifier referenceMatchUrlIdentifier) {
307                BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier");
308                IBase identifierIBase = identifierDefinition
309                                .getChildByName("identifier")
310                                .newInstance(identifierDefinition.getInstanceConstructorArguments());
311                IBase systemIBase = TerserUtil.newElement(
312                                myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString());
313                IBase valueIBase = TerserUtil.newElement(
314                                myContext,
315                                "string",
316                                referenceMatchUrlIdentifier.getValueElement().getValueAsString());
317                // Set system in the IBase Identifier
318
319                BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass());
320
321                BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system");
322                systemDefinition.getMutator().setValue(identifierIBase, systemIBase);
323
324                BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value");
325                valueDefinition.getMutator().setValue(identifierIBase, valueIBase);
326
327                // Set Value in the IBase identifier
328                identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase);
329        }
330
331        private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) {
332                Optional<IBase> identifier =
333                                myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class);
334                if (!identifier.isPresent()) {
335                        return null;
336                } else {
337                        CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier();
338                        Optional<IPrimitiveType> system =
339                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class);
340                        Optional<IPrimitiveType> value =
341                                        myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class);
342
343                        system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString()));
344                        value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString()));
345                        return canonicalIdentifier;
346                }
347        }
348
349        /**
350         * Extracts the first available identifier from the URL part
351         *
352         * @param theValue Part of the URL to extract identifiers from
353         * @return Returns the first available identifier in the canonical form or null if URL contains no identifier param
354         * @throws IllegalArgumentException IllegalArgumentException is thrown in case identifier parameter can not be split using <code>system|value</code> pattern.
355         */
356        protected CanonicalIdentifier extractIdentifierFromUrl(String theValue) {
357                int identifierIndex = theValue.indexOf("identifier=");
358                if (identifierIndex == -1) {
359                        return null;
360                }
361
362                List<NameValuePair> params =
363                                URLEncodedUtils.parse(theValue.substring(identifierIndex), StandardCharsets.UTF_8, '&', ';');
364                Optional<NameValuePair> idOptional =
365                                params.stream().filter(p -> p.getName().equals("identifier")).findFirst();
366                if (!idOptional.isPresent()) {
367                        return null;
368                }
369
370                NameValuePair id = idOptional.get();
371                String identifierString = id.getValue();
372                String[] split = identifierString.split("\\|");
373                if (split.length != 2) {
374                        throw new IllegalArgumentException(Msg.code(1097) + "Can't create a placeholder reference with identifier "
375                                        + theValue + ". It is not a valid identifier");
376                }
377
378                CanonicalIdentifier identifier = new CanonicalIdentifier();
379                identifier.setSystem(split[0]);
380                identifier.setValue(split[1]);
381                return identifier;
382        }
383
384        @Override
385        public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
386                myDaoRegistry.getDaoOrThrowException(theType);
387        }
388
389        private static class ResourceLookupPersistentIdWrapper<P extends IResourcePersistentId> implements IResourceLookup {
390                private final P myPersistentId;
391
392                public ResourceLookupPersistentIdWrapper(P thePersistentId) {
393                        myPersistentId = thePersistentId;
394                }
395
396                @Override
397                public String getResourceType() {
398                        return myPersistentId.getAssociatedResourceId().getResourceType();
399                }
400
401                @Override
402                public Date getDeleted() {
403                        return null;
404                }
405
406                @Override
407                public P getPersistentId() {
408                        return myPersistentId;
409                }
410        }
411}