001/*- 002 * #%L 003 * HAPI FHIR JPA Server - International Patient Summary (IPS) 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.ips.generator; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; 024import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; 025import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; 026import ca.uhn.fhir.jpa.ips.api.IpsContext; 027import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; 028import ca.uhn.fhir.jpa.ips.api.Section; 029import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; 030import ca.uhn.fhir.rest.api.server.RequestDetails; 031import ca.uhn.fhir.rest.param.TokenParam; 032import ca.uhn.fhir.util.BundleBuilder; 033import ca.uhn.fhir.util.CompositionBuilder; 034import ca.uhn.fhir.util.ResourceReferenceInfo; 035import com.google.common.collect.BiMap; 036import com.google.common.collect.HashBiMap; 037import jakarta.annotation.Nonnull; 038import jakarta.annotation.Nullable; 039import org.apache.commons.lang3.Validate; 040import org.hl7.fhir.instance.model.api.IBase; 041import org.hl7.fhir.instance.model.api.IBaseBundle; 042import org.hl7.fhir.instance.model.api.IBaseExtension; 043import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 044import org.hl7.fhir.instance.model.api.IBaseResource; 045import org.hl7.fhir.instance.model.api.IIdType; 046import org.hl7.fhir.instance.model.api.IPrimitiveType; 047import org.hl7.fhir.r4.model.Bundle; 048import org.hl7.fhir.r4.model.Composition; 049import org.hl7.fhir.r4.model.IdType; 050import org.hl7.fhir.r4.model.InstantType; 051import org.hl7.fhir.r4.model.Resource; 052 053import java.util.ArrayList; 054import java.util.HashMap; 055import java.util.List; 056import java.util.Map; 057import java.util.UUID; 058 059import static java.util.Objects.requireNonNull; 060import static org.apache.commons.lang3.StringUtils.isBlank; 061import static org.apache.commons.lang3.StringUtils.isNotBlank; 062 063public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { 064 065 public static final String RESOURCE_ENTRY_INCLUSION_TYPE = "RESOURCE_ENTRY_INCLUSION_TYPE"; 066 public static final String URL_NARRATIVE_LINK = "http://hl7.org/fhir/StructureDefinition/narrativeLink"; 067 private final List<IIpsGenerationStrategy> myGenerationStrategies; 068 private final FhirContext myFhirContext; 069 070 /** 071 * Constructor 072 */ 073 public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { 074 this(theFhirContext, List.of(theGenerationStrategy)); 075 } 076 077 public IpsGeneratorSvcImpl(FhirContext theFhirContext, List<IIpsGenerationStrategy> theIpsGenerationStrategies) { 078 myGenerationStrategies = theIpsGenerationStrategies; 079 myFhirContext = theFhirContext; 080 081 myGenerationStrategies.forEach(IIpsGenerationStrategy::initialize); 082 } 083 084 /** 085 * Generate an IPS using a patient ID 086 */ 087 @Override 088 public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile) { 089 IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); 090 IBaseResource patient = strategy.fetchPatient(thePatientId, theRequestDetails); 091 return generateIpsForPatient(strategy, theRequestDetails, patient); 092 } 093 094 /** 095 * Generate an IPS using a patient identifier 096 */ 097 @Override 098 public IBaseBundle generateIps( 099 RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile) { 100 IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); 101 IBaseResource patient = strategy.fetchPatient(thePatientIdentifier, theRequestDetails); 102 return generateIpsForPatient(strategy, theRequestDetails, patient); 103 } 104 105 IIpsGenerationStrategy selectGenerationStrategy(@Nullable String theRequestedProfile) { 106 return myGenerationStrategies.stream() 107 .filter(t -> isBlank(theRequestedProfile) || theRequestedProfile.equals(t.getBundleProfile())) 108 .findFirst() 109 .orElse(myGenerationStrategies.get(0)); 110 } 111 112 private IBaseBundle generateIpsForPatient( 113 IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, IBaseResource thePatient) { 114 IIdType originalSubjectId = myFhirContext 115 .getVersion() 116 .newIdType() 117 .setValue(thePatient.getIdElement().getValue()) 118 .toUnqualifiedVersionless(); 119 massageResourceId(theStrategy, theRequestDetails, null, thePatient); 120 IpsContext context = new IpsContext(thePatient, originalSubjectId); 121 122 ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection(); 123 globalResourcesToInclude.addResourceIfNotAlreadyPresent(thePatient, originalSubjectId.getValue()); 124 125 IBaseResource author = theStrategy.createAuthor(); 126 massageResourceId(theStrategy, theRequestDetails, context, author); 127 128 CompositionBuilder compositionBuilder = createComposition(theStrategy, thePatient, context, author); 129 determineInclusions(theStrategy, theRequestDetails, context, compositionBuilder, globalResourcesToInclude); 130 131 IBaseResource composition = compositionBuilder.getComposition(); 132 133 // Create the narrative for the Composition itself 134 CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, globalResourcesToInclude); 135 generator.populateResourceNarrative(myFhirContext, composition); 136 137 return createDocumentBundleForComposition(theStrategy, author, composition, globalResourcesToInclude); 138 } 139 140 private IBaseBundle createDocumentBundleForComposition( 141 IIpsGenerationStrategy theStrategy, 142 IBaseResource author, 143 IBaseResource composition, 144 ResourceInclusionCollection theResourcesToInclude) { 145 BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); 146 bundleBuilder.setType(Bundle.BundleType.DOCUMENT.toCode()); 147 bundleBuilder.setIdentifier("urn:ietf:rfc:4122", UUID.randomUUID().toString()); 148 bundleBuilder.setTimestamp(InstantType.now()); 149 150 // Add composition to document 151 bundleBuilder.addDocumentEntry(composition); 152 153 // Add inclusion candidates 154 for (IBaseResource next : theResourcesToInclude.getResources()) { 155 bundleBuilder.addDocumentEntry(next); 156 } 157 158 // Add author to document 159 bundleBuilder.addDocumentEntry(author); 160 161 IBaseBundle retVal = bundleBuilder.getBundle(); 162 163 theStrategy.postManipulateIpsBundle(retVal); 164 165 return retVal; 166 } 167 168 private void determineInclusions( 169 IIpsGenerationStrategy theStrategy, 170 RequestDetails theRequestDetails, 171 IpsContext theIpsContext, 172 CompositionBuilder theCompositionBuilder, 173 ResourceInclusionCollection theGlobalResourcesToInclude) { 174 for (Section nextSection : theStrategy.getSections()) { 175 determineInclusionsForSection( 176 theStrategy, 177 theRequestDetails, 178 theIpsContext, 179 theCompositionBuilder, 180 theGlobalResourcesToInclude, 181 nextSection); 182 } 183 } 184 185 private void determineInclusionsForSection( 186 IIpsGenerationStrategy theStrategy, 187 RequestDetails theRequestDetails, 188 IpsContext theIpsContext, 189 CompositionBuilder theCompositionBuilder, 190 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 191 Section theSection) { 192 ResourceInclusionCollection sectionResourceCollectionToPopulate = new ResourceInclusionCollection(); 193 ISectionResourceSupplier resourceSupplier = theStrategy.getSectionResourceSupplier(theSection); 194 195 determineInclusionsForSectionResourceTypes( 196 theStrategy, 197 theRequestDetails, 198 theIpsContext, 199 theGlobalResourceCollectionToPopulate, 200 theSection, 201 resourceSupplier, 202 sectionResourceCollectionToPopulate); 203 204 generateSectionNoInfoResourceIfNoInclusionsFound( 205 theIpsContext, theGlobalResourceCollectionToPopulate, theSection, sectionResourceCollectionToPopulate); 206 207 /* 208 * Update any references within the added candidates - This is important 209 * because we might be replacing resource IDs before including them in 210 * the summary, so we need to also update the references to those 211 * resources. 212 */ 213 updateReferencesInInclusionsForSection(theGlobalResourceCollectionToPopulate); 214 215 if (sectionResourceCollectionToPopulate.isEmpty()) { 216 return; 217 } 218 219 addSection( 220 theStrategy, 221 theSection, 222 theCompositionBuilder, 223 sectionResourceCollectionToPopulate, 224 theGlobalResourceCollectionToPopulate); 225 } 226 227 private void updateReferencesInInclusionsForSection( 228 ResourceInclusionCollection theGlobalResourceCollectionToPopulate) { 229 for (IBaseResource nextResource : theGlobalResourceCollectionToPopulate.getResources()) { 230 List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(nextResource); 231 for (ResourceReferenceInfo nextReference : references) { 232 String existingReference = nextReference 233 .getResourceReference() 234 .getReferenceElement() 235 .getValue(); 236 if (isNotBlank(existingReference)) { 237 existingReference = new IdType(existingReference) 238 .toUnqualifiedVersionless() 239 .getValue(); 240 String replacement = theGlobalResourceCollectionToPopulate.getIdSubstitution(existingReference); 241 if (isNotBlank(replacement)) { 242 if (!replacement.equals(existingReference)) { 243 nextReference.getResourceReference().setReference(replacement); 244 } 245 } else if (theGlobalResourceCollectionToPopulate.getResourceById(existingReference) == null) { 246 // If this reference doesn't point to something we have actually 247 // included in the bundle, clear the reference. 248 nextReference.getResourceReference().setReference(null); 249 nextReference.getResourceReference().setResource(null); 250 } 251 } 252 } 253 } 254 } 255 256 private static void generateSectionNoInfoResourceIfNoInclusionsFound( 257 IpsContext theIpsContext, 258 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 259 Section theSection, 260 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 261 if (sectionResourceCollectionToPopulate.isEmpty() && theSection.getNoInfoGenerator() != null) { 262 IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); 263 String id = IdType.newRandomUuid().getValue(); 264 if (noInfoResource.getIdElement().isEmpty()) { 265 noInfoResource.setId(id); 266 } 267 noInfoResource.setUserData( 268 RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); 269 theGlobalResourceCollectionToPopulate.addResourceIfNotAlreadyPresent( 270 noInfoResource, 271 noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); 272 sectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(noInfoResource, id); 273 } 274 } 275 276 private void determineInclusionsForSectionResourceTypes( 277 IIpsGenerationStrategy theStrategy, 278 RequestDetails theRequestDetails, 279 IpsContext theIpsContext, 280 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 281 Section theSection, 282 ISectionResourceSupplier resourceSupplier, 283 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 284 for (Class<? extends IBaseResource> nextResourceType : theSection.getResourceTypes()) { 285 determineInclusionsForSectionResourceType( 286 theStrategy, 287 theRequestDetails, 288 theIpsContext, 289 theGlobalResourceCollectionToPopulate, 290 theSection, 291 nextResourceType, 292 resourceSupplier, 293 sectionResourceCollectionToPopulate); 294 } 295 } 296 297 private <T extends IBaseResource> void determineInclusionsForSectionResourceType( 298 IIpsGenerationStrategy theStrategy, 299 RequestDetails theRequestDetails, 300 IpsContext theIpsContext, 301 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 302 Section theSection, 303 Class<T> nextResourceType, 304 ISectionResourceSupplier resourceSupplier, 305 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 306 IpsSectionContext<T> ipsSectionContext = theIpsContext.newSectionContext(theSection, nextResourceType); 307 308 List<ISectionResourceSupplier.ResourceEntry> resources = 309 resourceSupplier.fetchResourcesForSection(theIpsContext, ipsSectionContext, theRequestDetails); 310 if (resources != null) { 311 for (ISectionResourceSupplier.ResourceEntry nextEntry : resources) { 312 IBaseResource resource = nextEntry.getResource(); 313 Validate.isTrue( 314 resource.getIdElement().hasIdPart(), 315 "fetchResourcesForSection(..) returned resource(s) with no ID populated"); 316 resource.setUserData(RESOURCE_ENTRY_INCLUSION_TYPE, nextEntry.getInclusionType()); 317 } 318 addResourcesToIpsContents( 319 theStrategy, 320 theRequestDetails, 321 theIpsContext, 322 resources, 323 theGlobalResourceCollectionToPopulate, 324 sectionResourceCollectionToPopulate); 325 } 326 } 327 328 /** 329 * Given a collection of resources that have been fetched, analyze them and add them as appropriate 330 * to the collection that will be included in a given IPS section context. 331 * 332 * @param theStrategy The generation strategy 333 * @param theIpsContext The overall IPS generation context for this IPS. 334 * @param theCandidateResources The resources that have been fetched for inclusion in the IPS bundle 335 */ 336 private void addResourcesToIpsContents( 337 IIpsGenerationStrategy theStrategy, 338 RequestDetails theRequestDetails, 339 IpsContext theIpsContext, 340 List<ISectionResourceSupplier.ResourceEntry> theCandidateResources, 341 ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, 342 ResourceInclusionCollection theSectionResourceCollectionToPopulate) { 343 for (ISectionResourceSupplier.ResourceEntry nextCandidateEntry : theCandidateResources) { 344 if (nextCandidateEntry.getInclusionType() == ISectionResourceSupplier.InclusionTypeEnum.EXCLUDE) { 345 continue; 346 } 347 348 IBaseResource nextCandidate = nextCandidateEntry.getResource(); 349 boolean primaryResource = nextCandidateEntry.getInclusionType() 350 == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE; 351 352 String originalResourceId = 353 nextCandidate.getIdElement().toUnqualifiedVersionless().getValue(); 354 355 // Check if we already have this resource included so that we don't 356 // include it twice 357 IBaseResource previouslyExistingResource = 358 theGlobalResourcesCollectionToPopulate.getResourceByOriginalId(originalResourceId); 359 360 if (previouslyExistingResource != null) { 361 reuseAlreadyIncludedGlobalResourceInSectionCollection( 362 theSectionResourceCollectionToPopulate, 363 previouslyExistingResource, 364 primaryResource, 365 originalResourceId); 366 } else if (theGlobalResourcesCollectionToPopulate.hasResourceWithReplacementId(originalResourceId)) { 367 addResourceToSectionCollectionOnlyIfPrimary( 368 theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); 369 } else { 370 addResourceToGlobalCollectionAndSectionCollection( 371 theStrategy, 372 theRequestDetails, 373 theIpsContext, 374 theGlobalResourcesCollectionToPopulate, 375 theSectionResourceCollectionToPopulate, 376 nextCandidate, 377 originalResourceId, 378 primaryResource); 379 } 380 } 381 } 382 383 private static void addResourceToSectionCollectionOnlyIfPrimary( 384 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 385 boolean primaryResource, 386 IBaseResource nextCandidate, 387 String originalResourceId) { 388 if (primaryResource) { 389 theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 390 } 391 } 392 393 private void addResourceToGlobalCollectionAndSectionCollection( 394 IIpsGenerationStrategy theStrategy, 395 RequestDetails theRequestDetails, 396 IpsContext theIpsContext, 397 ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, 398 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 399 IBaseResource nextCandidate, 400 String originalResourceId, 401 boolean primaryResource) { 402 massageResourceId(theStrategy, theRequestDetails, theIpsContext, nextCandidate); 403 theGlobalResourcesCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 404 addResourceToSectionCollectionOnlyIfPrimary( 405 theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); 406 } 407 408 private static void reuseAlreadyIncludedGlobalResourceInSectionCollection( 409 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 410 IBaseResource previouslyExistingResource, 411 boolean primaryResource, 412 String originalResourceId) { 413 IBaseResource nextCandidate; 414 ISectionResourceSupplier.InclusionTypeEnum previouslyIncludedResourceInclusionType = 415 (ISectionResourceSupplier.InclusionTypeEnum) 416 previouslyExistingResource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 417 if (previouslyIncludedResourceInclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 418 if (primaryResource) { 419 previouslyExistingResource.setUserData( 420 RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); 421 } 422 } 423 424 nextCandidate = previouslyExistingResource; 425 theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 426 } 427 428 @SuppressWarnings("unchecked") 429 private void addSection( 430 IIpsGenerationStrategy theStrategy, 431 Section theSection, 432 CompositionBuilder theCompositionBuilder, 433 ResourceInclusionCollection theResourcesToInclude, 434 ResourceInclusionCollection theGlobalResourcesToInclude) { 435 436 CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection(); 437 438 sectionBuilder.setTitle(theSection.getTitle()); 439 sectionBuilder.addCodeCoding( 440 theSection.getSectionSystem(), theSection.getSectionCode(), theSection.getSectionDisplay()); 441 442 for (IBaseResource next : theResourcesToInclude.getResources()) { 443 ISectionResourceSupplier.InclusionTypeEnum inclusionType = 444 (ISectionResourceSupplier.InclusionTypeEnum) next.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 445 if (inclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 446 continue; 447 } 448 449 IBaseHasExtensions extensionHolder = (IBaseHasExtensions) next; 450 if (extensionHolder.getExtension().stream() 451 .noneMatch(t -> t.getUrl().equals(URL_NARRATIVE_LINK))) { 452 IBaseExtension<?, ?> narrativeLink = extensionHolder.addExtension(); 453 narrativeLink.setUrl(URL_NARRATIVE_LINK); 454 String narrativeLinkValue = 455 theCompositionBuilder.getComposition().getIdElement().getValue() 456 + "#" 457 + myFhirContext.getResourceType(next) 458 + "-" 459 + next.getIdElement().getValue(); 460 IPrimitiveType<String> narrativeLinkUri = 461 (IPrimitiveType<String>) requireNonNull(myFhirContext.getElementDefinition("url")) 462 .newInstance(); 463 narrativeLinkUri.setValueAsString(narrativeLinkValue); 464 narrativeLink.setValue(narrativeLinkUri); 465 } 466 467 sectionBuilder.addEntry(next.getIdElement()); 468 } 469 470 String narrative = 471 createSectionNarrative(theStrategy, theSection, theResourcesToInclude, theGlobalResourcesToInclude); 472 sectionBuilder.setText("generated", narrative); 473 } 474 475 private CompositionBuilder createComposition( 476 IIpsGenerationStrategy theStrategy, IBaseResource thePatient, IpsContext context, IBaseResource author) { 477 CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext); 478 compositionBuilder.setId(IdType.newRandomUuid()); 479 480 compositionBuilder.setStatus(Composition.CompositionStatus.FINAL.toCode()); 481 compositionBuilder.setSubject(thePatient.getIdElement().toUnqualifiedVersionless()); 482 compositionBuilder.addTypeCoding("http://loinc.org", "60591-5", "Patient Summary Document"); 483 compositionBuilder.setDate(InstantType.now()); 484 compositionBuilder.setTitle(theStrategy.createTitle(context)); 485 compositionBuilder.setConfidentiality(theStrategy.createConfidentiality(context)); 486 compositionBuilder.addAuthor(author.getIdElement()); 487 488 return compositionBuilder; 489 } 490 491 private void massageResourceId( 492 IIpsGenerationStrategy theStrategy, 493 RequestDetails theRequestDetails, 494 IpsContext theIpsContext, 495 IBaseResource theResource) { 496 String base = theRequestDetails.getFhirServerBase(); 497 498 IIdType id = theResource.getIdElement(); 499 if (!id.hasBaseUrl() && id.hasResourceType() && id.hasIdPart()) { 500 id = id.withServerBase(base, id.getResourceType()); 501 theResource.setId(id); 502 } 503 504 id = theStrategy.massageResourceId(theIpsContext, theResource); 505 if (id != null) { 506 theResource.setId(id); 507 } 508 } 509 510 private String createSectionNarrative( 511 IIpsGenerationStrategy theStrategy, 512 Section theSection, 513 ResourceInclusionCollection theResources, 514 ResourceInclusionCollection theGlobalResourceCollection) { 515 CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, theGlobalResourceCollection); 516 517 Bundle bundle = new Bundle(); 518 for (IBaseResource resource : theResources.getResources()) { 519 ISectionResourceSupplier.InclusionTypeEnum inclusionType = 520 (ISectionResourceSupplier.InclusionTypeEnum) resource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 521 if (inclusionType == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 522 bundle.addEntry().setResource((Resource) resource); 523 } 524 } 525 String profile = theSection.getProfile(); 526 bundle.getMeta().addProfile(profile); 527 528 // Generate the narrative 529 return generator.generateResourceNarrative(myFhirContext, bundle); 530 } 531 532 @Nonnull 533 private CustomThymeleafNarrativeGenerator newNarrativeGenerator( 534 IIpsGenerationStrategy theStrategy, ResourceInclusionCollection theGlobalResourceCollection) { 535 List<String> narrativePropertyFiles = theStrategy.getNarrativePropertyFiles(); 536 CustomThymeleafNarrativeGenerator generator = new CustomThymeleafNarrativeGenerator(narrativePropertyFiles); 537 generator.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() { 538 @Override 539 public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { 540 return theGlobalResourceCollection.getResourceById(theReference); 541 } 542 }); 543 return generator; 544 } 545 546 private static class ResourceInclusionCollection { 547 548 private final List<IBaseResource> myResources = new ArrayList<>(); 549 private final Map<String, IBaseResource> myIdToResource = new HashMap<>(); 550 private final BiMap<String, String> myOriginalIdToNewId = HashBiMap.create(); 551 552 public List<IBaseResource> getResources() { 553 return myResources; 554 } 555 556 /** 557 * @param theOriginalResourceId Must be an unqualified versionless ID 558 */ 559 public void addResourceIfNotAlreadyPresent(IBaseResource theResource, String theOriginalResourceId) { 560 assert theOriginalResourceId.matches("([A-Z][a-z]([A-Za-z]+)/[a-zA-Z0-9._-]+)|(urn:uuid:[0-9a-z-]+)") 561 : "Not an unqualified versionless ID: " + theOriginalResourceId; 562 563 String resourceId = 564 theResource.getIdElement().toUnqualifiedVersionless().getValue(); 565 if (myIdToResource.containsKey(resourceId)) { 566 return; 567 } 568 569 myResources.add(theResource); 570 myIdToResource.put(resourceId, theResource); 571 myOriginalIdToNewId.put(theOriginalResourceId, resourceId); 572 } 573 574 public String getIdSubstitution(String theExistingReference) { 575 return myOriginalIdToNewId.get(theExistingReference); 576 } 577 578 public IBaseResource getResourceById(IIdType theReference) { 579 return getResourceById(theReference.toUnqualifiedVersionless().getValue()); 580 } 581 582 public boolean hasResourceWithReplacementId(String theReplacementId) { 583 return myOriginalIdToNewId.containsValue(theReplacementId); 584 } 585 586 public IBaseResource getResourceById(String theReference) { 587 return myIdToResource.get(theReference); 588 } 589 590 @Nullable 591 public IBaseResource getResourceByOriginalId(String theOriginalResourceId) { 592 String newResourceId = myOriginalIdToNewId.get(theOriginalResourceId); 593 if (newResourceId != null) { 594 return myIdToResource.get(newResourceId); 595 } 596 return null; 597 } 598 599 public boolean isEmpty() { 600 return myResources.isEmpty(); 601 } 602 } 603}