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}