
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}