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