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