0

I'm developing an application to detect phishing websites, in javafx. I have a "predict" button that does all the necessary processing within its EventHandler. I also want to be able to update a ProgressBar with information about how far it's gotten during processing. I'm using a Task for this, calling updateProgress and updateValue with the final result.

However, in the event of an exception, I want to update the UI then immediately terminate execution of the EventHandler, using updateProgress with some error value. However, updateProgress does not update the UI immediately. Is there something that exists that can not only update the UI from inside an EventHandler, like Task, but also give me control over exactly when the UI updates?

For reference, here's my complete event handler code:

 predict.setOnAction(new EventHandler<ActionEvent>() {

        @Override
        public void handle(ActionEvent event)
        {
            Task<Result> task = new Task<Result>()
            {
                @Override
                protected Result call() throws Exception
                {
                    String url = urlText.getText();
                    ArffData arffData = new ArffData();
                    try
                    {
                        updateProgress(1, 10);
                        URL uri = new URL(url);

                        String domain = uri.getHost();
                        arffData.setUrlSimilarity(DataGatherer.readLevenshtein(domain));
                        updateProgress(2, 10);
                        boolean redirection = DataGatherer.getRedirectionStatus(url);

                        arffData.setRedirection(redirection);
                        updateProgress(3, 10);
                        Response response = Jsoup.connect(url).execute();
                        arffData.setSpellingErrors(DataGatherer.getSpellingErrors(response).size());
                    }
                    catch (IOException e1)
                    {
                        updateProgress(-1, 10); //Should update UI before terminating
                        return null;
                    }



                    Classifier rf;
                    Instances instances;
                    try
                    {
                        updateProgress(4, 10);
                        rf = (Classifier) SerializationHelper.read("RF100.model");
                        instances = new DataSource("phishingData.arff").getDataSet();
                    }
                    catch (Exception e1)
                    {
                        updateProgress(-1, 10);
                        return null;
                    }
                    if (instances.classIndex() == -1)
                        instances.setClassIndex(instances.numAttributes() - 1);
                    String offers = offerText.getValue();
                    String lf = lfText.getValue();
                    updateProgress(5, 10);
                    Instance inst = InstanceSetup.setUpInstance(arffData, offers, lf, instances);

                    try
                    {
                        updateProgress(6, 10);
                        double clsLabel = rf.classifyInstance(inst);
                        instances.add(inst); 
                        rf.buildClassifier(instances);
                        SerializationHelper.write("RF100.model", rf);
                        Evaluation eval = new Evaluation(instances);
                        eval.crossValidateModel(rf, instances, 10, new Random(1));
                        boolean phishing = clsLabel ==0 ?true: false;
                        Result result = new Result(phishing, eval.pctCorrect());
                        updateProgress(10, 10);
                        if(clsLabel == 0)
                        {
                            predictionLabel.setText("the given website IS a phishing website.");
                        }
                        else
                        {
                            predictionLabel.setText("the given website IS NOT a phishing website.");
                        }
                        updateValue(result);
                        accuracyLabel.setText("PhishGuard is " + String.format("%.4f%%", eval.pctCorrect()) + 
                                " confident in this prediction.");
                        return result;

                    }
                    catch (Exception e)
                    {
                        updateProgress(-1, 10);
                        return null;
                    }

                }                   
            };

            task.progressProperty().addListener(new ChangeListener<Number>()
            {

                @Override
                public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
                {
                    switch(newValue.intValue())
                    {
                        case 1:
                        {
                            pBar.setProgress(10);
                            progressLabel.setText(progressLabels[1]);
                        }
                        case 2:
                        {
                            pBar.setProgress(30);
                            progressLabel.setText(progressLabels[2]);
                        }
                        case 3:
                        {
                            pBar.setProgress(50);
                            progressLabel.setText(progressLabels[3]);
                        }
                        case 4:
                        {
                            pBar.setProgress(70);
                            progressLabel.setText(progressLabels[4]);
                        }
                        case 5:
                        {
                            pBar.setProgress(80);
                            progressLabel.setText(progressLabels[5]);                               
                        }
                        case 6:
                        {
                            pBar.setProgress(90);
                            progressLabel.setText(progressLabels[6]);
                        }
                        case 10:
                        {
                            pBar.setProgress(100);
                            progressLabel.setText(progressLabels[0]);
                        }
                        case -1:
                        {
                            predictionLabel.setText("a prediction could not be made.");
                            accuracyLabel.setText("");
                            pBar.setProgress(0);
                            progressLabel.setText(progressLabels[0]);
                        }

                    }


                }

            });

            task.valueProperty().addListener(new ChangeListener<Result>(){

                @Override
                public void changed(ObservableValue<? extends Result> observable, Result oldValue, Result newValue)
                {
                    // TODO Auto-generated method stub
                    boolean phishing = newValue.isPhishing();
                    if(phishing)
                    {
                        predictionLabel.setText("the given website IS a phishing website.");
                    }
                    else
                    {
                        predictionLabel.setText("the given website IS NOT a phishing website.");
                    }

                    accuracyLabel.setText("PhishGuard is " + String.format("%.4f%%", newValue.getAccuracy()) + 
                            " confident in this prediction.");

                }

            });
            new Thread(task).start();
        }});
  • I'm not exactly sure what you are asking. Perhaps it is: [Can I pause a background Task / Service?](http://stackoverflow.com/questions/14941084/javafx2-can-i-pause-a-background-task-service). Perhaps it is not... – jewelsea Mar 07 '17 at 21:31
  • [tasks have in-built exception handling](http://stackoverflow.com/questions/25863480/javafxs-task-seem-to-consume-exceptions-is-it-a-bug-or-a-feature), so perhaps using that might be better than "using updateProgress with some error value". – jewelsea Mar 07 '17 at 21:35
  • The [Task javadoc](https://docs.oracle.com/javase/8/javafx/api/javafx/concurrent/Task.html) has info on how to cancel a Task, perhaps that is what you mean by "halting execution", though you also mention "immediately terminate execution of the EventHandler" and that seems unrelated to the Task, so I'm not sure if that is helpful. Immediate termination of stuff running on multiple threads is often a bad idea as you get race conditions. Usually, you need some kind of co-operative communication for graceful cancellation. – jewelsea Mar 07 '17 at 21:36
  • use Platform.runLater see http://stackoverflow.com/questions/15160410/usage-of-javafx-platform-runlater-and-access-to-ui-from-a-different-thread/15160824#15160824 and http://stackoverflow.com/questions/16708931/javafx-working-with-threads-and-gui – guillaume girod-vitouchkina Mar 07 '17 at 21:39
  • @jewelsea what I'm trying to do is do the necessary processing, periodically update the UI, and update the UI with an error message if an exception is raised. The latter of these - which I'll admit is all I've currently tested - terminates execution BEFORE updating the UI, which obviously isn't what I want. – Sophie Brown Mar 07 '17 at 21:43
  • 1
    Isn't a progress value always between 0 and 1? – James_D Mar 07 '17 at 21:47
  • It doesn't make much sense to me. You declare a Task within the EventHandler and the last statement in the EventHandler is to start the task. The EventHandler does nothing else. This means the EventHandler effectively "terminates" and returns the control of the JavaFX thread to the JavaFX system immediately (regardless of whether any exception is raised in the Task, which is running on a different thread anyway). Perhaps you mean termination of the Task rather than the event handler, but that is not what you state in the question. – jewelsea Mar 07 '17 at 21:50
  • For a property that is not 0 to 1 [workDone](https://docs.oracle.com/javase/8/javafx/api/javafx/concurrent/Task.html#workDoneProperty) is probably the thing you want, not [progress](https://docs.oracle.com/javase/8/javafx/api/javafx/concurrent/Task.html#progressProperty). – jewelsea Mar 07 '17 at 21:56
  • @jewelsea I'll admit, I've never done much concurrency so the fundamentals are new to me. All I really want is the UI updates to be done at the specified points in the code. as if updateProgress encapsulated the UI update code. – Sophie Brown Mar 07 '17 at 22:00
  • See the first question I linked on pausing a Task, it probably provides some insights into how you might go about this. Create `FutureTask` to update the UI and run it on the JavaFX thread using `Platform.runLater`, use `get()` on the FutureTask from within your Task thread to pause the task until the UI operation has completed. – jewelsea Mar 07 '17 at 22:06
  • I see. So using this method will I not need the original `Task`, because the UI updates are done within `FutureTask`? – Sophie Brown Mar 07 '17 at 22:35
  • For an exception, you can just throw the exception from the task's `call()` method, which will obviously terminate execution of the task. Set an `onFailed` handler with the task, where you can update the UI to reflect that an exception was thrown (the handler is executed on the FX Application thread). – James_D Mar 07 '17 at 22:57

1 Answers1

1

The progressProperty() both of a Task and of a ProgressBar are either INDETERMINATE or are set to a value between 0 and 1 (inclusive). For the progress bar:

A positive value between 0 and 1 indicates the percentage of progress where 0 is 0% and 1 is 100%. Any value greater than 1 is interpreted as 100%.

So when you observe the progress value of the task, and then get the intValue(), there are only three possibilities: -1 (when the progress is indeterminate), 1 (when the task is complete), or 0 (all other values). When you register the listener with the task's progressProperty, it is already in an INDETERMINATE state, so the first change you see has an intValue() of 0 (matching none of your cases), and all subsequent changes see the same value until the task is complete, when you see 1. At that point you set the progress of the progress bar to 10, which, as above, is interpreted as 100%.

Probably what you really want to do is just bind the progress property of the progress bar to the progress property of the task (so they just increase together).

To update text periodically, you can use the task's messageProperty.

So I would remove the listener on the task's progress property entirely, and replace it with

pBar.progressProperty().bind(task.progressProperty());
pBar.textProperty().bind(task.messageProperty());

Call updateMessage() as needed in the task with each message you need (code later).

For this:

However, in the event of an exception, I want to update the UI then immediately terminate execution of the EventHandler

I assume you really mean "terminate execution of the Task", since the event handler has long since completed. As noted in the comments, Tasks handle exceptions by default. So you can just let the exception propagate from the call() method, and do the UI updates in an onFailed handler.

The result looks something like this:

predict.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent event)
    {
        Task<Result> task = new Task<Result>()
        {
            @Override
            protected Result call() throws Exception
            {
                String url = urlText.getText();
                ArffData arffData = new ArffData();

                updateProgress(1, 10);
                updateMessage(progressLabels[1]);
                URL uri = new URL(url);

                String domain = uri.getHost();
                arffData.setUrlSimilarity(DataGatherer.readLevenshtein(domain));
                updateProgress(2, 10);
                updateMessage(progressLabels[2]);

                boolean redirection = DataGatherer.getRedirectionStatus(url);

                arffData.setRedirection(redirection);
                updateProgress(3, 10);
                updateMessage(progressLabels[3]);
                Response response = Jsoup.connect(url).execute();
                arffData.setSpellingErrors(DataGatherer.getSpellingErrors(response).size());






                Classifier rf;
                Instances instances;

                updateProgress(4, 10);
                updateMessage(progressLabels[4]);
                rf = (Classifier) SerializationHelper.read("RF100.model");
                instances = new DataSource("phishingData.arff").getDataSet();
                if (instances.classIndex() == -1)
                    instances.setClassIndex(instances.numAttributes() - 1);
                String offers = offerText.getValue();
                String lf = lfText.getValue();
                updateProgress(5, 10);
                updateMessage(progressLabels[5]);

                Instance inst = InstanceSetup.setUpInstance(arffData, offers, lf, instances);

                updateProgress(6, 10);
                updateMessage(progressLabels[6]);

                double clsLabel = rf.classifyInstance(inst);
                instances.add(inst); 
                rf.buildClassifier(instances);
                SerializationHelper.write("RF100.model", rf);
                Evaluation eval = new Evaluation(instances);
                eval.crossValidateModel(rf, instances, 10, new Random(1));
                boolean phishing = clsLabel ==0 ?true: false;
                Result result = new Result(phishing, eval.pctCorrect());
                updateProgress(10, 10);
                updateMessage(progressLabels[0]);

                if(clsLabel == 0)
                {
                    predictionLabel.setText("the given website IS a phishing website.");
                }
                else
                {
                    predictionLabel.setText("the given website IS NOT a phishing website.");
                }
                updateValue(result);
                accuracyLabel.setText("PhishGuard is " + String.format("%.4f%%", eval.pctCorrect()) + 
                        " confident in this prediction.");
                return result;


            }                   
        };

        pBar.progressProperty().bind(task.progressProperty());
        pLabel.textProperty().bind(task.messageProperty());


        task.valueProperty().addListener(new ChangeListener<Result>(){

            @Override
            public void changed(ObservableValue<? extends Result> observable, Result oldValue, Result newValue)
            {
                // TODO Auto-generated method stub
                boolean phishing = newValue.isPhishing();
                if(phishing)
                {
                    predictionLabel.setText("the given website IS a phishing website.");
                }
                else
                {
                    predictionLabel.setText("the given website IS NOT a phishing website.");
                }

                accuracyLabel.setText("PhishGuard is " + String.format("%.4f%%", newValue.getAccuracy()) + 
                        " confident in this prediction.");

            }

        });

        task.setOnFailed(e -> {
            predictionLabel.setText("a prediction could not be made.");
            accuracyLabel.setText("");
            pBar.progressProperty().unbind();
            pBar.setProgress(0);
            progressLabel.setText(progressLabels[0]);
        });

        new Thread(task).start();
    }});
James_D
  • 201,275
  • 16
  • 291
  • 322