001package org.hl7.fhir.r4.hapi.rest.server;
002
003/*
004 * #%L
005 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0)
006 * %%
007 * Copyright (C) 2014 - 2015 University Health Network
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.FhirContext;
024import ca.uhn.fhir.context.api.BundleInclusionRule;
025import ca.uhn.fhir.model.api.Include;
026import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
027import ca.uhn.fhir.model.valueset.BundleTypeEnum;
028import ca.uhn.fhir.rest.api.Constants;
029import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
030import ca.uhn.fhir.rest.server.RestfulServerUtils;
031import ca.uhn.fhir.util.ResourceReferenceInfo;
032import org.hl7.fhir.instance.model.api.*;
033import org.hl7.fhir.r4.model.Bundle;
034import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
035import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent;
036import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
037import org.hl7.fhir.r4.model.Bundle.SearchEntryMode;
038import org.hl7.fhir.r4.model.DomainResource;
039import org.hl7.fhir.r4.model.IdType;
040import org.hl7.fhir.r4.model.Resource;
041
042import java.util.*;
043
044import static org.apache.commons.lang3.StringUtils.isNotBlank;
045
046@SuppressWarnings("Duplicates")
047public class R4BundleFactory implements IVersionSpecificBundleFactory {
048  private String myBase;
049  private Bundle myBundle;
050  private FhirContext myContext;
051
052  public R4BundleFactory(FhirContext theContext) {
053    myContext = theContext;
054  }
055
056  private void addResourcesForSearch(List<? extends IBaseResource> theResult) {
057    List<IBaseResource> includedResources = new ArrayList<IBaseResource>();
058    Set<IIdType> addedResourceIds = new HashSet<IIdType>();
059
060    for (IBaseResource next : theResult) {
061      if (next.getIdElement().isEmpty() == false) {
062        addedResourceIds.add(next.getIdElement());
063      }
064    }
065
066    for (IBaseResource nextBaseRes : theResult) {
067      Resource next = (Resource) nextBaseRes;
068      Set<String> containedIds = new HashSet<String>();
069      if (next instanceof DomainResource) {
070        for (Resource nextContained : ((DomainResource) next).getContained()) {
071          if (nextContained.getIdElement().isEmpty() == false) {
072            containedIds.add(nextContained.getIdElement().getValue());
073          }
074        }
075      }
076
077      List<IBaseReference> references = myContext.newTerser().getAllPopulatedChildElementsOfType(next, IBaseReference.class);
078      do {
079        List<IAnyResource> addedResourcesThisPass = new ArrayList<>();
080
081        for (IBaseReference nextRef : references) {
082          IAnyResource nextRes = (IAnyResource) nextRef.getResource();
083          if (nextRes != null) {
084            if (nextRes.getIdElement().hasIdPart()) {
085              if (containedIds.contains(nextRes.getIdElement().getValue())) {
086                // Don't add contained IDs as top level resources
087                continue;
088              }
089
090              IIdType id = nextRes.getIdElement();
091              if (id.hasResourceType() == false) {
092                String resName = myContext.getResourceDefinition(nextRes).getName();
093                id = id.withResourceType(resName);
094              }
095
096              if (!addedResourceIds.contains(id)) {
097                addedResourceIds.add(id);
098                addedResourcesThisPass.add(nextRes);
099              }
100
101            }
102          }
103        }
104
105        // Linked resources may themselves have linked resources
106        references = new ArrayList<>();
107        for (IAnyResource iResource : addedResourcesThisPass) {
108          List<IBaseReference> newReferences = myContext.newTerser().getAllPopulatedChildElementsOfType(iResource, IBaseReference.class);
109          references.addAll(newReferences);
110        }
111
112        includedResources.addAll(addedResourcesThisPass);
113
114      } while (references.isEmpty() == false);
115
116      BundleEntryComponent entry = myBundle.addEntry().setResource(next);
117      if (next.getIdElement().hasBaseUrl()) {
118        entry.setFullUrl(next.getId());
119      }
120
121      String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next);
122      if (httpVerb != null) {
123        entry.getRequest().getMethodElement().setValueAsString(httpVerb);
124        entry.getRequest().getUrlElement().setValue(next.getId());
125      }
126      if ("DELETE".equals(httpVerb)) {
127        entry.setResource(null);
128      }
129    }
130
131    /*
132     * Actually add the resources to the bundle
133     */
134    for (IBaseResource next : includedResources) {
135      BundleEntryComponent entry = myBundle.addEntry();
136      entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
137      if (next.getIdElement().hasBaseUrl()) {
138        entry.setFullUrl(next.getIdElement().getValue());
139      }
140    }
141  }
142
143  @Override
144  public void addResourcesToBundle(List<IBaseResource> theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set<Include> theIncludes) {
145    ensureBundle();
146
147    List<IAnyResource> includedResources = new ArrayList<IAnyResource>();
148    Set<IIdType> addedResourceIds = new HashSet<IIdType>();
149
150    for (IBaseResource next : theResult) {
151      if (next.getIdElement().isEmpty() == false) {
152        addedResourceIds.add(next.getIdElement());
153      }
154    }
155
156    for (IBaseResource next : theResult) {
157
158      Set<String> containedIds = new HashSet<String>();
159
160      if (next instanceof DomainResource) {
161        for (Resource nextContained : ((DomainResource) next).getContained()) {
162          if (isNotBlank(nextContained.getId())) {
163            containedIds.add(nextContained.getId());
164          }
165        }
166      }
167
168      List<ResourceReferenceInfo> references = myContext.newTerser().getAllResourceReferences(next);
169      do {
170        List<IAnyResource> addedResourcesThisPass = new ArrayList<IAnyResource>();
171
172        for (ResourceReferenceInfo nextRefInfo : references) {
173          if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) {
174            continue;
175          }
176
177          IAnyResource nextRes = (IAnyResource) nextRefInfo.getResourceReference().getResource();
178          if (nextRes != null) {
179            if (nextRes.getIdElement().hasIdPart()) {
180              if (containedIds.contains(nextRes.getIdElement().getValue())) {
181                // Don't add contained IDs as top level resources
182                continue;
183              }
184
185              IIdType id = nextRes.getIdElement();
186              if (id.hasResourceType() == false) {
187                String resName = myContext.getResourceDefinition(nextRes).getName();
188                id = id.withResourceType(resName);
189              }
190
191              if (!addedResourceIds.contains(id)) {
192                addedResourceIds.add(id);
193                addedResourcesThisPass.add(nextRes);
194              }
195
196            }
197          }
198        }
199
200        includedResources.addAll(addedResourcesThisPass);
201
202        // Linked resources may themselves have linked resources
203        references = new ArrayList<>();
204        for (IAnyResource iResource : addedResourcesThisPass) {
205          List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
206          references.addAll(newReferences);
207        }
208      } while (references.isEmpty() == false);
209
210      BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next);
211      Resource nextAsResource = (Resource) next;
212      IIdType id = populateBundleEntryFullUrl(next, entry);
213
214      // Populate Request
215      String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextAsResource);
216      if (httpVerb != null) {
217        entry.getRequest().getMethodElement().setValueAsString(httpVerb);
218        if (id != null) {
219          entry.getRequest().setUrl(id.getValue());
220        }
221      }
222      if ("DELETE".equals(httpVerb)) {
223        entry.setResource(null);
224      }
225
226      // Populate Bundle.entry.response
227      if (theBundleType != null) {
228        switch (theBundleType) {
229          case BATCH_RESPONSE:
230          case TRANSACTION_RESPONSE:
231          case HISTORY:
232            if ("1".equals(id.getVersionIdPart())) {
233              entry.getResponse().setStatus("201 Created");
234            } else if (isNotBlank(id.getVersionIdPart())) {
235              entry.getResponse().setStatus("200 OK");
236            }
237            if (isNotBlank(id.getVersionIdPart())) {
238              entry.getResponse().setEtag(RestfulServerUtils.createEtag(id.getVersionIdPart()));
239            }
240            break;
241        }
242      }
243
244      // Populate Bundle.entry.search
245      String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource);
246      if (searchMode != null) {
247        entry.getSearch().getModeElement().setValueAsString(searchMode);
248      }
249    }
250
251    /*
252     * Actually add the resources to the bundle
253     */
254    for (IAnyResource next : includedResources) {
255      BundleEntryComponent entry = myBundle.addEntry();
256      entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
257      populateBundleEntryFullUrl(next, entry);
258    }
259
260  }
261
262  @Override
263  public void addRootPropertiesToBundle(String theId, String theServerBase, String theLinkSelf, String theLinkPrev, String theLinkNext, Integer theTotalResults, BundleTypeEnum theBundleType,
264                                        IPrimitiveType<Date> theLastUpdated) {
265    ensureBundle();
266
267    myBase = theServerBase;
268
269    if (myBundle.getIdElement().isEmpty()) {
270      myBundle.setId(theId);
271    }
272    if (myBundle.getIdElement().isEmpty()) {
273      myBundle.setId(UUID.randomUUID().toString());
274    }
275
276    if (myBundle.getMeta().getLastUpdated() == null && theLastUpdated != null) {
277      myBundle.getMeta().getLastUpdatedElement().setValueAsString(theLastUpdated.getValueAsString());
278    }
279
280    if (!hasLink(Constants.LINK_SELF, myBundle) && isNotBlank(theLinkSelf)) {
281      myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theLinkSelf);
282    }
283    if (!hasLink(Constants.LINK_NEXT, myBundle) && isNotBlank(theLinkNext)) {
284      myBundle.addLink().setRelation(Constants.LINK_NEXT).setUrl(theLinkNext);
285    }
286    if (!hasLink(Constants.LINK_PREVIOUS, myBundle) && isNotBlank(theLinkPrev)) {
287      myBundle.addLink().setRelation(Constants.LINK_PREVIOUS).setUrl(theLinkPrev);
288    }
289
290    if (myBundle.getTypeElement().isEmpty() && theBundleType != null) {
291      myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
292    }
293
294    if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) {
295      myBundle.getTotalElement().setValue(theTotalResults);
296    }
297  }
298
299  private void ensureBundle() {
300    if (myBundle == null) {
301      myBundle = new Bundle();
302    }
303  }
304
305  @Override
306  public IBaseResource getResourceBundle() {
307    return myBundle;
308  }
309
310  private boolean hasLink(String theLinkType, Bundle theBundle) {
311    for (BundleLinkComponent next : theBundle.getLink()) {
312      if (theLinkType.equals(next.getRelation())) {
313        return true;
314      }
315    }
316    return false;
317  }
318
319  @Override
320  public void initializeBundleFromResourceList(String theAuthor, List<? extends IBaseResource> theResources, String theServerBase, String theCompleteUrl, int theTotalResults,
321                                               BundleTypeEnum theBundleType) {
322    myBundle = new Bundle();
323
324    myBundle.setId(UUID.randomUUID().toString());
325
326    myBundle.getMeta().setLastUpdated(new Date());
327
328    myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase);
329    myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl);
330    myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
331
332    if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) {
333      for (IBaseResource nextBaseRes : theResources) {
334        Resource next = (Resource) nextBaseRes;
335        BundleEntryComponent nextEntry = myBundle.addEntry();
336
337        nextEntry.setResource(next);
338        if (next.getIdElement().isEmpty()) {
339          nextEntry.getRequest().setMethod(HTTPVerb.POST);
340        } else {
341          nextEntry.getRequest().setMethod(HTTPVerb.PUT);
342          if (next.getIdElement().isAbsolute()) {
343            nextEntry.getRequest().setUrl(next.getId());
344          } else {
345            String resourceType = myContext.getResourceDefinition(next).getName();
346            nextEntry.getRequest().setUrl(new IdType(theServerBase, resourceType, next.getIdElement().getIdPart(), next.getIdElement().getVersionIdPart()).getValue());
347          }
348        }
349      }
350    } else {
351      addResourcesForSearch(theResources);
352    }
353
354    myBundle.getTotalElement().setValue(theTotalResults);
355  }
356
357  @Override
358  public void initializeWithBundleResource(IBaseResource theBundle) {
359    myBundle = (Bundle) theBundle;
360  }
361
362  private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) {
363    IIdType idElement = null;
364    if (next.getIdElement().hasBaseUrl()) {
365      idElement = next.getIdElement();
366      entry.setFullUrl(idElement.toVersionless().getValue());
367    } else {
368      if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) {
369        idElement = next.getIdElement();
370        idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName());
371        entry.setFullUrl(idElement.toVersionless().getValue());
372      }
373    }
374    return idElement;
375  }
376
377  @Override
378  public List<IBaseResource> toListOfResources() {
379    ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();
380    for (BundleEntryComponent next : myBundle.getEntry()) {
381      if (next.getResource() != null) {
382        retVal.add(next.getResource());
383      } else if (next.getResponse().getLocationElement().isEmpty() == false) {
384        IdType id = new IdType(next.getResponse().getLocation());
385        String resourceType = id.getResourceType();
386        if (isNotBlank(resourceType)) {
387          IAnyResource res = (IAnyResource) myContext.getResourceDefinition(resourceType).newInstance();
388          res.setId(id);
389          retVal.add(res);
390        }
391      }
392    }
393    return retVal;
394  }
395
396}