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