11package app ;
22
3+ import gvisual .ExportUtils ;
34import java .io .BufferedWriter ;
45import java .io .File ;
56import java .io .FileWriter ;
@@ -82,12 +83,7 @@ public static void generateFile(String path, String Month, String Date, int dThr
8283
8384 // Validate output path - prevent directory traversal attacks
8485 File outputFile = new File (path ).getCanonicalFile ();
85- File workingDir = new File ("." ).getCanonicalFile ();
86- if (!outputFile .toPath ().startsWith (workingDir .toPath ())) {
87- throw new SecurityException (
88- "Output path must be within the working directory. "
89- + "Resolved path: " + outputFile .getAbsolutePath ());
90- }
86+ ExportUtils .validateOutputPath (outputFile );
9187
9288 System .out .println ("connecting..." );
9389
@@ -100,63 +96,26 @@ public static void generateFile(String path, String Month, String Date, int dThr
10096 // "location = 'public'" filter silently dropped long meetings at
10197 // cafés, libraries, paths, etc., under-counting friend edges.
10298 // See: https://github.com/sauravbhattacharya001/GraphVisual/issues/134
103- String friendSql = " SELECT x.id , y.id , C , d "
104- + " FROM ( SELECT imei1 , imei2, count(*) as C,avg(duration) as d"
105- + " FROM ( SELECT imei1, imei2, duration"
106- + " FROM meeting"
107- + " WHERE month = ? AND date = ? AND location NOT IN ('class', 'unknown', '') AND duration > ?) as b"
108- + " GROUP BY imei1, imei2) as a, deviceID as x, deviceID as y"
109- + " WHERE C >= ? AND a.imei1= x.imei AND a.imei2 = y.imei" ;
110-
111- // --- Study groups query ---
112- String studygSql = " SELECT x.id , y.id , C , d "
113- + " FROM ( SELECT imei1, imei2, count(*) as C,avg(duration) as d"
114- + " FROM ( SELECT imei1, imei2, duration"
115- + " FROM meeting"
116- + " WHERE month = ? AND date = ? AND location= 'class' AND duration > ?) as b"
117- + " GROUP BY imei1, imei2) as a, deviceID as x, deviceID as y"
118- + " WHERE C <= ? AND a.imei1= x.imei AND a.imei2 = y.imei" ;
119-
120- // --- Classmates query ---
121- String cmateSql = " SELECT x.id , y.id, C , d "
122- + " FROM ( SELECT imei1, imei2, count(*) as C, avg(duration) as d"
123- + " FROM ( SELECT imei1, imei2, duration"
124- + " FROM meeting"
125- + " WHERE month = ? AND date = ? AND location= 'class' AND duration > ?) as b"
126- + " GROUP BY imei1, imei2) as a, deviceID as x, deviceID as y"
127- + " WHERE C >= ? AND a.imei1= x.imei AND a.imei2 = y.imei" ;
128-
129- // --- Strangers query ---
130- // Exclude both 'class' and 'unknown' locations so only meetings with
131- // a resolved location (e.g. 'public', 'path') are considered.
132- String strangerSql = " SELECT x.id , y.id , C , d "
133- + " FROM ( SELECT imei1, imei2, count(*) as C,avg(duration) as d"
134- + " FROM ( SELECT imei1, imei2, duration"
135- + " FROM meeting"
136- + " WHERE month = ? AND date = ? AND location NOT IN ('class', 'unknown', '') AND duration < ?) as b"
137- + " GROUP BY imei1, imei2) as a, deviceID as x, deviceID as y"
138- + " WHERE C < ? AND a.imei1= x.imei AND a.imei2 = y.imei" ;
139-
140- // --- Familiar strangers query ---
141- String famstrangerSql = " SELECT x.id , y.id , C , d "
142- + " FROM ( SELECT imei1, imei2, count(*) as C, avg(duration) as d"
143- + " FROM ( SELECT imei1, imei2, duration"
144- + " FROM meeting"
145- + " WHERE month = ? AND date = ? AND location NOT IN ('class', 'unknown', '') AND duration < ?) as b"
146- + " GROUP BY imei1, imei2) as a , deviceID as x, deviceID as y"
147- + " WHERE C > ? AND a.imei1= x.imei AND a.imei2 = y.imei" ;
14899
149100 try (Connection conn = Util .getAppConnection ()) {
150101
151102 // Use StringBuilder instead of String concatenation for performance
152103 StringBuilder sb = new StringBuilder ("edges" );
153104
154- // Execute each relationship query using the shared helper
155- appendEdges (conn , sb , friendSql , "f" , Month , Date , dThresF , CThresF );
156- appendEdges (conn , sb , studygSql , "sg" , Month , Date , dThresSg , CThresSg );
157- appendEdges (conn , sb , cmateSql , "c" , Month , Date , dThresC , CThresC );
158- appendEdges (conn , sb , strangerSql , "s" , Month , Date , dThresS , CThresS );
159- appendEdges (conn , sb , famstrangerSql , "fs" , Month , Date , dThresFS , CThresFS );
105+ // All five relationship queries share the same structure, differing
106+ // only in location filter, duration comparison, and count comparison.
107+ // buildMeetingSql() generates the parameterized SQL from these axes,
108+ // eliminating the duplicated query strings.
109+ appendEdges (conn , sb , buildMeetingSql ("NOT IN ('class', 'unknown', '')" , ">" , ">=" ),
110+ "f" , Month , Date , dThresF , CThresF );
111+ appendEdges (conn , sb , buildMeetingSql ("= 'class'" , ">" , "<=" ),
112+ "sg" , Month , Date , dThresSg , CThresSg );
113+ appendEdges (conn , sb , buildMeetingSql ("= 'class'" , ">" , ">=" ),
114+ "c" , Month , Date , dThresC , CThresC );
115+ appendEdges (conn , sb , buildMeetingSql ("NOT IN ('class', 'unknown', '')" , "<" , "<" ),
116+ "s" , Month , Date , dThresS , CThresS );
117+ appendEdges (conn , sb , buildMeetingSql ("NOT IN ('class', 'unknown', '')" , "<" , ">" ),
118+ "fs" , Month , Date , dThresFS , CThresFS );
160119
161120 // Write output file - use validated outputFile, not raw path
162121 if (outputFile .exists ()) {
@@ -168,6 +127,27 @@ public static void generateFile(String path, String Month, String Date, int dThr
168127 }
169128 }
170129
130+ /**
131+ * Builds a parameterized meeting SQL query from the three axes that
132+ * vary between relationship types: location filter, duration comparison
133+ * operator, and count comparison operator.
134+ *
135+ * @param locationFilter SQL fragment for location (e.g. {@code "= 'class'"})
136+ * @param durationOp comparison operator for duration threshold
137+ * @param countOp comparison operator for count threshold
138+ * @return parameterized SQL with 4 placeholders: month, date, duration, count
139+ */
140+ private static String buildMeetingSql (String locationFilter , String durationOp , String countOp ) {
141+ return "SELECT x.id, y.id, C, d"
142+ + " FROM (SELECT imei1, imei2, count(*) AS C, avg(duration) AS d"
143+ + " FROM (SELECT imei1, imei2, duration"
144+ + " FROM meeting"
145+ + " WHERE month = ? AND date = ? AND location " + locationFilter
146+ + " AND duration " + durationOp + " ?) AS b"
147+ + " GROUP BY imei1, imei2) AS a, deviceID AS x, deviceID AS y"
148+ + " WHERE C " + countOp + " ? AND a.imei1 = x.imei AND a.imei2 = y.imei" ;
149+ }
150+
171151 /**
172152 * Executes a parameterized meeting query and appends edges to the output buffer.
173153 *
0 commit comments