
001/*- 002 * #%L 003 * HAPI FHIR - Core Library 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.util; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; 027import ca.uhn.fhir.context.RuntimeResourceDefinition; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.rest.api.EncodingEnum; 030import jakarta.annotation.Nonnull; 031import org.apache.commons.lang3.Strings; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.hl7.fhir.instance.model.api.IBaseBundle; 034import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036import org.hl7.fhir.instance.model.api.IPrimitiveType; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import java.lang.reflect.Method; 041import java.util.ArrayList; 042import java.util.List; 043import java.util.Objects; 044import java.util.Optional; 045import java.util.function.Predicate; 046 047public class ResourceUtil { 048 049 private static final String ENCODING = "ENCODING_TYPE"; 050 private static final String RAW_ = "RAW_"; 051 private static final String EQUALS_DEEP = "equalsDeep"; 052 public static final String DATA_ABSENT_REASON_EXTENSION_URI = 053 "http://hl7.org/fhir/StructureDefinition/data-absent-reason"; 054 055 private static final Logger ourLog = LoggerFactory.getLogger(ResourceUtil.class); 056 057 /** 058 * A strategy object that specifies which rules to apply when merging <code>Coding</code> 059 * and <code>CodeableConcept</code> fields 060 */ 061 public static class MergeControlParameters { 062 private boolean myIgnoreCodeableConceptCodingOrder; 063 private boolean myMergeCodings; 064 private boolean myMergeCodingDetails; 065 066 public boolean isIgnoreCodeableConceptCodingOrder() { 067 return myIgnoreCodeableConceptCodingOrder; 068 } 069 070 /** 071 * In most cases, the order of elements in a FHIR list should not be considered meaningful. This parameter 072 * specifies whether the order of <code>Coding</code> entities within a <code>CodeableConcept</code> 073 * matters when performing a merge. 074 * @param theIgnoreCodeableConceptCodingOrder if true, two <code>CodeableConcept</code> entities will 075 * be considered to match each other if they contain 076 * matching <code>Coding</code>s in any order. If false, 077 * the <code>Coding</code>s must be in the same order. 078 */ 079 public void setIgnoreCodeableConceptCodingOrder(boolean theIgnoreCodeableConceptCodingOrder) { 080 myIgnoreCodeableConceptCodingOrder = theIgnoreCodeableConceptCodingOrder; 081 } 082 083 public boolean isMergeCodings() { 084 return myMergeCodings; 085 } 086 087 /** 088 * If the <code>Coding</code>s of one <code>CodeableConcept</code> are a strict subset of another, the two 089 * may be considered to match, and can be merged into a single element that contains the larger list of 090 * <code>Coding</code>s. The case where the two lists of <code>Coding</code>s overlap, but each contains 091 * elements that are absent from the other, the two <code>CodeableConcept</code>s will be considered distinct, 092 * and will not be merged. 093 * @param theMergeCodings if true, two <code>CodeableConcept</code> entities will be considered to match each 094 * other if all the <code>Coding</code>s from the shorter list occur in the longer list. 095 * If false, the lists must be the same length, and contain exactly the same elements. 096 */ 097 public void setMergeCodings(boolean theMergeCodings) { 098 myMergeCodings = theMergeCodings; 099 } 100 101 public boolean isMergeCodingDetails() { 102 return myMergeCodingDetails; 103 } 104 105 /** 106 * Two <code>Coding</code>s may be considered to match if they share the same business key (system, code). 107 * If this is the case, the remaining fields of the element can be merged into a survivor <code>Coding</code>. 108 * @param theMergeCodingDetails if true, will match on the <code>Coding</code> business key only. 109 * if false, matching requires exact equality of every field. 110 */ 111 public void setMergeCodingDetails(boolean theMergeCodingDetails) { 112 myMergeCodingDetails = theMergeCodingDetails; 113 } 114 } 115 116 private ResourceUtil() {} 117 118 /** 119 * Exclusion predicate for keeping all fields. 120 */ 121 public static final Predicate<String> INCLUDE_ALL = s -> true; 122 123 /** 124 * This method removes the narrative from the resource, or if the resource is a bundle, removes the narrative from 125 * all of the resources in the bundle 126 * 127 * @param theContext The fhir context 128 * @param theInput The resource to remove the narrative from 129 */ 130 public static void removeNarrative(FhirContext theContext, IBaseResource theInput) { 131 if (theInput instanceof IBaseBundle) { 132 for (IBaseResource next : BundleUtil.toListOfResources(theContext, (IBaseBundle) theInput)) { 133 removeNarrative(theContext, next); 134 } 135 } 136 137 BaseRuntimeElementCompositeDefinition<?> element = theContext.getResourceDefinition(theInput.getClass()); 138 BaseRuntimeChildDefinition textElement = element.getChildByName("text"); 139 if (textElement != null) { 140 textElement.getMutator().setValue(theInput, null); 141 } 142 } 143 144 public static void addRawDataToResource( 145 @Nonnull IBaseResource theResource, @Nonnull EncodingEnum theEncodingType, String theSerializedData) { 146 theResource.setUserData(getRawUserDataKey(theEncodingType), theSerializedData); 147 theResource.setUserData(ENCODING, theEncodingType); 148 } 149 150 public static EncodingEnum getEncodingTypeFromUserData(@Nonnull IBaseResource theResource) { 151 return (EncodingEnum) theResource.getUserData(ENCODING); 152 } 153 154 public static String getRawStringFromResourceOrNull(@Nonnull IBaseResource theResource) { 155 EncodingEnum type = (EncodingEnum) theResource.getUserData(ENCODING); 156 if (type != null) { 157 return (String) theResource.getUserData(getRawUserDataKey(type)); 158 } 159 return null; 160 } 161 162 private static String getRawUserDataKey(EncodingEnum theEncodingEnum) { 163 return RAW_ + theEncodingEnum.name(); 164 } 165 166 /** 167 * Merges all fields on the provided instance. <code>theTarget</code> will contain a union of all values from 168 * <code>theSource</code> instance and <code>theTarget</code> instance. 169 * 170 * @param theFhirContext Context holding resource definition 171 * @param theSource The FHIR element to merge the fields from 172 * @param theTarget The FHIR element to merge the fields into 173 */ 174 public static void mergeAllFields(FhirContext theFhirContext, IBase theSource, IBase theTarget) { 175 mergeAllFields(theFhirContext, theSource, theTarget, new MergeControlParameters()); 176 } 177 178 /** 179 * Merges all fields on the provided instance. <code>theTarget</code> will contain a union of all values from 180 * <code>theSource</code> instance and <code>theTarget</code> instance. 181 * 182 * @param theFhirContext Context holding resource definition 183 * @param theSource The FHIR element to merge the fields from 184 * @param theTarget The FHIR element to merge the fields into 185 * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge 186 */ 187 public static void mergeAllFields( 188 FhirContext theFhirContext, 189 IBase theSource, 190 IBase theTarget, 191 MergeControlParameters theMergeControlParameters) { 192 mergeFields(theFhirContext, theSource, theTarget, INCLUDE_ALL, theMergeControlParameters); 193 } 194 195 /** 196 * Merges values of all field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields 197 * values are compared via the equalsDeep method, or via object identity if this method is not available. 198 * 199 * @param theFhirContext Context holding resource definition 200 * @param theSource Resource to merge the specified field from 201 * @param theTarget Resource to merge the specified field into 202 * @param theInclusionStrategy Predicate to test which fields should be merged 203 */ 204 public static void mergeFields( 205 FhirContext theFhirContext, IBase theSource, IBase theTarget, Predicate<String> theInclusionStrategy) { 206 mergeFields(theFhirContext, theSource, theTarget, theInclusionStrategy, new MergeControlParameters()); 207 } 208 209 /** 210 * Merges values of all field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields 211 * with type <code>Coding</code> or <code>CodeableConcept</code> will be recursively merged according to the 212 * strategy specified by <code>theMergeControlParameters</code>. Fields of other types 213 * are compared via the equalsDeep method, or via object identity if this method is not available. 214 * 215 * @param theFhirContext Context holding resource definition 216 * @param theSource Resource to merge the specified field from 217 * @param theTarget Resource to merge the specified field into 218 * @param theInclusionStrategy Predicate to test which fields should be merged 219 * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge 220 */ 221 public static void mergeFields( 222 FhirContext theFhirContext, 223 IBase theSource, 224 IBase theTarget, 225 Predicate<String> theInclusionStrategy, 226 MergeControlParameters theMergeControlParameters) { 227 BaseRuntimeElementDefinition<?> definition = theFhirContext.getElementDefinition(theSource.getClass()); 228 if (definition instanceof BaseRuntimeElementCompositeDefinition<?> compositeDefinition) { 229 for (BaseRuntimeChildDefinition childDefinition : compositeDefinition.getChildrenAndExtension()) { 230 if (!theInclusionStrategy.test(childDefinition.getElementName())) { 231 continue; 232 } 233 234 List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theSource); 235 List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTarget); 236 237 mergeFields( 238 theFhirContext, 239 theTarget, 240 childDefinition, 241 theFromFieldValues, 242 theToFieldValues, 243 theMergeControlParameters); 244 } 245 } 246 } 247 248 /** 249 * Merges value of the specified field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields 250 * values are compared via the equalsDeep method, or via object identity if this method is not available. 251 * 252 * @param theFhirContext Context holding resource definition 253 * @param theFieldName Name of the child filed to merge 254 * @param theSource Resource to merge the specified field from 255 * @param theTarget Resource to merge the specified field into 256 */ 257 public static void mergeField( 258 FhirContext theFhirContext, String theFieldName, IBaseResource theSource, IBaseResource theTarget) { 259 mergeField(theFhirContext, theFieldName, theSource, theTarget, new MergeControlParameters()); 260 } 261 262 /** 263 * Merges value of the specified field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields 264 * with type <code>Coding</code> or <code>CodeableConcept</code> will be recursively merged according to the 265 * strategy specified by <code>theMergeControlParameters</code>. Fields of other types 266 * are compared via the equalsDeep method, or via object identity if this method is not available. 267 * 268 * @param theFhirContext Context holding resource definition 269 * @param theFieldName Name of the child filed to merge 270 * @param theSource Resource to merge the specified field from 271 * @param theTarget Resource to merge the specified field into 272 * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge 273 */ 274 public static void mergeField( 275 FhirContext theFhirContext, 276 String theFieldName, 277 IBaseResource theSource, 278 IBaseResource theTarget, 279 MergeControlParameters theMergeControlParameters) { 280 BaseRuntimeChildDefinition childDefinition = 281 getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theSource); 282 283 List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theSource); 284 List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTarget); 285 286 mergeFields( 287 theFhirContext, 288 theTarget, 289 childDefinition, 290 theFromFieldValues, 291 theToFieldValues, 292 theMergeControlParameters); 293 } 294 295 private static void mergeFields( 296 FhirContext theFhirContext, 297 IBase theTarget, 298 BaseRuntimeChildDefinition theChildDefinition, 299 List<IBase> theSourceFieldValues, 300 List<IBase> theTargetFieldValues, 301 MergeControlParameters theMergeControlParameters) { 302 FhirTerser terser = theFhirContext.newTerser(); 303 304 if (!theSourceFieldValues.isEmpty() 305 && theTargetFieldValues.stream().anyMatch(ResourceUtil::hasDataAbsentReason)) { 306 // If the target resource has a data absent reason, and there is potentially real data incoming 307 // in the source resource, we should clear the data absent reason because it won't be absent anymore. 308 theTargetFieldValues = removeDataAbsentReason(theTarget, theChildDefinition, theTargetFieldValues); 309 } 310 311 List<IBase> filteredFromFieldValues = filterValuesThatAlreadyExistInTarget( 312 terser, theSourceFieldValues, theTargetFieldValues, theMergeControlParameters); 313 314 for (IBase fromFieldValue : filteredFromFieldValues) { 315 IBase newFieldValue; 316 if (Strings.CI.equals(fromFieldValue.fhirType(), "CodeableConcept")) { 317 newFieldValue = mergeOrClone( 318 theFhirContext, 319 theChildDefinition, 320 fromFieldValue, 321 theTargetFieldValues, 322 theMergeControlParameters, 323 targetValue -> isCodeableConceptMergeCandidate( 324 fromFieldValue, targetValue, terser, theMergeControlParameters)); 325 } else if (Strings.CI.equals(fromFieldValue.fhirType(), "Coding")) { 326 newFieldValue = mergeOrClone( 327 theFhirContext, 328 theChildDefinition, 329 fromFieldValue, 330 theTargetFieldValues, 331 theMergeControlParameters, 332 targetValue -> 333 isCodingMergeCandidate(terser, fromFieldValue, targetValue, theMergeControlParameters)); 334 } else { 335 newFieldValue = createNewElement(terser, theChildDefinition, fromFieldValue); 336 } 337 338 if (newFieldValue != null) { 339 try { 340 theTargetFieldValues.add(newFieldValue); 341 } catch (UnsupportedOperationException e) { 342 theChildDefinition.getMutator().setValue(theTarget, newFieldValue); 343 theTargetFieldValues = theChildDefinition.getAccessor().getValues(theTarget); 344 } 345 } 346 } 347 } 348 349 /** 350 * If <code>theMergePredicate</code> identifies one of the elements in <code>theTargetFieldValues</code> 351 * as a match for <code>theSourceFieldValue</code>, those two elements will be merged. 352 * Otherwise, <code>theSourceFieldValue</code> will be cloned and returned. 353 * @param theFhirContext Context holding resource definition 354 * @param theChildDefinition The definition of the field being merged 355 * @param theSourceFieldValue A value from the source resource 356 * @param theTargetFieldValues The values already present in the target resource 357 * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge 358 * @param theMergePredicate An algorithm to identify matching resources 359 * @return a clone of <code>theSourceFieldValue</code> if none of the values in <code>theTargetFieldValues</code> 360 * is a match. Otherwise, null, indicating that a merge took place 361 */ 362 private static IBase mergeOrClone( 363 FhirContext theFhirContext, 364 BaseRuntimeChildDefinition theChildDefinition, 365 IBase theSourceFieldValue, 366 List<IBase> theTargetFieldValues, 367 MergeControlParameters theMergeControlParameters, 368 Predicate<IBase> theMergePredicate) { 369 IBase newFieldValue = null; 370 FhirTerser terser = theFhirContext.newTerser(); 371 Optional<IBase> matchedTargetValue = 372 theTargetFieldValues.stream().filter(theMergePredicate).findFirst(); 373 if (matchedTargetValue.isPresent()) { 374 mergeAllFields(theFhirContext, theSourceFieldValue, matchedTargetValue.get(), theMergeControlParameters); 375 } else { 376 newFieldValue = createNewElement(terser, theChildDefinition, theSourceFieldValue); 377 } 378 return newFieldValue; 379 } 380 381 private static IBase createNewElement( 382 FhirTerser theTerser, BaseRuntimeChildDefinition theChildDefinition, IBase theFromFieldValue) { 383 IBase newFieldValue = newElement(theTerser, theChildDefinition, theFromFieldValue); 384 if (theFromFieldValue instanceof IPrimitiveType) { 385 try { 386 Method copyMethod = getMethod(theFromFieldValue, "copy"); 387 if (copyMethod != null) { 388 newFieldValue = (IBase) copyMethod.invoke(theFromFieldValue); 389 } 390 } catch (Exception t) { 391 ((IPrimitiveType<?>) newFieldValue) 392 .setValueAsString(((IPrimitiveType<?>) theFromFieldValue).getValueAsString()); 393 } 394 } else { 395 theTerser.cloneInto(theFromFieldValue, newFieldValue, true); 396 } 397 return newFieldValue; 398 } 399 400 private static List<IBase> filterValuesThatAlreadyExistInTarget( 401 FhirTerser theTerser, 402 List<IBase> theFromFieldValues, 403 List<IBase> theToFieldValues, 404 MergeControlParameters theMergeControlParameters) { 405 List<IBase> filteredFromFieldValues = new ArrayList<>(); 406 for (IBase fromFieldValue : theFromFieldValues) { 407 if (theToFieldValues.isEmpty()) { 408 // if the target field is unpopulated, accept any value from the source field 409 filteredFromFieldValues.add(fromFieldValue); 410 } else if (!hasDataAbsentReason(fromFieldValue)) { 411 // if the value from the source field does not have a data absent reason extension, 412 // evaluate its suitability for inclusion 413 if (Strings.CI.equals(fromFieldValue.fhirType(), "codeableConcept")) { 414 if (!containsCodeableConcept( 415 fromFieldValue, theToFieldValues, theTerser, theMergeControlParameters)) { 416 filteredFromFieldValues.add(fromFieldValue); 417 } 418 } else if (!contains(fromFieldValue, theToFieldValues)) { 419 // include it if the target list doesn't already contain an exact match 420 filteredFromFieldValues.add(fromFieldValue); 421 } 422 } 423 } 424 return filteredFromFieldValues; 425 } 426 427 private static BaseRuntimeChildDefinition getBaseRuntimeChildDefinition( 428 FhirContext theFhirContext, String theFieldName, IBaseResource theFrom) { 429 RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom); 430 BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theFieldName); 431 Objects.requireNonNull(childDefinition); 432 return childDefinition; 433 } 434 435 private static Method getMethod(IBase theBase, String theMethodName) { 436 Method method = null; 437 for (Method m : theBase.getClass().getDeclaredMethods()) { 438 if (m.getName().equals(theMethodName)) { 439 method = m; 440 break; 441 } 442 } 443 return method; 444 } 445 446 private static boolean evaluateEquality(IBase theItem1, IBase theItem2, Method theMethod) { 447 if (theMethod != null) { 448 try { 449 return (Boolean) theMethod.invoke(theItem1, theItem2); 450 } catch (Exception e) { 451 ourLog.debug("{} Unable to compare equality via {}", Msg.code(2821), theMethod.getName(), e); 452 } 453 } 454 return theItem1.equals(theItem2); 455 } 456 457 private static boolean contains(IBase theItem, List<IBase> theItems) { 458 final Method method = getMethod(theItem, EQUALS_DEEP); 459 return theItems.stream().anyMatch(i -> evaluateEquality(i, theItem, method)); 460 } 461 462 /** 463 * Evaluates whether a given source CodeableConcept can be merged into a target 464 * CodeableConcept 465 * 466 * @param theSourceItem the source item 467 * @param theTargetItem the target item 468 * @param theTerser a terser for introspecting the items 469 * @param theMergeControlParameters parameters providing fine-grained control over the merge operation 470 * @return true if the source item can be merged into the target item 471 */ 472 private static boolean isCodeableConceptMergeCandidate( 473 IBase theSourceItem, 474 IBase theTargetItem, 475 FhirTerser theTerser, 476 MergeControlParameters theMergeControlParameters) { 477 // First, compare the shallow fields of the CodeableConcepts. 478 Method shallowEquals = getMethod(theSourceItem, "equalsShallow"); 479 boolean isMergeCandidate = evaluateEquality(theSourceItem, theTargetItem, shallowEquals); 480 481 // if the shallow fields match, we proceed to compare the lists of Codings 482 if (theMergeControlParameters.isIgnoreCodeableConceptCodingOrder()) { 483 isMergeCandidate &= 484 isCodingListsMatchUnordered(theSourceItem, theTargetItem, theTerser, theMergeControlParameters); 485 } else { 486 isMergeCandidate &= isCodingListsMatchOrdered( 487 theSourceItem, theTargetItem, theTerser, theMergeControlParameters, isMergeCandidate); 488 } 489 490 return isMergeCandidate; 491 } 492 493 private static boolean isCodingListsMatchOrdered( 494 IBase theSourceItem, 495 IBase theTargetItem, 496 FhirTerser theTerser, 497 MergeControlParameters theMergeControlParameters, 498 boolean isMergeCandidate) { 499 List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding"); 500 List<IBase> targetCodings = theTerser.getValues(theTargetItem, "coding"); 501 if (theMergeControlParameters.isMergeCodings()) { 502 int prefixLength = Math.min(sourceCodings.size(), targetCodings.size()); 503 for (int i = 0; i < prefixLength; i++) { 504 isMergeCandidate &= isCodingMergeCandidate( 505 theTerser, sourceCodings.get(i), targetCodings.get(i), theMergeControlParameters); 506 } 507 } else { 508 if (sourceCodings.size() == targetCodings.size()) { 509 for (int i = 0; i < sourceCodings.size(); i++) { 510 isMergeCandidate &= isCodingMergeCandidate( 511 theTerser, sourceCodings.get(i), targetCodings.get(i), theMergeControlParameters); 512 } 513 } else { 514 isMergeCandidate = false; 515 } 516 } 517 return isMergeCandidate; 518 } 519 520 private static boolean isCodingListsMatchUnordered( 521 IBase theSourceItem, 522 IBase theTargetItem, 523 FhirTerser theTerser, 524 MergeControlParameters theMergeControlParameters) { 525 boolean isMergeCandidate; 526 List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding"); 527 List<IBase> targetCodings = theTerser.getValues(theTargetItem, "coding"); 528 if (theMergeControlParameters.isMergeCodings()) { 529 if (sourceCodings.size() < targetCodings.size()) { 530 isMergeCandidate = sourceCodings.stream().allMatch(sourceCoding -> targetCodings.stream() 531 .anyMatch(targetCoding -> isCodingMergeCandidate( 532 theTerser, sourceCoding, targetCoding, theMergeControlParameters))); 533 } else { 534 isMergeCandidate = targetCodings.stream().allMatch(targetCoding -> sourceCodings.stream() 535 .anyMatch(sourceCoding -> isCodingMergeCandidate( 536 theTerser, sourceCoding, targetCoding, theMergeControlParameters))); 537 } 538 } else { 539 isMergeCandidate = sourceCodings.size() == targetCodings.size() 540 && sourceCodings.stream().allMatch(sourceCoding -> targetCodings.stream() 541 .anyMatch(targetCoding -> isCodingMergeCandidate( 542 theTerser, sourceCoding, targetCoding, theMergeControlParameters))); 543 } 544 return isMergeCandidate; 545 } 546 547 @SuppressWarnings("rawtypes") 548 private static boolean isCodingMergeCandidate( 549 FhirTerser theTerser, 550 IBase theSourceCoding, 551 IBase theTargetCoding, 552 MergeControlParameters theMergeControlParameters) { 553 boolean codingMatches; 554 if (theMergeControlParameters.isMergeCodingDetails()) { 555 // Use the tuple (system,code) as a business key on Coding 556 Optional<IPrimitiveType> sourceSystem = 557 theTerser.getSingleValue(theSourceCoding, "system", IPrimitiveType.class); 558 Optional<IPrimitiveType> sourceCode = 559 theTerser.getSingleValue(theSourceCoding, "code", IPrimitiveType.class); 560 Optional<IPrimitiveType> targetSystem = 561 theTerser.getSingleValue(theTargetCoding, "system", IPrimitiveType.class); 562 Optional<IPrimitiveType> targetCode = 563 theTerser.getSingleValue(theTargetCoding, "code", IPrimitiveType.class); 564 boolean systemMatches = sourceSystem.isPresent() 565 && targetSystem.isPresent() 566 && Strings.CS.equals( 567 sourceSystem.get().getValueAsString(), 568 targetSystem.get().getValueAsString()); 569 boolean codeMatches = sourceCode.isPresent() 570 && targetCode.isPresent() 571 && Strings.CS.equals( 572 sourceCode.get().getValueAsString(), 573 targetCode.get().getValueAsString()); 574 codingMatches = systemMatches && codeMatches; 575 } else { 576 // require an exact match on every field 577 Method deepEquals = getMethod(theSourceCoding, EQUALS_DEEP); 578 codingMatches = evaluateEquality(theSourceCoding, theTargetCoding, deepEquals); 579 } 580 return codingMatches; 581 } 582 583 /** 584 * Evaluates whether a list of target CodeableConcepts already contains a given source 585 * CodeableConcept. The order of Codings may or may not matter, depending on the 586 * configuration parameters, but otherwise we evaluate equivalence in the strictest 587 * available sense, since values filtered out by this method will not be candidates 588 * for subsequent merge operations. 589 * 590 * @param theSourceItem The source value 591 * @param theTargetItems The list of target values 592 * @param theTerser A terser to use to inspect the values 593 * @param theMergeControlParameters A set of parameters to control the operation 594 * @return true if the source item already exists in the list of target items 595 */ 596 private static boolean containsCodeableConcept( 597 IBase theSourceItem, 598 List<IBase> theTargetItems, 599 FhirTerser theTerser, 600 MergeControlParameters theMergeControlParameters) { 601 Method shallowEquals = getMethod(theSourceItem, "equalsShallow"); 602 List<IBase> shallowMatches = theTargetItems.stream() 603 .filter(targetItem -> evaluateEquality(targetItem, theSourceItem, shallowEquals)) 604 .toList(); 605 606 if (theMergeControlParameters.isIgnoreCodeableConceptCodingOrder()) { 607 return shallowMatches.stream().anyMatch(targetItem -> { 608 List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding"); 609 List<IBase> targetCodings = theTerser.getValues(targetItem, "coding"); 610 return sourceCodings.stream().allMatch(sourceCoding -> { 611 Method deepEquals = getMethod(sourceCoding, EQUALS_DEEP); 612 return targetCodings.stream() 613 .anyMatch(targetCoding -> evaluateEquality(sourceCoding, targetCoding, deepEquals)); 614 }); 615 }); 616 } else { 617 return shallowMatches.stream().anyMatch(targetItem -> { 618 boolean match = true; 619 List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding"); 620 List<IBase> targetCodings = theTerser.getValues(targetItem, "coding"); 621 if (sourceCodings.size() == targetCodings.size()) { 622 for (int i = 0; i < sourceCodings.size(); i++) { 623 Method deepEquals = getMethod(sourceCodings.get(i), EQUALS_DEEP); 624 match &= evaluateEquality(sourceCodings.get(i), targetCodings.get(i), deepEquals); 625 } 626 } else { 627 match = false; 628 } 629 return match; 630 }); 631 } 632 } 633 634 private static boolean hasDataAbsentReason(IBase theItem) { 635 if (theItem instanceof IBaseHasExtensions hasExtensions) { 636 return hasExtensions.getExtension().stream() 637 .anyMatch(t -> Strings.CS.equals(t.getUrl(), DATA_ABSENT_REASON_EXTENSION_URI)); 638 } 639 return false; 640 } 641 642 private static List<IBase> removeDataAbsentReason( 643 IBase theFhirElement, BaseRuntimeChildDefinition theFieldDefinition, List<IBase> theFieldValues) { 644 for (int i = 0; i < theFieldValues.size(); i++) { 645 if (hasDataAbsentReason(theFieldValues.get(i))) { 646 try { 647 theFieldDefinition.getMutator().remove(theFhirElement, i); 648 } catch (UnsupportedOperationException e) { 649 // the field must be single-valued, just clear it 650 theFieldDefinition.getMutator().setValue(theFhirElement, null); 651 } 652 } 653 } 654 return theFieldDefinition.getAccessor().getValues(theFhirElement); 655 } 656 657 /** 658 * Creates a new element taking into consideration elements with choice that are not directly retrievable by element 659 * name 660 * 661 * @param theFhirTerser A terser instance for the FHIR release 662 * @param theChildDefinition Child to create a new instance for 663 * @param theFromFieldValue The base parent field 664 * @return Returns the new element with the given value if configured 665 */ 666 private static IBase newElement( 667 FhirTerser theFhirTerser, BaseRuntimeChildDefinition theChildDefinition, IBase theFromFieldValue) { 668 BaseRuntimeElementDefinition<?> runtimeElementDefinition; 669 if (theChildDefinition instanceof RuntimeChildChoiceDefinition) { 670 runtimeElementDefinition = 671 theChildDefinition.getChildElementDefinitionByDatatype(theFromFieldValue.getClass()); 672 } else { 673 runtimeElementDefinition = theChildDefinition.getChildByName(theChildDefinition.getElementName()); 674 } 675 if ("contained".equals(runtimeElementDefinition.getName())) { 676 IBaseResource sourceResource = (IBaseResource) theFromFieldValue; 677 return theFhirTerser.clone(sourceResource); 678 } else { 679 return runtimeElementDefinition.newInstance(); 680 } 681 } 682}