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