Issue
I am trying to develop an Application, which should be able to run in CLI-Only Environments as well as on GUI Enabled mode. As some of my work is done by JavaFX Threads, I need to start a JavaFX Main Thread without starting the graphic engine, as this will Crash in CLI-only Environments. How do I do this?
I already wrote a first main class, which will decide with the commandline Args, if the GUI will be started or it should run in CLI Mode. The GUI already works, I just need to figure out how to run the FX Main Thread without GUI in another class.
----- EDIT ------
Further Explanation:
Consider I have 2 UI's, one CLI and one GUI. Code is cut into UI Handling and Operations. The UI Handling does only parsing Commandline Arguments in CLI Mode and Drawing a GUI in GUI Mode. The Button Events will just call Code from within the Operations Classes.
This Question and Answer shows which code is lying beneath in my Operations Segment Code Reference
I am now trying to reuse the Code of the operations within my CLI Interface. The code there is shortened, consider the create Method as more code.
As stated above, CLI Mode is designed for Environments, where Graphic Environment can not be started.
Trying to inherit from Application and implementing the start (Stage s)
Method will result within an
UnsupportedOperationException : unable to open DISPLAY
even if the stage will be ignored within the start method
--- second edit ---
Take the Code described in [here] 1
Consider I want to call createKey(length)
not from an Button, but from an second UI which is Commandline Only
private static void progressBar(Task task) {
task.progressProperty().addListener((new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
// Return to line beginning
System.out.print("\r");
int percentage = (int) (100 * task.progressProperty().get());
System.out.format("[%3d%%] %s", percentage, task.messageProperty().get());
if (percentage == 100) {
System.out.println("Finished");
}
}
}));
If I try to run this from a new main class, this will not execute, as the Worker thread waits for an JavaFX Main Thread.
I Need to create a JavaFX Main Thread, which is able to Call progressBar(Task)
but does not try to create a GUI, as this will result in above posted error
---- Edit Three --- I am trying to post this as a minimal example. My Application Startup Looks like this
public static void main(String[] args) {
// If option has been set, start CLI
if (0 < args.length) {
// Start CLI
CLI.Main.execute(args);
} else {
// Trying if GUI can be started
try {
GUI.Main.execute();
}
GUI Main Contains a JavaFX GUI and is working fine, as desired. If we are in an Environment which does not Support drawing a GUI, we have to start the program with arguments. This will invoke the CLI
public class CLI extends Application {
public static void execute(String[] args){
launch(args);
}
@Override
public void start(Stage s) {
progressBar(Creator.createKey());
}
private static void progressBar(Task task) {
task.progressProperty().addListener((new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
// Return to line beginning
System.out.print("\r");
int percentage = (int) (100 * task.progressProperty().get());
System.out.format("[%3d%%] %s", percentage, task.messageProperty().get());
if (percentage == 100) {
System.out.println("Finished");
}
}
}));
}
The GUI will call the Creator within a Button Event
private void createKeyPressed(ActionEvent event) {
// Make Progressbar visible
pbKeyProgress.visibleProperty().set(true);
Task<Void> task = Creator.createKey();
pbKeyProgress.progressProperty().bind(task.progressProperty());
lblKeyProgress.textProperty().bind(task.messageProperty());
}
Consider the Creator.createKey() as
public static Task<Void> createKey() {
Task<Void> task;
task = new Task<Void>() {
final int totalSteps = ... ;
@Override
public Void call() throws Exception {
updateProgress(0, totalSteps);
updateMessage("Start");
doStuff();
updateProgress(1, totalSteps);
updateMessage("First");
doStuff();
updateProgress(2, totalSteps);
updateMessage("Second");
// and so on
return null;
}
};
new Thread(task)
.start();
return task ;
}
Within the GUI, the whole code is working as desired. Now I try to make everything working in a non graphic Environment but by using the same Java Version with installed JavaFX. The Creator.createKey should be callable and executable. If I try to execute the CLI on a Machine which supports GUIs, the Commandline Output is as desired and gets updated, the threads are working. If I try it without the JavaFX Extension within the CLI Main Class, the Threads in Creator will not execute, because there is no JavaFX Main Thread. If I try to execute the solution posted above in an Environment which does not allow to draw a GUI, I get an UnsupportedOperationException : Unable to open Display
Solution
It seems you want to write an application that can be run either in a full JavaFX environment, or in an environment that doesn't have a native graphical toolkit (and so cannot start the JavaFX toolkit). The application requires some threading and you want to avoid replicating code.
The JavaFX concurrency API requires the JavaFX toolkit to work properly: it updates its status on the FX Application Thread, for example, so the FX Application Thread (and consequently the FX toolkit) must be running. So your shared code cannot use the JavaFX concurrency API. (It can, technically, use JavaFX properties, though it's probably cleaner to avoid using these too.)
Suppose you want a simple countdown timer. The user enters the number of seconds to countdown, and the timer then counts down. The application needs to be informed when the number of seconds remaining changes, and when the timer reaches zero. Start with an FX-agnostic class for doing the countdown. It can have fields representing callbacks for the number of seconds remaining and for when the timer finishes:
package countdown;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.IntConsumer;
public class CountdownTimer {
private IntConsumer secondsRemainingChangedHandler ;
private Runnable onFinishedHandler ;
private final Timer timer = new Timer();
private int secondsRemaining ;
public CountdownTimer(int totalSeconds) {
this.secondsRemaining = totalSeconds ;
}
public void setSecondsRemainingChangedHandler(IntConsumer secondsRemainingChangedHandler) {
this.secondsRemainingChangedHandler = secondsRemainingChangedHandler;
}
public void setOnFinishedHandler(Runnable onFinishedHandler) {
this.onFinishedHandler = onFinishedHandler ;
}
private void tick() {
secondsRemaining-- ;
if (secondsRemainingChangedHandler != null) {
secondsRemainingChangedHandler.accept(secondsRemaining);
}
if (secondsRemaining == 0) {
timer.cancel();
if (onFinishedHandler != null) {
onFinishedHandler.run();
}
}
}
public void start() {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
tick();
}
}, 1000, 1000);
}
}
Now you can use this from a purely command line application, with
package countdown.cli;
import java.util.Scanner;
import countdown.CountdownTimer;
public class CLICountdownApp {
public void runApp() {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter time for timer:");
int time = scanner.nextInt() ;
scanner.close();
CountdownTimer timer = new CountdownTimer(time);
timer.setSecondsRemainingChangedHandler(t -> System.out.println(t +" seconds remaining"));
timer.setOnFinishedHandler(() -> System.out.println("Timer finished!"));
timer.start();
}
}
Or you can use it directly in a JavaFX UI. Note that the callbacks are invoked on the background thread created by the java.util.Timer
, so you need to use Platform.runLater()
in the callbacks if you want to update the UI:
int time = Integer.parseInt(timeTextField.getText());
CountdownTimer timer = new CountdownTimer(time);
timer.setSecondsRemainingChangedHandler(t -> Platform.runLater(() -> progressBar.setProgress(1.0*(time-t)/time)));
timer.setOnFinishedHandler(() -> Platform.runLater(() -> label.setText("Timer Complete")));
timer.start();
With a little work, you can wrap it in a Task
. You probably want the task not to complete until the timer completes. Here the callbacks update the task's progress property, and allow the task to complete, respectively. (This is basically an implementation of the "Facade" design pattern, creating a Task
which is a facade to the CountdownTimer
. The Task
, of course is easier to use in a JavaFX environment. Note this is part of the gui
package, which I did because it will only work if the FX toolkit is running.)
package countdown.gui;
import java.util.concurrent.CountDownLatch;
import countdown.CountdownTimer;
import javafx.concurrent.Task;
public class CountdownTask extends Task<Void> {
private final int totalSeconds ;
public CountdownTask(int totalSeconds) {
this.totalSeconds = totalSeconds ;
}
@Override
protected Void call() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
CountdownTimer timer = new CountdownTimer(totalSeconds);
timer.setSecondsRemainingChangedHandler(t -> updateProgress(totalSeconds -t , totalSeconds));
timer.setOnFinishedHandler(() -> latch.countDown());
timer.start();
latch.await();
return null ;
}
}
Then you can use this in the usual JavaFX way:
package countdown.gui;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class JavaFXCountdownApp extends Application {
@Override
public void start(Stage primaryStage) {
ProgressBar progressBar = new ProgressBar(0);
Label label = new Label() ;
TextField timeTextField = new TextField();
timeTextField.setOnAction(e -> {
CountdownTask countdownTask = new CountdownTask(Integer.parseInt(timeTextField.getText()));
progressBar.progressProperty().bind(countdownTask.progressProperty());
countdownTask.setOnSucceeded(evt -> label.setText("Timer finished!"));
Thread t = new Thread(countdownTask);
t.setDaemon(true);
t.start();
});
VBox root = new VBox(5, timeTextField, progressBar, label);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
}
And of course it's trivial to launch this with a main class that switches, based on (for example) the command line args:
package countdown;
import countdown.cli.CLICountdownApp;
import countdown.gui.JavaFXCountdownApp;
import javafx.application.Application;
public class Countdown {
public static void main(String[] args) {
if (args.length == 1 && "cli".equalsIgnoreCase(args[0])) {
new CLICountdownApp().runApp();
} else {
Application.launch(JavaFXCountdownApp.class);
}
}
}
I bundled the complete classes above into a jar file called Countdown.jar
, with the main class countdown.Countdown
specified in the manifest, and tested this on a Mac OS X with JDK 1.8.0_121, and then via a ssh terminal to a Linux box running the same JDK, but without graphical support in the terminal.
Running java -jar Countdown.jar
on the Mac gave the JavaFX UI. Running the same command on the ssh terminal gave, as expected, a java.lang.UnsupportedOperationException
(Unable to open DISPLAY). Running java -jar Countdown.jar cli
on either ran the command line version.
Note this uses no other techniques than simply separating concerns. The CountdownTimer
is general and does not require JavaFX to be running (or even available). The CountdownTask
doesn't do anything specific to the logic of the countdown (which of course would be something much more complex in a real app), but just wraps this up as a JavaFX task, updating the progress on the FX Application thread (via Task.updateProgress(...)
), and exiting when the whole thing is complete, etc. The CLICountdownApp
manages user input and output from/to the console, and the JavaFXCountdownApp
just builds and displays the UI for interacting with the CountdownTask
.
Answered By - James_D
Answer Checked By - Willingham (JavaFixing Volunteer)