001package ca.uhn.fhir.util;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2022 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.IBaseResource;
033import org.hl7.fhir.instance.model.api.IPrimitiveType;
034
035import java.util.Objects;
036
037/**
038 * This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide
039 * support for setting various bundle fields and working with bundle parts such as metadata and entry
040 * (method and search).
041 *
042 * <p>
043 *
044 * This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API
045 * may change.
046 *
047 * @since 5.1.0
048 */
049public class BundleBuilder {
050
051        private final FhirContext myContext;
052        private final IBaseBundle myBundle;
053        private final RuntimeResourceDefinition myBundleDef;
054        private final BaseRuntimeChildDefinition myEntryChild;
055        private final BaseRuntimeChildDefinition myMetaChild;
056        private final BaseRuntimeChildDefinition mySearchChild;
057        private final BaseRuntimeElementDefinition<?> myEntryDef;
058        private final BaseRuntimeElementDefinition<?> myMetaDef;
059        private final BaseRuntimeElementDefinition mySearchDef;
060        private final BaseRuntimeChildDefinition myEntryResourceChild;
061        private final BaseRuntimeChildDefinition myEntryFullUrlChild;
062        private final BaseRuntimeChildDefinition myEntryRequestChild;
063        private final BaseRuntimeElementDefinition<?> myEntryRequestDef;
064        private final BaseRuntimeChildDefinition myEntryRequestUrlChild;
065        private final BaseRuntimeChildDefinition myEntryRequestMethodChild;
066        private final BaseRuntimeElementDefinition<?> myEntryRequestMethodDef;
067        private final BaseRuntimeChildDefinition myEntryRequestIfNoneExistChild;
068
069        /**
070         * Constructor
071         */
072        public BundleBuilder(FhirContext theContext) {
073                myContext = theContext;
074
075                myBundleDef = myContext.getResourceDefinition("Bundle");
076                myBundle = (IBaseBundle) myBundleDef.newInstance();
077
078                myEntryChild = myBundleDef.getChildByName("entry");
079                myEntryDef = myEntryChild.getChildByName("entry");
080
081                mySearchChild = myEntryDef.getChildByName("search");
082                mySearchDef = mySearchChild.getChildByName("search");
083
084                myMetaChild = myBundleDef.getChildByName("meta");
085                myMetaDef = myMetaChild.getChildByName("meta");
086
087                myEntryResourceChild = myEntryDef.getChildByName("resource");
088                myEntryFullUrlChild = myEntryDef.getChildByName("fullUrl");
089
090                myEntryRequestChild = myEntryDef.getChildByName("request");
091                myEntryRequestDef = myEntryRequestChild.getChildByName("request");
092
093                myEntryRequestUrlChild = myEntryRequestDef.getChildByName("url");
094
095                myEntryRequestMethodChild = myEntryRequestDef.getChildByName("method");
096                myEntryRequestMethodDef = myEntryRequestMethodChild.getChildByName("method");
097
098                myEntryRequestIfNoneExistChild = myEntryRequestDef.getChildByName("ifNoneExist");
099        }
100
101        /**
102         * Sets the specified primitive field on the bundle with the value provided.
103         *
104         * @param theFieldName
105         *              Name of the primitive field.
106         * @param theFieldValue
107         *              Value of the field to be set.
108         */
109        public BundleBuilder setBundleField(String theFieldName, String theFieldValue) {
110                BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName(theFieldName);
111                Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
112
113                IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments());
114                type.setValueAsString(theFieldValue);
115                typeChild.getMutator().setValue(myBundle, type);
116                return this;
117        }
118
119        /**
120         * Sets the specified primitive field on the search entry with the value provided.
121         *
122         * @param theSearch
123         *              Search part of the entry
124         * @param theFieldName
125         *              Name of the primitive field.
126         * @param theFieldValue
127         *              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 an entry containing an update (PUT) request.
149         * Also sets the Bundle.type value to "transaction" if it is not already set.
150         *
151         * @param theResource The resource to update
152         */
153        public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) {
154                setBundleField("type", "transaction");
155
156                IBase request = addEntryAndReturnRequest(theResource);
157
158                // Bundle.entry.request.url
159                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
160                String resourceType = myContext.getResourceType(theResource);
161                url.setValueAsString(theResource.getIdElement().toUnqualifiedVersionless().withResourceType(resourceType).getValue());
162                myEntryRequestUrlChild.getMutator().setValue(request, url);
163
164                // Bundle.entry.request.url
165                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
166                method.setValueAsString("PUT");
167                myEntryRequestMethodChild.getMutator().setValue(request, method);
168
169                return new UpdateBuilder(url);
170        }
171
172        /**
173         * Adds an entry containing an create (POST) request.
174         * Also sets the Bundle.type value to "transaction" if it is not already set.
175         *
176         * @param theResource The resource to create
177         */
178        public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) {
179                setBundleField("type", "transaction");
180
181                IBase request = addEntryAndReturnRequest(theResource);
182
183                String resourceType = myContext.getResourceType(theResource);
184
185                // Bundle.entry.request.url
186                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
187                url.setValueAsString(resourceType);
188                myEntryRequestUrlChild.getMutator().setValue(request, url);
189
190                // Bundle.entry.request.url
191                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
192                method.setValueAsString("POST");
193                myEntryRequestMethodChild.getMutator().setValue(request, method);
194
195                return new CreateBuilder(request);
196        }
197
198        /**
199         * Adds an entry containing a delete (DELETE) request.
200         * Also sets the Bundle.type value to "transaction" if it is not already set.
201         *
202         * 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,
203         *
204         * @param theResource The resource to delete.
205         */
206        public void addTransactionDeleteEntry(IBaseResource theResource) {
207                String resourceType = myContext.getResourceType(theResource);
208                String idPart = theResource.getIdElement().toUnqualifiedVersionless().getIdPart();
209                addTransactionDeleteEntry(resourceType, idPart);
210        }
211
212        /**
213         * Adds an entry containing a delete (DELETE) request.
214         * Also sets the Bundle.type value to "transaction" if it is not already set.
215         *
216         * @param theResourceType The type resource to delete.
217         * @param theIdPart  the ID of the resource to delete.
218         */
219        public void addTransactionDeleteEntry(String theResourceType, String theIdPart) {
220                setBundleField("type", "transaction");
221                IBase request = addEntryAndReturnRequest();
222                IdDt idDt = new IdDt(theIdPart);
223                
224                // Bundle.entry.request.url
225                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
226                url.setValueAsString(idDt.toUnqualifiedVersionless().withResourceType(theResourceType).getValue());
227                myEntryRequestUrlChild.getMutator().setValue(request, url);
228
229                // Bundle.entry.request.method
230                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
231                method.setValueAsString("DELETE");
232                myEntryRequestMethodChild.getMutator().setValue(request, method);
233        }
234
235
236
237        /**
238         * Adds an entry for a Collection bundle type
239         */
240        public void addCollectionEntry(IBaseResource theResource) {
241                setType("collection");
242                addEntryAndReturnRequest(theResource);
243        }
244
245        /**
246         * Creates new entry and adds it to the bundle
247         *
248         * @return
249         *              Returns the new entry.
250         */
251        public IBase addEntry() {
252                IBase entry = myEntryDef.newInstance();
253                myEntryChild.getMutator().addValue(myBundle, entry);
254                return entry;
255        }
256
257        /**
258         * Creates new search instance for the specified entry
259         *
260         * @param entry Entry to create search instance for
261         * @return
262         *              Returns the search instance
263         */
264        public IBaseBackboneElement addSearch(IBase entry) {
265                IBase searchInstance = mySearchDef.newInstance();
266                mySearchChild.getMutator().setValue(entry, searchInstance);
267                return (IBaseBackboneElement) searchInstance;
268        }
269
270        /**
271         *
272         * @param theResource
273         * @return
274         */
275        public IBase addEntryAndReturnRequest(IBaseResource theResource) {
276                Validate.notNull(theResource, "theResource must not be null");
277
278                IBase entry = addEntry();
279
280                // Bundle.entry.fullUrl
281                IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
282                fullUrl.setValueAsString(theResource.getIdElement().getValue());
283                myEntryFullUrlChild.getMutator().setValue(entry, fullUrl);
284
285                // Bundle.entry.resource
286                myEntryResourceChild.getMutator().setValue(entry, theResource);
287
288                // Bundle.entry.request
289                IBase request = myEntryRequestDef.newInstance();
290                myEntryRequestChild.getMutator().setValue(entry, request);
291                return request;
292        }
293
294        public IBase addEntryAndReturnRequest() {
295                IBase entry = addEntry();
296
297                // Bundle.entry.request
298                IBase request = myEntryRequestDef.newInstance();
299                myEntryRequestChild.getMutator().setValue(entry, request);
300                return request;
301
302        }
303
304
305        public IBaseBundle getBundle() {
306                return myBundle;
307        }
308
309        public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) {
310                BaseRuntimeChildDefinition.IMutator mutator = myMetaDef.getChildByName(theFieldName).getMutator();
311                mutator.setValue(myBundle.getMeta(), theFieldValue);
312                return this;
313        }
314
315        /**
316         * Sets the specified entry field.
317         *
318         * @param theEntry
319         *              The entry instance to set values on
320         * @param theEntryChildName
321         *              The child field name of the entry instance to be set
322         * @param theValue
323         *              The field value to set
324         */
325        public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) {
326                addToBase(theEntry, theEntryChildName, theValue, myEntryDef);
327        }
328
329        /**
330         * Sets the specified search field.
331         *
332         * @param theSearch
333         *              The search instance to set values on
334         * @param theSearchFieldName
335         *              The child field name of the search instance to be set
336         * @param theSearchFieldValue
337         *              The field value to set
338         */
339        public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) {
340                addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef);
341        }
342
343        private void addToBase(IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) {
344                BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName);
345                Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase);
346                defn.getMutator().addValue(theBase, theValue);
347        }
348
349        /**
350         * Creates a new primitive.
351         *
352         * @param theTypeName
353         *              The element type for the primitive
354         * @param <T>
355         *      Actual type of the parameterized primitive type interface
356         * @return
357         *              Returns the new empty instance of the element definition.
358         */
359        public <T> IPrimitiveType<T> newPrimitive(String theTypeName) {
360                BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName);
361                Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName);
362                return (IPrimitiveType<T>) primitiveDefinition.newInstance();
363        }
364
365        /**
366         * Creates a new primitive instance of the specified element type.
367         *
368         * @param theTypeName
369         *              Element type to create
370         * @param theInitialValue
371         *              Initial value to be set on the new instance
372         * @param <T>
373         *      Actual type of the parameterized primitive type interface
374         * @return
375         *              Returns the newly created instance
376         */
377        public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) {
378                IPrimitiveType<T> retVal = newPrimitive(theTypeName);
379                retVal.setValue(theInitialValue);
380                return retVal;
381        }
382
383        /**
384         * Sets a value for <code>Bundle.type</code>. That this is a coded field so {@literal theType}
385         * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException}
386         * will be thrown.
387         */
388        public void setType(String theType) {
389                setBundleField("type", theType);
390        }
391
392        public static class UpdateBuilder {
393
394                private final IPrimitiveType<?> myUrl;
395
396                public UpdateBuilder(IPrimitiveType<?> theUrl) {
397                        myUrl = theUrl;
398                }
399
400                /**
401                 * Make this update a Conditional Update
402                 */
403                public void conditional(String theConditionalUrl) {
404                        myUrl.setValueAsString(theConditionalUrl);
405                }
406        }
407
408        public class CreateBuilder {
409                private final IBase myRequest;
410
411                public CreateBuilder(IBase theRequest) {
412                        myRequest = theRequest;
413                }
414
415                /**
416                 * Make this create a Conditional Create
417                 */
418                public void conditional(String theConditionalUrl) {
419                        BaseRuntimeElementDefinition<?> stringDefinition = Objects.requireNonNull(myContext.getElementDefinition("string"));
420                        IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance();
421                        ifNoneExist.setValueAsString(theConditionalUrl);
422
423                        myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist);
424                }
425
426        }
427}