001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 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 /** Non instantiable */ 075 private BundleUtil() { 076 // nothing 077 } 078 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 085 public static final String DIFFERENT_LINK_ERROR_MSG = 086 "Mismatching 'previous' and 'prev' links exist. 'previous' " + "is: '$PREVIOUS' and 'prev' is: '$PREV'."; 087 088 /** 089 * @return Returns <code>null</code> if the link isn't found or has no value 090 */ 091 public static String getLinkUrlOfType(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) { 092 return getLinkUrlOfType(theContext, theBundle, theLinkRelation, true); 093 } 094 095 private static String getLinkUrlOfType( 096 FhirContext theContext, IBaseBundle theBundle, String theLinkRelation, boolean isPreviousCheck) { 097 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 098 BaseRuntimeChildDefinition entryChild = def.getChildByName("link"); 099 List<IBase> links = entryChild.getAccessor().getValues(theBundle); 100 for (IBase nextLink : links) { 101 102 boolean isRightRel = false; 103 BaseRuntimeElementCompositeDefinition<?> relDef = 104 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass()); 105 BaseRuntimeChildDefinition relChild = relDef.getChildByName("relation"); 106 List<IBase> relValues = relChild.getAccessor().getValues(nextLink); 107 for (IBase next : relValues) { 108 IPrimitiveType<?> nextValue = (IPrimitiveType<?>) next; 109 if (isRelationMatch( 110 theContext, theBundle, theLinkRelation, nextValue.getValueAsString(), isPreviousCheck)) { 111 isRightRel = true; 112 } 113 } 114 115 if (!isRightRel) { 116 continue; 117 } 118 119 BaseRuntimeElementCompositeDefinition<?> linkDef = 120 (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass()); 121 BaseRuntimeChildDefinition urlChild = linkDef.getChildByName("url"); 122 List<IBase> values = urlChild.getAccessor().getValues(nextLink); 123 for (IBase nextUrl : values) { 124 IPrimitiveType<?> nextValue = (IPrimitiveType<?>) nextUrl; 125 if (isNotBlank(nextValue.getValueAsString())) { 126 return nextValue.getValueAsString(); 127 } 128 } 129 } 130 131 return null; 132 } 133 134 private static boolean isRelationMatch( 135 FhirContext theContext, IBaseBundle theBundle, String value, String matching, boolean theIsPreviousCheck) { 136 if (!theIsPreviousCheck) { 137 return value.equals(matching); 138 } 139 140 if (previousOrPrev.contains(value)) { 141 validateUniqueOrMatchingPreviousValues(theContext, theBundle); 142 if (previousOrPrev.contains(matching)) { 143 return true; 144 } 145 } 146 return (value.equals(matching)); 147 } 148 149 private static void validateUniqueOrMatchingPreviousValues(FhirContext theContext, IBaseBundle theBundle) { 150 String previousLink = getLinkNoCheck(theContext, theBundle, PREVIOUS); 151 String prevLink = getLinkNoCheck(theContext, theBundle, PREV); 152 if (prevLink != null && previousLink != null) { 153 if (!previousLink.equals(prevLink)) { 154 String msg = DIFFERENT_LINK_ERROR_MSG 155 .replace("$PREVIOUS", previousLink) 156 .replace("$PREV", prevLink); 157 throw new InternalErrorException(Msg.code(2368) + msg); 158 } 159 } 160 } 161 162 private static String getLinkNoCheck(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) { 163 return getLinkUrlOfType(theContext, theBundle, theLinkRelation, false); 164 } 165 166 /** 167 * Returns a collection of Pairs, one for each entry in the bundle. Each pair will contain 168 * the values of Bundle.entry.fullUrl, and Bundle.entry.resource respectively. Nulls 169 * are possible in either or both values in the Pair. 170 * 171 * @since 7.0.0 172 */ 173 @SuppressWarnings("unchecked") 174 public static List<Pair<String, IBaseResource>> getBundleEntryFullUrlsAndResources( 175 FhirContext theContext, IBaseBundle theBundle) { 176 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 177 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 178 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 179 180 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 181 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 182 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 183 184 BaseRuntimeChildDefinition urlChild = entryChildElem.getChildByName("fullUrl"); 185 186 List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size()); 187 for (IBase nextEntry : entries) { 188 189 String fullUrl = urlChild.getAccessor() 190 .getFirstValueOrNull(nextEntry) 191 .map(t -> (((IPrimitiveType<?>) t).getValueAsString())) 192 .orElse(null); 193 IBaseResource resource = (IBaseResource) 194 resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null); 195 196 retVal.add(Pair.of(fullUrl, resource)); 197 } 198 199 return retVal; 200 } 201 202 public static List<Pair<String, IBaseResource>> getBundleEntryUrlsAndResources( 203 FhirContext theContext, IBaseBundle theBundle) { 204 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 205 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 206 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 207 208 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 209 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 210 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 211 212 BaseRuntimeChildDefinition requestChild = entryChildElem.getChildByName("request"); 213 BaseRuntimeElementCompositeDefinition<?> requestDef = 214 (BaseRuntimeElementCompositeDefinition<?>) requestChild.getChildByName("request"); 215 216 BaseRuntimeChildDefinition urlChild = requestDef.getChildByName("url"); 217 218 List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size()); 219 for (IBase nextEntry : entries) { 220 221 String url = requestChild 222 .getAccessor() 223 .getFirstValueOrNull(nextEntry) 224 .flatMap(e -> urlChild.getAccessor().getFirstValueOrNull(e)) 225 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 226 .orElse(null); 227 228 IBaseResource resource = (IBaseResource) 229 resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null); 230 231 retVal.add(Pair.of(url, resource)); 232 } 233 234 return retVal; 235 } 236 237 public static String getBundleType(FhirContext theContext, IBaseBundle theBundle) { 238 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 239 BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); 240 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 241 if (entries.size() > 0) { 242 IPrimitiveType<?> typeElement = (IPrimitiveType<?>) entries.get(0); 243 return typeElement.getValueAsString(); 244 } 245 return null; 246 } 247 248 public static BundleTypeEnum getBundleTypeEnum(FhirContext theContext, IBaseBundle theBundle) { 249 String bundleTypeCode = BundleUtil.getBundleType(theContext, theBundle); 250 if (isBlank(bundleTypeCode)) { 251 return null; 252 } 253 return BundleTypeEnum.forCode(bundleTypeCode); 254 } 255 256 public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) { 257 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 258 BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); 259 BaseRuntimeElementDefinition<?> element = entryChild.getChildByName("type"); 260 IPrimitiveType<?> typeInstance = 261 (IPrimitiveType<?>) element.newInstance(entryChild.getInstanceConstructorArguments()); 262 typeInstance.setValueAsString(theType); 263 264 entryChild.getMutator().setValue(theBundle, typeInstance); 265 } 266 267 public static Integer getTotal(FhirContext theContext, IBaseBundle theBundle) { 268 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 269 BaseRuntimeChildDefinition entryChild = def.getChildByName("total"); 270 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 271 if (entries.size() > 0) { 272 @SuppressWarnings("unchecked") 273 IPrimitiveType<Number> typeElement = (IPrimitiveType<Number>) entries.get(0); 274 if (typeElement != null && typeElement.getValue() != null) { 275 return typeElement.getValue().intValue(); 276 } 277 } 278 return null; 279 } 280 281 public static void setTotal(FhirContext theContext, IBaseBundle theBundle, Integer theTotal) { 282 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 283 BaseRuntimeChildDefinition entryChild = def.getChildByName("total"); 284 @SuppressWarnings("unchecked") 285 IPrimitiveType<Integer> value = 286 (IPrimitiveType<Integer>) entryChild.getChildByName("total").newInstance(); 287 value.setValue(theTotal); 288 entryChild.getMutator().setValue(theBundle, value); 289 } 290 291 /** 292 * Extract all of the resources from a given bundle 293 */ 294 public static List<BundleEntryParts> toListOfEntries(FhirContext theContext, IBaseBundle theBundle) { 295 EntryListAccumulator entryListAccumulator = new EntryListAccumulator(); 296 processEntries(theContext, theBundle, entryListAccumulator); 297 return entryListAccumulator.getList(); 298 } 299 300 static int WHITE = 1; 301 static int GRAY = 2; 302 static int BLACK = 3; 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 * @param theContext The FHIR Context 634 * @param theBundle The bundle to have its entries processed. 635 * @param theProcessor a {@link Consumer} which will operate on all the entries of a bundle. 636 */ 637 public static void processEntries( 638 FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntry> theProcessor) { 639 RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle); 640 BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry"); 641 List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle); 642 643 BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef = 644 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 645 BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl"); 646 BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource"); 647 BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request"); 648 BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef = 649 (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request"); 650 BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url"); 651 BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist"); 652 BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method"); 653 654 for (IBase nextEntry : entries) { 655 BundleEntryParts parts = getBundleEntryParts( 656 fullUrlChildDef, 657 resourceChildDef, 658 requestChildDef, 659 requestUrlChildDef, 660 requestIfNoneExistChildDef, 661 methodChildDef, 662 nextEntry); 663 /* 664 * All 3 might be null - That's ok because we still want to know the 665 * order in the original bundle. 666 */ 667 BundleEntryMutator mutator = new BundleEntryMutator( 668 theContext, nextEntry, requestChildDef, requestChildContentsDef, entryChildContentsDef); 669 ModifiableBundleEntry entry = new ModifiableBundleEntry(parts, mutator); 670 theProcessor.accept(entry); 671 } 672 } 673 674 private static BundleEntryParts getBundleEntryParts( 675 BaseRuntimeChildDefinition fullUrlChildDef, 676 BaseRuntimeChildDefinition resourceChildDef, 677 BaseRuntimeChildDefinition requestChildDef, 678 BaseRuntimeChildDefinition requestUrlChildDef, 679 BaseRuntimeChildDefinition requestIfNoneExistChildDef, 680 BaseRuntimeChildDefinition methodChildDef, 681 IBase nextEntry) { 682 IBaseResource resource = null; 683 String url = null; 684 RequestTypeEnum requestType = null; 685 String conditionalUrl = null; 686 String fullUrl = fullUrlChildDef 687 .getAccessor() 688 .getFirstValueOrNull(nextEntry) 689 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 690 .orElse(null); 691 692 for (IBase nextResource : resourceChildDef.getAccessor().getValues(nextEntry)) { 693 resource = (IBaseResource) nextResource; 694 } 695 for (IBase nextRequest : requestChildDef.getAccessor().getValues(nextEntry)) { 696 for (IBase nextUrl : requestUrlChildDef.getAccessor().getValues(nextRequest)) { 697 url = ((IPrimitiveType<?>) nextUrl).getValueAsString(); 698 } 699 for (IBase nextMethod : methodChildDef.getAccessor().getValues(nextRequest)) { 700 String methodString = ((IPrimitiveType<?>) nextMethod).getValueAsString(); 701 if (isNotBlank(methodString)) { 702 requestType = RequestTypeEnum.valueOf(methodString); 703 } 704 } 705 706 if (requestType != null) { 707 //noinspection EnumSwitchStatementWhichMissesCases 708 switch (requestType) { 709 case PUT: 710 case DELETE: 711 case PATCH: 712 conditionalUrl = url != null && url.contains("?") ? url : null; 713 break; 714 case POST: 715 List<IBase> ifNoneExistReps = 716 requestIfNoneExistChildDef.getAccessor().getValues(nextRequest); 717 if (ifNoneExistReps.size() > 0) { 718 IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) ifNoneExistReps.get(0); 719 conditionalUrl = ifNoneExist.getValueAsString(); 720 } 721 break; 722 } 723 } 724 } 725 return new BundleEntryParts(fullUrl, requestType, url, resource, conditionalUrl); 726 } 727 728 /** 729 * Extract all of the resources from a given bundle 730 */ 731 public static List<IBaseResource> toListOfResources(FhirContext theContext, IBaseBundle theBundle) { 732 return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class); 733 } 734 735 /** 736 * Extract all of ids of all the resources from a given bundle 737 */ 738 public static List<String> toListOfResourceIds(FhirContext theContext, IBaseBundle theBundle) { 739 return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class).stream() 740 .map(resource -> resource.getIdElement().getIdPart()) 741 .collect(Collectors.toList()); 742 } 743 744 /** 745 * Extract all of the resources of a given type from a given bundle 746 */ 747 @SuppressWarnings("unchecked") 748 public static <T extends IBaseResource> List<T> toListOfResourcesOfType( 749 FhirContext theContext, IBaseBundle theBundle, Class<T> theTypeToInclude) { 750 Objects.requireNonNull(theTypeToInclude, "ResourceType must not be null"); 751 List<T> retVal = new ArrayList<>(); 752 753 RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); 754 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 755 List<IBase> entries = entryChild.getAccessor().getValues(theBundle); 756 757 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 758 (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 759 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); 760 for (IBase nextEntry : entries) { 761 for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) { 762 if (theTypeToInclude.isAssignableFrom(next.getClass())) { 763 retVal.add((T) next); 764 } 765 } 766 } 767 return retVal; 768 } 769 770 public static IBase getReferenceInBundle( 771 @Nonnull FhirContext theFhirContext, @Nonnull String theUrl, @Nullable Object theAppContext) { 772 if (!(theAppContext instanceof IBaseBundle) || isBlank(theUrl) || theUrl.startsWith("#")) { 773 return null; 774 } 775 776 /* 777 * If this is a reference that is a UUID, we must be looking for local references within a Bundle 778 */ 779 IBaseBundle bundle = (IBaseBundle) theAppContext; 780 781 final boolean isPlaceholderReference = theUrl.startsWith("urn:"); 782 final String unqualifiedVersionlessReference = 783 new IdDt(theUrl).toUnqualifiedVersionless().getValue(); 784 785 for (BundleEntryParts next : BundleUtil.toListOfEntries(theFhirContext, bundle)) { 786 IBaseResource nextResource = next.getResource(); 787 if (nextResource == null) { 788 continue; 789 } 790 if (isPlaceholderReference) { 791 if (theUrl.equals(next.getFullUrl()) 792 || theUrl.equals(nextResource.getIdElement().getValue())) { 793 return nextResource; 794 } 795 } else { 796 if (unqualifiedVersionlessReference.equals( 797 nextResource.getIdElement().toUnqualifiedVersionless().getValue())) { 798 return nextResource; 799 } 800 } 801 } 802 return null; 803 } 804 805 /** 806 * DSTU3 did not allow the PATCH verb for transaction bundles- so instead we infer that a bundle 807 * is a patch if the payload is a binary resource containing a patch. This method 808 * tests whether a resource (which should have come from 809 * <code>Bundle.entry.resource</code> is a Binary resource with a patch 810 * payload type. 811 */ 812 public static boolean isDstu3TransactionPatch(FhirContext theContext, IBaseResource thePayloadResource) { 813 boolean isPatch = false; 814 if (thePayloadResource instanceof IBaseBinary) { 815 String contentType = ((IBaseBinary) thePayloadResource).getContentType(); 816 try { 817 PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(theContext, contentType); 818 isPatch = true; 819 } catch (InvalidRequestException e) { 820 // ignore 821 } 822 } 823 return isPatch; 824 } 825 826 /** 827 * create a new bundle entry and set a value for a single field 828 * @param theContext Context holding resource definition 829 * @param theFieldName Child field name of the bundle entry to set 830 * @param theValues The values to set on the bundle entry child field name 831 * @return the new bundle entry 832 */ 833 public static IBase createNewBundleEntryWithSingleField( 834 FhirContext theContext, String theFieldName, IBase... theValues) { 835 IBaseBundle newBundle = TerserUtil.newResource(theContext, "Bundle"); 836 BaseRuntimeChildDefinition entryChildDef = 837 theContext.getResourceDefinition(newBundle).getChildByName("entry"); 838 839 BaseRuntimeElementCompositeDefinition<?> entryChildElem = 840 (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry"); 841 BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName(theFieldName); 842 IBase bundleEntry = entryChildElem.newInstance(); 843 for (IBase value : theValues) { 844 try { 845 resourceChild.getMutator().addValue(bundleEntry, value); 846 } catch (UnsupportedOperationException e) { 847 ourLog.warn( 848 "Resource {} does not support multiple values, but an attempt to set {} was made. Setting the first item only", 849 bundleEntry, 850 theValues); 851 resourceChild.getMutator().setValue(bundleEntry, value); 852 break; 853 } 854 } 855 return bundleEntry; 856 } 857 858 /** 859 * Get resource from bundle by resource type and reference 860 * @param theContext FhirContext 861 * @param theBundle IBaseBundle 862 * @param theReference IBaseReference 863 * @return IBaseResource if found and null if not found. 864 */ 865 @Nonnull 866 public static IBaseResource getResourceByReferenceAndResourceType( 867 @Nonnull FhirContext theContext, @Nonnull IBaseBundle theBundle, @Nonnull IBaseReference theReference) { 868 return toListOfResources(theContext, theBundle).stream() 869 .filter(theResource -> theReference 870 .getReferenceElement() 871 .getIdPart() 872 .equals(theResource.getIdElement().getIdPart())) 873 .findFirst() 874 .orElse(null); 875 } 876 877 private static class SortLegality { 878 private boolean myIsLegal; 879 880 SortLegality() { 881 this.myIsLegal = true; 882 } 883 884 private void setLegal(boolean theLegal) { 885 myIsLegal = theLegal; 886 } 887 888 public boolean isLegal() { 889 return myIsLegal; 890 } 891 } 892}