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