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 bundleBuilder.addProfile(theStrategy.getBundleProfile()); 150 151 // Add composition to document 152 bundleBuilder.addDocumentEntry(composition); 153 154 // Add inclusion candidates 155 for (IBaseResource next : theResourcesToInclude.getResources()) { 156 bundleBuilder.addDocumentEntry(next); 157 } 158 159 // Add author to document 160 bundleBuilder.addDocumentEntry(author); 161 162 IBaseBundle retVal = bundleBuilder.getBundle(); 163 164 theStrategy.postManipulateIpsBundle(retVal); 165 166 return retVal; 167 } 168 169 private void determineInclusions( 170 IIpsGenerationStrategy theStrategy, 171 RequestDetails theRequestDetails, 172 IpsContext theIpsContext, 173 CompositionBuilder theCompositionBuilder, 174 ResourceInclusionCollection theGlobalResourcesToInclude) { 175 for (Section nextSection : theStrategy.getSections()) { 176 determineInclusionsForSection( 177 theStrategy, 178 theRequestDetails, 179 theIpsContext, 180 theCompositionBuilder, 181 theGlobalResourcesToInclude, 182 nextSection); 183 } 184 } 185 186 private void determineInclusionsForSection( 187 IIpsGenerationStrategy theStrategy, 188 RequestDetails theRequestDetails, 189 IpsContext theIpsContext, 190 CompositionBuilder theCompositionBuilder, 191 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 192 Section theSection) { 193 ResourceInclusionCollection sectionResourceCollectionToPopulate = new ResourceInclusionCollection(); 194 ISectionResourceSupplier resourceSupplier = theStrategy.getSectionResourceSupplier(theSection); 195 196 determineInclusionsForSectionResourceTypes( 197 theStrategy, 198 theRequestDetails, 199 theIpsContext, 200 theGlobalResourceCollectionToPopulate, 201 theSection, 202 resourceSupplier, 203 sectionResourceCollectionToPopulate); 204 205 generateSectionNoInfoResourceIfNoInclusionsFound( 206 theIpsContext, theGlobalResourceCollectionToPopulate, theSection, sectionResourceCollectionToPopulate); 207 208 /* 209 * Update any references within the added candidates - This is important 210 * because we might be replacing resource IDs before including them in 211 * the summary, so we need to also update the references to those 212 * resources. 213 */ 214 updateReferencesInInclusionsForSection(theGlobalResourceCollectionToPopulate); 215 216 if (sectionResourceCollectionToPopulate.isEmpty()) { 217 return; 218 } 219 220 addSection( 221 theStrategy, 222 theSection, 223 theCompositionBuilder, 224 sectionResourceCollectionToPopulate, 225 theGlobalResourceCollectionToPopulate); 226 } 227 228 private void updateReferencesInInclusionsForSection( 229 ResourceInclusionCollection theGlobalResourceCollectionToPopulate) { 230 for (IBaseResource nextResource : theGlobalResourceCollectionToPopulate.getResources()) { 231 List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(nextResource); 232 for (ResourceReferenceInfo nextReference : references) { 233 String existingReference = nextReference 234 .getResourceReference() 235 .getReferenceElement() 236 .getValue(); 237 if (isNotBlank(existingReference)) { 238 existingReference = new IdType(existingReference) 239 .toUnqualifiedVersionless() 240 .getValue(); 241 String replacement = theGlobalResourceCollectionToPopulate.getIdSubstitution(existingReference); 242 if (isNotBlank(replacement)) { 243 if (!replacement.equals(existingReference)) { 244 nextReference.getResourceReference().setReference(replacement); 245 } 246 } else if (theGlobalResourceCollectionToPopulate.getResourceById(existingReference) == null) { 247 // If this reference doesn't point to something we have actually 248 // included in the bundle, clear the reference. 249 nextReference.getResourceReference().setReference(null); 250 nextReference.getResourceReference().setResource(null); 251 } 252 } 253 } 254 } 255 } 256 257 private static void generateSectionNoInfoResourceIfNoInclusionsFound( 258 IpsContext theIpsContext, 259 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 260 Section theSection, 261 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 262 if (sectionResourceCollectionToPopulate.isEmpty() && theSection.getNoInfoGenerator() != null) { 263 IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); 264 String id = IdType.newRandomUuid().getValue(); 265 if (noInfoResource.getIdElement().isEmpty()) { 266 noInfoResource.setId(id); 267 } 268 noInfoResource.setUserData( 269 RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); 270 theGlobalResourceCollectionToPopulate.addResourceIfNotAlreadyPresent( 271 noInfoResource, 272 noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); 273 sectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(noInfoResource, id); 274 } 275 } 276 277 private void determineInclusionsForSectionResourceTypes( 278 IIpsGenerationStrategy theStrategy, 279 RequestDetails theRequestDetails, 280 IpsContext theIpsContext, 281 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 282 Section theSection, 283 ISectionResourceSupplier resourceSupplier, 284 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 285 for (Class<? extends IBaseResource> nextResourceType : theSection.getResourceTypes()) { 286 determineInclusionsForSectionResourceType( 287 theStrategy, 288 theRequestDetails, 289 theIpsContext, 290 theGlobalResourceCollectionToPopulate, 291 theSection, 292 nextResourceType, 293 resourceSupplier, 294 sectionResourceCollectionToPopulate); 295 } 296 } 297 298 private <T extends IBaseResource> void determineInclusionsForSectionResourceType( 299 IIpsGenerationStrategy theStrategy, 300 RequestDetails theRequestDetails, 301 IpsContext theIpsContext, 302 ResourceInclusionCollection theGlobalResourceCollectionToPopulate, 303 Section theSection, 304 Class<T> nextResourceType, 305 ISectionResourceSupplier resourceSupplier, 306 ResourceInclusionCollection sectionResourceCollectionToPopulate) { 307 IpsSectionContext<T> ipsSectionContext = theIpsContext.newSectionContext(theSection, nextResourceType); 308 309 List<ISectionResourceSupplier.ResourceEntry> resources = 310 resourceSupplier.fetchResourcesForSection(theIpsContext, ipsSectionContext, theRequestDetails); 311 if (resources != null) { 312 for (ISectionResourceSupplier.ResourceEntry nextEntry : resources) { 313 IBaseResource resource = nextEntry.getResource(); 314 Validate.isTrue( 315 resource.getIdElement().hasIdPart(), 316 "fetchResourcesForSection(..) returned resource(s) with no ID populated"); 317 resource.setUserData(RESOURCE_ENTRY_INCLUSION_TYPE, nextEntry.getInclusionType()); 318 } 319 addResourcesToIpsContents( 320 theStrategy, 321 theRequestDetails, 322 theIpsContext, 323 resources, 324 theGlobalResourceCollectionToPopulate, 325 sectionResourceCollectionToPopulate); 326 } 327 } 328 329 /** 330 * Given a collection of resources that have been fetched, analyze them and add them as appropriate 331 * to the collection that will be included in a given IPS section context. 332 * 333 * @param theStrategy The generation strategy 334 * @param theIpsContext The overall IPS generation context for this IPS. 335 * @param theCandidateResources The resources that have been fetched for inclusion in the IPS bundle 336 */ 337 private void addResourcesToIpsContents( 338 IIpsGenerationStrategy theStrategy, 339 RequestDetails theRequestDetails, 340 IpsContext theIpsContext, 341 List<ISectionResourceSupplier.ResourceEntry> theCandidateResources, 342 ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, 343 ResourceInclusionCollection theSectionResourceCollectionToPopulate) { 344 for (ISectionResourceSupplier.ResourceEntry nextCandidateEntry : theCandidateResources) { 345 if (nextCandidateEntry.getInclusionType() == ISectionResourceSupplier.InclusionTypeEnum.EXCLUDE) { 346 continue; 347 } 348 349 IBaseResource nextCandidate = nextCandidateEntry.getResource(); 350 boolean primaryResource = nextCandidateEntry.getInclusionType() 351 == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE; 352 353 String originalResourceId = 354 nextCandidate.getIdElement().toUnqualifiedVersionless().getValue(); 355 356 // Check if we already have this resource included so that we don't 357 // include it twice 358 IBaseResource previouslyExistingResource = 359 theGlobalResourcesCollectionToPopulate.getResourceByOriginalId(originalResourceId); 360 361 if (previouslyExistingResource != null) { 362 reuseAlreadyIncludedGlobalResourceInSectionCollection( 363 theSectionResourceCollectionToPopulate, 364 previouslyExistingResource, 365 primaryResource, 366 originalResourceId); 367 } else if (theGlobalResourcesCollectionToPopulate.hasResourceWithReplacementId(originalResourceId)) { 368 addResourceToSectionCollectionOnlyIfPrimary( 369 theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); 370 } else { 371 addResourceToGlobalCollectionAndSectionCollection( 372 theStrategy, 373 theRequestDetails, 374 theIpsContext, 375 theGlobalResourcesCollectionToPopulate, 376 theSectionResourceCollectionToPopulate, 377 nextCandidate, 378 originalResourceId, 379 primaryResource); 380 } 381 } 382 } 383 384 private static void addResourceToSectionCollectionOnlyIfPrimary( 385 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 386 boolean primaryResource, 387 IBaseResource nextCandidate, 388 String originalResourceId) { 389 if (primaryResource) { 390 theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 391 } 392 } 393 394 private void addResourceToGlobalCollectionAndSectionCollection( 395 IIpsGenerationStrategy theStrategy, 396 RequestDetails theRequestDetails, 397 IpsContext theIpsContext, 398 ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, 399 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 400 IBaseResource nextCandidate, 401 String originalResourceId, 402 boolean primaryResource) { 403 massageResourceId(theStrategy, theRequestDetails, theIpsContext, nextCandidate); 404 theGlobalResourcesCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 405 addResourceToSectionCollectionOnlyIfPrimary( 406 theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); 407 } 408 409 private static void reuseAlreadyIncludedGlobalResourceInSectionCollection( 410 ResourceInclusionCollection theSectionResourceCollectionToPopulate, 411 IBaseResource previouslyExistingResource, 412 boolean primaryResource, 413 String originalResourceId) { 414 IBaseResource nextCandidate; 415 ISectionResourceSupplier.InclusionTypeEnum previouslyIncludedResourceInclusionType = 416 (ISectionResourceSupplier.InclusionTypeEnum) 417 previouslyExistingResource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 418 if (previouslyIncludedResourceInclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 419 if (primaryResource) { 420 previouslyExistingResource.setUserData( 421 RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); 422 } 423 } 424 425 nextCandidate = previouslyExistingResource; 426 theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); 427 } 428 429 @SuppressWarnings("unchecked") 430 private void addSection( 431 IIpsGenerationStrategy theStrategy, 432 Section theSection, 433 CompositionBuilder theCompositionBuilder, 434 ResourceInclusionCollection theResourcesToInclude, 435 ResourceInclusionCollection theGlobalResourcesToInclude) { 436 437 CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection(); 438 439 sectionBuilder.setTitle(theSection.getTitle()); 440 sectionBuilder.addCodeCoding( 441 theSection.getSectionSystem(), theSection.getSectionCode(), theSection.getSectionDisplay()); 442 443 for (IBaseResource next : theResourcesToInclude.getResources()) { 444 ISectionResourceSupplier.InclusionTypeEnum inclusionType = 445 (ISectionResourceSupplier.InclusionTypeEnum) next.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 446 if (inclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 447 continue; 448 } 449 450 IBaseHasExtensions extensionHolder = (IBaseHasExtensions) next; 451 if (extensionHolder.getExtension().stream() 452 .noneMatch(t -> t.getUrl().equals(URL_NARRATIVE_LINK))) { 453 IBaseExtension<?, ?> narrativeLink = extensionHolder.addExtension(); 454 narrativeLink.setUrl(URL_NARRATIVE_LINK); 455 String narrativeLinkValue = 456 theCompositionBuilder.getComposition().getIdElement().getValue() 457 + "#" 458 + myFhirContext.getResourceType(next) 459 + "-" 460 + next.getIdElement().getValue(); 461 IPrimitiveType<String> narrativeLinkUri = 462 (IPrimitiveType<String>) requireNonNull(myFhirContext.getElementDefinition("url")) 463 .newInstance(); 464 narrativeLinkUri.setValueAsString(narrativeLinkValue); 465 narrativeLink.setValue(narrativeLinkUri); 466 } 467 468 sectionBuilder.addEntry(next.getIdElement()); 469 } 470 471 String narrative = 472 createSectionNarrative(theStrategy, theSection, theResourcesToInclude, theGlobalResourcesToInclude); 473 sectionBuilder.setText("generated", narrative); 474 } 475 476 private CompositionBuilder createComposition( 477 IIpsGenerationStrategy theStrategy, IBaseResource thePatient, IpsContext context, IBaseResource author) { 478 CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext); 479 compositionBuilder.setId(IdType.newRandomUuid()); 480 481 compositionBuilder.setStatus(Composition.CompositionStatus.FINAL.toCode()); 482 compositionBuilder.setSubject(thePatient.getIdElement().toUnqualifiedVersionless()); 483 compositionBuilder.addTypeCoding("http://loinc.org", "60591-5", "Patient Summary Document"); 484 compositionBuilder.setDate(InstantType.now()); 485 compositionBuilder.setTitle(theStrategy.createTitle(context)); 486 compositionBuilder.setConfidentiality(theStrategy.createConfidentiality(context)); 487 compositionBuilder.addAuthor(author.getIdElement()); 488 489 return compositionBuilder; 490 } 491 492 private void massageResourceId( 493 IIpsGenerationStrategy theStrategy, 494 RequestDetails theRequestDetails, 495 IpsContext theIpsContext, 496 IBaseResource theResource) { 497 String base = theRequestDetails.getFhirServerBase(); 498 499 IIdType id = theResource.getIdElement(); 500 if (!id.hasBaseUrl() && id.hasResourceType() && id.hasIdPart()) { 501 id = id.withServerBase(base, id.getResourceType()); 502 theResource.setId(id); 503 } 504 505 id = theStrategy.massageResourceId(theIpsContext, theResource); 506 if (id != null) { 507 theResource.setId(id); 508 } 509 } 510 511 private String createSectionNarrative( 512 IIpsGenerationStrategy theStrategy, 513 Section theSection, 514 ResourceInclusionCollection theResources, 515 ResourceInclusionCollection theGlobalResourceCollection) { 516 CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, theGlobalResourceCollection); 517 518 Bundle bundle = new Bundle(); 519 for (IBaseResource resource : theResources.getResources()) { 520 ISectionResourceSupplier.InclusionTypeEnum inclusionType = 521 (ISectionResourceSupplier.InclusionTypeEnum) resource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); 522 if (inclusionType == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { 523 bundle.addEntry().setResource((Resource) resource); 524 } 525 } 526 String profile = theSection.getProfile(); 527 bundle.getMeta().addProfile(profile); 528 529 // Generate the narrative 530 return generator.generateResourceNarrative(myFhirContext, bundle); 531 } 532 533 @Nonnull 534 private CustomThymeleafNarrativeGenerator newNarrativeGenerator( 535 IIpsGenerationStrategy theStrategy, ResourceInclusionCollection theGlobalResourceCollection) { 536 List<String> narrativePropertyFiles = theStrategy.getNarrativePropertyFiles(); 537 CustomThymeleafNarrativeGenerator generator = new CustomThymeleafNarrativeGenerator(narrativePropertyFiles); 538 generator.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() { 539 @Override 540 public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { 541 return theGlobalResourceCollection.getResourceById(theReference); 542 } 543 }); 544 return generator; 545 } 546 547 private static class ResourceInclusionCollection { 548 549 private final List<IBaseResource> myResources = new ArrayList<>(); 550 private final Map<String, IBaseResource> myIdToResource = new HashMap<>(); 551 private final BiMap<String, String> myOriginalIdToNewId = HashBiMap.create(); 552 553 public List<IBaseResource> getResources() { 554 return myResources; 555 } 556 557 /** 558 * @param theOriginalResourceId Must be an unqualified versionless ID 559 */ 560 public void addResourceIfNotAlreadyPresent(IBaseResource theResource, String theOriginalResourceId) { 561 assert theOriginalResourceId.matches("([A-Z][a-z]([A-Za-z]+)/[a-zA-Z0-9._-]+)|(urn:uuid:[0-9a-z-]+)") 562 : "Not an unqualified versionless ID: " + theOriginalResourceId; 563 564 String resourceId = 565 theResource.getIdElement().toUnqualifiedVersionless().getValue(); 566 if (myIdToResource.containsKey(resourceId)) { 567 return; 568 } 569 570 myResources.add(theResource); 571 myIdToResource.put(resourceId, theResource); 572 myOriginalIdToNewId.put(theOriginalResourceId, resourceId); 573 } 574 575 public String getIdSubstitution(String theExistingReference) { 576 return myOriginalIdToNewId.get(theExistingReference); 577 } 578 579 public IBaseResource getResourceById(IIdType theReference) { 580 return getResourceById(theReference.toUnqualifiedVersionless().getValue()); 581 } 582 583 public boolean hasResourceWithReplacementId(String theReplacementId) { 584 return myOriginalIdToNewId.containsValue(theReplacementId); 585 } 586 587 public IBaseResource getResourceById(String theReference) { 588 return myIdToResource.get(theReference); 589 } 590 591 @Nullable 592 public IBaseResource getResourceByOriginalId(String theOriginalResourceId) { 593 String newResourceId = myOriginalIdToNewId.get(theOriginalResourceId); 594 if (newResourceId != null) { 595 return myIdToResource.get(newResourceId); 596 } 597 return null; 598 } 599 600 public boolean isEmpty() { 601 return myResources.isEmpty(); 602 } 603 } 604}