001package org.hl7.fhir.dstu3.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 */
022import static org.apache.commons.lang3.StringUtils.isNotBlank;
023
024import java.util.*;
025
026import org.apache.commons.lang3.Validate;
027import org.hl7.fhir.dstu3.model.Bundle;
028import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
029import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent;
030import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
031import org.hl7.fhir.dstu3.model.Bundle.SearchEntryMode;
032import org.hl7.fhir.dstu3.model.DomainResource;
033import org.hl7.fhir.dstu3.model.IdType;
034import org.hl7.fhir.dstu3.model.Resource;
035import org.hl7.fhir.instance.model.api.*;
036
037import ca.uhn.fhir.context.FhirContext;
038import ca.uhn.fhir.model.api.Include;
039import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
040import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
041import ca.uhn.fhir.model.valueset.BundleTypeEnum;
042import ca.uhn.fhir.rest.server.*;
043import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
044import ca.uhn.fhir.util.ResourceReferenceInfo;
045
046public class Dstu3BundleFactory implements IVersionSpecificBundleFactory {
047
048  private Bundle myBundle;
049  private FhirContext myContext;
050  private String myBase;
051
052  public Dstu3BundleFactory(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<IAnyResource>();
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<IBaseReference>();
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    if (myBundle == null) {
143      myBundle = new Bundle();
144    }
145
146    List<IAnyResource> includedResources = new ArrayList<IAnyResource>();
147    Set<IIdType> addedResourceIds = new HashSet<IIdType>();
148
149    for (IBaseResource next : theResult) {
150      if (next.getIdElement().isEmpty() == false) {
151        addedResourceIds.add(next.getIdElement());
152      }
153    }
154
155    for (IBaseResource next : theResult) {
156
157      Set<String> containedIds = new HashSet<String>();
158
159      if (next instanceof DomainResource) {
160        for (Resource nextContained : ((DomainResource) next).getContained()) {
161          if (isNotBlank(nextContained.getId())) {
162            containedIds.add(nextContained.getId());
163          }
164        }
165      }
166
167      List<ResourceReferenceInfo> references = myContext.newTerser().getAllResourceReferences(next);
168      do {
169        List<IAnyResource> addedResourcesThisPass = new ArrayList<IAnyResource>();
170
171        for (ResourceReferenceInfo nextRefInfo : references) {
172          if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) {
173            continue;
174          }
175
176          IAnyResource nextRes = (IAnyResource) nextRefInfo.getResourceReference().getResource();
177          if (nextRes != null) {
178            if (nextRes.getIdElement().hasIdPart()) {
179              if (containedIds.contains(nextRes.getIdElement().getValue())) {
180                // Don't add contained IDs as top level resources
181                continue;
182              }
183
184              IIdType id = nextRes.getIdElement();
185              if (id.hasResourceType() == false) {
186                String resName = myContext.getResourceDefinition(nextRes).getName();
187                id = id.withResourceType(resName);
188              }
189
190              if (!addedResourceIds.contains(id)) {
191                addedResourceIds.add(id);
192                addedResourcesThisPass.add(nextRes);
193              }
194
195            }
196          }
197        }
198
199        includedResources.addAll(addedResourcesThisPass);
200
201        // Linked resources may themselves have linked resources
202        references = new ArrayList<ResourceReferenceInfo>();
203        for (IAnyResource iResource : addedResourcesThisPass) {
204          List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
205          references.addAll(newReferences);
206        }
207      } while (references.isEmpty() == false);
208
209      BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next);
210      Resource nextAsResource = (Resource) next;
211      IIdType id = populateBundleEntryFullUrl(next, entry);
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      String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource);
221      if (searchMode != null) {
222        entry.getSearch().getModeElement().setValueAsString(searchMode);
223      }
224    }
225
226    /*
227     * Actually add the resources to the bundle
228     */
229    for (IAnyResource next : includedResources) {
230      BundleEntryComponent entry = myBundle.addEntry();
231      entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
232      populateBundleEntryFullUrl(next, entry);
233    }
234
235  }
236
237  private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) {
238    IIdType idElement = null;
239    if (next.getIdElement().hasBaseUrl()) {
240      idElement = next.getIdElement();
241      entry.setFullUrl(idElement.toVersionless().getValue());
242    } else {
243      if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) {
244        idElement = next.getIdElement();
245        idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName());
246        entry.setFullUrl(idElement.toVersionless().getValue());
247      }
248    }
249    return idElement;
250  }
251
252  @Override
253  public void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType<Date> theLastUpdated) {
254
255    myBase = theServerBase;
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(theCompleteUrl)) {
266      myBundle.addLink().setRelation("self").setUrl(theCompleteUrl);
267    }
268
269    if (myBundle.getTypeElement().isEmpty() && theBundleType != null) {
270      myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
271    }
272
273    if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) {
274      myBundle.getTotalElement().setValue(theTotalResults);
275    }
276  }
277
278  @Override
279  public ca.uhn.fhir.model.api.Bundle getDstu1Bundle() {
280    return null;
281  }
282
283  @Override
284  public IBaseResource getResourceBundle() {
285    return myBundle;
286  }
287
288  private boolean hasLink(String theLinkType, Bundle theBundle) {
289    for (BundleLinkComponent next : theBundle.getLink()) {
290      if (theLinkType.equals(next.getRelation())) {
291        return true;
292      }
293    }
294    return false;
295  }
296
297  @Override
298  public void initializeBundleFromBundleProvider(IRestfulServer<?> theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl,
299      boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set<Include> theIncludes) {
300    myBase = theServerBase;
301
302    int numToReturn;
303    String searchId = null;
304    List<IBaseResource> resourceList;
305    Integer numTotalResults = theResult.size();
306    if (theServer.getPagingProvider() == null) {
307      numToReturn = numTotalResults;
308      if (numToReturn > 0) {
309        resourceList = theResult.getResources(0, numToReturn);
310      } else {
311        resourceList = Collections.emptyList();
312      }
313      RestfulServerUtils.validateResourceListNotNull(resourceList);
314
315    } else {
316      IPagingProvider pagingProvider = theServer.getPagingProvider();
317      if (theLimit == null || theLimit.equals(Integer.valueOf(0))) {
318        numToReturn = pagingProvider.getDefaultPageSize();
319      } else {
320        numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit);
321      }
322
323      if (numTotalResults != null) {
324        numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
325      }
326
327      if (numToReturn > 0) {
328        resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
329      } else {
330        resourceList = Collections.emptyList();
331      }
332      RestfulServerUtils.validateResourceListNotNull(resourceList);
333
334      if (theSearchId != null) {
335        searchId = theSearchId;
336      } else {
337        if (numTotalResults == null || numTotalResults > numToReturn) {
338          searchId = pagingProvider.storeResultList(theResult);
339          Validate.notNull(searchId, "Paging provider returned null searchId");
340        }
341      }
342    }
343
344    for (IBaseResource next : resourceList) {
345      if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
346        if (!(next instanceof BaseOperationOutcome)) {
347          throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)");
348        }
349      }
350    }
351
352    addResourcesToBundle(new ArrayList<IBaseResource>(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes);
353    addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished());
354
355    if (theServer.getPagingProvider() != null) {
356      int limit;
357      limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize();
358      limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize());
359
360      if (searchId != null) {
361        if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
362          myBundle.addLink().setRelation(Constants.LINK_NEXT)
363              .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType));
364        }
365        if (theOffset > 0) {
366          int start = Math.max(0, theOffset - limit);
367          myBundle.addLink().setRelation(Constants.LINK_PREVIOUS)
368              .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, start, limit, theResponseEncoding, thePrettyPrint, theBundleType));
369        }
370      }
371    }
372  }
373
374  @Override
375  public void initializeBundleFromResourceList(String theAuthor, List<? extends IBaseResource> theResources, String theServerBase, String theCompleteUrl, int theTotalResults,
376      BundleTypeEnum theBundleType) {
377    myBundle = new Bundle();
378
379    myBundle.setId(UUID.randomUUID().toString());
380
381    myBundle.getMeta().setLastUpdated(new Date());
382
383    myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase);
384    myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl);
385    myBundle.getTypeElement().setValueAsString(theBundleType.getCode());
386
387    if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) {
388      for (IBaseResource nextBaseRes : theResources) {
389        Resource next = (Resource) nextBaseRes;
390        BundleEntryComponent nextEntry = myBundle.addEntry();
391
392        nextEntry.setResource(next);
393        if (next.getIdElement().isEmpty()) {
394          nextEntry.getRequest().setMethod(HTTPVerb.POST);
395        } else {
396          nextEntry.getRequest().setMethod(HTTPVerb.PUT);
397          if (next.getIdElement().isAbsolute()) {
398            nextEntry.getRequest().setUrl(next.getId());
399          } else {
400            String resourceType = myContext.getResourceDefinition(next).getName();
401            nextEntry.getRequest().setUrl(new IdType(theServerBase, resourceType, next.getIdElement().getIdPart(), next.getIdElement().getVersionIdPart()).getValue());
402          }
403        }
404      }
405    } else {
406      addResourcesForSearch(theResources);
407    }
408
409    myBundle.getTotalElement().setValue(theTotalResults);
410  }
411
412  @Override
413  public void initializeWithBundleResource(IBaseResource theBundle) {
414    myBundle = (Bundle) theBundle;
415  }
416
417  @Override
418  public List<IBaseResource> toListOfResources() {
419    ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();
420    for (BundleEntryComponent next : myBundle.getEntry()) {
421      if (next.getResource() != null) {
422        retVal.add(next.getResource());
423      } else if (next.getResponse().getLocationElement().isEmpty() == false) {
424        IdType id = new IdType(next.getResponse().getLocation());
425        String resourceType = id.getResourceType();
426        if (isNotBlank(resourceType)) {
427          IAnyResource res = (IAnyResource) myContext.getResourceDefinition(resourceType).newInstance();
428          res.setId(id);
429          retVal.add(res);
430        }
431      }
432    }
433    return retVal;
434  }
435
436}