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.model.api.IProvenanceAgent;
026import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.util.FhirTerser;
029import jakarta.annotation.Nullable;
030import org.hl7.fhir.instance.model.api.IBaseReference;
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.CodeableConcept;
035import org.hl7.fhir.r4.model.Period;
036import org.hl7.fhir.r4.model.Provenance;
037import org.hl7.fhir.r4.model.Reference;
038
039import java.util.ArrayList;
040import java.util.Date;
041import java.util.List;
042
043/**
044 * This service is used to create a Provenance resource for the $replace-references operation
045 * and also used as a base class for {@link ca.uhn.fhir.merge.MergeProvenanceSvc} used in $merge operations.
046 * The two operations use different activity codes.
047 */
048public class ReplaceReferencesProvenanceSvc {
049
050        private static final String ACT_REASON_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v3-ActReason";
051        private static final String ACT_REASON_PATIENT_ADMINISTRATION_CODE = "PATADMIN";
052        protected static final String ACTIVITY_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle";
053        private static final String ACTIVITY_CODE_LINK = "link";
054        private final IFhirResourceDao<IBaseResource> myProvenanceDao;
055        private final FhirContext myFhirContext;
056
057        public ReplaceReferencesProvenanceSvc(DaoRegistry theDaoRegistry) {
058                myProvenanceDao = theDaoRegistry.getResourceDao("Provenance");
059                myFhirContext = theDaoRegistry.getFhirContext();
060        }
061
062        @Nullable
063        protected CodeableConcept getActivityCodeableConcept() {
064                CodeableConcept retVal = new CodeableConcept();
065                retVal.addCoding().setSystem(ACTIVITY_CODE_SYSTEM).setCode(ACTIVITY_CODE_LINK);
066                return retVal;
067        }
068
069        protected Provenance createProvenanceObject(
070                        Reference theTargetReference,
071                        @Nullable Reference theSourceReference,
072                        List<Reference> theUpdatedReferencingResources,
073                        Date theStartTime,
074                        List<IProvenanceAgent> theProvenanceAgents) {
075                Provenance provenance = new Provenance();
076
077                Date now = new Date();
078                provenance.setOccurred(new Period()
079                                .setStart(theStartTime, TemporalPrecisionEnum.MILLI)
080                                .setEnd(now, TemporalPrecisionEnum.MILLI));
081                provenance.setRecorded(now);
082
083                addAgents(theProvenanceAgents, provenance);
084
085                CodeableConcept activityCodeableConcept = getActivityCodeableConcept();
086                if (activityCodeableConcept != null) {
087                        provenance.setActivity(activityCodeableConcept);
088                }
089                CodeableConcept activityReasonCodeableConcept = new CodeableConcept();
090                activityReasonCodeableConcept
091                                .addCoding()
092                                .setSystem(ACT_REASON_CODE_SYSTEM)
093                                .setCode(ACT_REASON_PATIENT_ADMINISTRATION_CODE);
094
095                provenance.addReason(activityReasonCodeableConcept);
096
097                provenance.addTarget(theTargetReference);
098                if (theSourceReference != null) {
099                        provenance.addTarget(theSourceReference);
100                }
101
102                theUpdatedReferencingResources.forEach(provenance::addTarget);
103                return provenance;
104        }
105
106        /**
107         * Creates a Provenance resource for the $replace-references and $merge operations.
108         *
109         * @param theTargetId           the versioned id of the target resource of the operation.
110         * @param theSourceId           the versioned id of the source resource of the operation. Can be null if the operation is $merge and the source resource is deleted.
111         * @param thePatchResultBundles the list of patch result bundles that contain the updated resources.
112         * @param theStartTime          the start time of the operation.
113         * @param theRequestDetails     the request details
114         * @param theProvenanceAgents   the list of agents to be included in the Provenance resource.
115         */
116        public void createProvenance(
117                        IIdType theTargetId,
118                        @Nullable IIdType theSourceId,
119                        List<Bundle> thePatchResultBundles,
120                        Date theStartTime,
121                        RequestDetails theRequestDetails,
122                        List<IProvenanceAgent> theProvenanceAgents) {
123                Reference targetReference = new Reference(theTargetId);
124                Reference sourceReference = null;
125                if (theSourceId != null) {
126                        sourceReference = new Reference(theSourceId);
127                }
128                List<Reference> references = extractUpdatedResourceReferences(thePatchResultBundles);
129                Provenance provenance =
130                                createProvenanceObject(targetReference, sourceReference, references, theStartTime, theProvenanceAgents);
131                myProvenanceDao.create(provenance, theRequestDetails);
132        }
133
134        protected List<Reference> extractUpdatedResourceReferences(List<Bundle> thePatchBundles) {
135                List<Reference> patchedResourceReferences = new ArrayList<>();
136                thePatchBundles.forEach(outputBundle -> {
137                        outputBundle.getEntry().forEach(entry -> {
138                                if (entry.getResponse() != null && entry.getResponse().hasLocation()) {
139                                        Reference reference = new Reference(entry.getResponse().getLocation());
140                                        patchedResourceReferences.add(reference);
141                                }
142                        });
143                });
144                return patchedResourceReferences;
145        }
146
147        private Provenance.ProvenanceAgentComponent createR4ProvenanceAgent(IProvenanceAgent theProvenanceAgent) {
148                Provenance.ProvenanceAgentComponent agent = new Provenance.ProvenanceAgentComponent();
149                Reference whoRef = convertToR4Reference(theProvenanceAgent.getWho());
150                agent.setWho(whoRef);
151                Reference onBehalfOfRef = convertToR4Reference(theProvenanceAgent.getOnBehalfOf());
152                agent.setOnBehalfOf(onBehalfOfRef);
153                return agent;
154        }
155
156        private void addAgents(List<IProvenanceAgent> theProvenanceAgents, Provenance theProvenance) {
157                if (theProvenanceAgents != null) {
158                        for (IProvenanceAgent agent : theProvenanceAgents) {
159                                Provenance.ProvenanceAgentComponent r4Agent = createR4ProvenanceAgent(agent);
160                                theProvenance.addAgent(r4Agent);
161                        }
162                }
163        }
164
165        private Reference convertToR4Reference(IBaseReference sourceRef) {
166                if (sourceRef == null) {
167                        return null;
168                }
169                FhirTerser terser = myFhirContext.newTerser();
170                Reference targetRef = new Reference();
171                terser.cloneInto(sourceRef, targetRef, false);
172                return targetRef;
173        }
174}