From ba1cff6c54439d0a65f340d3dcd3d9fe8b811139 Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Wed, 7 Jan 2026 17:34:08 +0300 Subject: [PATCH 1/6] #13771 - Add Epipulse export functionality for Measles disease --- .../EpipulseDiseaseExportEntryDto.java | 285 +++++++++ .../epipulse/EpipulseDiseaseExportResult.java | 47 ++ .../epipulse/EpipulseLaboratoryMapper.java | 290 +++++++++ .../api/epipulse/EpipulseSubjectCode.java | 3 +- .../referencevalue/EpipulseDiseaseRef.java | 3 +- sormas-api/src/main/resources/enum.properties | 7 +- .../EpipulseDiseaseExportFacadeEjb.java | 253 ++++++++ .../EpipulseDiseaseExportService.java | 603 ++++++++++++++++++ .../epipulse/EpipulseExportTimerEjb.java | 3 + 9 files changed, 1491 insertions(+), 3 deletions(-) create mode 100644 sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportEntryDto.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportEntryDto.java index 026696a1d67..8fcbee93860 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportEntryDto.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportEntryDto.java @@ -82,6 +82,27 @@ public class EpipulseDiseaseExportEntryDto { private List immunizations; private List vaccinations; + // MEAS-specific laboratory fields (can be mapped from existing SORMAS data) + private Date dateOfSpecimen; + private Date dateOfLaboratoryResult; + private List typeOfSpecimenCollected; // SampleMaterial mapped to EpiPulse codes (repeatable) + private String resultOfVirusDetection; // PathogenTestResultType mapped to POS/NEG/EQUI/NOTEST + private String genotype; // PathogenTest typingId/genoTypeResult + private List typeOfSpecimenSerology; // SampleMaterial for serology tests (repeatable) + private String resultIgG; // IgG test result + private String resultIgM; // IgM test result + + // Phase 3: Clinical and epidemiology fields (mapped from existing SORMAS data) + private Date dateOfInvestigation; // CaseDataDto.investigatedDate + private Boolean clusterRelated; // EpiDataDto.clusterRelated + private String clusterIdentification; // EpiDataDto.clusterTypeText + private List clusterSetting; // EpiDataDto.clusterType mapped to EpiPulse codes (repeatable) + private String importedStatus; // EpiDataDto.caseImportedStatus mapped to EpiPulse codes + private List complicationDiagnosis; // SymptomsDto complications (repeatable) + private Boolean clinicalCriteriaStatus; // Derived from CaseDataDto.clinicalConfirmation + private List placeOfInfection; // EpiDataDto.exposures locations (repeatable) + private String causeOfDeath; // PersonDto.causeOfDeathDetails + public String getReportingCountry() { return reportingCountry; } @@ -326,6 +347,70 @@ public void setVaccinations(List vaccinations) { this.vaccinations = vaccinations; } + public Date getDateOfSpecimen() { + return dateOfSpecimen; + } + + public void setDateOfSpecimen(Date dateOfSpecimen) { + this.dateOfSpecimen = dateOfSpecimen; + } + + public Date getDateOfLaboratoryResult() { + return dateOfLaboratoryResult; + } + + public void setDateOfLaboratoryResult(Date dateOfLaboratoryResult) { + this.dateOfLaboratoryResult = dateOfLaboratoryResult; + } + + public List getTypeOfSpecimenCollected() { + return typeOfSpecimenCollected; + } + + public void setTypeOfSpecimenCollected(List typeOfSpecimenCollected) { + this.typeOfSpecimenCollected = typeOfSpecimenCollected; + } + + public String getResultOfVirusDetection() { + return resultOfVirusDetection; + } + + public void setResultOfVirusDetection(String resultOfVirusDetection) { + this.resultOfVirusDetection = resultOfVirusDetection; + } + + public String getGenotype() { + return genotype; + } + + public void setGenotype(String genotype) { + this.genotype = genotype; + } + + public List getTypeOfSpecimenSerology() { + return typeOfSpecimenSerology; + } + + public void setTypeOfSpecimenSerology(List typeOfSpecimenSerology) { + this.typeOfSpecimenSerology = typeOfSpecimenSerology; + } + + public String getResultIgG() { + return resultIgG; + } + + public void setResultIgG(String resultIgG) { + this.resultIgG = resultIgG; + } + + public String getResultIgM() { + return resultIgM; + } + + public void setResultIgM(String resultIgM) { + this.resultIgM = resultIgM; + } + public String getDiseaseForCsv() { return EpipulseDiseaseRef.getBySubjectCode(subjectCode).name(); } @@ -365,6 +450,7 @@ public String getAgeForCsv() { public String getAgeMonthForCsv() { switch (subjectCode) { case PERT: + case MEAS: if (ageYears != null && ageYears < 2) { return ageMonths == null ? null : ageMonths.toString(); } @@ -384,6 +470,7 @@ public String getGenderForCsv() { public String getPlaceOfResidenceForCsv() { switch (subjectCode) { case PERT: + case MEAS: if (addressCommunityNutsCode != null && !addressCommunityNutsCode.isEmpty()) { return addressCommunityNutsCode; } else if (addressDistrictNutsCode != null && !addressDistrictNutsCode.isEmpty()) { @@ -400,6 +487,7 @@ public String getPlaceOfResidenceForCsv() { public String getPlaceOfNotificationForCsv() { switch (subjectCode) { case PERT: + case MEAS: if (responsibleCommunityNutsCode != null && !responsibleCommunityNutsCode.isEmpty()) { return responsibleCommunityNutsCode; } else if (responsibleDistrictNutsCode != null && !responsibleDistrictNutsCode.isEmpty()) { @@ -529,6 +617,203 @@ public String getGestationalAgeAtVaccinationForCsv() { return null; } + // MEAS-specific laboratory field CSV getters + public String getDateOfSpecimenForCsv() { + return formatDateForCsv(dateOfSpecimen); + } + + public String getDateOfLaboratoryResultForCsv() { + return formatDateForCsv(dateOfLaboratoryResult); + } + + public List getTypeOfSpecimenCollectedForCsv(int maxSpecimenVirDetect) { + // Repeatable field - return list padded to max length + List specimens = new ArrayList<>(); + for (int i = 0; i < maxSpecimenVirDetect; i++) { + if (typeOfSpecimenCollected != null && i < typeOfSpecimenCollected.size()) { + specimens.add(typeOfSpecimenCollected.get(i)); + } else { + specimens.add(""); + } + } + return specimens; + } + + public String getResultOfVirusDetectionForCsv() { + // Already mapped to EpiPulse codes (POS/NEG/EQUI/NOTEST) + return resultOfVirusDetection; + } + + public String getGenotypeForCsv() { + // Already mapped to EpiPulse genotype codes (MEASV_A, MEASV_B1, etc.) + return genotype; + } + + public List getTypeOfSpecimenSerologyForCsv(int maxSpecimenSero) { + // Repeatable field - return list padded to max length + List specimens = new ArrayList<>(); + for (int i = 0; i < maxSpecimenSero; i++) { + if (typeOfSpecimenSerology != null && i < typeOfSpecimenSerology.size()) { + specimens.add(typeOfSpecimenSerology.get(i)); + } else { + specimens.add(""); + } + } + return specimens; + } + + public String getResultIgGForCsv() { + // Already mapped to EpiPulse codes (POS/NEG/EQUI/NOTEST) + return resultIgG; + } + + public String getResultIgMForCsv() { + // Already mapped to EpiPulse codes (POS/NEG/EQUI/NOTEST) + return resultIgM; + } + + // Phase 3: Getters and setters for clinical and epidemiology fields + public Date getDateOfInvestigation() { + return dateOfInvestigation; + } + + public void setDateOfInvestigation(Date dateOfInvestigation) { + this.dateOfInvestigation = dateOfInvestigation; + } + + public Boolean getClusterRelated() { + return clusterRelated; + } + + public void setClusterRelated(Boolean clusterRelated) { + this.clusterRelated = clusterRelated; + } + + public String getClusterIdentification() { + return clusterIdentification; + } + + public void setClusterIdentification(String clusterIdentification) { + this.clusterIdentification = clusterIdentification; + } + + public List getClusterSetting() { + return clusterSetting; + } + + public void setClusterSetting(List clusterSetting) { + this.clusterSetting = clusterSetting; + } + + public String getImportedStatus() { + return importedStatus; + } + + public void setImportedStatus(String importedStatus) { + this.importedStatus = importedStatus; + } + + public List getComplicationDiagnosis() { + return complicationDiagnosis; + } + + public void setComplicationDiagnosis(List complicationDiagnosis) { + this.complicationDiagnosis = complicationDiagnosis; + } + + public Boolean getClinicalCriteriaStatus() { + return clinicalCriteriaStatus; + } + + public void setClinicalCriteriaStatus(Boolean clinicalCriteriaStatus) { + this.clinicalCriteriaStatus = clinicalCriteriaStatus; + } + + public List getPlaceOfInfection() { + return placeOfInfection; + } + + public void setPlaceOfInfection(List placeOfInfection) { + this.placeOfInfection = placeOfInfection; + } + + public String getCauseOfDeath() { + return causeOfDeath; + } + + public void setCauseOfDeath(String causeOfDeath) { + this.causeOfDeath = causeOfDeath; + } + + // Phase 3: CSV getter methods + public String getDateOfInvestigationForCsv() { + return formatDateForCsv(dateOfInvestigation); + } + + public String getClusterRelatedForCsv() { + return clusterRelated != null ? String.valueOf(clusterRelated) : ""; + } + + public String getClusterIdentificationForCsv() { + return clusterIdentification != null ? clusterIdentification : ""; + } + + public List getClusterSettingForCsv(int maxClusterSettings) { + // Repeatable field - return list padded to max length + List settings = new ArrayList<>(); + for (int i = 0; i < maxClusterSettings; i++) { + if (clusterSetting != null && i < clusterSetting.size()) { + settings.add(clusterSetting.get(i)); + } else { + settings.add(""); + } + } + return settings; + } + + public String getImportedStatusForCsv() { + return importedStatus != null ? importedStatus : ""; + } + + public List getComplicationDiagnosisForCsv(int maxComplicationDiagnosis) { + // Repeatable field - return list padded to max length + List complications = new ArrayList<>(); + for (int i = 0; i < maxComplicationDiagnosis; i++) { + if (complicationDiagnosis != null && i < complicationDiagnosis.size()) { + complications.add(complicationDiagnosis.get(i)); + } else { + // Use "NONE" for first empty slot if no complications, otherwise empty + if (i == 0 && (complicationDiagnosis == null || complicationDiagnosis.isEmpty())) { + complications.add("NONE"); + } else { + complications.add(""); + } + } + } + return complications; + } + + public String getClinicalCriteriaStatusForCsv() { + return clinicalCriteriaStatus != null ? String.valueOf(clinicalCriteriaStatus) : ""; + } + + public List getPlaceOfInfectionForCsv(int maxPlaceOfInfection) { + // Repeatable field - return list padded to max length + List places = new ArrayList<>(); + for (int i = 0; i < maxPlaceOfInfection; i++) { + if (placeOfInfection != null && i < placeOfInfection.size()) { + places.add(placeOfInfection.get(i)); + } else { + places.add(""); + } + } + return places; + } + + public String getCauseOfDeathForCsv() { + return causeOfDeath != null ? causeOfDeath : ""; + } + public void calculateAge() { if (symptomOnsetDate == null || yearOfBirth == null || monthOfBirth == null || dayOfBirth == null) { return; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportResult.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportResult.java index d543c307caa..d1118a428d9 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportResult.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportResult.java @@ -23,6 +23,13 @@ public class EpipulseDiseaseExportResult { private int maxImmunizations; private List exportEntryList; + // MEAS repeatable field max counts + private int maxComplicationDiagnosis; + private int maxClusterSettings; + private int maxPlaceOfInfection; + private int maxSpecimenVirDetect; + private int maxSpecimenSero; + public int getMaxPathogenTests() { return maxPathogenTests; } @@ -46,4 +53,44 @@ public List getExportEntryList() { public void setExportEntryList(List exportEntryList) { this.exportEntryList = exportEntryList; } + + public int getMaxComplicationDiagnosis() { + return maxComplicationDiagnosis; + } + + public void setMaxComplicationDiagnosis(int maxComplicationDiagnosis) { + this.maxComplicationDiagnosis = maxComplicationDiagnosis; + } + + public int getMaxClusterSettings() { + return maxClusterSettings; + } + + public void setMaxClusterSettings(int maxClusterSettings) { + this.maxClusterSettings = maxClusterSettings; + } + + public int getMaxPlaceOfInfection() { + return maxPlaceOfInfection; + } + + public void setMaxPlaceOfInfection(int maxPlaceOfInfection) { + this.maxPlaceOfInfection = maxPlaceOfInfection; + } + + public int getMaxSpecimenVirDetect() { + return maxSpecimenVirDetect; + } + + public void setMaxSpecimenVirDetect(int maxSpecimenVirDetect) { + this.maxSpecimenVirDetect = maxSpecimenVirDetect; + } + + public int getMaxSpecimenSero() { + return maxSpecimenSero; + } + + public void setMaxSpecimenSero(int maxSpecimenSero) { + this.maxSpecimenSero = maxSpecimenSero; + } } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java new file mode 100644 index 00000000000..c6acc1b3799 --- /dev/null +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java @@ -0,0 +1,290 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.api.epipulse; + +import java.util.ArrayList; +import java.util.List; + +import de.symeda.sormas.api.epidata.CaseImportedStatus; +import de.symeda.sormas.api.epidata.ClusterType; +import de.symeda.sormas.api.sample.PathogenTestResultType; +import de.symeda.sormas.api.sample.SampleMaterial; +import de.symeda.sormas.api.symptoms.SymptomState; +import de.symeda.sormas.api.utils.YesNoUnknown; + +/** + * Utility class for mapping SORMAS laboratory and epidemiology data to EpiPulse codes for MEAS export. + * Based on metadata analysis from 20250929_EpiPulse_CasesMetadata_mapped.xlsx + */ +public class EpipulseLaboratoryMapper { + + /** + * Maps SORMAS SampleMaterial enum to EpiPulse specimen type codes. + *

+ * EpiPulse Reference Values: + * - DRYBLOSP = Dry blood spot + * - EDTA = EDTA whole blood + * - NASALSWAB = Nasal swab + * - OTH = Other + * - SALOR = Saliva/oral fluid + * - SER = Serum + * - URINE = Urine + * + * @param sampleMaterial + * SORMAS sample material enum + * @return EpiPulse specimen type code, or null if not mappable + */ + public static String mapSampleMaterialToEpipulseCode(SampleMaterial sampleMaterial) { + if (sampleMaterial == null) { + return null; + } + + switch (sampleMaterial) { + case BLOOD: + case SERA: + return "SER"; // Serum + case URINE: + return "URINE"; + case NASAL_SWAB: + return "NASALSWAB"; + case THROAT_SWAB: + case RECTAL_SWAB: + case OTHER: + return "OTH"; + case SALIVA: + return "SALOR"; // Saliva/oral fluid + case EDTA_WHOLE_BLOOD: + return "EDTA"; // EDTA whole blood + default: + return "OTH"; + } + } + + /** + * Maps SORMAS PathogenTestResultType enum to EpiPulse test result codes. + *

+ * EpiPulse Reference Values: + * - EQUI = Equivocal + * - NEG = Negative + * - NOTEST = Not tested + * - POS = Positive + * + * @param testResult + * SORMAS test result enum + * @return EpiPulse result code (POS/NEG/EQUI/NOTEST) + */ + public static String mapTestResultToEpipulseCode(PathogenTestResultType testResult) { + if (testResult == null) { + return "NOTEST"; + } + + switch (testResult) { + case POSITIVE: + return "POS"; + case NEGATIVE: + return "NEG"; + case INDETERMINATE: + return "EQUI"; + case PENDING: + case NOT_DONE: + return "NOTEST"; + default: + return "NOTEST"; + } + } + + /** + * Validates and normalizes genotype string to match EpiPulse reference values. + *

+ * EpiPulse accepts 49 measles virus genotypes: MEASV_A, MEASV_B1, MEASV_B2, MEASV_B3, + * MEASV_C1, MEASV_C2, MEASV_D1-D11, MEASV_E, MEASV_F, MEASV_G1-G3, MEASV_H1-H2, etc. + * + * @param genotypeText + * SORMAS genotype string (from typingId or genoTypeResult) + * @return Normalized EpiPulse genotype code, or null if not a valid measles genotype + */ + public static String normalizeGenotypeForEpipulse(String genotypeText) { + if (genotypeText == null || genotypeText.trim().isEmpty()) { + return null; + } + + String normalized = genotypeText.trim().toUpperCase(); + + // If already in MEASV_ format, return as-is + if (normalized.startsWith("MEASV_")) { + return normalized; + } + + // Try to parse formats like "A", "B1", "D10", etc. and add MEASV_ prefix + if (normalized.matches("^[A-Z]\\d*$")) { + return "MEASV_" + normalized; + } + + // Try to parse formats like "Genotype A", "MeV-A", etc. + if (normalized.contains("A")) { + return "MEASV_A"; + } + if (normalized.matches(".*B[12]?.*")) { + if (normalized.contains("B1")) { + return "MEASV_B1"; + } else if (normalized.contains("B2")) { + return "MEASV_B2"; + } else if (normalized.contains("B3")) { + return "MEASV_B3"; + } + return "MEASV_B1"; // Default to B1 + } + + // If we can't parse it, return null (field will be empty in CSV) + return null; + } + + /** + * Maps SORMAS ClusterType enum to EpiPulse cluster setting codes. + *

+ * EpiPulse Reference Values: + * - CHILDCARE = Childcare setting + * - FAM = Family + * - MIL = Military + * - NOS = Nosocomial (healthcare) + * - OTH = Other + * - SCH = School + * - SPORT = Sports team + * - UNI = University + * + * @param clusterType + * SORMAS cluster type enum + * @return EpiPulse cluster setting code + */ + public static String mapClusterTypeToEpipulseCode(ClusterType clusterType) { + if (clusterType == null) { + return null; + } + + switch (clusterType) { + case KINDERGARTEN_OR_CHILDCARE: + return "CHILDCARE"; + case FAMILY: + return "FAM"; + case MILITARY: + return "MIL"; + case NOSOCOMIAL: + return "NOS"; + case SCHOOL: + return "SCH"; + case SPORTS_TEAM: + return "SPORT"; + case UNIVERSITY: + return "UNI"; + case OTHER: + return "OTH"; + default: + return null; + } + } + + /** + * Maps SORMAS CaseImportedStatus enum to EpiPulse imported status codes. + *

+ * EpiPulse Reference Values: + * - AUTOCH = Autochthonous (not imported case) + * - IMP = Imported case + * - IMPR = Import-related case + * - UNK = Unknown importation status + * + * @param importedStatus + * SORMAS case imported status enum + * @return EpiPulse imported status code + */ + public static String mapCaseImportedStatusToEpipulseCode(CaseImportedStatus importedStatus) { + if (importedStatus == null) { + return null; + } + + switch (importedStatus) { + case IMPORTED_CASE: + return "IMP"; + case IMPORT_RELATED_CASE: + return "IMPR"; + case UNKNOWN_IMPORTATION_STATUS: + return "UNK"; + case NOT_IMPORTED_CASE: + return "AUTOCH"; + default: + return null; + } + } + + /** + * Maps SORMAS Symptoms complication fields to EpiPulse complication diagnosis codes. + *

+ * EpiPulse Reference Values: + * - ACENCE = Acute encephalitis + * - DIARR = Diarrhea + * - NONE = No complications + * - OME = Otitis media + * - OTH = Other + * - PNEU = Pneumonia + * + * @param acuteEncephalitis + * Acute encephalitis symptom state + * @param diarrhea + * Diarrhea symptom state + * @param otitisMedia + * Otitis media symptom state + * @param otherComplications + * Other complications symptom state + * @return List of EpiPulse complication codes (empty list returns "NONE" in CSV) + */ + public static List mapSymptomsToComplicationCodes( + SymptomState acuteEncephalitis, + SymptomState diarrhea, + SymptomState otitisMedia, + SymptomState otherComplications) { + + List complications = new ArrayList<>(); + + if (acuteEncephalitis == SymptomState.YES) { + complications.add("ACENCE"); + } + if (diarrhea == SymptomState.YES) { + complications.add("DIARR"); + } + if (otitisMedia == SymptomState.YES) { + complications.add("OME"); + } + if (otherComplications == SymptomState.YES) { + complications.add("OTH"); + } + + // Note: PNEU (pneumonia) not currently available in SORMAS Symptoms for MEAS + // If no complications found, empty list will result in "NONE" in CSV + + return complications; + } + + /** + * Derives clinical criteria status from clinical confirmation field. + * Clinical criteria are considered met if case has clinical confirmation = YES. + * + * @param clinicalConfirmation + * SORMAS clinical confirmation field + * @return true if clinically confirmed, false otherwise + */ + public static Boolean deriveClinicalCriteriaStatus(YesNoUnknown clinicalConfirmation) { + return clinicalConfirmation == YesNoUnknown.YES; + } +} diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseSubjectCode.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseSubjectCode.java index 6b8e83f526b..6f3af3bcedc 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseSubjectCode.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseSubjectCode.java @@ -23,7 +23,8 @@ public enum EpipulseSubjectCode { - PERT(true, Disease.PERTUSSIS, false); + PERT(true, Disease.PERTUSSIS, false), + MEAS(true, Disease.MEASLES, false); private final boolean diseaseModel; private final Disease disease; diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/referencevalue/EpipulseDiseaseRef.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/referencevalue/EpipulseDiseaseRef.java index 177eb7425d2..636d0f67153 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/referencevalue/EpipulseDiseaseRef.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/referencevalue/EpipulseDiseaseRef.java @@ -20,7 +20,8 @@ public enum EpipulseDiseaseRef { - PERT(EpipulseSubjectCode.PERT); + PERT(EpipulseSubjectCode.PERT), + MEAS(EpipulseSubjectCode.MEAS); private final EpipulseSubjectCode[] subjectCodes; diff --git a/sormas-api/src/main/resources/enum.properties b/sormas-api/src/main/resources/enum.properties index 41671c60bd7..069789a8a1c 100644 --- a/sormas-api/src/main/resources/enum.properties +++ b/sormas-api/src/main/resources/enum.properties @@ -2859,4 +2859,9 @@ EpipulseExportStatus.FAILED=Failed EpipulseExportStatus.CANCELLED=Cancelled # EpipulseSubjectCode -EpipulseSubjectCode.PERT = Pertussis \ No newline at end of file +EpipulseSubjectCode.PERT = Pertussis +EpipulseSubjectCode.MEAS = Measles + +# EpipulseDiseaseRef +EpipulseDiseaseRef.PERT = Pertussis +EpipulseDiseaseRef.MEAS = Measles \ No newline at end of file diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java index 6a988bedf8f..c700548dabd 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java @@ -225,6 +225,259 @@ public void startPertussisExport(String uuid) { } } + public void startMeaslesExport(String uuid) { + + CSVWriter writer = null; + EpipulseExport epipulseExport = null; + EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; + boolean shouldUpdateStatus = false; + + Integer totalRecords = null; + BigDecimal exportFileSizeBytes = null; + String exportFileName = null; + String exportFilePath = null; + + try { + epipulseExport = epipulseExportService.getByUuid(uuid); + + if (epipulseExport == null) { + logger.error("EpipulseExport with uuid " + uuid + " not found"); + return; + } + + if (epipulseExport.getStatus() != EpipulseExportStatus.PENDING) { + logger.error("EpipulseExport with uuid " + uuid + " is not in status PENDING"); + return; + } + + shouldUpdateStatus = true; + + diseaseExportService.updateStatusForBackgroundProcess(uuid, EpipulseExportStatus.IN_PROGRESS, null, null, null); + + EpipulseExportDto exportDto = epipulseExportEjb.toEpipulseExportDto(epipulseExport); + + String serverCountryLocale = configFacadeEjb.getCountryLocale(); + String serverCountryCode = configFacadeEjb.getCountryCode(); + String serverCountryName = configFacadeEjb.getCountryName(); + + String generatedFilesPath = configFacadeEjb.getGeneratedFilesPath(); + exportFileName = diseaseExportService.generateDownloadFileName(exportDto, epipulseExport.getId()); + exportFilePath = generatedFilesPath + "/" + exportFileName; + + EpipulseDiseaseExportResult exportResult = diseaseExportService.exportMeaslesCaseBased(exportDto, serverCountryCode, serverCountryName); + totalRecords = exportResult.getExportEntryList().size(); + + writer = CSVUtils.createCSVWriter( + new OutputStreamWriter(new FileOutputStream(exportFilePath), StandardCharsets.UTF_8), + configFacadeEjb.getCsvSeparator()); + + // MEAS CSV columns - including Phase 2 laboratory fields and Phase 3 clinical/epidemiology fields + List columnNames = new ArrayList<>( + List.of( + "Disease", + "ReportingCountry", + "Status", + "SubjectCode", + "NationalRecordId", + "DataSource", + "DateUsedForStatistics", + "Age", + "AgeMonth", + "Gender", + "CaseClassification", + "DateOfOnset", + "DateOfNotification", + "Hospitalisation", + "Outcome", + "PlaceOfNotification", + "PlaceOfResidence", + "DateOfSpecimen", + "DateOfLaboratoryResult")); + + // Repeatable field: TypeOfSpecimenCollected + if (exportResult.getMaxSpecimenVirDetect() > 0) { + for (int i = 1; i <= exportResult.getMaxSpecimenVirDetect(); i++) { + columnNames.add("TypeOfSpecimenCollected"); + } + } + + columnNames.addAll(List.of("ResultOfVirusDetection", "Genotype")); + + // Repeatable field: TypeOfSpecimenForSerologicalAnalysis + if (exportResult.getMaxSpecimenSero() > 0) { + for (int i = 1; i <= exportResult.getMaxSpecimenSero(); i++) { + columnNames.add("TypeOfSpecimenForSerologicalAnalysis"); + } + } + + columnNames.addAll(List.of("ResultIgG", "ResultIgM", "DateOfInvestigation", "ClusterRelated", "ClusterIdentification")); + + // Repeatable field: ClusterSetting + if (exportResult.getMaxClusterSettings() > 0) { + for (int i = 1; i <= exportResult.getMaxClusterSettings(); i++) { + columnNames.add("ClusterSetting"); + } + } + + columnNames.add("ImportedStatus"); + + // Repeatable field: ComplicationDiagnosis + if (exportResult.getMaxComplicationDiagnosis() > 0) { + for (int i = 1; i <= exportResult.getMaxComplicationDiagnosis(); i++) { + columnNames.add("ComplicationDiagnosis"); + } + } + + columnNames.add("ClinicalCriteriaStatus"); + + // Repeatable field: PlaceOfInfection + if (exportResult.getMaxPlaceOfInfection() > 0) { + for (int i = 1; i <= exportResult.getMaxPlaceOfInfection(); i++) { + columnNames.add("PlaceOfInfection"); + } + } + + columnNames.add("CauseOfDeath"); + + if (exportResult.getMaxImmunizations() > 0) { + columnNames.add("DateOfLastVaccination"); + } + + columnNames.add("VaccinationStatus"); + + //write the headers + writer.writeNext(columnNames.toArray(new String[columnNames.size()])); + + //write entries + String[] exportLine = new String[columnNames.size()]; + int index; + for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { + index = -1; + + exportLine[++index] = dto.getDiseaseForCsv(); + exportLine[++index] = dto.getReportingCountryForCsv(); + exportLine[++index] = dto.getStatusForCsv(); + exportLine[++index] = dto.getSubjectCodeForCsv(); + exportLine[++index] = dto.getNationalRecordIdForCsv(); + exportLine[++index] = dto.getDataSourceForCsv(); + exportLine[++index] = dto.getDateUsedForStatisticsCsv(); + exportLine[++index] = dto.getAgeForCsv(); + exportLine[++index] = dto.getAgeMonthForCsv(); + exportLine[++index] = dto.getGenderForCsv(); + exportLine[++index] = dto.getCaseClassificationForCsv(); + exportLine[++index] = dto.getDateOfOnsetForCsv(); + exportLine[++index] = dto.getDateOfNotificationForCsv(); + exportLine[++index] = dto.getHospitalizationForCsv(); + exportLine[++index] = dto.getOutcomeForCsv(); + exportLine[++index] = dto.getPlaceOfNotificationForCsv(); + exportLine[++index] = dto.getPlaceOfResidenceForCsv(); + + // Phase 2: Laboratory fields + exportLine[++index] = dto.getDateOfSpecimenForCsv(); + exportLine[++index] = dto.getDateOfLaboratoryResultForCsv(); + + // Repeatable: TypeOfSpecimenCollected + if (exportResult.getMaxSpecimenVirDetect() > 0) { + List specimenCollected = dto.getTypeOfSpecimenCollectedForCsv(exportResult.getMaxSpecimenVirDetect()); + for (String specimen : specimenCollected) { + exportLine[++index] = specimen; + } + } + + exportLine[++index] = dto.getResultOfVirusDetectionForCsv(); + exportLine[++index] = dto.getGenotypeForCsv(); + + // Repeatable: TypeOfSpecimenForSerologicalAnalysis + if (exportResult.getMaxSpecimenSero() > 0) { + List specimenSerology = dto.getTypeOfSpecimenSerologyForCsv(exportResult.getMaxSpecimenSero()); + for (String specimen : specimenSerology) { + exportLine[++index] = specimen; + } + } + + exportLine[++index] = dto.getResultIgGForCsv(); + exportLine[++index] = dto.getResultIgMForCsv(); + + // Phase 3: Clinical and epidemiology fields + exportLine[++index] = dto.getDateOfInvestigationForCsv(); + exportLine[++index] = dto.getClusterRelatedForCsv(); + exportLine[++index] = dto.getClusterIdentificationForCsv(); + + // Repeatable: ClusterSetting + if (exportResult.getMaxClusterSettings() > 0) { + List clusterSettings = dto.getClusterSettingForCsv(exportResult.getMaxClusterSettings()); + for (String setting : clusterSettings) { + exportLine[++index] = setting; + } + } + + exportLine[++index] = dto.getImportedStatusForCsv(); + + // Repeatable: ComplicationDiagnosis + if (exportResult.getMaxComplicationDiagnosis() > 0) { + List complications = dto.getComplicationDiagnosisForCsv(exportResult.getMaxComplicationDiagnosis()); + for (String complication : complications) { + exportLine[++index] = complication; + } + } + + exportLine[++index] = dto.getClinicalCriteriaStatusForCsv(); + + // Repeatable: PlaceOfInfection + if (exportResult.getMaxPlaceOfInfection() > 0) { + List placesOfInfection = dto.getPlaceOfInfectionForCsv(exportResult.getMaxPlaceOfInfection()); + for (String place : placesOfInfection) { + exportLine[++index] = place; + } + } + + exportLine[++index] = dto.getCauseOfDeathForCsv(); + + if (exportResult.getMaxImmunizations() > 0) { + exportLine[++index] = dto.getDateOfLastVaccinationForCsv(); + } + + exportLine[++index] = dto.getVaccinationStatusForCsv(); + + writer.writeNext(exportLine); + } + + exportStatus = EpipulseExportStatus.COMPLETED; + } catch (Exception e) { + exportStatus = EpipulseExportStatus.FAILED; + + logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (Exception e) { + logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); + } + } + + // Calculate file size after writer is closed + if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { + try { + long fileSizeInBytes = Files.size(Paths.get(exportFilePath)); + exportFileSizeBytes = new BigDecimal(fileSizeInBytes); + logger.info("Export file size for uuid {}: {} bytes", uuid, fileSizeInBytes); + } catch (Exception e) { + logger.error("CRITICAL: Failed to calculate file size for uuid {}: {}", uuid, e.getMessage(), e); + } + } + + if (shouldUpdateStatus && epipulseExport != null) { + try { + diseaseExportService + .updateStatusForBackgroundProcess(epipulseExport.getUuid(), exportStatus, totalRecords, exportFileName, exportFileSizeBytes); + } catch (Exception e) { + logger.error("CRITICAL: Failed to update export status for uuid " + uuid + ": " + e.getMessage(), e); + } + } + } + } + @LocalBean @Stateless public static class EpipulseDiseaseExportFacadeEjbLocal extends EpipulseDiseaseExportFacadeEjb { diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java index e08ab82a46d..c8c9b71c2ab 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java @@ -36,16 +36,22 @@ import de.symeda.sormas.api.caze.CaseClassification; import de.symeda.sormas.api.caze.CaseOutcome; +import de.symeda.sormas.api.epidata.CaseImportedStatus; +import de.symeda.sormas.api.epidata.ClusterType; import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; import de.symeda.sormas.api.epipulse.EpipulseExportDto; import de.symeda.sormas.api.epipulse.EpipulseExportStatus; +import de.symeda.sormas.api.epipulse.EpipulseLaboratoryMapper; import de.symeda.sormas.api.epipulse.EpipulseSubjectCode; import de.symeda.sormas.api.epipulse.referencevalue.EpipulsePathogenTestTypeRef; import de.symeda.sormas.api.hospitalization.HospitalizationReasonType; import de.symeda.sormas.api.immunization.MeansOfImmunization; import de.symeda.sormas.api.person.Sex; +import de.symeda.sormas.api.sample.PathogenTestResultType; import de.symeda.sormas.api.sample.PathogenTestType; +import de.symeda.sormas.api.symptoms.SymptomState; +import de.symeda.sormas.api.sample.SampleMaterial; import de.symeda.sormas.api.utils.DateHelper; import de.symeda.sormas.api.utils.YesNoUnknown; import de.symeda.sormas.backend.util.ModelConstants; @@ -387,6 +393,591 @@ public EpipulseDiseaseExportResult exportPertussisCaseBased(EpipulseExportDto ex return exportResult; } + public EpipulseDiseaseExportResult exportMeaslesCaseBased(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) + throws SQLException, IllegalStateException, IllegalArgumentException { + + EpipulseDiseaseExportResult exportResult = new EpipulseDiseaseExportResult(); + + try { + //lookup reporting country + //@formatter:off + String reportingCountryQuery = + "select code as reporting_country " + + "from epipulse_location_configuration " + + "where type='Country' and country_iso2_code = :countryIso2Code"; + //@formatter:on + + @SuppressWarnings("unchecked") + String reportingCountry = (String) em.createNativeQuery(reportingCountryQuery) + .setParameter("countryIso2Code", serverCountryLocale) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + if (StringUtils.isBlank(reportingCountry)) { + throw new IllegalArgumentException("Invalid server country code: " + serverCountryLocale); + } + + //lookup server country nuts code + //@formatter:off + String serverCountryQuery = + "select nutscode " + + "from country " + + "where lower(defaultname) = :countryName"; + //@formatter:on + + @SuppressWarnings("unchecked") + String serverCountryNutsCode = (String) em.createNativeQuery(serverCountryQuery) + .setParameter("countryName", serverCountryName.toLowerCase()) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + //get subject code + //@formatter:off + String subjectCodeQuery = + "select subjectcode " + + "from epipulse_subjectcode_configuration " + + "where disease=:disease and aggregatedreporting='No'"; + //@formatter:on + + @SuppressWarnings("unchecked") + String subjectCode = (String) em.createNativeQuery(subjectCodeQuery) + .setParameter("disease", exportDto.getSubjectCode().name()) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + if (StringUtils.isBlank(subjectCode)) { + throw new IllegalStateException("Subject code is empty"); + } + + //@formatter:off + String diseaseExportQuery = + "WITH variables AS (SELECT :disease AS disease," + + " :subjectCode AS subject_code," + + " :countryLocale AS country_locale," + + " CAST(:startDate AS date) AS start_date," + + " CAST(:endDate AS date) AS end_date)," + + " config_data AS (SELECT v.subject_code," + + " (SELECT epl.code" + + " FROM epipulse_location_configuration epl" + + " WHERE epl.type = 'Country'" + + " AND epl.country_iso2_code = v.country_locale) as reporting_country," + + " (SELECT epd.datasource" + + " FROM epipulse_datasource_configuration epd" + + " WHERE epd.country_iso2_code = v.country_locale" + + " AND epd.subjectcode = v.subject_code) as datasource" + + " FROM variables v)," + + " filtered_cases AS (SELECT c.id," + + " c.uuid," + + " c.deleted," + + " c.reportdate," + + " c.caseclassification," + + " c.outcome," + + " c.person_id," + + " c.symptoms_id," + + " c.hospitalization_id," + + " c.responsibleregion_id," + + " c.responsibledistrict_id," + + " c.responsiblecommunity_id," + + " c.epidata_id," + + " c.investigateddate," + + " c.clinicalconfirmation" + + " FROM cases c" + + " CROSS JOIN variables v" + + " WHERE c.disease = v.disease" + + " AND c.reportdate >= v.start_date" + + " AND c.reportdate < (v.end_date + interval '1 day'))," + + " case_all_prev_hsp_from_latest AS (SELECT prev_hsp.hospitalization_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(prev_hsp.admittedtohealthfacility, '')," + + " COALESCE(prev_hsp.hospitalizationreason, '')," + + " COALESCE(" + + " TO_CHAR(prev_hsp.admissiondate, 'YYYY-MM-DD')," + + " '')," + + " COALESCE(" + + " TO_CHAR(prev_hsp.dischargedate, 'YYYY-MM-DD')," + + " '')" + + " ), '#'" + + " ORDER BY prev_hsp.admissiondate DESC) as all_prev_hsp_from_latest" + + " FROM previoushospitalization as prev_hsp" + + " WHERE hospitalization_id IN (SELECT hospitalization_id" + + " FROM filtered_cases" + + " WHERE hospitalization_id IS NOT NULL)" + + " GROUP BY prev_hsp.hospitalization_id)," + + " case_all_samples_from_latest AS (SELECT samples.associatedcase_id," + + " ARRAY_AGG(samples.id ORDER BY samples.sampledatetime DESC) as all_sample_ids_from_latest" + + " FROM samples" + + " WHERE samples.associatedcase_id IN (SELECT id FROM filtered_cases)" + + " GROUP BY samples.associatedcase_id)," + + " sample_all_pathogen_tests_from_latest AS (SELECT pathogentest.sample_id," + + " STRING_AGG(CONCAT_WS('|'," + + " pathogentest.testtype," + + " pathogentest.testresult" + + " ), '#'" + + " ORDER BY pathogentest.testdatetime DESC) AS all_pathogen_tests_from_latest" + + " FROM pathogentest" + + " INNER JOIN case_all_samples_from_latest" + + " ON pathogentest.sample_id = ANY" + + " (case_all_samples_from_latest.all_sample_ids_from_latest)" + + " GROUP BY pathogentest.sample_id)," + + "case_all_immunizations AS (SELECT i.person_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(to_char(i.startdate, 'YYYY-MM-DD'), '')," + + " COALESCE(to_char(i.enddate, 'YYYY-MM-DD'), '')," + + " COALESCE(i.meansofimmunization, '')," + + " COALESCE(CAST(i.numberofdoses as text), '')), '#'" + + " ORDER BY i.startdate DESC) as all_immunizations_from_latest" + + " FROM immunization i" + + " CROSS JOIN variables v" + + " where i.person_id IN (SELECT person_id FROM filtered_cases)" + + " and i.disease = v.disease" + + " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + + " GROUP BY i.person_id)," + + "case_all_vaccinations AS (SELECT i.person_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(to_char(v.vaccinationdate, 'YYYY-MM-DD'), '')," + + " COALESCE(v.vaccinedose, '')), '#'" + + " ORDER BY v.vaccinationdate DESC) as all_vaccinations_from_latest" + + " FROM immunization i" + + " INNER JOIN vaccination v ON i.id = v.immunization_id" + + " CROSS JOIN variables" + + " WHERE i.person_id IN (SELECT person_id FROM filtered_cases)" + + " and i.disease = variables.disease" + + " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + + " GROUP BY i.person_id), " + + "sample_data AS (SELECT c.id as case_id," + + " MIN(s.sampledatetime) as first_specimen_date," + + " STRING_AGG(DISTINCT CAST(s2.samplematerial AS text), ',' ORDER BY CAST(s2.samplematerial AS text)) as specimen_types_virus," + + " STRING_AGG(DISTINCT CAST(s3.samplematerial AS text), ',' ORDER BY CAST(s3.samplematerial AS text)) as specimen_types_serology " + + " FROM filtered_cases c " + + " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + + " LEFT JOIN samples s2 ON s2.associatedcase_id = c.id AND s2.deleted = false AND s2.samplematerial IS NOT NULL " + + " LEFT JOIN (SELECT DISTINCT s_sero.id, s_sero.associatedcase_id, s_sero.samplematerial " + + " FROM samples s_sero " + + " JOIN pathogentest pt_sero ON pt_sero.sample_id = s_sero.id " + + " WHERE s_sero.deleted = false " + + " AND pt_sero.testtype IN ('IGG_SERUM_ANTIBODY', 'IGM_SERUM_ANTIBODY', 'SEROLOGY')) s3 " + + " ON s3.associatedcase_id = c.id " + + " GROUP BY c.id), " + + "virus_detection_data AS (SELECT c.id as case_id," + + " MIN(pt.testdatetime) as lab_result_date," + + " (SELECT pt2.testresult " + + " FROM samples s2 " + + " JOIN pathogentest pt2 ON pt2.sample_id = s2.id " + + " WHERE s2.associatedcase_id = c.id " + + " AND s2.deleted = false " + + " AND pt2.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + + " AND pt2.testresultverified = true " + + " ORDER BY pt2.testdatetime ASC " + + " LIMIT 1) as virus_detection_result," + + " (SELECT COALESCE(pt3.typingid, pt3.genotyperesult) " + + " FROM samples s3 " + + " JOIN pathogentest pt3 ON pt3.sample_id = s3.id " + + " WHERE s3.associatedcase_id = c.id " + + " AND s3.deleted = false " + + " AND (pt3.typingid IS NOT NULL OR pt3.genotyperesult IS NOT NULL) " + + " ORDER BY pt3.testdatetime ASC " + + " LIMIT 1) as genotype_raw " + + " FROM filtered_cases c " + + " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + + " LEFT JOIN pathogentest pt ON pt.sample_id = s.id " + + " AND pt.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + + " AND pt.testresultverified = true " + + " GROUP BY c.id), " + + "igg_serology_data AS (SELECT c.id as case_id," + + " (SELECT CASE " + + " WHEN pt_igg.fourfoldincreaseantibodytiter = 'YES' THEN 'POSITIVE' " + + " ELSE pt_igg.testresult " + + " END " + + " FROM samples s_igg " + + " JOIN pathogentest pt_igg ON pt_igg.sample_id = s_igg.id " + + " WHERE s_igg.associatedcase_id = c.id " + + " AND s_igg.deleted = false " + + " AND pt_igg.testtype = 'IGG_SERUM_ANTIBODY' " + + " ORDER BY pt_igg.testdatetime ASC " + + " LIMIT 1) as igg_result " + + " FROM filtered_cases c), " + + "igm_serology_data AS (SELECT c.id as case_id," + + " (SELECT pt_igm.testresult " + + " FROM samples s_igm " + + " JOIN pathogentest pt_igm ON pt_igm.sample_id = s_igm.id " + + " WHERE s_igm.associatedcase_id = c.id " + + " AND s_igm.deleted = false " + + " AND pt_igm.testtype = 'IGM_SERUM_ANTIBODY' " + + " ORDER BY pt_igm.testdatetime ASC " + + " LIMIT 1) as igm_result " + + " FROM filtered_cases c), " + + "epidata_cluster AS (SELECT c.id as case_id," + + " epi.clusterrelated," + + " epi.clustertypetext," + + " epi.clustertype," + + " epi.caseimportedstatus " + + " FROM filtered_cases c " + + " LEFT JOIN epidata epi ON c.epidata_id = epi.id), " + + "exposure_locations AS (SELECT e.epidata_id," + + " STRING_AGG(" + + " CASE " + + " WHEN co.defaultname IS NOT NULL THEN co.defaultname " + + " WHEN l.city IS NOT NULL THEN l.city " + + " ELSE l.details " + + " END, " + + " '; ' " + + " ORDER BY e.startdate DESC" + + " ) as infection_locations " + + " FROM exposures e " + + " JOIN location l ON e.location_id = l.id " + + " LEFT JOIN country co ON l.country_id = co.id " + + " WHERE e.epidata_id IN (SELECT epidata_id FROM filtered_cases WHERE epidata_id IS NOT NULL) " + + " GROUP BY e.epidata_id), " + + "complications_data AS (SELECT c.id as case_id," + + " s.acuteencephalitis," + + " s.diarrhea," + + " s.otitismedia," + + " s.othercomplications " + + " FROM filtered_cases c " + + " LEFT JOIN symptoms s ON c.symptoms_id = s.id) " + + "SELECT cd.reporting_country," + + " c.deleted," + + " cd.subject_code," + + " c.uuid as case_uuid," + + " cd.datasource," + + " cast(c.reportdate as date) as case_reportdate," + + " person.birthdate_yyyy," + + " person.birthdate_mm," + + " person.birthdate_dd," + + " cast(symptom.onsetdate as date) as symptom_onsetdate," + + " person.sex," + + " person_address_community.nutscode as address_community_nutscode," + + " person_address_district.nutscode as address_district_nutscode," + + " person_address_region.nutscode as address_region_nutscode," + + " person_address_country.nutscode as address_country_nutscode," + + " responsible_community.nutscode as responsible_community_nutscode," + + " responsible_district.nutscode as responsible_district_nutscode," + + " responsible_region.nutscode as responsible_region_nutscode," + + " c.caseclassification," + + " hospitalization.admittedtohealthfacility," + + " hospitalization.hospitalizationreason," + + " cast(hospitalization.admissiondate as date) as admissiondate," + + " cast(hospitalization.dischargedate as date) as dischargedate," + + " c.outcome as case_outcome," + + " case_all_prev_hsp_from_latest.all_prev_hsp_from_latest," + + " sample_all_pathogen_tests_from_latest.all_pathogen_tests_from_latest," + + " case_all_immunizations.all_immunizations_from_latest," + + " case_all_vaccinations.all_vaccinations_from_latest," + + " sd.first_specimen_date," + + " vd.lab_result_date," + + " sd.specimen_types_virus," + + " vd.virus_detection_result," + + " vd.genotype_raw," + + " sd.specimen_types_serology," + + " igg.igg_result," + + " igm.igm_result," + + " cast(c.investigateddate as date) as investigated_date," + + " ec.clusterrelated," + + " ec.clustertypetext," + + " ec.clustertype," + + " ec.caseimportedstatus," + + " comp.acuteencephalitis," + + " comp.diarrhea," + + " comp.otitismedia," + + " comp.othercomplications," + + " c.clinicalconfirmation," + + " el.infection_locations," + + " person.causeofdeathdetails " + + "FROM filtered_cases c" + + " CROSS JOIN config_data cd" + + " LEFT JOIN region responsible_region ON c.responsibleregion_id = responsible_region.id" + + " LEFT JOIN district responsible_district ON c.responsibledistrict_id = responsible_district.id" + + " LEFT JOIN community responsible_community ON c.responsiblecommunity_id = responsible_community.id" + + " LEFT JOIN person ON c.person_id = person.id" + + " LEFT JOIN location person_address ON person.address_id = person_address.id" + + " LEFT JOIN country person_address_country ON person_address.country_id = person_address_country.id" + + " LEFT JOIN region person_address_region ON person_address.region_id = person_address_region.id" + + " LEFT JOIN district person_address_district ON person_address.district_id = person_address_district.id" + + " LEFT JOIN community person_address_community ON person_address.community_id = person_address_community.id" + + " LEFT JOIN symptoms symptom ON c.symptoms_id = symptom.id" + + " LEFT JOIN hospitalization ON c.hospitalization_id = hospitalization.id" + + " LEFT JOIN case_all_prev_hsp_from_latest ON (" + + " hospitalization.id = case_all_prev_hsp_from_latest.hospitalization_id" + + " )" + + " LEFT JOIN case_all_samples_from_latest ON (" + + " c.id = case_all_samples_from_latest.associatedcase_id" + + " )" + + " LEFT JOIN sample_all_pathogen_tests_from_latest ON (" + + " sample_all_pathogen_tests_from_latest.sample_id = ANY (case_all_samples_from_latest.all_sample_ids_from_latest)" + + " )" + + " LEFT JOIN case_all_immunizations ON (" + + " case_all_immunizations.person_id = c.person_id" + + " )" + + " LEFT JOIN case_all_vaccinations ON (" + + " case_all_vaccinations.person_id = c.person_id" + + " )" + + " LEFT JOIN sample_data sd ON sd.case_id = c.id" + + " LEFT JOIN virus_detection_data vd ON vd.case_id = c.id" + + " LEFT JOIN igg_serology_data igg ON igg.case_id = c.id" + + " LEFT JOIN igm_serology_data igm ON igm.case_id = c.id" + + " LEFT JOIN epidata_cluster ec ON ec.case_id = c.id" + + " LEFT JOIN exposure_locations el ON el.epidata_id = c.epidata_id" + + " LEFT JOIN complications_data comp ON comp.case_id = c.id " + + "ORDER BY c.reportdate"; + //@formatter:on + Query query = em.createNativeQuery(diseaseExportQuery); + query.setParameter("disease", exportDto.getSubjectCode().getDisease().name()); + query.setParameter("subjectCode", subjectCode); + query.setParameter("countryLocale", serverCountryLocale); + query.setParameter("startDate", DateHelper.convertDateToDbFormat(exportDto.getStartDate())); + query.setParameter("endDate", DateHelper.convertDateToDbFormat(exportDto.getEndDate())); + query.setParameter("meansOfImmVaccination", MeansOfImmunization.VACCINATION.name()); + query.setParameter("meansOfImmVaccinationRecovery", MeansOfImmunization.VACCINATION_RECOVERY.name()); + + @SuppressWarnings("unchecked") + List resultList = query.getResultList(); + + List exportEntryList = new ArrayList<>(); + EpipulseDiseaseExportEntryDto dto = null; + int maxPathogenTests = 0; + int maxImmunizations = 0; + int pathogenTestCount = 0; + int immunizationCount = 0; + + List subjectCodePathogenTestTypes = + EpipulsePathogenTestTypeRef.getPathogenTestTypesByDisease(exportDto.getSubjectCode()); + + int index; + for (Object[] row : resultList) { + index = -1; + + dto = new EpipulseDiseaseExportEntryDto(); + dto.setReportingCountry((String) row[++index]); + dto.setDeleted((Boolean) row[++index]); + + String subjectCodeFromDb = (String) row[++index]; + if (!StringUtils.isBlank(subjectCodeFromDb)) { + dto.setSubjectCode(EpipulseSubjectCode.valueOf(subjectCodeFromDb)); + } + + dto.setNationalRecordId((String) row[++index]); + + dto.setDataSource((String) row[++index]); + dto.setReportDate((Date) row[++index]); + dto.setYearOfBirth((Integer) row[++index]); + dto.setMonthOfBirth((Integer) row[++index]); + dto.setDayOfBirth((Integer) row[++index]); + dto.setSymptomOnsetDate((Date) row[++index]); + + String sex = (String) row[++index]; + if (!StringUtils.isBlank(sex)) { + dto.setSex(Sex.valueOf(sex)); + } + + dto.setAddressCommunityNutsCode((String) row[++index]); + dto.setAddressDistrictNutsCode((String) row[++index]); + dto.setAddressRegionNutsCode((String) row[++index]); + dto.setAddressCountryNutsCode((String) row[++index]); + + dto.setResponsibleCommunityNutsCode((String) row[++index]); + dto.setResponsibleDistrictNutsCode((String) row[++index]); + dto.setResponsibleRegionNutsCode((String) row[++index]); + + dto.setServerCountryNutsCode(serverCountryNutsCode); + + String caseClassification = (String) row[++index]; + if (!StringUtils.isBlank(caseClassification)) { + dto.setCaseClassification(CaseClassification.valueOf(caseClassification)); + } + + String admittedToHealthFacility = (String) row[++index]; + if (!StringUtils.isBlank(admittedToHealthFacility)) { + dto.setAdmittedToHealthFacility(YesNoUnknown.valueOf(admittedToHealthFacility)); + } + + String hospitalizationReason = (String) row[++index]; + if (!StringUtils.isBlank(hospitalizationReason)) { + dto.setHospitalizationReason(HospitalizationReasonType.valueOf(hospitalizationReason)); + } + + dto.setAdmissionDate((Date) row[++index]); + dto.setDischargeDate((Date) row[++index]); + + String caseOutcome = (String) row[++index]; + if (!StringUtils.isBlank(caseOutcome)) { + dto.setCaseOutcome(CaseOutcome.valueOf(caseOutcome)); + } + + dto.setPreviousHospitalizations(dto.parsePreviousHospitalizationChecks((String) row[++index])); + dto.setPathogenTests(dto.parsePathogenTestChecks((String) row[++index], subjectCodePathogenTestTypes)); + dto.setImmunizations(dto.parseImmunizationChecks((String) row[++index])); + dto.setVaccinations(dto.parseVaccinations((String) row[++index])); + + // Phase 2: Laboratory data population for MEAS export + dto.setDateOfSpecimen((Date) row[++index]); + dto.setDateOfLaboratoryResult((Date) row[++index]); + + String specimenTypesVirusRaw = (String) row[++index]; + if (!StringUtils.isBlank(specimenTypesVirusRaw)) { + List specimenTypesVirus = new ArrayList<>(); + for (String specimenType : specimenTypesVirusRaw.split(",")) { + specimenTypesVirus.add( + EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); + } + dto.setTypeOfSpecimenCollected(specimenTypesVirus); + } + + String virusDetectionResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(virusDetectionResultRaw)) { + dto.setResultOfVirusDetection( + EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(virusDetectionResultRaw))); + } + + String genotypeRaw = (String) row[++index]; + if (!StringUtils.isBlank(genotypeRaw)) { + dto.setGenotype(EpipulseLaboratoryMapper.normalizeGenotypeForEpipulse(genotypeRaw)); + } + + String specimenTypesSerologyRaw = (String) row[++index]; + if (!StringUtils.isBlank(specimenTypesSerologyRaw)) { + List specimenTypesSerology = new ArrayList<>(); + for (String specimenType : specimenTypesSerologyRaw.split(",")) { + specimenTypesSerology.add( + EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); + } + dto.setTypeOfSpecimenSerology(specimenTypesSerology); + } + + String iggResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(iggResultRaw)) { + dto.setResultIgG(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(iggResultRaw))); + } + + String igmResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(igmResultRaw)) { + dto.setResultIgM(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(igmResultRaw))); + } + + // Phase 3: Clinical and epidemiology data population for MEAS export + dto.setDateOfInvestigation((Date) row[++index]); + + Boolean clusterRelated = (Boolean) row[++index]; + dto.setClusterRelated(clusterRelated); + + dto.setClusterIdentification((String) row[++index]); + + String clusterTypeRaw = (String) row[++index]; + if (!StringUtils.isBlank(clusterTypeRaw)) { + // ClusterSetting is repeatable, but typically only one value per case + List clusterSettings = new ArrayList<>(); + clusterSettings.add( + EpipulseLaboratoryMapper.mapClusterTypeToEpipulseCode(ClusterType.valueOf(clusterTypeRaw))); + dto.setClusterSetting(clusterSettings); + } + + String caseImportedStatusRaw = (String) row[++index]; + if (!StringUtils.isBlank(caseImportedStatusRaw)) { + dto.setImportedStatus( + EpipulseLaboratoryMapper.mapCaseImportedStatusToEpipulseCode(CaseImportedStatus.valueOf(caseImportedStatusRaw))); + } + + // Complications mapping + String acuteEncephalitisRaw = (String) row[++index]; + String diarrheaRaw = (String) row[++index]; + String otitisMediaRaw = (String) row[++index]; + String otherComplicationsRaw = (String) row[++index]; + + SymptomState acuteEncephalitis = parseSymptomState(acuteEncephalitisRaw); + SymptomState diarrhea = parseSymptomState(diarrheaRaw); + SymptomState otitisMedia = parseSymptomState(otitisMediaRaw); + SymptomState otherComplications = parseSymptomState(otherComplicationsRaw); + + dto.setComplicationDiagnosis( + EpipulseLaboratoryMapper.mapSymptomsToComplicationCodes( + acuteEncephalitis, + diarrhea, + otitisMedia, + otherComplications)); + + // Clinical criteria status + String clinicalConfirmationRaw = (String) row[++index]; + if (!StringUtils.isBlank(clinicalConfirmationRaw)) { + dto.setClinicalCriteriaStatus( + EpipulseLaboratoryMapper.deriveClinicalCriteriaStatus(YesNoUnknown.valueOf(clinicalConfirmationRaw))); + } + + // Place of infection (exposure locations - semicolon-separated from SQL) + String placeOfInfectionRaw = (String) row[++index]; + if (!StringUtils.isBlank(placeOfInfectionRaw)) { + List placesOfInfection = new ArrayList<>(); + for (String place : placeOfInfectionRaw.split(";")) { + if (!place.trim().isEmpty()) { + placesOfInfection.add(place.trim()); + } + } + dto.setPlaceOfInfection(placesOfInfection); + } + + // Cause of death + dto.setCauseOfDeath((String) row[++index]); + + dto.calculateAge(); + + pathogenTestCount = dto.getPathogenTests().size(); + if (pathogenTestCount > maxPathogenTests) { + maxPathogenTests = pathogenTestCount; + } + + immunizationCount = dto.getImmunizations().size(); + if (immunizationCount > maxImmunizations) { + maxImmunizations = immunizationCount; + } + + exportEntryList.add(dto); + } + + // Track max counts for MEAS repeatable fields + int maxComplicationDiagnosis = 0; + int maxClusterSettings = 0; + int maxPlaceOfInfection = 0; + int maxSpecimenVirDetect = 0; + int maxSpecimenSero = 0; + + for (EpipulseDiseaseExportEntryDto entry : exportEntryList) { + if (entry.getComplicationDiagnosis() != null && entry.getComplicationDiagnosis().size() > maxComplicationDiagnosis) { + maxComplicationDiagnosis = entry.getComplicationDiagnosis().size(); + } + if (entry.getClusterSetting() != null && entry.getClusterSetting().size() > maxClusterSettings) { + maxClusterSettings = entry.getClusterSetting().size(); + } + if (entry.getPlaceOfInfection() != null && entry.getPlaceOfInfection().size() > maxPlaceOfInfection) { + maxPlaceOfInfection = entry.getPlaceOfInfection().size(); + } + if (entry.getTypeOfSpecimenCollected() != null && entry.getTypeOfSpecimenCollected().size() > maxSpecimenVirDetect) { + maxSpecimenVirDetect = entry.getTypeOfSpecimenCollected().size(); + } + if (entry.getTypeOfSpecimenSerology() != null && entry.getTypeOfSpecimenSerology().size() > maxSpecimenSero) { + maxSpecimenSero = entry.getTypeOfSpecimenSerology().size(); + } + } + + exportResult.setMaxPathogenTests(maxPathogenTests); + exportResult.setMaxImmunizations(maxImmunizations); + exportResult.setMaxComplicationDiagnosis(maxComplicationDiagnosis); + exportResult.setMaxClusterSettings(maxClusterSettings); + exportResult.setMaxPlaceOfInfection(maxPlaceOfInfection); + exportResult.setMaxSpecimenVirDetect(maxSpecimenVirDetect); + exportResult.setMaxSpecimenSero(maxSpecimenSero); + exportResult.setExportEntryList(exportEntryList); + } catch (Exception e) { + logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage()); + throw e; + } + + return exportResult; + } + @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void updateStatusForBackgroundProcess( String exportUuid, @@ -464,4 +1055,16 @@ public String generateDownloadFileName(EpipulseExportDto exportDto, Long exportI + "_" + StringUtils.replace(DateHelper.convertDateToDbFormat(exportDto.getEndDate()), "-", "") + "_" + exportId + "_" + (System.currentTimeMillis()) + ".csv"; } + + private SymptomState parseSymptomState(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return SymptomState.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid SymptomState value '{}', treating as null", value); + return null; + } + } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseExportTimerEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseExportTimerEjb.java index d5342f16e1f..54cf245b560 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseExportTimerEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseExportTimerEjb.java @@ -67,6 +67,9 @@ public void exportDiseaseTimeout(Timer timer) { case PERT: diseaseExportFacadeEjb.startPertussisExport(uuid); break; + case MEAS: + diseaseExportFacadeEjb.startMeaslesExport(uuid); + break; default: logger.warn("No export for subject code: {}", subjectCodeStr); break; From 16d75d6080d6a20e92e2df3ad661334d2477f68d Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Fri, 9 Jan 2026 13:02:50 +0300 Subject: [PATCH 2/6] #13771 - Add Epipulse export functionality for Measles disease --- .../epipulse/EpipulseCommonDtoMapper.java | 190 ++++ .../EpipulseConfigurationLookupService.java | 160 +++ .../EpipulseCsvExportOrchestrator.java | 197 ++++ .../EpipulseDiseaseExportFacadeEjb.java | 448 +-------- .../EpipulseDiseaseExportService.java | 945 +----------------- .../epipulse/EpipulseSqlCteBuilder.java | 306 ++++++ ...AbstractEpipulseDiseaseExportStrategy.java | 220 ++++ .../epipulse/strategy/CsvExportStrategy.java | 46 + .../strategy/MeaslesCsvExportStrategy.java | 212 ++++ .../strategy/MeaslesExportStrategy.java | 394 ++++++++ .../strategy/PertussisCsvExportStrategy.java | 114 +++ .../strategy/PertussisExportStrategy.java | 65 ++ .../util/EpipulseConfigurationContext.java | 60 ++ 13 files changed, 1983 insertions(+), 1374 deletions(-) create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseSqlCteBuilder.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/CsvExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesCsvExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisCsvExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisExportStrategy.java create mode 100644 sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/util/EpipulseConfigurationContext.java diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java new file mode 100644 index 00000000000..c8c78ffd190 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java @@ -0,0 +1,190 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse; + +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.symeda.sormas.api.caze.CaseClassification; +import de.symeda.sormas.api.caze.CaseOutcome; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; +import de.symeda.sormas.api.epipulse.EpipulseSubjectCode; +import de.symeda.sormas.api.hospitalization.HospitalizationReasonType; +import de.symeda.sormas.api.person.Sex; +import de.symeda.sormas.api.sample.PathogenTestType; +import de.symeda.sormas.api.utils.YesNoUnknown; + +/** + * Static utility class for mapping common DTO fields that are identical across all disease exports. + * This mapper handles the first fields (indices 0-27) that are shared between Pertussis, Measles, + * and other disease exports. + */ +public class EpipulseCommonDtoMapper { + + private static final Logger logger = LoggerFactory.getLogger(EpipulseCommonDtoMapper.class); + + private EpipulseCommonDtoMapper() { + // Utility class - prevent instantiation + } + + /** + * Maps common fields that are identical for all disease exports. + * This method handles the standard case data, demographics, location, hospitalization, + * and outcome information that is common to all diseases. + * + * @param dto + * the DTO to populate + * @param row + * the database result row + * @param serverCountryNutsCode + * the NUTS code for the server country + * @param subjectCodePathogenTestTypes + * the pathogen test types for the disease + * @return the next index position for disease-specific fields + */ + public static int mapCommonFields( + EpipulseDiseaseExportEntryDto dto, + Object[] row, + String serverCountryNutsCode, + List subjectCodePathogenTestTypes) { + + int index = -1; + + // Index 0: Reporting Country + dto.setReportingCountry((String) row[++index]); + + // Index 1: Deleted flag + dto.setDeleted((Boolean) row[++index]); + + // Index 2: Subject Code + String subjectCodeFromDb = (String) row[++index]; + if (!StringUtils.isBlank(subjectCodeFromDb)) { + dto.setSubjectCode(EpipulseSubjectCode.valueOf(subjectCodeFromDb)); + } + + // Index 3: National Record ID (case UUID) + dto.setNationalRecordId((String) row[++index]); + + // Index 4: Data Source + dto.setDataSource((String) row[++index]); + + // Index 5: Report Date + dto.setReportDate((Date) row[++index]); + + // Index 6-8: Birth Date Components + dto.setYearOfBirth((Integer) row[++index]); + dto.setMonthOfBirth((Integer) row[++index]); + dto.setDayOfBirth((Integer) row[++index]); + + // Index 9: Symptom Onset Date + dto.setSymptomOnsetDate((Date) row[++index]); + + // Index 10: Sex + String sex = (String) row[++index]; + if (!StringUtils.isBlank(sex)) { + dto.setSex(Sex.valueOf(sex)); + } + + // Index 11-14: Address NUTS Codes + dto.setAddressCommunityNutsCode((String) row[++index]); + dto.setAddressDistrictNutsCode((String) row[++index]); + dto.setAddressRegionNutsCode((String) row[++index]); + dto.setAddressCountryNutsCode((String) row[++index]); + + // Index 15-17: Responsible Area NUTS Codes + dto.setResponsibleCommunityNutsCode((String) row[++index]); + dto.setResponsibleDistrictNutsCode((String) row[++index]); + dto.setResponsibleRegionNutsCode((String) row[++index]); + + // Server Country NUTS Code (not from row, passed as parameter) + dto.setServerCountryNutsCode(serverCountryNutsCode); + + // Index 18: Case Classification + String caseClassification = (String) row[++index]; + if (!StringUtils.isBlank(caseClassification)) { + dto.setCaseClassification(CaseClassification.valueOf(caseClassification)); + } + + // Index 19: Admitted to Health Facility + String admittedToHealthFacility = (String) row[++index]; + if (!StringUtils.isBlank(admittedToHealthFacility)) { + dto.setAdmittedToHealthFacility(YesNoUnknown.valueOf(admittedToHealthFacility)); + } + + // Index 20: Hospitalization Reason + String hospitalizationReason = (String) row[++index]; + if (!StringUtils.isBlank(hospitalizationReason)) { + dto.setHospitalizationReason(HospitalizationReasonType.valueOf(hospitalizationReason)); + } + + // Index 21-22: Admission and Discharge Dates + dto.setAdmissionDate((Date) row[++index]); + dto.setDischargeDate((Date) row[++index]); + + // Index 23: Case Outcome + String caseOutcome = (String) row[++index]; + if (!StringUtils.isBlank(caseOutcome)) { + dto.setCaseOutcome(CaseOutcome.valueOf(caseOutcome)); + } + + // Index 24-27: Aggregated Collections (previous hospitalizations, pathogen tests, immunizations, vaccinations) + dto.setPreviousHospitalizations(dto.parsePreviousHospitalizationChecks((String) row[++index])); + dto.setPathogenTests(dto.parsePathogenTestChecks((String) row[++index], subjectCodePathogenTestTypes)); + dto.setImmunizations(dto.parseImmunizationChecks((String) row[++index])); + dto.setVaccinations(dto.parseVaccinations((String) row[++index])); + + // Return the last index used (27 for the vaccinations field) + // The aggregated collections (previous hospitalizations, pathogen tests, immunizations, vaccinations) + // are also common fields + return index; + } + + /** + * Calculates common max counts (pathogenTests and immunizations) that are tracked + * across all disease exports. Disease-specific max counts are handled by the + * individual strategy implementations. + * + * @param entries + * the list of export entries + * @param result + * the result object to populate with max counts + */ + public static void calculateCommonMaxCounts(List entries, EpipulseDiseaseExportResult result) { + + int maxPathogenTests = 0; + int maxImmunizations = 0; + + for (EpipulseDiseaseExportEntryDto entry : entries) { + int pathogenTestCount = entry.getPathogenTests().size(); + if (pathogenTestCount > maxPathogenTests) { + maxPathogenTests = pathogenTestCount; + } + + int immunizationCount = entry.getImmunizations().size(); + if (immunizationCount > maxImmunizations) { + maxImmunizations = immunizationCount; + } + } + + result.setMaxPathogenTests(maxPathogenTests); + result.setMaxImmunizations(maxImmunizations); + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java new file mode 100644 index 00000000000..df0df8936cd --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java @@ -0,0 +1,160 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.symeda.sormas.api.epipulse.EpipulseExportDto; +import de.symeda.sormas.api.epipulse.EpipulseSubjectCode; +import de.symeda.sormas.backend.epipulse.util.EpipulseConfigurationContext; +import de.symeda.sormas.backend.util.ModelConstants; + +/** + * Service for looking up Epipulse configuration values from the database. + * This service extracts the common configuration lookup logic that was duplicated + * in both exportPertussisCaseBased and exportMeaslesCaseBased methods. + */ +@Stateless +@LocalBean +public class EpipulseConfigurationLookupService { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @PersistenceContext(unitName = ModelConstants.PERSISTENCE_UNIT_NAME) + private EntityManager em; + + /** + * Looks up all required configuration values for an Epipulse export. + * + * @param exportDto + * the export request DTO containing the subject code + * @param serverCountryLocale + * the server country ISO2 code (e.g., "LU") + * @param serverCountryName + * the server country name (e.g., "Luxembourg") + * @return a context object containing all looked-up configuration values + * @throws IllegalArgumentException + * if the server country code is invalid + * @throws IllegalStateException + * if the subject code lookup fails + */ + public EpipulseConfigurationContext lookupConfiguration(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) + throws IllegalArgumentException, IllegalStateException { + + String reportingCountry = lookupReportingCountry(serverCountryLocale); + String serverCountryNutsCode = lookupServerCountryNutsCode(serverCountryName); + String subjectCode = lookupSubjectCode(exportDto.getSubjectCode()); + + return new EpipulseConfigurationContext(reportingCountry, serverCountryNutsCode, subjectCode); + } + + /** + * Looks up the Epipulse reporting country code for the given ISO2 country code. + * + * @param countryIso2Code + * the ISO2 country code (e.g., "LU") + * @return the Epipulse reporting country code + * @throws IllegalArgumentException + * if the country code is invalid + */ + private String lookupReportingCountry(String countryIso2Code) throws IllegalArgumentException { + //@formatter:off + String reportingCountryQuery = + "select code as reporting_country " + + "from epipulse_location_configuration " + + "where type='Country' and country_iso2_code = :countryIso2Code"; + //@formatter:on + + @SuppressWarnings("unchecked") + String reportingCountry = (String) em.createNativeQuery(reportingCountryQuery) + .setParameter("countryIso2Code", countryIso2Code) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + if (StringUtils.isBlank(reportingCountry)) { + throw new IllegalArgumentException("Invalid server country code: " + countryIso2Code); + } + + return reportingCountry; + } + + /** + * Looks up the NUTS code for the given country name. + * + * @param countryName + * the country name (e.g., "Luxembourg") + * @return the NUTS code for the country, or null if not found + */ + private String lookupServerCountryNutsCode(String countryName) { + //@formatter:off + String serverCountryQuery = + "select nutscode " + + "from country " + + "where lower(defaultname) = :countryName"; + //@formatter:on + + @SuppressWarnings("unchecked") + String serverCountryNutsCode = (String) em.createNativeQuery(serverCountryQuery) + .setParameter("countryName", countryName.toLowerCase()) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + return serverCountryNutsCode; + } + + /** + * Looks up the Epipulse subject code for the given disease. + * + * @param subjectCodeEnum + * the subject code enum value (e.g., PERT, MEAS) + * @return the Epipulse subject code string + * @throws IllegalStateException + * if the subject code lookup fails + */ + private String lookupSubjectCode(EpipulseSubjectCode subjectCodeEnum) throws IllegalStateException { + //@formatter:off + String subjectCodeQuery = + "select subjectcode " + + "from epipulse_subjectcode_configuration " + + "where disease=:disease and aggregatedreporting='No'"; + //@formatter:on + + @SuppressWarnings("unchecked") + String subjectCode = (String) em.createNativeQuery(subjectCodeQuery) + .setParameter("disease", subjectCodeEnum.name()) + .getResultStream() + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + + if (StringUtils.isBlank(subjectCode)) { + throw new IllegalStateException("Subject code is empty"); + } + + return subjectCode; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java new file mode 100644 index 00000000000..42b03b96687 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java @@ -0,0 +1,197 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse; + +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.List; + +import javax.ejb.EJB; +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.opencsv.CSVWriter; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; +import de.symeda.sormas.api.epipulse.EpipulseExportDto; +import de.symeda.sormas.api.epipulse.EpipulseExportStatus; +import de.symeda.sormas.api.utils.CSVUtils; +import de.symeda.sormas.backend.common.ConfigFacadeEjb; +import de.symeda.sormas.backend.epipulse.strategy.CsvExportStrategy; + +/** + * Orchestrator service that handles the common export flow for all disease-specific CSV exports. + * It is responsible for extracting setup, validation, error handling, and finalization logic. + */ +@Stateless +@LocalBean +public class EpipulseCsvExportOrchestrator { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @EJB + private EpipulseExportFacadeEjb.EpipulseExportFacadeEjbLocal epipulseExportEjb; + + @EJB + private EpipulseExportService epipulseExportService; + + @EJB + private EpipulseDiseaseExportService diseaseExportService; + + @EJB + private ConfigFacadeEjb.ConfigFacadeEjbLocal configFacadeEjb; + + /** + * Orchestrates the complete export flow for a disease-specific CSV export. + * + * @param uuid + * the UUID of the export + * @param exportFunction + * the function that performs the disease-specific data export + * @param csvStrategy + * the strategy that defines CSV columns and row writing + */ + public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExportStrategy csvStrategy) { + + CSVWriter writer = null; + EpipulseExport epipulseExport = null; + EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; + boolean shouldUpdateStatus = false; + + Integer totalRecords = null; + BigDecimal exportFileSizeBytes = null; + String exportFileName = null; + String exportFilePath = null; + + try { + // Validation + epipulseExport = epipulseExportService.getByUuid(uuid); + + if (epipulseExport == null) { + logger.error("EpipulseExport with uuid " + uuid + " not found"); + return; + } + + if (epipulseExport.getStatus() != EpipulseExportStatus.PENDING) { + logger.error("EpipulseExport with uuid " + uuid + " is not in status PENDING"); + return; + } + + shouldUpdateStatus = true; + + // Update status to IN_PROGRESS + diseaseExportService.updateStatusForBackgroundProcess(uuid, EpipulseExportStatus.IN_PROGRESS, null, null, null); + + // Load configuration + EpipulseExportDto exportDto = epipulseExportEjb.toEpipulseExportDto(epipulseExport); + String serverCountryCode = configFacadeEjb.getCountryCode(); + String serverCountryName = configFacadeEjb.getCountryName(); + + // Setup file path + String generatedFilesPath = configFacadeEjb.getGeneratedFilesPath(); + exportFileName = diseaseExportService.generateDownloadFileName(exportDto, epipulseExport.getId()); + exportFilePath = generatedFilesPath + "/" + exportFileName; + + // Execute disease-specific export + EpipulseDiseaseExportResult exportResult = exportFunction.execute(exportDto, serverCountryCode, serverCountryName); + totalRecords = exportResult.getExportEntryList().size(); + + // Setup CSV writer + writer = CSVUtils.createCSVWriter( + new OutputStreamWriter(new FileOutputStream(exportFilePath), StandardCharsets.UTF_8), + configFacadeEjb.getCsvSeparator()); + + // Build column names using strategy + List columnNames = csvStrategy.buildColumnNames(exportResult); + + // Write headers + writer.writeNext(columnNames.toArray(new String[columnNames.size()])); + + // Write entries using strategy + String[] exportLine = new String[columnNames.size()]; + for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { + csvStrategy.writeEntryRow(dto, exportLine, exportResult); + writer.writeNext(exportLine); + } + + exportStatus = EpipulseExportStatus.COMPLETED; + } catch (Exception e) { + exportStatus = EpipulseExportStatus.FAILED; + logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); + } finally { + // Close writer + if (writer != null) { + try { + writer.close(); + } catch (Exception e) { + logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); + } + } + + // Calculate file size after writer is closed + if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { + try { + long fileSizeInBytes = Files.size(Paths.get(exportFilePath)); + exportFileSizeBytes = new BigDecimal(fileSizeInBytes); + logger.info("Export file size for uuid {}: {} bytes", uuid, fileSizeInBytes); + } catch (Exception e) { + logger.error("CRITICAL: Failed to calculate file size for uuid {}: {}", uuid, e.getMessage(), e); + } + } + + // Update final status + if (shouldUpdateStatus && epipulseExport != null) { + try { + diseaseExportService + .updateStatusForBackgroundProcess(epipulseExport.getUuid(), exportStatus, totalRecords, exportFileName, exportFileSizeBytes); + } catch (Exception e) { + logger.error("CRITICAL: Failed to update export status for uuid " + uuid + ": " + e.getMessage(), e); + } + } + } + } + + /** + * Functional interface for disease-specific export operations. + */ + @FunctionalInterface + public interface ExportFunction { + + /** + * Executes the disease-specific data export. + * + * @param dto + * the export DTO + * @param countryCode + * the country code + * @param countryName + * the country name + * @return the export result containing entries and max counts + * @throws SQLException + * if database error occurs + */ + EpipulseDiseaseExportResult execute(EpipulseExportDto dto, String countryCode, String countryName) throws SQLException; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java index c700548dabd..1ef257bae55 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportFacadeEjb.java @@ -15,467 +15,35 @@ package de.symeda.sormas.backend.epipulse; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - import javax.ejb.EJB; import javax.ejb.LocalBean; import javax.ejb.Stateless; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.opencsv.CSVWriter; - -import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportFacade; -import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; -import de.symeda.sormas.api.epipulse.EpipulseExportDto; -import de.symeda.sormas.api.epipulse.EpipulseExportStatus; -import de.symeda.sormas.api.utils.CSVUtils; -import de.symeda.sormas.backend.common.ConfigFacadeEjb; +import de.symeda.sormas.backend.epipulse.strategy.MeaslesCsvExportStrategy; +import de.symeda.sormas.backend.epipulse.strategy.PertussisCsvExportStrategy; @Stateless(name = "EpipulseDiseaseExportFacade") public class EpipulseDiseaseExportFacadeEjb implements EpipulseDiseaseExportFacade { - private final Logger logger = LoggerFactory.getLogger(getClass()); - @EJB - private EpipulseDiseaseExportService diseaseExportService; + private EpipulseCsvExportOrchestrator orchestrator; @EJB - private EpipulseExportFacadeEjb.EpipulseExportFacadeEjbLocal epipulseExportEjb; + private PertussisCsvExportStrategy pertussisStrategy; @EJB - private EpipulseExportService epipulseExportService; + private MeaslesCsvExportStrategy measlesStrategy; @EJB - private ConfigFacadeEjb.ConfigFacadeEjbLocal configFacadeEjb; + private EpipulseDiseaseExportService diseaseExportService; public void startPertussisExport(String uuid) { - - CSVWriter writer = null; - EpipulseExport epipulseExport = null; - EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; - boolean shouldUpdateStatus = false; - - Integer totalRecords = null; - BigDecimal exportFileSizeBytes = null; - String exportFileName = null; - String exportFilePath = null; - - try { - epipulseExport = epipulseExportService.getByUuid(uuid); - - if (epipulseExport == null) { - logger.error("EpipulseExport with uuid " + uuid + " not found"); - return; - } - - if (epipulseExport.getStatus() != EpipulseExportStatus.PENDING) { - logger.error("EpipulseExport with uuid " + uuid + " is not in status PENDING"); - return; - } - - shouldUpdateStatus = true; - - diseaseExportService.updateStatusForBackgroundProcess(uuid, EpipulseExportStatus.IN_PROGRESS, null, null, null); - - EpipulseExportDto exportDto = epipulseExportEjb.toEpipulseExportDto(epipulseExport); - - String serverCountryLocale = configFacadeEjb.getCountryLocale(); - String serverCountryCode = configFacadeEjb.getCountryCode(); - String serverCountryName = configFacadeEjb.getCountryName(); - - String generatedFilesPath = configFacadeEjb.getGeneratedFilesPath(); - exportFileName = diseaseExportService.generateDownloadFileName(exportDto, epipulseExport.getId()); - exportFilePath = generatedFilesPath + "/" + exportFileName; - - EpipulseDiseaseExportResult exportResult = diseaseExportService.exportPertussisCaseBased(exportDto, serverCountryCode, serverCountryName); - totalRecords = exportResult.getExportEntryList().size(); - - //logger.info("Total records found for export: " + exportResult.getExportEntryList().size() + ""); - - writer = CSVUtils.createCSVWriter( - new OutputStreamWriter(new FileOutputStream(exportFilePath), StandardCharsets.UTF_8), - configFacadeEjb.getCsvSeparator()); - - List columnNames = new ArrayList<>( - List.of( - "Disease", - "ReportingCountry", - "Status", - "SubjectCode", - "NationalRecordId", - "DataSource", - "DateUsedForStatistics", - "Age", - "AgeMonth", - "Gender", - "PlaceOfResidence", - "PlaceOfNotification", - "CaseClassification", - "DateOfOnset", - "DateOfNotification", - "Hospitalisation", - "Outcome")); - - if (exportResult.getMaxPathogenTests() > 0) { - for (int i = 0; i < exportResult.getMaxPathogenTests(); i++) { - columnNames.add("PathogenDetectionMethod"); - } - } - - if (exportResult.getMaxImmunizations() > 0) { - columnNames.add("DateOfLastVaccination"); - } - - columnNames.add("VaccinationStatus"); - - //columnNames.add("VaccinationStatusMaternal"); - //columnNames.add("GestationalAgeAtVaccination"); - - //write the headers - writer.writeNext(columnNames.toArray(new String[columnNames.size()])); - - //write entries - String[] exportLine = new String[columnNames.size()]; - List pathogenDetectionMethods = new ArrayList<>(); - int index; - for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { - index = -1; - - exportLine[++index] = dto.getDiseaseForCsv(); - exportLine[++index] = dto.getReportingCountryForCsv(); - - exportLine[++index] = dto.getStatusForCsv(); - exportLine[++index] = dto.getSubjectCodeForCsv(); - exportLine[++index] = dto.getNationalRecordIdForCsv(); - exportLine[++index] = dto.getDataSourceForCsv(); - exportLine[++index] = dto.getDateUsedForStatisticsCsv(); - exportLine[++index] = dto.getAgeForCsv(); - exportLine[++index] = dto.getAgeMonthForCsv(); - exportLine[++index] = dto.getGenderForCsv(); - exportLine[++index] = dto.getPlaceOfResidenceForCsv(); - exportLine[++index] = dto.getPlaceOfNotificationForCsv(); - exportLine[++index] = dto.getCaseClassificationForCsv(); - exportLine[++index] = dto.getDateOfOnsetForCsv(); - exportLine[++index] = dto.getDateOfNotificationForCsv(); - exportLine[++index] = dto.getHospitalizationForCsv(); - exportLine[++index] = dto.getOutcomeForCsv(); - - if (exportResult.getMaxPathogenTests() > 0) { - pathogenDetectionMethods = dto.getPathogenDetectionMethodsForCsv(exportResult.getMaxPathogenTests()); - for (String pathogenDetectionMethod : pathogenDetectionMethods) { - exportLine[++index] = pathogenDetectionMethod; - } - } - - if (exportResult.getMaxImmunizations() > 0) { - exportLine[++index] = dto.getDateOfLastVaccinationForCsv(); - } - - exportLine[++index] = dto.getVaccinationStatusForCsv(); - - //exportLine[++index] = dto.getVaccinationStatusMaternalForCsv(); - //exportLine[++index] = dto.getGestationalAgeAtVaccinationForCsv(); - - writer.writeNext(exportLine); - } - - exportStatus = EpipulseExportStatus.COMPLETED; - } catch (Exception e) { - exportStatus = EpipulseExportStatus.FAILED; - - logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (Exception e) { - logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); - } - } - - // Calculate file size after writer is closed - if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { - try { - long fileSizeInBytes = Files.size(Paths.get(exportFilePath)); - exportFileSizeBytes = new BigDecimal(fileSizeInBytes); - logger.info("Export file size for uuid {}: {} bytes", uuid, fileSizeInBytes); - } catch (Exception e) { - logger.error("CRITICAL: Failed to calculate file size for uuid {}: {}", uuid, e.getMessage(), e); - } - } - - if (shouldUpdateStatus && epipulseExport != null) { - try { - diseaseExportService - .updateStatusForBackgroundProcess(epipulseExport.getUuid(), exportStatus, totalRecords, exportFileName, exportFileSizeBytes); - } catch (Exception e) { - logger.error("CRITICAL: Failed to update export status for uuid " + uuid + ": " + e.getMessage(), e); - } - } - } + orchestrator.orchestrateExport(uuid, diseaseExportService::exportPertussisCaseBased, pertussisStrategy); } public void startMeaslesExport(String uuid) { - - CSVWriter writer = null; - EpipulseExport epipulseExport = null; - EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; - boolean shouldUpdateStatus = false; - - Integer totalRecords = null; - BigDecimal exportFileSizeBytes = null; - String exportFileName = null; - String exportFilePath = null; - - try { - epipulseExport = epipulseExportService.getByUuid(uuid); - - if (epipulseExport == null) { - logger.error("EpipulseExport with uuid " + uuid + " not found"); - return; - } - - if (epipulseExport.getStatus() != EpipulseExportStatus.PENDING) { - logger.error("EpipulseExport with uuid " + uuid + " is not in status PENDING"); - return; - } - - shouldUpdateStatus = true; - - diseaseExportService.updateStatusForBackgroundProcess(uuid, EpipulseExportStatus.IN_PROGRESS, null, null, null); - - EpipulseExportDto exportDto = epipulseExportEjb.toEpipulseExportDto(epipulseExport); - - String serverCountryLocale = configFacadeEjb.getCountryLocale(); - String serverCountryCode = configFacadeEjb.getCountryCode(); - String serverCountryName = configFacadeEjb.getCountryName(); - - String generatedFilesPath = configFacadeEjb.getGeneratedFilesPath(); - exportFileName = diseaseExportService.generateDownloadFileName(exportDto, epipulseExport.getId()); - exportFilePath = generatedFilesPath + "/" + exportFileName; - - EpipulseDiseaseExportResult exportResult = diseaseExportService.exportMeaslesCaseBased(exportDto, serverCountryCode, serverCountryName); - totalRecords = exportResult.getExportEntryList().size(); - - writer = CSVUtils.createCSVWriter( - new OutputStreamWriter(new FileOutputStream(exportFilePath), StandardCharsets.UTF_8), - configFacadeEjb.getCsvSeparator()); - - // MEAS CSV columns - including Phase 2 laboratory fields and Phase 3 clinical/epidemiology fields - List columnNames = new ArrayList<>( - List.of( - "Disease", - "ReportingCountry", - "Status", - "SubjectCode", - "NationalRecordId", - "DataSource", - "DateUsedForStatistics", - "Age", - "AgeMonth", - "Gender", - "CaseClassification", - "DateOfOnset", - "DateOfNotification", - "Hospitalisation", - "Outcome", - "PlaceOfNotification", - "PlaceOfResidence", - "DateOfSpecimen", - "DateOfLaboratoryResult")); - - // Repeatable field: TypeOfSpecimenCollected - if (exportResult.getMaxSpecimenVirDetect() > 0) { - for (int i = 1; i <= exportResult.getMaxSpecimenVirDetect(); i++) { - columnNames.add("TypeOfSpecimenCollected"); - } - } - - columnNames.addAll(List.of("ResultOfVirusDetection", "Genotype")); - - // Repeatable field: TypeOfSpecimenForSerologicalAnalysis - if (exportResult.getMaxSpecimenSero() > 0) { - for (int i = 1; i <= exportResult.getMaxSpecimenSero(); i++) { - columnNames.add("TypeOfSpecimenForSerologicalAnalysis"); - } - } - - columnNames.addAll(List.of("ResultIgG", "ResultIgM", "DateOfInvestigation", "ClusterRelated", "ClusterIdentification")); - - // Repeatable field: ClusterSetting - if (exportResult.getMaxClusterSettings() > 0) { - for (int i = 1; i <= exportResult.getMaxClusterSettings(); i++) { - columnNames.add("ClusterSetting"); - } - } - - columnNames.add("ImportedStatus"); - - // Repeatable field: ComplicationDiagnosis - if (exportResult.getMaxComplicationDiagnosis() > 0) { - for (int i = 1; i <= exportResult.getMaxComplicationDiagnosis(); i++) { - columnNames.add("ComplicationDiagnosis"); - } - } - - columnNames.add("ClinicalCriteriaStatus"); - - // Repeatable field: PlaceOfInfection - if (exportResult.getMaxPlaceOfInfection() > 0) { - for (int i = 1; i <= exportResult.getMaxPlaceOfInfection(); i++) { - columnNames.add("PlaceOfInfection"); - } - } - - columnNames.add("CauseOfDeath"); - - if (exportResult.getMaxImmunizations() > 0) { - columnNames.add("DateOfLastVaccination"); - } - - columnNames.add("VaccinationStatus"); - - //write the headers - writer.writeNext(columnNames.toArray(new String[columnNames.size()])); - - //write entries - String[] exportLine = new String[columnNames.size()]; - int index; - for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { - index = -1; - - exportLine[++index] = dto.getDiseaseForCsv(); - exportLine[++index] = dto.getReportingCountryForCsv(); - exportLine[++index] = dto.getStatusForCsv(); - exportLine[++index] = dto.getSubjectCodeForCsv(); - exportLine[++index] = dto.getNationalRecordIdForCsv(); - exportLine[++index] = dto.getDataSourceForCsv(); - exportLine[++index] = dto.getDateUsedForStatisticsCsv(); - exportLine[++index] = dto.getAgeForCsv(); - exportLine[++index] = dto.getAgeMonthForCsv(); - exportLine[++index] = dto.getGenderForCsv(); - exportLine[++index] = dto.getCaseClassificationForCsv(); - exportLine[++index] = dto.getDateOfOnsetForCsv(); - exportLine[++index] = dto.getDateOfNotificationForCsv(); - exportLine[++index] = dto.getHospitalizationForCsv(); - exportLine[++index] = dto.getOutcomeForCsv(); - exportLine[++index] = dto.getPlaceOfNotificationForCsv(); - exportLine[++index] = dto.getPlaceOfResidenceForCsv(); - - // Phase 2: Laboratory fields - exportLine[++index] = dto.getDateOfSpecimenForCsv(); - exportLine[++index] = dto.getDateOfLaboratoryResultForCsv(); - - // Repeatable: TypeOfSpecimenCollected - if (exportResult.getMaxSpecimenVirDetect() > 0) { - List specimenCollected = dto.getTypeOfSpecimenCollectedForCsv(exportResult.getMaxSpecimenVirDetect()); - for (String specimen : specimenCollected) { - exportLine[++index] = specimen; - } - } - - exportLine[++index] = dto.getResultOfVirusDetectionForCsv(); - exportLine[++index] = dto.getGenotypeForCsv(); - - // Repeatable: TypeOfSpecimenForSerologicalAnalysis - if (exportResult.getMaxSpecimenSero() > 0) { - List specimenSerology = dto.getTypeOfSpecimenSerologyForCsv(exportResult.getMaxSpecimenSero()); - for (String specimen : specimenSerology) { - exportLine[++index] = specimen; - } - } - - exportLine[++index] = dto.getResultIgGForCsv(); - exportLine[++index] = dto.getResultIgMForCsv(); - - // Phase 3: Clinical and epidemiology fields - exportLine[++index] = dto.getDateOfInvestigationForCsv(); - exportLine[++index] = dto.getClusterRelatedForCsv(); - exportLine[++index] = dto.getClusterIdentificationForCsv(); - - // Repeatable: ClusterSetting - if (exportResult.getMaxClusterSettings() > 0) { - List clusterSettings = dto.getClusterSettingForCsv(exportResult.getMaxClusterSettings()); - for (String setting : clusterSettings) { - exportLine[++index] = setting; - } - } - - exportLine[++index] = dto.getImportedStatusForCsv(); - - // Repeatable: ComplicationDiagnosis - if (exportResult.getMaxComplicationDiagnosis() > 0) { - List complications = dto.getComplicationDiagnosisForCsv(exportResult.getMaxComplicationDiagnosis()); - for (String complication : complications) { - exportLine[++index] = complication; - } - } - - exportLine[++index] = dto.getClinicalCriteriaStatusForCsv(); - - // Repeatable: PlaceOfInfection - if (exportResult.getMaxPlaceOfInfection() > 0) { - List placesOfInfection = dto.getPlaceOfInfectionForCsv(exportResult.getMaxPlaceOfInfection()); - for (String place : placesOfInfection) { - exportLine[++index] = place; - } - } - - exportLine[++index] = dto.getCauseOfDeathForCsv(); - - if (exportResult.getMaxImmunizations() > 0) { - exportLine[++index] = dto.getDateOfLastVaccinationForCsv(); - } - - exportLine[++index] = dto.getVaccinationStatusForCsv(); - - writer.writeNext(exportLine); - } - - exportStatus = EpipulseExportStatus.COMPLETED; - } catch (Exception e) { - exportStatus = EpipulseExportStatus.FAILED; - - logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (Exception e) { - logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); - } - } - - // Calculate file size after writer is closed - if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { - try { - long fileSizeInBytes = Files.size(Paths.get(exportFilePath)); - exportFileSizeBytes = new BigDecimal(fileSizeInBytes); - logger.info("Export file size for uuid {}: {} bytes", uuid, fileSizeInBytes); - } catch (Exception e) { - logger.error("CRITICAL: Failed to calculate file size for uuid {}: {}", uuid, e.getMessage(), e); - } - } - - if (shouldUpdateStatus && epipulseExport != null) { - try { - diseaseExportService - .updateStatusForBackgroundProcess(epipulseExport.getUuid(), exportStatus, totalRecords, exportFileName, exportFileSizeBytes); - } catch (Exception e) { - logger.error("CRITICAL: Failed to update export status for uuid " + uuid + ": " + e.getMessage(), e); - } - } - } + orchestrator.orchestrateExport(uuid, diseaseExportService::exportMeaslesCaseBased, measlesStrategy); } @LocalBean diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java index c8c9b71c2ab..26fa424f9dc 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java @@ -18,10 +18,8 @@ import java.math.BigDecimal; import java.sql.SQLException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import javax.ejb.EJB; import javax.ejb.LocalBean; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; @@ -34,26 +32,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.symeda.sormas.api.caze.CaseClassification; -import de.symeda.sormas.api.caze.CaseOutcome; -import de.symeda.sormas.api.epidata.CaseImportedStatus; -import de.symeda.sormas.api.epidata.ClusterType; -import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; import de.symeda.sormas.api.epipulse.EpipulseExportDto; import de.symeda.sormas.api.epipulse.EpipulseExportStatus; -import de.symeda.sormas.api.epipulse.EpipulseLaboratoryMapper; -import de.symeda.sormas.api.epipulse.EpipulseSubjectCode; -import de.symeda.sormas.api.epipulse.referencevalue.EpipulsePathogenTestTypeRef; -import de.symeda.sormas.api.hospitalization.HospitalizationReasonType; -import de.symeda.sormas.api.immunization.MeansOfImmunization; -import de.symeda.sormas.api.person.Sex; -import de.symeda.sormas.api.sample.PathogenTestResultType; -import de.symeda.sormas.api.sample.PathogenTestType; -import de.symeda.sormas.api.symptoms.SymptomState; -import de.symeda.sormas.api.sample.SampleMaterial; import de.symeda.sormas.api.utils.DateHelper; -import de.symeda.sormas.api.utils.YesNoUnknown; +import de.symeda.sormas.backend.epipulse.strategy.MeaslesExportStrategy; +import de.symeda.sormas.backend.epipulse.strategy.PertussisExportStrategy; import de.symeda.sormas.backend.util.ModelConstants; @Stateless @@ -67,915 +51,20 @@ public class EpipulseDiseaseExportService { @PersistenceContext(unitName = ModelConstants.PERSISTENCE_UNIT_NAME) private EntityManager em; - public EpipulseDiseaseExportResult exportPertussisCaseBased(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) - throws SQLException, IllegalStateException, IllegalArgumentException { - - EpipulseDiseaseExportResult exportResult = new EpipulseDiseaseExportResult(); - - try { - //lookup reporting country - //@formatter:off - String reportingCountryQuery = - "select code as reporting_country " + - "from epipulse_location_configuration " + - "where type='Country' and country_iso2_code = :countryIso2Code"; - //@formatter:on - - @SuppressWarnings("unchecked") - String reportingCountry = (String) em.createNativeQuery(reportingCountryQuery) - .setParameter("countryIso2Code", serverCountryLocale) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); - - if (StringUtils.isBlank(reportingCountry)) { - throw new IllegalArgumentException("Invalid server country code: " + serverCountryLocale); - } - - //lookup server country nuts code - //@formatter:off - String serverCountryQuery = - "select nutscode " + - "from country " + - "where lower(defaultname) = :countryName"; - //@formatter:on - - @SuppressWarnings("unchecked") - String serverCountryNutsCode = (String) em.createNativeQuery(serverCountryQuery) - .setParameter("countryName", serverCountryName.toLowerCase()) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); - - //get subject code - //@formatter:off - String subjectCodeQuery = - "select subjectcode " + - "from epipulse_subjectcode_configuration " + - "where disease=:disease and aggregatedreporting='No'"; - //@formatter:on - - @SuppressWarnings("unchecked") - String subjectCode = (String) em.createNativeQuery(subjectCodeQuery) - .setParameter("disease", exportDto.getSubjectCode().name()) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); + @EJB + private PertussisExportStrategy pertussisExportStrategy; - if (StringUtils.isBlank(subjectCode)) { - throw new IllegalStateException("Subject code is empty"); - } - - //@formatter:off - String diseaseExportQuery = - "WITH variables AS (SELECT :disease AS disease," + - " :subjectCode AS subject_code," + - " :countryLocale AS country_locale," + - " CAST(:startDate AS date) AS start_date," + - " CAST(:endDate AS date) AS end_date)," + - " config_data AS (SELECT v.subject_code," + - " (SELECT epl.code" + - " FROM epipulse_location_configuration epl" + - " WHERE epl.type = 'Country'" + - " AND epl.country_iso2_code = v.country_locale) as reporting_country," + - " (SELECT epd.datasource" + - " FROM epipulse_datasource_configuration epd" + - " WHERE epd.country_iso2_code = v.country_locale" + - " AND epd.subjectcode = v.subject_code) as datasource" + - " FROM variables v)," + - " filtered_cases AS (SELECT c.id," + - " c.uuid," + - " c.deleted," + - " c.reportdate," + - " c.caseclassification," + - " c.outcome," + - " c.person_id," + - " c.symptoms_id," + - " c.hospitalization_id," + - " c.responsibleregion_id," + - " c.responsibledistrict_id," + - " c.responsiblecommunity_id" + - " FROM cases c" + - " CROSS JOIN variables v" + - " WHERE c.disease = v.disease" + - " AND c.reportdate >= v.start_date" + - " AND c.reportdate < (v.end_date + interval '1 day'))," + - " case_all_prev_hsp_from_latest AS (SELECT prev_hsp.hospitalization_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(prev_hsp.admittedtohealthfacility, '')," + - " COALESCE(prev_hsp.hospitalizationreason, '')," + - " COALESCE(" + - " TO_CHAR(prev_hsp.admissiondate, 'YYYY-MM-DD')," + - " '')," + - " COALESCE(" + - " TO_CHAR(prev_hsp.dischargedate, 'YYYY-MM-DD')," + - " '')" + - " ), '#'" + - " ORDER BY prev_hsp.admissiondate DESC) as all_prev_hsp_from_latest" + - " FROM previoushospitalization as prev_hsp" + - " WHERE hospitalization_id IN (SELECT hospitalization_id" + - " FROM filtered_cases" + - " WHERE hospitalization_id IS NOT NULL)" + - " GROUP BY prev_hsp.hospitalization_id)," + - " case_all_samples_from_latest AS (SELECT samples.associatedcase_id," + - " ARRAY_AGG(samples.id ORDER BY samples.sampledatetime DESC) as all_sample_ids_from_latest" + - " FROM samples" + - " WHERE samples.associatedcase_id IN (SELECT id FROM filtered_cases)" + - " GROUP BY samples.associatedcase_id)," + - " sample_all_pathogen_tests_from_latest AS (SELECT pathogentest.sample_id," + - " STRING_AGG(CONCAT_WS('|'," + - " pathogentest.testtype," + - " pathogentest.testresult" + - " ), '#'" + - " ORDER BY pathogentest.testdatetime DESC) AS all_pathogen_tests_from_latest" + - " FROM pathogentest" + - " INNER JOIN case_all_samples_from_latest" + - " ON pathogentest.sample_id = ANY" + - " (case_all_samples_from_latest.all_sample_ids_from_latest)" + - " GROUP BY pathogentest.sample_id)," + - "case_all_immunizations AS (SELECT i.person_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(to_char(i.startdate, 'YYYY-MM-DD'), '')," + - " COALESCE(to_char(i.enddate, 'YYYY-MM-DD'), '')," + - " COALESCE(i.meansofimmunization, '')," + - " COALESCE(CAST(i.numberofdoses as text), '')), '#'" + - " ORDER BY i.startdate DESC) as all_immunizations_from_latest" + - " FROM immunization i" + - " CROSS JOIN variables v" + - " where i.person_id IN (SELECT person_id FROM filtered_cases)" + - " and i.disease = v.disease" + - " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + - " GROUP BY i.person_id)," + - "case_all_vaccinations AS (SELECT i.person_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(to_char(v.vaccinationdate, 'YYYY-MM-DD'), '')," + - " COALESCE(v.vaccinedose, '')), '#'" + - " ORDER BY v.vaccinationdate DESC) as all_vaccinations_from_latest" + - " FROM immunization i" + - " INNER JOIN vaccination v ON i.id = v.immunization_id" + - " CROSS JOIN variables" + - " WHERE i.person_id IN (SELECT person_id FROM filtered_cases)" + - " and i.disease = variables.disease" + - " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + - " GROUP BY i.person_id) " + - "SELECT cd.reporting_country," + - " c.deleted," + - " cd.subject_code," + - " c.uuid as case_uuid," + - " cd.datasource," + - " cast(c.reportdate as date) as case_reportdate," + - " person.birthdate_yyyy," + - " person.birthdate_mm," + - " person.birthdate_dd," + - " cast(symptom.onsetdate as date) as symptom_onsetdate," + - " person.sex," + - " person_address_community.nutscode as address_community_nutscode," + - " person_address_district.nutscode as address_district_nutscode," + - " person_address_region.nutscode as address_region_nutscode," + - " person_address_country.nutscode as address_country_nutscode," + - " responsible_community.nutscode as responsible_community_nutscode," + - " responsible_district.nutscode as responsible_district_nutscode," + - " responsible_region.nutscode as responsible_region_nutscode," + - " c.caseclassification," + - " hospitalization.admittedtohealthfacility," + - " hospitalization.hospitalizationreason," + - " cast(hospitalization.admissiondate as date) as admissiondate," + - " cast(hospitalization.dischargedate as date) as dischargedate," + - " c.outcome as case_outcome," + - " case_all_prev_hsp_from_latest.all_prev_hsp_from_latest," + - " sample_all_pathogen_tests_from_latest.all_pathogen_tests_from_latest," + - " case_all_immunizations.all_immunizations_from_latest," + - " case_all_vaccinations.all_vaccinations_from_latest " + - "FROM filtered_cases c" + - " CROSS JOIN config_data cd" + - " LEFT JOIN region responsible_region ON c.responsibleregion_id = responsible_region.id" + - " LEFT JOIN district responsible_district ON c.responsibledistrict_id = responsible_district.id" + - " LEFT JOIN community responsible_community ON c.responsiblecommunity_id = responsible_community.id" + - " LEFT JOIN person ON c.person_id = person.id" + - " LEFT JOIN location person_address ON person.address_id = person_address.id" + - " LEFT JOIN country person_address_country ON person_address.country_id = person_address_country.id" + - " LEFT JOIN region person_address_region ON person_address.region_id = person_address_region.id" + - " LEFT JOIN district person_address_district ON person_address.district_id = person_address_district.id" + - " LEFT JOIN community person_address_community ON person_address.community_id = person_address_community.id" + - " LEFT JOIN symptoms symptom ON c.symptoms_id = symptom.id" + - " LEFT JOIN hospitalization ON c.hospitalization_id = hospitalization.id" + - " LEFT JOIN case_all_prev_hsp_from_latest ON (" + - " hospitalization.id = case_all_prev_hsp_from_latest.hospitalization_id" + - " )" + - " LEFT JOIN case_all_samples_from_latest ON (" + - " c.id = case_all_samples_from_latest.associatedcase_id" + - " )" + - " LEFT JOIN sample_all_pathogen_tests_from_latest ON (" + - " sample_all_pathogen_tests_from_latest.sample_id = ANY (case_all_samples_from_latest.all_sample_ids_from_latest)" + - " )" + - " LEFT JOIN case_all_immunizations ON (" + - " case_all_immunizations.person_id = c.person_id" + - " )" + - " LEFT JOIN case_all_vaccinations ON (" + - " case_all_vaccinations.person_id = c.person_id" + - " ) " + - "ORDER BY c.reportdate"; - //@formatter:on - Query query = em.createNativeQuery(diseaseExportQuery); - query.setParameter("disease", exportDto.getSubjectCode().getDisease().name()); - query.setParameter("subjectCode", subjectCode); - query.setParameter("countryLocale", serverCountryLocale); - query.setParameter("startDate", DateHelper.convertDateToDbFormat(exportDto.getStartDate())); - query.setParameter("endDate", DateHelper.convertDateToDbFormat(exportDto.getEndDate())); - query.setParameter("meansOfImmVaccination", MeansOfImmunization.VACCINATION.name()); - query.setParameter("meansOfImmVaccinationRecovery", MeansOfImmunization.VACCINATION_RECOVERY.name()); - - @SuppressWarnings("unchecked") - List resultList = query.getResultList(); - - List exportEntryList = new ArrayList<>(); - EpipulseDiseaseExportEntryDto dto = null; - int maxPathogenTests = 0; - int maxImmunizations = 0; - int pathogenTestCount = 0; - int immunizationCount = 0; - - List subjectCodePathogenTestTypes = - EpipulsePathogenTestTypeRef.getPathogenTestTypesByDisease(exportDto.getSubjectCode()); - - int index; - for (Object[] row : resultList) { - index = -1; - - dto = new EpipulseDiseaseExportEntryDto(); - dto.setReportingCountry((String) row[++index]); - dto.setDeleted((Boolean) row[++index]); - - String subjectCodeFromDb = (String) row[++index]; - if (!StringUtils.isBlank(subjectCodeFromDb)) { - dto.setSubjectCode(EpipulseSubjectCode.valueOf(subjectCodeFromDb)); - } - - dto.setNationalRecordId((String) row[++index]); - - dto.setDataSource((String) row[++index]); - dto.setReportDate((Date) row[++index]); - dto.setYearOfBirth((Integer) row[++index]); - dto.setMonthOfBirth((Integer) row[++index]); - dto.setDayOfBirth((Integer) row[++index]); - dto.setSymptomOnsetDate((Date) row[++index]); - - String sex = (String) row[++index]; - if (!StringUtils.isBlank(sex)) { - dto.setSex(Sex.valueOf(sex)); - } - - dto.setAddressCommunityNutsCode((String) row[++index]); - dto.setAddressDistrictNutsCode((String) row[++index]); - dto.setAddressRegionNutsCode((String) row[++index]); - dto.setAddressCountryNutsCode((String) row[++index]); - - dto.setResponsibleCommunityNutsCode((String) row[++index]); - dto.setResponsibleDistrictNutsCode((String) row[++index]); - dto.setResponsibleRegionNutsCode((String) row[++index]); - - dto.setServerCountryNutsCode(serverCountryNutsCode); - - String caseClassification = (String) row[++index]; - if (!StringUtils.isBlank(caseClassification)) { - dto.setCaseClassification(CaseClassification.valueOf(caseClassification)); - } + @EJB + private MeaslesExportStrategy measlesExportStrategy; - String admittedToHealthFacility = (String) row[++index]; - if (!StringUtils.isBlank(admittedToHealthFacility)) { - dto.setAdmittedToHealthFacility(YesNoUnknown.valueOf(admittedToHealthFacility)); - } - - String hospitalizationReason = (String) row[++index]; - if (!StringUtils.isBlank(hospitalizationReason)) { - dto.setHospitalizationReason(HospitalizationReasonType.valueOf(hospitalizationReason)); - } - - dto.setAdmissionDate((Date) row[++index]); - dto.setDischargeDate((Date) row[++index]); - - String caseOutcome = (String) row[++index]; - if (!StringUtils.isBlank(caseOutcome)) { - dto.setCaseOutcome(CaseOutcome.valueOf(caseOutcome)); - } - - dto.setPreviousHospitalizations(dto.parsePreviousHospitalizationChecks((String) row[++index])); - dto.setPathogenTests(dto.parsePathogenTestChecks((String) row[++index], subjectCodePathogenTestTypes)); - dto.setImmunizations(dto.parseImmunizationChecks((String) row[++index])); - dto.setVaccinations(dto.parseVaccinations((String) row[++index])); - - dto.calculateAge(); - - pathogenTestCount = dto.getPathogenTests().size(); - if (pathogenTestCount > maxPathogenTests) { - maxPathogenTests = pathogenTestCount; - } - - immunizationCount = dto.getImmunizations().size(); - if (immunizationCount > maxImmunizations) { - maxImmunizations = immunizationCount; - } - - exportEntryList.add(dto); - } - - exportResult.setMaxPathogenTests(maxPathogenTests); - exportResult.setMaxImmunizations(maxImmunizations); - exportResult.setExportEntryList(exportEntryList); - } catch (Exception e) { - logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage()); - throw e; - } - - return exportResult; + public EpipulseDiseaseExportResult exportPertussisCaseBased(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) + throws SQLException, IllegalStateException, IllegalArgumentException { + return pertussisExportStrategy.export(exportDto, serverCountryLocale, serverCountryName); } public EpipulseDiseaseExportResult exportMeaslesCaseBased(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) throws SQLException, IllegalStateException, IllegalArgumentException { - - EpipulseDiseaseExportResult exportResult = new EpipulseDiseaseExportResult(); - - try { - //lookup reporting country - //@formatter:off - String reportingCountryQuery = - "select code as reporting_country " + - "from epipulse_location_configuration " + - "where type='Country' and country_iso2_code = :countryIso2Code"; - //@formatter:on - - @SuppressWarnings("unchecked") - String reportingCountry = (String) em.createNativeQuery(reportingCountryQuery) - .setParameter("countryIso2Code", serverCountryLocale) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); - - if (StringUtils.isBlank(reportingCountry)) { - throw new IllegalArgumentException("Invalid server country code: " + serverCountryLocale); - } - - //lookup server country nuts code - //@formatter:off - String serverCountryQuery = - "select nutscode " + - "from country " + - "where lower(defaultname) = :countryName"; - //@formatter:on - - @SuppressWarnings("unchecked") - String serverCountryNutsCode = (String) em.createNativeQuery(serverCountryQuery) - .setParameter("countryName", serverCountryName.toLowerCase()) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); - - //get subject code - //@formatter:off - String subjectCodeQuery = - "select subjectcode " + - "from epipulse_subjectcode_configuration " + - "where disease=:disease and aggregatedreporting='No'"; - //@formatter:on - - @SuppressWarnings("unchecked") - String subjectCode = (String) em.createNativeQuery(subjectCodeQuery) - .setParameter("disease", exportDto.getSubjectCode().name()) - .getResultStream() - .filter(java.util.Objects::nonNull) - .findFirst() - .orElse(null); - - if (StringUtils.isBlank(subjectCode)) { - throw new IllegalStateException("Subject code is empty"); - } - - //@formatter:off - String diseaseExportQuery = - "WITH variables AS (SELECT :disease AS disease," + - " :subjectCode AS subject_code," + - " :countryLocale AS country_locale," + - " CAST(:startDate AS date) AS start_date," + - " CAST(:endDate AS date) AS end_date)," + - " config_data AS (SELECT v.subject_code," + - " (SELECT epl.code" + - " FROM epipulse_location_configuration epl" + - " WHERE epl.type = 'Country'" + - " AND epl.country_iso2_code = v.country_locale) as reporting_country," + - " (SELECT epd.datasource" + - " FROM epipulse_datasource_configuration epd" + - " WHERE epd.country_iso2_code = v.country_locale" + - " AND epd.subjectcode = v.subject_code) as datasource" + - " FROM variables v)," + - " filtered_cases AS (SELECT c.id," + - " c.uuid," + - " c.deleted," + - " c.reportdate," + - " c.caseclassification," + - " c.outcome," + - " c.person_id," + - " c.symptoms_id," + - " c.hospitalization_id," + - " c.responsibleregion_id," + - " c.responsibledistrict_id," + - " c.responsiblecommunity_id," + - " c.epidata_id," + - " c.investigateddate," + - " c.clinicalconfirmation" + - " FROM cases c" + - " CROSS JOIN variables v" + - " WHERE c.disease = v.disease" + - " AND c.reportdate >= v.start_date" + - " AND c.reportdate < (v.end_date + interval '1 day'))," + - " case_all_prev_hsp_from_latest AS (SELECT prev_hsp.hospitalization_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(prev_hsp.admittedtohealthfacility, '')," + - " COALESCE(prev_hsp.hospitalizationreason, '')," + - " COALESCE(" + - " TO_CHAR(prev_hsp.admissiondate, 'YYYY-MM-DD')," + - " '')," + - " COALESCE(" + - " TO_CHAR(prev_hsp.dischargedate, 'YYYY-MM-DD')," + - " '')" + - " ), '#'" + - " ORDER BY prev_hsp.admissiondate DESC) as all_prev_hsp_from_latest" + - " FROM previoushospitalization as prev_hsp" + - " WHERE hospitalization_id IN (SELECT hospitalization_id" + - " FROM filtered_cases" + - " WHERE hospitalization_id IS NOT NULL)" + - " GROUP BY prev_hsp.hospitalization_id)," + - " case_all_samples_from_latest AS (SELECT samples.associatedcase_id," + - " ARRAY_AGG(samples.id ORDER BY samples.sampledatetime DESC) as all_sample_ids_from_latest" + - " FROM samples" + - " WHERE samples.associatedcase_id IN (SELECT id FROM filtered_cases)" + - " GROUP BY samples.associatedcase_id)," + - " sample_all_pathogen_tests_from_latest AS (SELECT pathogentest.sample_id," + - " STRING_AGG(CONCAT_WS('|'," + - " pathogentest.testtype," + - " pathogentest.testresult" + - " ), '#'" + - " ORDER BY pathogentest.testdatetime DESC) AS all_pathogen_tests_from_latest" + - " FROM pathogentest" + - " INNER JOIN case_all_samples_from_latest" + - " ON pathogentest.sample_id = ANY" + - " (case_all_samples_from_latest.all_sample_ids_from_latest)" + - " GROUP BY pathogentest.sample_id)," + - "case_all_immunizations AS (SELECT i.person_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(to_char(i.startdate, 'YYYY-MM-DD'), '')," + - " COALESCE(to_char(i.enddate, 'YYYY-MM-DD'), '')," + - " COALESCE(i.meansofimmunization, '')," + - " COALESCE(CAST(i.numberofdoses as text), '')), '#'" + - " ORDER BY i.startdate DESC) as all_immunizations_from_latest" + - " FROM immunization i" + - " CROSS JOIN variables v" + - " where i.person_id IN (SELECT person_id FROM filtered_cases)" + - " and i.disease = v.disease" + - " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + - " GROUP BY i.person_id)," + - "case_all_vaccinations AS (SELECT i.person_id," + - " STRING_AGG(CONCAT_WS('|'," + - " COALESCE(to_char(v.vaccinationdate, 'YYYY-MM-DD'), '')," + - " COALESCE(v.vaccinedose, '')), '#'" + - " ORDER BY v.vaccinationdate DESC) as all_vaccinations_from_latest" + - " FROM immunization i" + - " INNER JOIN vaccination v ON i.id = v.immunization_id" + - " CROSS JOIN variables" + - " WHERE i.person_id IN (SELECT person_id FROM filtered_cases)" + - " and i.disease = variables.disease" + - " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + - " GROUP BY i.person_id), " + - "sample_data AS (SELECT c.id as case_id," + - " MIN(s.sampledatetime) as first_specimen_date," + - " STRING_AGG(DISTINCT CAST(s2.samplematerial AS text), ',' ORDER BY CAST(s2.samplematerial AS text)) as specimen_types_virus," + - " STRING_AGG(DISTINCT CAST(s3.samplematerial AS text), ',' ORDER BY CAST(s3.samplematerial AS text)) as specimen_types_serology " + - " FROM filtered_cases c " + - " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + - " LEFT JOIN samples s2 ON s2.associatedcase_id = c.id AND s2.deleted = false AND s2.samplematerial IS NOT NULL " + - " LEFT JOIN (SELECT DISTINCT s_sero.id, s_sero.associatedcase_id, s_sero.samplematerial " + - " FROM samples s_sero " + - " JOIN pathogentest pt_sero ON pt_sero.sample_id = s_sero.id " + - " WHERE s_sero.deleted = false " + - " AND pt_sero.testtype IN ('IGG_SERUM_ANTIBODY', 'IGM_SERUM_ANTIBODY', 'SEROLOGY')) s3 " + - " ON s3.associatedcase_id = c.id " + - " GROUP BY c.id), " + - "virus_detection_data AS (SELECT c.id as case_id," + - " MIN(pt.testdatetime) as lab_result_date," + - " (SELECT pt2.testresult " + - " FROM samples s2 " + - " JOIN pathogentest pt2 ON pt2.sample_id = s2.id " + - " WHERE s2.associatedcase_id = c.id " + - " AND s2.deleted = false " + - " AND pt2.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + - " AND pt2.testresultverified = true " + - " ORDER BY pt2.testdatetime ASC " + - " LIMIT 1) as virus_detection_result," + - " (SELECT COALESCE(pt3.typingid, pt3.genotyperesult) " + - " FROM samples s3 " + - " JOIN pathogentest pt3 ON pt3.sample_id = s3.id " + - " WHERE s3.associatedcase_id = c.id " + - " AND s3.deleted = false " + - " AND (pt3.typingid IS NOT NULL OR pt3.genotyperesult IS NOT NULL) " + - " ORDER BY pt3.testdatetime ASC " + - " LIMIT 1) as genotype_raw " + - " FROM filtered_cases c " + - " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + - " LEFT JOIN pathogentest pt ON pt.sample_id = s.id " + - " AND pt.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + - " AND pt.testresultverified = true " + - " GROUP BY c.id), " + - "igg_serology_data AS (SELECT c.id as case_id," + - " (SELECT CASE " + - " WHEN pt_igg.fourfoldincreaseantibodytiter = 'YES' THEN 'POSITIVE' " + - " ELSE pt_igg.testresult " + - " END " + - " FROM samples s_igg " + - " JOIN pathogentest pt_igg ON pt_igg.sample_id = s_igg.id " + - " WHERE s_igg.associatedcase_id = c.id " + - " AND s_igg.deleted = false " + - " AND pt_igg.testtype = 'IGG_SERUM_ANTIBODY' " + - " ORDER BY pt_igg.testdatetime ASC " + - " LIMIT 1) as igg_result " + - " FROM filtered_cases c), " + - "igm_serology_data AS (SELECT c.id as case_id," + - " (SELECT pt_igm.testresult " + - " FROM samples s_igm " + - " JOIN pathogentest pt_igm ON pt_igm.sample_id = s_igm.id " + - " WHERE s_igm.associatedcase_id = c.id " + - " AND s_igm.deleted = false " + - " AND pt_igm.testtype = 'IGM_SERUM_ANTIBODY' " + - " ORDER BY pt_igm.testdatetime ASC " + - " LIMIT 1) as igm_result " + - " FROM filtered_cases c), " + - "epidata_cluster AS (SELECT c.id as case_id," + - " epi.clusterrelated," + - " epi.clustertypetext," + - " epi.clustertype," + - " epi.caseimportedstatus " + - " FROM filtered_cases c " + - " LEFT JOIN epidata epi ON c.epidata_id = epi.id), " + - "exposure_locations AS (SELECT e.epidata_id," + - " STRING_AGG(" + - " CASE " + - " WHEN co.defaultname IS NOT NULL THEN co.defaultname " + - " WHEN l.city IS NOT NULL THEN l.city " + - " ELSE l.details " + - " END, " + - " '; ' " + - " ORDER BY e.startdate DESC" + - " ) as infection_locations " + - " FROM exposures e " + - " JOIN location l ON e.location_id = l.id " + - " LEFT JOIN country co ON l.country_id = co.id " + - " WHERE e.epidata_id IN (SELECT epidata_id FROM filtered_cases WHERE epidata_id IS NOT NULL) " + - " GROUP BY e.epidata_id), " + - "complications_data AS (SELECT c.id as case_id," + - " s.acuteencephalitis," + - " s.diarrhea," + - " s.otitismedia," + - " s.othercomplications " + - " FROM filtered_cases c " + - " LEFT JOIN symptoms s ON c.symptoms_id = s.id) " + - "SELECT cd.reporting_country," + - " c.deleted," + - " cd.subject_code," + - " c.uuid as case_uuid," + - " cd.datasource," + - " cast(c.reportdate as date) as case_reportdate," + - " person.birthdate_yyyy," + - " person.birthdate_mm," + - " person.birthdate_dd," + - " cast(symptom.onsetdate as date) as symptom_onsetdate," + - " person.sex," + - " person_address_community.nutscode as address_community_nutscode," + - " person_address_district.nutscode as address_district_nutscode," + - " person_address_region.nutscode as address_region_nutscode," + - " person_address_country.nutscode as address_country_nutscode," + - " responsible_community.nutscode as responsible_community_nutscode," + - " responsible_district.nutscode as responsible_district_nutscode," + - " responsible_region.nutscode as responsible_region_nutscode," + - " c.caseclassification," + - " hospitalization.admittedtohealthfacility," + - " hospitalization.hospitalizationreason," + - " cast(hospitalization.admissiondate as date) as admissiondate," + - " cast(hospitalization.dischargedate as date) as dischargedate," + - " c.outcome as case_outcome," + - " case_all_prev_hsp_from_latest.all_prev_hsp_from_latest," + - " sample_all_pathogen_tests_from_latest.all_pathogen_tests_from_latest," + - " case_all_immunizations.all_immunizations_from_latest," + - " case_all_vaccinations.all_vaccinations_from_latest," + - " sd.first_specimen_date," + - " vd.lab_result_date," + - " sd.specimen_types_virus," + - " vd.virus_detection_result," + - " vd.genotype_raw," + - " sd.specimen_types_serology," + - " igg.igg_result," + - " igm.igm_result," + - " cast(c.investigateddate as date) as investigated_date," + - " ec.clusterrelated," + - " ec.clustertypetext," + - " ec.clustertype," + - " ec.caseimportedstatus," + - " comp.acuteencephalitis," + - " comp.diarrhea," + - " comp.otitismedia," + - " comp.othercomplications," + - " c.clinicalconfirmation," + - " el.infection_locations," + - " person.causeofdeathdetails " + - "FROM filtered_cases c" + - " CROSS JOIN config_data cd" + - " LEFT JOIN region responsible_region ON c.responsibleregion_id = responsible_region.id" + - " LEFT JOIN district responsible_district ON c.responsibledistrict_id = responsible_district.id" + - " LEFT JOIN community responsible_community ON c.responsiblecommunity_id = responsible_community.id" + - " LEFT JOIN person ON c.person_id = person.id" + - " LEFT JOIN location person_address ON person.address_id = person_address.id" + - " LEFT JOIN country person_address_country ON person_address.country_id = person_address_country.id" + - " LEFT JOIN region person_address_region ON person_address.region_id = person_address_region.id" + - " LEFT JOIN district person_address_district ON person_address.district_id = person_address_district.id" + - " LEFT JOIN community person_address_community ON person_address.community_id = person_address_community.id" + - " LEFT JOIN symptoms symptom ON c.symptoms_id = symptom.id" + - " LEFT JOIN hospitalization ON c.hospitalization_id = hospitalization.id" + - " LEFT JOIN case_all_prev_hsp_from_latest ON (" + - " hospitalization.id = case_all_prev_hsp_from_latest.hospitalization_id" + - " )" + - " LEFT JOIN case_all_samples_from_latest ON (" + - " c.id = case_all_samples_from_latest.associatedcase_id" + - " )" + - " LEFT JOIN sample_all_pathogen_tests_from_latest ON (" + - " sample_all_pathogen_tests_from_latest.sample_id = ANY (case_all_samples_from_latest.all_sample_ids_from_latest)" + - " )" + - " LEFT JOIN case_all_immunizations ON (" + - " case_all_immunizations.person_id = c.person_id" + - " )" + - " LEFT JOIN case_all_vaccinations ON (" + - " case_all_vaccinations.person_id = c.person_id" + - " )" + - " LEFT JOIN sample_data sd ON sd.case_id = c.id" + - " LEFT JOIN virus_detection_data vd ON vd.case_id = c.id" + - " LEFT JOIN igg_serology_data igg ON igg.case_id = c.id" + - " LEFT JOIN igm_serology_data igm ON igm.case_id = c.id" + - " LEFT JOIN epidata_cluster ec ON ec.case_id = c.id" + - " LEFT JOIN exposure_locations el ON el.epidata_id = c.epidata_id" + - " LEFT JOIN complications_data comp ON comp.case_id = c.id " + - "ORDER BY c.reportdate"; - //@formatter:on - Query query = em.createNativeQuery(diseaseExportQuery); - query.setParameter("disease", exportDto.getSubjectCode().getDisease().name()); - query.setParameter("subjectCode", subjectCode); - query.setParameter("countryLocale", serverCountryLocale); - query.setParameter("startDate", DateHelper.convertDateToDbFormat(exportDto.getStartDate())); - query.setParameter("endDate", DateHelper.convertDateToDbFormat(exportDto.getEndDate())); - query.setParameter("meansOfImmVaccination", MeansOfImmunization.VACCINATION.name()); - query.setParameter("meansOfImmVaccinationRecovery", MeansOfImmunization.VACCINATION_RECOVERY.name()); - - @SuppressWarnings("unchecked") - List resultList = query.getResultList(); - - List exportEntryList = new ArrayList<>(); - EpipulseDiseaseExportEntryDto dto = null; - int maxPathogenTests = 0; - int maxImmunizations = 0; - int pathogenTestCount = 0; - int immunizationCount = 0; - - List subjectCodePathogenTestTypes = - EpipulsePathogenTestTypeRef.getPathogenTestTypesByDisease(exportDto.getSubjectCode()); - - int index; - for (Object[] row : resultList) { - index = -1; - - dto = new EpipulseDiseaseExportEntryDto(); - dto.setReportingCountry((String) row[++index]); - dto.setDeleted((Boolean) row[++index]); - - String subjectCodeFromDb = (String) row[++index]; - if (!StringUtils.isBlank(subjectCodeFromDb)) { - dto.setSubjectCode(EpipulseSubjectCode.valueOf(subjectCodeFromDb)); - } - - dto.setNationalRecordId((String) row[++index]); - - dto.setDataSource((String) row[++index]); - dto.setReportDate((Date) row[++index]); - dto.setYearOfBirth((Integer) row[++index]); - dto.setMonthOfBirth((Integer) row[++index]); - dto.setDayOfBirth((Integer) row[++index]); - dto.setSymptomOnsetDate((Date) row[++index]); - - String sex = (String) row[++index]; - if (!StringUtils.isBlank(sex)) { - dto.setSex(Sex.valueOf(sex)); - } - - dto.setAddressCommunityNutsCode((String) row[++index]); - dto.setAddressDistrictNutsCode((String) row[++index]); - dto.setAddressRegionNutsCode((String) row[++index]); - dto.setAddressCountryNutsCode((String) row[++index]); - - dto.setResponsibleCommunityNutsCode((String) row[++index]); - dto.setResponsibleDistrictNutsCode((String) row[++index]); - dto.setResponsibleRegionNutsCode((String) row[++index]); - - dto.setServerCountryNutsCode(serverCountryNutsCode); - - String caseClassification = (String) row[++index]; - if (!StringUtils.isBlank(caseClassification)) { - dto.setCaseClassification(CaseClassification.valueOf(caseClassification)); - } - - String admittedToHealthFacility = (String) row[++index]; - if (!StringUtils.isBlank(admittedToHealthFacility)) { - dto.setAdmittedToHealthFacility(YesNoUnknown.valueOf(admittedToHealthFacility)); - } - - String hospitalizationReason = (String) row[++index]; - if (!StringUtils.isBlank(hospitalizationReason)) { - dto.setHospitalizationReason(HospitalizationReasonType.valueOf(hospitalizationReason)); - } - - dto.setAdmissionDate((Date) row[++index]); - dto.setDischargeDate((Date) row[++index]); - - String caseOutcome = (String) row[++index]; - if (!StringUtils.isBlank(caseOutcome)) { - dto.setCaseOutcome(CaseOutcome.valueOf(caseOutcome)); - } - - dto.setPreviousHospitalizations(dto.parsePreviousHospitalizationChecks((String) row[++index])); - dto.setPathogenTests(dto.parsePathogenTestChecks((String) row[++index], subjectCodePathogenTestTypes)); - dto.setImmunizations(dto.parseImmunizationChecks((String) row[++index])); - dto.setVaccinations(dto.parseVaccinations((String) row[++index])); - - // Phase 2: Laboratory data population for MEAS export - dto.setDateOfSpecimen((Date) row[++index]); - dto.setDateOfLaboratoryResult((Date) row[++index]); - - String specimenTypesVirusRaw = (String) row[++index]; - if (!StringUtils.isBlank(specimenTypesVirusRaw)) { - List specimenTypesVirus = new ArrayList<>(); - for (String specimenType : specimenTypesVirusRaw.split(",")) { - specimenTypesVirus.add( - EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); - } - dto.setTypeOfSpecimenCollected(specimenTypesVirus); - } - - String virusDetectionResultRaw = (String) row[++index]; - if (!StringUtils.isBlank(virusDetectionResultRaw)) { - dto.setResultOfVirusDetection( - EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(virusDetectionResultRaw))); - } - - String genotypeRaw = (String) row[++index]; - if (!StringUtils.isBlank(genotypeRaw)) { - dto.setGenotype(EpipulseLaboratoryMapper.normalizeGenotypeForEpipulse(genotypeRaw)); - } - - String specimenTypesSerologyRaw = (String) row[++index]; - if (!StringUtils.isBlank(specimenTypesSerologyRaw)) { - List specimenTypesSerology = new ArrayList<>(); - for (String specimenType : specimenTypesSerologyRaw.split(",")) { - specimenTypesSerology.add( - EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); - } - dto.setTypeOfSpecimenSerology(specimenTypesSerology); - } - - String iggResultRaw = (String) row[++index]; - if (!StringUtils.isBlank(iggResultRaw)) { - dto.setResultIgG(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(iggResultRaw))); - } - - String igmResultRaw = (String) row[++index]; - if (!StringUtils.isBlank(igmResultRaw)) { - dto.setResultIgM(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(igmResultRaw))); - } - - // Phase 3: Clinical and epidemiology data population for MEAS export - dto.setDateOfInvestigation((Date) row[++index]); - - Boolean clusterRelated = (Boolean) row[++index]; - dto.setClusterRelated(clusterRelated); - - dto.setClusterIdentification((String) row[++index]); - - String clusterTypeRaw = (String) row[++index]; - if (!StringUtils.isBlank(clusterTypeRaw)) { - // ClusterSetting is repeatable, but typically only one value per case - List clusterSettings = new ArrayList<>(); - clusterSettings.add( - EpipulseLaboratoryMapper.mapClusterTypeToEpipulseCode(ClusterType.valueOf(clusterTypeRaw))); - dto.setClusterSetting(clusterSettings); - } - - String caseImportedStatusRaw = (String) row[++index]; - if (!StringUtils.isBlank(caseImportedStatusRaw)) { - dto.setImportedStatus( - EpipulseLaboratoryMapper.mapCaseImportedStatusToEpipulseCode(CaseImportedStatus.valueOf(caseImportedStatusRaw))); - } - - // Complications mapping - String acuteEncephalitisRaw = (String) row[++index]; - String diarrheaRaw = (String) row[++index]; - String otitisMediaRaw = (String) row[++index]; - String otherComplicationsRaw = (String) row[++index]; - - SymptomState acuteEncephalitis = parseSymptomState(acuteEncephalitisRaw); - SymptomState diarrhea = parseSymptomState(diarrheaRaw); - SymptomState otitisMedia = parseSymptomState(otitisMediaRaw); - SymptomState otherComplications = parseSymptomState(otherComplicationsRaw); - - dto.setComplicationDiagnosis( - EpipulseLaboratoryMapper.mapSymptomsToComplicationCodes( - acuteEncephalitis, - diarrhea, - otitisMedia, - otherComplications)); - - // Clinical criteria status - String clinicalConfirmationRaw = (String) row[++index]; - if (!StringUtils.isBlank(clinicalConfirmationRaw)) { - dto.setClinicalCriteriaStatus( - EpipulseLaboratoryMapper.deriveClinicalCriteriaStatus(YesNoUnknown.valueOf(clinicalConfirmationRaw))); - } - - // Place of infection (exposure locations - semicolon-separated from SQL) - String placeOfInfectionRaw = (String) row[++index]; - if (!StringUtils.isBlank(placeOfInfectionRaw)) { - List placesOfInfection = new ArrayList<>(); - for (String place : placeOfInfectionRaw.split(";")) { - if (!place.trim().isEmpty()) { - placesOfInfection.add(place.trim()); - } - } - dto.setPlaceOfInfection(placesOfInfection); - } - - // Cause of death - dto.setCauseOfDeath((String) row[++index]); - - dto.calculateAge(); - - pathogenTestCount = dto.getPathogenTests().size(); - if (pathogenTestCount > maxPathogenTests) { - maxPathogenTests = pathogenTestCount; - } - - immunizationCount = dto.getImmunizations().size(); - if (immunizationCount > maxImmunizations) { - maxImmunizations = immunizationCount; - } - - exportEntryList.add(dto); - } - - // Track max counts for MEAS repeatable fields - int maxComplicationDiagnosis = 0; - int maxClusterSettings = 0; - int maxPlaceOfInfection = 0; - int maxSpecimenVirDetect = 0; - int maxSpecimenSero = 0; - - for (EpipulseDiseaseExportEntryDto entry : exportEntryList) { - if (entry.getComplicationDiagnosis() != null && entry.getComplicationDiagnosis().size() > maxComplicationDiagnosis) { - maxComplicationDiagnosis = entry.getComplicationDiagnosis().size(); - } - if (entry.getClusterSetting() != null && entry.getClusterSetting().size() > maxClusterSettings) { - maxClusterSettings = entry.getClusterSetting().size(); - } - if (entry.getPlaceOfInfection() != null && entry.getPlaceOfInfection().size() > maxPlaceOfInfection) { - maxPlaceOfInfection = entry.getPlaceOfInfection().size(); - } - if (entry.getTypeOfSpecimenCollected() != null && entry.getTypeOfSpecimenCollected().size() > maxSpecimenVirDetect) { - maxSpecimenVirDetect = entry.getTypeOfSpecimenCollected().size(); - } - if (entry.getTypeOfSpecimenSerology() != null && entry.getTypeOfSpecimenSerology().size() > maxSpecimenSero) { - maxSpecimenSero = entry.getTypeOfSpecimenSerology().size(); - } - } - - exportResult.setMaxPathogenTests(maxPathogenTests); - exportResult.setMaxImmunizations(maxImmunizations); - exportResult.setMaxComplicationDiagnosis(maxComplicationDiagnosis); - exportResult.setMaxClusterSettings(maxClusterSettings); - exportResult.setMaxPlaceOfInfection(maxPlaceOfInfection); - exportResult.setMaxSpecimenVirDetect(maxSpecimenVirDetect); - exportResult.setMaxSpecimenSero(maxSpecimenSero); - exportResult.setExportEntryList(exportEntryList); - } catch (Exception e) { - logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage()); - throw e; - } - - return exportResult; + return measlesExportStrategy.export(exportDto, serverCountryLocale, serverCountryName); } @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) @@ -1055,16 +144,4 @@ public String generateDownloadFileName(EpipulseExportDto exportDto, Long exportI + "_" + StringUtils.replace(DateHelper.convertDateToDbFormat(exportDto.getEndDate()), "-", "") + "_" + exportId + "_" + (System.currentTimeMillis()) + ".csv"; } - - private SymptomState parseSymptomState(String value) { - if (StringUtils.isBlank(value)) { - return null; - } - try { - return SymptomState.valueOf(value); - } catch (IllegalArgumentException e) { - logger.warn("Invalid SymptomState value '{}', treating as null", value); - return null; - } - } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseSqlCteBuilder.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseSqlCteBuilder.java new file mode 100644 index 00000000000..c426b278c96 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseSqlCteBuilder.java @@ -0,0 +1,306 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +/** + * Service for building SQL Common Table Expressions (CTEs) for Epipulse disease exports. + * This service extracts the common SQL query fragments that are shared between different + * disease export strategies (Pertussis, Measles, etc.). + */ +@Stateless +@LocalBean +public class EpipulseSqlCteBuilder { + + /** + * Builds the variables CTE that defines the export parameters. + * This CTE is used by all other CTEs to access the disease, subject code, dates, etc. + * + * @return the variables CTE SQL fragment + */ + public String buildVariablesCte() { + //@formatter:off + return "WITH variables AS (SELECT :disease AS disease," + + " :subjectCode AS subject_code," + + " :countryLocale AS country_locale," + + " CAST(:startDate AS date) AS start_date," + + " CAST(:endDate AS date) AS end_date),"; + //@formatter:on + } + + /** + * Builds the config_data CTE that looks up reporting country and data source. + * + * @return the config_data CTE SQL fragment + */ + public String buildConfigDataCte() { + //@formatter:off + return " config_data AS (SELECT v.subject_code," + + " (SELECT epl.code" + + " FROM epipulse_location_configuration epl" + + " WHERE epl.type = 'Country'" + + " AND epl.country_iso2_code = v.country_locale) as reporting_country," + + " (SELECT epd.datasource" + + " FROM epipulse_datasource_configuration epd" + + " WHERE epd.country_iso2_code = v.country_locale" + + " AND epd.subjectcode = v.subject_code) as datasource" + + " FROM variables v),"; + //@formatter:on + } + + /** + * Builds the filtered_cases CTE that selects all cases matching the export criteria. + * + * @param includeMeaslesFields + * if true, includes additional fields required for Measles export + * (epidata_id, investigateddate, clinicalconfirmation) + * @return the filtered_cases CTE SQL fragment + */ + public String buildFilteredCasesCte(boolean includeMeaslesFields) { + StringBuilder cte = new StringBuilder(); + //@formatter:off + cte.append(" filtered_cases AS (SELECT c.id,") + .append(" c.uuid,") + .append(" c.deleted,") + .append(" c.reportdate,") + .append(" c.caseclassification,") + .append(" c.outcome,") + .append(" c.person_id,") + .append(" c.symptoms_id,") + .append(" c.hospitalization_id,") + .append(" c.responsibleregion_id,") + .append(" c.responsibledistrict_id,") + .append(" c.responsiblecommunity_id"); + + if (includeMeaslesFields) { + cte.append(",") + .append(" c.epidata_id,") + .append(" c.investigateddate,") + .append(" c.clinicalconfirmation"); + } + + cte.append(" FROM cases c") + .append(" CROSS JOIN variables v") + .append(" WHERE c.disease = v.disease") + .append(" AND c.reportdate >= v.start_date") + .append(" AND c.reportdate < (v.end_date + interval '1 day')),"); + //@formatter:on + + return cte.toString(); + } + + /** + * Builds the case_all_prev_hsp_from_latest CTE that aggregates previous hospitalizations. + * + * @return the previous hospitalizations CTE SQL fragment + */ + public String buildPreviousHospitalizationsCte() { + //@formatter:off + return " case_all_prev_hsp_from_latest AS (SELECT prev_hsp.hospitalization_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(prev_hsp.admittedtohealthfacility, '')," + + " COALESCE(prev_hsp.hospitalizationreason, '')," + + " COALESCE(" + + " TO_CHAR(prev_hsp.admissiondate, 'YYYY-MM-DD')," + + " '')," + + " COALESCE(" + + " TO_CHAR(prev_hsp.dischargedate, 'YYYY-MM-DD')," + + " '')" + + " ), '#'" + + " ORDER BY prev_hsp.admissiondate DESC) as all_prev_hsp_from_latest" + + " FROM previoushospitalization as prev_hsp" + + " WHERE hospitalization_id IN (SELECT hospitalization_id" + + " FROM filtered_cases" + + " WHERE hospitalization_id IS NOT NULL)" + + " GROUP BY prev_hsp.hospitalization_id),"; + //@formatter:on + } + + /** + * Builds the case_all_samples_from_latest CTE that aggregates all samples for filtered cases. + * + * @return the samples CTE SQL fragment + */ + public String buildSamplesCte() { + //@formatter:off + return " case_all_samples_from_latest AS (SELECT samples.associatedcase_id," + + " ARRAY_AGG(samples.id ORDER BY samples.sampledatetime DESC) as all_sample_ids_from_latest" + + " FROM samples" + + " WHERE samples.associatedcase_id IN (SELECT id FROM filtered_cases)" + + " GROUP BY samples.associatedcase_id),"; + //@formatter:on + } + + /** + * Builds the sample_all_pathogen_tests_from_latest CTE that aggregates pathogen test results. + * Groups by associatedcase_id to prevent duplicate rows when a case has multiple samples. + * + * @return the pathogen tests CTE SQL fragment + */ + public String buildPathogenTestsCte() { + //@formatter:off + return " sample_all_pathogen_tests_from_latest AS (SELECT case_all_samples_from_latest.associatedcase_id," + + " STRING_AGG(CONCAT_WS('|'," + + " pathogentest.testtype," + + " pathogentest.testresult" + + " ), '#'" + + " ORDER BY pathogentest.testdatetime DESC) AS all_pathogen_tests_from_latest" + + " FROM pathogentest" + + " INNER JOIN case_all_samples_from_latest" + + " ON pathogentest.sample_id = ANY" + + " (case_all_samples_from_latest.all_sample_ids_from_latest)" + + " GROUP BY case_all_samples_from_latest.associatedcase_id),"; + //@formatter:on + } + + /** + * Builds the case_all_immunizations CTE that aggregates immunization records. + * + * @return the immunizations CTE SQL fragment + */ + public String buildImmunizationsCte() { + //@formatter:off + return "case_all_immunizations AS (SELECT i.person_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(to_char(i.startdate, 'YYYY-MM-DD'), '')," + + " COALESCE(to_char(i.enddate, 'YYYY-MM-DD'), '')," + + " COALESCE(i.meansofimmunization, '')," + + " COALESCE(CAST(i.numberofdoses as text), '')), '#'" + + " ORDER BY i.startdate DESC) as all_immunizations_from_latest" + + " FROM immunization i" + + " CROSS JOIN variables v" + + " where i.person_id IN (SELECT person_id FROM filtered_cases)" + + " and i.disease = v.disease" + + " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + + " GROUP BY i.person_id),"; + //@formatter:on + } + + /** + * Builds the case_all_vaccinations CTE that aggregates vaccination records. + * + * @return the vaccinations CTE SQL fragment + */ + public String buildVaccinationsCte() { + //@formatter:off + return "case_all_vaccinations AS (SELECT i.person_id," + + " STRING_AGG(CONCAT_WS('|'," + + " COALESCE(to_char(v.vaccinationdate, 'YYYY-MM-DD'), '')," + + " COALESCE(v.vaccinedose, '')), '#'" + + " ORDER BY v.vaccinationdate DESC) as all_vaccinations_from_latest" + + " FROM immunization i" + + " INNER JOIN vaccination v ON i.id = v.immunization_id" + + " CROSS JOIN variables" + + " WHERE i.person_id IN (SELECT person_id FROM filtered_cases)" + + " and i.disease = variables.disease" + + " and i.meansofimmunization IN (:meansOfImmVaccination, :meansOfImmVaccinationRecovery)" + + " GROUP BY i.person_id) "; + //@formatter:on + } + + /** + * Builds just the common SELECT field list (without the SELECT keyword or FROM clause). + * This allows strategies to append disease-specific fields before the FROM clause. + * + * @return the common SELECT fields as a comma-separated list + */ + public String buildCommonSelectFields() { + //@formatter:off + return "cd.reporting_country," + + " c.deleted," + + " cd.subject_code," + + " c.uuid as case_uuid," + + " cd.datasource," + + " cast(c.reportdate as date) as case_reportdate," + + " person.birthdate_yyyy," + + " person.birthdate_mm," + + " person.birthdate_dd," + + " cast(symptom.onsetdate as date) as symptom_onsetdate," + + " person.sex," + + " person_address_community.nutscode as address_community_nutscode," + + " person_address_district.nutscode as address_district_nutscode," + + " person_address_region.nutscode as address_region_nutscode," + + " person_address_country.nutscode as address_country_nutscode," + + " responsible_community.nutscode as responsible_community_nutscode," + + " responsible_district.nutscode as responsible_district_nutscode," + + " responsible_region.nutscode as responsible_region_nutscode," + + " c.caseclassification," + + " hospitalization.admittedtohealthfacility," + + " hospitalization.hospitalizationreason," + + " cast(hospitalization.admissiondate as date) as admissiondate," + + " cast(hospitalization.dischargedate as date) as dischargedate," + + " c.outcome as case_outcome," + + " case_all_prev_hsp_from_latest.all_prev_hsp_from_latest," + + " sample_all_pathogen_tests_from_latest.all_pathogen_tests_from_latest," + + " case_all_immunizations.all_immunizations_from_latest," + + " case_all_vaccinations.all_vaccinations_from_latest"; + //@formatter:on + } + + /** + * Builds the common FROM and JOIN clauses. + * This includes all joins that are common to all disease exports. + * + * @return the FROM and JOIN clauses SQL fragment + */ + public String buildCommonFromAndJoins() { + StringBuilder joins = new StringBuilder(); + //@formatter:off + joins.append(" FROM filtered_cases c") + .append(" CROSS JOIN config_data cd") + .append(" LEFT JOIN region responsible_region ON c.responsibleregion_id = responsible_region.id") + .append(" LEFT JOIN district responsible_district ON c.responsibledistrict_id = responsible_district.id") + .append(" LEFT JOIN community responsible_community ON c.responsiblecommunity_id = responsible_community.id") + .append(" LEFT JOIN person ON c.person_id = person.id") + .append(" LEFT JOIN location person_address ON person.address_id = person_address.id") + .append(" LEFT JOIN country person_address_country ON person_address.country_id = person_address_country.id") + .append(" LEFT JOIN region person_address_region ON person_address.region_id = person_address_region.id") + .append(" LEFT JOIN district person_address_district ON person_address.district_id = person_address_district.id") + .append(" LEFT JOIN community person_address_community ON person_address.community_id = person_address_community.id") + .append(" LEFT JOIN symptoms symptom ON c.symptoms_id = symptom.id") + .append(" LEFT JOIN hospitalization ON c.hospitalization_id = hospitalization.id") + .append(" LEFT JOIN case_all_prev_hsp_from_latest ON (") + .append(" hospitalization.id = case_all_prev_hsp_from_latest.hospitalization_id") + .append(" )") + .append(" LEFT JOIN case_all_samples_from_latest ON (") + .append(" c.id = case_all_samples_from_latest.associatedcase_id") + .append(" )") + .append(" LEFT JOIN sample_all_pathogen_tests_from_latest ON (") + .append(" c.id = sample_all_pathogen_tests_from_latest.associatedcase_id") + .append(" )") + .append(" LEFT JOIN case_all_immunizations ON (") + .append(" case_all_immunizations.person_id = c.person_id") + .append(" )") + .append(" LEFT JOIN case_all_vaccinations ON (") + .append(" case_all_vaccinations.person_id = c.person_id") + .append(" )"); + //@formatter:on + + return joins.toString(); + } + + /** + * Builds the main SELECT clause with all common fields and joins. + * This is a convenience method for diseases that don't need additional fields. + * + * @return the complete SELECT statement with FROM, JOINs, and ORDER BY + */ + public String buildMainSelectClause() { + return "SELECT " + buildCommonSelectFields() + buildCommonFromAndJoins() + " ORDER BY c.reportdate"; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java new file mode 100644 index 00000000000..02a46f85046 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java @@ -0,0 +1,220 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.EJB; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; +import de.symeda.sormas.api.epipulse.EpipulseExportDto; +import de.symeda.sormas.api.epipulse.referencevalue.EpipulsePathogenTestTypeRef; +import de.symeda.sormas.api.immunization.MeansOfImmunization; +import de.symeda.sormas.api.sample.PathogenTestType; +import de.symeda.sormas.api.utils.DateHelper; +import de.symeda.sormas.backend.epipulse.EpipulseCommonDtoMapper; +import de.symeda.sormas.backend.epipulse.EpipulseConfigurationLookupService; +import de.symeda.sormas.backend.epipulse.EpipulseSqlCteBuilder; +import de.symeda.sormas.backend.epipulse.util.EpipulseConfigurationContext; +import de.symeda.sormas.backend.util.ModelConstants; + +/** + * Abstract base class implementing the Template Method pattern for Epipulse disease exports. + * This class defines the overall export algorithm while allowing subclasses to customize + * disease-specific behavior through abstract methods. + *

+ * The template method {@link #export(EpipulseExportDto, String, String)} orchestrates: + * 1. Configuration lookup (common) + * 2. SQL query building (disease-specific hook) + * 3. Query execution (common) + * 4. DTO mapping (combination of common + disease-specific) + * 5. Result building with max counts (common + disease-specific) + */ +public abstract class AbstractEpipulseDiseaseExportStrategy { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + @PersistenceContext(unitName = ModelConstants.PERSISTENCE_UNIT_NAME) + protected EntityManager em; + + @EJB + protected EpipulseConfigurationLookupService configLookupService; + + @EJB + protected EpipulseSqlCteBuilder sqlCteBuilder; + + /** + * Template method that orchestrates the entire export process. + * This method defines the algorithm structure and delegates disease-specific + * variations to abstract methods implemented by subclasses. + *

+ * Note: This method should not be overridden by subclasses (template method pattern). + * The 'final' modifier was removed to allow EJB proxy generation. + * + * @param exportDto + * the export request containing subject code, date range, etc. + * @param serverCountryLocale + * the server country ISO2 code (e.g., "LU") + * @param serverCountryName + * the server country name (e.g., "Luxembourg") + * @return the export result containing all case entries and max counts + * @throws SQLException + * if database query execution fails + * @throws IllegalStateException + * if configuration lookup fails + * @throws IllegalArgumentException + * if parameters are invalid + */ + public EpipulseDiseaseExportResult export(EpipulseExportDto exportDto, String serverCountryLocale, String serverCountryName) + throws SQLException, IllegalStateException, IllegalArgumentException { + + EpipulseDiseaseExportResult exportResult = new EpipulseDiseaseExportResult(); + + try { + // Step 1: Lookup configuration (common) + EpipulseConfigurationContext config = configLookupService.lookupConfiguration(exportDto, serverCountryLocale, serverCountryName); + + // Step 2: Build disease-specific query (template method hook) + String queryString = buildDiseaseExportQuery(); + + // Step 3: Execute query with parameters (common) + Query query = em.createNativeQuery(queryString); + setQueryParameters(query, exportDto, config, serverCountryLocale); + @SuppressWarnings("unchecked") + List resultList = query.getResultList(); + + // Step 4: Map results to DTOs (combination of common + disease-specific) + List exportEntryList = mapResultsToEntryDtos(resultList, exportDto, config.getServerCountryNutsCode()); + + // Step 5: Calculate max counts and build result + EpipulseCommonDtoMapper.calculateCommonMaxCounts(exportEntryList, exportResult); + calculateDiseaseSpecificMaxCounts(exportEntryList, exportResult); + + exportResult.setExportEntryList(exportEntryList); + + } catch (Exception e) { + logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage()); + throw e; + } + + return exportResult; + } + + /** + * Builds the complete SQL query for the disease export. + * Subclasses must implement this to construct the full query including: + * - Common CTEs (via sqlCteBuilder) + * - Disease-specific CTEs + * - Main SELECT clause + * + * @return the complete SQL query string + */ + protected abstract String buildDiseaseExportQuery(); + + /** + * Maps disease-specific fields from the database row to the DTO. + * This method is called after common fields have been mapped. + * + * @param dto + * the DTO to populate + * @param row + * the database result row + * @param startIndex + * the index where disease-specific fields start (typically 27) + */ + protected abstract void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Object[] row, int startIndex); + + /** + * Calculates disease-specific max counts for repeatable fields. + * Common max counts (pathogenTests, immunizations) are handled by EpipulseCommonDtoMapper. + * + * @param entries + * the list of export entries + * @param result + * the result object to populate with max counts + */ + protected abstract void calculateDiseaseSpecificMaxCounts(List entries, EpipulseDiseaseExportResult result); + + /** + * Sets common query parameters on the prepared query. + * Parameter binding is identical for all diseases. + * This method is private to prevent overriding by subclasses. + * + * @param query + * the query to set parameters on + * @param exportDto + * the export request DTO + * @param config + * the configuration context + */ + private void setQueryParameters(Query query, EpipulseExportDto exportDto, EpipulseConfigurationContext config, String serverCountryLocale) { + query.setParameter("disease", exportDto.getSubjectCode().getDisease().name()); + query.setParameter("subjectCode", config.getSubjectCode()); + query.setParameter("countryLocale", serverCountryLocale); // Use ISO2 country code (e.g., "LU"), not full subject code + query.setParameter("startDate", DateHelper.convertDateToDbFormat(exportDto.getStartDate())); + query.setParameter("endDate", DateHelper.convertDateToDbFormat(exportDto.getEndDate())); + query.setParameter("meansOfImmVaccination", MeansOfImmunization.VACCINATION.name()); + query.setParameter("meansOfImmVaccinationRecovery", MeansOfImmunization.VACCINATION_RECOVERY.name()); + } + + /** + * Maps database result rows to export entry DTOs. + * This method combines common field mapping with disease-specific field mapping. + * This method is private to prevent overriding by subclasses. + * + * @param resultList + * the list of database result rows + * @param exportDto + * the export request DTO + * @param serverCountryNutsCode + * the server country NUTS code + * @return the list of mapped export entry DTOs + */ + private List mapResultsToEntryDtos( + List resultList, + EpipulseExportDto exportDto, + String serverCountryNutsCode) { + + List exportEntryList = new ArrayList<>(); + List subjectCodePathogenTestTypes = EpipulsePathogenTestTypeRef.getPathogenTestTypesByDisease(exportDto.getSubjectCode()); + + for (Object[] row : resultList) { + EpipulseDiseaseExportEntryDto dto = new EpipulseDiseaseExportEntryDto(); + + // Map common fields (indices 0-27, returns last index used) + int nextIndex = EpipulseCommonDtoMapper.mapCommonFields(dto, row, serverCountryNutsCode, subjectCodePathogenTestTypes); + + // Map disease-specific fields starting at nextIndex + mapDiseaseSpecificFields(dto, row, nextIndex); + + // Calculate age (common to all diseases) + dto.calculateAge(); + + exportEntryList.add(dto); + } + + return exportEntryList; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/CsvExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/CsvExportStrategy.java new file mode 100644 index 00000000000..3404532a5fd --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/CsvExportStrategy.java @@ -0,0 +1,46 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.util.List; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; + +/** + * Strategy interface for disease-specific CSV export generation. + * Implementations define column names and row writing logic for each disease. + */ +public interface CsvExportStrategy { + + /** + * Builds the CSV column names list for this disease. + * + * @param exportResult the export result containing max counts for repeatable fields + * @return ordered list of column names + */ + List buildColumnNames(EpipulseDiseaseExportResult exportResult); + + /** + * Writes one export entry to the exportLine array. + * + * @param dto the entry to write + * @param exportLine the output array (pre-sized to column count) + * @param exportResult the export result containing max counts + * @return the final index written (for validation) + */ + int writeEntryRow(EpipulseDiseaseExportEntryDto dto, String[] exportLine, EpipulseDiseaseExportResult exportResult); +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesCsvExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesCsvExportStrategy.java new file mode 100644 index 00000000000..0e40e9674cc --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesCsvExportStrategy.java @@ -0,0 +1,212 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; + +/** + * CSV export strategy for Measles disease. + * Handles 41+ columns with 5 repeatable field types: + * - TypeOfSpecimenCollected (virus detection) + * - TypeOfSpecimenForSerologicalAnalysis (serology) + * - ClusterSetting + * - ComplicationDiagnosis + * - PlaceOfInfection + */ +@Stateless +@LocalBean +public class MeaslesCsvExportStrategy implements CsvExportStrategy { + + @Override + public List buildColumnNames(EpipulseDiseaseExportResult exportResult) { + List columnNames = new ArrayList<>( + List.of( + "Disease", + "ReportingCountry", + "Status", + "SubjectCode", + "NationalRecordId", + "DataSource", + "DateUsedForStatistics", + "Age", + "AgeMonth", + "Gender", + "CaseClassification", + "DateOfOnset", + "DateOfNotification", + "Hospitalisation", + "Outcome", + "PlaceOfNotification", + "PlaceOfResidence", + "DateOfSpecimen", + "DateOfLaboratoryResult")); + + // Repeatable field: TypeOfSpecimenCollected (virus detection) + if (exportResult.getMaxSpecimenVirDetect() > 0) { + for (int i = 1; i <= exportResult.getMaxSpecimenVirDetect(); i++) { + columnNames.add("TypeOfSpecimenCollected"); + } + } + + columnNames.addAll(List.of("ResultOfVirusDetection", "Genotype")); + + // Repeatable field: TypeOfSpecimenForSerologicalAnalysis + if (exportResult.getMaxSpecimenSero() > 0) { + for (int i = 1; i <= exportResult.getMaxSpecimenSero(); i++) { + columnNames.add("TypeOfSpecimenForSerologicalAnalysis"); + } + } + + columnNames.addAll(List.of("ResultIgG", "ResultIgM", "DateOfInvestigation", "ClusterRelated", "ClusterIdentification")); + + // Repeatable field: ClusterSetting + if (exportResult.getMaxClusterSettings() > 0) { + for (int i = 1; i <= exportResult.getMaxClusterSettings(); i++) { + columnNames.add("ClusterSetting"); + } + } + + columnNames.add("ImportedStatus"); + + // Repeatable field: ComplicationDiagnosis + if (exportResult.getMaxComplicationDiagnosis() > 0) { + for (int i = 1; i <= exportResult.getMaxComplicationDiagnosis(); i++) { + columnNames.add("ComplicationDiagnosis"); + } + } + + columnNames.add("ClinicalCriteriaStatus"); + + // Repeatable field: PlaceOfInfection + if (exportResult.getMaxPlaceOfInfection() > 0) { + for (int i = 1; i <= exportResult.getMaxPlaceOfInfection(); i++) { + columnNames.add("PlaceOfInfection"); + } + } + + columnNames.add("CauseOfDeath"); + + // Add vaccination columns + if (exportResult.getMaxImmunizations() > 0) { + columnNames.add("DateOfLastVaccination"); + } + + columnNames.add("VaccinationStatus"); + + return columnNames; + } + + @Override + public int writeEntryRow(EpipulseDiseaseExportEntryDto dto, String[] exportLine, EpipulseDiseaseExportResult exportResult) { + int index = -1; + + // Write fixed columns + exportLine[++index] = dto.getDiseaseForCsv(); + exportLine[++index] = dto.getReportingCountryForCsv(); + exportLine[++index] = dto.getStatusForCsv(); + exportLine[++index] = dto.getSubjectCodeForCsv(); + exportLine[++index] = dto.getNationalRecordIdForCsv(); + exportLine[++index] = dto.getDataSourceForCsv(); + exportLine[++index] = dto.getDateUsedForStatisticsCsv(); + exportLine[++index] = dto.getAgeForCsv(); + exportLine[++index] = dto.getAgeMonthForCsv(); + exportLine[++index] = dto.getGenderForCsv(); + exportLine[++index] = dto.getCaseClassificationForCsv(); + exportLine[++index] = dto.getDateOfOnsetForCsv(); + exportLine[++index] = dto.getDateOfNotificationForCsv(); + exportLine[++index] = dto.getHospitalizationForCsv(); + exportLine[++index] = dto.getOutcomeForCsv(); + exportLine[++index] = dto.getPlaceOfNotificationForCsv(); + exportLine[++index] = dto.getPlaceOfResidenceForCsv(); + + // Laboratory fields + exportLine[++index] = dto.getDateOfSpecimenForCsv(); + exportLine[++index] = dto.getDateOfLaboratoryResultForCsv(); + + // Repeatable: TypeOfSpecimenCollected (virus detection) + if (exportResult.getMaxSpecimenVirDetect() > 0) { + List specimenCollected = dto.getTypeOfSpecimenCollectedForCsv(exportResult.getMaxSpecimenVirDetect()); + for (String specimen : specimenCollected) { + exportLine[++index] = specimen; + } + } + + exportLine[++index] = dto.getResultOfVirusDetectionForCsv(); + exportLine[++index] = dto.getGenotypeForCsv(); + + // Repeatable: TypeOfSpecimenForSerologicalAnalysis + if (exportResult.getMaxSpecimenSero() > 0) { + List specimenSerology = dto.getTypeOfSpecimenSerologyForCsv(exportResult.getMaxSpecimenSero()); + for (String specimen : specimenSerology) { + exportLine[++index] = specimen; + } + } + + exportLine[++index] = dto.getResultIgGForCsv(); + exportLine[++index] = dto.getResultIgMForCsv(); + + // Clinical and epidemiology fields + exportLine[++index] = dto.getDateOfInvestigationForCsv(); + exportLine[++index] = dto.getClusterRelatedForCsv(); + exportLine[++index] = dto.getClusterIdentificationForCsv(); + + // Repeatable: ClusterSetting + if (exportResult.getMaxClusterSettings() > 0) { + List clusterSettings = dto.getClusterSettingForCsv(exportResult.getMaxClusterSettings()); + for (String setting : clusterSettings) { + exportLine[++index] = setting; + } + } + + exportLine[++index] = dto.getImportedStatusForCsv(); + + // Repeatable: ComplicationDiagnosis + if (exportResult.getMaxComplicationDiagnosis() > 0) { + List complications = dto.getComplicationDiagnosisForCsv(exportResult.getMaxComplicationDiagnosis()); + for (String complication : complications) { + exportLine[++index] = complication; + } + } + + exportLine[++index] = dto.getClinicalCriteriaStatusForCsv(); + + // Repeatable: PlaceOfInfection + if (exportResult.getMaxPlaceOfInfection() > 0) { + List placesOfInfection = dto.getPlaceOfInfectionForCsv(exportResult.getMaxPlaceOfInfection()); + for (String place : placesOfInfection) { + exportLine[++index] = place; + } + } + + exportLine[++index] = dto.getCauseOfDeathForCsv(); + + // Vaccination columns + if (exportResult.getMaxImmunizations() > 0) { + exportLine[++index] = dto.getDateOfLastVaccinationForCsv(); + } + + exportLine[++index] = dto.getVaccinationStatusForCsv(); + + return index; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java new file mode 100644 index 00000000000..12b9bc727ee --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java @@ -0,0 +1,394 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +import org.apache.commons.lang3.StringUtils; + +import de.symeda.sormas.api.epidata.CaseImportedStatus; +import de.symeda.sormas.api.epidata.ClusterType; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; +import de.symeda.sormas.api.epipulse.EpipulseLaboratoryMapper; +import de.symeda.sormas.api.sample.PathogenTestResultType; +import de.symeda.sormas.api.sample.SampleMaterial; +import de.symeda.sormas.api.symptoms.SymptomState; +import de.symeda.sormas.api.utils.YesNoUnknown; + +/** + * Export strategy for Measles (MEAS) disease exports. + * Measles extends the common fields with laboratory data and clinical/epidemiology data. + * This includes 7 additional CTEs, 20 additional DTO fields, and 5 additional max count trackers. + */ +@Stateless +@LocalBean +public class MeaslesExportStrategy extends AbstractEpipulseDiseaseExportStrategy { + + @Override + protected String buildDiseaseExportQuery() { + StringBuilder query = new StringBuilder(); + + // Common CTEs + query.append(sqlCteBuilder.buildVariablesCte()); + query.append(sqlCteBuilder.buildConfigDataCte()); + query.append(sqlCteBuilder.buildFilteredCasesCte(true)); // Include Measles-specific fields (epidata_id, investigateddate, clinicalconfirmation) + query.append(sqlCteBuilder.buildPreviousHospitalizationsCte()); + query.append(sqlCteBuilder.buildSamplesCte()); + query.append(sqlCteBuilder.buildPathogenTestsCte()); + query.append(sqlCteBuilder.buildImmunizationsCte()); + query.append(sqlCteBuilder.buildVaccinationsCte()); + + // Measles-specific CTEs + query.append(buildSampleDataCte()); + query.append(buildVirusDetectionDataCte()); + query.append(buildIggSerologyDataCte()); + query.append(buildIgmSerologyDataCte()); + query.append(buildEpidataClusterCte()); + query.append(buildExposureLocationsCte()); + query.append(buildComplicationsDataCte()); + + // Main SELECT clause with Measles-specific fields + query.append(buildMeaslesSelectClause()); + + return query.toString(); + } + + @Override + protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Object[] row, int startIndex) { + int index = startIndex; + + // Laboratory data (indices 28-35) + dto.setDateOfSpecimen((Date) row[++index]); + dto.setDateOfLaboratoryResult((Date) row[++index]); + + String specimenTypesVirusRaw = (String) row[++index]; + if (!StringUtils.isBlank(specimenTypesVirusRaw)) { + List specimenTypesVirus = new ArrayList<>(); + for (String specimenType : specimenTypesVirusRaw.split(",")) { + specimenTypesVirus.add(EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); + } + dto.setTypeOfSpecimenCollected(specimenTypesVirus); + } + + String virusDetectionResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(virusDetectionResultRaw)) { + dto.setResultOfVirusDetection( + EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(virusDetectionResultRaw))); + } + + String genotypeRaw = (String) row[++index]; + if (!StringUtils.isBlank(genotypeRaw)) { + dto.setGenotype(EpipulseLaboratoryMapper.normalizeGenotypeForEpipulse(genotypeRaw)); + } + + String specimenTypesSerologyRaw = (String) row[++index]; + if (!StringUtils.isBlank(specimenTypesSerologyRaw)) { + List specimenTypesSerology = new ArrayList<>(); + for (String specimenType : specimenTypesSerologyRaw.split(",")) { + specimenTypesSerology.add(EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); + } + dto.setTypeOfSpecimenSerology(specimenTypesSerology); + } + + String iggResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(iggResultRaw)) { + dto.setResultIgG(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(iggResultRaw))); + } + + String igmResultRaw = (String) row[++index]; + if (!StringUtils.isBlank(igmResultRaw)) { + dto.setResultIgM(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(igmResultRaw))); + } + + // Clinical and epidemiology data (indices 36-47) + dto.setDateOfInvestigation((Date) row[++index]); + + Boolean clusterRelated = (Boolean) row[++index]; + dto.setClusterRelated(clusterRelated); + + dto.setClusterIdentification((String) row[++index]); + + String clusterTypeRaw = (String) row[++index]; + if (!StringUtils.isBlank(clusterTypeRaw)) { + List clusterSettings = new ArrayList<>(); + clusterSettings.add(EpipulseLaboratoryMapper.mapClusterTypeToEpipulseCode(ClusterType.valueOf(clusterTypeRaw))); + dto.setClusterSetting(clusterSettings); + } + + String caseImportedStatusRaw = (String) row[++index]; + if (!StringUtils.isBlank(caseImportedStatusRaw)) { + dto.setImportedStatus(EpipulseLaboratoryMapper.mapCaseImportedStatusToEpipulseCode(CaseImportedStatus.valueOf(caseImportedStatusRaw))); + } + + // Complications mapping (4 fields) + String acuteEncephalitisRaw = (String) row[++index]; + String diarrheaRaw = (String) row[++index]; + String otitisMediaRaw = (String) row[++index]; + String otherComplicationsRaw = (String) row[++index]; + + SymptomState acuteEncephalitis = parseSymptomState(acuteEncephalitisRaw); + SymptomState diarrhea = parseSymptomState(diarrheaRaw); + SymptomState otitisMedia = parseSymptomState(otitisMediaRaw); + SymptomState otherComplications = parseSymptomState(otherComplicationsRaw); + + dto.setComplicationDiagnosis( + EpipulseLaboratoryMapper.mapSymptomsToComplicationCodes(acuteEncephalitis, diarrhea, otitisMedia, otherComplications)); + + // Clinical criteria status + String clinicalConfirmationRaw = (String) row[++index]; + if (!StringUtils.isBlank(clinicalConfirmationRaw)) { + dto.setClinicalCriteriaStatus(EpipulseLaboratoryMapper.deriveClinicalCriteriaStatus(YesNoUnknown.valueOf(clinicalConfirmationRaw))); + } + + // Place of infection (exposure locations) + String placeOfInfectionRaw = (String) row[++index]; + if (!StringUtils.isBlank(placeOfInfectionRaw)) { + List placesOfInfection = new ArrayList<>(); + for (String place : placeOfInfectionRaw.split(";")) { + if (!place.trim().isEmpty()) { + placesOfInfection.add(place.trim()); + } + } + dto.setPlaceOfInfection(placesOfInfection); + } + + // Cause of death + dto.setCauseOfDeath((String) row[++index]); + } + + @Override + protected void calculateDiseaseSpecificMaxCounts(List entries, EpipulseDiseaseExportResult result) { + int maxComplicationDiagnosis = 0; + int maxClusterSettings = 0; + int maxPlaceOfInfection = 0; + int maxSpecimenVirDetect = 0; + int maxSpecimenSero = 0; + + for (EpipulseDiseaseExportEntryDto entry : entries) { + if (entry.getComplicationDiagnosis() != null && entry.getComplicationDiagnosis().size() > maxComplicationDiagnosis) { + maxComplicationDiagnosis = entry.getComplicationDiagnosis().size(); + } + if (entry.getClusterSetting() != null && entry.getClusterSetting().size() > maxClusterSettings) { + maxClusterSettings = entry.getClusterSetting().size(); + } + if (entry.getPlaceOfInfection() != null && entry.getPlaceOfInfection().size() > maxPlaceOfInfection) { + maxPlaceOfInfection = entry.getPlaceOfInfection().size(); + } + if (entry.getTypeOfSpecimenCollected() != null && entry.getTypeOfSpecimenCollected().size() > maxSpecimenVirDetect) { + maxSpecimenVirDetect = entry.getTypeOfSpecimenCollected().size(); + } + if (entry.getTypeOfSpecimenSerology() != null && entry.getTypeOfSpecimenSerology().size() > maxSpecimenSero) { + maxSpecimenSero = entry.getTypeOfSpecimenSerology().size(); + } + } + + result.setMaxComplicationDiagnosis(maxComplicationDiagnosis); + result.setMaxClusterSettings(maxClusterSettings); + result.setMaxPlaceOfInfection(maxPlaceOfInfection); + result.setMaxSpecimenVirDetect(maxSpecimenVirDetect); + result.setMaxSpecimenSero(maxSpecimenSero); + } + + // Helper methods for Measles-specific CTEs + + private String buildSampleDataCte() { + //@formatter:off + return ", sample_data AS (SELECT c.id as case_id," + + " MIN(s.sampledatetime) as first_specimen_date," + + " STRING_AGG(DISTINCT CAST(s2.samplematerial AS text), ',' ORDER BY CAST(s2.samplematerial AS text)) as specimen_types_virus," + + " STRING_AGG(DISTINCT CAST(s3.samplematerial AS text), ',' ORDER BY CAST(s3.samplematerial AS text)) as specimen_types_serology " + + " FROM filtered_cases c " + + " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + + " LEFT JOIN samples s2 ON s2.associatedcase_id = c.id AND s2.deleted = false AND s2.samplematerial IS NOT NULL " + + " LEFT JOIN (SELECT DISTINCT s_sero.id, s_sero.associatedcase_id, s_sero.samplematerial " + + " FROM samples s_sero " + + " JOIN pathogentest pt_sero ON pt_sero.sample_id = s_sero.id " + + " WHERE s_sero.deleted = false " + + " AND pt_sero.testtype IN ('IGG_SERUM_ANTIBODY', 'IGM_SERUM_ANTIBODY', 'SEROLOGY')) s3 " + + " ON s3.associatedcase_id = c.id " + + " GROUP BY c.id), "; + //@formatter:on + } + + private String buildVirusDetectionDataCte() { + //@formatter:off + return "virus_detection_data AS (SELECT c.id as case_id," + + " MIN(pt.testdatetime) as lab_result_date," + + " (SELECT pt2.testresult " + + " FROM samples s2 " + + " JOIN pathogentest pt2 ON pt2.sample_id = s2.id " + + " WHERE s2.associatedcase_id = c.id " + + " AND s2.deleted = false " + + " AND pt2.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + + " AND pt2.testresultverified = true " + + " ORDER BY pt2.testdatetime ASC " + + " LIMIT 1) as virus_detection_result," + + " (SELECT COALESCE(pt3.typingid, pt3.genotyperesult) " + + " FROM samples s3 " + + " JOIN pathogentest pt3 ON pt3.sample_id = s3.id " + + " WHERE s3.associatedcase_id = c.id " + + " AND s3.deleted = false " + + " AND (pt3.typingid IS NOT NULL OR pt3.genotyperesult IS NOT NULL) " + + " ORDER BY pt3.testdatetime ASC " + + " LIMIT 1) as genotype_raw " + + " FROM filtered_cases c " + + " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + + " LEFT JOIN pathogentest pt ON pt.sample_id = s.id " + + " AND pt.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY') " + + " AND pt.testresultverified = true " + + " GROUP BY c.id), "; + //@formatter:on + } + + private String buildIggSerologyDataCte() { + //@formatter:off + return "igg_serology_data AS (SELECT c.id as case_id," + + " (SELECT CASE " + + " WHEN pt_igg.fourfoldincreaseantibodytiter = 'YES' THEN 'POSITIVE' " + + " ELSE pt_igg.testresult " + + " END " + + " FROM samples s_igg " + + " JOIN pathogentest pt_igg ON pt_igg.sample_id = s_igg.id " + + " WHERE s_igg.associatedcase_id = c.id " + + " AND s_igg.deleted = false " + + " AND pt_igg.testtype = 'IGG_SERUM_ANTIBODY' " + + " ORDER BY pt_igg.testdatetime ASC " + + " LIMIT 1) as igg_result " + + " FROM filtered_cases c), "; + //@formatter:on + } + + private String buildIgmSerologyDataCte() { + //@formatter:off + return "igm_serology_data AS (SELECT c.id as case_id," + + " (SELECT pt_igm.testresult " + + " FROM samples s_igm " + + " JOIN pathogentest pt_igm ON pt_igm.sample_id = s_igm.id " + + " WHERE s_igm.associatedcase_id = c.id " + + " AND s_igm.deleted = false " + + " AND pt_igm.testtype = 'IGM_SERUM_ANTIBODY' " + + " ORDER BY pt_igm.testdatetime ASC " + + " LIMIT 1) as igm_result " + + " FROM filtered_cases c), "; + //@formatter:on + } + + private String buildEpidataClusterCte() { + //@formatter:off + return "epidata_cluster AS (SELECT c.id as case_id," + + " epi.clusterrelated," + + " epi.clustertypetext," + + " epi.clustertype," + + " epi.caseimportedstatus " + + " FROM filtered_cases c " + + " LEFT JOIN epidata epi ON c.epidata_id = epi.id), "; + //@formatter:on + } + + private String buildExposureLocationsCte() { + //@formatter:off + return "exposure_locations AS (SELECT e.epidata_id," + + " STRING_AGG(" + + " CASE " + + " WHEN co.defaultname IS NOT NULL THEN co.defaultname " + + " WHEN l.city IS NOT NULL THEN l.city " + + " ELSE l.details " + + " END, " + + " '; ' " + + " ORDER BY e.startdate DESC" + + " ) as infection_locations " + + " FROM exposures e " + + " JOIN location l ON e.location_id = l.id " + + " LEFT JOIN country co ON l.country_id = co.id " + + " WHERE e.epidata_id IN (SELECT epidata_id FROM filtered_cases WHERE epidata_id IS NOT NULL) " + + " GROUP BY e.epidata_id), "; + //@formatter:on + } + + private String buildComplicationsDataCte() { + //@formatter:off + return "complications_data AS (SELECT c.id as case_id," + + " s.acuteencephalitis," + + " s.diarrhea," + + " s.otitismedia," + + " s.othercomplications " + + " FROM filtered_cases c " + + " LEFT JOIN symptoms s ON c.symptoms_id = s.id) "; + //@formatter:on + } + + private String buildMeaslesSelectClause() { + StringBuilder select = new StringBuilder(); + //@formatter:off + // Use common SELECT fields and append Measles-specific fields + select.append("SELECT ") + .append(sqlCteBuilder.buildCommonSelectFields()) + .append(",") + .append(" sd.first_specimen_date,") + .append(" vd.lab_result_date,") + .append(" sd.specimen_types_virus,") + .append(" vd.virus_detection_result,") + .append(" vd.genotype_raw,") + .append(" sd.specimen_types_serology,") + .append(" igg.igg_result,") + .append(" igm.igm_result,") + .append(" cast(c.investigateddate as date) as investigated_date,") + .append(" ec.clusterrelated,") + .append(" ec.clustertypetext,") + .append(" ec.clustertype,") + .append(" ec.caseimportedstatus,") + .append(" comp.acuteencephalitis,") + .append(" comp.diarrhea,") + .append(" comp.otitismedia,") + .append(" comp.othercomplications,") + .append(" c.clinicalconfirmation,") + .append(" el.infection_locations,") + .append(" person.causeofdeathdetails "); + + // Use common FROM and JOINs, then append Measles-specific joins + select.append(sqlCteBuilder.buildCommonFromAndJoins()) + .append(" LEFT JOIN sample_data sd ON sd.case_id = c.id") + .append(" LEFT JOIN virus_detection_data vd ON vd.case_id = c.id") + .append(" LEFT JOIN igg_serology_data igg ON igg.case_id = c.id") + .append(" LEFT JOIN igm_serology_data igm ON igm.case_id = c.id") + .append(" LEFT JOIN epidata_cluster ec ON ec.case_id = c.id") + .append(" LEFT JOIN exposure_locations el ON el.epidata_id = c.epidata_id") + .append(" LEFT JOIN complications_data comp ON comp.case_id = c.id "); + + select.append("ORDER BY c.reportdate"); + //@formatter:on + + return select.toString(); + } + + private SymptomState parseSymptomState(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return SymptomState.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid SymptomState value '{}', treating as null", value); + return null; + } + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisCsvExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisCsvExportStrategy.java new file mode 100644 index 00000000000..ec2240fc62f --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisCsvExportStrategy.java @@ -0,0 +1,114 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.util.ArrayList; +import java.util.List; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; + +/** + * CSV export strategy for Pertussis disease. + * Handles 34 columns maximum (17 fixed + dynamic pathogen tests + vaccination). + */ +@Stateless +@LocalBean +public class PertussisCsvExportStrategy implements CsvExportStrategy { + + @Override + public List buildColumnNames(EpipulseDiseaseExportResult exportResult) { + List columnNames = new ArrayList<>( + List.of( + "Disease", + "ReportingCountry", + "Status", + "SubjectCode", + "NationalRecordId", + "DataSource", + "DateUsedForStatistics", + "Age", + "AgeMonth", + "Gender", + "PlaceOfResidence", + "PlaceOfNotification", + "CaseClassification", + "DateOfOnset", + "DateOfNotification", + "Hospitalisation", + "Outcome")); + + // Add repeatable PathogenDetectionMethod columns + if (exportResult.getMaxPathogenTests() > 0) { + for (int i = 0; i < exportResult.getMaxPathogenTests(); i++) { + columnNames.add("PathogenDetectionMethod"); + } + } + + // Add vaccination columns + if (exportResult.getMaxImmunizations() > 0) { + columnNames.add("DateOfLastVaccination"); + } + + columnNames.add("VaccinationStatus"); + + return columnNames; + } + + @Override + public int writeEntryRow(EpipulseDiseaseExportEntryDto dto, String[] exportLine, EpipulseDiseaseExportResult exportResult) { + int index = -1; + + // Write fixed columns + exportLine[++index] = dto.getDiseaseForCsv(); + exportLine[++index] = dto.getReportingCountryForCsv(); + exportLine[++index] = dto.getStatusForCsv(); + exportLine[++index] = dto.getSubjectCodeForCsv(); + exportLine[++index] = dto.getNationalRecordIdForCsv(); + exportLine[++index] = dto.getDataSourceForCsv(); + exportLine[++index] = dto.getDateUsedForStatisticsCsv(); + exportLine[++index] = dto.getAgeForCsv(); + exportLine[++index] = dto.getAgeMonthForCsv(); + exportLine[++index] = dto.getGenderForCsv(); + exportLine[++index] = dto.getPlaceOfResidenceForCsv(); + exportLine[++index] = dto.getPlaceOfNotificationForCsv(); + exportLine[++index] = dto.getCaseClassificationForCsv(); + exportLine[++index] = dto.getDateOfOnsetForCsv(); + exportLine[++index] = dto.getDateOfNotificationForCsv(); + exportLine[++index] = dto.getHospitalizationForCsv(); + exportLine[++index] = dto.getOutcomeForCsv(); + + // Write repeatable pathogen detection methods + if (exportResult.getMaxPathogenTests() > 0) { + List pathogenDetectionMethods = dto.getPathogenDetectionMethodsForCsv(exportResult.getMaxPathogenTests()); + for (String pathogenDetectionMethod : pathogenDetectionMethods) { + exportLine[++index] = pathogenDetectionMethod; + } + } + + // Write vaccination columns + if (exportResult.getMaxImmunizations() > 0) { + exportLine[++index] = dto.getDateOfLastVaccinationForCsv(); + } + + exportLine[++index] = dto.getVaccinationStatusForCsv(); + + return index; + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisExportStrategy.java new file mode 100644 index 00000000000..6683cd102fa --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/PertussisExportStrategy.java @@ -0,0 +1,65 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.strategy; + +import java.util.List; + +import javax.ejb.LocalBean; +import javax.ejb.Stateless; + +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportEntryDto; +import de.symeda.sormas.api.epipulse.EpipulseDiseaseExportResult; + +/** + * Export strategy for Pertussis (PERT) disease exports. + * Pertussis uses only the common fields and CTEs - no disease-specific extensions. + */ +@Stateless +@LocalBean +public class PertussisExportStrategy extends AbstractEpipulseDiseaseExportStrategy { + + @Override + protected String buildDiseaseExportQuery() { + StringBuilder query = new StringBuilder(); + + // Build query using common CTEs only + query.append(sqlCteBuilder.buildVariablesCte()); + query.append(sqlCteBuilder.buildConfigDataCte()); + query.append(sqlCteBuilder.buildFilteredCasesCte(false)); // No Measles-specific fields + query.append(sqlCteBuilder.buildPreviousHospitalizationsCte()); + query.append(sqlCteBuilder.buildSamplesCte()); + query.append(sqlCteBuilder.buildPathogenTestsCte()); + query.append(sqlCteBuilder.buildImmunizationsCte()); + query.append(sqlCteBuilder.buildVaccinationsCte()); + + // Main SELECT clause with common fields only + query.append(sqlCteBuilder.buildMainSelectClause()); + + return query.toString(); + } + + @Override + protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Object[] row, int startIndex) { + // Pertussis has no disease-specific fields beyond the common 0-27 + // This method is intentionally empty + } + + @Override + protected void calculateDiseaseSpecificMaxCounts(List entries, EpipulseDiseaseExportResult result) { + // Pertussis has no disease-specific max counts + // Only maxPathogenTests and maxImmunizations are tracked (handled by common mapper) + } +} diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/util/EpipulseConfigurationContext.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/util/EpipulseConfigurationContext.java new file mode 100644 index 00000000000..c5031b96cb3 --- /dev/null +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/util/EpipulseConfigurationContext.java @@ -0,0 +1,60 @@ +/* + * SORMAS® - Surveillance Outbreak Response Management & Analysis System + * Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.symeda.sormas.backend.epipulse.util; + +import java.io.Serializable; + +/** + * Context object that holds configuration values looked up during Epipulse disease export. + * This DTO encapsulates the three configuration lookups that are common to all disease exports: + * reporting country, server country NUTS code, and subject code. + */ +public class EpipulseConfigurationContext implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String reportingCountry; + private final String serverCountryNutsCode; + private final String subjectCode; + + /** + * Creates a new configuration context. + * + * @param reportingCountry + * the Epipulse reporting country code (e.g., "LU") + * @param serverCountryNutsCode + * the NUTS code for the server country (e.g., "LU") + * @param subjectCode + * the Epipulse subject code (e.g., "PERT", "MEAS") + */ + public EpipulseConfigurationContext(String reportingCountry, String serverCountryNutsCode, String subjectCode) { + this.reportingCountry = reportingCountry; + this.serverCountryNutsCode = serverCountryNutsCode; + this.subjectCode = subjectCode; + } + + public String getReportingCountry() { + return reportingCountry; + } + + public String getServerCountryNutsCode() { + return serverCountryNutsCode; + } + + public String getSubjectCode() { + return subjectCode; + } +} From c89a203f3b5e5ab576c75b771f88a705f6be8437 Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Fri, 9 Jan 2026 13:47:33 +0300 Subject: [PATCH 3/6] #13771 - Add Epipulse export functionality for Measles disease --- .../epipulse/EpipulseDiseaseExportFacade.java | 2 + .../epipulse/EpipulseLaboratoryMapper.java | 42 ++++-- .../epipulse/EpipulseCommonDtoMapper.java | 16 ++- .../EpipulseConfigurationLookupService.java | 4 + .../EpipulseCsvExportOrchestrator.java | 2 +- ...AbstractEpipulseDiseaseExportStrategy.java | 2 +- .../strategy/MeaslesExportStrategy.java | 125 ++++++++++++++---- 7 files changed, 149 insertions(+), 44 deletions(-) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportFacade.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportFacade.java index 1f1e9bfacf2..6ea289db4ce 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportFacade.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportFacade.java @@ -21,4 +21,6 @@ public interface EpipulseDiseaseExportFacade { public void startPertussisExport(String uuid); + + public void startMeaslesExport(String uuid); } diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java index c6acc1b3799..b7c43176979 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java @@ -129,26 +129,42 @@ public static String normalizeGenotypeForEpipulse(String genotypeText) { } // Try to parse formats like "A", "B1", "D10", etc. and add MEASV_ prefix + // This matches a single uppercase letter optionally followed by digits if (normalized.matches("^[A-Z]\\d*$")) { return "MEASV_" + normalized; } - // Try to parse formats like "Genotype A", "MeV-A", etc. - if (normalized.contains("A")) { - return "MEASV_A"; + // Try to extract genotype from common formats with delimiters + // Matches patterns like "MeV-A", "Genotype-B1", "MV/A", "MEASLES-D4" + String extracted = extractGenotypeFromDelimitedFormat(normalized); + if (extracted != null) { + return "MEASV_" + extracted; } - if (normalized.matches(".*B[12]?.*")) { - if (normalized.contains("B1")) { - return "MEASV_B1"; - } else if (normalized.contains("B2")) { - return "MEASV_B2"; - } else if (normalized.contains("B3")) { - return "MEASV_B3"; - } - return "MEASV_B1"; // Default to B1 + + // Return null for ambiguous or unparseable inputs + return null; + } + + /** + * Extracts genotype code from delimited formats like "MeV-A", "Genotype B1", etc. + * Uses strict pattern matching to avoid false positives. + * + * @param normalized + * Uppercase normalized genotype string + * @return Extracted genotype code (e.g., "A", "B1", "D4"), or null if not extractable + */ + private static String extractGenotypeFromDelimitedFormat(String normalized) { + // Match patterns like "PREFIX-A", "PREFIX/B1", "PREFIX_D10", "PREFIX A" + // where PREFIX is some non-numeric text + // This captures the genotype part after common delimiters + String pattern = "(?:MEV|MEASLES?|GENOTYPE|MV)[-_/\\s]+([A-Z]\\d*)"; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher m = p.matcher(normalized); + + if (m.find()) { + return m.group(1); } - // If we can't parse it, return null (field will be empty in CSV) return null; } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java index c8c78ffd190..6323a89881e 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCommonDtoMapper.java @@ -173,14 +173,18 @@ public static void calculateCommonMaxCounts(List int maxImmunizations = 0; for (EpipulseDiseaseExportEntryDto entry : entries) { - int pathogenTestCount = entry.getPathogenTests().size(); - if (pathogenTestCount > maxPathogenTests) { - maxPathogenTests = pathogenTestCount; + if (entry.getPathogenTests() != null) { + int pathogenTestCount = entry.getPathogenTests().size(); + if (pathogenTestCount > maxPathogenTests) { + maxPathogenTests = pathogenTestCount; + } } - int immunizationCount = entry.getImmunizations().size(); - if (immunizationCount > maxImmunizations) { - maxImmunizations = immunizationCount; + if (entry.getImmunizations() != null) { + int immunizationCount = entry.getImmunizations().size(); + if (immunizationCount > maxImmunizations) { + maxImmunizations = immunizationCount; + } } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java index df0df8936cd..40533134a48 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseConfigurationLookupService.java @@ -108,6 +108,10 @@ private String lookupReportingCountry(String countryIso2Code) throws IllegalArgu * @return the NUTS code for the country, or null if not found */ private String lookupServerCountryNutsCode(String countryName) { + if (countryName == null) { + return null; + } + //@formatter:off String serverCountryQuery = "select nutscode " + diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java index 42b03b96687..33ff177f9b0 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java @@ -130,8 +130,8 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp writer.writeNext(columnNames.toArray(new String[columnNames.size()])); // Write entries using strategy - String[] exportLine = new String[columnNames.size()]; for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { + String[] exportLine = new String[columnNames.size()]; csvStrategy.writeEntryRow(dto, exportLine, exportResult); writer.writeNext(exportLine); } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java index 02a46f85046..1fc316cb6a2 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/AbstractEpipulseDiseaseExportStrategy.java @@ -115,7 +115,7 @@ public EpipulseDiseaseExportResult export(EpipulseExportDto exportDto, String se exportResult.setExportEntryList(exportEntryList); } catch (Exception e) { - logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage()); + logger.error("Error while exporting case based " + exportDto.getSubjectCode() + ":" + e.getMessage(), e); throw e; } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java index 12b9bc727ee..7bd7f812072 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java @@ -81,18 +81,14 @@ protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Objec dto.setDateOfLaboratoryResult((Date) row[++index]); String specimenTypesVirusRaw = (String) row[++index]; - if (!StringUtils.isBlank(specimenTypesVirusRaw)) { - List specimenTypesVirus = new ArrayList<>(); - for (String specimenType : specimenTypesVirusRaw.split(",")) { - specimenTypesVirus.add(EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); - } - dto.setTypeOfSpecimenCollected(specimenTypesVirus); - } + dto.setTypeOfSpecimenCollected(parseSpecimenTypes(specimenTypesVirusRaw)); String virusDetectionResultRaw = (String) row[++index]; if (!StringUtils.isBlank(virusDetectionResultRaw)) { - dto.setResultOfVirusDetection( - EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(virusDetectionResultRaw))); + PathogenTestResultType virusDetectionResult = parsePathogenTestResultType(virusDetectionResultRaw); + if (virusDetectionResult != null) { + dto.setResultOfVirusDetection(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(virusDetectionResult)); + } } String genotypeRaw = (String) row[++index]; @@ -101,22 +97,22 @@ protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Objec } String specimenTypesSerologyRaw = (String) row[++index]; - if (!StringUtils.isBlank(specimenTypesSerologyRaw)) { - List specimenTypesSerology = new ArrayList<>(); - for (String specimenType : specimenTypesSerologyRaw.split(",")) { - specimenTypesSerology.add(EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(SampleMaterial.valueOf(specimenType.trim()))); - } - dto.setTypeOfSpecimenSerology(specimenTypesSerology); - } + dto.setTypeOfSpecimenSerology(parseSpecimenTypes(specimenTypesSerologyRaw)); String iggResultRaw = (String) row[++index]; if (!StringUtils.isBlank(iggResultRaw)) { - dto.setResultIgG(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(iggResultRaw))); + PathogenTestResultType iggResult = parsePathogenTestResultType(iggResultRaw); + if (iggResult != null) { + dto.setResultIgG(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(iggResult)); + } } String igmResultRaw = (String) row[++index]; if (!StringUtils.isBlank(igmResultRaw)) { - dto.setResultIgM(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(PathogenTestResultType.valueOf(igmResultRaw))); + PathogenTestResultType igmResult = parsePathogenTestResultType(igmResultRaw); + if (igmResult != null) { + dto.setResultIgM(EpipulseLaboratoryMapper.mapTestResultToEpipulseCode(igmResult)); + } } // Clinical and epidemiology data (indices 36-47) @@ -129,14 +125,20 @@ protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Objec String clusterTypeRaw = (String) row[++index]; if (!StringUtils.isBlank(clusterTypeRaw)) { - List clusterSettings = new ArrayList<>(); - clusterSettings.add(EpipulseLaboratoryMapper.mapClusterTypeToEpipulseCode(ClusterType.valueOf(clusterTypeRaw))); - dto.setClusterSetting(clusterSettings); + ClusterType clusterType = parseClusterType(clusterTypeRaw); + if (clusterType != null) { + List clusterSettings = new ArrayList<>(); + clusterSettings.add(EpipulseLaboratoryMapper.mapClusterTypeToEpipulseCode(clusterType)); + dto.setClusterSetting(clusterSettings); + } } String caseImportedStatusRaw = (String) row[++index]; if (!StringUtils.isBlank(caseImportedStatusRaw)) { - dto.setImportedStatus(EpipulseLaboratoryMapper.mapCaseImportedStatusToEpipulseCode(CaseImportedStatus.valueOf(caseImportedStatusRaw))); + CaseImportedStatus caseImportedStatus = parseCaseImportedStatus(caseImportedStatusRaw); + if (caseImportedStatus != null) { + dto.setImportedStatus(EpipulseLaboratoryMapper.mapCaseImportedStatusToEpipulseCode(caseImportedStatus)); + } } // Complications mapping (4 fields) @@ -156,7 +158,10 @@ protected void mapDiseaseSpecificFields(EpipulseDiseaseExportEntryDto dto, Objec // Clinical criteria status String clinicalConfirmationRaw = (String) row[++index]; if (!StringUtils.isBlank(clinicalConfirmationRaw)) { - dto.setClinicalCriteriaStatus(EpipulseLaboratoryMapper.deriveClinicalCriteriaStatus(YesNoUnknown.valueOf(clinicalConfirmationRaw))); + YesNoUnknown clinicalConfirmation = parseYesNoUnknown(clinicalConfirmationRaw); + if (clinicalConfirmation != null) { + dto.setClinicalCriteriaStatus(EpipulseLaboratoryMapper.deriveClinicalCriteriaStatus(clinicalConfirmation)); + } } // Place of infection (exposure locations) @@ -380,6 +385,20 @@ private String buildMeaslesSelectClause() { return select.toString(); } + private List parseSpecimenTypes(String specimenTypesRaw) { + if (StringUtils.isBlank(specimenTypesRaw)) { + return null; + } + List specimenTypes = new ArrayList<>(); + for (String specimenType : specimenTypesRaw.split(",")) { + SampleMaterial material = parseSampleMaterial(specimenType.trim()); + if (material != null) { + specimenTypes.add(EpipulseLaboratoryMapper.mapSampleMaterialToEpipulseCode(material)); + } + } + return specimenTypes; + } + private SymptomState parseSymptomState(String value) { if (StringUtils.isBlank(value)) { return null; @@ -391,4 +410,64 @@ private SymptomState parseSymptomState(String value) { return null; } } + + private SampleMaterial parseSampleMaterial(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return SampleMaterial.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid SampleMaterial value '{}', treating as null", value); + return null; + } + } + + private PathogenTestResultType parsePathogenTestResultType(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return PathogenTestResultType.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid PathogenTestResultType value '{}', treating as null", value); + return null; + } + } + + private ClusterType parseClusterType(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return ClusterType.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid ClusterType value '{}', treating as null", value); + return null; + } + } + + private CaseImportedStatus parseCaseImportedStatus(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return CaseImportedStatus.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid CaseImportedStatus value '{}', treating as null", value); + return null; + } + } + + private YesNoUnknown parseYesNoUnknown(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return YesNoUnknown.valueOf(value); + } catch (IllegalArgumentException e) { + logger.warn("Invalid YesNoUnknown value '{}', treating as null", value); + return null; + } + } } From ef17b1eda59e2ae33ab9d83254748a717ef8bf80 Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Fri, 9 Jan 2026 14:27:49 +0300 Subject: [PATCH 4/6] #13771 - Add Epipulse export functionality for Measles disease --- .../epipulse/EpipulseLaboratoryMapper.java | 9 +++-- .../EpipulseCsvExportOrchestrator.java | 9 +++-- .../EpipulseDiseaseExportService.java | 33 +++++++++++++++++++ .../strategy/MeaslesExportStrategy.java | 13 ++++++-- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java index b7c43176979..ad49817b7d2 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java @@ -69,7 +69,7 @@ public static String mapSampleMaterialToEpipulseCode(SampleMaterial sampleMateri case EDTA_WHOLE_BLOOD: return "EDTA"; // EDTA whole blood default: - return "OTH"; + return null; } } @@ -129,8 +129,8 @@ public static String normalizeGenotypeForEpipulse(String genotypeText) { } // Try to parse formats like "A", "B1", "D10", etc. and add MEASV_ prefix - // This matches a single uppercase letter optionally followed by digits - if (normalized.matches("^[A-Z]\\d*$")) { + // Measles genotypes are limited (e.g., A, B1-3, C1-2, D1-11, E, F, G1-3, H1-2) + if (normalized.matches("^(A|B[1-3]|C[1-2]|D(1[0-1]|[1-9])|E|F|G[1-3]|H[1-2])$")) { return "MEASV_" + normalized; } @@ -301,6 +301,9 @@ public static List mapSymptomsToComplicationCodes( * @return true if clinically confirmed, false otherwise */ public static Boolean deriveClinicalCriteriaStatus(YesNoUnknown clinicalConfirmation) { + if (clinicalConfirmation == null || clinicalConfirmation == YesNoUnknown.UNKNOWN) { + return null; + } return clinicalConfirmation == YesNoUnknown.YES; } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java index 33ff177f9b0..cfe99388d78 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java @@ -94,16 +94,15 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp return; } - if (epipulseExport.getStatus() != EpipulseExportStatus.PENDING) { - logger.error("EpipulseExport with uuid " + uuid + " is not in status PENDING"); + // Atomic status claim: try to update from PENDING to IN_PROGRESS + boolean claimed = diseaseExportService.tryClaimExportForProcessing(uuid); + if (!claimed) { + logger.info("Export {} not claimed - either already processing or not in PENDING status", uuid); return; } shouldUpdateStatus = true; - // Update status to IN_PROGRESS - diseaseExportService.updateStatusForBackgroundProcess(uuid, EpipulseExportStatus.IN_PROGRESS, null, null, null); - // Load configuration EpipulseExportDto exportDto = epipulseExportEjb.toEpipulseExportDto(epipulseExport); String serverCountryCode = configFacadeEjb.getCountryCode(); diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java index 26fa424f9dc..4964744023e 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseDiseaseExportService.java @@ -67,6 +67,39 @@ public EpipulseDiseaseExportResult exportMeaslesCaseBased(EpipulseExportDto expo return measlesExportStrategy.export(exportDto, serverCountryLocale, serverCountryName); } + @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) + public boolean tryClaimExportForProcessing(String exportUuid) { + try { + String sql = "UPDATE epipulse_export SET " + + "status = :newStatus, " + + "status_change_date = now(), " + + "changedate = now() " + + "WHERE uuid = :uuid AND status = :expectedStatus"; + + int updated = em.createNativeQuery(sql) + .setParameter("newStatus", EpipulseExportStatus.IN_PROGRESS.name()) + .setParameter("uuid", exportUuid) + .setParameter("expectedStatus", EpipulseExportStatus.PENDING.name()) + .executeUpdate(); + + em.flush(); + + if (updated > 0) { + logger.info("Successfully claimed export {} for processing", exportUuid); + return true; + } else { + logger.info("Export {} already claimed by another process or not in PENDING status", exportUuid); + return false; + } + + } catch (Exception e) { + logger.error("Failed to claim export {} for processing: {}", exportUuid, e.getMessage(), e); + return false; + } finally { + em.clear(); + } + } + @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void updateStatusForBackgroundProcess( String exportUuid, diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java index 7bd7f812072..d502aca744b 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/strategy/MeaslesExportStrategy.java @@ -223,7 +223,13 @@ private String buildSampleDataCte() { " STRING_AGG(DISTINCT CAST(s3.samplematerial AS text), ',' ORDER BY CAST(s3.samplematerial AS text)) as specimen_types_serology " + " FROM filtered_cases c " + " LEFT JOIN samples s ON s.associatedcase_id = c.id AND s.deleted = false " + - " LEFT JOIN samples s2 ON s2.associatedcase_id = c.id AND s2.deleted = false AND s2.samplematerial IS NOT NULL " + + " LEFT JOIN (SELECT DISTINCT s_vir.id, s_vir.associatedcase_id, s_vir.samplematerial " + + " FROM samples s_vir " + + " JOIN pathogentest pt_vir ON pt_vir.sample_id = s_vir.id " + + " WHERE s_vir.deleted = false " + + " AND s_vir.samplematerial IS NOT NULL " + + " AND pt_vir.testtype IN ('PCR_RT_PCR', 'CULTURE', 'ISOLATION', 'DIRECT_FLUORESCENT_ANTIBODY', 'INDIRECT_FLUORESCENT_ANTIBODY')) s2 " + + " ON s2.associatedcase_id = c.id " + " LEFT JOIN (SELECT DISTINCT s_sero.id, s_sero.associatedcase_id, s_sero.samplematerial " + " FROM samples s_sero " + " JOIN pathogentest pt_sero ON pt_sero.sample_id = s_sero.id " + @@ -316,13 +322,14 @@ private String buildExposureLocationsCte() { " CASE " + " WHEN co.defaultname IS NOT NULL THEN co.defaultname " + " WHEN l.city IS NOT NULL THEN l.city " + - " ELSE l.details " + + " WHEN l.details IS NOT NULL THEN l.details " + + " ELSE 'Unknown' " + " END, " + " '; ' " + " ORDER BY e.startdate DESC" + " ) as infection_locations " + " FROM exposures e " + - " JOIN location l ON e.location_id = l.id " + + " LEFT JOIN location l ON e.location_id = l.id " + " LEFT JOIN country co ON l.country_id = co.id " + " WHERE e.epidata_id IN (SELECT epidata_id FROM filtered_cases WHERE epidata_id IS NOT NULL) " + " GROUP BY e.epidata_id), "; From 1b70f5f9f19224d54c39ec6f07f99761fc13f630 Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Fri, 9 Jan 2026 14:57:15 +0300 Subject: [PATCH 5/6] #13771 - Add Epipulse export functionality for Measles disease --- .../epipulse/EpipulseLaboratoryMapper.java | 24 ++++++++++++++--- .../EpipulseCsvExportOrchestrator.java | 26 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java index ad49817b7d2..24a6e5350af 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java @@ -123,9 +123,13 @@ public static String normalizeGenotypeForEpipulse(String genotypeText) { String normalized = genotypeText.trim().toUpperCase(); - // If already in MEASV_ format, return as-is + // If already in MEASV_ format, validate the suffix and return if valid if (normalized.startsWith("MEASV_")) { - return normalized; + String suffix = normalized.substring(6); // Extract part after "MEASV_" + if (isValidMeaslesGenotype(suffix)) { + return normalized; + } + return null; } // Try to parse formats like "A", "B1", "D10", etc. and add MEASV_ prefix @@ -137,7 +141,7 @@ public static String normalizeGenotypeForEpipulse(String genotypeText) { // Try to extract genotype from common formats with delimiters // Matches patterns like "MeV-A", "Genotype-B1", "MV/A", "MEASLES-D4" String extracted = extractGenotypeFromDelimitedFormat(normalized); - if (extracted != null) { + if (isValidMeaslesGenotype(extracted)) { return "MEASV_" + extracted; } @@ -145,6 +149,20 @@ public static String normalizeGenotypeForEpipulse(String genotypeText) { return null; } + /** + * Validates if a genotype code matches the known measles genotype pattern. + * + * @param genotype + * Genotype code without MEASV_ prefix (e.g., "A", "B1", "D10") + * @return true if the genotype is a valid measles genotype, false otherwise + */ + private static boolean isValidMeaslesGenotype(String genotype) { + if (genotype == null) { + return false; + } + return genotype.matches("^(A|B[1-3]|C[1-2]|D(1[0-1]|[1-9])|E|F|G[1-3]|H[1-2])$"); + } + /** * Extracts genotype code from delimited formats like "MeV-A", "Genotype B1", etc. * Uses strict pattern matching to avoid false positives. diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java index cfe99388d78..ae47b069ea7 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java @@ -76,6 +76,8 @@ public class EpipulseCsvExportOrchestrator { public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExportStrategy csvStrategy) { CSVWriter writer = null; + FileOutputStream fos = null; + OutputStreamWriter osw = null; EpipulseExport epipulseExport = null; EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; boolean shouldUpdateStatus = false; @@ -117,10 +119,10 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp EpipulseDiseaseExportResult exportResult = exportFunction.execute(exportDto, serverCountryCode, serverCountryName); totalRecords = exportResult.getExportEntryList().size(); - // Setup CSV writer - writer = CSVUtils.createCSVWriter( - new OutputStreamWriter(new FileOutputStream(exportFilePath), StandardCharsets.UTF_8), - configFacadeEjb.getCsvSeparator()); + // Setup CSV writer with explicit stream management + fos = new FileOutputStream(exportFilePath); + osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + writer = CSVUtils.createCSVWriter(osw, configFacadeEjb.getCsvSeparator()); // Build column names using strategy List columnNames = csvStrategy.buildColumnNames(exportResult); @@ -140,7 +142,7 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp exportStatus = EpipulseExportStatus.FAILED; logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); } finally { - // Close writer + // Close resources in reverse order if (writer != null) { try { writer.close(); @@ -148,6 +150,20 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); } } + if (osw != null) { + try { + osw.close(); + } catch (Exception e) { + logger.error("CRITICAL: Failed to close OutputStreamWriter for uuid " + uuid + ": " + e.getMessage(), e); + } + } + if (fos != null) { + try { + fos.close(); + } catch (Exception e) { + logger.error("CRITICAL: Failed to close FileOutputStream for uuid " + uuid + ": " + e.getMessage(), e); + } + } // Calculate file size after writer is closed if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { From c97279ae36d0a582f1b633a58f49f84dc24a7275 Mon Sep 17 00:00:00 2001 From: Harold Asiimwe Date: Fri, 9 Jan 2026 15:11:19 +0300 Subject: [PATCH 6/6] #13771 - Add Epipulse export functionality for Measles disease --- .../epipulse/EpipulseLaboratoryMapper.java | 6 +-- .../EpipulseCsvExportOrchestrator.java | 53 +++++-------------- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java index 24a6e5350af..e1b9145213a 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java @@ -84,11 +84,11 @@ public static String mapSampleMaterialToEpipulseCode(SampleMaterial sampleMateri * * @param testResult * SORMAS test result enum - * @return EpiPulse result code (POS/NEG/EQUI/NOTEST) + * @return EpiPulse result code (POS/NEG/EQUI/NOTEST), or null if input is null */ public static String mapTestResultToEpipulseCode(PathogenTestResultType testResult) { if (testResult == null) { - return "NOTEST"; + return null; } switch (testResult) { @@ -102,7 +102,7 @@ public static String mapTestResultToEpipulseCode(PathogenTestResultType testResu case NOT_DONE: return "NOTEST"; default: - return "NOTEST"; + return null; } } diff --git a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java index ae47b069ea7..ee6f0dc56d4 100644 --- a/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java +++ b/sormas-backend/src/main/java/de/symeda/sormas/backend/epipulse/EpipulseCsvExportOrchestrator.java @@ -75,9 +75,6 @@ public class EpipulseCsvExportOrchestrator { */ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExportStrategy csvStrategy) { - CSVWriter writer = null; - FileOutputStream fos = null; - OutputStreamWriter osw = null; EpipulseExport epipulseExport = null; EpipulseExportStatus exportStatus = EpipulseExportStatus.FAILED; boolean shouldUpdateStatus = false; @@ -119,22 +116,23 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp EpipulseDiseaseExportResult exportResult = exportFunction.execute(exportDto, serverCountryCode, serverCountryName); totalRecords = exportResult.getExportEntryList().size(); - // Setup CSV writer with explicit stream management - fos = new FileOutputStream(exportFilePath); - osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); - writer = CSVUtils.createCSVWriter(osw, configFacadeEjb.getCsvSeparator()); + // Setup CSV writer with try-with-resources for automatic resource management + try (FileOutputStream fos = new FileOutputStream(exportFilePath); + OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + CSVWriter writer = CSVUtils.createCSVWriter(osw, configFacadeEjb.getCsvSeparator())) { - // Build column names using strategy - List columnNames = csvStrategy.buildColumnNames(exportResult); + // Build column names using strategy + List columnNames = csvStrategy.buildColumnNames(exportResult); - // Write headers - writer.writeNext(columnNames.toArray(new String[columnNames.size()])); + // Write headers + writer.writeNext(columnNames.toArray(new String[columnNames.size()])); - // Write entries using strategy - for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { - String[] exportLine = new String[columnNames.size()]; - csvStrategy.writeEntryRow(dto, exportLine, exportResult); - writer.writeNext(exportLine); + // Write entries using strategy + for (EpipulseDiseaseExportEntryDto dto : exportResult.getExportEntryList()) { + String[] exportLine = new String[columnNames.size()]; + csvStrategy.writeEntryRow(dto, exportLine, exportResult); + writer.writeNext(exportLine); + } } exportStatus = EpipulseExportStatus.COMPLETED; @@ -142,29 +140,6 @@ public void orchestrateExport(String uuid, ExportFunction exportFunction, CsvExp exportStatus = EpipulseExportStatus.FAILED; logger.error("Error during export with uuid " + uuid + ": " + e.getMessage(), e); } finally { - // Close resources in reverse order - if (writer != null) { - try { - writer.close(); - } catch (Exception e) { - logger.error("CRITICAL: Failed to close CSVWriter for uuid " + uuid + ": " + e.getMessage(), e); - } - } - if (osw != null) { - try { - osw.close(); - } catch (Exception e) { - logger.error("CRITICAL: Failed to close OutputStreamWriter for uuid " + uuid + ": " + e.getMessage(), e); - } - } - if (fos != null) { - try { - fos.close(); - } catch (Exception e) { - logger.error("CRITICAL: Failed to close FileOutputStream for uuid " + uuid + ": " + e.getMessage(), e); - } - } - // Calculate file size after writer is closed if (exportFilePath != null && exportStatus == EpipulseExportStatus.COMPLETED) { try {