001/*- 002 * #%L 003 * HAPI FHIR - Master Data Management 004 * %% 005 * Copyright (C) 2014 - 2024 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.mdm.util; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.context.FhirVersionEnum; 026import ca.uhn.fhir.context.RuntimeResourceDefinition; 027import ca.uhn.fhir.fhirpath.IFhirPath; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.mdm.api.IMdmSettings; 030import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 031import ca.uhn.fhir.mdm.log.Logs; 032import ca.uhn.fhir.mdm.model.CanonicalEID; 033import ca.uhn.fhir.mdm.model.MdmTransactionContext; 034import ca.uhn.fhir.rest.api.Constants; 035import ca.uhn.fhir.util.FhirTerser; 036import jakarta.annotation.Nonnull; 037import org.hl7.fhir.instance.model.api.IAnyResource; 038import org.hl7.fhir.instance.model.api.IBase; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IPrimitiveType; 041import org.slf4j.Logger; 042import org.springframework.beans.factory.annotation.Autowired; 043import org.springframework.stereotype.Service; 044 045import java.util.ArrayList; 046import java.util.List; 047import java.util.Objects; 048import java.util.Optional; 049import java.util.stream.Collectors; 050 051import static ca.uhn.fhir.context.FhirVersionEnum.DSTU3; 052import static ca.uhn.fhir.context.FhirVersionEnum.R4; 053import static ca.uhn.fhir.context.FhirVersionEnum.R5; 054 055@Service 056public class GoldenResourceHelper { 057 058 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 059 060 static final String FIELD_NAME_IDENTIFIER = "identifier"; 061 062 private final IMdmSettings myMdmSettings; 063 064 private final EIDHelper myEIDHelper; 065 066 private final MdmPartitionHelper myMdmPartitionHelper; 067 068 private final FhirContext myFhirContext; 069 070 @Autowired 071 public GoldenResourceHelper( 072 FhirContext theFhirContext, 073 IMdmSettings theMdmSettings, 074 EIDHelper theEIDHelper, 075 MdmPartitionHelper theMdmPartitionHelper) { 076 myFhirContext = theFhirContext; 077 myMdmSettings = theMdmSettings; 078 myEIDHelper = theEIDHelper; 079 myMdmPartitionHelper = theMdmPartitionHelper; 080 } 081 082 /** 083 * Creates a copy of the specified resource. This method will carry over resource EID if it exists. If it does not exist, 084 * a randomly generated UUID EID will be created. 085 * 086 * @param <T> Supported MDM resource type (e.g. Patient, Practitioner) 087 * @param theIncomingResource The resource to build the golden resource off of. 088 * Could be the source resource or another golden resource. 089 * If a golden resource, do not provide an IMdmSurvivorshipService 090 * @param theMdmTransactionContext The mdm transaction context 091 * @param theMdmSurvivorshipService IMdmSurvivorshipSvc. Provide only if survivorshipskills are desired 092 * to be applied. Provide null otherwise. 093 */ 094 @Nonnull 095 public <T extends IAnyResource> T createGoldenResourceFromMdmSourceResource( 096 T theIncomingResource, 097 MdmTransactionContext theMdmTransactionContext, 098 IMdmSurvivorshipService theMdmSurvivorshipService) { 099 validateContextSupported(); 100 101 // get a ref to the actual ID Field 102 RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theIncomingResource); 103 IBaseResource newGoldenResource = resourceDefinition.newInstance(); 104 105 if (theMdmSurvivorshipService != null) { 106 theMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( 107 theIncomingResource, newGoldenResource, theMdmTransactionContext); 108 } 109 110 // hapi has 2 metamodels: for children and types 111 BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); 112 113 cloneMDMEidsIntoNewGoldenResource(goldenResourceIdentifier, theIncomingResource, newGoldenResource); 114 115 addHapiEidIfNoExternalEidIsPresent(newGoldenResource, goldenResourceIdentifier, theIncomingResource); 116 117 MdmResourceUtil.setMdmManaged(newGoldenResource); 118 MdmResourceUtil.setGoldenResource(newGoldenResource); 119 120 // TODO - on updating links, if resolving a link, this should go away? 121 // blocked resource's golden resource will be marked special 122 // they are not part of MDM matching algorithm (will not link to other resources) 123 // but other resources can link to them 124 if (theMdmTransactionContext.getIsBlocked()) { 125 MdmResourceUtil.setGoldenResourceAsBlockedResourceGoldenResource(newGoldenResource); 126 } 127 128 // add the partition id to the new resource 129 newGoldenResource.setUserData( 130 Constants.RESOURCE_PARTITION_ID, 131 myMdmPartitionHelper.getRequestPartitionIdForNewGoldenResources(theIncomingResource)); 132 133 return (T) newGoldenResource; 134 } 135 136 /** 137 * If there are no external EIDs on the incoming resource, create a new HAPI EID on the new Golden Resource. 138 */ 139 // TODO GGG ask james if there is any way we can convert this canonical EID into a generic STU-agnostic IBase. 140 private <T extends IAnyResource> void addHapiEidIfNoExternalEidIsPresent( 141 IBaseResource theNewGoldenResource, 142 BaseRuntimeChildDefinition theGoldenResourceIdentifier, 143 IAnyResource theSourceResource) { 144 145 List<CanonicalEID> eidsToApply = myEIDHelper.getExternalEid(theNewGoldenResource); 146 if (!eidsToApply.isEmpty()) { 147 return; 148 } 149 150 CanonicalEID hapiEid = myEIDHelper.createHapiEid(); 151 theGoldenResourceIdentifier 152 .getMutator() 153 .addValue(theNewGoldenResource, IdentifierUtil.toId(myFhirContext, hapiEid)); 154 155 // set identifier on the source resource 156 cloneEidIntoResource(myFhirContext, theSourceResource, hapiEid); 157 } 158 159 private void cloneMDMEidsIntoNewGoldenResource( 160 BaseRuntimeChildDefinition theGoldenResourceIdentifier, 161 IAnyResource theIncomingResource, 162 IBaseResource theNewGoldenResource) { 163 String incomingResourceType = myFhirContext.getResourceType(theIncomingResource); 164 String mdmEIDSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(incomingResourceType); 165 166 if (mdmEIDSystem == null) { 167 return; 168 } 169 170 // FHIR choice types - fields within fhir where we have a choice of ids 171 IFhirPath fhirPath = myFhirContext.newFhirPath(); 172 List<IBase> incomingResourceIdentifiers = 173 theGoldenResourceIdentifier.getAccessor().getValues(theIncomingResource); 174 175 for (IBase incomingResourceIdentifier : incomingResourceIdentifiers) { 176 Optional<IPrimitiveType> incomingIdentifierSystem = 177 fhirPath.evaluateFirst(incomingResourceIdentifier, "system", IPrimitiveType.class); 178 if (incomingIdentifierSystem.isPresent()) { 179 String incomingIdentifierSystemString = 180 incomingIdentifierSystem.get().getValueAsString(); 181 if (Objects.equals(incomingIdentifierSystemString, mdmEIDSystem)) { 182 ourLog.debug( 183 "Incoming resource EID System {} matches EID system in the MDM rules. Copying to Golden Resource.", 184 incomingIdentifierSystemString); 185 ca.uhn.fhir.util.TerserUtil.cloneIdentifierIntoResource( 186 myFhirContext, 187 theGoldenResourceIdentifier, 188 incomingResourceIdentifier, 189 theNewGoldenResource); 190 } else { 191 ourLog.debug( 192 "Incoming resource EID System {} differs from EID system in the MDM rules {}. Not copying to Golden Resource.", 193 incomingIdentifierSystemString, 194 mdmEIDSystem); 195 } 196 } else { 197 ourLog.debug("No EID System in incoming resource."); 198 } 199 } 200 } 201 202 private void validateContextSupported() { 203 FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); 204 if (fhirVersion == R4 || fhirVersion == DSTU3 || fhirVersion == R5) { 205 return; 206 } 207 throw new UnsupportedOperationException(Msg.code(1489) + "Version not supported: " 208 + myFhirContext.getVersion().getVersion()); 209 } 210 211 /** 212 * Updates EID on Golden Resource, based on the incoming source resource. If the incoming resource has an external EID, it is applied 213 * to the Golden Resource, unless that golden resource already has an external EID which does not match, in which case throw {@link IllegalArgumentException} 214 * <p> 215 * If running in multiple EID mode, then incoming EIDs are simply added to the Golden Resource without checking for matches. 216 * 217 * @param theGoldenResource The golden resource to update the external EID on. 218 * @param theSourceResource The source we will retrieve the external EID from. 219 * @return the modified {@link IBaseResource} representing the Golden Resource. 220 */ 221 public IAnyResource updateGoldenResourceExternalEidFromSourceResource( 222 IAnyResource theGoldenResource, 223 IAnyResource theSourceResource, 224 MdmTransactionContext theMdmTransactionContext) { 225 // This handles overwriting an automatically assigned EID if a patient that links is coming in with an official 226 // EID. 227 List<CanonicalEID> incomingSourceEid = myEIDHelper.getExternalEid(theSourceResource); 228 List<CanonicalEID> goldenResourceOfficialEid = myEIDHelper.getExternalEid(theGoldenResource); 229 230 if (incomingSourceEid.isEmpty()) { 231 return theGoldenResource; 232 } 233 234 if (goldenResourceOfficialEid.isEmpty() || !myMdmSettings.isPreventMultipleEids()) { 235 if (addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, incomingSourceEid)) { 236 log( 237 theMdmTransactionContext, 238 "Incoming resource:" + theSourceResource.getIdElement().toUnqualifiedVersionless() 239 + " + with EID " 240 + incomingSourceEid.stream() 241 .map(CanonicalEID::toString) 242 .collect(Collectors.joining(",")) 243 + " is applying this EID to its related Golden Resource, as this Golden Resource does not yet have an external EID"); 244 } 245 } else if (!goldenResourceOfficialEid.isEmpty() 246 && myEIDHelper.eidMatchExists(goldenResourceOfficialEid, incomingSourceEid)) { 247 log( 248 theMdmTransactionContext, 249 "Incoming resource:" + theSourceResource.getIdElement().toVersionless() + " with EIDs " 250 + incomingSourceEid.stream() 251 .map(CanonicalEID::toString) 252 .collect(Collectors.joining(",")) 253 + " does not need to overwrite the EID in the Golden Resource, as this EID is already present in the Golden Resource"); 254 } else { 255 throw new IllegalArgumentException(Msg.code(1490) 256 + String.format( 257 "Incoming resource EID %s would create a duplicate Golden Resource, as Golden Resource EID %s already exists!", 258 incomingSourceEid.toString(), goldenResourceOfficialEid.toString())); 259 } 260 return theGoldenResource; 261 } 262 263 public IBaseResource overwriteExternalEids(IBaseResource theGoldenResource, List<CanonicalEID> theNewEid) { 264 clearExternalEids(theGoldenResource); 265 addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, theNewEid); 266 return theGoldenResource; 267 } 268 269 private void clearExternalEidsFromTheGoldenResource( 270 BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBaseResource theGoldenResource) { 271 IFhirPath fhirPath = myFhirContext.newFhirPath(); 272 List<IBase> goldenResourceIdentifiers = 273 theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource); 274 List<IBase> clonedIdentifiers = new ArrayList<>(); 275 FhirTerser terser = myFhirContext.newTerser(); 276 277 for (IBase base : goldenResourceIdentifiers) { 278 Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class); 279 if (system.isPresent()) { 280 String resourceType = myFhirContext.getResourceType(theGoldenResource); 281 String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType); 282 String baseSystem = system.get().getValueAsString(); 283 if (Objects.equals(baseSystem, mdmSystem)) { 284 ourLog.debug( 285 "Found EID confirming to MDM rules {}. It does not need to be copied, skipping", 286 baseSystem); 287 continue; 288 } 289 } 290 291 BaseRuntimeElementCompositeDefinition<?> childIdentifier = (BaseRuntimeElementCompositeDefinition<?>) 292 theGoldenResourceIdentifier.getChildByName(FIELD_NAME_IDENTIFIER); 293 IBase goldenResourceNewIdentifier = childIdentifier.newInstance(); 294 terser.cloneInto(base, goldenResourceNewIdentifier, true); 295 296 clonedIdentifiers.add(goldenResourceNewIdentifier); 297 } 298 299 goldenResourceIdentifiers.clear(); 300 goldenResourceIdentifiers.addAll(clonedIdentifiers); 301 } 302 303 private void clearExternalEids(IBaseResource theGoldenResource) { 304 // validate the system - if it's set to EID system - then clear it - type and STU version 305 validateContextSupported(); 306 307 // get a ref to the actual ID Field 308 RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theGoldenResource); 309 BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); 310 clearExternalEidsFromTheGoldenResource(goldenResourceIdentifier, theGoldenResource); 311 } 312 313 /** 314 * Given a list of incoming External EIDs, and a Golden Resource, apply all the EIDs to this resource, which did not already exist on it. 315 * @return true if an EID was added 316 */ 317 private boolean addCanonicalEidsToGoldenResourceIfAbsent( 318 IBaseResource theGoldenResource, List<CanonicalEID> theIncomingSourceExternalEids) { 319 List<CanonicalEID> goldenResourceExternalEids = myEIDHelper.getExternalEid(theGoldenResource); 320 boolean addedEid = false; 321 for (CanonicalEID incomingExternalEid : theIncomingSourceExternalEids) { 322 if (goldenResourceExternalEids.contains(incomingExternalEid)) { 323 continue; 324 } 325 cloneEidIntoResource(myFhirContext, theGoldenResource, incomingExternalEid); 326 addedEid = true; 327 } 328 return addedEid; 329 } 330 331 public boolean hasIdentifier(IBaseResource theResource) { 332 return ca.uhn.fhir.util.TerserUtil.hasValues(myFhirContext, theResource, FIELD_NAME_IDENTIFIER); 333 } 334 335 public void mergeIndentifierFields( 336 IBaseResource theFromGoldenResource, 337 IBaseResource theToGoldenResource, 338 MdmTransactionContext theMdmTransactionContext) { 339 ca.uhn.fhir.util.TerserUtil.cloneCompositeField( 340 myFhirContext, theFromGoldenResource, theToGoldenResource, FIELD_NAME_IDENTIFIER); 341 } 342 343 /** 344 * An incoming resource is a potential duplicate if it matches a source that has a golden resource with an official 345 * EID, but the incoming resource also has an EID that does not match. 346 */ 347 public boolean isPotentialDuplicate( 348 IAnyResource theExistingGoldenResource, IAnyResource theComparingGoldenResource) { 349 List<CanonicalEID> externalEidsGoldenResource = myEIDHelper.getExternalEid(theExistingGoldenResource); 350 List<CanonicalEID> externalEidsResource = myEIDHelper.getExternalEid(theComparingGoldenResource); 351 return !externalEidsGoldenResource.isEmpty() 352 && !externalEidsResource.isEmpty() 353 && !myEIDHelper.eidMatchExists(externalEidsResource, externalEidsGoldenResource); 354 } 355 356 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 357 theMdmTransactionContext.addTransactionLogMessage(theMessage); 358 ourLog.debug(theMessage); 359 } 360 361 public void handleExternalEidAddition( 362 IAnyResource theGoldenResource, 363 IAnyResource theSourceResource, 364 MdmTransactionContext theMdmTransactionContext) { 365 List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theSourceResource); 366 if (!eidFromResource.isEmpty()) { 367 updateGoldenResourceExternalEidFromSourceResource( 368 theGoldenResource, theSourceResource, theMdmTransactionContext); 369 } 370 } 371 372 /** 373 * Clones the specified canonical EID into the identifier field on the resource 374 * 375 * @param theFhirContext Context to pull resource definitions from 376 * @param theResourceToCloneInto Resource to set the EID on 377 * @param theEid EID to be set 378 */ 379 public void cloneEidIntoResource( 380 FhirContext theFhirContext, IBaseResource theResourceToCloneInto, CanonicalEID theEid) { 381 // get a ref to the actual ID Field 382 RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceToCloneInto); 383 // hapi has 2 metamodels: for children and types 384 BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); 385 ca.uhn.fhir.util.TerserUtil.cloneIdentifierIntoResource( 386 theFhirContext, 387 resourceIdentifier, 388 IdentifierUtil.toId(theFhirContext, theEid), 389 theResourceToCloneInto); 390 } 391}