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