
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 // the patchParams could be empty if the resource contains only versioned references to the source, 089 // no need to add patch entry in that case 090 if (patchParams.hasParameter()) { 091 IIdType resourceId = resource.getIdElement(); 092 bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); 093 } 094 }); 095 return bundleBuilder.getBundleTyped(); 096 } 097 098 private @Nonnull Parameters buildPatchParams( 099 ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) { 100 Parameters params = new Parameters(); 101 102 myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() 103 .filter(refInfo -> matches( 104 refInfo, 105 theReplaceReferencesRequest.sourceId)) // We only care about references to our source resource 106 .map(refInfo -> createReplaceReferencePatchOperation( 107 getFhirPathForPatch(referencingResource, refInfo), 108 new Reference(theReplaceReferencesRequest.targetId.getValueAsString()))) 109 .forEach(params::addParameter); // Add each operation to parameters 110 return params; 111 } 112 113 private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { 114 return refInfo.getResourceReference() 115 .getReferenceElement() 116 .toUnqualified() 117 .getValueAsString() 118 .equals(theSourceId.getValueAsString()); 119 } 120 121 private String getFhirPathForPatch(IBaseResource theReferencingResource, ResourceReferenceInfo theRefInfo) { 122 // construct the path to the element containing the reference in the resource, e.g. "Observation.subject" 123 String path = theReferencingResource.fhirType() + "." + theRefInfo.getName(); 124 // check the allowed cardinality of the element containing the reference 125 int maxCardinality = myFhirContext 126 .newTerser() 127 .getDefinition(theReferencingResource.getClass(), path) 128 .getMax(); 129 if (maxCardinality != 1) { 130 // if the element allows high cardinality, specify the exact reference to replace by appending a where 131 // filter to the path. If we don't do this, all the existing references in the element would be lost as a 132 // result of getting replaced with the new reference, and that is not the behaviour we want. 133 // e.g. "Observation.performer.where(reference='Practitioner/123')" 134 return String.format( 135 "%s.where(reference='%s')", 136 path, 137 theRefInfo.getResourceReference().getReferenceElement().getValueAsString()); 138 } 139 // the element allows max cardinality of 1, so the whole element can be safely replaced 140 return path; 141 } 142 143 @Nonnull 144 private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( 145 String thePath, Type theValue) { 146 147 Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); 148 operation.setName(PARAMETER_OPERATION); 149 operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); 150 operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); 151 operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); 152 return operation; 153 } 154}