Displaying a ListView of Hyperlinks using JSoup as a Service
Asked Answered
L

1

3

I recently needed to collect and display a list of hyperlinks. This helpful example illustrates using and a Task<List<JSoupData>> to collect anchor tags for display in a TableView. As I wanted to reuse the Task, I created a Service<List<Element>> that follows the API example:

private static class SoupService extends Service<ObservableList<Element>> {

    private final StringProperty url = new SimpleStringProperty();

    @Override
    protected Task<ObservableList<Element>> createTask() {
        final var site = url.get();
        ObservableList<Element> list = FXCollections.observableArrayList();
        return new Task<ObservableList<Element>>() {
            @Override
            protected ObservableList<Element> call() throws Exception {
                Document document = Jsoup.connect(site).get();
                Elements elements = document.select("a[href]");
                for (Element element : elements) {
                    list.add(element);
                }
                return list;
            }
        };
    }
  …
}

In this case, List is an ObservableList and Element contains the data to construct and use a Hyperlink. I think I can use a ListCell<Element> as a cell factory to separate the link itself from the text of the link, but I'm not sure how to proceed. Any guidance would be welcome.

Lachesis answered 29/8, 2022 at 12:11 Comment(0)
L
3

The approach looks reasonable, and this ListCell<URL> example focuses on the problem you encounter. The variation below combines your SoupService with a corresponding cell factory. The hyperlink's action handler invokes the service and the updateItem implementation conditions the hyperlink to display the link text:

private final Hyperlink link = new Hyperlink(); { 
   link.setOnAction(e ->
        service.load(getItem().attr("abs:href")));
}

@Override
protected void updateItem(Element item, boolean empty) {
    …
    link.setText(item.text());
    …
}

enter image description here

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

/**
 * @see https://mcmap.net/q/1921234/-displaying-a-listview-of-hyperlinks-using-jsoup-as-a-service/230513
 * @see https://github.com/jhy/jsoup/
 */
public class SoupTest extends Application {

    private static final String HOME = "https://www.example.com";

    @Override
    public void start(Stage stage) {
        var view = new ListView<Element>();
        var url = new TextField(HOME);
        url.setPrefColumnCount(url.getPrefColumnCount() * 2);
        var service = new SoupService();
        service.url.bindBidirectional(url.textProperty());
        service.setOnSucceeded(o -> view.setItems(service.getValue()));
        var home = new Button("Home");
        home.setOnAction(e -> service.load(HOME));
        url.setOnAction(e -> service.restart());
        view.setCellFactory(c -> {
            // https://mcmap.net/q/1869833/-when-click-on-hyperlink-in-javafx-a-relevant-url-should-open-in-browser
            ListCell<Element> cell = new ListCell<>() {
                private final Hyperlink link = new Hyperlink();

                {
                    link.setOnAction(e
                        -> service.load(getItem().attr("abs:href")));
                }

                @Override
                protected void updateItem(Element item, boolean empty) {
                    super.updateItem(item, empty);
                    if (item != null && !empty) {
                        link.setText(item.text());
                        setGraphic(link);
                    } else {
                        setGraphic(null);
                    }
                }
            };
            return cell;
        });
        var progress = new ProgressIndicator();
        progress.visibleProperty().bind(service.runningProperty());
        var stack = new StackPane(view, progress);
        var root = new BorderPane();
        root.setCenter(stack);
        root.setBottom(new ToolBar(url, home));
        stage.setTitle("SoupTest");
        stage.setScene(new Scene(root));
        stage.show();
        service.start();
    }

    private static class SoupService extends Service<ObservableList<Element>> {

        private final StringProperty url = new SimpleStringProperty();

        @Override
        protected Task<ObservableList<Element>> createTask() {
            final var site = url.get();
            ObservableList<Element> list = FXCollections.observableArrayList();
            return new Task<ObservableList<Element>>() {
                @Override
                protected ObservableList<Element> call() throws Exception {
                    Document document = Jsoup.connect(site).get();
                    Elements elements = document.select("a[href]");
                    for (Element element : elements) {
                        list.add(element);
                    }
                    return list;
                }
            };
        }

        private void load(String url) {
            this.url.set(url);
            this.restart();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Lachesis answered 29/8, 2022 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.