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