Get up to 80 % extra points for free! More info:

Lesson 16 - Java Chat - Client - View Local Servers

In the previous lesson, Java Chat - Client - Introducing the Application Structure, we learned about the client's app structure.

Today we're going to implement the server administration window. Therefore, we'll display the servers found in the local network.

Modifying the LanServerFinder Class

Before we start implementing client parts, we must first modify the OnServerFoundListener attribute interface in the LanServerFinder class a bit. We'll add an InetAddress address parameter to the onServerFound() method, which will contain the address of the server where the datagram came from:

void onServerFound(ServerStatusData data, InetAddress address);

Next, we need to fix the calling of this method:

serverFoundListener.onServerFound(statusData, datagramPacket.getAddress());

We get the source address by calling the getAddress() method on the received datagram.

Displaying Local Servers

The server display window is already created. We'll show all found servers in the listView. We'll display three server information:

  • server name
  • server address and port
  • server occupancy

Server Entry Model

We'll create a ServerEntry class and put it in the model package. This class will contain all the information above:

package cz.stechy.chat.model;

public class ServerEntry {

    private final UUID serverID;
    private final InetAddress serverAddress;
    private final StringProperty serverName = new SimpleStringProperty(this, "serverName", null);
    private final IntegerProperty connectedClients = new SimpleIntegerProperty(this, "connectedClients", 0);
    private final IntegerProperty maxClients = new SimpleIntegerProperty(this, "maxClients", Integer.MAX_VALUE);
    private final ObjectProperty<ServerStatus> serverStatus = new SimpleObjectProperty<>(this, "serverStatus", ServerStatus.EMPTY);
    private final BooleanProperty connected = new SimpleBooleanProperty(this, "connected", false);
    private final IntegerProperty port = new SimpleIntegerProperty(this, "port", 0);
    private final AtomicLong lastUpdate = new AtomicLong();

    public ServerEntry(ServerStatusData serverStatusData, InetAddress serverAddress) {
        this.serverID = serverStatusData.serverID;
        this.serverAddress = serverAddress;
        this.serverName.set(serverStatusData.serverName);
        this.connectedClients.set(serverStatusData.clientCount);
        this.maxClients.set(serverStatusData.maxClients);
        this.serverStatus.set(serverStatusData.serverStatus);
        this.port.set(serverStatusData.port);
        this.lastUpdate.set(System.currentTimeMillis());
    }

    public void update(ServerStatusData newServerStatusData) {
        this.serverName.set(newServerStatusData.serverName);
        this.connectedClients.set(newServerStatusData.clientCount);
        this.maxClients.set(newServerStatusData.maxClients);
        this.serverStatus.set(newServerStatusData.serverStatus);
        this.port.set(newServerStatusData.port);
        this.lastUpdate.set(System.currentTimeMillis());
    }

    public boolean hasOldData() {
        final long time = System.currentTimeMillis();
        return time - lastUpdate.get() > 3000;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ServerEntry that = (ServerEntry) o;
        return Objects.equals(serverID, that.serverID);
    }

    @Override
    public int hashCode() {
        return Objects.hash(serverID);
    }

The class contains the same variables as the ServerStatusData class + a lastUpdate variable, which contains information about the most recent entry update. The update() method updates the information. The hasOldData() method tells us whether or not the current instance contains old data. If the data is out of date, we'll remove the entry from the list of found servers (we haven't done this yet). It was necessary to override the equals() and hashCode() to make the HashMap search work correctly.

Server Entry Cell

To be able to show a custom ListView item, we have to create it first. I've already designed a graphical representation of the item. It's located in the connect/server_entry.fxml file. We'll now create a "controller" to control this item. We'll create a new widget package and place it next to the model packages. In the newly created package, we'll create a ServerEntryCell class to represent a listView item. The class will inherit from the generic ListCell<ServerEntry> class and will be typed to the ServerEntry model class:

package cz.stechy.chat.widget;

public class ServerEntryCell extends ListCell<ServerEntry> {

    private static final String FXML_PATH = "/fxml/connect/server_entry.fxml";
    private static final String ADDRESS_PORT_FORMAT = "%s:%d";

    @FXML
    private Label lblName;

    @FXML
    private Label lblClients;

    @FXML
    private Label lblAddress;

    private Parent container;

    public ServerEntryCell() {
        final FXMLLoader loader = new FXMLLoader(getClass().getResource(FXML_PATH));
        loader.setController(this);
        try {
            container = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void updateItem(ServerEntry item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
            lblName.textProperty().unbind();
            lblClients.textProperty().unbind();
        } else {
            lblName.textProperty().bind(item.serverNameProperty());
            lblClients.textProperty().bind(item.clientsProperty());
            lblAddress.textProperty().set(String.format(ADDRESS_PORT_FORMAT, item.getServerAddress().getHostAddress(), item.getPort()));
            setGraphic(container);
        }
    }
}

The FXML_PATH class constant contains the path to the view file. We use the FXMLLoader to load the file in the class constructor and bind the view with our class using the setController() method. Calling this method is very important because variables annotated with the @FXML annotation wouldn't be initialized otherwise. Finally, we override the updateItem() method, which is called whenever a listView item needs to be updated. In the method, we must first call updateItem() on the parent to set the parent variables correctly. Our code follows. If the item contains no entry, we clear the text and graphics and remove the bindings to the name and number of clients. If the item contains an entry, we bind all the information to the corresponding components. Using the setGraphic() method, we display our components in a single listView item.

Local Server Management Service

We'll create a class to manage servers found in the local network. Next to the widget and controller packages, we'll create a new package named service, in which we'll define a new LocalServerService class:

public final class LocalServerService implements OnServerFoundListener {

    private static final String BROADCAST_ADDRESS = "224.0.2.60";
    private static final int BROADCAST_PORT = 56489;

    // map of all found servers
    private final ObservableMap<UUID, ServerEntry> serverMap = FXCollections.observableMap(new HashMap<>());

    private LanServerFinder serverFinder;

    public LocalServerService() {
        try {
            this.serverFinder = new LanServerFinder(InetAddress.getByName(BROADCAST_ADDRESS), BROADCAST_PORT);
            this.serverFinder.setServerFoundListener(this);
            ThreadPool.COMMON_EXECUTOR.submit(this.serverFinder);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void onServerFound(ServerStatusData data, InetAddress address) {
        ThreadPool.JAVAFX_EXECUTOR.execute(() -> {
            final UUID serverID = data.serverID;
            if (serverMap.containsKey(serverID)) {
                serverMap.get(serverID).update(data);
            } else {
                serverMap.put(serverID, new ServerEntry(data, address));
            }
        });
    }

    public ObservableMap<UUID, ServerEntry> getServerMap() {
        return FXCollections.unmodifiableObservableMap(serverMap);
    }

    public void stop() {
        serverFinder.shutdown();
    }

The class implements the OnServerFoundListener interface, which is located in the LanServerFinder class. We'll use this interface to add servers we found to serverMap. The class also contains the BROADCAST_ADDRESS and BROADCAST_PORT constants whose values must match those on the server. serverMap is of the ObservableMap type, which allows us to update the resulting map list in the GUI automatically. In the constructor, we create a new LanServerFinder class instance and set the listener. The class also has a getter for the observable map. Note the call to the unmodifiableObservableMap() method to ensure that the map obtained by the getter cannot be changed from the outside. With the stop() method, we stop LanServerFinder. The OnServerFoundListener interface requires us to implement the onServerFound() method, which is called each time we find a new server. In the method, we check if the map already contains the entry. If it's there, we update the information, otherwise a new entry is added. This must happen in the JavaFX thread, because if we changed the map in another thread, it'd throw an exception with the description: Not on FX application thread. Later, we'll wire the server map with a graphical component and it can only be changed in the JavaFX thread.

Wiring the Service With The Controller

Finally, we have all the components ready. Now we'll wire them all together. We'll edit the ConnectController class. Let's begin by adding the Initializable and OnCloseListener interfaces, whose methods we'll implement in a moment.

We'll set the ServerEntry data type to the lvServers variable:

@FXML private ListView<ServerEntry> lvServers;

Next, we'll create a new serverService instance constant:

private final LocalServerService serverService = new LocalServerService();

In the initialize() method, we'll set the factory for graphical components to servers and set the listener to the server map from our service:

public void initialize(URL url, ResourceBundle resourceBundle) {
    lvServers.setCellFactory(param -> new ServerEntryCell());
    serverService.getServerMap().addListener(serverMapListener);
}

The serverMapListener variable will contain a converter from the map to to a list:

private MapChangeListener<? super UUID, ? super ServerEntry> serverMapListener = change -> {
    if (change.wasAdded()) {
        lvServers.getItems().addAll(change.getValueAdded());
    }

    if (change.wasRemoved()) {
        lvServers.getItems().removeAll(change.getValueRemoved());
    }
};

Finally, we'll implement the onClose() method, which is called when the window is closed:

public void onClose() {
    serverService.stop();
}

Display the Server Selection Window

In the MainController we'll add a menu button handler in the handleConnect() method to display the server management window:

@FXML
private void handleConnect(ActionEvent actionEvent) {
    try {
        showNewWindow("connect/connect", "Connect to server...");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

We use the showNewWindow() utility method which accepts the path to the fxml document and the window name as parameters.

If you did everything correctly, after starting the server, client, and displaying the server selection window, you should see the following window with one server found:

Window with found server in local network - Server for Client Applications in Java

In the next lesson, Java Chat - Client - Server Connection Part 1, we'll create a class that will keep in touch with the server and finally log in to the server.


 

Previous article
Java Chat - Client - Introducing the Application Structure
All articles in this section
Server for Client Applications in Java
Skip article
(not recommended)
Java Chat - Client - Server Connection Part 1
Article has been written for you by Petr Štechmüller
Avatar
User rating:
1 votes
Activities