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
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                        IIdType resourceId = resource.getIdElement();
090                        bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams);
091                });
092                return bundleBuilder.getBundleTyped();
093        }
094
095        private @Nonnull Parameters buildPatchParams(
096                        ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) {
097                Parameters params = new Parameters();
098
099                myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream()
100                                .filter(refInfo -> matches(
101                                                refInfo,
102                                                theReplaceReferencesRequest.sourceId)) // We only care about references to our source resource
103                                .map(refInfo -> createReplaceReferencePatchOperation(
104                                                referencingResource.fhirType() + "." + refInfo.getName(),
105                                                new Reference(theReplaceReferencesRequest.targetId.getValueAsString())))
106                                .forEach(params::addParameter); // Add each operation to parameters
107                return params;
108        }
109
110        private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) {
111                return refInfo.getResourceReference()
112                                .getReferenceElement()
113                                .toUnqualifiedVersionless()
114                                .getValueAsString()
115                                .equals(theSourceId.getValueAsString());
116        }
117
118        @Nonnull
119        private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation(
120                        String thePath, Type theValue) {
121
122                Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent();
123                operation.setName(PARAMETER_OPERATION);
124                operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE));
125                operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath));
126                operation.addPart().setName(PARAMETER_VALUE).setValue(theValue);
127                return operation;
128        }
129}