001/*- 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2023 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 org.apache.commons.lang3.Validate; 029import org.hl7.fhir.instance.model.api.IBase; 030import org.hl7.fhir.instance.model.api.IBaseBackboneElement; 031import org.hl7.fhir.instance.model.api.IBaseBundle; 032import org.hl7.fhir.instance.model.api.IBaseParameters; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034import org.hl7.fhir.instance.model.api.IIdType; 035import org.hl7.fhir.instance.model.api.IPrimitiveType; 036 037import javax.annotation.Nonnull; 038import javax.annotation.Nullable; 039import java.util.Date; 040import java.util.Objects; 041 042/** 043 * This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide 044 * support for setting various bundle fields and working with bundle parts such as metadata and entry 045 * (method and search). 046 * 047 * <p> 048 * <p> 049 * This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API 050 * may change. 051 * 052 * @since 5.1.0 053 */ 054public class BundleBuilder { 055 056 private final FhirContext myContext; 057 private final IBaseBundle myBundle; 058 private final RuntimeResourceDefinition myBundleDef; 059 private final BaseRuntimeChildDefinition myEntryChild; 060 private final BaseRuntimeChildDefinition myMetaChild; 061 private final BaseRuntimeChildDefinition mySearchChild; 062 private final BaseRuntimeElementDefinition<?> myEntryDef; 063 private final BaseRuntimeElementDefinition<?> myMetaDef; 064 private final BaseRuntimeElementDefinition mySearchDef; 065 private final BaseRuntimeChildDefinition myEntryResourceChild; 066 private final BaseRuntimeChildDefinition myEntryFullUrlChild; 067 private final BaseRuntimeChildDefinition myEntryRequestChild; 068 private final BaseRuntimeElementDefinition<?> myEntryRequestDef; 069 private final BaseRuntimeChildDefinition myEntryRequestUrlChild; 070 private final BaseRuntimeChildDefinition myEntryRequestMethodChild; 071 private final BaseRuntimeElementDefinition<?> myEntryRequestMethodDef; 072 private final BaseRuntimeChildDefinition myEntryRequestIfNoneExistChild; 073 074 /** 075 * Constructor 076 */ 077 public BundleBuilder(FhirContext theContext) { 078 myContext = theContext; 079 080 myBundleDef = myContext.getResourceDefinition("Bundle"); 081 myBundle = (IBaseBundle) myBundleDef.newInstance(); 082 083 myEntryChild = myBundleDef.getChildByName("entry"); 084 myEntryDef = myEntryChild.getChildByName("entry"); 085 086 mySearchChild = myEntryDef.getChildByName("search"); 087 mySearchDef = mySearchChild.getChildByName("search"); 088 089 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 090 myMetaChild = myBundleDef.getChildByName("meta"); 091 myMetaDef = myMetaChild.getChildByName("meta"); 092 } else { 093 myMetaChild = null; 094 myMetaDef = null; 095 } 096 097 myEntryResourceChild = myEntryDef.getChildByName("resource"); 098 myEntryFullUrlChild = myEntryDef.getChildByName("fullUrl"); 099 100 myEntryRequestChild = myEntryDef.getChildByName("request"); 101 myEntryRequestDef = myEntryRequestChild.getChildByName("request"); 102 103 myEntryRequestUrlChild = myEntryRequestDef.getChildByName("url"); 104 105 myEntryRequestMethodChild = myEntryRequestDef.getChildByName("method"); 106 myEntryRequestMethodDef = myEntryRequestMethodChild.getChildByName("method"); 107 108 myEntryRequestIfNoneExistChild = myEntryRequestDef.getChildByName("ifNoneExist"); 109 } 110 111 /** 112 * Sets the specified primitive field on the bundle with the value provided. 113 * 114 * @param theFieldName Name of the primitive field. 115 * @param theFieldValue Value of the field to be set. 116 */ 117 public BundleBuilder setBundleField(String theFieldName, String theFieldValue) { 118 BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName(theFieldName); 119 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 120 121 IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments()); 122 type.setValueAsString(theFieldValue); 123 typeChild.getMutator().setValue(myBundle, type); 124 return this; 125 } 126 127 /** 128 * Sets the specified primitive field on the search entry with the value provided. 129 * 130 * @param theSearch Search part of the entry 131 * @param theFieldName Name of the primitive field. 132 * @param theFieldValue Value of the field to be set. 133 */ 134 public BundleBuilder setSearchField(IBase theSearch, String theFieldName, String theFieldValue) { 135 BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName); 136 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 137 138 IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments()); 139 type.setValueAsString(theFieldValue); 140 typeChild.getMutator().setValue(theSearch, type); 141 return this; 142 } 143 144 public BundleBuilder setSearchField(IBase theSearch, String theFieldName, IPrimitiveType<?> theFieldValue) { 145 BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName); 146 Validate.notNull(typeChild, "Unable to find field %s", theFieldName); 147 148 typeChild.getMutator().setValue(theSearch, theFieldValue); 149 return this; 150 } 151 152 /** 153 * Adds a FHIRPatch patch bundle to the transaction 154 * 155 * @param theTarget The target resource ID to patch 156 * @param thePatch The FHIRPath Parameters resource 157 * @since 6.3.0 158 */ 159 public PatchBuilder addTransactionFhirPatchEntry(IIdType theTarget, IBaseParameters thePatch) { 160 Validate.notNull(theTarget, "theTarget must not be null"); 161 Validate.notBlank(theTarget.getResourceType(), "theTarget must contain a resource type"); 162 Validate.notBlank(theTarget.getIdPart(), "theTarget must contain an ID"); 163 164 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(thePatch, theTarget.getValue(), theTarget.toUnqualifiedVersionless().getValue(), "PATCH"); 165 166 return new PatchBuilder(url); 167 } 168 169 /** 170 * Adds a FHIRPatch patch bundle to the transaction. This method is intended for conditional PATCH operations. If you 171 * know the ID of the resource you wish to patch, use {@link #addTransactionFhirPatchEntry(IIdType, IBaseParameters)} 172 * instead. 173 * 174 * @param thePatch The FHIRPath Parameters resource 175 * @see #addTransactionFhirPatchEntry(IIdType, IBaseParameters) 176 * @since 6.3.0 177 */ 178 public PatchBuilder addTransactionFhirPatchEntry(IBaseParameters thePatch) { 179 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(thePatch, null, null, "PATCH"); 180 181 return new PatchBuilder(url); 182 } 183 184 /** 185 * Adds an entry containing an update (PUT) request. 186 * Also sets the Bundle.type value to "transaction" if it is not already set. 187 * 188 * @param theResource The resource to update 189 */ 190 public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) { 191 Validate.notNull(theResource, "theResource must not be null"); 192 193 IIdType id = theResource.getIdElement(); 194 if (id.hasIdPart() && !id.hasResourceType()) { 195 String resourceType = myContext.getResourceType(theResource); 196 id = id.withResourceType(resourceType); 197 } 198 199 String requestUrl = id.toUnqualifiedVersionless().getValue(); 200 String fullUrl = id.getValue(); 201 String verb = "PUT"; 202 203 IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(theResource, fullUrl, requestUrl, verb); 204 205 return new UpdateBuilder(url); 206 } 207 208 @Nonnull 209 private IPrimitiveType<?> addAndPopulateTransactionBundleEntryRequest(IBaseResource theResource, String theFullUrl, String theRequestUrl, String theHttpVerb) { 210 setBundleField("type", "transaction"); 211 212 IBase request = addEntryAndReturnRequest(theResource, theFullUrl); 213 214 // Bundle.entry.request.url 215 IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 216 url.setValueAsString(theRequestUrl); 217 myEntryRequestUrlChild.getMutator().setValue(request, url); 218 219 // Bundle.entry.request.method 220 IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); 221 method.setValueAsString(theHttpVerb); 222 myEntryRequestMethodChild.getMutator().setValue(request, method); 223 return url; 224 } 225 226 /** 227 * Adds an entry containing an create (POST) request. 228 * Also sets the Bundle.type value to "transaction" if it is not already set. 229 * 230 * @param theResource The resource to create 231 */ 232 public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) { 233 setBundleField("type", "transaction"); 234 235 IBase request = addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); 236 237 String resourceType = myContext.getResourceType(theResource); 238 239 // Bundle.entry.request.url 240 IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 241 url.setValueAsString(resourceType); 242 myEntryRequestUrlChild.getMutator().setValue(request, url); 243 244 // Bundle.entry.request.url 245 IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); 246 method.setValueAsString("POST"); 247 myEntryRequestMethodChild.getMutator().setValue(request, method); 248 249 return new CreateBuilder(request); 250 } 251 252 /** 253 * Adds an entry containing a delete (DELETE) request. 254 * Also sets the Bundle.type value to "transaction" if it is not already set. 255 * <p> 256 * 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, 257 * 258 * @param theResource The resource to delete. 259 */ 260 public DeleteBuilder addTransactionDeleteEntry(IBaseResource theResource) { 261 String resourceType = myContext.getResourceType(theResource); 262 String idPart = theResource.getIdElement().toUnqualifiedVersionless().getIdPart(); 263 return addTransactionDeleteEntry(resourceType, idPart); 264 } 265 266 /** 267 * Adds an entry containing a delete (DELETE) request. 268 * Also sets the Bundle.type value to "transaction" if it is not already set. 269 * <p> 270 * 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, 271 * 272 * @param theResourceId The resource ID to delete. 273 * @return 274 */ 275 public DeleteBuilder addTransactionDeleteEntry(IIdType theResourceId) { 276 String resourceType = theResourceId.getResourceType(); 277 String idPart = theResourceId.getIdPart(); 278 return addTransactionDeleteEntry(resourceType, idPart); 279 } 280 281 /** 282 * Adds an entry containing a delete (DELETE) request. 283 * Also sets the Bundle.type value to "transaction" if it is not already set. 284 * 285 * @param theResourceType The type resource to delete. 286 * @param theIdPart the ID of the resource to delete. 287 */ 288 public DeleteBuilder addTransactionDeleteEntry(String theResourceType, String theIdPart) { 289 setBundleField("type", "transaction"); 290 IdDt idDt = new IdDt(theIdPart); 291 292 String deleteUrl = idDt.toUnqualifiedVersionless().withResourceType(theResourceType).getValue(); 293 294 return addDeleteEntry(deleteUrl); 295 } 296 297 /** 298 * Adds an entry containing a delete (DELETE) request. 299 * Also sets the Bundle.type value to "transaction" if it is not already set. 300 * 301 * @param theMatchUrl The match URL, e.g. <code>Patient?identifier=http://foo|123</code> 302 * @since 6.3.0 303 */ 304 public BaseOperationBuilder addTransactionDeleteEntryConditional(String theMatchUrl) { 305 Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank"); 306 return addDeleteEntry(theMatchUrl); 307 } 308 309 @Nonnull 310 private DeleteBuilder addDeleteEntry(String theDeleteUrl) { 311 IBase request = addEntryAndReturnRequest(); 312 313 // Bundle.entry.request.url 314 IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 315 url.setValueAsString(theDeleteUrl); 316 myEntryRequestUrlChild.getMutator().setValue(request, url); 317 318 // Bundle.entry.request.method 319 IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); 320 method.setValueAsString("DELETE"); 321 myEntryRequestMethodChild.getMutator().setValue(request, method); 322 323 return new DeleteBuilder(); 324 } 325 326 327 /** 328 * Adds an entry for a Collection bundle type 329 */ 330 public void addCollectionEntry(IBaseResource theResource) { 331 setType("collection"); 332 addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); 333 } 334 335 /** 336 * Adds an entry for a Document bundle type 337 */ 338 public void addDocumentEntry(IBaseResource theResource) { 339 setType("document"); 340 addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); 341 } 342 343 /** 344 * Creates new entry and adds it to the bundle 345 * 346 * @return Returns the new entry. 347 */ 348 public IBase addEntry() { 349 IBase entry = myEntryDef.newInstance(); 350 myEntryChild.getMutator().addValue(myBundle, entry); 351 return entry; 352 } 353 354 /** 355 * Creates new search instance for the specified entry. 356 * Note that this method does not work for DSTU2 model classes, it will only work 357 * on DSTU3+. 358 * 359 * @param entry Entry to create search instance for 360 * @return Returns the search instance 361 */ 362 public IBaseBackboneElement addSearch(IBase entry) { 363 Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "This method may only be called for FHIR version DSTU3 and above"); 364 365 IBase searchInstance = mySearchDef.newInstance(); 366 mySearchChild.getMutator().setValue(entry, searchInstance); 367 return (IBaseBackboneElement) searchInstance; 368 } 369 370 private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) { 371 Validate.notNull(theResource, "theResource must not be null"); 372 373 IBase entry = addEntry(); 374 375 // Bundle.entry.fullUrl 376 IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance(); 377 fullUrl.setValueAsString(theFullUrl); 378 myEntryFullUrlChild.getMutator().setValue(entry, fullUrl); 379 380 // Bundle.entry.resource 381 myEntryResourceChild.getMutator().setValue(entry, theResource); 382 383 // Bundle.entry.request 384 IBase request = myEntryRequestDef.newInstance(); 385 myEntryRequestChild.getMutator().setValue(entry, request); 386 return request; 387 } 388 389 public IBase addEntryAndReturnRequest() { 390 IBase entry = addEntry(); 391 392 // Bundle.entry.request 393 IBase request = myEntryRequestDef.newInstance(); 394 myEntryRequestChild.getMutator().setValue(entry, request); 395 return request; 396 397 } 398 399 400 public IBaseBundle getBundle() { 401 return myBundle; 402 } 403 404 /** 405 * Convenience method which auto-casts the results of {@link #getBundle()} 406 * 407 * @since 6.3.0 408 */ 409 public <T extends IBaseBundle> T getBundleTyped() { 410 return (T) myBundle; 411 } 412 413 /** 414 * Note that this method does not work for DSTU2 model classes, it will only work 415 * on DSTU3+. 416 */ 417 public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) { 418 Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "This method may only be called for FHIR version DSTU3 and above"); 419 420 BaseRuntimeChildDefinition.IMutator mutator = myMetaDef.getChildByName(theFieldName).getMutator(); 421 mutator.setValue(myBundle.getMeta(), theFieldValue); 422 return this; 423 } 424 425 /** 426 * Sets the specified entry field. 427 * 428 * @param theEntry The entry instance to set values on 429 * @param theEntryChildName The child field name of the entry instance to be set 430 * @param theValue The field value to set 431 */ 432 public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) { 433 addToBase(theEntry, theEntryChildName, theValue, myEntryDef); 434 } 435 436 /** 437 * Sets the specified search field. 438 * 439 * @param theSearch The search instance to set values on 440 * @param theSearchFieldName The child field name of the search instance to be set 441 * @param theSearchFieldValue The field value to set 442 */ 443 public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) { 444 addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef); 445 } 446 447 private void addToBase(IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) { 448 BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName); 449 Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase); 450 defn.getMutator().addValue(theBase, theValue); 451 } 452 453 /** 454 * Creates a new primitive. 455 * 456 * @param theTypeName The element type for the primitive 457 * @param <T> Actual type of the parameterized primitive type interface 458 * @return Returns the new empty instance of the element definition. 459 */ 460 public <T> IPrimitiveType<T> newPrimitive(String theTypeName) { 461 BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName); 462 Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName); 463 return (IPrimitiveType<T>) primitiveDefinition.newInstance(); 464 } 465 466 /** 467 * Creates a new primitive instance of the specified element type. 468 * 469 * @param theTypeName Element type to create 470 * @param theInitialValue Initial value to be set on the new instance 471 * @param <T> Actual type of the parameterized primitive type interface 472 * @return Returns the newly created instance 473 */ 474 public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) { 475 IPrimitiveType<T> retVal = newPrimitive(theTypeName); 476 retVal.setValue(theInitialValue); 477 return retVal; 478 } 479 480 /** 481 * Sets a value for <code>Bundle.type</code>. That this is a coded field so {@literal theType} 482 * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException} 483 * will be thrown. 484 */ 485 public void setType(String theType) { 486 setBundleField("type", theType); 487 } 488 489 /** 490 * Adds an identifier to <code>Bundle.identifier</code> 491 * 492 * @param theSystem The system 493 * @param theValue The value 494 * @since 6.4.0 495 */ 496 public void setIdentifier(@Nullable String theSystem, @Nullable String theValue) { 497 FhirTerser terser = myContext.newTerser(); 498 IBase identifier = terser.addElement(myBundle, "identifier"); 499 terser.setElement(identifier, "system", theSystem); 500 terser.setElement(identifier, "value", theValue); 501 } 502 503 /** 504 * Sets the timestamp in <code>Bundle.timestamp</code> 505 * 506 * @since 6.4.0 507 */ 508 public void setTimestamp(@Nonnull IPrimitiveType<Date> theTimestamp) { 509 FhirTerser terser = myContext.newTerser(); 510 terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString()); 511 } 512 513 514 public class DeleteBuilder extends BaseOperationBuilder { 515 516 // nothing yet 517 518 } 519 520 521 public class PatchBuilder extends BaseOperationBuilderWithConditionalUrl<PatchBuilder> { 522 523 PatchBuilder(IPrimitiveType<?> theUrl) { 524 super(theUrl); 525 } 526 527 } 528 529 public class UpdateBuilder extends BaseOperationBuilderWithConditionalUrl<UpdateBuilder> { 530 UpdateBuilder(IPrimitiveType<?> theUrl) { 531 super(theUrl); 532 } 533 534 } 535 536 public class CreateBuilder extends BaseOperationBuilder { 537 private final IBase myRequest; 538 539 CreateBuilder(IBase theRequest) { 540 myRequest = theRequest; 541 } 542 543 /** 544 * Make this create a Conditional Create 545 */ 546 public CreateBuilder conditional(String theConditionalUrl) { 547 BaseRuntimeElementDefinition<?> stringDefinition = Objects.requireNonNull(myContext.getElementDefinition("string")); 548 IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance(); 549 ifNoneExist.setValueAsString(theConditionalUrl); 550 551 myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist); 552 553 return this; 554 } 555 556 } 557 558 public abstract class BaseOperationBuilder { 559 560 /** 561 * Returns a reference to the BundleBuilder instance. 562 * <p> 563 * Calling this method has no effect at all, it is only 564 * provided for easy method chaning if you want to build 565 * your bundle as a single fluent call. 566 * 567 * @since 6.3.0 568 */ 569 public BundleBuilder andThen() { 570 return BundleBuilder.this; 571 } 572 573 574 } 575 576 public abstract class BaseOperationBuilderWithConditionalUrl<T extends BaseOperationBuilder> extends BaseOperationBuilder { 577 578 private final IPrimitiveType<?> myUrl; 579 580 BaseOperationBuilderWithConditionalUrl(IPrimitiveType<?> theUrl) { 581 myUrl = theUrl; 582 } 583 584 /** 585 * Make this update a Conditional Update 586 */ 587 @SuppressWarnings("unchecked") 588 public T conditional(String theConditionalUrl) { 589 myUrl.setValueAsString(theConditionalUrl); 590 return (T) this; 591 } 592 593 } 594}