From 3a2c6b76d69646acc43f9510c35df74760f92138 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Wed, 12 Nov 2025 16:56:34 -0800 Subject: [PATCH 1/8] Add metrics and timestamps in ResearchService --- .../services/system/ResearchService.java | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 76d990193..9833bd746 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -50,6 +50,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; +import play.Logger; public class ResearchService implements IResearchService { @@ -342,7 +343,8 @@ else if (filters.getPrimaryDataset().equals("prescribedMeds") || @Override public ServiceResponse exportPatientsByTrip(Integer tripId){ - + Logger.info("ResearchService:exportPatientsByTrip called with tripId={} at 0 seconds. ", tripId); + long startTimeNanos = System.nanoTime(); ServiceResponse response = new ServiceResponse<>(); // Build Query based on Filters @@ -353,31 +355,62 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ .fetch("patientPrescriptions") .fetch("patientPrescriptions.medication"); + //PERF: can optimize so that each join step doesn't have to retrieve all data in the model (table) + // .fetch and .where only build up the SQL query definition + + ExpressionList researchEncounterExpressionList = researchEncounterQuery.where(); // -1 is default from form if ( tripId != null && tripId != -1 ) { - researchEncounterExpressionList.eq("missionTrip.id",tripId); + } else { + Logger.error("ResearchService:exportPatientsByTrip passed with trip id: {} where tripId=null and tripId=-1 are invalidated" + , tripId); } + // Not sure how to handle the case when no trips are selected? + // Maybe can retrieve an aggregate of all trips + researchEncounterExpressionList.isNull("patient.isDeleted"); researchEncounterExpressionList.orderBy().desc("date_of_triage_visit"); - researchEncounterExpressionList.findList(); + // Do we really need to order the patients? + // Right now in the current (old) approach, you retrieve the entire list of patientEncounters joined with patients and + // patientPrescriptions and ?patientPrescriptions.medications? + + researchEncounterExpressionList.findList(); //executes the query and returns into researchEncounterExpressionList List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); + // why are you using the repository again to find the patientEncounters?? +// List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); +// // removing old code above caused no significant performance benefits +// // new code might just be simpler though + + Logger.info("ResearchService:exportPatientsByTrip executed query and populated patientEncounters at {} seconds. ", + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); // As new patients are encountered, generate a UUID to represent them in the export file + // Why do we need patient_uuid as the key in the map?? + // There is a patient_id field in the generated csv files which contains the UUID + // But since the UUID is random generated in the for loop below, whats the point of it + // tldr: it doesn't matter if no performance issues + Map patientIdMap = new HashMap<>(); // Format patient data for the csv file List researchExportItemsForCSVExport = new ArrayList<>(); + long duration_iteration = System.nanoTime(); + long timestamp_sec1 = -1; + long timestamp_sec2 = -1; + long timestamp_sec3 = -1; + for(IResearchEncounter patientEncounter : patientEncounters ){ UUID patient_uuid; + // SECTION 1 // If UUID already generated for patient, use that if( patientIdMap.containsKey(patientEncounter.getPatient().getId()) ){ @@ -389,15 +422,46 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ patient_uuid = UUID.randomUUID(); patientIdMap.put(patientEncounter.getPatient().getId(), patient_uuid); } + if (timestamp_sec1 == -1) timestamp_sec1 = System.nanoTime() - duration_iteration; + // + // SECTION 2 + //!!!!! MAIN BOTTLENECK !!!!!! ResearchExportItem item = createResearchExportItem(patientEncounter, patient_uuid); + if (timestamp_sec2 == -1) timestamp_sec2 = System.nanoTime() - duration_iteration; + // + + // SECTION 3 researchExportItemsForCSVExport.add(item); + if (timestamp_sec3 == -1) timestamp_sec3 = System.nanoTime() - duration_iteration; + // } + duration_iteration = System.nanoTime() - duration_iteration; + + Logger.info("ResearchService:exportPatientsByTrip iterations finished in {} at {} seconds with an average of {} seconds per iteration. ", + String.format("%.3f", (float) (duration_iteration) / 1_000_000_000), + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000), + String.format("%.3f", (float) (duration_iteration/patientEncounters.size()) / 1_000_000_000)); + Logger.info("ResearchService:exportPatientsByTrip first iteration section timestamps: {} {} {} milliseconds. ", + String.format("%.3f", (float) (timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec2) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec3) / 1_000_000)); + Logger.info("ResearchService:exportPatientsByTrip first iteration section durations : {} {} {} milliseconds. ", + String.format("%.3f", (float) (timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec2 - timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec3 - timestamp_sec2) / 1_000_000)); + Logger.info("ResearchService:exportPatientsByTrip called createCsvFile() at {} seconds. ", + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); File eFile = createCsvFile(researchExportItemsForCSVExport); response.setResponseObject(eFile); + long endTimeNanos = System.nanoTime(); + float executionTimeSeconds = (float) (endTimeNanos - startTimeNanos) / 1_000_000_000; + Logger.info("ResearchService:exportPatientsByTrip finished {} encounters in {} seconds. ", + patientEncounters.size(), String.format("%.3f", executionTimeSeconds)); + return response; } @@ -408,6 +472,9 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ * @return a csv formatted file of the encounters */ private File createCsvFile( List encounters ){ + long startTimeNanos = System.nanoTime(); + Logger.info("ResearchService:createCsvFile called with {} encounters. ", + encounters.size()); // Make File and get path String csvFilePath = LogicDoer.getCsvFilePath(); @@ -453,6 +520,9 @@ private File createCsvFile( List encounters ){ } } + Logger.info("ResearchService:createCsvFile created a csv file in {} seconds. ", + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); + return eFile; } From 99ed5938ab58456c7a7e1b331066551632c4600b Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Wed, 10 Dec 2025 13:44:48 -0800 Subject: [PATCH 2/8] Refactor research export for perf --- Build.sbt | 3 +- .../services/system/ResearchService.java | 156 ++++++++++++------ .../ui/controllers/ResearchController.java | 3 +- app/femr/util/stringhelpers/StringUtils.java | 61 ++++++- 4 files changed, 168 insertions(+), 55 deletions(-) diff --git a/Build.sbt b/Build.sbt index bd3fd915d..5dc1c62a8 100644 --- a/Build.sbt +++ b/Build.sbt @@ -28,7 +28,8 @@ val appDependencies = Seq( "com.h2database" % "h2" % "1.4.193", "com.jcraft" % "jsch" % "0.1.54", "ca.uhn.hapi.fhir" % "hapi-fhir-base" % "5.7.0", - "ca.uhn.hapi.fhir" % "hapi-fhir-structures-r5" % "5.7.0" + "ca.uhn.hapi.fhir" % "hapi-fhir-structures-r5" % "5.7.0", + "org.apache.commons" % "commons-csv" % "1.10.0" ) diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 9833bd746..ce07818d0 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -43,9 +43,12 @@ import femr.util.stringhelpers.CSVWriterGson; import femr.util.stringhelpers.GsonFlattener; import femr.util.stringhelpers.StringUtils; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileWriter; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -347,20 +350,58 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ long startTimeNanos = System.nanoTime(); ServiceResponse response = new ServiceResponse<>(); - // Build Query based on Filters - Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); + // + + String csvParentDirPath = LogicDoer.getCsvFilePath(); + File parentDir = new File(csvParentDirPath); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + + SimpleDateFormat dateformat = new SimpleDateFormat("MMddyy-HHmmss"); + String timestamp = dateformat.format(new Date()); + String csvFilePath = csvParentDirPath+"export-"+timestamp+".csv"; + + File exportFile = new File(csvFilePath); + + CSVFormat fileformat = CSVFormat.DEFAULT + .withHeader( + "patientId", + "gender", + "age", + "isPregnant", + "weeksPregnant", + "dayOfVisit", + "tripId", + "trip_team", + "trip_country", + "chiefComplaints", + "prescribedMedications", + "dispensedMedications", + "vitals", + "tabFields" + ); + CSVPrinter printer; + try { + printer = new CSVPrinter(new FileWriter(exportFile), fileformat); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip CSVPrinter could not be instantiated:"); + e.printStackTrace(); + return null; + } + if (!exportFile.exists()){ + Logger.error("ResearchService:exportPatientsByTrip export file could not be created."); + return null; + } + // + // + Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); researchEncounterQuery .fetch("patient") .fetch("patientPrescriptions") .fetch("patientPrescriptions.medication"); - - //PERF: can optimize so that each join step doesn't have to retrieve all data in the model (table) - // .fetch and .where only build up the SQL query definition - - ExpressionList researchEncounterExpressionList = researchEncounterQuery.where(); - // -1 is default from form if ( tripId != null && tripId != -1 ) { researchEncounterExpressionList.eq("missionTrip.id",tripId); @@ -368,39 +409,21 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ Logger.error("ResearchService:exportPatientsByTrip passed with trip id: {} where tripId=null and tripId=-1 are invalidated" , tripId); } - // Not sure how to handle the case when no trips are selected? - // Maybe can retrieve an aggregate of all trips - + // Need to add how to handle the case when no trips are selected instead of error? researchEncounterExpressionList.isNull("patient.isDeleted"); researchEncounterExpressionList.orderBy().desc("date_of_triage_visit"); // Do we really need to order the patients? - // Right now in the current (old) approach, you retrieve the entire list of patientEncounters joined with patients and - // patientPrescriptions and ?patientPrescriptions.medications? researchEncounterExpressionList.findList(); //executes the query and returns into researchEncounterExpressionList List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); - // why are you using the repository again to find the patientEncounters?? - -// List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); -// // removing old code above caused no significant performance benefits -// // new code might just be simpler though Logger.info("ResearchService:exportPatientsByTrip executed query and populated patientEncounters at {} seconds. ", String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); - // As new patients are encountered, generate a UUID to represent them in the export file - // Why do we need patient_uuid as the key in the map?? - // There is a patient_id field in the generated csv files which contains the UUID - // But since the UUID is random generated in the for loop below, whats the point of it - // tldr: it doesn't matter if no performance issues - Map patientIdMap = new HashMap<>(); - // Format patient data for the csv file - List researchExportItemsForCSVExport = new ArrayList<>(); - long duration_iteration = System.nanoTime(); long timestamp_sec1 = -1; long timestamp_sec2 = -1; @@ -409,35 +432,58 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ for(IResearchEncounter patientEncounter : patientEncounters ){ UUID patient_uuid; - // SECTION 1 // If UUID already generated for patient, use that if( patientIdMap.containsKey(patientEncounter.getPatient().getId()) ){ - patient_uuid = patientIdMap.get(patientEncounter.getPatient().getId()); } // otherwise generate and store for potential additional patient encounters else{ - patient_uuid = UUID.randomUUID(); patientIdMap.put(patientEncounter.getPatient().getId(), patient_uuid); } if (timestamp_sec1 == -1) timestamp_sec1 = System.nanoTime() - duration_iteration; - // // SECTION 2 - //!!!!! MAIN BOTTLENECK !!!!!! ResearchExportItem item = createResearchExportItem(patientEncounter, patient_uuid); if (timestamp_sec2 == -1) timestamp_sec2 = System.nanoTime() - duration_iteration; - // - // SECTION 3 - researchExportItemsForCSVExport.add(item); + //new SECTION 3 + try { + printer.printRecord( + item.getPatientId() != null ? item.getPatientId().toString() : "", + item.getGender(), + item.getAge(), + item.getIsPregnant(), + item.getWeeksPregnant(), + item.getDayOfVisit(), + item.getTripId(), + item.getTrip_team(), + item.getTrip_country(), + StringUtils.joinList(item.getChiefComplaints()), + StringUtils.joinList(item.getPrescribedMedications()), + StringUtils.joinList(item.getDispensedMedications()), + StringUtils.joinFloatMap(item.getVitalMap()), + StringUtils.joinStringMap(item.getTabFieldMap()) + ); + printer.flush(); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip section3 failed to append row."); + return null; + } if (timestamp_sec3 == -1) timestamp_sec3 = System.nanoTime() - duration_iteration; - // + + } + try { + printer.close(); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip exception occurred attempting to close printer."); + e.printStackTrace(); } - duration_iteration = System.nanoTime() - duration_iteration; + response.setResponseObject(exportFile); + // + duration_iteration = System.nanoTime() - duration_iteration; Logger.info("ResearchService:exportPatientsByTrip iterations finished in {} at {} seconds with an average of {} seconds per iteration. ", String.format("%.3f", (float) (duration_iteration) / 1_000_000_000), String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000), @@ -452,16 +498,11 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ String.format("%.3f", (float) (timestamp_sec3 - timestamp_sec2) / 1_000_000)); Logger.info("ResearchService:exportPatientsByTrip called createCsvFile() at {} seconds. ", String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); - - File eFile = createCsvFile(researchExportItemsForCSVExport); - - response.setResponseObject(eFile); - long endTimeNanos = System.nanoTime(); float executionTimeSeconds = (float) (endTimeNanos - startTimeNanos) / 1_000_000_000; Logger.info("ResearchService:exportPatientsByTrip finished {} encounters in {} seconds. ", patientEncounters.size(), String.format("%.3f", executionTimeSeconds)); - + // return response; } @@ -472,9 +513,9 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ * @return a csv formatted file of the encounters */ private File createCsvFile( List encounters ){ - long startTimeNanos = System.nanoTime(); - Logger.info("ResearchService:createCsvFile called with {} encounters. ", - encounters.size()); +// long startTimeNanos = System.nanoTime(); +// Logger.info("ResearchService:createCsvFile called with {} encounters. ", +// encounters.size()); // Make File and get path String csvFilePath = LogicDoer.getCsvFilePath(); @@ -495,7 +536,6 @@ private File createCsvFile( List encounters ){ fileCreated = eFile.createNewFile(); } catch( IOException e ){ - e.printStackTrace(); } } @@ -503,34 +543,50 @@ private File createCsvFile( List encounters ){ if( fileCreated ) { Gson gson = new Gson(); + // a Gson turns a Java object into a Json JsonParser gsonParser = new JsonParser(); String jsonString = gson.toJson(encounters); + // stores a BIG String called jsonString + // + the huge list of encounters in memory now GsonFlattener parser = new GsonFlattener(); CSVWriterGson writer = new CSVWriterGson(); + Logger.info("ResearchService-createCsvFile: jsonString: {} ", jsonString); + try { List> flatJson = parser.parse(gsonParser.parse(jsonString).getAsJsonArray()); + // converts the String into Array of JsonElements + // !!which is temporarily stored as a BIG array of JsonElements + + // parser.parse() goes and flattens each nested JsonElement in the JsonArray... + // ...turning them into key, value pairs + + //!!flatJson itself is BIG List of key-value pairs writer.writeAsCSV(flatJson, csvFileName); + //writeAsCSV internally also builds a BIG list } catch (FileNotFoundException e) { - e.printStackTrace(); } } - Logger.info("ResearchService:createCsvFile created a csv file in {} seconds. ", - String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); +// Logger.info("ResearchService:createCsvFile created a csv file in {} seconds. ", +// String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); + // work on a file with a + // rename the final file at the end return eFile; } + /** * take filters and make appropriate query, get list of matching patient encounters * @param filters an object that contains all possible filters for the data * @return a list of the encounters */ + private List queryPatientData(ResearchFilterItem filters){ String datasetName = filters.getPrimaryDataset(); diff --git a/app/femr/ui/controllers/ResearchController.java b/app/femr/ui/controllers/ResearchController.java index 19d48d6a0..269ca3c43 100644 --- a/app/femr/ui/controllers/ResearchController.java +++ b/app/femr/ui/controllers/ResearchController.java @@ -83,10 +83,11 @@ public Result indexGet() { filterViewModel.setMissionTrips(missionItemServiceResponse.getResponseObject()); // Set Default Start (30 Days Ago) and End Date (Today) + // temporarily "-365 * 10" modified for development to default to last ten years Calendar today = Calendar.getInstance(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); filterViewModel.setEndDate(dateFormat.format(today.getTime())); - today.add(Calendar.DAY_OF_MONTH, -120); + today.add(Calendar.DAY_OF_MONTH, -365 * 10); filterViewModel.setStartDate(dateFormat.format(today.getTime())); diff --git a/app/femr/util/stringhelpers/StringUtils.java b/app/femr/util/stringhelpers/StringUtils.java index 45027f563..37181b2eb 100644 --- a/app/femr/util/stringhelpers/StringUtils.java +++ b/app/femr/util/stringhelpers/StringUtils.java @@ -23,6 +23,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.List; +import java.util.Map; /** * This class contains utilities for manipulating strings. If you add something here, please clearly document the @@ -240,14 +242,67 @@ public static String outputBloodPressureOrNA(String systolic, String diastolic) * @return The user friendly trip title or null if parameters were null */ public static String generateMissionTripTitle(String teamName, String country, Date startDate, Date endDate){ - if (StringUtils.isNullOrWhiteSpace(teamName) || StringUtils.isNullOrWhiteSpace(country) || startDate == null || endDate == null){ - return null; } - String tripTitle = teamName + "-" + country + "-(" + dateUtils.getFriendlyInternationalDate(startDate) + "-" + dateUtils.getFriendlyInternationalDate(endDate) + ")"; return tripTitle; } + /** + * Converts a list of strings into a single semicolon-separated value. + * Example: + * ["headache", "nausea"] → "headache; nausea" + * @param list the list of strings to join; may be null or empty + * @return a semicolon-separated string, or an empty string if the list is null or empty + */ + public static String joinList(List list) { + if (list == null || list.isEmpty()) return ""; + return String.join("; ", list); + } + + /** + * Converts a map of string keys to float values into a flattened + * semicolon-separated representation suitable for a single CSV column. + * Format: + * { "bp": 120.0, "hr": 80.0 } + * → "bp=120.0; hr=80.0" + * @param map the map of string keys to float values; may be null or empty + * @return a semicolon-delimited {@code key=value} string, or empty string for null/empty input + */ + public static String joinFloatMap(Map map) { + if (map == null || map.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry e : map.entrySet()) { + if (!first) sb.append("; "); + sb.append(e.getKey()).append("=").append(e.getValue()); + first = false; + } + return sb.toString(); + } + + /** + * Converts a map of string keys to string values into a flattened + * semicolon-separated representation for a single CSV cell. + * Format: + * { "field1": "value1", "field2": "value2" } + * → "field1=value1; field2=value2" + * @param map the map of string keys to string values; may be null or empty + * @return a semicolon-delimited {@code key=value} string, or empty string for null/empty input + */ + public static String joinStringMap(Map map) { + if (map == null || map.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry e : map.entrySet()) { + if (!first) sb.append("; "); + sb.append(e.getKey()).append("=").append(e.getValue()); + first = false; + } + return sb.toString(); + } + } + + From d130432997d3ae54695b4e574dd0c4ce2ed56377 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Wed, 17 Dec 2025 21:19:33 -0800 Subject: [PATCH 3/8] Fix csv format --- .../services/system/ResearchService.java | 148 ++++++++++++++---- .../common/models/ResearchExportItem.java | 16 +- 2 files changed, 119 insertions(+), 45 deletions(-) diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index ce07818d0..5a356fda3 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -202,14 +202,13 @@ private ResearchExportItem createResearchExportItem(IResearchEncounter encounter ResearchExportItem exportitem = new ResearchExportItem(); IPatient patient = encounter.getPatient(); - - // Patient Id - exportitem.setPatientId(patientId); - // Age Integer age = (int)Math.floor(dateUtils.getAgeAsOfDateFloat(patient.getAge(), encounter.getDateOfTriageVisit())); exportitem.setAge(age); + // Patient ID + exportitem.setPatientId(patientId); + // Gender String gender = StringUtils.outputGenderOrMissing(patient.getSex()); exportitem.setGender(gender); @@ -366,21 +365,58 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ CSVFormat fileformat = CSVFormat.DEFAULT .withHeader( - "patientId", - "gender", "age", - "isPregnant", - "weeksPregnant", + "alcohol", + "assessment", + "bloodPressureDiastolic", + "bloodPressureSystolic", + "chiefComplaints1", + "currentMedication", "dayOfVisit", + "diabetic", + "dispensedMedications1", + "dispensedMedications2", + "dispensedMedications3", + "dispensedMedications4", + "familyHistory", + "gender", + "glucose", + "heartRate", + "heightFeet", + "heightInches", + "isPregnant", + "medicalSurgicalHistory", + "narrative", + "onset", + "oxygenSaturation", + "palliates", + "patientId", + "pharmacy_note", + "physicalExamination", + "prescribedMedications1", + "prescribedMedications2", + "prescribedMedications3", + "prescribedMedications4", + "prescribedMedications5", + "prescribedMedications6", + "problem", + "procedure_counseling", + "provokes", + "quality", + "radiation", + "respiratoryRate", + "severity", + "smoker", + "socialHistory", + "temperature", + "timeOfDay", "tripId", - "trip_team", "trip_country", - "chiefComplaints", - "prescribedMedications", - "dispensedMedications", - "vitals", - "tabFields" + "trip_team", + "weeksPregnant", + "weight" ); + CSVPrinter printer; try { printer = new CSVPrinter(new FileWriter(exportFile), fileformat); @@ -406,7 +442,7 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ if ( tripId != null && tripId != -1 ) { researchEncounterExpressionList.eq("missionTrip.id",tripId); } else { - Logger.error("ResearchService:exportPatientsByTrip passed with trip id: {} where tripId=null and tripId=-1 are invalidated" + Logger.error("ResearchService:exportPatientsByTrip called with trip id: {} where tripId=null and tripId=-1 are invalidated" , tripId); } // Need to add how to handle the case when no trips are selected instead of error? @@ -451,21 +487,73 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ //new SECTION 3 try { printer.printRecord( - item.getPatientId() != null ? item.getPatientId().toString() : "", - item.getGender(), - item.getAge(), - item.getIsPregnant(), - item.getWeeksPregnant(), - item.getDayOfVisit(), - item.getTripId(), - item.getTrip_team(), - item.getTrip_country(), - StringUtils.joinList(item.getChiefComplaints()), - StringUtils.joinList(item.getPrescribedMedications()), - StringUtils.joinList(item.getDispensedMedications()), - StringUtils.joinFloatMap(item.getVitalMap()), - StringUtils.joinStringMap(item.getTabFieldMap()) + item.getAge(), // age + item.getVitalMap().get("alcohol"), // alcohol + item.getTabFieldMap().get("assessment"), // assessment + item.getVitalMap().get("bloodPressureDiastolic"), // bloodPressureDiastolic + item.getVitalMap().get("bloodPressureSystolic"), // bloodPressureSystolic + item.getChiefComplaints().isEmpty() + ? null + : item.getChiefComplaints().get(0), // chiefComplaints1 + item.getTabFieldMap().get("currentMedication"), // currentMedication + item.getDayOfVisit(), // dayOfVisit + item.getVitalMap().get("diabetic"), // diabetic + item.getDispensedMedications().size() > 0 + ? item.getDispensedMedications().get(0) : null, // dispensedMedications1 + item.getDispensedMedications().size() > 1 + ? item.getDispensedMedications().get(1) : null, // dispensedMedications2 + item.getDispensedMedications().size() > 2 + ? item.getDispensedMedications().get(2) : null, // dispensedMedications3 + item.getDispensedMedications().size() > 3 + ? item.getDispensedMedications().get(3) : null, // dispensedMedications4 + + item.getTabFieldMap().get("familyHistory"), // familyHistory + item.getGender(), // gender + item.getVitalMap().get("glucose"), // glucose + item.getVitalMap().get("heartRate"), // heartRate + item.getVitalMap().get("heightFeet"), // heightFeet + item.getVitalMap().get("heightInches"), // heightInches + item.getIsPregnant(), // isPregnant + item.getTabFieldMap().get("medicalSurgicalHistory"), // medicalSurgicalHistory + item.getTabFieldMap().get("narrative"), // narrative + item.getTabFieldMap().get("onset"), // onset + item.getVitalMap().get("oxygenSaturation"), // oxygenSaturation + item.getTabFieldMap().get("palliates"), // palliates + item.getPatientId(), // patientId + item.getTabFieldMap().get("pharmacy_note"), // pharmacy_note + item.getTabFieldMap().get("physicalExamination"), // physicalExamination + + item.getPrescribedMedications().size() > 0 + ? item.getPrescribedMedications().get(0) : null, // prescribedMedications1 + item.getPrescribedMedications().size() > 1 + ? item.getPrescribedMedications().get(1) : null, // prescribedMedications2 + item.getPrescribedMedications().size() > 2 + ? item.getPrescribedMedications().get(2) : null, // prescribedMedications3 + item.getPrescribedMedications().size() > 3 + ? item.getPrescribedMedications().get(3) : null, // prescribedMedications4 + item.getPrescribedMedications().size() > 4 + ? item.getPrescribedMedications().get(4) : null, // prescribedMedications5 + item.getPrescribedMedications().size() > 5 + ? item.getPrescribedMedications().get(5) : null, // prescribedMedications6 + + item.getTabFieldMap().get("problem"), // problem + item.getTabFieldMap().get("procedure_counseling"),// procedure_counseling + item.getTabFieldMap().get("provokes"), // provokes + item.getTabFieldMap().get("quality"), // quality + item.getTabFieldMap().get("radiation"), // radiation + item.getVitalMap().get("respiratoryRate"), // respiratoryRate + item.getTabFieldMap().get("severity"), // severity + item.getVitalMap().get("smoker"), // smoker + item.getTabFieldMap().get("socialHistory"), // socialHistory + item.getVitalMap().get("temperature"), // temperature + item.getTabFieldMap().get("timeOfDay"), // timeOfDay + item.getTripId(), // tripId + item.getTrip_country(), // trip_country + item.getTrip_team(), // trip_team + item.getWeeksPregnant(), // weeksPregnant + item.getVitalMap().get("weight") // weight ); + printer.flush(); } catch (Exception e) { Logger.error("ResearchService:exportPatientsByTrip section3 failed to append row."); @@ -496,8 +584,6 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ String.format("%.3f", (float) (timestamp_sec1) / 1_000_000), String.format("%.3f", (float) (timestamp_sec2 - timestamp_sec1) / 1_000_000), String.format("%.3f", (float) (timestamp_sec3 - timestamp_sec2) / 1_000_000)); - Logger.info("ResearchService:exportPatientsByTrip called createCsvFile() at {} seconds. ", - String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); long endTimeNanos = System.nanoTime(); float executionTimeSeconds = (float) (endTimeNanos - startTimeNanos) / 1_000_000_000; Logger.info("ResearchService:exportPatientsByTrip finished {} encounters in {} seconds. ", diff --git a/app/femr/common/models/ResearchExportItem.java b/app/femr/common/models/ResearchExportItem.java index 17e56b519..0a7c295f4 100644 --- a/app/femr/common/models/ResearchExportItem.java +++ b/app/femr/common/models/ResearchExportItem.java @@ -32,18 +32,19 @@ public class ResearchExportItem { private Integer weeksPregnant; private String dayOfVisit; private Integer tripId; + private List chiefComplaints; private List prescribedMedications; private List dispensedMedications; private Map vitalMap; private Map tabFieldMap; + private String trip_team; private String trip_country; public UUID getPatientId() { return patientId; } - public void setPatientId(UUID patientId) { this.patientId = patientId; } @@ -51,7 +52,6 @@ public void setPatientId(UUID patientId) { public String getGender() { return gender; } - public void setGender(String gender) { this.gender = gender; } @@ -59,7 +59,6 @@ public void setGender(String gender) { public Integer getAge() { return age; } - public void setAge(Integer age) { this.age = age; } @@ -67,7 +66,6 @@ public void setAge(Integer age) { public Boolean getIsPregnant() { return isPregnant; } - public void setIsPregnant(Boolean isPregnant) { this.isPregnant = isPregnant; } @@ -75,7 +73,6 @@ public void setIsPregnant(Boolean isPregnant) { public Integer getWeeksPregnant() { return weeksPregnant; } - public void setWeeksPregnant(Integer weeksPregnant) { this.weeksPregnant = weeksPregnant; } @@ -83,7 +80,6 @@ public void setWeeksPregnant(Integer weeksPregnant) { public List getChiefComplaints() { return chiefComplaints; } - public void setChiefComplaints(List chiefComplaints) { this.chiefComplaints = chiefComplaints; } @@ -91,7 +87,6 @@ public void setChiefComplaints(List chiefComplaints) { public List getPrescribedMedications() { return prescribedMedications; } - public void setPrescribedMedications(List prescribedMedications) { this.prescribedMedications = prescribedMedications; } @@ -99,7 +94,6 @@ public void setPrescribedMedications(List prescribedMedications) { public List getDispensedMedications() { return dispensedMedications; } - public void setDispensedMedications(List dispensedMedications) { this.dispensedMedications = dispensedMedications; } @@ -107,7 +101,6 @@ public void setDispensedMedications(List dispensedMedications) { public Map getVitalMap() { return vitalMap; } - public void setVitalMap(Map vitalMap) { this.vitalMap = vitalMap; } @@ -115,7 +108,6 @@ public void setVitalMap(Map vitalMap) { public Map getTabFieldMap() { return tabFieldMap; } - public void setTabFieldMap(Map tabFieldMap) { this.tabFieldMap = tabFieldMap; } @@ -123,7 +115,6 @@ public void setTabFieldMap(Map tabFieldMap) { public String getDayOfVisit() { return dayOfVisit; } - public void setDayOfVisit(String day) { this.dayOfVisit = day; } @@ -131,7 +122,6 @@ public void setDayOfVisit(String day) { public Integer getTripId() { return tripId; } - public void setTripId(Integer tripId) { this.tripId = tripId; } @@ -139,7 +129,6 @@ public void setTripId(Integer tripId) { public String getTrip_team() { return trip_team; } - public void setTrip_team(String trip_team) { this.trip_team = trip_team; } @@ -147,7 +136,6 @@ public void setTrip_team(String trip_team) { public String getTrip_country() { return trip_country; } - public void setTrip_country(String trip_country) { this.trip_country = trip_country; } From 60fb0cdb6aafa234f0563d1b1399b65822509a32 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Wed, 7 Jan 2026 13:43:44 -0800 Subject: [PATCH 4/8] Safety commit before reworking temp directory use --- Dockerfile | 10 +- app/femr/business/helpers/LogicDoer.java | 6 +- .../services/system/ResearchService.java | 237 ++++++++++++------ 3 files changed, 176 insertions(+), 77 deletions(-) diff --git a/Dockerfile b/Dockerfile index ca1ed757d..3779fa882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,11 +64,13 @@ RUN --mount=type=cache,target=/root/.ivy2 \ WORKDIR $PROJECT_HOME/app/target/universal RUN unzip femr-*.zip && rm femr-*.zip -FROM openjdk:8-jre-alpine - +#FROM openjdk:8-jre-alpine +FROM eclipse-temurin:8-jre-alpine-3.23 RUN apk update -RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev -RUN pip3 install psutil +#RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev +#RUN pip3 install psutil +RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev + #database variables ARG APP_VERSION diff --git a/app/femr/business/helpers/LogicDoer.java b/app/femr/business/helpers/LogicDoer.java index 0afdf2a3a..2322ad187 100644 --- a/app/femr/business/helpers/LogicDoer.java +++ b/app/femr/business/helpers/LogicDoer.java @@ -93,9 +93,9 @@ public static String getCsvFilePath() { path += File.separator; return path; } catch (Exception ex) { - //If config doesn't exist, default to "photos" - path = "../Upload/Csv"; - return path; + //If config doesn't exist, default to "../Upload/CSV" + return ".." + File.separator + "Upload" + File.separator + "CSV" + File.separator; + } } diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 5a356fda3..0337124a0 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -46,10 +46,10 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; @@ -351,77 +351,105 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ // - String csvParentDirPath = LogicDoer.getCsvFilePath(); - File parentDir = new File(csvParentDirPath); - if (!parentDir.exists()) { - parentDir.mkdirs(); - } + String csvParentDirPath = LogicDoer.getCsvFilePath(); + File parentDir = new File(csvParentDirPath); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } - SimpleDateFormat dateformat = new SimpleDateFormat("MMddyy-HHmmss"); - String timestamp = dateformat.format(new Date()); - String csvFilePath = csvParentDirPath+"export-"+timestamp+".csv"; - - File exportFile = new File(csvFilePath); - - CSVFormat fileformat = CSVFormat.DEFAULT - .withHeader( - "age", - "alcohol", - "assessment", - "bloodPressureDiastolic", - "bloodPressureSystolic", - "chiefComplaints1", - "currentMedication", - "dayOfVisit", - "diabetic", - "dispensedMedications1", - "dispensedMedications2", - "dispensedMedications3", - "dispensedMedications4", - "familyHistory", - "gender", - "glucose", - "heartRate", - "heightFeet", - "heightInches", - "isPregnant", - "medicalSurgicalHistory", - "narrative", - "onset", - "oxygenSaturation", - "palliates", - "patientId", - "pharmacy_note", - "physicalExamination", - "prescribedMedications1", - "prescribedMedications2", - "prescribedMedications3", - "prescribedMedications4", - "prescribedMedications5", - "prescribedMedications6", - "problem", - "procedure_counseling", - "provokes", - "quality", - "radiation", - "respiratoryRate", - "severity", - "smoker", - "socialHistory", - "temperature", - "timeOfDay", - "tripId", - "trip_country", - "trip_team", - "weeksPregnant", - "weight" - ); + SimpleDateFormat dateformat = new SimpleDateFormat("MMddyy-HHmmss"); + String timestamp = dateformat.format(new Date()); + + String exportFilePath = csvParentDirPath + "export-"+timestamp+".csv"; + File exportFile = null; + try { + exportFile = new File(exportFilePath); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + exportFile.getParentFile().mkdirs(); // no-op if already exists + CSVFormat fileformat = CSVFormat.DEFAULT + .withHeader( + "age", + "alcohol", + "assessment", + "bloodPressureDiastolic", + "bloodPressureSystolic", + "chiefComplaints1", + "currentMedication", + "dayOfVisit", + "diabetic", + "dispensedMedications1", + "dispensedMedications2", + "dispensedMedications3", + "dispensedMedications4", + "familyHistory", + "gender", + "glucose", + "heartRate", + "heightFeet", + "heightInches", + "isPregnant", + "medicalSurgicalHistory", + "narrative", + "onset", + "oxygenSaturation", + "palliates", + "patientId", + "pharmacy_note", + "physicalExamination", + "prescribedMedications1", + "prescribedMedications2", + "prescribedMedications3", + "prescribedMedications4", + "prescribedMedications5", + "prescribedMedications6", + "problem", + "procedure_counseling", + "provokes", + "quality", + "radiation", + "respiratoryRate", + "severity", + "smoker", + "socialHistory", + "temperature", + "timeOfDay", + "tripId", + "trip_country", + "trip_team", + "weeksPregnant", + "weight" + ); + + CSVPrinter printer; + + assert exportFile != null; + Path p = exportFile.toPath(); // exportFile is your current string/path + System.out.println("Export absolute path: " + p.toAbsolutePath()); + System.out.println("CWD: " + System.getProperty("user.dir")); + System.out.println("Parent exists? " + (p.getParent() != null && Files.exists(p.getParent()))); + System.out.println("Parent writable? " + (p.getParent() != null && Files.isWritable(p.getParent()))); + + + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new FileWriter(exportFile)); + // new FileWriter fails + } catch (IOException e) { + Logger.error("ResearchService:exportPatientsByTrip FileWriter to produce export could not be instantiated:"); + e.printStackTrace(); + return null; + } - CSVPrinter printer; try { - printer = new CSVPrinter(new FileWriter(exportFile), fileformat); + printer = new CSVPrinter(writer, fileformat); } catch (Exception e) { Logger.error("ResearchService:exportPatientsByTrip CSVPrinter could not be instantiated:"); + Logger.info("user.dir=" + System.getProperty("user.dir")); + Logger.info("exportFile=" + exportFile.getAbsolutePath()); e.printStackTrace(); return null; } @@ -589,9 +617,78 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ Logger.info("ResearchService:exportPatientsByTrip finished {} encounters in {} seconds. ", patientEncounters.size(), String.format("%.3f", executionTimeSeconds)); // + + try { + writer.close(); + } catch (Exception e) + { + e.printStackTrace(); + return response; + } return response; } + // @Override + // public ServiceResponse exportPatientsByTrip(Integer tripId){ + + // ServiceResponse response = new ServiceResponse<>(); + + // // Build Query based on Filters + // Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); + + // researchEncounterQuery + // .fetch("patient") + // .fetch("patientPrescriptions") + // .fetch("patientPrescriptions.medication"); + + // ExpressionList researchEncounterExpressionList = researchEncounterQuery.where(); + + // // -1 is default from form + // if ( tripId != null && tripId != -1 ) { + + // researchEncounterExpressionList.eq("missionTrip.id",tripId); + // } + + // researchEncounterExpressionList.isNull("patient.isDeleted"); + // researchEncounterExpressionList.orderBy().desc("date_of_triage_visit"); + // researchEncounterExpressionList.findList(); + + // List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); + + + // // As new patients are encountered, generate a UUID to represent them in the export file + // Map patientIdMap = new HashMap<>(); + + // // Format patient data for the csv file + // List researchExportItemsForCSVExport = new ArrayList<>(); + + // for(IResearchEncounter patientEncounter : patientEncounters ){ + + // UUID patient_uuid; + + // // If UUID already generated for patient, use that + // if( patientIdMap.containsKey(patientEncounter.getPatient().getId()) ){ + + // patient_uuid = patientIdMap.get(patientEncounter.getPatient().getId()); + // } + // // otherwise generate and store for potential additional patient encounters + // else{ + + // patient_uuid = UUID.randomUUID(); + // patientIdMap.put(patientEncounter.getPatient().getId(), patient_uuid); + // } + + // ResearchExportItem item = createResearchExportItem(patientEncounter, patient_uuid); + // researchExportItemsForCSVExport.add(item); + // } + + // File eFile = createCsvFile(researchExportItemsForCSVExport); + + // response.setResponseObject(eFile); +// +// return response; +// } + /** * Creates a csv file from a list of ResearchExportItems * From 692d824238b3170fc06a87258b6f45b8ada3e5f2 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Wed, 7 Jan 2026 18:24:18 -0800 Subject: [PATCH 5/8] File create permission fixes --- app/femr/business/services/system/ResearchService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 0337124a0..990e508c2 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -360,16 +360,16 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ SimpleDateFormat dateformat = new SimpleDateFormat("MMddyy-HHmmss"); String timestamp = dateformat.format(new Date()); - String exportFilePath = csvParentDirPath + "export-"+timestamp+".csv"; File exportFile = null; try { - exportFile = new File(exportFilePath); + exportFile = File.createTempFile("export" + timestamp+"-", ".csv"); } catch (Exception e) { e.printStackTrace(); return null; } - exportFile.getParentFile().mkdirs(); // no-op if already exists +// +// exportFile.getParentFile().mkdirs(); // no-op if already exists CSVFormat fileformat = CSVFormat.DEFAULT .withHeader( "age", From a2fb6f6f7f1cd24355aa095972ee672c8b89aa72 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Thu, 22 Jan 2026 13:24:30 -0800 Subject: [PATCH 6/8] Add UX improvement + alert --- .../services/system/ResearchService.java | 1 + .../ui/controllers/ResearchController.java | 3 ++ public/js/research/filter-menu.js | 54 ++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 990e508c2..ce03fc024 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -625,6 +625,7 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ e.printStackTrace(); return response; } + return response; } diff --git a/app/femr/ui/controllers/ResearchController.java b/app/femr/ui/controllers/ResearchController.java index 269ca3c43..d497d1816 100644 --- a/app/femr/ui/controllers/ResearchController.java +++ b/app/femr/ui/controllers/ResearchController.java @@ -131,6 +131,9 @@ public Result exportPost() { // This does weird stuff and isn't reliable. //ServiceResponse exportServiceResponse = researchService.retrieveCsvExportFile(filterItem); ServiceResponse exportServiceResponse = researchService.exportPatientsByTrip(filterItem.getMissionTripId()); + if (exportServiceResponse == null) { + return internalServerError("Export Failed"); + } File csvFile = exportServiceResponse.getResponseObject(); diff --git a/public/js/research/filter-menu.js b/public/js/research/filter-menu.js index 0be4cce01..ab921829e 100644 --- a/public/js/research/filter-menu.js +++ b/public/js/research/filter-menu.js @@ -157,10 +157,60 @@ var filterMenuModule = (function () { return false; }; - var exportData = function () { + var exportData = async function (e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + const btn = document.getElementById("export-button"); + btn.disabled = true; + btn.value = "Exporting…"; + + // form is often a jQuery object in this codebase + var formEl = form && form.jquery ? form[0] : form; + if (!formEl) { + alert("Export failed (form not found)."); + btn.disabled = false; + btn.value = "Export Data"; + return false; + } + + try { + const resp = await fetch(formEl.action, { + method: "POST", + body: new FormData(formEl), + credentials: "same-origin" + }); + + if (!resp.ok) { + alert("Export failed (" + resp.status + ")"); + btn.disabled = false; + btn.value = "Export Data"; + return false; + } + + const blob = await resp.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + + const cd = resp.headers.get("content-disposition") || ""; + const match = cd.match(/filename="?([^"]+)"?/i); + a.download = match ? match[1] : "research_export.csv"; + + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); - $(form).submit(); + alert("Research export completed!"); + } catch (err) { + alert("Export failed."); + } + btn.disabled = false; + btn.value = "Export Data"; return false; }; From 531cbb0eb976461aa8a64ea4bcb7f3d5bd520a21 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Tue, 17 Feb 2026 20:02:22 -0800 Subject: [PATCH 7/8] Fix deprecated image + pid bug --- Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3779fa882..7fe9c2451 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,14 +64,13 @@ RUN --mount=type=cache,target=/root/.ivy2 \ WORKDIR $PROJECT_HOME/app/target/universal RUN unzip femr-*.zip && rm femr-*.zip -#FROM openjdk:8-jre-alpine FROM eclipse-temurin:8-jre-alpine-3.23 + RUN apk update -#RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev +#RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev #RUN pip3 install psutil RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev - #database variables ARG APP_VERSION ARG BUILD_DATE @@ -88,4 +87,4 @@ EXPOSE 9000 # run fEMR using env variables #ENTRYPOINT url=$DB_URL usr=$DB_USER pass=$DB_PASS sbt ~run -ENTRYPOINT ["/bin/bash", "-c", "/opt/bin/femr/bin/femr"] \ No newline at end of file +ENTRYPOINT ["/bin/bash", "-c", "rm -f /opt/bin/femr/RUNNING_PID; exec /opt/bin/femr/bin/femr -Dpidfile.path=/dev/null"] \ No newline at end of file From 6c64f8b04b29e1425f5823b42461752fe0cc17c7 Mon Sep 17 00:00:00 2001 From: "KrakenM (T)" Date: Tue, 3 Mar 2026 18:28:49 -0800 Subject: [PATCH 8/8] Fix implicit imports into explicit exports --- Dockerfile | 2 +- app/femr/business/services/system/ResearchService.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7fe9c2451..fd386e5b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,7 +67,7 @@ RUN unzip femr-*.zip && rm femr-*.zip FROM eclipse-temurin:8-jre-alpine-3.23 RUN apk update -#RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev +#RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl -dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev #RUN pip3 install psutil RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index ce03fc024..e8b430783 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -46,10 +46,13 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; -import java.io.*; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*;