
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.RuntimeResourceDefinition; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 029import ca.uhn.fhir.model.primitive.IdDt; 030import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; 031import ca.uhn.fhir.model.valueset.BundleTypeEnum; 032import ca.uhn.fhir.rest.api.PatchTypeEnum; 033import ca.uhn.fhir.rest.api.RequestTypeEnum; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 036import ca.uhn.fhir.util.bundle.BundleEntryMutator; 037import ca.uhn.fhir.util.bundle.BundleEntryParts; 038import ca.uhn.fhir.util.bundle.EntryListAccumulator; 039import ca.uhn.fhir.util.bundle.ModifiableBundleEntry; 040import ca.uhn.fhir.util.bundle.ModifiableBundleEntryParts; 041import ca.uhn.fhir.util.bundle.PartsConverter; 042import ca.uhn.fhir.util.bundle.SearchBundleEntryParts; 043import com.google.common.collect.Sets; 044import jakarta.annotation.Nonnull; 045import jakarta.annotation.Nullable; 046import org.apache.commons.lang3.Validate; 047import org.apache.commons.lang3.tuple.Pair; 048import org.hl7.fhir.instance.model.api.IBase; 049import org.hl7.fhir.instance.model.api.IBaseBackboneElement; 050import org.hl7.fhir.instance.model.api.IBaseBinary; 051import org.hl7.fhir.instance.model.api.IBaseBundle; 052import org.hl7.fhir.instance.model.api.IBaseReference; 053import org.hl7.fhir.instance.model.api.IBaseResource; 054import org.hl7.fhir.instance.model.api.IPrimitiveType; 055import org.slf4j.Logger; 056import org.slf4j.LoggerFactory; 057 058import java.math.BigDecimal; 059import java.util.ArrayList; 060import java.util.HashMap; 061import java.util.LinkedHashSet; 062import java.util.List; 063import java.util.Map; 064import java.util.Objects; 065import java.util.Set; 066import java.util.function.Consumer; 067import java.util.stream.Collectors; 068 069import static org.apache.commons.lang3.StringUtils.defaultString; 070import static org.apache.commons.lang3.StringUtils.isBlank; 071import static org.apache.commons.lang3.StringUtils.isNotBlank; 072import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV; 073 074/** 075 * Fetch resources from a bundle 076 */ 077public class BundleUtil { 078 079 public static final String DIFFERENT_LINK_ERROR_MSG = 080 "Mismatching 'previous' and 'prev' links exist. 'previous' " + "is: '$PREVIOUS' and 'prev' is: '$PREV'."; 081 public static final String BUNDLE_TYPE_TRANSACTION_RESPONSE = "transaction-response"; 082 private static final Logger ourLog = LoggerFactory.getLogger(BundleUtil.class); 083 084 private static final String PREVIOUS = LINK_PREV; 085 private static final String PREV = "prev"; 086 private static final Set<String> previousOrPrev = Sets.newHashSet(PREVIOUS, PREV); 087 static int WHITE = 1; 088 static int GRAY = 2; 089 static int BLACK = 3; 090 091 /** 092 * Non instantiable 093 */ 094 private BundleUtil() { 095 // nothing 096 } 097 098 /** 099 * @return Returns <code>null</code> if the link isn't found or has no value 100 */ 101 public static String getLinkUrlOfType(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) { 102 return getLinkUrlOfType(theContext, theBundle, theLinkRelation, true); 103 } 104 105 private static String getLinkUrlOfType( 106 FhirContext theContext, IBaseBundle theBundle, String theLinkRelation, boolean isPreviousCheck) { 107 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 108 BaseRuntimeChildDefinition entryChild = def.getChildByName("link"); 109 List<IBase> links = entryChild.getAccessor().getValues(theBundle); 110 for (IBase nextLink : links) { 111 112 boolean isRightRel = false; 113 BaseRuntimeElementCompositeDefinition<?> relDef = 114 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass()); 115 BaseRuntimeChildDefinition relChild = relDef.getChildByName("relation"); 116 List<IBase> relValues = relChild.getAccessor().getValues(nextLink); 117 for (IBase next : relValues) { 118 IPrimitiveType<?> nextValue = (IPrimitiveType<?>) next; 119 if (isRelationMatch( 120 theContext, theBundle, theLinkRelation, nextValue.getValueAsString(), isPreviousCheck)) { 121 isRightRel = true; 122 } 123 } 124 125 if (!isRightRel) { 126 continue; 127 } 128 129 BaseRuntimeElementCompositeDefinition<?> linkDef = 130 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass()); 131 BaseRuntimeChildDefinition urlChild = linkDef.getChildByName("url"); 132 List<IBase> values = urlChild.getAccessor().getValues(nextLink); 133 for (IBase nextUrl : values) { 134 IPrimitiveType<?> nextValue = (IPrimitiveType<?>) nextUrl; 135 if (isNotBlank(nextValue.getValueAsString())) { 136 return nextValue.getValueAsString(); 137 } 138 } 139 } 140 141 return null; 142 } 143 144 private static boolean isRelationMatch( 145 FhirContext theContext, IBaseBundle theBundle, String value, String matching, boolean theIsPreviousCheck) { 146 if (!theIsPreviousCheck) { 147 return value.equals(matching); 148 } 149 150 if (previousOrPrev.contains(value)) { 151 validateUniqueOrMatchingPreviousValues(theContext, theBundle); 152 if (previousOrPrev.contains(matching)) { 153 return true; 154 } 155 } 156 return (value.equals(matching)); 157 } 158 159 private static void validateUniqueOrMatchingPreviousValues(FhirContext theContext, IBaseBundle theBundle) { 160 String previousLink = getLinkNoCheck(theContext, theBundle, PREVIOUS); 161 String prevLink = getLinkNoCheck(theContext, theBundle, PREV); 162 if (prevLink != null && previousLink != null) { 163 if (!previousLink.equals(prevLink)) { 164 String msg = DIFFERENT_LINK_ERROR_MSG 165 .replace("$PREVIOUS", previousLink) 166 .replace("$PREV", prevLink); 167 throw new InternalErrorException(Msg.code(2368) + msg); 168 } 169 } 170 } 171 172 private static String getLinkNoCheck(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) { 173 return getLinkUrlOfType(theContext, theBundle, theLinkRelation, false); 174 } 175 176 /** 177 * Returns a collection of Pairs, one for each entry in the bundle. Each pair will contain 178 * the values of Bundle.entry.fullUrl, and Bundle.entry.resource respectively. Nulls 179 * are possible in either or both values in the Pair. 180 * 181 * @since 7.0.0 182 */ 183 @SuppressWarnings("unchecked") 184 public static List<Pair<String, IBaseResource>> getBundleEntryFullUrlsAndResources( 185 FhirContext theContext, IBaseBundle theBundle) { 186 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 187 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 188 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 189 190 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 191 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 192 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 193 194 BaseRuntimeChildDefinition urlChild = entryChildElem.getChildByName("fullUrl"); 195 196 List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size()); 197 for (IBase nextEntry : entries) { 198 199 String fullUrl = urlChild.getAccessor() 200 .getFirstValueOrNull(nextEntry) 201 .map(t -> (((IPrimitiveType<?>) t).getValueAsString())) 202 .orElse(null); 203 IBaseResource resource = (IBaseResource) 204 resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null); 205 206 retVal.add(Pair.of(fullUrl, resource)); 207 } 208 209 return retVal; 210 } 211 212 /** 213 * Returns true if the resource provided is *not* a BundleEntrySearchModeEnum OUTCOME or INCLUDE 214 * (ie, if MATCH or null, this will return true) 215 * @param theResource 216 * @return 217 */ 218 public static boolean isMatchResource(IBaseResource theResource) { 219 BundleEntrySearchModeEnum matchType = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(theResource); 220 if (matchType == null || matchType == BundleEntrySearchModeEnum.MATCH) { 221 return true; 222 } 223 return false; 224 } 225 226 public static List<Pair<String, IBaseResource>> getBundleEntryUrlsAndResources( 227 FhirContext theContext, IBaseBundle theBundle) { 228 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 229 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 230 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 231 232 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 233 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 234 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 235 236 BaseRuntimeChildDefinition requestChild = entryChildElem.getChildByName("request"); 237 BaseRuntimeElementCompositeDefinition<?> requestDef = 238 (BaseRuntimeElementCompositeDefinition<?>) requestChild.getChildByName("request"); 239 240 BaseRuntimeChildDefinition urlChild = requestDef.getChildByName("url"); 241 242 List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size()); 243 for (IBase nextEntry : entries) { 244 245 String url = requestChild 246 .getAccessor() 247 .getFirstValueOrNull(nextEntry) 248 .flatMap(e -> urlChild.getAccessor().getFirstValueOrNull(e)) 249 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 250 .orElse(null); 251 252 IBaseResource resource = (IBaseResource) 253 resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null); 254 255 retVal.add(Pair.of(url, resource)); 256 } 257 258 return retVal; 259 } 260 261 public static String getBundleType(FhirContext theContext, IBaseBundle theBundle) { 262 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 263 BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); 264 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 265 if (entries.size() > 0) { 266 IPrimitiveType<?> typeElement = (IPrimitiveType<?>) entries.get(0); 267 return typeElement.getValueAsString(); 268 } 269 return null; 270 } 271 272 public static BundleTypeEnum getBundleTypeEnum(FhirContext theContext, IBaseBundle theBundle) { 273 String bundleTypeCode = BundleUtil.getBundleType(theContext, theBundle); 274 if (isBlank(bundleTypeCode)) { 275 return null; 276 } 277 return BundleTypeEnum.forCode(bundleTypeCode); 278 } 279 280 public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) { 281 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 282 BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); 283 BaseRuntimeElementDefinition<?> element = entryChild.getChildByName("type"); 284 IPrimitiveType<?> typeInstance = 285 (IPrimitiveType<?>) element.newInstance(entryChild.getInstanceConstructorArguments()); 286 typeInstance.setValueAsString(theType); 287 288 entryChild.getMutator().setValue(theBundle, typeInstance); 289 } 290 291 public static Integer getTotal(FhirContext theContext, IBaseBundle theBundle) { 292 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 293 BaseRuntimeChildDefinition entryChild = def.getChildByName("total"); 294 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 295 if (entries.size() > 0) { 296 @SuppressWarnings("unchecked") 297 IPrimitiveType<Number> typeElement = (IPrimitiveType<Number>) entries.get(0); 298 if (typeElement != null && typeElement.getValue() != null) { 299 return typeElement.getValue().intValue(); 300 } 301 } 302 return null; 303 } 304 305 public static void setTotal(FhirContext theContext, IBaseBundle theBundle, Integer theTotal) { 306 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 307 BaseRuntimeChildDefinition entryChild = def.getChildByName("total"); 308 @SuppressWarnings("unchecked") 309 IPrimitiveType<Integer> value = 310 (IPrimitiveType<Integer>) entryChild.getChildByName("total").newInstance(); 311 value.setValue(theTotal); 312 entryChild.getMutator().setValue(theBundle, value); 313 } 314 315 /** 316 * Extract all of the resources from a given bundle 317 */ 318 public static List<BundleEntryParts> toListOfEntries(FhirContext theContext, IBaseBundle theBundle) { 319 EntryListAccumulator entryListAccumulator = new EntryListAccumulator(); 320 processEntries(theContext, theBundle, entryListAccumulator); 321 return entryListAccumulator.getList(); 322 } 323 324 public static <T> List<T> toListOfEntries( 325 FhirContext theContext, IBaseBundle theBundle, PartsConverter<T> partsConverter) { 326 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 327 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 328 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 329 return entries.stream().map(partsConverter::fromElement).toList(); 330 } 331 332 /** 333 * Returns a list of entries in the Bundle with a modifiable type that can be used 334 * to manipulate the entries. 335 * 336 * @param theContext The FhirContext associated with the version of FHIR 337 * @param theBundle The Bundle 338 * @return A list of modifiable entries 339 * @since 8.6.0 340 */ 341 public static List<ModifiableBundleEntryParts> toListOfEntriesModifiable( 342 FhirContext theContext, IBaseBundle theBundle) { 343 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 344 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 345 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 346 347 BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = 348 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 349 BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl"); 350 BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource"); 351 BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request"); 352 BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = 353 (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request"); 354 BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url"); 355 BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist"); 356 BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method"); 357 358 List<ModifiableBundleEntryParts> retVal = new ArrayList<>(entries.size()); 359 for (IBase nextEntry : entries) { 360 BundleEntryParts parts = getBundleEntryParts( 361 fullUrlChildDef, 362 resourceChildDef, 363 requestChildDef, 364 requestUrlChildDef, 365 requestIfNoneExistChildDef, 366 methodChildDef, 367 nextEntry); 368 /* 369 * All 3 might be null - That's ok because we still want to know the 370 * order in the original bundle. 371 */ 372 BundleEntryMutator mutator = new BundleEntryMutator( 373 theContext, 374 nextEntry, 375 requestChildDef, 376 requestChildContentsDef, 377 entryChildContentsDef, 378 methodChildDef); 379 ModifiableBundleEntryParts entry = new ModifiableBundleEntryParts(theContext, parts, mutator); 380 retVal.add(entry); 381 } 382 383 return retVal; 384 } 385 386 /** 387 * Invokes a consumer for each entry in the Bundle, passing a {@link ModifiableBundleEntryParts} 388 * which contains a version-independent means of accessing and modifiing each entry 389 * 390 * @param theContext The FhirContext for the FHIR version associated with the Bundle 391 * @param theBundle The Bundle to process 392 * @param theEntryConsumer A consumer to process each entry 393 * @since 8.6.0 394 */ 395 public static void processAllEntries( 396 FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntryParts> theEntryConsumer) { 397 toListOfEntriesModifiable(theContext, theBundle).forEach(theEntryConsumer); 398 } 399 400 /** 401 * Function which will do an in-place sort of a bundles' entries, to the correct processing order, which is: 402 * 1. Deletes 403 * 2. Creates 404 * 3. Updates 405 * <p> 406 * Furthermore, within these operation types, the entries will be sorted based on the order in which they should be processed 407 * e.g. if you have 2 CREATEs, one for a Patient, and one for an Observation which has this Patient as its Subject, 408 * the patient will come first, then the observation. 409 * <p> 410 * In cases of there being a cyclic dependency (e.g. Organization/1 is partOf Organization/2 and Organization/2 is partOf Organization/1) 411 * this function will throw an IllegalStateException. 412 * 413 * @param theContext The FhirContext. 414 * @param theBundle The {@link IBaseBundle} which contains the entries you would like sorted into processing order. 415 */ 416 public static void sortEntriesIntoProcessingOrder(FhirContext theContext, IBaseBundle theBundle) 417 throws IllegalStateException { 418 Map<BundleEntryParts, IBase> partsToIBaseMap = getPartsToIBaseMap(theContext, theBundle); 419 420 // Get all deletions. 421 LinkedHashSet<IBase> deleteParts = 422 sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.DELETE, partsToIBaseMap); 423 validatePartsNotNull(deleteParts); 424 LinkedHashSet<IBase> retVal = new LinkedHashSet<>(deleteParts); 425 426 // Get all Creations 427 LinkedHashSet<IBase> createParts = 428 sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.POST, partsToIBaseMap); 429 validatePartsNotNull(createParts); 430 retVal.addAll(createParts); 431 432 // Get all Updates 433 LinkedHashSet<IBase> updateParts = 434 sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.PUT, partsToIBaseMap); 435 validatePartsNotNull(updateParts); 436 retVal.addAll(updateParts); 437 438 // Once we are done adding all DELETE, POST, PUT operations, add everything else. 439 // Since this is a set, it will just fail to add already-added operations. 440 retVal.addAll(partsToIBaseMap.values()); 441 442 // Blow away the entries and reset them in the right order. 443 TerserUtil.clearField(theContext, theBundle, "entry"); 444 TerserUtil.setField(theContext, "entry", theBundle, retVal.toArray(new IBase[0])); 445 } 446 447 /** 448 * Converts a Bundle containing resources into a FHIR transaction which 449 * creates/updates the resources. This method does not modify the original 450 * bundle, but returns a new copy. 451 * <p> 452 * This method is mostly intended for test scenarios where you have a Bundle 453 * containing search results or other sourced resources, and want to upload 454 * these resources to a server using a single FHIR transaction. 455 * </p> 456 * <p> 457 * The Bundle is converted using the following logic: 458 * <ul> 459 * <li>Bundle.type is changed to <code>transaction</code></li> 460 * <li>Bundle.request.method is changed to <code>PUT</code></li> 461 * <li>Bundle.request.url is changed to <code>[resourceType]/[id]</code></li> 462 * <li>Bundle.fullUrl is changed to <code>[resourceType]/[id]</code></li> 463 * </ul> 464 * </p> 465 * 466 * @param theContext The FhirContext to use with the bundle 467 * @param theBundle The Bundle to modify. All resources in the Bundle should have an ID. 468 * @param thePrefixIdsOrNull If not <code>null</code>, all resource IDs and all references in the Bundle will be 469 * modified to such that their IDs contain the given prefix. For example, for a value 470 * of "A", the resource "Patient/123" will be changed to be "Patient/A123". If set to 471 * <code>null</code>, resource IDs are unchanged. 472 * @since 7.4.0 473 */ 474 public static <T extends IBaseBundle> T convertBundleIntoTransaction( 475 @Nonnull FhirContext theContext, @Nonnull T theBundle, @Nullable String thePrefixIdsOrNull) { 476 String prefix = defaultString(thePrefixIdsOrNull); 477 478 BundleBuilder bb = new BundleBuilder(theContext); 479 480 FhirTerser terser = theContext.newTerser(); 481 List<IBase> entries = terser.getValues(theBundle, "Bundle.entry"); 482 for (var entry : entries) { 483 IBaseResource resource = terser.getSingleValueOrNull(entry, "resource", IBaseResource.class); 484 if (resource != null) { 485 Validate.isTrue(resource.getIdElement().hasIdPart(), "Resource in bundle has no ID"); 486 String newId = theContext.getResourceType(resource) + "/" + prefix 487 + resource.getIdElement().getIdPart(); 488 489 IBaseResource resourceClone = terser.clone(resource); 490 resourceClone.setId(newId); 491 492 if (isNotBlank(prefix)) { 493 for (var ref : terser.getAllResourceReferences(resourceClone)) { 494 var refElement = ref.getResourceReference().getReferenceElement(); 495 ref.getResourceReference() 496 .setReference(refElement.getResourceType() + "/" + prefix + refElement.getIdPart()); 497 } 498 } 499 500 bb.addTransactionUpdateEntry(resourceClone); 501 } 502 } 503 504 return bb.getBundleTyped(); 505 } 506 507 private static void validatePartsNotNull(LinkedHashSet<IBase> theDeleteParts) { 508 if (theDeleteParts == null) { 509 throw new IllegalStateException( 510 Msg.code(1745) + "This transaction contains a cycle, so it cannot be sorted."); 511 } 512 } 513 514 private static LinkedHashSet<IBase> sortEntriesOfTypeIntoProcessingOrder( 515 FhirContext theContext, 516 RequestTypeEnum theRequestTypeEnum, 517 Map<BundleEntryParts, IBase> thePartsToIBaseMap) { 518 SortLegality legality = new SortLegality(); 519 HashMap<String, Integer> color = new HashMap<>(); 520 HashMap<String, List<String>> adjList = new HashMap<>(); 521 List<String> topologicalOrder = new ArrayList<>(); 522 Set<BundleEntryParts> bundleEntryParts = thePartsToIBaseMap.keySet().stream() 523 .filter(part -> part.getRequestType().equals(theRequestTypeEnum)) 524 .collect(Collectors.toSet()); 525 HashMap<String, BundleEntryParts> resourceIdToBundleEntryMap = new HashMap<>(); 526 527 for (BundleEntryParts bundleEntryPart : bundleEntryParts) { 528 IBaseResource resource = bundleEntryPart.getResource(); 529 if (resource != null) { 530 String resourceId = resource.getIdElement().toVersionless().toString(); 531 resourceIdToBundleEntryMap.put(resourceId, bundleEntryPart); 532 if (resourceId == null) { 533 if (bundleEntryPart.getFullUrl() != null) { 534 resourceId = bundleEntryPart.getFullUrl(); 535 } 536 } 537 538 color.put(resourceId, WHITE); 539 } 540 } 541 542 for (BundleEntryParts bundleEntryPart : bundleEntryParts) { 543 IBaseResource resource = bundleEntryPart.getResource(); 544 if (resource != null) { 545 String resourceId = resource.getIdElement().toVersionless().toString(); 546 resourceIdToBundleEntryMap.put(resourceId, bundleEntryPart); 547 if (resourceId == null) { 548 if (bundleEntryPart.getFullUrl() != null) { 549 resourceId = bundleEntryPart.getFullUrl(); 550 } 551 } 552 List<ResourceReferenceInfo> allResourceReferences = 553 theContext.newTerser().getAllResourceReferences(resource); 554 String finalResourceId = resourceId; 555 allResourceReferences.forEach(refInfo -> { 556 String referencedResourceId = refInfo.getResourceReference() 557 .getReferenceElement() 558 .toVersionless() 559 .getValue(); 560 if (color.containsKey(referencedResourceId)) { 561 if (!adjList.containsKey(finalResourceId)) { 562 adjList.put(finalResourceId, new ArrayList<>()); 563 } 564 adjList.get(finalResourceId).add(referencedResourceId); 565 } 566 }); 567 } 568 } 569 570 for (Map.Entry<String, Integer> entry : color.entrySet()) { 571 if (entry.getValue() == WHITE) { 572 depthFirstSearch(entry.getKey(), color, adjList, topologicalOrder, legality); 573 } 574 } 575 576 if (legality.isLegal()) { 577 if (ourLog.isDebugEnabled()) { 578 ourLog.debug("Topological order is: {}", String.join(",", topologicalOrder)); 579 } 580 581 LinkedHashSet<IBase> orderedEntries = new LinkedHashSet<>(); 582 for (int i = 0; i < topologicalOrder.size(); i++) { 583 BundleEntryParts bep; 584 if (theRequestTypeEnum.equals(RequestTypeEnum.DELETE)) { 585 int index = topologicalOrder.size() - i - 1; 586 bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(index)); 587 } else { 588 bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(i)); 589 } 590 IBase base = thePartsToIBaseMap.get(bep); 591 orderedEntries.add(base); 592 } 593 594 return orderedEntries; 595 596 } else { 597 return null; 598 } 599 } 600 601 private static void depthFirstSearch( 602 String theResourceId, 603 HashMap<String, Integer> theResourceIdToColor, 604 HashMap<String, List<String>> theAdjList, 605 List<String> theTopologicalOrder, 606 SortLegality theLegality) { 607 608 if (!theLegality.isLegal()) { 609 ourLog.debug("Found a cycle while trying to sort bundle entries. This bundle is not sortable."); 610 return; 611 } 612 613 // We are currently recursing over this node (gray) 614 theResourceIdToColor.put(theResourceId, GRAY); 615 616 for (String neighbourResourceId : theAdjList.getOrDefault(theResourceId, new ArrayList<>())) { 617 if (theResourceIdToColor.get(neighbourResourceId) == WHITE) { 618 depthFirstSearch( 619 neighbourResourceId, theResourceIdToColor, theAdjList, theTopologicalOrder, theLegality); 620 } else if (theResourceIdToColor.get(neighbourResourceId) == GRAY) { 621 theLegality.setLegal(false); 622 return; 623 } 624 } 625 // Mark the node as black 626 theResourceIdToColor.put(theResourceId, BLACK); 627 theTopologicalOrder.add(theResourceId); 628 } 629 630 private static Map<BundleEntryParts, IBase> getPartsToIBaseMap(FhirContext theContext, IBaseBundle theBundle) { 631 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 632 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 633 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 634 635 BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = 636 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 637 BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl"); 638 BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource"); 639 BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request"); 640 BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = 641 (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request"); 642 BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url"); 643 BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist"); 644 BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method"); 645 Map<BundleEntryParts, IBase> map = new HashMap<>(); 646 for (IBase nextEntry : entries) { 647 BundleEntryParts parts = getBundleEntryParts( 648 fullUrlChildDef, 649 resourceChildDef, 650 requestChildDef, 651 requestUrlChildDef, 652 requestIfNoneExistChildDef, 653 methodChildDef, 654 nextEntry); 655 /* 656 * All 3 might be null - That's ok because we still want to know the 657 * order in the original bundle. 658 */ 659 map.put(parts, nextEntry); 660 } 661 return map; 662 } 663 664 public static List<SearchBundleEntryParts> getSearchBundleEntryParts( 665 FhirContext theContext, IBaseBundle theBundle) { 666 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 667 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 668 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 669 670 BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = 671 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 672 BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl"); 673 BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource"); 674 BaseRuntimeChildDefinition searchChildDef = entryChildContentsDef.getChildByName("search"); 675 BaseRuntimeElementCompositeDefinition<?> searchChildContentsDef = 676 (BaseRuntimeElementCompositeDefinition<?>) searchChildDef.getChildByName("search"); 677 BaseRuntimeChildDefinition searchModeChildDef = searchChildContentsDef.getChildByName("mode"); 678 BaseRuntimeChildDefinition searchScoreChildDef = searchChildContentsDef.getChildByName("score"); 679 680 List<SearchBundleEntryParts> retVal = new ArrayList<>(); 681 for (IBase nextEntry : entries) { 682 SearchBundleEntryParts parts = getSearchBundleEntryParts( 683 fullUrlChildDef, 684 resourceChildDef, 685 searchChildDef, 686 searchModeChildDef, 687 searchScoreChildDef, 688 nextEntry); 689 retVal.add(parts); 690 } 691 return retVal; 692 } 693 694 private static SearchBundleEntryParts getSearchBundleEntryParts( 695 BaseRuntimeChildDefinition theFullUrlChildDef, 696 BaseRuntimeChildDefinition theResourceChildDef, 697 BaseRuntimeChildDefinition theSearchChildDef, 698 BaseRuntimeChildDefinition theSearchModeChildDef, 699 BaseRuntimeChildDefinition theSearchScoreChildDef, 700 IBase entry) { 701 IBaseResource resource = null; 702 String matchMode = null; 703 BigDecimal searchScore = null; 704 705 String fullUrl = theFullUrlChildDef 706 .getAccessor() 707 .getFirstValueOrNull(entry) 708 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 709 .orElse(null); 710 711 for (IBase nextResource : theResourceChildDef.getAccessor().getValues(entry)) { 712 resource = (IBaseResource) nextResource; 713 } 714 715 for (IBase nextSearch : theSearchChildDef.getAccessor().getValues(entry)) { 716 for (IBase nextUrl : theSearchModeChildDef.getAccessor().getValues(nextSearch)) { 717 matchMode = ((IPrimitiveType<?>) nextUrl).getValueAsString(); 718 } 719 for (IBase nextUrl : theSearchScoreChildDef.getAccessor().getValues(nextSearch)) { 720 searchScore = (BigDecimal) ((IPrimitiveType<?>) nextUrl).getValue(); 721 } 722 } 723 724 return new SearchBundleEntryParts(fullUrl, resource, matchMode, searchScore); 725 } 726 727 /** 728 * Given a bundle, and a consumer, apply the consumer to each entry in the bundle. 729 * 730 * @param theContext The FHIR Context 731 * @param theBundle The bundle to have its entries processed. 732 * @param theProcessor a {@link Consumer} which will operate on all the entries of a bundle. 733 * @deprecated Use {@link #processAllEntries(FhirContext, IBaseBundle, Consumer)} instead 734 */ 735 @Deprecated(since = "8.6.0", forRemoval = true) 736 public static void processEntries( 737 FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntry> theProcessor) { 738 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 739 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 740 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 741 742 BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = 743 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 744 BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl"); 745 BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource"); 746 BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request"); 747 BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = 748 (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request"); 749 BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url"); 750 BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist"); 751 BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method"); 752 753 for (IBase nextEntry : entries) { 754 BundleEntryParts parts = getBundleEntryParts( 755 fullUrlChildDef, 756 resourceChildDef, 757 requestChildDef, 758 requestUrlChildDef, 759 requestIfNoneExistChildDef, 760 methodChildDef, 761 nextEntry); 762 /* 763 * All 3 might be null - That's ok because we still want to know the 764 * order in the original bundle. 765 */ 766 BundleEntryMutator mutator = new BundleEntryMutator( 767 theContext, 768 nextEntry, 769 requestChildDef, 770 requestChildContentsDef, 771 entryChildContentsDef, 772 methodChildDef); 773 ModifiableBundleEntry entry = new ModifiableBundleEntry(parts, mutator); 774 theProcessor.accept(entry); 775 } 776 } 777 778 private static BundleEntryParts getBundleEntryParts( 779 BaseRuntimeChildDefinition fullUrlChildDef, 780 BaseRuntimeChildDefinition resourceChildDef, 781 BaseRuntimeChildDefinition requestChildDef, 782 BaseRuntimeChildDefinition requestUrlChildDef, 783 BaseRuntimeChildDefinition requestIfNoneExistChildDef, 784 BaseRuntimeChildDefinition methodChildDef, 785 IBase nextEntry) { 786 IBaseResource resource = null; 787 String url = null; 788 RequestTypeEnum requestType = null; 789 String conditionalUrl = null; 790 String fullUrl = fullUrlChildDef 791 .getAccessor() 792 .getFirstValueOrNull(nextEntry) 793 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 794 .orElse(null); 795 796 for (IBase nextResource : resourceChildDef.getAccessor().getValues(nextEntry)) { 797 resource = (IBaseResource) nextResource; 798 } 799 for (IBase nextRequest : requestChildDef.getAccessor().getValues(nextEntry)) { 800 for (IBase nextUrl : requestUrlChildDef.getAccessor().getValues(nextRequest)) { 801 url = ((IPrimitiveType<?>) nextUrl).getValueAsString(); 802 } 803 for (IBase nextMethod : methodChildDef.getAccessor().getValues(nextRequest)) { 804 String methodString = ((IPrimitiveType<?>) nextMethod).getValueAsString(); 805 if (isNotBlank(methodString)) { 806 requestType = RequestTypeEnum.valueOf(methodString); 807 } 808 } 809 810 if (requestType != null) { 811 //noinspection EnumSwitchStatementWhichMissesCases 812 switch (requestType) { 813 case PUT, DELETE, PATCH -> conditionalUrl = url != null && url.contains("?") ? url : null; 814 case POST -> { 815 List<IBase> ifNoneExistReps = 816 requestIfNoneExistChildDef.getAccessor().getValues(nextRequest); 817 if (!ifNoneExistReps.isEmpty()) { 818 IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) ifNoneExistReps.get(0); 819 conditionalUrl = ifNoneExist.getValueAsString(); 820 } 821 } 822 } 823 } 824 } 825 return new BundleEntryParts(fullUrl, requestType, url, resource, conditionalUrl, requestType); 826 } 827 828 /** 829 * Extract all of the resources from a given bundle 830 */ 831 public static List<IBaseResource> toListOfResources(FhirContext theContext, IBaseBundle theBundle) { 832 return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class); 833 } 834 835 /** 836 * Extract all of ids of all the resources from a given bundle 837 */ 838 public static List<String> toListOfResourceIds(FhirContext theContext, IBaseBundle theBundle) { 839 return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class).stream() 840 .map(resource -> resource.getIdElement().getIdPart()) 841 .collect(Collectors.toList()); 842 } 843 844 /** 845 * Extract all of the resources of a given type from a given bundle 846 */ 847 @SuppressWarnings("unchecked") 848 public static <T extends IBaseResource> List<T> toListOfResourcesOfType( 849 FhirContext theContext, IBaseBundle theBundle, Class<T> theTypeToInclude) { 850 Objects.requireNonNull(theTypeToInclude, "ResourceType must not be null"); 851 List<T> retVal = new ArrayList<>(); 852 853 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 854 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 855 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 856 857 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 858 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 859 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 860 for (IBase nextEntry : entries) { 861 for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) { 862 if (theTypeToInclude.isAssignableFrom(next.getClass())) { 863 retVal.add((T) next); 864 } 865 } 866 } 867 return retVal; 868 } 869 870 @Nonnull 871 public static List<CanonicalBundleEntry> toListOfCanonicalBundleEntries( 872 FhirContext theContext, IBaseBundle theBundle) { 873 List<CanonicalBundleEntry> retVal = new ArrayList<>(); 874 875 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 876 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 877 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 878 879 for (IBase nextEntry : entries) { 880 CanonicalBundleEntry canonicalEntry = 881 CanonicalBundleEntry.fromBundleEntry(theContext, (IBaseBackboneElement) nextEntry); 882 retVal.add(canonicalEntry); 883 } 884 885 return retVal; 886 } 887 888 public static IBase getReferenceInBundle( 889 @Nonnull FhirContext theFhirContext, @Nonnull String theUrl, @Nullable Object theAppContext) { 890 if (!(theAppContext instanceof IBaseBundle) || isBlank(theUrl) || theUrl.startsWith("#")) { 891 return null; 892 } 893 894 /* 895 * If this is a reference that is a UUID, we must be looking for local references within a Bundle 896 */ 897 IBaseBundle bundle = (IBaseBundle) theAppContext; 898 899 final boolean isPlaceholderReference = theUrl.startsWith("urn:"); 900 final String unqualifiedVersionlessReference = 901 new IdDt(theUrl).toUnqualifiedVersionless().getValue(); 902 903 for (BundleEntryParts next : BundleUtil.toListOfEntries(theFhirContext, bundle)) { 904 IBaseResource nextResource = next.getResource(); 905 if (nextResource == null) { 906 continue; 907 } 908 if (isPlaceholderReference) { 909 if (theUrl.equals(next.getFullUrl()) 910 || theUrl.equals(nextResource.getIdElement().getValue())) { 911 return nextResource; 912 } 913 } else { 914 if (unqualifiedVersionlessReference.equals( 915 nextResource.getIdElement().toUnqualifiedVersionless().getValue())) { 916 return nextResource; 917 } 918 } 919 } 920 return null; 921 } 922 923 /** 924 * DSTU3 did not allow the PATCH verb for transaction bundles- so instead we infer that a bundle 925 * is a patch if the payload is a binary resource containing a patch. This method 926 * tests whether a resource (which should have come from 927 * <code>Bundle.entry.resource</code> is a Binary resource with a patch 928 * payload type. 929 */ 930 public static boolean isDstu3TransactionPatch(FhirContext theContext, IBaseResource thePayloadResource) { 931 boolean isPatch = false; 932 if (thePayloadResource instanceof IBaseBinary) { 933 String contentType = ((IBaseBinary) thePayloadResource).getContentType(); 934 try { 935 PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(theContext, contentType); 936 isPatch = true; 937 } catch (InvalidRequestException e) { 938 // ignore 939 } 940 } 941 return isPatch; 942 } 943 944 /** 945 * create a new bundle entry and set a value for a single field 946 * 947 * @param theContext Context holding resource definition 948 * @param theFieldName Child field name of the bundle entry to set 949 * @param theValues The values to set on the bundle entry child field name 950 * @return the new bundle entry 951 */ 952 public static IBase createNewBundleEntryWithSingleField( 953 FhirContext theContext, String theFieldName, IBase... theValues) { 954 IBaseBundle newBundle = TerserUtil.newResource(theContext, "Bundle"); 955 BaseRuntimeChildDefinition entryChildDef = 956 theContext.getResourceDefinition(newBundle).getChildByName("entry"); 957 958 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 959 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 960 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName(theFieldName); 961 IBase bundleEntry = entryChildElem.newInstance(); 962 for (IBase value : theValues) { 963 try { 964 resourceChild.getMutator().addValue(bundleEntry, value); 965 } catch (UnsupportedOperationException e) { 966 ourLog.warn( 967 "Resource {} does not support multiple values, but an attempt to set {} was made. Setting the first item only", 968 bundleEntry, 969 theValues); 970 resourceChild.getMutator().setValue(bundleEntry, value); 971 break; 972 } 973 } 974 return bundleEntry; 975 } 976 977 /** 978 * Get resource from bundle by resource type and reference 979 * 980 * @param theContext FhirContext 981 * @param theBundle IBaseBundle 982 * @param theReference IBaseReference 983 * @return IBaseResource if found and null if not found. 984 */ 985 @Nonnull 986 public static IBaseResource getResourceByReferenceAndResourceType( 987 @Nonnull FhirContext theContext, @Nonnull IBaseBundle theBundle, @Nonnull IBaseReference theReference) { 988 return toListOfResources(theContext, theBundle).stream() 989 .filter(theResource -> theReference 990 .getReferenceElement() 991 .getIdPart() 992 .equals(theResource.getIdElement().getIdPart())) 993 .findFirst() 994 .orElse(null); 995 } 996 997 private static class SortLegality { 998 private boolean myIsLegal; 999 1000 SortLegality() { 1001 this.myIsLegal = true; 1002 } 1003 1004 public boolean isLegal() { 1005 return myIsLegal; 1006 } 1007 1008 private void setLegal(boolean theLegal) { 1009 myIsLegal = theLegal; 1010 } 1011 } 1012}