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                url.setValueAsString(theResource.getIdElement().toUnqualifiedVersionless().getValue());
160                myEntryRequestUrlChild.getMutator().setValue(request, url);
161
162                // Bundle.entry.request.url
163                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
164                method.setValueAsString("PUT");
165                myEntryRequestMethodChild.getMutator().setValue(request, method);
166
167                return new UpdateBuilder(url);
168        }
169
170        /**
171         * Adds an entry containing an create (POST) request.
172         * Also sets the Bundle.type value to "transaction" if it is not already set.
173         *
174         * @param theResource The resource to create
175         */
176        public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) {
177                setBundleField("type", "transaction");
178
179                IBase request = addEntryAndReturnRequest(theResource);
180
181                String resourceType = myContext.getResourceType(theResource);
182
183                // Bundle.entry.request.url
184                IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
185                url.setValueAsString(resourceType);
186                myEntryRequestUrlChild.getMutator().setValue(request, url);
187
188                // Bundle.entry.request.url
189                IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
190                method.setValueAsString("POST");
191                myEntryRequestMethodChild.getMutator().setValue(request, method);
192
193                return new CreateBuilder(request);
194        }
195
196        /**
197         * Adds an entry for a Collection bundle type
198         */
199        public void addCollectionEntry(IBaseResource theResource) {
200                setType("collection");
201                addEntryAndReturnRequest(theResource);
202        }
203
204        /**
205         * Creates new entry and adds it to the bundle
206         *
207         * @return
208         *              Returns the new entry.
209         */
210        public IBase addEntry() {
211                IBase entry = myEntryDef.newInstance();
212                myEntryChild.getMutator().addValue(myBundle, entry);
213                return entry;
214        }
215
216        /**
217         * Creates new search instance for the specified entry
218         *
219         * @param entry Entry to create search instance for
220         * @return
221         *              Returns the search instance
222         */
223        public IBaseBackboneElement addSearch(IBase entry) {
224                IBase searchInstance = mySearchDef.newInstance();
225                mySearchChild.getMutator().setValue(entry, searchInstance);
226                return (IBaseBackboneElement) searchInstance;
227        }
228
229        /**
230         *
231         * @param theResource
232         * @return
233         */
234        public IBase addEntryAndReturnRequest(IBaseResource theResource) {
235                Validate.notNull(theResource, "theResource must not be null");
236
237                IBase entry = addEntry();
238
239                // Bundle.entry.fullUrl
240                IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
241                fullUrl.setValueAsString(theResource.getIdElement().getValue());
242                myEntryFullUrlChild.getMutator().setValue(entry, fullUrl);
243
244                // Bundle.entry.resource
245                myEntryResourceChild.getMutator().setValue(entry, theResource);
246
247                // Bundle.entry.request
248                IBase request = myEntryRequestDef.newInstance();
249                myEntryRequestChild.getMutator().setValue(entry, request);
250                return request;
251        }
252
253
254        public IBaseBundle getBundle() {
255                return myBundle;
256        }
257
258        public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) {
259                BaseRuntimeChildDefinition.IMutator mutator = myMetaDef.getChildByName(theFieldName).getMutator();
260                mutator.setValue(myBundle.getMeta(), theFieldValue);
261                return this;
262        }
263
264        /**
265         * Sets the specified entry field.
266         *
267         * @param theEntry
268         *              The entry instance to set values on
269         * @param theEntryChildName
270         *              The child field name of the entry instance to be set
271         * @param theValue
272         *              The field value to set
273         */
274        public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) {
275                addToBase(theEntry, theEntryChildName, theValue, myEntryDef);
276        }
277
278        /**
279         * Sets the specified search field.
280         *
281         * @param theSearch
282         *              The search instance to set values on
283         * @param theSearchFieldName
284         *              The child field name of the search instance to be set
285         * @param theSearchFieldValue
286         *              The field value to set
287         */
288        public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) {
289                addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef);
290        }
291
292        private void addToBase(IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) {
293                BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName);
294                Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase);
295                defn.getMutator().addValue(theBase, theValue);
296        }
297
298        /**
299         * Creates a new primitive.
300         *
301         * @param theTypeName
302         *              The element type for the primitive
303         * @param <T>
304         *      Actual type of the parameterized primitive type interface
305         * @return
306         *              Returns the new empty instance of the element definition.
307         */
308        public <T> IPrimitiveType<T> newPrimitive(String theTypeName) {
309                BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName);
310                Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName);
311                return (IPrimitiveType<T>) primitiveDefinition.newInstance();
312        }
313
314        /**
315         * Creates a new primitive instance of the specified element type.
316         *
317         * @param theTypeName
318         *              Element type to create
319         * @param theInitialValue
320         *              Initial value to be set on the new instance
321         * @param <T>
322         *      Actual type of the parameterized primitive type interface
323         * @return
324         *              Returns the newly created instance
325         */
326        public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) {
327                IPrimitiveType<T> retVal = newPrimitive(theTypeName);
328                retVal.setValue(theInitialValue);
329                return retVal;
330        }
331
332        /**
333         * Sets a value for <code>Bundle.type</code>. That this is a coded field so {@literal theType}
334         * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException}
335         * will be thrown.
336         */
337        public void setType(String theType) {
338                setBundleField("type", theType);
339        }
340
341        public static class UpdateBuilder {
342
343                private final IPrimitiveType<?> myUrl;
344
345                public UpdateBuilder(IPrimitiveType<?> theUrl) {
346                        myUrl = theUrl;
347                }
348
349                /**
350                 * Make this update a Conditional Update
351                 */
352                public void conditional(String theConditionalUrl) {
353                        myUrl.setValueAsString(theConditionalUrl);
354                }
355
356        }
357
358        public class CreateBuilder {
359                private final IBase myRequest;
360
361                public CreateBuilder(IBase theRequest) {
362                        myRequest = theRequest;
363                }
364
365                /**
366                 * Make this create a Conditional Create
367                 */
368                public void conditional(String theConditionalUrl) {
369                        BaseRuntimeElementDefinition<?> stringDefinition = Objects.requireNonNull(myContext.getElementDefinition("string"));
370                        IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) stringDefinition.newInstance();
371                        ifNoneExist.setValueAsString(theConditionalUrl);
372
373                        myEntryRequestIfNoneExistChild.getMutator().setValue(myRequest, ifNoneExist);
374                }
375
376        }
377}