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