From 4b6565ec052f5fbc3862bd3a68d8cf030078cfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 00:03:13 -0400 Subject: [PATCH] Update date validation to allow for negative timestamps across multiple components. Modify AddDialog, HistoryViewDialog, and DateUtils to ensure consistent handling of timestamp inputs, enhancing robustness in date parsing. --- .../2197/IMG_2197_20251223_060623_312.jpg | Bin 0 -> 11280 bytes .../2197/IMG_2197_20251223_060625_544.jpg | Bin 0 -> 11270 bytes .../BsK/client/network/ConnectionManager.java | 462 ++++++++++++++++++ .../component/common/AddDialog/AddDialog.java | 2 +- .../common/ConnectionStatusOverlay.java | 289 +++++++++++ .../HistoryViewDialog/HistoryViewDialog.java | 2 +- .../common/exception/NetworkException.java | 24 + .../java/BsK/common/util/date/DateUtils.java | 4 +- 8 files changed, 779 insertions(+), 4 deletions(-) create mode 100644 image/checkup_media/2197/IMG_2197_20251223_060623_312.jpg create mode 100644 image/checkup_media/2197/IMG_2197_20251223_060625_544.jpg create mode 100644 src/main/java/BsK/client/network/ConnectionManager.java create mode 100644 src/main/java/BsK/client/ui/component/common/ConnectionStatusOverlay.java create mode 100644 src/main/java/BsK/common/exception/NetworkException.java diff --git a/image/checkup_media/2197/IMG_2197_20251223_060623_312.jpg b/image/checkup_media/2197/IMG_2197_20251223_060623_312.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6960c1061e52b21f2c9724bca79c6dfe3e71bb4d GIT binary patch literal 11280 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 state = new AtomicReference<>(ConnectionState.DISCONNECTED); + private volatile Channel channel; + private EventLoopGroup eventLoopGroup; + + // Reconnection settings + private static final int INITIAL_RETRY_DELAY_SECONDS = 1; + private static final int MAX_RETRY_DELAY_SECONDS = 30; + private static final double BACKOFF_MULTIPLIER = 2.0; + + // Ping/Pong timeout settings - MORE TOLERANT (1 minute total) + // Send ping every 15 seconds, allow 4 missed pongs = 60 seconds tolerance + private static final int WRITER_IDLE_SECONDS = 15; // Send ping every 15 seconds when idle + private static final int READER_IDLE_SECONDS = 90; // Fallback: no data for 90 seconds = disconnect + + private final AtomicInteger currentRetryDelay = new AtomicInteger(INITIAL_RETRY_DELAY_SECONDS); + private final AtomicInteger reconnectAttempt = new AtomicInteger(0); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "ConnectionManager-Scheduler"); + t.setDaemon(true); + return t; + }); + private ScheduledFuture reconnectFuture; + private ScheduledFuture countdownFuture; + + // Listeners + private final List listeners = new CopyOnWriteArrayList<>(); + + // Track if handshake is complete + private volatile boolean handshakeComplete = false; + + private ConnectionManager() { + // Private constructor for singleton + } + + /** + * Initialize connection parameters from config + */ + public void initialize(Properties config) { + this.serverAddress = config.getProperty("server.address", "localhost"); + String portStr = config.getProperty("server.port", "1999"); + this.serverPort = Integer.parseInt(portStr); + + try { + this.serverUri = new URI("ws://" + serverAddress + ":" + serverPort + "/"); + log.info("ConnectionManager initialized with server: {}:{}", serverAddress, serverPort); + } catch (URISyntaxException e) { + log.error("Invalid server URI", e); + throw new RuntimeException("Invalid server URI configuration", e); + } + } + + /** + * Add a connection state listener + */ + public void addListener(ConnectionStateListener listener) { + listeners.add(listener); + // Notify immediately of current state + listener.onStateChanged(state.get(), getStateMessage(state.get())); + } + + /** + * Remove a connection state listener + */ + public void removeListener(ConnectionStateListener listener) { + listeners.remove(listener); + } + + /** + * Start the connection process + */ + public void startConnection() { + if (state.get() == ConnectionState.CONNECTING || state.get() == ConnectionState.CONNECTED) { + log.info("Connection already in progress or established"); + return; + } + + setState(ConnectionState.CONNECTING, "Đang kết nối đến máy chủ..."); + connectAsync(); + } + + /** + * Manual retry triggered by user + */ + public void retryNow() { + log.info("Manual retry requested"); + + // Cancel any pending automatic retry + cancelPendingReconnect(); + + // Reset retry delay for manual retry + currentRetryDelay.set(INITIAL_RETRY_DELAY_SECONDS); + + // Start connection + setState(ConnectionState.RECONNECTING, "Đang thử kết nối lại..."); + connectAsync(); + } + + /** + * Called when connection is established and handshake is complete + */ + public void onConnected() { + log.info("Connection established and handshake complete"); + handshakeComplete = true; + reconnectAttempt.set(0); + currentRetryDelay.set(INITIAL_RETRY_DELAY_SECONDS); + setState(ConnectionState.CONNECTED, "Đã kết nối"); + } + + /** + * Called when connection is lost + */ + public void onDisconnected(String reason) { + log.warn("Connection lost: {}", reason); + handshakeComplete = false; + channel = null; + + // Don't reconnect if we're already disconnected (intentional disconnect) + if (state.get() == ConnectionState.DISCONNECTED) { + return; + } + + // Start reconnection process + scheduleReconnect(); + } + + /** + * Get current connection state + */ + public ConnectionState getState() { + return state.get(); + } + + /** + * Check if connected and ready to send packets + */ + public boolean isConnected() { + return state.get() == ConnectionState.CONNECTED && + channel != null && + channel.isActive() && + handshakeComplete; + } + + /** + * Get the current channel for sending packets + */ + public Channel getChannel() { + return channel; + } + + /** + * Send a packet, throwing NetworkException if not connected + */ + public void sendPacket(Packet packet) throws NetworkException { + if (!isConnected()) { + throw new NetworkException("Không có kết nối đến máy chủ. Vui lòng đợi kết nối được khôi phục."); + } + + try { + String text = PacketSerializer.GSON.toJson(packet); + channel.writeAndFlush(new TextWebSocketFrame(text)); + } catch (Exception e) { + log.error("Failed to send packet", e); + throw new NetworkException("Lỗi gửi dữ liệu: " + e.getMessage()); + } + } + + /** + * Get the current server address + */ + public String getServerAddress() { + return serverAddress; + } + + /** + * Get the current server port + */ + public int getServerPort() { + return serverPort; + } + + // ==================== Private Methods ==================== + + private void connectAsync() { + new Thread(() -> { + try { + doConnect(); + } catch (Exception e) { + log.error("Connection attempt failed", e); + scheduleReconnect(); + } + }, "ConnectionManager-Connect").start(); + } + + private void doConnect() throws InterruptedException { + log.info("Attempting to connect to {}:{}", serverAddress, serverPort); + + // Cleanup previous connection if any + if (eventLoopGroup != null && !eventLoopGroup.isShutdown()) { + eventLoopGroup.shutdownGracefully().sync(); + } + + eventLoopGroup = new NioEventLoopGroup(); + + try { + Bootstrap bootstrap = new Bootstrap() + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline() + .addLast(new HttpClientCodec()) + .addLast(new HttpObjectAggregator(50 * 1024 * 1024)) + .addLast(new WebSocketClientProtocolHandler( + serverUri, + WebSocketVersion.V13, + null, + true, + new DefaultHttpHeaders(), + 50 * 1024 * 1024)) + // MORE TOLERANT TIMEOUT SETTINGS: + // Reader timeout: 90 seconds - fallback disconnect if no data at all + // Writer timeout: 15 seconds - send ping every 15 seconds when idle + // Combined with MAX_MISSED_PONGS=4, this gives ~60 seconds tolerance + .addLast(new IdleStateHandler(READER_IDLE_SECONDS, WRITER_IDLE_SECONDS, 0)) + .addLast("ws", ClientHandler.INSTANCE); + } + }); + + ChannelFuture connectFuture = bootstrap.connect(serverAddress, serverPort); + + // Add listener for connection result + connectFuture.addListener(future -> { + if (future.isSuccess()) { + log.info("TCP connection established, waiting for WebSocket handshake..."); + channel = connectFuture.channel(); + + // Add close listener + channel.closeFuture().addListener(closeFuture -> { + log.info("Channel closed"); + if (state.get() != ConnectionState.DISCONNECTED) { + onDisconnected("Kết nối bị đóng"); + } + }); + } else { + log.error("Connection failed: {}", future.cause().getMessage()); + scheduleReconnect(); + } + }); + + } catch (Exception e) { + log.error("Error during connection setup", e); + scheduleReconnect(); + } + } + + private void scheduleReconnect() { + int attempt = reconnectAttempt.incrementAndGet(); + int delay = currentRetryDelay.get(); + + log.info("Scheduling reconnect attempt {} in {} seconds", attempt, delay); + setState(ConnectionState.RECONNECTING, + String.format("Đang chờ kết nối lại (lần thử %d)...", attempt)); + + // Start countdown notifications + startCountdown(delay, attempt); + + // Schedule the actual reconnect + reconnectFuture = scheduler.schedule(() -> { + cancelCountdown(); + setState(ConnectionState.RECONNECTING, "Đang kết nối lại..."); + connectAsync(); + }, delay, TimeUnit.SECONDS); + + // Calculate next delay with exponential backoff + int nextDelay = (int) Math.min(delay * BACKOFF_MULTIPLIER, MAX_RETRY_DELAY_SECONDS); + currentRetryDelay.set(nextDelay); + } + + private void startCountdown(int seconds, int attemptNumber) { + final AtomicInteger remaining = new AtomicInteger(seconds); + + countdownFuture = scheduler.scheduleAtFixedRate(() -> { + int r = remaining.getAndDecrement(); + if (r > 0) { + notifyCountdown(r, attemptNumber); + } + }, 0, 1, TimeUnit.SECONDS); + } + + private void cancelCountdown() { + if (countdownFuture != null && !countdownFuture.isDone()) { + countdownFuture.cancel(false); + } + } + + private void cancelPendingReconnect() { + cancelCountdown(); + if (reconnectFuture != null && !reconnectFuture.isDone()) { + reconnectFuture.cancel(false); + } + } + + private void setState(ConnectionState newState, String message) { + ConnectionState oldState = state.getAndSet(newState); + if (oldState != newState) { + log.info("Connection state changed: {} -> {} ({})", oldState, newState, message); + notifyStateChange(newState, message); + } + } + + private void notifyStateChange(ConnectionState newState, String message) { + SwingUtilities.invokeLater(() -> { + for (ConnectionStateListener listener : listeners) { + try { + listener.onStateChanged(newState, message); + } catch (Exception e) { + log.error("Error notifying listener", e); + } + } + }); + } + + private void notifyCountdown(int secondsRemaining, int attemptNumber) { + SwingUtilities.invokeLater(() -> { + for (ConnectionStateListener listener : listeners) { + try { + listener.onReconnectCountdown(secondsRemaining, attemptNumber); + } catch (Exception e) { + log.error("Error notifying countdown", e); + } + } + }); + } + + private String getStateMessage(ConnectionState state) { + return switch (state) { + case DISCONNECTED -> "Chưa kết nối"; + case CONNECTING -> "Đang kết nối..."; + case CONNECTED -> "Đã kết nối"; + case RECONNECTING -> "Đang kết nối lại..."; + }; + } + + /** + * Shutdown the connection manager (for application exit) + * Sends a proper WebSocket close frame so the server can clean up the session + */ + public void shutdown() { + log.info("Shutting down ConnectionManager"); + state.set(ConnectionState.DISCONNECTED); + cancelPendingReconnect(); + + if (channel != null && channel.isActive()) { + try { + // Send WebSocket close frame for proper server-side cleanup + log.info("Sending WebSocket close frame to server..."); + channel.writeAndFlush(new io.netty.handler.codec.http.websocketx.CloseWebSocketFrame()) + .sync(); // Wait for close frame to be sent + + // Give server a moment to process the close + Thread.sleep(100); + + // Now close the channel + channel.close().sync(); + log.info("Channel closed cleanly"); + } catch (InterruptedException e) { + log.warn("Interrupted while closing channel", e); + Thread.currentThread().interrupt(); + // Force close if interrupted + channel.close(); + } catch (Exception e) { + log.warn("Error during graceful close, forcing close", e); + channel.close(); + } + } + + if (eventLoopGroup != null && !eventLoopGroup.isShutdown()) { + try { + eventLoopGroup.shutdownGracefully().sync(); + } catch (InterruptedException e) { + log.warn("Interrupted while shutting down event loop", e); + Thread.currentThread().interrupt(); + } + } + + scheduler.shutdown(); + } +} diff --git a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java index 0329192..125f679 100644 --- a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java +++ b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java @@ -1075,7 +1075,7 @@ private void setupDebounceSearch() { try { Date parsedDate; - if (patientDob.matches("\\d+")) { // Check if the string is a timestamp + if (patientDob.matches("-?\\d+")) { // Check if the string is a timestamp long timestamp = Long.parseLong(patientDob); // Convert the string to a long parsedDate = new Date(timestamp); // Convert the timestamp to a Date } else { diff --git a/src/main/java/BsK/client/ui/component/common/ConnectionStatusOverlay.java b/src/main/java/BsK/client/ui/component/common/ConnectionStatusOverlay.java new file mode 100644 index 0000000..35b7ad9 --- /dev/null +++ b/src/main/java/BsK/client/ui/component/common/ConnectionStatusOverlay.java @@ -0,0 +1,289 @@ +package BsK.client.ui.component.common; + +import BsK.client.network.ConnectionManager; +import BsK.client.network.ConnectionManager.ConnectionState; +import BsK.client.network.ConnectionManager.ConnectionStateListener; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Arc2D; + +/** + * Glass pane overlay that blocks UI interaction when connection is lost. + * Shows connection status, retry countdown, and manual retry button. + */ +public class ConnectionStatusOverlay extends JPanel implements ConnectionStateListener { + + private static final Color OVERLAY_COLOR = new Color(0, 0, 0, 180); + private static final Color CARD_BACKGROUND = new Color(45, 45, 48); + private static final Color TEXT_COLOR = new Color(230, 230, 230); + private static final Color ACCENT_COLOR = new Color(0, 122, 204); + private static final Color BUTTON_HOVER_COLOR = new Color(0, 150, 230); + + private final JLabel statusLabel; + private final JLabel messageLabel; + private final JLabel countdownLabel; + private final JButton retryButton; + private final SpinnerPanel spinnerPanel; + + private volatile boolean showSpinner = true; + private volatile int countdownSeconds = 0; + private volatile int attemptNumber = 0; + + public ConnectionStatusOverlay() { + setOpaque(false); + setLayout(new GridBagLayout()); + + // Block all mouse events on the overlay + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + e.consume(); + } + @Override + public void mousePressed(MouseEvent e) { + e.consume(); + } + }); + addMouseMotionListener(new MouseAdapter() {}); + + // Create center card panel + JPanel cardPanel = createCardPanel(); + add(cardPanel); + + // Initialize components + spinnerPanel = new SpinnerPanel(); + statusLabel = createLabel("Đang kết nối...", 18, Font.BOLD); + messageLabel = createLabel("Vui lòng đợi trong giây lát", 14, Font.PLAIN); + countdownLabel = createLabel("", 12, Font.PLAIN); + countdownLabel.setForeground(new Color(180, 180, 180)); + + retryButton = createRetryButton(); + + // Layout components in card + cardPanel.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.insets = new Insets(10, 20, 10, 20); + + cardPanel.add(spinnerPanel, gbc); + + gbc.gridy = 1; + gbc.insets = new Insets(15, 20, 5, 20); + cardPanel.add(statusLabel, gbc); + + gbc.gridy = 2; + gbc.insets = new Insets(5, 20, 5, 20); + cardPanel.add(messageLabel, gbc); + + gbc.gridy = 3; + gbc.insets = new Insets(5, 20, 10, 20); + cardPanel.add(countdownLabel, gbc); + + gbc.gridy = 4; + gbc.insets = new Insets(10, 20, 20, 20); + cardPanel.add(retryButton, gbc); + + // Initially hidden until state is set + retryButton.setVisible(false); + countdownLabel.setVisible(false); + + // Register as listener + ConnectionManager.getInstance().addListener(this); + } + + private JPanel createCardPanel() { + JPanel card = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(CARD_BACKGROUND); + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 20, 20); + g2.dispose(); + } + }; + card.setOpaque(false); + card.setPreferredSize(new Dimension(400, 280)); + return card; + } + + private JLabel createLabel(String text, int size, int style) { + JLabel label = new JLabel(text); + label.setFont(new Font("Segoe UI", style, size)); + label.setForeground(TEXT_COLOR); + label.setHorizontalAlignment(SwingConstants.CENTER); + return label; + } + + private JButton createRetryButton() { + JButton button = new JButton("Thử lại ngay") { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (getModel().isPressed()) { + g2.setColor(ACCENT_COLOR.darker()); + } else if (getModel().isRollover()) { + g2.setColor(BUTTON_HOVER_COLOR); + } else { + g2.setColor(ACCENT_COLOR); + } + + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 10, 10); + + g2.setColor(Color.WHITE); + g2.setFont(getFont()); + FontMetrics fm = g2.getFontMetrics(); + int x = (getWidth() - fm.stringWidth(getText())) / 2; + int y = (getHeight() + fm.getAscent() - fm.getDescent()) / 2; + g2.drawString(getText(), x, y); + + g2.dispose(); + } + }; + + button.setFont(new Font("Segoe UI", Font.BOLD, 14)); + button.setForeground(Color.WHITE); + button.setPreferredSize(new Dimension(150, 40)); + button.setBorderPainted(false); + button.setContentAreaFilled(false); + button.setFocusPainted(false); + button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + + button.addActionListener(e -> { + ConnectionManager.getInstance().retryNow(); + }); + + return button; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(OVERLAY_COLOR); + g2.fillRect(0, 0, getWidth(), getHeight()); + g2.dispose(); + } + + @Override + public void onStateChanged(ConnectionState newState, String message) { + SwingUtilities.invokeLater(() -> { + switch (newState) { + case DISCONNECTED -> { + setVisible(true); + statusLabel.setText("Chưa kết nối"); + messageLabel.setText("Ứng dụng đang chờ kết nối đến máy chủ"); + showSpinner = false; + retryButton.setVisible(true); + countdownLabel.setVisible(false); + } + case CONNECTING -> { + setVisible(true); + statusLabel.setText("Đang kết nối..."); + messageLabel.setText(message); + showSpinner = true; + retryButton.setVisible(false); + countdownLabel.setVisible(false); + } + case CONNECTED -> { + setVisible(false); + showSpinner = false; + } + case RECONNECTING -> { + setVisible(true); + statusLabel.setText("Đang kết nối lại..."); + messageLabel.setText(message); + showSpinner = true; + retryButton.setVisible(true); + countdownLabel.setVisible(true); + } + } + spinnerPanel.setSpinning(showSpinner); + repaint(); + }); + } + + @Override + public void onReconnectCountdown(int secondsRemaining, int attemptNumber) { + SwingUtilities.invokeLater(() -> { + this.countdownSeconds = secondsRemaining; + this.attemptNumber = attemptNumber; + countdownLabel.setText(String.format("Thử lại sau %d giây (lần thử %d)", + secondsRemaining, attemptNumber)); + countdownLabel.setVisible(true); + }); + } + + /** + * Inner panel that shows a spinning animation + */ + private class SpinnerPanel extends JPanel { + private volatile boolean spinning = true; + private int angle = 0; + private Timer timer; + + public SpinnerPanel() { + setOpaque(false); + setPreferredSize(new Dimension(60, 60)); + + timer = new Timer(30, e -> { + if (spinning) { + angle = (angle + 10) % 360; + repaint(); + } + }); + timer.start(); + } + + public void setSpinning(boolean spinning) { + this.spinning = spinning; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int size = 50; + int x = (getWidth() - size) / 2; + int y = (getHeight() - size) / 2; + + if (spinning) { + // Draw spinning arc + g2.setStroke(new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2.setColor(new Color(80, 80, 85)); + g2.drawOval(x, y, size, size); + + g2.setColor(ACCENT_COLOR); + g2.draw(new Arc2D.Double(x, y, size, size, angle, 90, Arc2D.OPEN)); + } else { + // Draw static circle with X + g2.setStroke(new BasicStroke(3)); + g2.setColor(new Color(200, 80, 80)); + g2.drawOval(x, y, size, size); + + int padding = 15; + g2.drawLine(x + padding, y + padding, x + size - padding, y + size - padding); + g2.drawLine(x + size - padding, y + padding, x + padding, y + size - padding); + } + + g2.dispose(); + } + } + + /** + * Cleanup when overlay is no longer needed + */ + public void cleanup() { + ConnectionManager.getInstance().removeListener(this); + } +} + diff --git a/src/main/java/BsK/client/ui/component/common/HistoryViewDialog/HistoryViewDialog.java b/src/main/java/BsK/client/ui/component/common/HistoryViewDialog/HistoryViewDialog.java index 053b2b4..07912a5 100644 --- a/src/main/java/BsK/client/ui/component/common/HistoryViewDialog/HistoryViewDialog.java +++ b/src/main/java/BsK/client/ui/component/common/HistoryViewDialog/HistoryViewDialog.java @@ -773,7 +773,7 @@ public class HistoryViewDialog extends JDialog { private String formatDate(String dateStr) { try { - if (dateStr != null && dateStr.matches("\\d+")) { + if (dateStr != null && dateStr.matches("-?\\d+")) { // Timestamp - Use Vietnam timezone (UTC+7) for consistent display Date date = new Date(Long.parseLong(dateStr)); return DateUtils.createVietnamDateFormat("dd/MM/yyyy HH:mm").format(date); diff --git a/src/main/java/BsK/common/exception/NetworkException.java b/src/main/java/BsK/common/exception/NetworkException.java new file mode 100644 index 0000000..078919f --- /dev/null +++ b/src/main/java/BsK/common/exception/NetworkException.java @@ -0,0 +1,24 @@ +package BsK.common.exception; + +/** + * Unchecked exception thrown when a network operation fails. + * Contains a user-friendly message suitable for display in dialogs or toasts. + */ +public class NetworkException extends RuntimeException { + + public NetworkException(String message) { + super(message); + } + + public NetworkException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Get a user-friendly message suitable for display + */ + public String getUserMessage() { + return getMessage(); + } +} + diff --git a/src/main/java/BsK/common/util/date/DateUtils.java b/src/main/java/BsK/common/util/date/DateUtils.java index 583d934..2317eee 100644 --- a/src/main/java/BsK/common/util/date/DateUtils.java +++ b/src/main/java/BsK/common/util/date/DateUtils.java @@ -55,7 +55,7 @@ public class DateUtils { try { // Check if it's a timestamp (all digits) - if (dateInput.matches("\\d+")) { + if (dateInput.matches("-?\\d+")) { Date date = new Date(Long.parseLong(dateInput)); synchronized (DISPLAY_FORMAT) { return DISPLAY_FORMAT.format(date); @@ -90,7 +90,7 @@ public class DateUtils { try { // Check if it's a timestamp (all digits) - if (dateInput.matches("\\d+")) { + if (dateInput.matches("-?\\d+")) { return new Date(Long.parseLong(dateInput)); } else { // Try to parse as dd/MM/yyyy format using Vietnam timezone