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