0

I'm trying to coding a very simple Client-Server Email project in java. I've already code the communication between client and server using socket and now I'm tryng to code some test which includes also a very simple UI. My idea is to create many threads as many clients I have and I want that every sigle thread starts opening a simple UI window created with Java FX but I have some problems.

This is the main class:

import java.io.*;

public class ClientController{
    public static void main(String args[]) throws IOException {
        ParallelClient c1=new ParallelClient("aaaa@gmail.com");
        ParallelClient c2=new ParallelClient("bbbb@gmail.com");
        c1.start();
        c2.start();
    }
}

This is the ParallelClient class:

import ...

public class ParallelClient extends Thread{
    private String user;

    public ParallelClient(String user){
        this.user=user;
    }

    public void run(){
        ClientApp app=new ClientApp();
        try {
            app.start(new Stage());
        } catch (Exception e) {
            e.printStackTrace();
        }
        ...
    }
    ...
}

And this is the ClientApp class which set the new window:

import ...

public class ClientApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        try {
            Parent root = FXMLLoader.load(getClass().getResource("ui/client-management.fxml"));
            stage.setTitle("ClientMail");
            stage.setScene(new Scene(root, 1080, 720));
            stage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

When I try to run the code I get the followung problem and I can't understand how to fix it:

Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.NoClassDefFoundError: Could not initialize class javafx.stage.Screen
    at javafx.stage.Window.<init>(Window.java:1439)
    at javafx.stage.Stage.<init>(Stage.java:252)
    at javafx.stage.Stage.<init>(Stage.java:240)
    at model.ParallelClient.run(ParallelClient.java:25)
java.lang.ExceptionInInitializerError
    at javafx.stage.Window.<init>(Window.java:1439)
    at javafx.stage.Stage.<init>(Stage.java:252)
    at javafx.stage.Stage.<init>(Stage.java:240)
    at model.ParallelClient.run(ParallelClient.java:25)
Caused by: java.lang.IllegalStateException: This operation is permitted on the event thread only; currentThread = Thread-1
    at com.sun.glass.ui.Application.checkEventThread(Application.java:441)
    at com.sun.glass.ui.Screen.setEventHandler(Screen.java:369)
    at com.sun.javafx.tk.quantum.QuantumToolkit.setScreenConfigurationListener(QuantumToolkit.java:728)
    at javafx.stage.Screen.<clinit>(Screen.java:74)
    ... 4 more
Lkmuraz
  • 121
  • 1
  • 1
  • 6
  • 1
    UI events are serialized by only going through the main event thread. The right way to do something like this is to post an event to the event loop and have the event thread handle it. – mrmcgreg Aug 24 '20 at 13:39
  • You should also only have one `Application` instance per application. The purpose of that object is to manage the entire application lifecycle (via the `init()`, `start()`, and `stop()` methods). Your class representing each client/window should not be a subclass of `Application`. – James_D Aug 24 '20 at 13:50

1 Answers1

4

There are several problems with the structure of the application as you have posted it in the question.

The Application class represents the lifecycle of the entire application, which is managed via calls to its init(), start() and stop() methods. There should be only one Application instance in the entire application, and typically this instance is created by the JavaFX startup mechanism so you should not instantiate the Application subclass yourself.

JavaFX applications require the JavaFX runtime to be started, which includes launching the JavaFX Application Thread. This is done via a call to the static Application.launch() method, which must be called only once. The launch() method starts the JavaFX runtime, creates the instance of the Application class, calls init(), and then calls start() on the FX Application Thread. (In JavaFX 9 and later, you can also start the runtime by calling Platform.startup(), but use cases for this are rare).

Note that in your application, there is no call to Application.launch() (or Platform.startup()), so the JavaFX runtime is never started.

Certain operations can only be performed on the FX Application Thread. These include creating Stages and Scenes, and any modifications of properties of UI elements that are already displayed. Thus you cannot "run each client" in a separate thread. This is the cause of your exception: you are trying to create a new Stage on a thread that is not the FX Application Thread.

Each client does not need a new thread to display the UI (and, as described above, cannot do that). You likely do need to perform each client's communication with the server on a separate thread (because those are operations that take a long time, and you should not block the FX Application thread). You can do that by creating a new thread for each client's server communication, or using a shared executor service so that each client can get a thread from a pool (this is probably the preferred approach).

So your structure should look something like this:

public class Client {

    private Parent ui ;
    private ExecutorService exec ; // for handling server communication

    private final String user ;

    public Client(String user, ExecutorService exec) {
        this.user = user ;
        this.exec = exec ;
        try {
            ui = FXMLLoader.load(getClass().getResource("ui/client-management.fxml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Client(String user) {
        this(user, Executors.newSingleThreadExecutor());
    }

    public Parent getUI() {
        return ui ;
    }

    public void showInNewWindow() {
        Scene scene = new Scene(ui);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.show();
    }

    public void checkForNewEmail() {
        Task<List<Email>> newEmailTask = new Task<>() {
            @Override
            protected List<Email> call() throws Exception {
                List<Email> newEmails = new ArrayList<>();
                // contact server and retrieve any new emails
                return newEmails ;
            }
        };
        newEmailTask.setOnSucceeded(e -> {
            List<Email> newEmails = newEmailTask.getValue();
            // update UI with new emails...
        });
        exec.submit(newEmailTask);
    }

    // etc ...
}

Then your ClientController class could do something like this:

public class ClientController {

    private ExecutorService exec = Executors.newCachedThreadPool();

    public void startClients() {
        Client clientA = new Client("aaaa@gmail.com", exec);
        Client clientB = new Client("bbbb@gmail.com", exec);
        clientA.showInNewWindow();
        clientB.showInNewWindow();
    }
}

and your app class can do

public class ClientApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        ClientController controller = new ClientController();
        controller.startClients();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks for the super answer, but if I want to call a method of the Client class from the fxml page controller, how I can set the instance of Client class to the controller fxml related? – Lkmuraz Aug 25 '20 at 12:31
  • @Lucamuraz Just pass whatever you need to the controller [in the usual way](https://stackoverflow.com/questions/14187963/passing-parameters-javafx-fxml) – James_D Aug 25 '20 at 12:32
  • Ok but when i set the `Parent ui` in `Client` class I can't get the `ui` controller. My idea was to get the `ui` controller in the `Client` class and then pass the `Client` instance to the `ui` controller as a parameter – Lkmuraz Aug 25 '20 at 12:58
  • @Lucamuraz I don't really understand that comment, but it's really not clear to me why you couldn't pass the `Client` instance to the controller, using any of the techniques in the link in the previous comments if you needed (which seems unlikely). This sounds like a different question (though it sounds like a duplicate of that link). – James_D Aug 25 '20 at 13:00