001package ca.uhn.fhir.rest.server.provider.dstu2; 002 003/* 004 * #%L 005 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) 006 * %% 007 * Copyright (C) 2014 - 2016 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.ArrayList; 025import java.util.Collections; 026import java.util.Date; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Set; 030import java.util.UUID; 031 032import org.apache.commons.lang3.Validate; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034import org.hl7.fhir.instance.model.api.IIdType; 035import org.hl7.fhir.instance.model.api.IPrimitiveType; 036 037import ca.uhn.fhir.context.FhirContext; 038import ca.uhn.fhir.context.RuntimeResourceDefinition; 039import ca.uhn.fhir.model.api.IResource; 040import ca.uhn.fhir.model.api.Include; 041import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 042import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; 043import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; 044import ca.uhn.fhir.model.dstu2.resource.Bundle; 045import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; 046import ca.uhn.fhir.model.dstu2.resource.Bundle.Link; 047import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; 048import ca.uhn.fhir.model.dstu2.valueset.SearchEntryModeEnum; 049import ca.uhn.fhir.model.primitive.IdDt; 050import ca.uhn.fhir.model.primitive.InstantDt; 051import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; 052import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; 053import ca.uhn.fhir.model.valueset.BundleTypeEnum; 054import ca.uhn.fhir.rest.server.AddProfileTagEnum; 055import ca.uhn.fhir.rest.server.BundleInclusionRule; 056import ca.uhn.fhir.rest.server.Constants; 057import ca.uhn.fhir.rest.server.EncodingEnum; 058import ca.uhn.fhir.rest.server.IBundleProvider; 059import ca.uhn.fhir.rest.server.IPagingProvider; 060import ca.uhn.fhir.rest.server.IRestfulServer; 061import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; 062import ca.uhn.fhir.rest.server.RestfulServerUtils; 063import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 064import ca.uhn.fhir.util.ResourceReferenceInfo; 065 066public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { 067 068 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Dstu2BundleFactory.class); 069 private Bundle myBundle; 070 private FhirContext myContext; 071 private String myBase; 072 073 public Dstu2BundleFactory(FhirContext theContext) { 074 myContext = theContext; 075 } 076 077 private void addResourcesForSearch(List<? extends IBaseResource> theResult) { 078 List<IBaseResource> includedResources = new ArrayList<IBaseResource>(); 079 Set<IIdType> addedResourceIds = new HashSet<IIdType>(); 080 081 for (IBaseResource next : theResult) { 082 if (next.getIdElement().isEmpty() == false) { 083 addedResourceIds.add(next.getIdElement()); 084 } 085 } 086 087 for (IBaseResource nextBaseRes : theResult) { 088 IResource next = (IResource) nextBaseRes; 089 Set<String> containedIds = new HashSet<String>(); 090 for (IResource nextContained : next.getContained().getContainedResources()) { 091 if (nextContained.getId().isEmpty() == false) { 092 containedIds.add(nextContained.getId().getValue()); 093 } 094 } 095 096 List<BaseResourceReferenceDt> references = myContext.newTerser().getAllPopulatedChildElementsOfType(next, BaseResourceReferenceDt.class); 097 do { 098 List<IResource> addedResourcesThisPass = new ArrayList<IResource>(); 099 100 for (BaseResourceReferenceDt nextRef : references) { 101 IResource nextRes = (IResource) nextRef.getResource(); 102 if (nextRes != null) { 103 if (nextRes.getId().hasIdPart()) { 104 if (containedIds.contains(nextRes.getId().getValue())) { 105 // Don't add contained IDs as top level resources 106 continue; 107 } 108 109 IdDt id = nextRes.getId(); 110 if (id.hasResourceType() == false) { 111 String resName = myContext.getResourceDefinition(nextRes).getName(); 112 id = id.withResourceType(resName); 113 } 114 115 if (!addedResourceIds.contains(id)) { 116 addedResourceIds.add(id); 117 addedResourcesThisPass.add(nextRes); 118 } 119 120 } 121 } 122 } 123 124 // Linked resources may themselves have linked resources 125 references = new ArrayList<BaseResourceReferenceDt>(); 126 for (IResource iResource : addedResourcesThisPass) { 127 List<BaseResourceReferenceDt> newReferences = myContext.newTerser().getAllPopulatedChildElementsOfType(iResource, BaseResourceReferenceDt.class); 128 references.addAll(newReferences); 129 } 130 131 includedResources.addAll(addedResourcesThisPass); 132 133 } while (references.isEmpty() == false); 134 135 Entry entry = myBundle.addEntry().setResource(next); 136 if (next.getId().hasBaseUrl()) { 137 entry.setFullUrl(next.getId().getValue()); 138 } 139 BundleEntryTransactionMethodEnum httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next); 140 if (httpVerb != null) { 141 entry.getRequest().getMethodElement().setValueAsString(httpVerb.getCode()); 142 } 143 } 144 145 /* 146 * Actually add the resources to the bundle 147 */ 148 for (IBaseResource next : includedResources) { 149 Entry entry = myBundle.addEntry(); 150 entry.setResource((IResource) next).getSearch().setMode(SearchEntryModeEnum.INCLUDE); 151 if (next.getIdElement().hasBaseUrl()) { 152 entry.setFullUrl(next.getIdElement().getValue()); 153 } 154 } 155 } 156 157 @Override 158 public void addResourcesToBundle(List<IBaseResource> theResult, BundleTypeEnum theBundleType, String theServerBase, BundleInclusionRule theBundleInclusionRule, Set<Include> theIncludes) { 159 if (myBundle == null) { 160 myBundle = new Bundle(); 161 } 162 163 List<IResource> includedResources = new ArrayList<IResource>(); 164 Set<IdDt> addedResourceIds = new HashSet<IdDt>(); 165 166 for (IBaseResource next : theResult) { 167 if (next.getIdElement().isEmpty() == false) { 168 addedResourceIds.add((IdDt) next.getIdElement()); 169 } 170 } 171 172 for (IBaseResource nextBaseRes : theResult) { 173 IResource next = (IResource) nextBaseRes; 174 175 Set<String> containedIds = new HashSet<String>(); 176 for (IResource nextContained : next.getContained().getContainedResources()) { 177 if (nextContained.getId().isEmpty() == false) { 178 containedIds.add(nextContained.getId().getValue()); 179 } 180 } 181 182 List<ResourceReferenceInfo> references = myContext.newTerser().getAllResourceReferences(next); 183 do { 184 List<IResource> addedResourcesThisPass = new ArrayList<IResource>(); 185 186 for (ResourceReferenceInfo nextRefInfo : references) { 187 if (!theBundleInclusionRule.shouldIncludeReferencedResource(nextRefInfo, theIncludes)) 188 continue; 189 190 IResource nextRes = (IResource) nextRefInfo.getResourceReference().getResource(); 191 if (nextRes != null) { 192 if (nextRes.getId().hasIdPart()) { 193 if (containedIds.contains(nextRes.getId().getValue())) { 194 // Don't add contained IDs as top level resources 195 continue; 196 } 197 198 IdDt id = nextRes.getId(); 199 if (id.hasResourceType() == false) { 200 String resName = myContext.getResourceDefinition(nextRes).getName(); 201 id = id.withResourceType(resName); 202 } 203 204 if (!addedResourceIds.contains(id)) { 205 addedResourceIds.add(id); 206 addedResourcesThisPass.add(nextRes); 207 } 208 209 } 210 } 211 } 212 213 includedResources.addAll(addedResourcesThisPass); 214 215 // Linked resources may themselves have linked resources 216 references = new ArrayList<ResourceReferenceInfo>(); 217 for (IResource iResource : addedResourcesThisPass) { 218 List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource); 219 references.addAll(newReferences); 220 } 221 } while (references.isEmpty() == false); 222 223 Entry entry = myBundle.addEntry().setResource(next); 224 BundleEntryTransactionMethodEnum httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next); 225 if (httpVerb != null) { 226 entry.getRequest().getMethodElement().setValueAsString(httpVerb.getCode()); 227 } 228 populateBundleEntryFullUrl(next, entry); 229 230 BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next); 231 if (searchMode != null) { 232 entry.getSearch().getModeElement().setValue(searchMode.getCode()); 233 } 234 } 235 236 /* 237 * Actually add the resources to the bundle 238 */ 239 for (IResource next : includedResources) { 240 Entry entry = myBundle.addEntry(); 241 entry.setResource(next).getSearch().setMode(SearchEntryModeEnum.INCLUDE); 242 populateBundleEntryFullUrl(next, entry); 243 } 244 245 } 246 247 private void populateBundleEntryFullUrl(IResource next, Entry entry) { 248 if (next.getId().hasBaseUrl()) { 249 entry.setFullUrl(next.getId().toVersionless().getValue()); 250 } else { 251 if (isNotBlank(myBase) && next.getId().hasIdPart()) { 252 IdDt id = next.getId().toVersionless(); 253 id = id.withServerBase(myBase, myContext.getResourceDefinition(next).getName()); 254 entry.setFullUrl(id.getValue()); 255 } 256 } 257 } 258 259 @Override 260 public void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType<Date> theLastUpdated) { 261 262 myBase = theServerBase; 263 264 if (myBundle.getId().isEmpty()) { 265 myBundle.setId(UUID.randomUUID().toString()); 266 } 267 268 if (ResourceMetadataKeyEnum.UPDATED.get(myBundle) == null) { 269 ResourceMetadataKeyEnum.UPDATED.put(myBundle, (InstantDt) theLastUpdated); 270 } 271 272 if (!hasLink(Constants.LINK_SELF, myBundle) && isNotBlank(theCompleteUrl)) { 273 myBundle.addLink().setRelation("self").setUrl(theCompleteUrl); 274 } 275 276 if (myBundle.getTypeElement().isEmpty() && theBundleType != null) { 277 myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); 278 } 279 280 if (myBundle.getTotalElement().isEmpty() && theTotalResults != null) { 281 myBundle.getTotalElement().setValue(theTotalResults); 282 } 283 } 284 285 @Override 286 public ca.uhn.fhir.model.api.Bundle getDstu1Bundle() { 287 return null; 288 } 289 290 @Override 291 public IResource getResourceBundle() { 292 return myBundle; 293 } 294 295 private boolean hasLink(String theLinkType, Bundle theBundle) { 296 for (Link next : theBundle.getLink()) { 297 if (theLinkType.equals(next.getRelation())) { 298 return true; 299 } 300 } 301 return false; 302 } 303 304 @Override 305 public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, 306 boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set<Include> theIncludes) { 307 myBase = theServerBase; 308 309 int numToReturn; 310 String searchId = null; 311 List<IBaseResource> resourceList; 312 if (theServer.getPagingProvider() == null) { 313 numToReturn = theResult.size(); 314 if (numToReturn > 0) { 315 resourceList = theResult.getResources(0, numToReturn); 316 } else { 317 resourceList = Collections.emptyList(); 318 } 319 RestfulServerUtils.validateResourceListNotNull(resourceList); 320 321 } else { 322 IPagingProvider pagingProvider = theServer.getPagingProvider(); 323 if (theLimit == null) { 324 numToReturn = pagingProvider.getDefaultPageSize(); 325 } else { 326 numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); 327 } 328 329 numToReturn = Math.min(numToReturn, theResult.size() - theOffset); 330 if (numToReturn > 0) { 331 resourceList = theResult.getResources(theOffset, numToReturn + theOffset); 332 } else { 333 resourceList = Collections.emptyList(); 334 } 335 RestfulServerUtils.validateResourceListNotNull(resourceList); 336 337 if (theSearchId != null) { 338 searchId = theSearchId; 339 } else { 340 if (theResult.size() > numToReturn) { 341 searchId = pagingProvider.storeResultList(theResult); 342 Validate.notNull(searchId, "Paging provider returned null searchId"); 343 } 344 } 345 } 346 347 for (IBaseResource next : resourceList) { 348 if (next.getIdElement() == null || next.getIdElement().isEmpty()) { 349 if (!(next instanceof BaseOperationOutcome)) { 350 throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); 351 } 352 } 353 } 354 355 if (theServer.getAddProfileTag() != AddProfileTagEnum.NEVER) { 356 for (IBaseResource nextRes : resourceList) { 357 RuntimeResourceDefinition def = theServer.getFhirContext().getResourceDefinition(nextRes); 358 if (theServer.getAddProfileTag() == AddProfileTagEnum.ALWAYS || !def.isStandardProfile()) { 359 RestfulServerUtils.addProfileToBundleEntry(theServer.getFhirContext(), nextRes, theServerBase); 360 } 361 } 362 } 363 364 addResourcesToBundle(new ArrayList<IBaseResource>(resourceList), theBundleType, theServerBase, theServer.getBundleInclusionRule(), theIncludes); 365 addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); 366 367 if (theServer.getPagingProvider() != null) { 368 int limit; 369 limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); 370 limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); 371 372 if (searchId != null) { 373 if (theOffset + numToReturn < theResult.size()) { 374 myBundle.addLink().setRelation(Constants.LINK_NEXT) 375 .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, theOffset + numToReturn, numToReturn, theResponseEncoding, thePrettyPrint, theBundleType)); 376 } 377 if (theOffset > 0) { 378 int start = Math.max(0, theOffset - limit); 379 myBundle.addLink().setRelation(Constants.LINK_PREVIOUS) 380 .setUrl(RestfulServerUtils.createPagingLink(theIncludes, theServerBase, searchId, start, limit, theResponseEncoding, thePrettyPrint, theBundleType)); 381 } 382 } 383 } 384 } 385 386 @Override 387 public void initializeBundleFromResourceList(String theAuthor, List<? extends IBaseResource> theResources, String theServerBase, String theCompleteUrl, int theTotalResults, 388 BundleTypeEnum theBundleType) { 389 myBundle = new Bundle(); 390 391 myBundle.setId(UUID.randomUUID().toString()); 392 393 ResourceMetadataKeyEnum.PUBLISHED.put(myBundle, InstantDt.withCurrentTime()); 394 395 myBundle.addLink().setRelation(Constants.LINK_FHIR_BASE).setUrl(theServerBase); 396 myBundle.addLink().setRelation(Constants.LINK_SELF).setUrl(theCompleteUrl); 397 myBundle.getTypeElement().setValueAsString(theBundleType.getCode()); 398 399 if (theBundleType.equals(BundleTypeEnum.TRANSACTION)) { 400 for (IBaseResource nextBaseRes : theResources) { 401 IResource next = (IResource) nextBaseRes; 402 Entry nextEntry = myBundle.addEntry(); 403 404 nextEntry.setResource(next); 405 if (next.getId().isEmpty()) { 406 nextEntry.getRequest().setMethod(HTTPVerbEnum.POST); 407 } else { 408 nextEntry.getRequest().setMethod(HTTPVerbEnum.PUT); 409 if (next.getId().isAbsolute()) { 410 nextEntry.getRequest().setUrl(next.getId()); 411 } else { 412 String resourceType = myContext.getResourceDefinition(next).getName(); 413 nextEntry.getRequest().setUrl(new IdDt(theServerBase, resourceType, next.getId().getIdPart(), next.getId().getVersionIdPart()).getValue()); 414 } 415 } 416 } 417 } else { 418 addResourcesForSearch(theResources); 419 } 420 421 myBundle.getTotalElement().setValue(theTotalResults); 422 } 423 424 @Override 425 public void initializeWithBundleResource(IBaseResource theBundle) { 426 myBundle = (Bundle) theBundle; 427 } 428 429 @Override 430 public List<IBaseResource> toListOfResources() { 431 ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>(); 432 for (Entry next : myBundle.getEntry()) { 433 if (next.getResource() != null) { 434 retVal.add(next.getResource()); 435 } else if (next.getResponse().getLocationElement().isEmpty() == false) { 436 IdDt id = new IdDt(next.getResponse().getLocation()); 437 String resourceType = id.getResourceType(); 438 if (isNotBlank(resourceType)) { 439 IResource res = (IResource) myContext.getResourceDefinition(resourceType).newInstance(); 440 res.setId(id); 441 retVal.add(res); 442 } 443 } 444 } 445 return retVal; 446 } 447 448}