
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.BaseRuntimeElementDefinition; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.context.FhirVersionEnum; 026import ca.uhn.fhir.context.RuntimeResourceDefinition; 027import ca.uhn.fhir.model.primitive.IdDt; 028import jakarta.annotation.Nonnull; 029import jakarta.annotation.Nullable; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.lang3.Validate; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.hl7.fhir.instance.model.api.IBaseBackboneElement; 034import org.hl7.fhir.instance.model.api.IBaseBundle; 035import org.hl7.fhir.instance.model.api.IBaseParameters; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037import org.hl7.fhir.instance.model.api.IIdType; 038import org.hl7.fhir.instance.model.api.IPrimitiveType; 039 040import java.util.Date; 041import java.util.Objects; 042import java.util.Optional; 043 044/** 045 * This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide 046 * support for setting various bundle fields and working with bundle parts such as metadata and entry 047 * (method and search). 048 * 049 * <p> 050 * <p> 051 * This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API 052 * may change. 053 * 054 * @since 5.1.0 055 */ 056public class BundleBuilder { 057 058 private final FhirContext myContext; 059 private final IBaseBundle myBundle; 060 private final RuntimeResourceDefinition myBundleDef; 061 private final BaseRuntimeChildDefinition myEntryChild; 062 private final BaseRuntimeChildDefinition myMetaChild; 063 private final BaseRuntimeChildDefinition mySearchChild; 064 private final BaseRuntimeElementDefinition<?> myEntryDef; 065 private final BaseRuntimeElementDefinition<?> myMetaDef; 066 private final BaseRuntimeElementDefinition mySearchDef; 067 private final BaseRuntimeChildDefinition myEntryResourceChild; 068 private final BaseRuntimeChildDefinition myEntryFullUrlChild; 069 private final BaseRuntimeChildDefinition myEntryRequestChild; 070 private final BaseRuntimeElementDefinition<?> myEntryRequestDef; 071 private final BaseRuntimeChildDefinition myEntryRequestUrlChild; 072 private final BaseRuntimeChildDefinition myEntryRequestMethodChild; 073 private final BaseRuntimeElementDefinition<?> myEntryRequestMethodDef; 074 private final BaseRuntimeChildDefinition myEntryRequestIfNoneExistChild; 075 076 /** 077 * Constructor 078 * 079 * @param theContext The FHIR context appropriate for the Bundle type. 080 */ 081 public BundleBuilder(@Nonnull FhirContext theContext) { 082 this(theContext, (IBaseBundle) 083 theContext.getResourceDefinition("Bundle").newInstance()); 084 } 085 086 /** 087 * Constructor which accepts an existing Bundle. Any existing entries in the Bundle 088 * will be preserved, new entries may be added. 089 * 090 * @param theContext The FHIR context appropriate for the Bundle type. 091 * @param theBundle The Bundle to add to. 092 * @since 8.6.0 093 */ 094 public BundleBuilder(@Nonnull FhirContext theContext, @Nonnull IBaseBundle theBundle) { 095 myBundle = theBundle; 096 myContext = theContext; 097 098 myBundleDef = myContext.getResourceDefinition("Bundle"); 099 100 myEntryChild = myBundleDef.getChildByName("entry"); 101 myEntryDef = myEntryChild.getChildByName("entry"); 102 103 mySearchChild = myEntryDef.getChildByName("search"); 104 mySearchDef = mySearchChild.getChildByName("search"); 105 106 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 107 myMetaChild = myBundleDef.getChildByName("meta"); 108 myMetaDef = myMetaChild.getChildByName("meta"); 109 } else { 110 myMetaChild = null; 111 myMetaDef = null; 112 } 113 114 myEntryResourceChild = myEntryDef.getChildByName("resource"); 115 myEntryFullUrlChild = myEntryDef.getChildByName("fullUrl"); 116 117 myEntryRequestChild = myEntryDef.getChildByName("request"); 118 myEntryRequestDef = myEntryRequestChild.getChildByName("request"); 119 120 myEntryRequestUrlChild = myEntryRequestDef.getChildByName("url"); 121 122 myEntryRequestMethodChild = myEntryRequestDef.getChildByName("method"); 123 myEntryRequestMethodDef = myEntryRequestMethodChild.getChildByName("method"); 124 125 myEntryRequestIfNoneExistChild = myEntryRequestDef.getChildByName("ifNoneExist"); 126 } 127 128 /** 129 * Sets the specified primitive field on the bundle with the value provided. 130 * 131 * @param theFieldName Name of the primitive field. 132 * @param theFieldValue Value of the field to be set. 133 */ 134 public BundleBuilder setBundleField(String theFieldName, String theFieldValue) { 135 BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName(theFieldName); 136 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 137 138 IPrimitiveType<?> type = (IPrimitiveType<?>) 139 typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments()); 140 type.setValueAsString(theFieldValue); 141 typeChild.getMutator().setValue(myBundle, type); 142 return this; 143 } 144 145 private void setBundleFieldIfNotAlreadySet(String theFieldName, String theFieldValue) { 146 BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName(theFieldName); 147 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 148 Optional<IBase> firstValue = typeChild.getAccessor().getFirstValueOrNull(myBundle); 149 if (firstValue.isPresent()) { 150 IPrimitiveType<?> value = (IPrimitiveType<?>) firstValue.get(); 151 if (!value.isEmpty()) { 152 return; 153 } 154 } 155 156 setBundleField(theFieldName, theFieldValue); 157 } 158 159 /** 160 * Sets the specified primitive field on the search entry with the value provided. 161 * 162 * @param theSearch Search part of the entry 163 * @param theFieldName Name of the primitive field. 164 * @param theFieldValue Value of the field to be set. 165 */ 166 public BundleBuilder setSearchField(IBase theSearch, String theFieldName, String theFieldValue) { 167 BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName); 168 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 169 170 IPrimitiveType<?> type = (IPrimitiveType<?>) 171 typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments()); 172 type.setValueAsString(theFieldValue); 173 typeChild.getMutator().setValue(theSearch, type); 174 return this; 175 } 176 177 public BundleBuilder setSearchField(IBase theSearch, String theFieldName, IPrimitiveType<?> theFieldValue) { 178 BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName); 179 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 180 181 typeChild.getMutator().setValue(theSearch, theFieldValue); 182 return this; 183 } 184 185 /** 186 * Adds a FHIRPatch patch bundle to the transaction 187 * 188 * @param theTarget The target resource ID to patch 189 * @param thePatch The FHIRPath Parameters resource 190 * @since 6.3.0 191 */ 192 public PatchBuilder addTransactionFhirPatchEntry(IIdType theTarget, IBaseParameters thePatch) { 193 Validate.notNull(theTarget, "theTarget must not be null"); 194 Validate.notBlank(theTarget.getResourceType(), "theTarget must contain a resource type"); 195 Validate.notBlank(theTarget.getIdPart(), "theTarget must contain an ID"); 196 197 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest( 198 thePatch, 199 theTarget.getValue(), 200 theTarget.toUnqualifiedVersionless().getValue(), 201 "PATCH"); 202 203 return new PatchBuilder(url); 204 } 205 206 /** 207 * Adds a FHIRPatch patch bundle to the transaction. This method is intended for conditional PATCH operations. If you 208 * know the ID of the resource you wish to patch, use {@link #addTransactionFhirPatchEntry(IIdType, IBaseParameters)} 209 * instead. 210 * 211 * @param thePatch The FHIRPath Parameters resource 212 * @see #addTransactionFhirPatchEntry(IIdType, IBaseParameters) 213 * @since 6.3.0 214 */ 215 public PatchBuilder addTransactionFhirPatchEntry(IBaseParameters thePatch) { 216 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(thePatch, null, null, "PATCH"); 217 218 return new PatchBuilder(url); 219 } 220 221 /** 222 * Adds an entry containing an update (PUT) request. 223 * Also sets the Bundle.type value to "transaction" if it is not already set. 224 * 225 * @param theResource The resource to update 226 */ 227 public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) { 228 return addTransactionUpdateEntry(theResource, null); 229 } 230 231 /** 232 * Adds an entry containing an update (PUT) request. 233 * Also sets the Bundle.type value to "transaction" if it is not already set. 234 * 235 * @param theResource The resource to update 236 * @param theRequestUrl The url to attach to the Bundle.entry.request.url. If null, will default to the resource ID. 237 */ 238 public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource, String theRequestUrl) { 239 String fullUrl = null; 240 return addTransactionUpdateEntry(theResource, theRequestUrl, fullUrl); 241 } 242 243 /** 244 * Adds an entry containing an update (PUT) request. 245 * Also sets the Bundle.type value to "transaction" if it is not already set. 246 * 247 * @param theResource The resource to update 248 * @param theRequestUrl The url to attach to the Bundle.entry.request.url. If null, will default to the resource ID. 249 * @param theFullUrl The fullUrl to attach to the entry in {@literal Bundle.entry.fullUrl}. If null, will default to the resource ID. 250 * @since 8.6.0 251 */ 252 @Nonnull 253 public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource, String theRequestUrl, String theFullUrl) { 254 Validate.notNull(theResource, "theResource must not be null"); 255 256 IIdType id = getIdTypeForUpdate(theResource); 257 if (theFullUrl == null) { 258 theFullUrl = id.toVersionless().getValue(); 259 } 260 261 String verb = "PUT"; 262 String requestUrl = StringUtils.isBlank(theRequestUrl) 263 ? id.toUnqualifiedVersionless().getValue() 264 : theRequestUrl; 265 266 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(theResource, theFullUrl, requestUrl, verb); 267 268 return new UpdateBuilder(url); 269 } 270 271 @Nonnull 272 private IPrimitiveType<?> addAndPopulateTransactionBundleEntryRequest( 273 IBaseResource theResource, String theFullUrl, String theRequestUrl, String theHttpVerb) { 274 setBundleFieldIfNotAlreadySet("type", "transaction"); 275 276 IBase request = addEntryAndReturnRequest(theResource, theFullUrl); 277 278 // Bundle.entry.request.url 279 IPrimitiveType<?> url = addRequestUrl(request, theRequestUrl); 280 281 // Bundle.entry.request.method 282 addRequestMethod(request, theHttpVerb); 283 return url; 284 } 285 286 /** 287 * Adds an entry containing an update (UPDATE) request without the body of the resource. 288 * Also sets the Bundle.type value to "transaction" if it is not already set. 289 * 290 * @param theResource The resource to update. 291 */ 292 public void addTransactionUpdateIdOnlyEntry(IBaseResource theResource) { 293 setBundleFieldIfNotAlreadySet("type", "transaction"); 294 295 Validate.notNull(theResource, "theResource must not be null"); 296 297 IIdType id = getIdTypeForUpdate(theResource); 298 String requestUrl = id.toUnqualifiedVersionless().getValue(); 299 String fullUrl = id.toVersionless().getValue(); 300 String httpMethod = "PUT"; 301 302 addIdOnlyEntry(requestUrl, httpMethod, fullUrl); 303 } 304 305 /** 306 * Adds an entry containing an create (POST) request. 307 * Also sets the Bundle.type value to "transaction" if it is not already set. 308 * 309 * @param theResource The resource to create 310 */ 311 public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) { 312 return addTransactionCreateEntry(theResource, null); 313 } 314 315 /** 316 * Adds an entry containing an create (POST) request. 317 * Also sets the Bundle.type value to "transaction" if it is not already set. 318 * 319 * @param theResource The resource to create 320 * @param theFullUrl The fullUrl to attach to the entry. If null, will default to the resource ID. 321 */ 322 public CreateBuilder addTransactionCreateEntry(IBaseResource theResource, @Nullable String theFullUrl) { 323 setBundleFieldIfNotAlreadySet("type", "transaction"); 324 325 IBase request = addEntryAndReturnRequest( 326 theResource, 327 theFullUrl != null ? theFullUrl : theResource.getIdElement().getValue()); 328 329 String resourceType = myContext.getResourceType(theResource); 330 331 // Bundle.entry.request.url 332 addRequestUrl(request, resourceType); 333 334 // Bundle.entry.request.method 335 addRequestMethod(request, "POST"); 336 337 return new CreateBuilder(request); 338 } 339 340 /** 341 * Adds an entry containing a create (POST) request without the body of the resource. 342 * Also sets the Bundle.type value to "transaction" if it is not already set. 343 * 344 * @param theResource The resource to create 345 */ 346 public void addTransactionCreateEntryIdOnly(IBaseResource theResource) { 347 setBundleFieldIfNotAlreadySet("type", "transaction"); 348 349 String requestUrl = myContext.getResourceType(theResource); 350 String fullUrl = theResource.getIdElement().getValue(); 351 String httpMethod = "POST"; 352 353 addIdOnlyEntry(requestUrl, httpMethod, fullUrl); 354 } 355 356 private void addIdOnlyEntry(String theRequestUrl, String theHttpMethod, String theFullUrl) { 357 IBase entry = addEntry(); 358 359 // Bundle.entry.request 360 IBase request = myEntryRequestDef.newInstance(); 361 myEntryRequestChild.getMutator().setValue(entry, request); 362 363 // Bundle.entry.request.url 364 addRequestUrl(request, theRequestUrl); 365 366 // Bundle.entry.request.method 367 addRequestMethod(request, theHttpMethod); 368 369 // Bundle.entry.fullUrl 370 addFullUrl(entry, theFullUrl); 371 } 372 373 /** 374 * Adds an entry containing a delete (DELETE) request. 375 * Also sets the Bundle.type value to "transaction" if it is not already set. 376 * <p> 377 * Note that the resource is only used to extract its ID and type, and the body of the resource is not included in the entry, 378 * 379 * @param theCondition The conditional URL, e.g. "Patient?identifier=foo|bar" 380 * @since 6.8.0 381 */ 382 public DeleteBuilder addTransactionDeleteConditionalEntry(String theCondition) { 383 Validate.notBlank(theCondition, "theCondition must not be blank"); 384 385 setBundleFieldIfNotAlreadySet("type", "transaction"); 386 return addTransactionDeleteEntry(theCondition); 387 } 388 389 /** 390 * Adds an entry containing a delete (DELETE) request. 391 * Also sets the Bundle.type value to "transaction" if it is not already set. 392 * <p> 393 * Note that the resource is only used to extract its ID and type, and the body of the resource is not included in the entry, 394 * 395 * @param theResource The resource to delete. 396 */ 397 public DeleteBuilder addTransactionDeleteEntry(IBaseResource theResource) { 398 String resourceType = myContext.getResourceType(theResource); 399 String idPart = theResource.getIdElement().toUnqualifiedVersionless().getIdPart(); 400 return addTransactionDeleteEntry(resourceType, idPart); 401 } 402 403 /** 404 * Adds an entry containing a delete (DELETE) request. 405 * Also sets the Bundle.type value to "transaction" if it is not already set. 406 * <p> 407 * Note that the resource is only used to extract its ID and type, and the body of the resource is not included in the entry, 408 * 409 * @param theResourceId The resource ID to delete. 410 * @return 411 */ 412 public DeleteBuilder addTransactionDeleteEntry(IIdType theResourceId) { 413 String resourceType = theResourceId.getResourceType(); 414 String idPart = theResourceId.getIdPart(); 415 return addTransactionDeleteEntry(resourceType, idPart); 416 } 417 418 /** 419 * Adds an entry containing a delete (DELETE) request. 420 * Also sets the Bundle.type value to "transaction" if it is not already set. 421 * 422 * @param theResourceType The type resource to delete. 423 * @param theIdPart the ID of the resource to delete. 424 */ 425 public DeleteBuilder addTransactionDeleteEntry(String theResourceType, String theIdPart) { 426 setBundleFieldIfNotAlreadySet("type", "transaction"); 427 IdDt idDt = new IdDt(theIdPart); 428 429 String deleteUrl = idDt.toUnqualifiedVersionless() 430 .withResourceType(theResourceType) 431 .getValue(); 432 433 return addTransactionDeleteEntry(deleteUrl); 434 } 435 436 /** 437 * Adds an entry containing a delete (DELETE) request. 438 * Also sets the Bundle.type value to "transaction" if it is not already set. 439 * 440 * @param theMatchUrl The match URL, e.g. <code>Patient?identifier=http://foo|123</code> 441 * @since 6.3.0 442 */ 443 public BaseOperationBuilder addTransactionDeleteEntryConditional(String theMatchUrl) { 444 Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); 445 return addTransactionDeleteEntry(theMatchUrl); 446 } 447 448 /** 449 * Adds a DELETE entry using only a conditional URL 450 * 451 * @since 8.6.0 452 */ 453 @Nonnull 454 public DeleteBuilder addTransactionDeleteEntry(String theDeleteUrl) { 455 Validate.notBlank(theDeleteUrl, "theDeleteUrl must not be null or blank"); 456 IBase request = addEntryAndReturnRequest(); 457 458 // Bundle.entry.request.url 459 addRequestUrl(request, theDeleteUrl); 460 461 // Bundle.entry.request.method 462 addRequestMethod(request, "DELETE"); 463 464 return new DeleteBuilder(); 465 } 466 467 private IIdType getIdTypeForUpdate(IBaseResource theResource) { 468 IIdType id = theResource.getIdElement(); 469 if (id.hasIdPart() && !id.hasResourceType()) { 470 String resourceType = myContext.getResourceType(theResource); 471 id = id.withResourceType(resourceType); 472 } 473 return id; 474 } 475 476 public void addFullUrl(IBase theEntry, String theFullUrl) { 477 IPrimitiveType<?> fullUrl = 478 (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 479 fullUrl.setValueAsString(theFullUrl); 480 myEntryFullUrlChild.getMutator().setValue(theEntry, fullUrl); 481 } 482 483 private IPrimitiveType<?> addRequestUrl(IBase request, String theRequestUrl) { 484 IPrimitiveType<?> url = 485 (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 486 url.setValueAsString(theRequestUrl); 487 myEntryRequestUrlChild.getMutator().setValue(request, url); 488 return url; 489 } 490 491 private void addRequestMethod(IBase theRequest, String theMethod) { 492 IPrimitiveType<?> method = (IPrimitiveType<?>) 493 myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); 494 method.setValueAsString(theMethod); 495 myEntryRequestMethodChild.getMutator().setValue(theRequest, method); 496 } 497 498 /** 499 * Adds an entry for a Collection bundle type 500 */ 501 public void addCollectionEntry(IBaseResource theResource) { 502 setType("collection"); 503 addEntryAndReturnRequest(theResource); 504 } 505 506 /** 507 * Adds an entry for a Document bundle type 508 */ 509 public void addDocumentEntry(IBaseResource theResource) { 510 setType("document"); 511 addEntryAndReturnRequest(theResource); 512 } 513 514 public void addDocumentEntry(IBaseResource theResource, String theFullUrl) { 515 setType("document"); 516 addEntryAndReturnRequest(theResource, theFullUrl); 517 } 518 519 /** 520 * Adds an entry for a Message bundle type 521 * 522 * @since 8.4.0 523 */ 524 public void addMessageEntry(IBaseResource theResource) { 525 setType("message"); 526 addEntryAndReturnRequest(theResource); 527 } 528 529 /** 530 * Creates new entry and adds it to the bundle 531 * 532 * @return Returns the new entry. 533 */ 534 public IBase addEntry() { 535 return addEntry(myEntryDef.newInstance()); 536 } 537 538 /** 539 * Add an entry to the bundle. 540 * 541 * @param theEntry the entry to add to the bundle. 542 * @return theEntry 543 */ 544 public IBase addEntry(IBase theEntry) { 545 myEntryChild.getMutator().addValue(myBundle, theEntry); 546 return theEntry; 547 } 548 549 /** 550 * Add an entry to the bundle. 551 * 552 * @param theEntry the canonical entry to add to the bundle. It will be converted to a FHIR version specific entry before adding. 553 * @return 554 */ 555 public IBase addEntry(CanonicalBundleEntry theEntry) { 556 IBase bundleEntry = theEntry.toBundleEntry(myContext, myEntryDef.getImplementingClass()); 557 addEntry(bundleEntry); 558 return bundleEntry; 559 } 560 561 /** 562 * Creates new search instance for the specified entry. 563 * Note that this method does not work for DSTU2 model classes, it will only work 564 * on DSTU3+. 565 * 566 * @param entry Entry to create search instance for 567 * @return Returns the search instance 568 */ 569 public IBaseBackboneElement addSearch(IBase entry) { 570 Validate.isTrue( 571 myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), 572 "This method may only be called for FHIR version DSTU3 and above"); 573 574 IBase searchInstance = mySearchDef.newInstance(); 575 mySearchChild.getMutator().setValue(entry, searchInstance); 576 return (IBaseBackboneElement) searchInstance; 577 } 578 579 private IBase addEntryAndReturnRequest(IBaseResource theResource) { 580 IIdType id = theResource.getIdElement(); 581 if (id.hasVersionIdPart()) { 582 id = id.toVersionless(); 583 } 584 return addEntryAndReturnRequest(theResource, id.getValue()); 585 } 586 587 private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) { 588 Validate.notNull(theResource, "theResource must not be null"); 589 590 IBase entry = addEntry(); 591 592 // Bundle.entry.fullUrl 593 addFullUrl(entry, theFullUrl); 594 595 // Bundle.entry.resource 596 myEntryResourceChild.getMutator().setValue(entry, theResource); 597 598 // Bundle.entry.request 599 IBase request = myEntryRequestDef.newInstance(); 600 myEntryRequestChild.getMutator().setValue(entry, request); 601 return request; 602 } 603 604 public IBase addEntryAndReturnRequest() { 605 IBase entry = addEntry(); 606 607 // Bundle.entry.request 608 IBase request = myEntryRequestDef.newInstance(); 609 myEntryRequestChild.getMutator().setValue(entry, request); 610 return request; 611 } 612 613 public IBaseBundle getBundle() { 614 return myBundle; 615 } 616 617 /** 618 * Convenience method which auto-casts the results of {@link #getBundle()} 619 * 620 * @since 6.3.0 621 */ 622 public <T extends IBaseBundle> T getBundleTyped() { 623 return (T) myBundle; 624 } 625 626 /** 627 * Note that this method does not work for DSTU2 model classes, it will only work 628 * on DSTU3+. 629 */ 630 public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) { 631 Validate.isTrue( 632 myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), 633 "This method may only be called for FHIR version DSTU3 and above"); 634 635 BaseRuntimeChildDefinition.IMutator mutator = 636 myMetaDef.getChildByName(theFieldName).getMutator(); 637 mutator.setValue(myBundle.getMeta(), theFieldValue); 638 return this; 639 } 640 641 /** 642 * Sets the specified entry field. 643 * 644 * @param theEntry The entry instance to set values on 645 * @param theEntryChildName The child field name of the entry instance to be set 646 * @param theValue The field value to set 647 */ 648 public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) { 649 addToBase(theEntry, theEntryChildName, theValue, myEntryDef); 650 } 651 652 /** 653 * Sets the specified search field. 654 * 655 * @param theSearch The search instance to set values on 656 * @param theSearchFieldName The child field name of the search instance to be set 657 * @param theSearchFieldValue The field value to set 658 */ 659 public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) { 660 addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef); 661 } 662 663 private void addToBase( 664 IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) { 665 BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName); 666 Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase); 667 defn.getMutator().addValue(theBase, theValue); 668 } 669 670 /** 671 * Creates a new primitive. 672 * 673 * @param theTypeName The element type for the primitive 674 * @param <T> Actual type of the parameterized primitive type interface 675 * @return Returns the new empty instance of the element definition. 676 */ 677 public <T> IPrimitiveType<T> newPrimitive(String theTypeName) { 678 BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName); 679 Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName); 680 return (IPrimitiveType<T>) primitiveDefinition.newInstance(); 681 } 682 683 /** 684 * Creates a new primitive instance of the specified element type. 685 * 686 * @param theTypeName Element type to create 687 * @param theInitialValue Initial value to be set on the new instance 688 * @param <T> Actual type of the parameterized primitive type interface 689 * @return Returns the newly created instance 690 */ 691 public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) { 692 IPrimitiveType<T> retVal = newPrimitive(theTypeName); 693 retVal.setValue(theInitialValue); 694 return retVal; 695 } 696 697 /** 698 * Sets a value for <code>Bundle.type</code>. That this is a coded field so {@literal theType} 699 * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException} 700 * will be thrown. 701 */ 702 public void setType(String theType) { 703 setBundleField("type", theType); 704 } 705 706 /** 707 * Adds an identifier to <code>Bundle.identifier</code> 708 * 709 * @param theSystem The system 710 * @param theValue The value 711 * @since 6.4.0 712 */ 713 public void setIdentifier(@Nullable String theSystem, @Nullable String theValue) { 714 FhirTerser terser = myContext.newTerser(); 715 IBase identifier = terser.addElement(myBundle, "identifier"); 716 terser.setElement(identifier, "system", theSystem); 717 terser.setElement(identifier, "value", theValue); 718 } 719 720 /** 721 * Sets the timestamp in <code>Bundle.timestamp</code> 722 * 723 * @since 6.4.0 724 */ 725 public void setTimestamp(@Nonnull IPrimitiveType<Date> theTimestamp) { 726 FhirTerser terser = myContext.newTerser(); 727 terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString()); 728 } 729 730 /** 731 * Adds a profile URL to <code>Bundle.meta.profile</code> 732 * 733 * @since 7.4.0 734 */ 735 public void addProfile(String theProfile) { 736 FhirTerser terser = myContext.newTerser(); 737 terser.addElement(myBundle, "Bundle.meta.profile", theProfile); 738 } 739 740 public IBase addSearchMatchEntry(IBaseResource theResource) { 741 setType("searchset"); 742 743 IBase entry = addEntry(); 744 // Bundle.entry.resource 745 myEntryResourceChild.getMutator().setValue(entry, theResource); 746 // Bundle.entry.search 747 IBase search = addSearch(entry); 748 setSearchField(search, "mode", "match"); 749 750 return entry; 751 } 752 753 public class DeleteBuilder extends BaseOperationBuilder { 754 755 // nothing yet 756 757 } 758 759 public class PatchBuilder extends BaseOperationBuilderWithConditionalUrl<PatchBuilder> { 760 761 PatchBuilder(IPrimitiveType<?> theUrl) { 762 super(theUrl); 763 } 764 } 765 766 public class UpdateBuilder extends BaseOperationBuilderWithConditionalUrl<UpdateBuilder> { 767 UpdateBuilder(IPrimitiveType<?> theUrl) { 768 super(theUrl); 769 } 770 } 771 772 public class CreateBuilder extends BaseOperationBuilder { 773 private final IBase myRequest; 774 775 CreateBuilder(IBase theRequest) { 776 myRequest = theRequest; 777 } 778 779 /** 780 * Make this create a Conditional Create 781 */ 782 public CreateBuilder conditional(String theConditionalUrl) { 783 BaseRuntimeElementDefinition<?> stringDefinition = 784 Objects.requireNonNull(myContext.getElementDefinition("string")); 785 IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance(); 786 ifNoneExist.setValueAsString(theConditionalUrl); 787 788 myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist); 789 790 return this; 791 } 792 } 793 794 public abstract class BaseOperationBuilder { 795 796 /** 797 * Returns a reference to the BundleBuilder instance. 798 * <p> 799 * Calling this method has no effect at all, it is only 800 * provided for easy method chaning if you want to build 801 * your bundle as a single fluent call. 802 * 803 * @since 6.3.0 804 */ 805 public BundleBuilder andThen() { 806 return BundleBuilder.this; 807 } 808 } 809 810 public abstract class BaseOperationBuilderWithConditionalUrl<T extends BaseOperationBuilder> 811 extends BaseOperationBuilder { 812 813 private final IPrimitiveType<?> myUrl; 814 815 BaseOperationBuilderWithConditionalUrl(IPrimitiveType<?> theUrl) { 816 myUrl = theUrl; 817 } 818 819 /** 820 * Make this update a Conditional Update 821 */ 822 @SuppressWarnings("unchecked") 823 public T conditional(String theConditionalUrl) { 824 myUrl.setValueAsString(theConditionalUrl); 825 return (T) this; 826 } 827 } 828}