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