|
| 1 | +package app; |
| 2 | + |
| 3 | +import java.sql.Connection; |
| 4 | +import java.sql.PreparedStatement; |
| 5 | +import java.sql.ResultSet; |
| 6 | +import java.util.Collections; |
| 7 | +import java.util.HashMap; |
| 8 | +import java.util.Map; |
| 9 | + |
| 10 | +/** |
| 11 | + * Resolves meeting locations by cross-referencing WiFi access point data. |
| 12 | + * |
| 13 | + * <p>For each meeting record in the database, queries the trace/event tables |
| 14 | + * to find the strongest common access point shared by the two participants |
| 15 | + * during the meeting's time window. If no common AP exists, falls back to |
| 16 | + * the strongest AP observed by either participant individually.</p> |
| 17 | + * |
| 18 | + * <p>Replaces the original {@code addLocation} class with:</p> |
| 19 | + * <ul> |
| 20 | + * <li>Java naming conventions (PascalCase class name)</li> |
| 21 | + * <li>AP-to-location mapping extracted to a configurable {@code Map} |
| 22 | + * instead of a hard-coded switch statement</li> |
| 23 | + * <li>Duplicated fallback query logic extracted to {@link #findBestAP}</li> |
| 24 | + * </ul> |
| 25 | + * |
| 26 | + * @author zalenix |
| 27 | + */ |
| 28 | +public class LocationResolver { |
| 29 | + |
| 30 | + /** |
| 31 | + * Access-point ID → location type mapping. |
| 32 | + * <ul> |
| 33 | + * <li>"public" — common areas (cafeterias, lounges)</li> |
| 34 | + * <li>"class" — classrooms</li> |
| 35 | + * <li>Unmapped APs default to "path" (corridors, transit areas)</li> |
| 36 | + * <li>AP 0 (not found) maps to "" (unknown)</li> |
| 37 | + * </ul> |
| 38 | + */ |
| 39 | + private static final Map<Integer, String> AP_LOCATION_MAP; |
| 40 | + static { |
| 41 | + Map<Integer, String> m = new HashMap<>(); |
| 42 | + // Public-area access points |
| 43 | + for (int ap :/*** ap ids ***/ new int[]{7, 16, 20, 35, 38, 39}) { |
| 44 | + m.put(ap, "public"); |
| 45 | + } |
| 46 | + // Classroom access points |
| 47 | + for (int ap : new int[]{29, 30, 31, 32, 33, 34, 36}) { |
| 48 | + m.put(ap, "class"); |
| 49 | + } |
| 50 | + AP_LOCATION_MAP = Collections.unmodifiableMap(m); |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Maps an access-point ID to its location type. |
| 55 | + * |
| 56 | + * @param ap the access-point ID (0 = not found) |
| 57 | + * @return location type: "public", "class", "path", or "" (unknown) |
| 58 | + */ |
| 59 | + static String classifyAP(int ap) { |
| 60 | + if (ap == 0) return ""; |
| 61 | + return AP_LOCATION_MAP.getOrDefault(ap, "path"); |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * Validates that a time component string matches the expected |
| 66 | + * format of "HH.MM:SS.mmm" (two dot-separated parts on each |
| 67 | + * side of the colon). |
| 68 | + * |
| 69 | + * @param month the month string |
| 70 | + * @param date the date string |
| 71 | + * @param time the raw time string from the database |
| 72 | + * @return formatted timestamp string "2011-MM-DD HH:MM:SS.mmm" |
| 73 | + * @throws IllegalArgumentException if the format is invalid |
| 74 | + */ |
| 75 | + public static String getTimeStamp(String month, String date, String time) { |
| 76 | + if (time == null || time.isEmpty()) { |
| 77 | + throw new IllegalArgumentException("Time string must not be null or empty"); |
| 78 | + } |
| 79 | + String[] timeArr = time.split(":"); |
| 80 | + if (timeArr.length != 2) { |
| 81 | + throw new IllegalArgumentException( |
| 82 | + "Invalid time format (expected one colon separator): " + time); |
| 83 | + } |
| 84 | + String[] timeArr1 = timeArr[0].split("\\."); |
| 85 | + String[] timeArr2 = timeArr[1].split("\\."); |
| 86 | + if (timeArr1.length < 2 || timeArr2.length < 2) { |
| 87 | + throw new IllegalArgumentException( |
| 88 | + "Invalid time format (expected dot-separated components): " + time); |
| 89 | + } |
| 90 | + |
| 91 | + return "2011-" + month + "-" + date + " " |
| 92 | + + timeArr1[0] + ":" + timeArr1[1] + ":" |
| 93 | + + timeArr2[0] + "." + timeArr2[1]; |
| 94 | + } |
| 95 | + |
| 96 | + // SQL: find common access points between two IMEIs in a time window |
| 97 | + private static final String COMMON_AP_SQL = |
| 98 | + "SELECT * FROM (" |
| 99 | + + "SELECT DISTINCT ap, ssi FROM event AS a, trace AS b " |
| 100 | + + "WHERE a.trace = b.id AND imei = ? AND timestamp >= ? AND timestamp <= ? " |
| 101 | + + "INTERSECT " |
| 102 | + + "SELECT DISTINCT ap, ssi FROM event AS a, trace AS b " |
| 103 | + + "WHERE a.trace = b.id AND imei = ? AND timestamp >= ? AND timestamp <= ?" |
| 104 | + + ") AS d ORDER BY ssi DESC LIMIT 1"; |
| 105 | + |
| 106 | + // SQL: find access points for a single IMEI in a time window |
| 107 | + private static final String SINGLE_AP_SQL = |
| 108 | + "SELECT * FROM (" |
| 109 | + + "SELECT DISTINCT ap, ssi FROM event AS a, trace AS b " |
| 110 | + + "WHERE a.trace = b.id AND imei = ? AND timestamp >= ? AND timestamp <= ?" |
| 111 | + + ") AS d ORDER BY ssi DESC LIMIT 1"; |
| 112 | + |
| 113 | + // SQL: update the meeting location |
| 114 | + private static final String UPDATE_LOCATION_SQL = |
| 115 | + "UPDATE meeting SET location = ? " |
| 116 | + + "WHERE imei1 = ? AND imei2 = ? AND starttime = ? AND endtime = ? " |
| 117 | + + "AND month = ? AND date = ?"; |
| 118 | + |
| 119 | + /** |
| 120 | + * Queries for the strongest AP observed by a single IMEI in a time window. |
| 121 | + * |
| 122 | + * @param ps prepared statement for {@link #SINGLE_AP_SQL} |
| 123 | + * @param imei the device IMEI |
| 124 | + * @param startStamp window start timestamp |
| 125 | + * @param endStamp window end timestamp |
| 126 | + * @return AP id, or 0 if none found |
| 127 | + */ |
| 128 | + private static int findBestAP(PreparedStatement ps, String imei, |
| 129 | + String startStamp, String endStamp) |
| 130 | + throws Exception { |
| 131 | + ps.setString(1, imei); |
| 132 | + ps.setString(2, startStamp); |
| 133 | + ps.setString(3, endStamp); |
| 134 | + try (ResultSet rs = ps.executeQuery()) { |
| 135 | + if (rs.next()) { |
| 136 | + return rs.getInt("ap"); |
| 137 | + } |
| 138 | + } |
| 139 | + return 0; |
| 140 | + } |
| 141 | + |
| 142 | + public static void main(String[] argv) throws Exception { |
| 143 | + try (Connection azialaConn = Util.getAzialaConnection(); |
| 144 | + Connection appConn = Util.getAppConnection(); |
| 145 | + java.sql.Statement appStmt = appConn.createStatement( |
| 146 | + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); |
| 147 | + PreparedStatement commonApPs = azialaConn.prepareStatement(COMMON_AP_SQL, |
| 148 | + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); |
| 149 | + PreparedStatement singleApPs = azialaConn.prepareStatement(SINGLE_AP_SQL, |
| 150 | + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); |
| 151 | + PreparedStatement updatePs = appConn.prepareStatement(UPDATE_LOCATION_SQL)) { |
| 152 | + |
| 153 | + String query = "select * from meeting"; |
| 154 | + ResultSet meetings = appStmt.executeQuery(query); |
| 155 | + |
| 156 | + int count = 0; |
| 157 | + while (meetings.next()) { |
| 158 | + count++; |
| 159 | + System.out.println("finding location for meeting # " + count); |
| 160 | + String imei1 = meetings.getString("imei1"); |
| 161 | + String imei2 = meetings.getString("imei2"); |
| 162 | + |
| 163 | + String startTime = meetings.getString("starttime"); |
| 164 | + String endTime = meetings.getString("endtime"); |
| 165 | + String month = meetings.getString("month"); |
| 166 | + String date = meetings.getString("date"); |
| 167 | + String startTimeStamp = getTimeStamp(month, date, startTime); |
| 168 | + String endTimeStamp = getTimeStamp(month, date, endTime); |
| 169 | + |
| 170 | + // Try intersection of both IMEIs first |
| 171 | + commonApPs.setString(1, imei1); |
| 172 | + commonApPs.setString(2, startTimeStamp); |
| 173 | + commonApPs.setString(3, endTimeStamp); |
| 174 | + commonApPs.setString(4, imei2); |
| 175 | + commonApPs.setString(5, startTimeStamp); |
| 176 | + commonApPs.setString(6, endTimeStamp); |
| 177 | + |
| 178 | + int ap = 0; |
| 179 | + try (ResultSet rs = commonApPs.executeQuery()) { |
| 180 | + if (rs.next()) { |
| 181 | + ap = rs.getInt("ap"); |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + // Fallback: try each IMEI individually |
| 186 | + if (ap == 0) { |
| 187 | + ap = findBestAP(singleApPs, imei1, startTimeStamp, endTimeStamp); |
| 188 | + } |
| 189 | + if (ap == 0) { |
| 190 | + ap = findBestAP(singleApPs, imei2, startTimeStamp, endTimeStamp); |
| 191 | + } |
| 192 | + |
| 193 | + String apType = classifyAP(ap); |
| 194 | + System.out.println("common access point # " + ap + " → " + apType); |
| 195 | + |
| 196 | + updatePs.setString(1, apType); |
| 197 | + updatePs.setString(2, imei1); |
| 198 | + updatePs.setString(3, imei2); |
| 199 | + updatePs.setString(4, startTime); |
| 200 | + updatePs.setString(5, endTime); |
| 201 | + updatePs.setString(6, month); |
| 202 | + updatePs.setString(7, date); |
| 203 | + updatePs.executeUpdate(); |
| 204 | + } |
| 205 | + } |
| 206 | + } |
| 207 | +} |
0 commit comments