001package com.studentgui.apphelpers; 002 003import java.io.IOException; 004import java.nio.file.Files; 005import java.nio.file.Path; 006import java.nio.file.Paths; 007import java.util.ArrayList; 008import java.util.List; 009 010/** 011 * Miscellaneous filesystem and small utility helpers used by the UI pages. 012 * 013 * Responsibilities include selecting and creating the application home 014 * directory, creating per-student folder hierarchies, and providing a 015 * small roster fallback when no students.json exists. 016 */ 017public class Helpers { 018 /** 019 * Private constructor to prevent instantiation of this utility class. 020 */ 021 private Helpers() { 022 throw new AssertionError("Helpers is a utility class"); 023 } 024 private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Helpers.class); 025 /** The project working directory (where the process was started). */ 026 public static final Path PROJECT_ROOT = Paths.get(System.getProperty("user.dir")); 027 /** Application home used for storing app-specific files (defaults to ./app_home). */ 028 public static final Path APP_HOME = selectAppHome(); 029 /** Root directory for persisted application data (alias of APP_HOME). */ 030 public static final Path DATA_ROOT = APP_HOME; 031 /** Directory that holds the database file. */ 032 public static final Path DATABASE_ROOT = DATA_ROOT.resolve("StudentDatabase"); 033 /** Canonical database file path used by SQLite operations. */ 034 public static final Path DATABASE_PATH = DATABASE_ROOT.resolve("students20252026.db"); 035 036 /** 037 * Select a suitable application home directory. Attempts to use a 038 * ./app_home subdirectory of the working directory and falls back to the 039 * system temporary directory if creation fails. 040 */ 041 private static Path selectAppHome() { 042 try { 043 Path candidate = PROJECT_ROOT.resolve("app_home"); 044 Files.createDirectories(candidate); 045 // test write 046 Path test = candidate.resolve(".write_test"); 047 Files.writeString(test, ""); 048 Files.deleteIfExists(test); 049 return candidate; 050 } catch (IOException e) { 051 LOG.debug("Unable to create app_home; falling back to temp dir", e); 052 try { 053 Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "StudentDataGUI"); 054 Files.createDirectories(tmp); 055 return tmp; 056 } catch (IOException ex) { 057 LOG.debug("Unable to create fallback temp dir; using CWD", ex); 058 return Paths.get("."); 059 } 060 } 061 } 062 063 /** 064 * Attempt to set the JVM working directory to APP_HOME. Fails silently if 065 * the property cannot be set in the running environment. 066 */ 067 public static void setStartDir() { 068 /** 069 * Set the JVM working directory to the application home when possible. 070 * Fail silently if the property cannot be set. 071 */ 072 try { 073 System.setProperty("user.dir", APP_HOME.toString()); 074 } catch (SecurityException se) { 075 LOG.debug("Unable to set user.dir to APP_HOME {}", APP_HOME, se); 076 } 077 } 078 079 /** 080 * Ensure the working data directory exists under APP_HOME. This is 081 * idempotent and safe to call on startup. 082 */ 083 public static void workingDir() { 084 /** 085 * Ensure the working data directory exists under the application home. 086 */ 087 try { 088 Path studentDataDir = APP_HOME.resolve("StudentDataFiles"); 089 Files.createDirectories(studentDataDir); 090 } catch (IOException ioe) { 091 LOG.debug("Unable to create StudentDataFiles directory under {}", APP_HOME, ioe); 092 } 093 } 094 095 /** 096 * Create a basic folder hierarchy under DATA_ROOT for each student. 097 * This will create StudentDataFiles, backups and errorLogs and a 098 * per-student folder with subfolders for data sheets and materials. 099 */ 100 public static void createFolderHierarchy() { 101 /** 102 * Create a basic folder hierarchy under DATA_ROOT for each student. 103 * This is idempotent and will create per-student subfolders and an 104 * omnibus csv file when missing. 105 */ 106 // Create basic folders for each student in a simple roster 107 List<String> students = getStudents(); 108 Path studentDatafilesRoot = DATA_ROOT.resolve("StudentDataFiles"); 109 Path studentErrorlogsRoot = DATA_ROOT.resolve("errorLogs"); 110 Path studentBackupsRoot = DATA_ROOT.resolve("backups"); 111 try { 112 Files.createDirectories(studentDatafilesRoot); 113 Files.createDirectories(studentErrorlogsRoot); 114 Files.createDirectories(studentBackupsRoot); 115 } catch (IOException ioe) { 116 LOG.debug("Unable to create one or more data folders under {}", DATA_ROOT, ioe); 117 } 118 119 for (String name : students) { 120 String safe = sanitize(name); 121 Path studentFolder = studentDatafilesRoot.resolve(safe); 122 try { 123 Files.createDirectories(studentFolder.resolve("StudentDataSheets")); 124 Files.createDirectories(studentFolder.resolve("StudentInstructionMaterials")); 125 Files.createDirectories(studentFolder.resolve("StudentVisionAssessments")); 126 Path omnibus = studentFolder.resolve("omnibusDatabase.csv"); 127 if (!Files.exists(omnibus)) { 128 Files.createFile(omnibus); 129 } 130 } catch (IOException ioe) { 131 LOG.debug("Unable to create per-student folder or omnibus file for {}", name, ioe); 132 } 133 } 134 } 135 136 /** 137 * Make a filesystem-safe folder name by stripping or replacing forbidden 138 * characters. 139 */ 140 private static String sanitize(final String s) { 141 if (s == null) { 142 return ""; 143 } 144 String t = s.trim(); 145 // remove control characters (newline, carriage return, etc.) 146 t = t.replaceAll("[\\p{Cntrl}]", ""); 147 // replace common filesystem-forbidden characters with underscore 148 char[] forbidden = new char[]{'<','>',';',':','"','/','\\','|','?','*'}; 149 for (char c : forbidden) { 150 t = t.replace(c, '_'); 151 } 152 // collapse runs of whitespace into single space 153 t = t.replaceAll("\\s+", " ").trim(); 154 // prevent names that are just dots 155 if (t.matches("^[.]+$")) { 156 t = "_"; 157 } 158 return t; 159 } 160 161 /** 162 * Public safe name helper for filesystem paths. Mirrors the internal 163 * sanitize implementation but is callable from other packages. 164 * 165 * @param s input display name 166 * @return sanitized filesystem-safe name (never null) 167 */ 168 public static String safeName(final String s) { 169 if (s == null) { 170 return ""; 171 } 172 return sanitize(s); 173 } 174 175 /** 176 * Find the latest PNG plot file for a named student with the given prefix. 177 * Returns null when no matching files exist. 178 * 179 * @param studentName display name of student 180 * @param prefix file prefix such as "iOS" or "ScreenReader" 181 * @return path to the most recently modified matching PNG, or null 182 */ 183 public static java.nio.file.Path latestPlotPath(final String studentName, final String prefix) { 184 if (studentName == null || studentName.trim().isEmpty()) { 185 return null; 186 } 187 java.nio.file.Path dir = studentPlotsDir(studentName); 188 if (!java.nio.file.Files.exists(dir)) { 189 return null; 190 } 191 java.nio.file.Path latest = null; 192 try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(dir, prefix + "-*.png")) { 193 for (java.nio.file.Path p : ds) { 194 try { 195 if (latest == null) { 196 latest = p; 197 } else { 198 java.nio.file.attribute.FileTime t1 = java.nio.file.Files.getLastModifiedTime(p); 199 java.nio.file.attribute.FileTime t2 = java.nio.file.Files.getLastModifiedTime(latest); 200 if (t1.compareTo(t2) > 0) { 201 latest = p; 202 } 203 } 204 } catch (IOException ioe) { 205 LOG.debug("Error reading file metadata for {}", p, ioe); 206 } 207 } 208 } catch (IOException ioe) { 209 LOG.debug("Error listing plot directory {}", dir, ioe); 210 } 211 return latest; 212 } 213 214 /** 215 * Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots). 216 * 217 * @param studentName display name of the student 218 * @return path to the student's plots directory (never null) 219 */ 220 public static java.nio.file.Path studentPlotsDir(final String studentName) { 221 return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("plots"); 222 } 223 224 /** 225 * Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports). 226 * 227 * @param studentName display name of the student 228 * @return path to the student's reports directory (never null) 229 */ 230 public static java.nio.file.Path studentReportsDir(final String studentName) { 231 return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("reports"); 232 } 233 234 /** 235 * Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data). 236 * 237 * @param studentName display name of the student 238 * @return path to the student's collected data directory (never null) 239 */ 240 public static java.nio.file.Path studentCollectedDataDir(final String studentName) { 241 return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("collected_data"); 242 } 243 244 /** 245 * Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json. 246 * Falls back to a single 'Test Student' entry when the file is missing or cannot be read. 247 * 248 * @return list of student display names (never null) 249 */ 250 public static List<String> getStudents() { 251 // Attempt to read a simple students.json in PROJECT_ROOT/json_Files/students.json 252 List<String> list = new ArrayList<>(); 253 Path p = PROJECT_ROOT.resolve("json_Files").resolve("students.json"); 254 if (Files.exists(p)) { 255 try { 256 String text = Files.readString(p); 257 // try to isolate the array portion if present 258 int start = text.indexOf('['); 259 int end = text.lastIndexOf(']'); 260 String body = (start >= 0 && end > start) ? text.substring(start, end + 1) : text; 261 java.util.regex.Pattern pat = java.util.regex.Pattern.compile("\"([^\"]+)\""); 262 java.util.regex.Matcher m = pat.matcher(body); 263 while (m.find()) { 264 String candidate = m.group(1).trim(); 265 if (!candidate.isEmpty()) { 266 list.add(candidate); 267 } 268 } 269 } catch (IOException ioe) { 270 LOG.debug("Unable to read students.json {}", p, ioe); 271 } 272 } 273 if (list.isEmpty()) { 274 // fallback roster 275 list.add("Test Student"); 276 } 277 return list; 278 } 279 280 /** 281 * Return the default student to use when none is provided by the caller. 282 * This is the first entry from getStudents() or a sensible fallback when 283 * the roster is empty. 284 * 285 * @return display name of the default student (never null) 286 */ 287 public static String defaultStudent() { 288 /** 289 * Note: UI pages use this helper to provide a non-null default student 290 * when constructed with a null/empty student name so that charts and 291 * page logic can operate without requiring an immediate user selection. 292 */ 293 List<String> s = getStudents(); 294 if (s == null || s.isEmpty()) { 295 return "Demo Student"; 296 } 297 return s.get(0); 298 } 299}