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