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