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    }
127
128    /*
129     * Actually add the resources to the bundle
130     */
131    for (IBaseResource next : includedResources) {
132      BundleEntryComponent entry = myBundle.addEntry();
133      entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
134      if (next.getIdElement().hasBaseUrl()) {
135        entry.setFullUrl(next.getIdElement().getValue());
136      }
137    }
138  }
139
140  @Override
141  public void addResourcesToBundle(List<IBaseResource> theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set<Include> theIncludes) {
142    ensureBundle();
143
144    List<IAnyResource> includedResources = new ArrayList<IAnyResource>();
145    Set<IIdType> addedResourceIds = new HashSet<IIdType>();
146
147    for (IBaseResource next : theResult) {
148      if (next.getIdElement().isEmpty() == false) {
149        addedResourceIds.add(next.getIdElement());
150      }
151    }
152
153    for (IBaseResource next : theResult) {
154
155      Set<String> containedIds = new HashSet<String>();
156
157      if (next instanceof DomainResource) {
158        for (Resource nextContained : ((DomainResource) next).getContained()) {
159          if (isNotBlank(nextContained.getId())) {
160            containedIds.add(nextContained.getId());
161          }
162        }
163      }
164
165      List<ResourceReferenceInfo> references = myContext.newTerser().getAllResourceReferences(next);
166      do {
167        List<IAnyResource> addedResourcesThisPass = new ArrayList<IAnyResource>();
168
169        for (ResourceReferenceInfo nextRefInfo : references) {
170          if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) {
171            continue;
172          }
173
174          IAnyResource nextRes = (IAnyResource) nextRefInfo.getResourceReference().getResource();
175          if (nextRes != null) {
176            if (nextRes.getIdElement().hasIdPart()) {
177              if (containedIds.contains(nextRes.getIdElement().getValue())) {
178                // Don't add contained IDs as top level resources
179                continue;
180              }
181
182              IIdType id = nextRes.getIdElement();
183              if (id.hasResourceType() == false) {
184                String resName = myContext.getResourceDefinition(nextRes).getName();
185                id = id.withResourceType(resName);
186              }
187
188              if (!addedResourceIds.contains(id)) {
189                addedResourceIds.add(id);
190                addedResourcesThisPass.add(nextRes);
191              }
192
193            }
194          }
195        }
196
197        includedResources.addAll(addedResourcesThisPass);
198
199        // Linked resources may themselves have linked resources
200        references = new ArrayList<>();
201        for (IAnyResource iResource : addedResourcesThisPass) {
202          List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
203          references.addAll(newReferences);
204        }
205      } while (references.isEmpty() == false);
206
207      BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next);
208      Resource nextAsResource = (Resource) next;
209      IIdType id = populateBundleEntryFullUrl(next, entry);
210
211      // Populate Request
212      String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextAsResource);
213      if (httpVerb != null) {
214        entry.getRequest().getMethodElement().setValueAsString(httpVerb);
215        if (id != null) {
216          entry.getRequest().setUrl(id.getValue());
217        }
218      }
219
220      // Populate Response
221      if ("1".equals(id.getVersionIdPart())) {
222        entry.getResponse().setStatus("201 Created");
223      } else if (isNotBlank(id.getVersionIdPart())) {
224        entry.getResponse().setStatus("200 OK");
225      }
226      if (isNotBlank(id.getVersionIdPart())) {
227        entry.getResponse().setEtag(RestfulServerUtils.createEtag(id.getVersionIdPart()));
228      }
229
230      String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource);
231      if (searchMode != null) {
232        entry.getSearch().getModeElement().setValueAsString(searchMode);
233      }
234    }
235
236    /*
237     * Actually add the resources to the bundle
238     */
239    for (IAnyResource next : includedResources) {
240      BundleEntryComponent entry = myBundle.addEntry();
241      entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
242      populateBundleEntryFullUrl(next, entry);
243    }
244
245  }
246
247  @Override
248  public void addRootPropertiesToBundle(String theId, String theServerBase, String theLinkSelf, String theLinkPrev, String theLinkNext, Integer theTotalResults, BundleTypeEnum theBundleType,
249                                        IPrimitiveType<Date> theLastUpdated) {
250    ensureBundle();
251
252    myBase = theServerBase;
253
254    if (myBundle.getIdElement().isEmpty()) {
255      myBundle.setId(theId);
256    }
257    if (myBundle.getIdElement().isEmpty()) {
258      myBundle.setId(UUID.randomUUID().toString());
259    }
260
261    if (myBundle.getMeta().getLastUpdated() == null && theLastUpdated != null) {
262      myBundle.getMeta().getLastUpdatedElement().setValueAsString(theLastUpdated.getValueAsString());
263    }
264
265    if (!hasLink(Constants.LINK_SELF, myBundle) && isNotBlank(theLinkSelf)) {
266      myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theLinkSelf);
267    }
268    if (!hasLink(Constants.LINK_NEXT, myBundle) && isNotBlank(theLinkNext)) {
269      myBundle.addLink().setRelation(Constants.LINK_NEXT).setUrl(theLinkNext);
270    }
271    if (!hasLink(Constants.LINK_PREVIOUS, myBundle) && isNotBlank(theLinkPrev)) {
272      myBundle.addLink().setRelation(Constants.LINK_PREVIOUS).setUrl(theLinkPrev);
273    }
274
275    if (myBundle.getTypeElement().isEmpty() && theBundleType != null) {
276      myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
277    }
278
279    if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) {
280      myBundle.getTotalElement().setValue(theTotalResults);
281    }
282  }
283
284  private void ensureBundle() {
285    if (myBundle == null) {
286      myBundle = new Bundle();
287    }
288  }
289
290  @Override
291  public IBaseResource getResourceBundle() {
292    return myBundle;
293  }
294
295  private boolean hasLink(String theLinkType, Bundle theBundle) {
296    for (BundleLinkComponent next : theBundle.getLink()) {
297      if (theLinkType.equals(next.getRelation())) {
298        return true;
299      }
300    }
301    return false;
302  }
303
304  @Override
305  public void initializeBundleFromResourceList(String theAuthor, List<? extends IBaseResource> theResources, String theServerBase, String theCompleteUrl, int theTotalResults,
306                                               BundleTypeEnum theBundleType) {
307    myBundle = new Bundle();
308
309    myBundle.setId(UUID.randomUUID().toString());
310
311    myBundle.getMeta().setLastUpdated(new Date());
312
313    myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase);
314    myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl);
315    myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
316
317    if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) {
318      for (IBaseResource nextBaseRes : theResources) {
319        Resource next = (Resource) nextBaseRes;
320        BundleEntryComponent nextEntry = myBundle.addEntry();
321
322        nextEntry.setResource(next);
323        if (next.getIdElement().isEmpty()) {
324          nextEntry.getRequest().setMethod(HTTPVerb.POST);
325        } else {
326          nextEntry.getRequest().setMethod(HTTPVerb.PUT);
327          if (next.getIdElement().isAbsolute()) {
328            nextEntry.getRequest().setUrl(next.getId());
329          } else {
330            String resourceType = myContext.getResourceDefinition(next).getName();
331            nextEntry.getRequest().setUrl(new IdType(theServerBase, resourceType, next.getIdElement().getIdPart(), next.getIdElement().getVersionIdPart()).getValue());
332          }
333        }
334      }
335    } else {
336      addResourcesForSearch(theResources);
337    }
338
339    myBundle.getTotalElement().setValue(theTotalResults);
340  }
341
342  @Override
343  public void initializeWithBundleResource(IBaseResource theBundle) {
344    myBundle = (Bundle) theBundle;
345  }
346
347  private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) {
348    IIdType idElement = null;
349    if (next.getIdElement().hasBaseUrl()) {
350      idElement = next.getIdElement();
351      entry.setFullUrl(idElement.toVersionless().getValue());
352    } else {
353      if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) {
354        idElement = next.getIdElement();
355        idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName());
356        entry.setFullUrl(idElement.toVersionless().getValue());
357      }
358    }
359    return idElement;
360  }
361
362  @Override
363  public List<IBaseResource> toListOfResources() {
364    ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();
365    for (BundleEntryComponent next : myBundle.getEntry()) {
366      if (next.getResource() != null) {
367        retVal.add(next.getResource());
368      } else if (next.getResponse().getLocationElement().isEmpty() == false) {
369        IdType id = new IdType(next.getResponse().getLocation());
370        String resourceType = id.getResourceType();
371        if (isNotBlank(resourceType)) {
372          IAnyResource res = (IAnyResource) myContext.getResourceDefinition(resourceType).newInstance();
373          res.setId(id);
374          retVal.add(res);
375        }
376      }
377    }
378    return retVal;
379  }
380
381}