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}