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