Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified data/auction.db.sqlite
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down Expand Up @@ -180,6 +191,7 @@ private void handlePlaceBid() {
Platform.runLater(() -> {
updateUi(currentItem);
bidStatusLabel.setText("Failed: " + finalMsg);
animateShakeError();
});
} finally {
Platform.runLater(() -> {
Expand All @@ -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 {
Expand Down
77 changes: 54 additions & 23 deletions src/main/java/com/auction/client/controllers/ConnectController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,26 @@ public class GalleryController {
private static final Image PLACEHOLDER_IMAGE = loadPlaceholderImage();
private java.util.List<AuctionItem> 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<AuctionItem> 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<AuctionItem> items = service.getActiveAuctions();
allAuctions = (items == null) ? java.util.List.of() : items;
Platform.runLater(() -> renderAuctions(allAuctions));
Expand Down Expand Up @@ -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()) {
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/auction/client/network/RmiClientProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
47 changes: 27 additions & 20 deletions src/main/java/com/auction/client/network/UdpDiscoveryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +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|<ServerName>|<RmiPort>
if (data.startsWith(Constants.UDP_PREFIX + "|")) {
String[] parts = data.split("\\|");
if (parts.length == 3) {
String serverName = parts[1];
int rmiPort = Integer.parseInt(parts[2]);
String host = 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|<rmiPort>|<serverName>|<serverId>|<rmiHost>
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) {
Expand Down
37 changes: 21 additions & 16 deletions src/main/java/com/auction/client/service/PollingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuctionItem> 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();
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/auction/server/core/AdminManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ public List<String> getAuditLogs(int lastNLines, SessionContext context) throws
}

public List<User> getAllUsers(SessionContext context) {
return userRepo.findAllUsers();
return userRepo.findAllUsers().stream()
.map(u -> new User(u.getUsername(), null, u.getRoleType(), u.getCreatedAt()))
.toList();
}

public List<User> 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 {
Expand Down
Loading
Loading