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