[update] Remove DebugFileWalk class and enhance CheckUpPage with image deletion functionality. Implement ping/pong mechanism for connection stability in ClientHandler and Server, and improve timeout management with IdleStateHandler adjustments. Update binary files for database and enhance Google Drive integration for image deletion.

This commit is contained in:
Lê Thành Đạt committed 2025-10-13 02:49:03 -04:00
1 parent 700c90733b
commit ccfc9eaaed
17 files changed
+368 -87

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

-51
View File
@@ -1,51 +0,0 @@
package BsK.client.ui.component.CheckUpPage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class DebugFileWalk {
public static void main(String[] args) {
// ▼▼▼ IMPORTANT ▼▼▼
// Change this path to the EXACT folder you are trying to scan.
// Use double backslashes "\\" for Windows paths.
// Example: "C:\\Users\\YourName\\Pictures\\MyEvent"
Path mediaPath = Paths.get("ANH SIEU AM");
// ▲▲▲ IMPORTANT ▲▲▲
System.out.println("--- Starting Scan ---");
System.out.println("Scanning Path: " + mediaPath.toAbsolutePath());
System.out.println("Does path exist? " + Files.exists(mediaPath));
System.out.println("--------------------------------\n");
System.out.println(">>> ALL files and folders found by Files.walk():");
try (Stream<Path> pathStream = Files.walk(mediaPath)) {
pathStream.forEach(path -> {
System.out.println(" -> Found: " + path);
});
} catch (IOException e) {
System.err.println("An ERROR occurred during the scan:");
e.printStackTrace();
}
System.out.println("\n--------------------------------");
System.out.println(">>> Files that match the '.jpg' filter:");
try (Stream<Path> pathStream = Files.walk(mediaPath)) {
pathStream
.filter(path -> !Files.isDirectory(path))
.filter(path -> path.toString().toLowerCase().endsWith(".jpg"))
.forEach(path -> {
System.out.println(" -> Matched: " + path);
});
} catch (IOException e) {
System.err.println("An ERROR occurred during the filtered scan:");
e.printStackTrace();
}
System.out.println("\n--- Scan Finished ---");
}
}
+4 -3
View File
@@ -189,9 +189,10 @@ public class Client {
true,
new DefaultHttpHeaders(),
50 * 1024 * 1024))
// Add IdleStateHandler with longer timeouts for network resilience
// Read timeout: 60 seconds, Write timeout: 30 seconds
.addLast(new IdleStateHandler(60, 30, 0))
// Add IdleStateHandler for keep-alive and timeout detection
// Read timeout: 5 minutes (300s) - disconnect if no data from server
// Write timeout: 10 seconds - send ping if no data sent to server
.addLast(new IdleStateHandler(300, 10, 0))
.addLast("ws", ClientHandler.INSTANCE);
}
});
@@ -6,7 +6,9 @@ import BsK.client.ui.handler.UIHandler;
import BsK.common.entity.DoctorItem;
import BsK.common.packet.Packet;
import BsK.common.packet.PacketSerializer;
import BsK.common.packet.req.PingRequest;
import BsK.common.packet.res.*;
import BsK.common.util.network.NetworkUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.*;
@@ -35,6 +37,12 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
private static final long NETWORK_ISSUE_DIALOG_COOLDOWN = 30000; // 30 seconds
private static volatile boolean dialogCurrentlyShowing = false;
// Ping-pong tracking
private static volatile int missedPongCount = 0;
private static final int MAX_MISSED_PONGS = 3;
private static volatile long lastPingRequestTime = 0;
private static volatile boolean awaitingPongResponse = false;
// Disconnection type tracking
public enum DisconnectionType {
TIMEOUT, // Server not responding (READER_IDLE)
@@ -135,6 +143,19 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
LocalStorage.provinceToId = provinceResponse.getProvinceToId();
return;
}
// Handle PongResponse
if (packet instanceof PongResponse pongResponse) {
long roundTripTime = System.currentTimeMillis() - pongResponse.getTimestamp();
log.debug("Received PongResponse (round trip: {}ms)", roundTripTime);
// Reset missed pong counter on successful pong
missedPongCount = 0;
awaitingPongResponse = false;
wasTimeoutDetected = false;
return;
}
// Dispatch to registered listeners
List<ResponseListener<?>> responseListeners = listeners.get(packet.getClass());
@@ -156,33 +177,40 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
// Send ping to keep connection alive
log.debug("Sending ping to server");
lastPingTime = System.currentTimeMillis();
awaitingPong = true;
ctx.writeAndFlush(new PingWebSocketFrame());
// Send PingRequest packet to keep connection alive
if (awaitingPongResponse) {
// Previous ping not answered - increment missed counter
missedPongCount++;
log.warn("Missed pong #{} - no response to previous ping", missedPongCount);
if (missedPongCount >= MAX_MISSED_PONGS) {
log.error("Missed {} consecutive pongs - disconnecting", missedPongCount);
wasTimeoutDetected = true;
lastDisconnectionType = DisconnectionType.TIMEOUT;
ctx.close(); // Force disconnect
return;
}
}
// Send new ping packet
log.debug("Sending PingRequest to server (missed count: {})", missedPongCount);
lastPingRequestTime = System.currentTimeMillis();
awaitingPongResponse = true;
PingRequest pingRequest = new PingRequest();
NetworkUtil.sendPacket(ctx.channel(), pingRequest);
} else if (event.state() == IdleState.READER_IDLE) {
// Server hasn't responded in a while - this is a timeout scenario
log.warn("Server not responding - timeout detected");
// Server hasn't sent any data (including pong) for 5 minutes
log.warn("Server not responding - no data received for 5 minutes (READER_IDLE)");
wasTimeoutDetected = true;
lastDisconnectionType = DisconnectionType.TIMEOUT;
// Just set the flag, don't show dialog yet - let channelInactive handle it
// Try to send a ping to test connection
lastPingTime = System.currentTimeMillis();
awaitingPong = true;
ctx.writeAndFlush(new PingWebSocketFrame());
}
} else if (evt instanceof WebSocketFrame) {
// Handle pong responses
if (evt instanceof PongWebSocketFrame) {
lastPongTime = System.currentTimeMillis();
long pingRoundTrip = lastPongTime - lastPingTime;
log.debug("Received pong from server (round trip: {}ms)", pingRoundTrip);
awaitingPong = false;
// Reset timeout flag since we got a response
wasTimeoutDetected = false;
// Let channelInactive handle the disconnect
}
}
// IMPORTANT: Call super to ensure event propagation continues
super.userEventTriggered(ctx, evt);
}
@Override
@@ -223,10 +251,10 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// Determine disconnection type if not already set
if (lastDisconnectionType == DisconnectionType.UNKNOWN) {
if (wasTimeoutDetected) {
if (wasTimeoutDetected || missedPongCount >= MAX_MISSED_PONGS) {
lastDisconnectionType = DisconnectionType.TIMEOUT;
log.info("Channel inactive due to timeout");
} else if (awaitingPong && (System.currentTimeMillis() - lastPingTime) > 60000) {
log.info("Channel inactive due to timeout (missed {} pongs)", missedPongCount);
} else if (awaitingPongResponse && (System.currentTimeMillis() - lastPingRequestTime) > 30000) {
lastDisconnectionType = DisconnectionType.TIMEOUT;
log.info("Channel inactive - server stopped responding to pings");
} else {
@@ -262,6 +290,9 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
awaitingPong = false;
lastPingTime = 0;
lastPongTime = 0;
missedPongCount = 0;
awaitingPongResponse = false;
lastPingRequestTime = 0;
}
/**
@@ -186,6 +186,7 @@ public class CheckUpPage extends JPanel {
private final ResponseListener<UploadCheckupPdfResponse> uploadPdfResponseListener = this::handleUploadPdfResponse;
private final ResponseListener<SyncCheckupImagesResponse> syncImagesResponseListener = this::handleSyncImagesResponse;
private final ResponseListener<GetCheckupImageResponse> getImageResponseListener = this::handleGetCheckupImageResponse;
private final ResponseListener<DeleteCheckupImageResponse> deleteImageResponseListener = this::handleDeleteImageResponse;
private JTextField checkupIdField, customerLastNameField, customerFirstNameField,customerAddressField, customerPhoneField, customerIdField, customerCccdDdcnField;
private JTextArea suggestionField, diagnosisField, conclusionField; // Changed symptomsField to suggestionField
private JTextPane notesField;
@@ -365,6 +366,7 @@ public class CheckUpPage extends JPanel {
ClientHandler.addResponseListener(UploadCheckupPdfResponse.class, uploadPdfResponseListener);
ClientHandler.addResponseListener(SyncCheckupImagesResponse.class, syncImagesResponseListener);
ClientHandler.addResponseListener(GetCheckupImageResponse.class, getImageResponseListener);
ClientHandler.addResponseListener(DeleteCheckupImageResponse.class, deleteImageResponseListener);
// Instantiate the new queue page but don't show it yet
queueManagementPage = new QueueManagementPage();
@@ -3652,6 +3654,45 @@ public class CheckUpPage extends JPanel {
});
}
private void deleteImage(File imageFile) {
if (imageFile == null || !imageFile.exists()) {
log.warn("Cannot delete image - file does not exist or is null");
return;
}
String fileName = imageFile.getName();
String checkupId = currentCheckupIdForMedia;
if (checkupId == null || checkupId.trim().isEmpty()) {
log.error("Cannot delete image - no checkup ID available");
JOptionPane.showMessageDialog(this,
"Không thể xóa ảnh - không có mã khám.",
"Lỗi",
JOptionPane.ERROR_MESSAGE);
return;
}
// Send delete request to server
DeleteCheckupImageRequest request = new DeleteCheckupImageRequest(checkupId, fileName);
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request);
log.info("Sent DeleteCheckupImageRequest for file: {} in checkup: {}", fileName, checkupId);
// Delete local file immediately
try {
if (imageFile.delete()) {
log.info("Successfully deleted local image file: {}", fileName);
// Refresh the gallery
if (currentCheckupMediaPath != null && Files.exists(currentCheckupMediaPath)) {
loadAndDisplayImages(currentCheckupMediaPath);
}
} else {
log.warn("Failed to delete local image file: {}", fileName);
}
} catch (Exception e) {
log.error("Error deleting local image file: {}", fileName, e);
}
}
private void handleUploadImageResponse(UploadCheckupImageResponse response) {
String fileName = response.getFileName();
@@ -3672,6 +3713,29 @@ public class CheckUpPage extends JPanel {
}
}
private void handleDeleteImageResponse(DeleteCheckupImageResponse response) {
String fileName = response.getFileName();
if (response.isSuccess()) {
log.info("Successfully deleted image from server: {} - {}", fileName, response.getMessage());
SwingUtilities.invokeLater(() -> {
// Image already deleted locally, just log success
if (callingStatusLabel != null) {
callingStatusLabel.setText("Đã xóa ảnh: " + fileName);
callingStatusLabel.setForeground(new Color(0, 150, 0)); // Green color
}
});
} else {
log.error("Failed to delete image from server: {} - {}", fileName, response.getMessage());
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this,
"Lỗi khi xóa ảnh trên server: " + response.getMessage(),
"Lỗi Xóa Ảnh",
JOptionPane.ERROR_MESSAGE);
});
}
}
private void handleUploadPdfResponse(UploadCheckupPdfResponse response) {
if (response.isSuccess()) {
log.info("Successfully uploaded PDF: {} - {}", response.getPdfType(), response.getMessage());
@@ -4091,11 +4155,33 @@ public class CheckUpPage extends JPanel {
layeredPane.setBorder(BorderFactory.createLineBorder(Color.GRAY));
layeredPane.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Set the hand cursor
// Create popup menu for right-click
JPopupMenu imagePopupMenu = new JPopupMenu();
JMenuItem deleteMenuItem = new JMenuItem("Xoá");
deleteMenuItem.setIcon(UIManager.getIcon("FileView.fileIcon")); // Optional icon
deleteMenuItem.addActionListener(ev -> {
int confirm = JOptionPane.showConfirmDialog(
CheckUpPage.this,
"Bạn có chắc chắn muốn xoá ảnh này không?\n" + imageFile.getName(),
"Xác nhận xoá",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (confirm == JOptionPane.YES_OPTION) {
deleteImage(imageFile);
}
});
imagePopupMenu.add(deleteMenuItem);
// Add the click listener directly to the image label
imageLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showFullImageDialog(imageFile);
if (SwingUtilities.isRightMouseButton(e)) {
imagePopupMenu.show(e.getComponent(), e.getX(), e.getY());
} else if (SwingUtilities.isLeftMouseButton(e)) {
showFullImageDialog(imageFile);
}
}
});
@@ -4300,6 +4386,20 @@ public class CheckUpPage extends JPanel {
TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(queueTableModel);
queueTable.setRowSorter(sorter);
// Set custom comparator for STT column to sort as integers
sorter.setComparator(0, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
try {
Integer i1 = Integer.parseInt(s1);
Integer i2 = Integer.parseInt(s2);
return i1.compareTo(i2);
} catch (NumberFormatException e) {
return s1.compareTo(s2); // Fallback to string comparison
}
}
});
// Sort by "STT" (queue number) column in ascending order
List<RowSorter.SortKey> sortKeys = new ArrayList<>();
sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
@@ -0,0 +1,16 @@
package BsK.common.packet.req;
import BsK.common.packet.Packet;
import lombok.Data;
@Data
public class DeleteCheckupImageRequest implements Packet {
private String checkupId;
private String fileName;
public DeleteCheckupImageRequest(String checkupId, String fileName) {
this.checkupId = checkupId;
this.fileName = fileName;
}
}
@@ -1,9 +1,13 @@
package BsK.common.packet.req;
import BsK.common.packet.Packet;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class PingRequest implements Packet {}
public class PingRequest implements Packet {
private long timestamp;
public PingRequest() {
this.timestamp = System.currentTimeMillis();
}
}
@@ -0,0 +1,18 @@
package BsK.common.packet.res;
import BsK.common.packet.Packet;
import lombok.Data;
@Data
public class DeleteCheckupImageResponse implements Packet {
private boolean success;
private String message;
private String fileName;
public DeleteCheckupImageResponse(boolean success, String message, String fileName) {
this.success = success;
this.message = message;
this.fileName = fileName;
}
}
@@ -0,0 +1,18 @@
package BsK.common.packet.res;
import BsK.common.packet.Packet;
import lombok.Data;
@Data
public class PongResponse implements Packet {
private long timestamp;
public PongResponse() {
this.timestamp = System.currentTimeMillis();
}
public PongResponse(long clientTimestamp) {
this.timestamp = clientTimestamp;
}
}
+4 -3
View File
@@ -294,9 +294,10 @@ public class Server {
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(50 * 1024 * 1024))
.addLast(new WebSocketServerProtocolHandler("/", null, true, 50 * 1024 * 1024))
// Add IdleStateHandler with longer timeout to be more tolerant of network issues
// Reader timeout: 90 seconds (longer than client's 60s to allow for network delays)
.addLast(new IdleStateHandler(90, 0, 0))
// Add IdleStateHandler to detect inactive clients
// Reader timeout: 40 seconds - disconnect if no data from client (ping should arrive every 10s)
// Allow up to 4 missed pings (10s * 4 = 40s) before disconnecting
.addLast(new IdleStateHandler(40, 0, 0))
.addLast(new ServerHandler());
}
});
@@ -1217,6 +1217,59 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
}
}
if (packet instanceof DeleteCheckupImageRequest deleteCheckupImageRequest) {
log.info("Received DeleteCheckupImageRequest for checkupId: {}, fileName: {}",
deleteCheckupImageRequest.getCheckupId(), deleteCheckupImageRequest.getFileName());
String checkupId = deleteCheckupImageRequest.getCheckupId();
String fileName = deleteCheckupImageRequest.getFileName();
if (checkupId == null || checkupId.trim().isEmpty()) {
UserUtil.sendPacket(currentUser.getSessionId(),
new DeleteCheckupImageResponse(false, "CheckupID không hợp lệ.", fileName));
return;
}
if (fileName == null || fileName.trim().isEmpty()) {
UserUtil.sendPacket(currentUser.getSessionId(),
new DeleteCheckupImageResponse(false, "Tên file không hợp lệ.", fileName));
return;
}
try {
// 1. Define the local file path
Path storageDir = Paths.get(Server.imageDbPath, checkupId.trim());
Path filePath = storageDir.resolve(fileName);
// 2. Delete the local file if it exists
boolean localDeleted = false;
if (Files.exists(filePath)) {
Files.delete(filePath);
localDeleted = true;
log.info("Successfully deleted local image: {}", filePath);
} else {
log.warn("Local file not found: {}", filePath);
}
// 3. Send success response to client immediately
UserUtil.sendPacket(currentUser.getSessionId(),
new DeleteCheckupImageResponse(true, "Xóa ảnh thành công.", fileName));
// 4. Asynchronously delete from Google Drive
deleteCheckupImageFromGoogleDriveAsync(checkupId, fileName);
} catch (IOException e) {
log.error("Failed to delete image for checkupId {}: {}", checkupId, e.getMessage());
UserUtil.sendPacket(currentUser.getSessionId(),
new DeleteCheckupImageResponse(false, "Lỗi máy chủ khi xóa ảnh: " + e.getMessage(), fileName));
} catch (Exception e) {
log.error("An unexpected error occurred during image deletion for checkupId {}: {}",
checkupId, e.getMessage());
UserUtil.sendPacket(currentUser.getSessionId(),
new DeleteCheckupImageResponse(false, "Lỗi không xác định: " + e.getMessage(), fileName));
}
}
if (packet instanceof DeleteCheckupRequest deleteCheckupRequest) {
log.info("Recieved delete checkup request for checkupId: {}", deleteCheckupRequest.getCheckupId());
@@ -1616,6 +1669,12 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
UserUtil.sendPacket(currentUser.getSessionId(), new GetCheckupDataResponse(new String[0][0], 0, 1, 0, getCheckupDataRequest.getPageSize()));
}
}
if (packet instanceof PingRequest pingRequest) {
// Respond immediately to ping with pong
log.debug("Received PingRequest from session {}, responding with PongResponse", currentUser.getSessionId());
UserUtil.sendPacket(currentUser.getSessionId(), new PongResponse(pingRequest.getTimestamp()));
}
if (packet instanceof SimpleMessageRequest simpleMessageRequest) {
log.info("Received SimpleMessageRequest from {}", simpleMessageRequest.getSenderName());
SimpleMessageResponse response = new SimpleMessageResponse(simpleMessageRequest.getSenderName(), simpleMessageRequest.getMessage());
@@ -2416,6 +2475,80 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
return tempFile;
}
/**
* Delete an image from Google Drive asynchronously.
*
* @param checkupId ID of the checkup (as String).
* @param fileName Name of the image file to delete.
*/
private void deleteCheckupImageFromGoogleDriveAsync(String checkupId, String fileName) {
log.info("deleteCheckupImageFromGoogleDriveAsync - Checkup: {}, File: {}", checkupId, fileName);
CompletableFuture.runAsync(() -> {
try {
if (!Server.isGoogleDriveConnected()) {
log.info("Google Drive not connected - skipping image deletion for checkup {}", checkupId);
return;
}
// Get the checkup's Google Drive folder ID from database
String checkupDriveFolderId = null;
String sql = "SELECT drive_folder_id FROM Checkup WHERE checkup_id = ?";
try (Connection conn = DatabaseManager.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, Integer.parseInt(checkupId));
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
checkupDriveFolderId = rs.getString("drive_folder_id");
}
}
}
if (checkupDriveFolderId == null || checkupDriveFolderId.trim().isEmpty()) {
log.warn("Checkup {} has no Google Drive folder ID - cannot delete image", checkupId);
return;
}
log.info("Deleting image {} from Google Drive for checkup {}", fileName, checkupId);
// Find and delete the file from Google Drive
List<com.google.api.services.drive.model.File> files =
Server.getGoogleDriveService().findFilesByName(checkupDriveFolderId, fileName);
if (files.isEmpty()) {
log.warn("File {} not found in Google Drive folder for checkup {}", fileName, checkupId);
return;
}
// Delete all matching files (there should typically be only one)
for (com.google.api.services.drive.model.File file : files) {
Server.getGoogleDriveService().deleteFile(file.getId());
log.info("Successfully deleted file {} (ID: {}) from Google Drive for checkup {}",
fileName, file.getId(), checkupId);
if (ServerDashboard.getInstance() != null) {
ServerDashboard.getInstance().addLog(
String.format("Deleted image %s from Google Drive for checkup %s", fileName, checkupId)
);
}
}
} catch (NumberFormatException e) {
log.error("Invalid checkupId format '{}' for Google Drive deletion.", checkupId);
} catch (Exception e) {
log.error("Failed to delete image from Google Drive for checkup {}: {}", checkupId, e.getMessage(), e);
if (ServerDashboard.getInstance() != null) {
ServerDashboard.getInstance().addLog(
String.format("Failed to delete image %s from Google Drive for checkup %s: %s",
fileName, checkupId, e.getMessage())
);
}
}
});
}
private void broadcastQueueUpdate() {
// SỬA ĐỔI: Sử dụng một kết nối duy nhất, an toàn cho tất cả các truy vấn bên trong
@@ -451,7 +451,7 @@ public class GoogleDriveServiceOAuth {
* @return A list of File objects matching the name.
* @throws IOException if the Google Drive API call fails.
*/
private List<File> findFilesByName(String parentFolderId, String fileName) throws IOException {
public List<File> findFilesByName(String parentFolderId, String fileName) throws IOException {
String query = String.format("name = '%s' and '%s' in parents and trashed = false",
fileName.replace("'", "\\'"), // Escape single quotes in filename
parentFolderId);
@@ -523,4 +523,14 @@ public class GoogleDriveServiceOAuth {
return fileId;
}
/**
* Deletes a file from Google Drive by its file ID.
* @param fileId The ID of the file to delete.
* @throws IOException if the deletion fails.
*/
public void deleteFile(String fileId) throws IOException {
driveService.files().delete(fileId).execute();
log.info("Successfully deleted file with ID: {}", fileId);
}
}