From 981730437135c48ce1fd0dc4f24ea8f64557df1c Mon Sep 17 00:00:00 2001 From: Abel Mekonnen Date: Tue, 26 May 2026 01:54:22 +0300 Subject: [PATCH 1/2] feat: implement UDP auto-discovery, auction polling, and client-side UI controllers for real-time auction management. --- .../controllers/AuctionDetailController.java | 33 ++++++++++++++--- .../client/controllers/GalleryController.java | 34 +++++++++++++++++ .../controllers/UserDashboardController.java | 8 ++++ .../client/network/RmiClientProvider.java | 15 ++++++++ .../client/network/UdpDiscoveryClient.java | 11 +++--- .../client/service/PollingService.java | 37 +++++++++++-------- .../com/auction/server/core/AdminManager.java | 8 +++- .../auction/server/core/UdpBroadcaster.java | 7 +++- 8 files changed, 124 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/auction/client/controllers/AuctionDetailController.java b/src/main/java/com/auction/client/controllers/AuctionDetailController.java index d107776..91ddb8c 100644 --- a/src/main/java/com/auction/client/controllers/AuctionDetailController.java +++ b/src/main/java/com/auction/client/controllers/AuctionDetailController.java @@ -50,7 +50,7 @@ public void loadAuction(int auctionId) { this.currentAuctionId = auctionId; try { var service = ClientContext.getInstance().getRmiProvider().getService(); - this.pollingService = new PollingService(service); + this.pollingService = new PollingService(); // load initial item state AuctionItem initial = service.getAuctionById(auctionId); this.currentItem = initial; @@ -60,10 +60,21 @@ public void loadAuction(int auctionId) { loadDetailThumbnail(auctionId, 1, thumb1View); loadDetailThumbnail(auctionId, 2, thumb2View); loadDetailThumbnail(auctionId, 3, thumb3View); - pollingService.startPolling(auctionId, item -> { - this.currentItem = item; - Platform.runLater(() -> updateUi(item)); - }); + pollingService.startPolling(() -> { + try { + AuctionItem item = service.getAuctionById(auctionId); + if (item != null && currentItem != null) { + String oldEnd = currentItem.getEndTime(); + if (oldEnd != null && !oldEnd.equals(item.getEndTime())) { + Platform.runLater(() -> showToast("Timer Extended!")); + } + } + this.currentItem = item; + Platform.runLater(() -> updateUi(item)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, 2); } catch (Exception e) { e.printStackTrace(); } @@ -180,6 +191,7 @@ private void handlePlaceBid() { Platform.runLater(() -> { updateUi(currentItem); bidStatusLabel.setText("Failed: " + finalMsg); + animateShakeError(); }); } finally { Platform.runLater(() -> { @@ -191,6 +203,17 @@ private void handlePlaceBid() { }, executor); } + private void animateShakeError() { + if (bidAmountField == null) return; + bidAmountField.setStyle("-fx-border-color: #f85149; -fx-border-width: 2px;"); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(Duration.millis(50), bidAmountField); + tt.setByX(10f); + tt.setCycleCount(6); + tt.setAutoReverse(true); + tt.setOnFinished(e -> bidAmountField.setStyle("")); + tt.play(); + } + private void loadDetailThumbnail(int auctionId, int index, javafx.scene.image.ImageView target) { java.util.concurrent.CompletableFuture.supplyAsync(() -> { try { diff --git a/src/main/java/com/auction/client/controllers/GalleryController.java b/src/main/java/com/auction/client/controllers/GalleryController.java index b2442a7..4c70ce7 100644 --- a/src/main/java/com/auction/client/controllers/GalleryController.java +++ b/src/main/java/com/auction/client/controllers/GalleryController.java @@ -30,11 +30,26 @@ public class GalleryController { private static final Image PLACEHOLDER_IMAGE = loadPlaceholderImage(); private java.util.List allAuctions = java.util.List.of(); + private com.auction.client.service.PollingService pollingService; + @FXML public void initialize() { try { var context = ClientContext.getInstance(); var service = context.getRmiProvider().getService(); + + pollingService = new com.auction.client.service.PollingService(); + pollingService.startPolling(() -> { + try { + List items = service.getActiveAuctions(); + allAuctions = (items == null) ? java.util.List.of() : items; + Platform.runLater(() -> handleSearch()); // handleSearch applies current filter/sort + } catch (Exception e) { + throw new RuntimeException(e); + } + }, 2); + + // initial load List items = service.getActiveAuctions(); allAuctions = (items == null) ? java.util.List.of() : items; Platform.runLater(() -> renderAuctions(allAuctions)); @@ -216,6 +231,7 @@ private static Image loadPlaceholderImage() { @FXML private void handleBackToDashboard() { try { + if (pollingService != null) pollingService.shutdown(); ClientContext context = ClientContext.getInstance(); String targetView = context.getPreviousViewName(); if (targetView == null || targetView.isBlank()) { @@ -226,4 +242,22 @@ private void handleBackToDashboard() { e.printStackTrace(); } } + + // Also shutdown before going to detail view + private void loadDetailView(AuctionItem item, int index) { + try { + if (pollingService != null) pollingService.shutdown(); + ClientContext context = ClientContext.getInstance(); + context.setPreviousViewName("gallery.fxml"); + Object ctrl = context.getViewLoader().loadView("auction_detail.fxml"); + if (ctrl instanceof AuctionDetailController) { + ((AuctionDetailController) ctrl).loadAuction(item.getId()); + if (index >= 0) { + ((AuctionDetailController) ctrl).showHeroImageIndex(index); + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } } diff --git a/src/main/java/com/auction/client/controllers/UserDashboardController.java b/src/main/java/com/auction/client/controllers/UserDashboardController.java index b7ba506..246ba02 100644 --- a/src/main/java/com/auction/client/controllers/UserDashboardController.java +++ b/src/main/java/com/auction/client/controllers/UserDashboardController.java @@ -60,8 +60,14 @@ public class UserDashboardController { private byte[] img1Bytes, img2Bytes, img3Bytes; + private com.auction.client.service.PollingService pollingService; + @FXML public void initialize() { + pollingService = new com.auction.client.service.PollingService(); + pollingService.startPolling(() -> { + javafx.application.Platform.runLater(() -> refreshDashboard()); + }, 2); refreshDashboard(); } @@ -116,6 +122,7 @@ private void handleRefreshDashboard() { @FXML private void handleOpenGallery() { try { + if (pollingService != null) pollingService.shutdown(); com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); context.setPreviousViewName("user_dashboard.fxml"); @@ -139,6 +146,7 @@ private void handleOpenAuctionDetail() { if (selected != null) { try { + if (pollingService != null) pollingService.shutdown(); com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); context.setCurrentAuctionId(selected.getId()); context.setPreviousViewName("user_dashboard.fxml"); diff --git a/src/main/java/com/auction/client/network/RmiClientProvider.java b/src/main/java/com/auction/client/network/RmiClientProvider.java index 16d83f6..7f8c8ea 100644 --- a/src/main/java/com/auction/client/network/RmiClientProvider.java +++ b/src/main/java/com/auction/client/network/RmiClientProvider.java @@ -25,6 +25,21 @@ public class RmiClientProvider { public IAuctionService connect(String host, int port) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry(host, port); service = (IAuctionService) registry.lookup(Constants.RMI_SERVICE_NAME); + + // Health check + service.serverTime(); + + // Persist last server + try { + java.nio.file.Path dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".rtdas"); + if (!java.nio.file.Files.exists(dir)) { + java.nio.file.Files.createDirectories(dir); + } + java.nio.file.Files.writeString(dir.resolve("last_server"), host + ":" + port); + } catch (java.io.IOException e) { + System.err.println("Failed to persist last_server: " + e.getMessage()); + } + return service; } diff --git a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java index 4a86484..8ec75dd 100644 --- a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java +++ b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java @@ -39,13 +39,14 @@ public void startListening() { DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); String data = new String(packet.getData(), 0, packet.getLength()).trim(); - // Format: RTDAS|| - if (data.startsWith(Constants.UDP_PREFIX + "|")) { + // Format: RTDAS|v1|||| + if (data.startsWith(Constants.UDP_PREFIX + "|v1|")) { String[] parts = data.split("\\|"); - if (parts.length == 3) { - String serverName = parts[1]; + if (parts.length >= 6) { int rmiPort = Integer.parseInt(parts[2]); - String host = packet.getAddress().getHostAddress(); + String serverName = parts[3]; + String rmiHost = parts[5]; + String host = (rmiHost != null && !rmiHost.trim().isEmpty()) ? rmiHost : packet.getAddress().getHostAddress(); ServerInfo info = new ServerInfo(serverName, host, rmiPort); if (!discoveredServers.contains(info)) { discoveredServers.add(info); diff --git a/src/main/java/com/auction/client/service/PollingService.java b/src/main/java/com/auction/client/service/PollingService.java index fa6f647..fe7cf23 100644 --- a/src/main/java/com/auction/client/service/PollingService.java +++ b/src/main/java/com/auction/client/service/PollingService.java @@ -15,33 +15,38 @@ */ public class PollingService { - private final IAuctionService service; private ScheduledExecutorService scheduler; + private int failureCount = 0; - public PollingService(IAuctionService service) { - this.service = service; + public PollingService() { + // generic service } /** - * Start polling a specific auction for updates. - * @param auctionId auction to watch - * @param onUpdate callback with updated AuctionItem (called on background thread) + * Start polling a generic task. + * @param task the RMI call to execute + * @param intervalSeconds polling interval */ - public void startPolling(int auctionId, Consumer onUpdate) { - // TODO: schedule getAuctionById every 2s, call onUpdate with result + public void startPolling(Runnable task, int intervalSeconds) { scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { - try{ - AuctionItem item = service.getAuctionById(auctionId); - onUpdate.accept(item); - } catch(Exception e){ - System.err.println(e.getMessage()); - e.printStackTrace(); + try { + task.run(); + failureCount = 0; // reset on success + } catch (Exception e) { + failureCount++; + System.err.println("Polling failed (" + failureCount + "): " + e.getMessage()); + if (failureCount >= 3) { + shutdown(); + javafx.application.Platform.runLater(() -> { + com.auction.client.core.ClientContext.getInstance().handleConnectionLost(); + }); + } } - }, 0, 2, TimeUnit.SECONDS); + }, 0, intervalSeconds, TimeUnit.SECONDS); } - /** Stop all polling. Call when leaving the detail view. */ + /** Stop all polling. Call when leaving the view. */ public void shutdown() { if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdownNow(); diff --git a/src/main/java/com/auction/server/core/AdminManager.java b/src/main/java/com/auction/server/core/AdminManager.java index 2aa374f..9633b8f 100644 --- a/src/main/java/com/auction/server/core/AdminManager.java +++ b/src/main/java/com/auction/server/core/AdminManager.java @@ -47,11 +47,15 @@ public List getAuditLogs(int lastNLines, SessionContext context) throws } public List getAllUsers(SessionContext context) { - return userRepo.findAllUsers(); + return userRepo.findAllUsers().stream() + .map(u -> new User(u.getUsername(), null, u.getRoleType(), u.getCreatedAt())) + .toList(); } public List searchUsers(String query, SessionContext context) { - return userRepo.searchUsers(query); + return userRepo.searchUsers(query).stream() + .map(u -> new User(u.getUsername(), null, u.getRoleType(), u.getCreatedAt())) + .toList(); } public void promoteUserToAdmin(String username, SessionContext context) throws AuctionException { diff --git a/src/main/java/com/auction/server/core/UdpBroadcaster.java b/src/main/java/com/auction/server/core/UdpBroadcaster.java index 4062e6b..2bbe9e0 100644 --- a/src/main/java/com/auction/server/core/UdpBroadcaster.java +++ b/src/main/java/com/auction/server/core/UdpBroadcaster.java @@ -31,7 +31,12 @@ public void start() { if (scheduler != null) return; scheduler = Executors.newSingleThreadScheduledExecutor(); - String message = Constants.UDP_PREFIX + "|" + serverName + "|" + rmiPort; + String serverId = java.util.UUID.randomUUID().toString(); + // Fallback to localhost if we cannot determine address, but client prefers payload host + String rmiHost = "127.0.0.1"; + try { rmiHost = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception ignored) {} + + String message = Constants.UDP_PREFIX + "|v1|" + rmiPort + "|" + serverName + "|" + serverId + "|" + rmiHost; byte[] data = message.getBytes(); scheduler.scheduleAtFixedRate(() -> { From 507cef184b141ee8be12b593b482b8d3e53dfb1e Mon Sep 17 00:00:00 2001 From: Abel Mekonnen Date: Tue, 26 May 2026 12:12:19 +0300 Subject: [PATCH 2/2] feat: implement UDP server discovery and client connection UI --- data/auction.db.sqlite | Bin 36864 -> 36864 bytes ...c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1.jpg | Bin 0 -> 50628 bytes ...b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg | Bin 0 -> 1325 bytes .../client/controllers/ConnectController.java | 77 ++++++++++++------ .../client/network/UdpDiscoveryClient.java | 48 ++++++----- 5 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 resources/images/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1.jpg create mode 100644 resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg diff --git a/data/auction.db.sqlite b/data/auction.db.sqlite index d81fc694e840cd55067ace8e2f969c3d4a9ac7a7..08c5267d60d60a9809e09d134ddc302d94913a7f 100644 GIT binary patch delta 728 zcmb`FL2DC17>0MdAtq{eVneACL-tlYY?zsynVrqWLkuM#SfYX!p`G2CgxGANNn?9U z_8?vr!E-_XK+UZeFJ1&sJ@o3ui)P0gr0>N=7V?Ud%x#pCI?HCgQd4cv+(YA z%`6;le*#uMlf1u@FQqbnGRa}`G&7F()2m5tr4gzf5mUuE;Q^Ndb4Ywdk%Wq}7MBsj z0rq{5K~F--ga|_^85RhsmXDD{lwippKdd(~#LR`%#q1H}Rf?+^RUl^&BL?B-%e{1Y zZrAM!7n8C7|2>IcMYuv4CX@x*Gk%pWY0sKQ>?7=vK=~L3kU$=V)A6m>X(w+V8TJqG z1$+c=zz%o-JaEna4c6=<@XbE9zfK+JgMBk=t(xaC?dPnruI2>(-t@SF&9n(h1}IH$ zzycU2+vc)SzPn;sM!D8#6cfuZjP-$1^1kTFrV?GJ+i59h*ojA7vD@kQoVJM7y51(A z=ylQjQktmh!3su!4s`R9#O4x*Jm}4+7v-P|7vz~*f3neN*6$6}us<3^YWS=Zi?%)@ zCH_ zSH;868!1P}a$0@M8t>|P^D_gS>6gDi?z&aZUS#qYuTsAO delta 100 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ξsyF#tO2mg2|q-QSMHbBd98 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 F{{XhV%5DGv literal 0 HcmV?d00001 diff --git a/resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg b/resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..99f0bb5953722ad9ffb8396320fd701822046f21 GIT binary patch literal 1325 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!?(TlN2=Ugzihj*b@hw4y38&24s1-bWm>_q z@l^ri1K|r&pZEXB$Y=e}a9%h3KLg|S{|w!${xfVT|1is?L+*Z zPgmm|6-t#F?5DI+6L73d)PpCRM@ zPxW2(e_NCPGh7V1F8xrJ>++#}?3w2lFU|a?d(GRG=hv2b4wn~y+Ab{9=qV)e%3@r@(<-kS6ED`Q_xI;#6*U-nU! zvKM8I9ghQ6SjaqJFus1R!LI4!y7lHQUpQNQrYrxJcHcVj?K(*@2i}LLc0G8L)IBF* ziVkCtGv_}^|A)TZ;g_P_w@>rFFDkibyBw{A93-c{U&gTl1j`m@k^}^JU73UApdj hF=x`H3zoHA-eSpDHXEw;J5KUv;A;qU6=D2;69Bea0% literal 0 HcmV?d00001 diff --git a/src/main/java/com/auction/client/controllers/ConnectController.java b/src/main/java/com/auction/client/controllers/ConnectController.java index 0b10852..b062fe7 100644 --- a/src/main/java/com/auction/client/controllers/ConnectController.java +++ b/src/main/java/com/auction/client/controllers/ConnectController.java @@ -55,32 +55,63 @@ public void initialize() { @FXML private void handleConnect() { - String host = ipField.getText(); - if (host == null || host.trim().isEmpty()) { - host = "localhost"; - } + String hostInput = ipField.getText(); + String portInput = portField.getText(); + + String host = (hostInput == null || hostInput.trim().isEmpty()) ? "localhost" : hostInput.trim(); int port = 1099; - try { - String portStr = portField.getText(); - if (portStr != null && !portStr.trim().isEmpty()) { - port = Integer.parseInt(portStr); + if (portInput != null && !portInput.trim().isEmpty()) { + try { + port = Integer.parseInt(portInput.trim()); + } catch (NumberFormatException e) { + statusLabel.setText("Invalid port number."); + return; } - } catch (NumberFormatException e) { - statusLabel.setText("Invalid port number."); - return; } - try { - com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); - context.getRmiProvider().connect(host, port); - statusLabel.setText("Connected successfully!"); - context.getUdpClient().stopListening(); - - // Navigate to login - context.getViewLoader().loadView("login.fxml"); - } catch (Exception e) { - statusLabel.setText("Connection failed: " + e.getMessage()); - e.printStackTrace(); - } + statusLabel.setText("Connecting to " + host + ":" + port + "..."); + + final String finalHost = host; + final int finalPort = port; + + // Set short timeout for RMI connection attempts + System.setProperty("sun.rmi.transport.tcp.connectTimeout", "3000"); + + // Use a standard Thread to avoid any issues with common pool size or configuration + new Thread(() -> { + try { + com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); + // Perform the RMI connection and health check + context.getRmiProvider().connect(finalHost, finalPort); + + javafx.application.Platform.runLater(() -> { + try { + statusLabel.setText("Connected successfully!"); + context.getUdpClient().stopListening(); + // Navigate to login + context.getViewLoader().loadView("login.fxml"); + } catch (Exception e) { + statusLabel.setText("Navigation failed: " + e.getMessage()); + } + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> { + Throwable root = e; + while (root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + + String errorMsg; + if (root instanceof java.net.ConnectException || root instanceof java.rmi.ConnectException) { + errorMsg = "Server unreachable at " + finalHost + ":" + finalPort; + } else if (e instanceof java.rmi.NotBoundException) { + errorMsg = "RTDAS service not found on this server."; + } else { + errorMsg = (root.getMessage() != null) ? root.getMessage() : root.toString(); + } + statusLabel.setText("Connection failed: " + errorMsg); + }); + } + }).start(); } } diff --git a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java index 8ec75dd..dc97a9c 100644 --- a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java +++ b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java @@ -31,30 +31,36 @@ public void startListening() { if (running) return; running = true; listenerThread = new Thread(() -> { - try (DatagramSocket socket = new DatagramSocket(Constants.UDP_BROADCAST_PORT)) { - socket.setSoTimeout(1000); // 1 second timeout to allow interrupt checking - byte[] buffer = new byte[1024]; - while (running) { - try { - DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket.receive(packet); - String data = new String(packet.getData(), 0, packet.getLength()).trim(); - // Format: RTDAS|v1|||| - if (data.startsWith(Constants.UDP_PREFIX + "|v1|")) { - String[] parts = data.split("\\|"); - if (parts.length >= 6) { - int rmiPort = Integer.parseInt(parts[2]); - String serverName = parts[3]; - String rmiHost = parts[5]; - String host = (rmiHost != null && !rmiHost.trim().isEmpty()) ? rmiHost : packet.getAddress().getHostAddress(); - ServerInfo info = new ServerInfo(serverName, host, rmiPort); - if (!discoveredServers.contains(info)) { - discoveredServers.add(info); + try { + java.net.DatagramSocket socket = new java.net.DatagramSocket(null); + socket.setReuseAddress(true); + socket.bind(new java.net.InetSocketAddress(com.auction.shared.Constants.UDP_BROADCAST_PORT)); + + try (socket) { + socket.setSoTimeout(1000); // 1 second timeout to allow interrupt checking + byte[] buffer = new byte[1024]; + while (running) { + try { + java.net.DatagramPacket packet = new java.net.DatagramPacket(buffer, buffer.length); + socket.receive(packet); + String data = new String(packet.getData(), 0, packet.getLength()).trim(); + // Format: RTDAS|v1|||| + if (data.startsWith(com.auction.shared.Constants.UDP_PREFIX + "|v1|")) { + String[] parts = data.split("\\|"); + if (parts.length >= 6) { + int rmiPort = Integer.parseInt(parts[2]); + String serverName = parts[3]; + String rmiHost = parts[5]; + String host = (rmiHost != null && !rmiHost.trim().isEmpty()) ? rmiHost : packet.getAddress().getHostAddress(); + ServerInfo info = new ServerInfo(serverName, host, rmiPort); + if (!discoveredServers.contains(info)) { + discoveredServers.add(info); + } } } + } catch (java.net.SocketTimeoutException e) { + // Expected timeout, loop continues and checks running flag } - } catch (java.net.SocketTimeoutException e) { - // Expected timeout, loop continues and checks running flag } } } catch (Exception e) {