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