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