IPS Connectathon Fixes (#4834)

* IPS Connectathon Fixes

* Fix changelog

* Cleanup
This commit is contained in:
James Agnew 2023-07-24 15:32:42 -04:00 committed by GitHub
parent 53ceb7cac4
commit e14bbec83b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 356 additions and 337 deletions

View File

@ -0,0 +1,11 @@
---
type: change
issue: 4834
title: "Several enhancements were made to the IPS generator:<ul>
<li>Generated IPS documents will no longer include sections that contain no contents.</li>
<li>NarrativeLink extensions now use the correct datatype (url instead of uri)</li>
<li>Section profile URLs have been updated to no longer use an unknown URL</li>
<li>Some resources added to the generated IPS did not have their FHIR server IDs replaced with a placeholder UUID.</li>
<li>Immunization manufacturer was not fetched from the server</li>
</ul>
Thanks to Rio Bennin for all of the feedback!"

View File

@ -48,7 +48,7 @@ The narrative properties file should contain definitions using the profile URL o
```properties
ips-allergyintolerance.resourceType=Bundle
ips-allergyintolerance.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips
ips-allergyintolerance.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies
ips-allergyintolerance.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html
```

View File

@ -107,7 +107,8 @@ public class SectionRegistry {
.withSectionCode("48765-2")
.withSectionDisplay("Allergies and Adverse Reactions")
.withResourceTypes(ResourceType.AllergyIntolerance.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies")
.withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator())
.build();
}
@ -122,7 +123,8 @@ public class SectionRegistry {
ResourceType.MedicationRequest.name(),
ResourceType.MedicationAdministration.name(),
ResourceType.MedicationDispense.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications")
.withNoInfoGenerator(new MedicationNoInfoR4Generator())
.build();
}
@ -133,7 +135,8 @@ public class SectionRegistry {
.withSectionCode("11450-4")
.withSectionDisplay("Problem List")
.withResourceTypes(ResourceType.Condition.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems")
.withNoInfoGenerator(new ProblemNoInfoR4Generator())
.build();
}
@ -144,7 +147,8 @@ public class SectionRegistry {
.withSectionCode("11369-6")
.withSectionDisplay("History of Immunizations")
.withResourceTypes(ResourceType.Immunization.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations")
.build();
}
@ -154,7 +158,8 @@ public class SectionRegistry {
.withSectionCode("47519-4")
.withSectionDisplay("History of Procedures")
.withResourceTypes(ResourceType.Procedure.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx")
.build();
}
@ -164,7 +169,8 @@ public class SectionRegistry {
.withSectionCode("46240-8")
.withSectionDisplay("Medical Devices")
.withResourceTypes(ResourceType.DeviceUseStatement.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices")
.build();
}
@ -174,7 +180,8 @@ public class SectionRegistry {
.withSectionCode("30954-2")
.withSectionDisplay("Diagnostic Results")
.withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults")
.build();
}
@ -184,7 +191,8 @@ public class SectionRegistry {
.withSectionCode("8716-3")
.withSectionDisplay("Vital Signs")
.withResourceTypes(ResourceType.Observation.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns")
.build();
}
@ -194,7 +202,8 @@ public class SectionRegistry {
.withSectionCode("10162-6")
.withSectionDisplay("Pregnancy Information")
.withResourceTypes(ResourceType.Observation.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx")
.build();
}
@ -204,7 +213,8 @@ public class SectionRegistry {
.withSectionCode("29762-2")
.withSectionDisplay("Social History")
.withResourceTypes(ResourceType.Observation.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory")
.build();
}
@ -214,7 +224,8 @@ public class SectionRegistry {
.withSectionCode("11348-0")
.withSectionDisplay("History of Past Illness")
.withResourceTypes(ResourceType.Condition.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PastHistoryOfIllnesses-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx")
.build();
}
@ -224,7 +235,8 @@ public class SectionRegistry {
.withSectionCode("47420-5")
.withSectionDisplay("Functional Status")
.withResourceTypes(ResourceType.ClinicalImpression.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus")
.build();
}
@ -234,7 +246,8 @@ public class SectionRegistry {
.withSectionCode("18776-5")
.withSectionDisplay("Plan of Care")
.withResourceTypes(ResourceType.CarePlan.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare")
.build();
}
@ -244,7 +257,8 @@ public class SectionRegistry {
.withSectionCode("42349-0")
.withSectionDisplay("Advance Directives")
.withResourceTypes(ResourceType.Consent.name())
.withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips")
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives")
.build();
}

View File

@ -218,16 +218,16 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
for (IBaseResource nextCandidate : resources) {
boolean include;
if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate)
== BundleEntrySearchModeEnum.INCLUDE) {
include = true;
boolean candidateIsSearchInclude = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate)
== BundleEntrySearchModeEnum.INCLUDE;
boolean addResourceToBundle;
if (candidateIsSearchInclude) {
addResourceToBundle = true;
} else {
include = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate);
addResourceToBundle = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate);
}
if (include) {
if (addResourceToBundle) {
String originalResourceId = nextCandidate
.getIdElement()
@ -249,13 +249,19 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
nextCandidate = previouslyExistingResource;
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
} else if (theGlobalResourcesToInclude.hasResourceWithReplacementId(originalResourceId)) {
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
if (!candidateIsSearchInclude) {
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
}
} else {
IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate);
nextCandidate.setId(id);
theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId);
if (!candidateIsSearchInclude) {
sectionResourcesToInclude.addResourceIfNotAlreadyPresent(
nextCandidate, originalResourceId);
}
}
}
}
@ -281,7 +287,7 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
* the summary, so we need to also update the references to those
* resources.
*/
for (IBaseResource nextResource : sectionResourcesToInclude.getResources()) {
for (IBaseResource nextResource : theGlobalResourcesToInclude.getResources()) {
List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(nextResource);
for (ResourceReferenceInfo nextReference : references) {
String existingReference = nextReference
@ -307,6 +313,10 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
}
}
if (sectionResourcesToInclude.isEmpty()) {
return;
}
addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude);
}
@ -336,7 +346,7 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
+ "-"
+ next.getIdElement().getValue();
IPrimitiveType<String> narrativeLinkUri = (IPrimitiveType<String>)
myFhirContext.getElementDefinition("uri").newInstance();
myFhirContext.getElementDefinition("url").newInstance();
narrativeLinkUri.setValueAsString(narrativeLinkValue);
narrativeLink.setValue(narrativeLinkUri);
@ -417,152 +427,6 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc {
return generator;
}
/*
private static HashMap<PatientSummary.IPSSection, List<Resource>> hashPrimaries(List<Resource> resourceList) {
HashMap<PatientSummary.IPSSection, List<Resource>> iPSResourceMap = new HashMap<PatientSummary.IPSSection, List<Resource>>();
for (Resource resource : resourceList) {
for (PatientSummary.IPSSection iPSSection : PatientSummary.IPSSection.values()) {
if ( SectionTypes.get(iPSSection).contains(resource.getResourceType()) ) {
if ( !(resource.getResourceType() == ResourceType.Observation) || isObservationinSection(iPSSection, (Observation) resource)) {
if (iPSResourceMap.get(iPSSection) == null) {
iPSResourceMap.put(iPSSection, new ArrayList<Resource>());
}
iPSResourceMap.get(iPSSection).add(resource);
}
}
}
}
return iPSResourceMap;
}
private static HashMap<PatientSummary.IPSSection, List<Resource>> filterPrimaries(HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries) {
HashMap<PatientSummary.IPSSection, List<Resource>> filteredPrimaries = new HashMap<PatientSummary.IPSSection, List<Resource>>();
for ( PatientSummary.IPSSection section : sectionPrimaries.keySet() ) {
List<Resource> filteredList = new ArrayList<Resource>();
for (Resource resource : sectionPrimaries.get(section)) {
if (passesFilter(section, resource)) {
filteredList.add(resource);
}
}
if (filteredList.size() > 0) {
filteredPrimaries.put(section, filteredList);
}
}
return filteredPrimaries;
}
private static List<Resource> pruneResources(Patient patient, List<Resource> resources, HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, FhirContext ctx) {
List<String> resourceIds = new ArrayList<String>();
List<String> followedIds = new ArrayList<String>();
HashMap<String, Resource> resourcesById = new HashMap<String, Resource>();
for (Resource resource : resources) {
resourcesById.put(resource.getIdElement().getIdPart(), resource);
}
String patientId = patient.getIdElement().getIdPart();
resourcesById.put(patientId, patient);
recursivePrune(patientId, resourceIds, followedIds, resourcesById, ctx);
for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
for (Resource resource : sectionPrimaries.get(section)) {
String resourceId = resource.getIdElement().getIdPart();
recursivePrune(resourceId, resourceIds, followedIds, resourcesById, ctx);
}
}
List<Resource> prunedResources = new ArrayList<Resource>();
for (Resource resource : resources) {
if (resourceIds.contains(resource.getIdElement().getIdPart())) {
prunedResources.add(resource);
}
}
return prunedResources;
}
private static Void recursivePrune(String resourceId, List<String> resourceIds, List<String> followedIds, HashMap<String, Resource> resourcesById, FhirContext ctx) {
if (!resourceIds.contains(resourceId)) {
resourceIds.add(resourceId);
}
Resource resource = resourcesById.get(resourceId);
if (resource != null) {
ctx.newTerser().getAllResourceReferences(resource).stream()
.map( r -> r.getResourceReference().getReferenceElement().getIdPart() )
.forEach( id -> {
if (!followedIds.contains(id)) {
followedIds.add(id);
recursivePrune(id, resourceIds, followedIds, resourcesById, ctx);
}
});
}
return null;
}
private static List<Resource> addLinkToResources(List<Resource> resources, HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, Composition composition) {
List<Resource> linkedResources = new ArrayList<Resource>();
HashMap<String, String> valueUrls = new HashMap<String, String>();
String url = "http://hl7.org/fhir/StructureDefinition/narrativeLink";
String valueUrlBase = composition.getId() + "#";
for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
String profile = SectionProfiles.get(section);
String[] arr = profile.split("/");
String profileName = arr[arr.length - 1];
String sectionValueUrlBase = valueUrlBase + profileName.split("-uv-")[0];
for (Resource resource : sectionPrimaries.get(section)) {
String valueUrl = sectionValueUrlBase + "-" + resource.getIdElement().getIdPart();
valueUrls.put(resource.getIdElement().getIdPart(), valueUrl);
}
}
for (Resource resource : resources) {
if (valueUrls.containsKey(resource.getIdElement().getIdPart())) {
String valueUrl = valueUrls.get(resource.getIdElement().getIdPart());
Extension extension = new Extension();
extension.setUrl(url);
extension.setValue(new UriType(valueUrl));
DomainResource domainResource = (DomainResource) resource;
domainResource.addExtension(extension);
resource = (Resource) domainResource;
}
linkedResources.add(resource);
}
return linkedResources;
}
private static HashMap<PatientSummary.IPSSection, String> createNarratives(HashMap<PatientSummary.IPSSection, List<Resource>> sectionPrimaries, List<Resource> resources, FhirContext ctx) {
HashMap<PatientSummary.IPSSection, String> hashedNarratives = new HashMap<PatientSummary.IPSSection, String>();
for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) {
String narrative = createSectionNarrative(section, resources, ctx);
hashedNarratives.put(section, narrative);
}
return hashedNarratives;
}
*/
private static class ResourceInclusionCollection {
private final List<IBaseResource> myResources = new ArrayList<>();

View File

@ -44,6 +44,9 @@ public class IpsOperationProvider {
/**
* Patient/123/$summary
* <p>
* Note that not all parameters from the official specification are yet supported. See
* <a href="http://build.fhir.org/ig/HL7/fhir-ips/OperationDefinition-summary.html>http://build.fhir.org/ig/HL7/fhir-ips/OperationDefinition-summary.html</a>
*/
@Operation(
name = JpaConstants.OPERATION_SUMMARY,
@ -58,6 +61,9 @@ public class IpsOperationProvider {
/**
* /Patient/$summary?identifier=foo|bar
* <p>
* Note that not all parameters from the official specification are yet supported. See
* <a href="http://build.fhir.org/ig/HL7/fhir-ips/OperationDefinition-summary.html>http://build.fhir.org/ig/HL7/fhir-ips/OperationDefinition-summary.html</a>
*/
@Operation(
name = JpaConstants.OPERATION_SUMMARY,

View File

@ -293,9 +293,13 @@ public class DefaultIpsGenerationStrategy implements IIpsGenerationStrategy {
return Sets.newHashSet(DeviceUseStatement.INCLUDE_DEVICE);
}
break;
case IMMUNIZATIONS:
if (ResourceType.Immunization.name().equals(theIpsSectionContext.getResourceType())) {
return Sets.newHashSet(Immunization.INCLUDE_MANUFACTURER);
}
break;
case ALLERGY_INTOLERANCE:
case PROBLEM_LIST:
case IMMUNIZATIONS:
case PROCEDURES:
case DIAGNOSTIC_RESULTS:
case VITAL_SIGNS:

View File

@ -1,8 +1,8 @@
<!--/* AdvanceDirectives -->
<!--
Scope: Consent.scope.text || Consent.scope.coding[x].display
Scope: Consent.scope.text || Consent.scope.coding[x].display (separated by <br />)
Status: Consent.status.code
Action Controlled: Consent.provision.action[x].coding[x].display (concatenate items separated by comma, e.g. x, y, z)
Action Controlled: Consent.provision.action[x].{ text || coding[x].display (separated by <br />)} (concatenate with comma, e.g. x, y, z)
Date: Consent.dateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
@ -23,7 +23,6 @@ Date: Consent.dateTime
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getScope()},attr='display')">Scope</td>
<td th:text="*{getStatus().getCode()}">Status</td>
<td th:text="*{getdateTimeType().getValue()}">Action Controlled</td>
<td th:insert="IpsUtilityFragments :: concatCodeableConcept (list=*{getProvision().getAction()})">Action Controlled</td>
<td th:text="*{getDateTimeElement().getValue()}">Date</td>
</tr>

View File

@ -1,12 +1,11 @@
<!--/* AllergiesAndIntolerances -->
<!--
Allergen: AllergyIntolerance.code.text || AllergyIntolerance.code.coding[x].display
Status: AllergyIntolerance.clinicalStatus.coding[x].display
Category: AllergyIntolerance.code[x]
Reaction: AllergyIntolerance.reaction.manifestation.text || AllergyIntolerance.reaction.manifestation.coding[x].display *** What about getReaction().getDescription() ***
Severity: AllergyIntolerance.reaction.severity.code
Comments: AllergyIntolerance.note[x].text (display all notes separated by <br /> )
Onset: AllergyIntolerance.onsetDateTime
Allergen: AllergyIntolerance.code.text || AllergyIntolerance.code.coding[x].display (separated by <br />)
Status: AllergyIntolerance.clinicalStatus.text || AllergyIntolerance.clinicalStatus.coding[x].code (separated by <br />)
Category: AllergyIntolerance.category[x] (separated by <br />)
Reaction: AllergyIntolerance.reaction.manifestation.description || AllergyIntolerance.reaction.manifestation.text || AllergyIntolerance.reaction.manifestation.coding[x].display (separated by <br />)
Severity: AllergyIntolerance.reaction.severity[x].code (separated by <br />)
Comments: AllergyIntolerance.note[x].text (separated by <br />)
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,17 +1,17 @@
<!--/* DiagnosticResults -->
<!--
TABLE 1: Observation
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
TABLE 1: Observations
Code: Observation.code.text || Observation.code.coding[x].display (separated by <br />)
Result: Observation.valueQuantity || Observation.valueDateTime || Observation.valueCodeableConcept.text || Observation.valueCodeableConcept.coding[x].display (separated by <br />) || Observation.valueString
Unit: Observation.valueQuantity.unit
Interpretation: Observation.interpretation.text || Observation. interpretation.coding[x].display
Reference Range: Observation.referenceRange.low.value && “-“ && Observation.referenceRange.high.value
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
Interpretation: Observation.interpretation[0].text || Observation.interpretation[0].coding[x].display (separated by <br />)
Reference Range: Observation.referenceRange[x]{ text || low.value && “-“ && high.value} (concatenate with comma, e.g. x, y, z)
Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
TABLE 2: DiagnosticReport
Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display
Date: DiagnosticReport.effectiveDateTime
TABLE 2: Diagnostic Reports
Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display (separated by <br />)
Date: DiagnosticReport.effectiveDateTime || DiagnosticReport.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,10 +1,10 @@
<!--/* FunctionalStatus -->
<!--
Assessment: ClinicalImpression.code.text || ClinicalImpression.code[x].display
Assessment: ClinicalImpression.code.text || ClinicalImpression.code[x].display (separated by <br />)
Status: ClinicalImpression.status.code
Finding: ClinicalImpression.summary
Comments: ClinicalImpression.note[x].text (display all notes separated by <br /> )
Date: ClinicalImpression.effectiveDateTime
Finding: ClinicalImpression.summary
Comments: ClinicalImpression.note[x].text (separated by <br />)
Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,8 +1,8 @@
<!--/* HistoryOfProcedures -->
<!--
Procedure: Procedure.code.text || Procedure.code.coding[x].display
Comments: Procedure.note[x].text(display all notes separated by <br /> )
Date: Procedure.performedDateTime
Procedure: Procedure.code.text || Procedure.code.coding[x].display (separated by <br />)
Comments: Procedure.note[x].text(separated by <br />)
Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ && Procedure.performedPeriod.end || Procedure.performedAge || Procedure.performedRange.low && “-“ && Procedure.performedRange.high || Procedure.performedString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,12 +1,12 @@
<!--/* Immunizations -->
<!--
Immunization: Immunization.vaccineCode.text || Immunization.vaccineCode.coding[x].display
Status: Immunization.status.code
Dose Number: Immunization.doseNumberPositiveInt || Immunization.doseNumberString
Immunization: Immunization.vaccineCode.text || Immunization.vaccineCode.coding[x].display (separated by <br />)
Status: Immunization.status
Dose Number: Immunization.protocolApplied[x]{doseNumberPositiveInt || doseNumberString} (concatenate with comma, e.g. x, y, z)
Manufacturer: Organization.name
Lot Number: Immunization.lotNumber
Comments: Immunization.note[x].text (display all notes separated by <br /> )
Date: Immunization.occurrenceDateTime
Comments: Immunization.note[x].text (separated by <br />)
Date: Immunization.occurrenceDateTime || Immunization.occurrenceString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -3,43 +3,43 @@
################################################
ips-allergyintolerance.resourceType=Bundle
ips-allergyintolerance.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips
ips-allergyintolerance.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies
ips-allergyintolerance.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html
ips-medicationsummary.resourceType=Bundle
ips-medicationsummary.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips
ips-medicationsummary.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications
ips-medicationsummary.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html
ips-problemlist.resourceType=Bundle
ips-problemlist.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips
ips-problemlist.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems
ips-problemlist.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/problemlist.html
ips-immunizations.resourceType=Bundle
ips-immunizations.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips
ips-immunizations.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations
ips-immunizations.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/immunizations.html
ips-historyofprocedures.resourceType=Bundle
ips-historyofprocedures.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips
ips-historyofprocedures.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx
ips-historyofprocedures.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html
ips-medicaldevices.resourceType=Bundle
ips-medicaldevices.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips
ips-medicaldevices.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices
ips-medicaldevices.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html
ips-diagnosticresults.resourceType=Bundle
ips-diagnosticresults.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips
ips-diagnosticresults.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults
ips-diagnosticresults.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html
ips-vitalsigns.resourceType=Bundle
ips-vitalsigns.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips
ips-vitalsigns.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns
ips-vitalsigns.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html
ips-pregnancy.resourceType=Bundle
ips-pregnancy.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips
ips-pregnancy.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx
ips-pregnancy.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pregnancy.html
ips-socialhistory.resourceType=Bundle
ips-socialhistory.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips
ips-socialhistory.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory
ips-socialhistory.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/socialhistory.html
ips-pasthistoryofillness.resourceType=Bundle
@ -47,15 +47,15 @@ ips-pasthistoryofillness.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/
ips-pasthistoryofillness.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html
ips-functionalstatus.resourceType=Bundle
ips-functionalstatus.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips
ips-functionalstatus.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus
ips-functionalstatus.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html
ips-planofcare.resourceType=Bundle
ips-planofcare.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips
ips-planofcare.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare
ips-planofcare.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/planofcare.html
ips-advancedirectives.resourceType=Bundle
ips-advancedirectives.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips
ips-advancedirectives.profile=https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives
ips-advancedirectives.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html
################################################

View File

@ -1,9 +1,9 @@
<!--/* MedicalDevices -->
<!--
Device: Device.type.coding.text || Device.type.coding[x].display
Status: DeviceUseStatement.status.code
Comments: DeviceUseStatement.note[x].text (display all notes separated by <br /> )
Date Recorded: DeviceUseStatement.recordedOn
Device: Device.type.text || Device.type.coding[x].display (separated by <br />)
Status: DeviceUseStatement.status
Comments: DeviceUseStatement.note[x].text (separated by <br />)
Date Recorded: DeviceUseStatement.recordedDateTime
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,19 +1,19 @@
<!--/* MedicationSummary -->
<!--
Table 1 MedicationRequest
Medication: MedicationRequest.medicationCodeableConcept.coding[x].display || Medication.code.coding.text || Medication.code.coding.code[x].display
Table 1 Medication Requests
Medication: MedicationRequest.medicationCodeableConcept.text || MedicationRequest.medicationCodeableConcept.coding[x].display (separated by <br />) || Medication.code.text || Medication.code.coding[x].display (separated by <br />)
Status: MedicationRequest.status.display
Route: MedicationRequest.dosageInstruction[x].route.coding[x].display
Sig: MedicationRequest.dosageInstruction[x].text (display all sigs separated by <br /> )
Comments: MedicationRequest.note[x].text (display all notes separated by <br /> )
Authored Date: MedicationRequest.DateTime
Route: MedicationRequest.dosageInstruction[x].{ route.text || route.coding[x].display (separated by <br />) } (concatenate with comma, e.g. x, y, z)
Sig: MedicationRequest.dosageInstruction[x].text (display all sigs separated by <br />)
Comments: MedicationRequest.note[x].text (separated by <br />)
Authored Date: MedicationRequest.authoredOn
Table 2 MedicationStatement
Medication: MedicationStatement.medicationCodeableConcept.coding[x].display || Medication.code.coding.text || Medication.code.coding.code[x].display
Table 2 Medication Statements
Medication: MedicationStatement.medicationCodeableConcept.text || MedicationStatement.medicationCodeableConcept.coding[x].display (separated by <br />) || Medication.code.text || Medication.code.coding[x].display (separated by <br />)
Status: MedicationStatement.status.display
Route: MedicationStatement.dosage[x].route.coding[x].display
Sig: MedicationStatement.dosage[x].text (display all sigs separated by <br /> )
Date: MedicationStatement.effectiveDateTime
Route: MedicationStatement.dosage[x].{ route.text || route.coding[x].display (separated by <br />) } (concatenate with comma, e.g. x, y, z)
Sig: MedicationStatement.dosage[x].text (display all sigs separated by <br />)
Date: MedicationStatement.effectiveDateTime || MedicationStatement.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,9 +1,9 @@
<!--/* PastHistoryOfIllnesses -->
<!--
Medical Problem: Condition.code.text || Condition.code.coding[x].display
Status: Condition.clinicalStatus.coding[x].display
Comments: Condition.note[x].text (display all notes separated by <br /> )
Onset Date: Condition.onsetDateTime
Medical Problems: Condition.code.text || Condition.code.coding[x].display (separated by <br />)
Status: Condition.clinicalStatus.text || Condition.clinicalStatus.coding[x].display (separated by <br />)
Comments: Condition.note[x].text (separated by <br />)
Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -2,7 +2,7 @@
<!--
Activity: CarePlan.description
Intent: CarePlan.intent.code
Comments: CarePlan.dosage [x].text // Not dosaage but note... right?
Comments: CarePlan.note[x].text (separated by <br />)
Planned Start: CarePlan.period.start
Planned End: CarePlan.period.end
*/-->

View File

@ -1,9 +1,9 @@
<!--/* Pregnancy -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueDateTime || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
Code: Observation.code.text || Observation.code.coding[x].display (separated by <br />)
Result: Observation.valueQuantity || Observation.valueDateTime || Observation.valueCodeableConcept.text || Observation.valueCodeableConcept.coding[x].display (separated by <br />) || Observation.valueString
Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,9 +1,9 @@
<!--/* ProblemList -->
<!--
Medical Problem: Condition.code.text || Condition.code.coding[x].display
Status: Condition.clinicalStatus.coding[x].display
Comments: Condition.note[x].text (display all notes separated by <br /> )
Onset Date: Condition.onsetDateTime
Medical Problems: Condition.code.text || Condition.code.coding[x].display (separated by <br />)
Status: Condition.clinicalStatus.text || Condition.clinicalStatus.coding[x].display (separated by <br />)
Comments: Condition.note[x].text (separated by <br />)
Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,10 +1,10 @@
<!--/* SocialHistory -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Code: Observation.code.text || Observation.code.coding[x].display (separated by <br />)
Result: Observation.valueQuantity || Observation.valueDateTime || Observation.valueCodeableConcept.text || Observation.valueCodeableConcept.coding[x].display (separated by <br />) || Observation.valueString
Unit: Observation.valueQuantity.unit
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,11 +1,11 @@
<!--/* VitalSigns -->
<!--
Code: Observation.code.text || Observation.code.coding[x].display
Result: Observation.valueQuantity.value || Observation.valueCodeableConcept.coding[x].display || Observation.valueString
Code: Observation.code.text || Observation.code.coding[x].display (separated by <br />)
Result: Observation.valueQuantity || Observation.valueDateTime || Observation.valueCodeableConcept.text || Observation.valueCodeableConcept.coding[x].display (separated by <br />) || Observation.valueString
Unit: Observation.valueQuantity.unit
Interpretation: Observation.interpretation.text || Observation. interpretation.coding[x].display
Comments: Observation.note[x].text (display all notes separated by <br /> )
Date: Observation.effectiveDateTime
Interpretation: Observation.interpretation[0].text || Observation.interpretation[0].coding[x].display (separated by <br />)
Comments: Observation.note[x].text (separated by <br />)
Date: Observation.effectiveDateTime || Observation.effectivePeriod.start
*/-->
<div xmlns:th="http://www.thymeleaf.org">
<table class="hapiPropertyTable">

View File

@ -1,6 +1,9 @@
package ca.uhn.fhir.jpa.ips.generator;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy;
@ -9,11 +12,15 @@ import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Parameters;
@ -27,16 +34,25 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ContextConfiguration(classes = {IpsGenerationTest.IpsConfig.class})
public class IpsGenerationTest extends BaseResourceProviderR4Test {
/**
* This test uses a complete R4 JPA server as a backend and wires the
* {@link IpsOperationProvider} into the REST server to test the end-to-end
* IPS generation flow.
*/
@ContextConfiguration(classes = {IpsGenerationR4Test.IpsConfig.class})
public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
@Autowired
private IpsOperationProvider myIpsOperationProvider;
@ -74,13 +90,16 @@ public class IpsGenerationTest extends BaseResourceProviderR4Test {
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Verify
validateDocument(outcome);
validateDocument(output);
assertEquals(117, output.getEntry().size());
String patientId = findFirstEntryResource(output, Patient.class, 1).getId();
assertThat(patientId, matchesPattern("urn:uuid:.*"));
MedicationStatement medicationStatement = findFirstEntryResource(output, MedicationStatement.class, 2);
assertEquals(patientId, medicationStatement.getSubject().getReference());
assertNull(medicationStatement.getInformationSource().getReference());
List<String> sectionTitles = extractSectionTitles(output);
assertThat(sectionTitles.toString(), sectionTitles, contains("Allergies and Intolerances", "Medication List", "Problem List", "History of Immunizations", "Diagnostic Results"));
}
@Test
@ -106,19 +125,46 @@ public class IpsGenerationTest extends BaseResourceProviderR4Test {
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Verify
validateDocument(outcome);
validateDocument(output);
assertEquals(7, output.getEntry().size());
String patientId = findFirstEntryResource(output, Patient.class, 1).getId();
assertThat(patientId, matchesPattern("urn:uuid:.*"));
assertEquals(patientId, findEntryResource(output, Condition.class, 0, 2).getSubject().getReference());
assertEquals(patientId, findEntryResource(output, Condition.class, 1, 2).getSubject().getReference());
List<String> sectionTitles = extractSectionTitles(output);
assertThat(sectionTitles.toString(), sectionTitles, contains("Allergies and Intolerances", "Medication List", "Problem List"));
}
@Nonnull
private static List<String> extractSectionTitles(Bundle outcome) {
Composition composition = (Composition) outcome.getEntry().get(0).getResource();
List<String> sectionTitles = composition
.getSection()
.stream()
.map(Composition.SectionComponent::getTitle)
.toList();
return sectionTitles;
}
private void validateDocument(Bundle theOutcome) {
FhirValidator validator = myFhirContext.newValidator();
validator.registerValidatorModule(new FhirInstanceValidator(myFhirContext));
FhirInstanceValidator instanceValidator = new FhirInstanceValidator(myFhirContext);
instanceValidator.setValidationSupport(new ValidationSupportChain(new IpsTerminologySvc(), myFhirContext.getValidationSupport()));
validator.registerValidatorModule(instanceValidator);
ValidationResult validation = validator.validateWithResult(theOutcome);
assertTrue(validation.isSuccessful(), () -> myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(validation.toOperationOutcome()));
// Make sure that all refs have been replaced with UUIDs
List<ResourceReferenceInfo> references = myFhirContext.newTerser().getAllResourceReferences(theOutcome);
for (IBaseResource next : myFhirContext.newTerser().getAllEmbeddedResources(theOutcome, true)) {
references.addAll(myFhirContext.newTerser().getAllResourceReferences(next));
}
for (ResourceReferenceInfo next : references) {
if (!next.getResourceReference().getReferenceElement().getValue().startsWith("urn:uuid:")) {
fail(next.getName());
}
}
}
@Configuration
@ -159,4 +205,51 @@ public class IpsGenerationTest extends BaseResourceProviderR4Test {
return (T) resources.get(index);
}
/**
* This is a little fake terminology server that hardcodes the IPS terminology
* needed to validate these documents. This way we don't need to depend on a huge
* package.
*/
private class IpsTerminologySvc implements IValidationSupport {
@Override
public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
return true;
}
@Nullable
@Override
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
if ("http://loinc.org".equals(theCodeSystem)) {
if ("60591-5".equals(theCode)) {
return new CodeValidationResult().setCode(theCode);
}
}
if ("http://snomed.info/sct".equals(theCodeSystem)) {
if ("14657009".equals(theCode) || "255604002".equals(theCode)) {
return new CodeValidationResult().setCode(theCode);
}
}
return null;
}
@Nullable
@Override
public IBaseResource fetchCodeSystem(String theSystem) {
if ("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips".equals(theSystem)) {
CodeSystem cs = new CodeSystem();
cs.setUrl("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips");
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
cs.addConcept().setCode("no-allergy-info");
cs.addConcept().setCode("no-medication-info");
cs.addConcept().setCode("no-known-allergies");
return cs;
}
return null;
}
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
}
}

View File

@ -14,40 +14,10 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.test.utilities.HtmlUtil;
import ca.uhn.fhir.util.ClasspathUtil;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.gargoylesoftware.htmlunit.html.HtmlTableRow;
import com.gargoylesoftware.htmlunit.html.*;
import com.google.common.collect.Lists;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.AllergyIntolerance;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CarePlan;
import org.hl7.fhir.r4.model.ClinicalImpression;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.DeviceUseStatement;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.Medication;
import org.hl7.fhir.r4.model.MedicationAdministration;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.hl7.fhir.r4.model.MedicationRequest;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.PositiveIntType;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -61,17 +31,19 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationTest.findEntryResource;
import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationR4Test.findEntryResource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
/**
* This test verifies various IPS generation logic without using a full
* JPA backend.
*/
@ExtendWith(MockitoExtension.class)
public class IpsGeneratorSvcImplTest {
@ -103,6 +75,37 @@ public class IpsGeneratorSvcImplTest {
private IIpsGeneratorSvc mySvc;
private DefaultIpsGenerationStrategy myStrategy;
@Nonnull
private static List<String> toEntryResourceTypeStrings(Bundle outcome) {
return outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");
medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH);
return medicationStatement;
}
@BeforeEach
public void beforeEach() {
myDaoRegistry.setResourceDaos(Collections.emptyList());
@ -124,7 +127,7 @@ public class IpsGeneratorSvcImplTest {
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "MedicationStatement", "MedicationStatement", "Condition", "Condition", "Condition", "Organization"));
contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "MedicationStatement", "MedicationStatement", "Condition", "Condition", "Condition", "Organization"));
Composition composition = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section;
@ -145,7 +148,7 @@ public class IpsGeneratorSvcImplTest {
String compositionNarrative = composition.getText().getDivAsString();
ourLog.info("Composition narrative: {}", compositionNarrative);
assertThat(compositionNarrative, containsString("Allergies and Intolerances"));
assertThat(compositionNarrative, containsString("Pregnancy"));
assertThat(compositionNarrative, not(containsString("Pregnancy")));
}
@ -168,7 +171,7 @@ public class IpsGeneratorSvcImplTest {
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "Medication", "Condition", "Organization"));
contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "Medication", "Condition", "Organization"));
MedicationStatement actualMedicationStatement = (MedicationStatement) outcome.getEntry().get(3).getResource();
Medication actualMedication = (Medication) outcome.getEntry().get(4).getResource();
assertThat(actualMedication.getId(), startsWith("urn:uuid:"));
@ -226,7 +229,7 @@ public class IpsGeneratorSvcImplTest {
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains(
contains(
"Composition",
"Patient",
"MedicationStatement",
@ -265,7 +268,7 @@ public class IpsGeneratorSvcImplTest {
// Verify Bundle Contents
List<String> contentResourceTypes = toEntryResourceTypeStrings(outcome);
assertThat(contentResourceTypes.toString(), contentResourceTypes,
Matchers.contains(
contains(
"Composition",
"Patient",
"MedicationStatement",
@ -337,7 +340,7 @@ public class IpsGeneratorSvcImplTest {
// Setup Medication + MedicationStatement
Organization org = new Organization();
org.setId(new IdType("Organization/pfizer"));
org.setName("Pfizer");
org.setName("Pfizer Inc");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(org, BundleEntrySearchModeEnum.INCLUDE);
Immunization immunization = new Immunization();
@ -374,13 +377,12 @@ public class IpsGeneratorSvcImplTest {
assertEquals("SpikeVax", row.getCell(0).asNormalizedText());
assertEquals("COMPLETED", row.getCell(1).asNormalizedText());
assertEquals("2 , 4", row.getCell(2).asNormalizedText());
assertEquals("Pfizer", row.getCell(3).asNormalizedText());
assertEquals("Pfizer Inc", row.getCell(3).asNormalizedText());
assertEquals("35", row.getCell(4).asNormalizedText());
assertEquals("Hello World", row.getCell(5).asNormalizedText());
assertThat(row.getCell(6).asNormalizedText(), containsString("2023"));
}
@Test
public void testReferencesUpdatedInSecondaryInclusions() {
// Setup Patient
@ -422,6 +424,7 @@ public class IpsGeneratorSvcImplTest {
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome));
// Verify cross-references
Patient addedPatient = findEntryResource(outcome, Patient.class, 0, 1);
@ -452,6 +455,46 @@ public class IpsGeneratorSvcImplTest {
assertEquals(1, illnessHistorySection.getEntry().size());
}
@Test
public void testPatientIsReturnedAsAnIncludeResource() {
// Setup Patient
registerPatientDaoWithRead();
// Setup Condition
Condition conditionActive = new Condition();
conditionActive.setId("Condition/conditionActive");
conditionActive.getClinicalStatus().addCoding()
.setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical")
.setCode("active");
conditionActive.setSubject(new Reference(PATIENT_ID));
conditionActive.setEncounter(new Reference(ENCOUNTER_ID));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(conditionActive, BundleEntrySearchModeEnum.MATCH);
Patient patient = new Patient();
patient.setId(PATIENT_ID);
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(patient, BundleEntrySearchModeEnum.INCLUDE);
IFhirResourceDao<Condition> conditionDao = registerResourceDaoWithNoData(Condition.class);
when(conditionDao.search(any(), any())).thenReturn(
new SimpleBundleProvider(Lists.newArrayList(conditionActive, patient)),
new SimpleBundleProvider()
);
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
List<String> resources = outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
assertThat(resources.toString(), resources, contains(
"Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "Condition", "Organization"
));
}
private void registerPatientDaoWithRead() {
IFhirResourceDao<Patient> patientDao = registerResourceDaoWithNoData(Patient.class);
Patient patient = new Patient();
@ -499,35 +542,4 @@ public class IpsGeneratorSvcImplTest {
}
@Nonnull
private static List<String> toEntryResourceTypeStrings(Bundle outcome) {
return outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");
medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH);
return medicationStatement;
}
}

View File

@ -0,0 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>