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.replacereferences;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
025import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
026import ca.uhn.fhir.model.primitive.IdDt;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.util.BundleBuilder;
029import ca.uhn.fhir.util.ResourceReferenceInfo;
030import jakarta.annotation.Nonnull;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032import org.hl7.fhir.instance.model.api.IIdType;
033import org.hl7.fhir.r4.model.Bundle;
034import org.hl7.fhir.r4.model.CodeType;
035import org.hl7.fhir.r4.model.Meta;
036import org.hl7.fhir.r4.model.Parameters;
037import org.hl7.fhir.r4.model.Reference;
038import org.hl7.fhir.r4.model.StringType;
039import org.hl7.fhir.r4.model.Type;
040
041import java.util.List;
042import java.util.UUID;
043
044import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE;
045import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION;
046import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH;
047import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE;
048import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE;
049
050public class ReplaceReferencesPatchBundleSvc {
051
052        private final FhirContext myFhirContext;
053        private final DaoRegistry myDaoRegistry;
054
055        public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) {
056                myDaoRegistry = theDaoRegistry;
057                myFhirContext = theDaoRegistry.getFhirContext();
058        }
059
060        /**
061         * Build a bundle of PATCH entries that make the requested reference updates
062         * @param theReplaceReferencesRequest source and target for reference switch
063         * @param theResourceIds the ids of the resource to create the patch entries for (they will all have references to the source resource)
064         * @param theRequestDetails
065         * @return
066         */
067        public Bundle patchReferencingResources(
068                        ReplaceReferencesRequest theReplaceReferencesRequest,
069                        List<IdDt> theResourceIds,
070                        RequestDetails theRequestDetails) {
071                Bundle patchBundle = buildPatchBundle(theReplaceReferencesRequest, theResourceIds, theRequestDetails);
072                IFhirSystemDao<Bundle, Meta> systemDao = myDaoRegistry.getSystemDao();
073                Bundle result = systemDao.transaction(theRequestDetails, patchBundle);
074
075                result.setId(UUID.randomUUID().toString());
076                return result;
077        }
078
079        private Bundle buildPatchBundle(
080                        ReplaceReferencesRequest theReplaceReferencesRequest,
081                        List<IdDt> theResourceIds,
082                        RequestDetails theRequestDetails) {
083                BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext);
084                theResourceIds.forEach(referencingResourceId -> {
085                        IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType());
086                        IBaseResource resource = dao.read(referencingResourceId, theRequestDetails);
087                        Parameters patchParams = buildPatchParams(theReplaceReferencesRequest, resource);
088                        IIdType resourceId = resource.getIdElement();
089                        bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams);
090                });
091                return bundleBuilder.getBundleTyped();
092        }
093
094        private @Nonnull Parameters buildPatchParams(
095                        ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) {
096                Parameters params = new Parameters();
097
098                myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream()
099                                .filter(refInfo -> matches(
100                                                refInfo,
101                                                theReplaceReferencesRequest.sourceId)) // We only care about references to our source resource
102                                .map(refInfo -> createReplaceReferencePatchOperation(
103                                                getFhirPathForPatch(referencingResource, refInfo),
104                                                new Reference(theReplaceReferencesRequest.targetId.getValueAsString())))
105                                .forEach(params::addParameter); // Add each operation to parameters
106                return params;
107        }
108
109        private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) {
110                return refInfo.getResourceReference()
111                                .getReferenceElement()
112                                .toUnqualified()
113                                .getValueAsString()
114                                .equals(theSourceId.getValueAsString());
115        }
116
117        private String getFhirPathForPatch(IBaseResource theReferencingResource, ResourceReferenceInfo theRefInfo) {
118                // construct the path to the element containing the reference in the resource, e.g. "Observation.subject"
119                String path = theReferencingResource.fhirType() + "." + theRefInfo.getName();
120                // check the allowed cardinality of the element containing the reference
121                int maxCardinality = myFhirContext
122                                .newTerser()
123                                .getDefinition(theReferencingResource.getClass(), path)
124                                .getMax();
125                if (maxCardinality != 1) {
126                        // if the element allows high cardinality, specify the exact reference to replace by appending a where
127                        // filter to the path. If we don't do this, all the existing references in the element would be lost as a
128                        // result of getting replaced with the new reference, and that is not the behaviour we want.
129                        // e.g. "Observation.performer.where(reference='Practitioner/123')"
130                        return String.format(
131                                        "%s.where(reference='%s')",
132                                        path,
133                                        theRefInfo.getResourceReference().getReferenceElement().getValueAsString());
134                }
135                // the element allows max cardinality of 1, so the whole element can be safely replaced
136                return path;
137        }
138
139        @Nonnull
140        private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation(
141                        String thePath, Type theValue) {
142
143                Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent();
144                operation.setName(PARAMETER_OPERATION);
145                operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE));
146                operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath));
147                operation.addPart().setName(PARAMETER_VALUE).setValue(theValue);
148                return operation;
149        }
150}