001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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 org.apache.commons.lang3.Validate;
029import org.hl7.fhir.instance.model.api.IBase;
030import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
031import org.hl7.fhir.instance.model.api.IBaseBundle;
032import org.hl7.fhir.instance.model.api.IBaseParameters;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IIdType;
035import org.hl7.fhir.instance.model.api.IPrimitiveType;
036
037import javax.annotation.Nonnull;
038import javax.annotation.Nullable;
039import java.util.Date;
040import java.util.Objects;
041
042/**
043 * This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide
044 * support for setting various bundle fields and working with bundle parts such as metadata and entry
045 * (method and search).
046 *
047 * <p>
048 * <p>
049 * This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API
050 * may change.
051 *
052 * @since 5.1.0
053 */
054public class BundleBuilder {
055
056        private final FhirContext myContext;
057        private final IBaseBundle myBundle;
058        private final RuntimeResourceDefinition myBundleDef;
059        private final BaseRuntimeChildDefinition myEntryChild;
060        private final BaseRuntimeChildDefinition myMetaChild;
061        private final BaseRuntimeChildDefinition mySearchChild;
062        private final BaseRuntimeElementDefinition<?> myEntryDef;
063        private final BaseRuntimeElementDefinition<?> myMetaDef;
064        private final BaseRuntimeElementDefinition mySearchDef;
065        private final BaseRuntimeChildDefinition myEntryResourceChild;
066        private final BaseRuntimeChildDefinition myEntryFullUrlChild;
067        private final BaseRuntimeChildDefinition myEntryRequestChild;
068        private final BaseRuntimeElementDefinition<?> myEntryRequestDef;
069        private final BaseRuntimeChildDefinition myEntryRequestUrlChild;
070        private final BaseRuntimeChildDefinition myEntryRequestMethodChild;
071        private final BaseRuntimeElementDefinition<?> myEntryRequestMethodDef;
072        private final BaseRuntimeChildDefinition myEntryRequestIfNoneExistChild;
073
074        /**
075         * Constructor
076         */
077        public BundleBuilder(FhirContext theContext) {
078                myContext = theContext;
079
080                myBundleDef = myContext.getResourceDefinition("Bundle");
081                myBundle = (IBaseBundle) myBundleDef.newInstance();
082
083                myEntryChild = myBundleDef.getChildByName("entry");
084                myEntryDef = myEntryChild.getChildByName("entry");
085
086                mySearchChild = myEntryDef.getChildByName("search");
087                mySearchDef = mySearchChild.getChildByName("search");
088
089                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<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments());
122                type.setValueAsString(theFieldValue);
123                typeChild.getMutator().setValue(myBundle, type);
124                return this;
125        }
126
127        /**
128         * Sets the specified primitive field on the search entry with the value provided.
129         *
130         * @param theSearch     Search part of the entry
131         * @param theFieldName  Name of the primitive field.
132         * @param theFieldValue Value of the field to be set.
133         */
134        public BundleBuilder setSearchField(IBase theSearch, String theFieldName, String theFieldValue) {
135                BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName);
136                Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
137
138                IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments());
139                type.setValueAsString(theFieldValue);
140                typeChild.getMutator().setValue(theSearch, type);
141                return this;
142        }
143
144        public BundleBuilder setSearchField(IBase theSearch, String theFieldName, IPrimitiveType<?> theFieldValue) {
145                BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName);
146                Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
147
148                typeChild.getMutator().setValue(theSearch, theFieldValue);
149                return this;
150        }
151
152        /**
153         * Adds a FHIRPatch patch bundle to the transaction
154         *
155         * @param theTarget The target resource ID to patch
156         * @param thePatch  The FHIRPath Parameters resource
157         * @since 6.3.0
158         */
159        public PatchBuilder addTransactionFhirPatchEntry(IIdType theTarget, IBaseParameters thePatch) {
160                Validate.notNull(theTarget, "theTarget must not be null");
161                Validate.notBlank(theTarget.getResourceType(), "theTarget must contain a resource type");
162                Validate.notBlank(theTarget.getIdPart(), "theTarget must contain an ID");
163
164                IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(thePatch, theTarget.getValue(), theTarget.toUnqualifiedVersionless().getValue(), "PATCH");
165
166                return new PatchBuilder(url);
167        }
168
169        /**
170         * Adds a FHIRPatch patch bundle to the transaction. This method is intended for conditional PATCH operations. If you
171         * know the ID of the resource you wish to patch, use {@link #addTransactionFhirPatchEntry(IIdType, IBaseParameters)}
172         * instead.
173         *
174         * @param thePatch The FHIRPath Parameters resource
175         * @see #addTransactionFhirPatchEntry(IIdType, IBaseParameters)
176         * @since 6.3.0
177         */
178        public PatchBuilder addTransactionFhirPatchEntry(IBaseParameters thePatch) {
179                IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(thePatch, null, null, "PATCH");
180
181                return new PatchBuilder(url);
182        }
183
184        /**
185         * Adds an entry containing an update (PUT) request.
186         * Also sets the Bundle.type value to "transaction" if it is not already set.
187         *
188         * @param theResource The resource to update
189         */
190        public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) {
191                Validate.notNull(theResource, "theResource must not be null");
192
193                IIdType id = theResource.getIdElement();
194                if (id.hasIdPart() && !id.hasResourceType()) {
195                        String resourceType = myContext.getResourceType(theResource);
196                        id = id.withResourceType(resourceType);
197                }
198
199                String requestUrl = id.toUnqualifiedVersionless().getValue();
200                String fullUrl = id.getValue();
201                String verb = "PUT";
202
203                IPrimitiveType<?> url = addAndPopulateTransactionBundleEntryRequest(theResource, fullUrl, requestUrl, verb);
204
205                return new UpdateBuilder(url);
206        }
207
208        @Nonnull
209        private IPrimitiveType<?> addAndPopulateTransactionBundleEntryRequest(IBaseResource theResource, String theFullUrl, String theRequestUrl, String theHttpVerb) {
210                setBundleField("type", "transaction");
211
212                IBase request = addEntryAndReturnRequest(theResource, theFullUrl);
213
214                // Bundle.entry.request.url
215                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
216                url.setValueAsString(theRequestUrl);
217                myEntryRequestUrlChild.getMutator().setValue(request, url);
218
219                // Bundle.entry.request.method
220                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
221                method.setValueAsString(theHttpVerb);
222                myEntryRequestMethodChild.getMutator().setValue(request, method);
223                return url;
224        }
225
226        /**
227         * Adds an entry containing an create (POST) request.
228         * Also sets the Bundle.type value to "transaction" if it is not already set.
229         *
230         * @param theResource The resource to create
231         */
232        public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) {
233                setBundleField("type", "transaction");
234
235                IBase request = addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
236
237                String resourceType = myContext.getResourceType(theResource);
238
239                // Bundle.entry.request.url
240                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
241                url.setValueAsString(resourceType);
242                myEntryRequestUrlChild.getMutator().setValue(request, url);
243
244                // Bundle.entry.request.url
245                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
246                method.setValueAsString("POST");
247                myEntryRequestMethodChild.getMutator().setValue(request, method);
248
249                return new CreateBuilder(request);
250        }
251
252        /**
253         * Adds an entry containing a delete (DELETE) request.
254         * Also sets the Bundle.type value to "transaction" if it is not already set.
255         * <p>
256         * 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,
257         *
258         * @param theResource The resource to delete.
259         */
260        public DeleteBuilder addTransactionDeleteEntry(IBaseResource theResource) {
261                String resourceType = myContext.getResourceType(theResource);
262                String idPart = theResource.getIdElement().toUnqualifiedVersionless().getIdPart();
263                return addTransactionDeleteEntry(resourceType, idPart);
264        }
265
266        /**
267         * Adds an entry containing a delete (DELETE) request.
268         * Also sets the Bundle.type value to "transaction" if it is not already set.
269         * <p>
270         * 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,
271         *
272         * @param theResourceId The resource ID to delete.
273         * @return
274         */
275        public DeleteBuilder addTransactionDeleteEntry(IIdType theResourceId) {
276                String resourceType = theResourceId.getResourceType();
277                String idPart = theResourceId.getIdPart();
278                return addTransactionDeleteEntry(resourceType, idPart);
279        }
280
281        /**
282         * Adds an entry containing a delete (DELETE) request.
283         * Also sets the Bundle.type value to "transaction" if it is not already set.
284         *
285         * @param theResourceType The type resource to delete.
286         * @param theIdPart       the ID of the resource to delete.
287         */
288        public DeleteBuilder addTransactionDeleteEntry(String theResourceType, String theIdPart) {
289                setBundleField("type", "transaction");
290                IdDt idDt = new IdDt(theIdPart);
291
292                String deleteUrl = idDt.toUnqualifiedVersionless().withResourceType(theResourceType).getValue();
293
294                return addDeleteEntry(deleteUrl);
295        }
296
297        /**
298         * Adds an entry containing a delete (DELETE) request.
299         * Also sets the Bundle.type value to "transaction" if it is not already set.
300         *
301         * @param theMatchUrl The match URL, e.g. <code>Patient?identifier=http://foo|123</code>
302         * @since 6.3.0
303         */
304        public BaseOperationBuilder addTransactionDeleteEntryConditional(String theMatchUrl) {
305                Validate.notBlank(theMatchUrl, "theMatchUrl must not be null or blank");
306                return addDeleteEntry(theMatchUrl);
307        }
308
309        @Nonnull
310        private DeleteBuilder addDeleteEntry(String theDeleteUrl) {
311                IBase request = addEntryAndReturnRequest();
312
313                // Bundle.entry.request.url
314                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
315                url.setValueAsString(theDeleteUrl);
316                myEntryRequestUrlChild.getMutator().setValue(request, url);
317
318                // Bundle.entry.request.method
319                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
320                method.setValueAsString("DELETE");
321                myEntryRequestMethodChild.getMutator().setValue(request, method);
322
323                return new DeleteBuilder();
324        }
325
326
327        /**
328         * Adds an entry for a Collection bundle type
329         */
330        public void addCollectionEntry(IBaseResource theResource) {
331                setType("collection");
332                addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
333        }
334
335        /**
336         * Adds an entry for a Document bundle type
337         */
338        public void addDocumentEntry(IBaseResource theResource) {
339                setType("document");
340                addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue());
341        }
342
343        /**
344         * Creates new entry and adds it to the bundle
345         *
346         * @return Returns the new entry.
347         */
348        public IBase addEntry() {
349                IBase entry = myEntryDef.newInstance();
350                myEntryChild.getMutator().addValue(myBundle, entry);
351                return entry;
352        }
353
354        /**
355         * Creates new search instance for the specified entry.
356         * Note that this method does not work for DSTU2 model classes, it will only work
357         * on DSTU3+.
358         *
359         * @param entry Entry to create search instance for
360         * @return Returns the search instance
361         */
362        public IBaseBackboneElement addSearch(IBase entry) {
363                Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "This method may only be called for FHIR version DSTU3 and above");
364
365                IBase searchInstance = mySearchDef.newInstance();
366                mySearchChild.getMutator().setValue(entry, searchInstance);
367                return (IBaseBackboneElement) searchInstance;
368        }
369
370        private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) {
371                Validate.notNull(theResource, "theResource must not be null");
372
373                IBase entry = addEntry();
374
375                // Bundle.entry.fullUrl
376                IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
377                fullUrl.setValueAsString(theFullUrl);
378                myEntryFullUrlChild.getMutator().setValue(entry, fullUrl);
379
380                // Bundle.entry.resource
381                myEntryResourceChild.getMutator().setValue(entry, theResource);
382
383                // Bundle.entry.request
384                IBase request = myEntryRequestDef.newInstance();
385                myEntryRequestChild.getMutator().setValue(entry, request);
386                return request;
387        }
388
389        public IBase addEntryAndReturnRequest() {
390                IBase entry = addEntry();
391
392                // Bundle.entry.request
393                IBase request = myEntryRequestDef.newInstance();
394                myEntryRequestChild.getMutator().setValue(entry, request);
395                return request;
396
397        }
398
399
400        public IBaseBundle getBundle() {
401                return myBundle;
402        }
403
404        /**
405         * Convenience method which auto-casts the results of {@link #getBundle()}
406         *
407         * @since 6.3.0
408         */
409        public <T extends IBaseBundle> T getBundleTyped() {
410                return (T) myBundle;
411        }
412
413        /**
414         * Note that this method does not work for DSTU2 model classes, it will only work
415         * on DSTU3+.
416         */
417        public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) {
418                Validate.isTrue(myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3), "This method may only be called for FHIR version DSTU3 and above");
419
420                BaseRuntimeChildDefinition.IMutator mutator = myMetaDef.getChildByName(theFieldName).getMutator();
421                mutator.setValue(myBundle.getMeta(), theFieldValue);
422                return this;
423        }
424
425        /**
426         * Sets the specified entry field.
427         *
428         * @param theEntry          The entry instance to set values on
429         * @param theEntryChildName The child field name of the entry instance to be set
430         * @param theValue          The field value to set
431         */
432        public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) {
433                addToBase(theEntry, theEntryChildName, theValue, myEntryDef);
434        }
435
436        /**
437         * Sets the specified search field.
438         *
439         * @param theSearch           The search instance to set values on
440         * @param theSearchFieldName  The child field name of the search instance to be set
441         * @param theSearchFieldValue The field value to set
442         */
443        public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) {
444                addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef);
445        }
446
447        private void addToBase(IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) {
448                BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName);
449                Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase);
450                defn.getMutator().addValue(theBase, theValue);
451        }
452
453        /**
454         * Creates a new primitive.
455         *
456         * @param theTypeName The element type for the primitive
457         * @param <T>         Actual type of the parameterized primitive type interface
458         * @return Returns the new empty instance of the element definition.
459         */
460        public <T> IPrimitiveType<T> newPrimitive(String theTypeName) {
461                BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName);
462                Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName);
463                return (IPrimitiveType<T>) primitiveDefinition.newInstance();
464        }
465
466        /**
467         * Creates a new primitive instance of the specified element type.
468         *
469         * @param theTypeName     Element type to create
470         * @param theInitialValue Initial value to be set on the new instance
471         * @param <T>             Actual type of the parameterized primitive type interface
472         * @return Returns the newly created instance
473         */
474        public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) {
475                IPrimitiveType<T> retVal = newPrimitive(theTypeName);
476                retVal.setValue(theInitialValue);
477                return retVal;
478        }
479
480        /**
481         * Sets a value for <code>Bundle.type</code>. That this is a coded field so {@literal theType}
482         * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException}
483         * will be thrown.
484         */
485        public void setType(String theType) {
486                setBundleField("type", theType);
487        }
488
489        /**
490         * Adds an identifier to <code>Bundle.identifier</code>
491         *
492         * @param theSystem The system
493         * @param theValue  The value
494         * @since 6.4.0
495         */
496        public void setIdentifier(@Nullable String theSystem, @Nullable String theValue) {
497                FhirTerser terser = myContext.newTerser();
498                IBase identifier = terser.addElement(myBundle, "identifier");
499                terser.setElement(identifier, "system", theSystem);
500                terser.setElement(identifier, "value", theValue);
501        }
502
503        /**
504         * Sets the timestamp in <code>Bundle.timestamp</code>
505         *
506         * @since 6.4.0
507         */
508        public void setTimestamp(@Nonnull IPrimitiveType<Date> theTimestamp) {
509                FhirTerser terser = myContext.newTerser();
510                terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString());
511        }
512
513
514        public class DeleteBuilder extends BaseOperationBuilder {
515
516                // nothing yet
517
518        }
519
520
521        public class PatchBuilder extends BaseOperationBuilderWithConditionalUrl<PatchBuilder> {
522
523                PatchBuilder(IPrimitiveType<?> theUrl) {
524                        super(theUrl);
525                }
526
527        }
528
529        public class UpdateBuilder extends BaseOperationBuilderWithConditionalUrl<UpdateBuilder> {
530                UpdateBuilder(IPrimitiveType<?> theUrl) {
531                        super(theUrl);
532                }
533
534        }
535
536        public class CreateBuilder extends BaseOperationBuilder {
537                private final IBase myRequest;
538
539                CreateBuilder(IBase theRequest) {
540                        myRequest = theRequest;
541                }
542
543                /**
544                 * Make this create a Conditional Create
545                 */
546                public CreateBuilder conditional(String theConditionalUrl) {
547                        BaseRuntimeElementDefinition<?> stringDefinition = Objects.requireNonNull(myContext.getElementDefinition("string"));
548                        IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance();
549                        ifNoneExist.setValueAsString(theConditionalUrl);
550
551                        myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist);
552
553                        return this;
554                }
555
556        }
557
558        public abstract class BaseOperationBuilder {
559
560                /**
561                 * Returns a reference to the BundleBuilder instance.
562                 * <p>
563                 * Calling this method has no effect at all, it is only
564                 * provided for easy method chaning if you want to build
565                 * your bundle as a single fluent call.
566                 *
567                 * @since 6.3.0
568                 */
569                public BundleBuilder andThen() {
570                        return BundleBuilder.this;
571                }
572
573
574        }
575
576        public abstract class BaseOperationBuilderWithConditionalUrl<T extends BaseOperationBuilder> extends BaseOperationBuilder {
577
578                private final IPrimitiveType<?> myUrl;
579
580                BaseOperationBuilderWithConditionalUrl(IPrimitiveType<?> theUrl) {
581                        myUrl = theUrl;
582                }
583
584                /**
585                 * Make this update a Conditional Update
586                 */
587                @SuppressWarnings("unchecked")
588                public T conditional(String theConditionalUrl) {
589                        myUrl.setValueAsString(theConditionalUrl);
590                        return (T) this;
591                }
592
593        }
594}