This video shows a two-part JAR signing operations. The first part unsigns the JAR, removing any previous signatures. The second part signs the JAR with the specified certificate. As part of the signing, there are a lot of file operations such as unzipping and copying.
Non-Task Demo
The whole operation takes 6-7 seconds. Notice in the video that the user doesn't get any feedback for about 5 seconds. Then the user gets a "Not Responding" message and hourglass/circle icon for another 2 seconds. Finally, the feedback -- in the form of a JavaFX Label, a ProgressIndicator, and a TextArea -- comes pouring in. This is far too late; the user may attempt to kill the app.
Here is the code for the method "sign". unsignJAR() an signJAR() are the long-running operations. Notice that status updates are sprinkled throughout the calls. The IDE console shows me that the long-running operations are working in sequence and verifying that the program did execute the status updates.
@FXML public void sign() { UnsignCommand unsignCommand = unsignCommandProvider.get(); SignCommand signCommand = signCommandProvider.get(); lblStatus.setText(""); piSignProgress.setProgress(0.1d); try { unsignCommand.unsignJAR( Paths.get(activeProfile.getSourceFileFileName()), Paths.get(activeProfile.getTargetFileFileName()), s -> Platform.runLater(() -> txtConsole.appendText(s + System.getProperty("line.separator")) ) ); piSignProgress.setProgress(0.5d); signCommand.signJAR( Paths.get(activeProfile.getTargetFileFileName()), Paths.get(activeProfile.getJarsignerConfigKeystore()), activeProfile.getJarsignerConfigStorepass(), activeProfile.getJarsignerConfigAlias(), activeProfile.getJarsignerConfigKeypass(), s -> Platform.runLater(() -> txtConsole.appendText(s + System.getProperty("line.separator")) ) ); lblStatus.setText("JAR signed successfully"); piSignProgress.setProgress(1.0d); } catch(CommandExecutionException exc) { logger.error("error unsigning and signing jar", exc); lblStatus.setText("Error signing JAR"); piSignProgress.setProgress(1.0d); Alert alert = new Alert(Alert.AlertType.ERROR, exc.getMessage()); alert.showAndWait(); } }
With Task
This version of the sign() method uses a Task. It keeps the UI lively and gives continuous feedback to the user. I'm using the same long-running operations and making the same status update calls in the same order. The only difference is the Task and adapting some of the calls to the Task class such as overridden methods an bound properties.
This video shows the improved version. The operations still take the same amount of time, but you get a ProgressIndicator right away. As the processing continues, the ProgressIndicator is updated, the status Label is updated, and logging messages scroll onto the TextArea.
The operation takes the same amount of time, but because the user is receiving visual feedback, they are far less inclined to force quit the app or describe it as lagging.
@FXML public void sign() { UnsignCommand unsignCommand = unsignCommandProvider.get(); SignCommand signCommand = signCommandProvider.get(); Task<Void> task = new Task<Void>() { @Override protected Void call() throws Exception { updateMessage(""); updateProgress(0.1d, 1.0d); updateTitle("Unsigning JAR"); unsignCommand.unsignJAR( Paths.get(activeProfile.getSourceFileFileName()), Paths.get(activeProfile.getTargetFileFileName()), s -> Platform.runLater(() -> txtConsole.appendText(s + System.getProperty("line.separator")) ) ); if( isCancelled() ) { return null; } updateProgress(0.5d, 1.0d); updateTitle("Signing JAR"); signCommand.signJAR( Paths.get(activeProfile.getTargetFileFileName()), Paths.get(activeProfile.getJarsignerConfigKeystore()), activeProfile.getJarsignerConfigStorepass(), activeProfile.getJarsignerConfigAlias(), activeProfile.getJarsignerConfigKeypass(), s -> Platform.runLater(() -> txtConsole.appendText(s + System.getProperty("line.separator")) ) ); return null; } @Override protected void succeeded() { super.succeeded(); updateProgress(1.0d, 1.0d); updateMessage("JAR signed successfully"); Platform.runLater( () -> { piSignProgress.progressProperty().unbind(); lblStatus.textProperty().unbind(); }); } @Override protected void failed() { super.failed(); logger.error("error unsigning and signing jar", exceptionProperty().getValue()); updateProgress(1.0d, 1.0d); updateMessage("Error signing JAR"); Platform.runLater(() -> { piSignProgress.progressProperty().unbind(); lblStatus.textProperty().unbind(); Alert alert = new Alert(Alert.AlertType.ERROR, exceptionProperty().getValue().getMessage()); alert.showAndWait(); }); } @Override protected void cancelled() { super.cancelled(); if( logger.isWarnEnabled() ) { logger.warn("signing jar operation cancelled"); } updateProgress(1.0d, 1.0d); updateMessage("JAR signing cancelled"); Platform.runLater(() -> { piSignProgress.progressProperty().unbind(); lblStatus.textProperty().unbind(); Alert alert = new Alert(Alert.AlertType.INFORMATION, "JAR signing cancelle"); alert.showAndWait(); }); } }; piSignProgress.progressProperty().bind(task.progressProperty()); lblStatus.textProperty().bind(task.messageProperty()); new Thread(task).start(); }
Notes on Task Implementation
Other Overrides
Rather than catching any exceptions in the call() method, I'm implementing a failed() method. The call signature of call() has a throws which will allow my CommandExecutionException to be omitted from call().
I've also added a cancelled() implementation. This would be called if the operation were halted in between the long-running unsignJAR() and signJAR() operations. I return if the isCancelled() returns true.
Binding
I'm binding the Task's progress and message properties to my UI controls. This allows me to make JavaFX thread-safe calls within the call() method. I can't call lblStatus.setText() in my threaded-code, but i can call updateMessage() on the Task and have it call lblStatus.setText() in a thread-safe manner.
Notice that I'm also unbinding in the Task methods succeeded, failed, and cancelled. Although the Task is built on-the-fly, piProgressIndicator and lblStatus will be around for the life of the app. I clear these settings in other areas of the app, so I need to break the binding to the now unused Task or I'll get an error about updating bound property.
runLater()
For manipulating the UI outside of binding, I have to provide my own protection. This comes in the form of a Platform.runLater() wrapper which makes my UI code run on the JavaFX thread. Note that the whole thing isn't wrapped up in runLater(). I'd still like to preserve some parallelism with the file-oriented, back-end nature of the unsignJAR() an signJAR() methods, so I'll keep their implements (the file copies, jarsigner, etc) deliberately off the FX thread.
In cases where I do want the unsignJAR() and signJAR() operations to give feedback, I pass in an Observer. This does have to be on the FX thread.
You would think that a call to JavaFX would immediately update the UI, but that's clearly not the case. In my Swing programming, I might be inclined to force a refresh or paint operation in those circumstances. You don't have that these calls in JavaFX, but once you master the use of the Task class, you'll find a suitable technique.
No comments:
Post a Comment