I've written a small Spring Boot/Vaadin application that displays a simple UI to take user input and make a call to another service that takes some time to run. When the task is submitted, I'm displaying a progress dialog that shows a progress bar, a message informing the user what is going on and a close button to allow them to close the dialog when the job completes. I'm using a ListenableFuture to be notified when the task is done.
I can get the dialog to appear with status of "executing" and the progress bar doing its thing, but when the task is done (I have debug statements going to the console to let me know), it's not triggering the logic to update the status message and enable the close button. I can't figure out what I'm doing wrong.
Here's the code:
MainView1.java
#Route("rescheduleWorkOrders1")
#CssImport("./styles/shared-styles.css")
public class MainView1 extends VerticalLayout {
...
private final BackendService service;
public MainView1(BackendService service) {
this.service = service;
configureView();
addSubmitButton();
bindFields();
}
private void addSubmitButton() {
Button submit = new Button("Submit", this::submit);
add(submit);
}
private void submit(ClickEvent<?> event) {
UserData data = binder.getBean();
ListenableFuture<ResponseEntity<String>> future = service.executeTask(data);
ProgressDialog dialog = new ProgressDialog(future);
dialog.open();
}
private void configureView() {
...
}
private void bindFields() {
...
}
}
ProgressDialog.java
public class ProgressDialog extends Dialog {
private final ListenableFuture<ResponseEntity<String>> future;
private ProgressBar bar;
private Paragraph message;
private Button close;
public ProgressDialog(ListenableFuture<ResponseEntity<String>> future) {
super();
this.future = future;
configureView();
this.future.addCallback(new ListenableFutureCallback<>() {
#Override
public void onSuccess(ResponseEntity<String> result) {
message.setText("Task complete. Status: " + result.getStatusCode());
bar.setVisible(false);
close.setEnabled(true);
}
#Override
public void onFailure(Throwable ex) {
message.setText(ex.getMessage());
bar.setVisible(false);
close.setEnabled(true);
}
});
}
private void configureView() {
bar = new ProgressBar();
bar.setIndeterminate(true);
bar.setVisible(true);
message = new Paragraph("Executing task ...");
close = new Button("Close", this::close);
close.setEnabled(false);
add(bar, message, close);
}
private void close(ClickEvent<?> event) {
this.close();
}
}
BackendService.java
#Service
public class BackendService {
#Async
public ListenableFuture<ResponseEntity<String>> executeTask(UserData data) {
...
RestTemplate template = new RestTemplate();
ResponseEntity<String> response = template.postForEntity(uri, entity, String.class);
System.out.println(response);
return AsyncResult.forValue(response);
}
}
Note: I do have #EnableAsync specified in a #Configuration annotated class.
When dealing with asynchronous code in Vaadin you need to:
Use UI#access when updating the UI outside an active request. This acquires a lock on the UI, to prevent it being updated by two threads simultaneously.
Enable server push by adding the #Push annotation to your main layout or view. This allows the server to push updates to the client even if no request is active.
Without the former, you can get ConcurrentModificationExceptions in the best case, and very subtle bugs in the worst.
Without the latter, the changes will be applied (i.e. dialog closed), but the changes will only be sent to the client the next time the client sends a request. I believe this is your main issue.
More information can be found in the documentation.
Related
I have written a piece of code for downloading a file from internet (in background service) and showing the progress of download in a popup stage. The code compiles successfully and there is no runtime error. However no download takes place and progress indicator remains indeterminate.
The code is tailored for illustrating my point. Please have a look at it and let me understand where I have gone wrong.
Thanks!
public class ExampleService extends Application {
URL url;
Stage stage;
public void start(Stage stage)
{
this.stage = stage;
stage.setTitle("Hello World!");
stage.setScene(new Scene(new StackPane(addButton()), 400, 200));
stage.show();
}
private Button addButton()
{
Button downloadButton = new Button("Download");
downloadButton.setOnAction(new EventHandler<ActionEvent>()
{
public void handle(ActionEvent e)
{
FileChooser fileSaver = new FileChooser();
fileSaver.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF", "pdf"));
File file = fileSaver.showSaveDialog(stage);
getDownloadService(file).start();
}
});
return downloadButton;
}
private Service getDownloadService(File file)
{
Service downloadService = new Service()
{
protected Task createTask()
{
return doDownload(file);
}
};
return downloadService;
}
private Task doDownload(File file)
{
Task downloadTask = new Task<Void>()
{
protected Void call() throws Exception
{
url = new URL("http://www.daoudisamir.com/references/vs_ebooks/html5_css3.pdf");
// I have used this url for this context only
org.apache.commons.io.FileUtils.copyURLToFile(url, file);
return null;
}
};
showPopup(downloadTask);
return downloadTask;
}
Popup showPopup(Task downloadTask)
{
ProgressIndicator progressIndicator = new ProgressIndicator();
progressIndicator.progressProperty().bind(downloadTask.progressProperty());
Popup progressPop = new Popup();
progressPop.getContent().add(progressIndicator);
progressPop.show(stage);
return progressPop;
// I have left out function to remove popup for simplicity
}
public static void main(String[] args)
{
launch(args);
}}
The line:
org.apache.commons.io.FileUtils.copyURLToFile(url, file);
...doesn't provide you any information about the progress of your download (there is no callback or any other indication of its progress). It just downloads something without giving you feedback.
You will have to use something else that gives you feedback on the progress.
Take a look at this questions answers for solutions with feedback (it is for Swing, but you should be able to adapt them for JavaFX): Java getting download progress
You bind the ProgressIndicator's progress property to the Task's progress property, so that changes in the latter will be reflected in the former. However you never actually update your Task's progress.
If you want the progress indicator to show something, you're going to have to call updateProgress(workDone, max) within your task's body (or elsewhere). And that might be tricky if the download logic you're using doesn't give you any progress callbacks. (You could, perhaps, spawn a thread to repeatedly check the size of the file on the filesystem and use that as your current workDone; but you'd need to know what the eventual/complete size of the file would be in order to turn this into a percentage, which may or may not be easy.)
btnDownloadExcel.addClickHandler(new ClickHandler()
{
public void onClick(ClickEvent event)
{
ApplicationController.isLogout = false;
final ProgressBarWindow progressBarWindow = newProgressBarWindow();
DSRequest dsRequestProperties = new DSRequest();
dsRequestProperties.setExportAs((ExportFormat)
ExportFormat.OOXML);
dsRequestProperties.setExportDatesAsFormattedString(true);
dsRequestProperties.setExportResults(true);
dsRequestProperties.setExportToClient(true);
dsRequestProperties.setExportFilename("DownloadLogs.xlsx");
String title = "Export log";
String message = "Exporting data...";
progressBarWindow.setShowMinimizeButton(false);
progressBarWindow.setIsModal(true);
progressBarWindow.setShowModalMask(true);
progressBarWindow.initiate(title, message);
mainGrid.exportData(dsRequestProperties);
progressBarWindow.show();
Timer timer = new Timer() {
#Override
public void run(){
progressBarWindow.hide();
}
};
timer.schedule(50000);
}
});
hstack.addMember(btnDownloadExcel);
return hstack;
Instead of Timer, I want to use callback to hide the progress bar
once export document is done. Timer is not hiding progress bar on time.
I don't think you can do it like that.
The docs for the callback parameter of exportData says:
Note that this parameter only applies where DSRequest.exportToClient is explicitly set to false, because file downloads do not provide ordinary SmartClient callbacks
I think that the only way to do it is using a 'server push' mechanism, for example the RealtimeMessaging module of SmartGWT
I've created a class to download raw data from url(api): GetRawData - GitHub
I extend the class:
GetTVShowDetailsJsonData - GitHub
So, my problem is, I call GetTVShowDetailsJsonData.java on my activity
public class ProcessTVShowsDetails extends GetTVShowDetailsJsonData {
private ProgressDialog progress;
public ProcessTVShowsDetails(TVShow show) {
super(show, TVShowDetailsActivity.this);
}
public void execute() {
// Start loading dialog
progress = ProgressDialog.show(TVShowDetailsActivity.this, "Aguarde...", "Estamos carregando os dados da série.", true);
// Start process data (download and get)
ProcessData processData = new ProcessData();
processData.execute();
}
public class ProcessData extends DownloadJsonData {
protected void onPostExecute(String webData) {
super.onPostExecute(webData);
mTVShowDetails = getTVShowsDetails();
bindParams();
// Close loading dialog.
if (progress.isShowing()) progress.dismiss();
}
}
}
But inside this call, I want to call another download another JSON from other url, using GetSeasonJsonData.java, is the same GetTVShowDetailsJsonData.java do, but for another PARSE.I need to wait the task being completed or maybe do in synchronous way, to add the result from GetSeasonJsonData.java inside the first result from GetTvShowDetailsJsonData. How can I do that?
Just a note, i need to run more than one time, and I already try this, but doesn't work:
// Process and execute data into recycler view
public class ProcessTVShowsDetails extends GetTVShowDetailsJsonData {
private ProgressDialog progress;
public ProcessTVShowsDetails(TVShow show) {
super(show, TVShowDetailsActivity.this);
}
public void execute() {
// Start loading dialog
progress = ProgressDialog.show(TVShowDetailsActivity.this, "Aguarde...", "Estamos carregando os dados da série.", true);
// Start process data (download and get)
ProcessData processData = new ProcessData();
processData.execute();
}
public class ProcessData extends DownloadJsonData {
protected void onPostExecute(String webData) {
super.onPostExecute(webData);
mTVShowDetails = getTVShowsDetails();
//Process SeasonData
for(int seasonNumber = 1; seasonNumber <= mTVShowDetails.getNumberOfSeasons(); seasonNumber++) {
ProcessSeason processSeason = new ProcessSeason(mTVShowDetails.getId(), seasonNumber);
processSeason.execute();
}
bindParams();
// Close loading dialog.
if (progress.isShowing()) progress.dismiss();
}
}
}
// Process Season Data
public class ProcessSeason extends GetTVShowSeasonJsonData {
public ProcessSeason(int showId, int serieNumber) {
super(showId, serieNumber, TVShowDetailsActivity.this);
}
public void execute() {
// Start process data (download and get)
ProcessData processData = new ProcessData();
processData.execute();
}
public class ProcessData extends DownloadJsonData {
protected void onPostExecute(String webData) {
super.onPostExecute(webData);
}
}
}
How can I safely update the widgets on a JavaFX GUI from within a JavaFX Service. I remember when I was developing with Swing, I used to 'invoke later' and other various swing worker utilities to ensure that all updates to the UI were handled safely in the Java Event Thread. Here is an example of a simple service thread that handles datagram messages. The bit that is missing is where the datagram message is parsed and corresponding UI widgets are updated. As you can see the service class is very simplistic.
I'm not sure if I need to use simple binding properties (like message) or alternatively should I should pass widgets to the constructor of my StatusListenerService (which is probably not the best thing to do). Can someone give me a good similar example that I would work from.
public class StatusListenerService extends Service<Void> {
private final int mPortNum;
/**
*
* #param aPortNum server listen port for inbound status messages
*/
public StatusListenerService(final int aPortNum) {
this.mPortNum = aPortNum;
}
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
updateMessage("Running...");
try {
DatagramSocket serverSocket = new DatagramSocket(mPortNum);
// allocate space for received datagrams
byte[] bytes = new byte[512];
//message.setByteBuffer(ByteBuffer.wrap(bytes), 0);
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
while (!isCancelled()) {
serverSocket.receive(packet);
SystemStatusMessage message = new SystemStatusMessage();
message.setByteBuffer(ByteBuffer.wrap(bytes), 0);
}
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
updateMessage("Cancelled");
return null;
}
};
}
}
The "low-level" approach is to use Platform.runLater(Runnable r) to update the UI. This will execute r on the FX Application Thread, and is the equivalent of Swing's SwingUtilities.invokeLater(...). So one approach is simply to call Platform.runLater(...) from inside your call() method and update the UI. As you point out, though, this essentially requires the service knowing details of the UI, which is undesirable (though there are patterns that work around this).
Task defines some properties and has corresponding updateXXX methods, such as the updateMessage(...) method you call in your example code. These methods are safe to call from any thread, and result in an update to the corresponding property to be executed on the FX Application Thread. (So, in your example, you can safely bind the text of a label to the messageProperty of the service.) As well as ensuring the updates are performed on the correct thread, these updateXXX methods also throttle the updates, so that you can essentially call them as often as you like without flooding the FX Application Thread with too many events to process: updates that occur within a single frame of the UI will be coalesced so that only the last such update (within a given frame) is visible.
You could leverage this to update the valueProperty of the task/service, if it is appropriate for your use case. So if you have some (preferably immutable) class that represents the result of parsing the packet (let's call it PacketData; but maybe it is as simple as a String), you make
public class StatusListener implements Service<PacketData> {
// ...
#Override
protected Task<PacketData> createTask() {
return new Task<PacketData>() {
// ...
#Override
public PacketData call() {
// ...
while (! isCancelled()) {
// receive packet, parse data, and wrap results:
PacketData data = new PacketData(...);
updateValue(data);
}
return null ;
}
};
}
}
Now you can do
StatusListener listener = new StatusListener();
listener.valueProperty().addListener((obs, oldValue, newValue) -> {
// update UI with newValue...
});
listener.start();
Note that the value is updated to null by the code when the service is cancelled, so with the implementation I outlined you need to make sure that your listener on the valueProperty() handles this case.
Also note that this will coalesce consecutive calls to updateValue() if they occur within the same frame rendering. So this is not an appropriate approach if you need to be sure to process every data in your handler (though typically such functionality would not need to be performed on the FX Application Thread anyway). This is a good approach if your UI is only going to need to show the "most recent state" of the background process.
SSCCE showing this technique:
import java.util.Random;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class LongRunningTaskExample extends Application {
#Override
public void start(Stage primaryStage) {
CheckBox enabled = new CheckBox("Enabled");
enabled.setDisable(true);
CheckBox activated = new CheckBox("Activated");
activated.setDisable(true);
Label name = new Label();
Label value = new Label();
Label serviceStatus = new Label();
StatusService service = new StatusService();
serviceStatus.textProperty().bind(service.messageProperty());
service.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue == null) {
enabled.setSelected(false);
activated.setSelected(false);
name.setText("");
value.setText("");
} else {
enabled.setSelected(newValue.isEnabled());
activated.setSelected(newValue.isActivated());
name.setText(newValue.getName());
value.setText("Value: "+newValue.getValue());
}
});
Button startStop = new Button();
startStop.textProperty().bind(Bindings
.when(service.runningProperty())
.then("Stop")
.otherwise("Start"));
startStop.setOnAction(e -> {
if (service.isRunning()) {
service.cancel() ;
} else {
service.restart();
}
});
VBox root = new VBox(5, serviceStatus, name, value, enabled, activated, startStop);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private static class StatusService extends Service<Status> {
#Override
protected Task<Status> createTask() {
return new Task<Status>() {
#Override
protected Status call() throws Exception {
Random rng = new Random();
updateMessage("Running");
while (! isCancelled()) {
// mimic sporadic data feed:
try {
Thread.sleep(rng.nextInt(2000));
} catch (InterruptedException exc) {
Thread.currentThread().interrupt();
if (isCancelled()) {
break ;
}
}
Status status = new Status("Status "+rng.nextInt(100),
rng.nextInt(100), rng.nextBoolean(), rng.nextBoolean());
updateValue(status);
}
updateMessage("Cancelled");
return null ;
}
};
}
}
private static class Status {
private final boolean enabled ;
private final boolean activated ;
private final String name ;
private final int value ;
public Status(String name, int value, boolean enabled, boolean activated) {
this.name = name ;
this.value = value ;
this.enabled = enabled ;
this.activated = activated ;
}
public boolean isEnabled() {
return enabled;
}
public boolean isActivated() {
return activated;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
public static void main(String[] args) {
launch(args);
}
}
I have a javafx program which I want to use to analyze a website. For the start I just want to print the sourcecode of a site into a TextArea, but before that, I write "loading website sourcecode..."
My target is to write "loading website sourcecode..." first, and then after some seconds when the parsing of the site is finished, add that sourcecode.
At the Moment when I press the button, nothing happens and after 3-5 seconds the "loading website sourcecode..." message and the sourcecode is displayed at once.
So I actually want to show strings one after another. I already googled for 2 hours and tried things with threads, invokelater, platform.runLater() and so on but nothing worked, the code is simple.
ModelView.java - Controller Class
package root;
import javafx.application.Platform;
...
public class ModelView {
#FXML public TextField UrlInput;
// This gets called when the button is pressed
public void checkUrl() throws InterruptedException
{
String url = UrlInput.getText();
LogWriter lw = new LogWriter();
lw.printMsg("loading website sourcecode...");
lw.printMsg(HTMLParser.directUrlToCode(url));
}
}
LogWriter.java
package root;
import javafx.beans.value.ObservableValue;
...
public class LogWriter extends Thread{
#FXML TextArea Log;
public LogWriter()
{
Log = (TextArea) Main.scene.lookup("#Log");
}
void printMsg(String s)
{
Log.setText(this.Log.getText()+"\n"+s);
}
}
EDIT:
There is not much to say about the HTMLParser methods, but I add that it extends Thread.
I tried changing ModelView.java to that:
ModelView.java - version 2
package root;
import javafx.application.Platform;
...
public class ModelView<V> {
#FXML public TextField UrlInput;
// This gets called when the button is pressed
public void checkUrl() throws InterruptedException
{
String url = UrlInput.getText();
LogWriter lw0 = new LogWriter();
lw0.start();
lw0.printMsg("loading website sourcecode...");
HTMLParser hp = new HTMLParser();
hp.start();
LogWriter lw1 = new LogWriter();
lw1.start();
lw1.printMsg(hp.directUrlToCode(url));
}
}
Still the same effect.
EDIT2:
This is another version I tried, in this case, "loading website sourcecode..." is not even displaying, I am going on with my tries...
ModelView.java checkUrl() - Version 3
public void checkUrl() throws InterruptedException
{
String url = UrlInput.getText();
Task<Void> task = new Task<Void>(){
#Override protected Void call() throws Exception {
if(isCancelled()) {
updateMessage("Cancelled");
LogWriter lw = new LogWriter();
lw.printMsg("loading website sourcecode...");
}
return null;
}
};
Thread t = new Thread(task);
t.start();
HTMLParser hp = new HTMLParser();
LogWriter lw1 = new LogWriter();
lw1.printMsg(hp.directUrlToCode(url));
}
First of all, manipulating an observable variable from outside of the JavaFX application thread is a bad idea. You won't be able to bind other variables to it (you'll get IllegalStateExceptions)
Second, I'd implement LogWriter like this:
// ...
public class LogWriter {
private final TextArea txtLog;
public LogWriter(TextArea txtLog) {
this.txtLog = txtLog;
}
void printMsg(final String s) {
if (!Platform.isFxApplicationThread()) {
Platform.runLater(new Runnable() {
#Override
public void run() {
printMsg(s);
}
});
} else {
txtLog.setText(txtLog.getText() + "\n" + s);
}
}
}
and use it this way:
//...
#FXML
private TextArea txtLog;
// ...
public void checkUrl() {
final String url = UrlInput.getText();
final LogWriter lw = new LogWriter(txtLog);
Task<String> task = new Task<String>() {
#Override
protected String call() {
lw.printMsg("loading website sourcecode...");
return HTMLParser.directUrlToCode(url);
}
#Override
protected void succeeded() {
super.succeeded();
lw.printMsg(getValue());
}
#Override
protected void failed() {
super.failed();
if (getException() != null) {
getException().printStackTrace();
}
lw.printMsg("failed!");
}
};
new Thread(task).start();
}
Note that HTMLParser does not need to extend Thread.