1

In short, I have a ScrollPane with a TextFlow that contains hyperlinks (maven repository). When I click on a hyperlink, I want to see the a-tags that the link contains on the same ScrollPane in the same window (or even in a new window). JavaFX is really new to me and I am struggling.

I am using jsoup to extract a-tags from a url.

More detailed: Here is my Main:

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            Parent root = FXMLLoader.load(getClass().getResource("TestFetch.fxml"));
            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

There I call the TestFetch.fxml:

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="575.0" prefWidth="787.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.TestFetch1Controller">
   <children>
      <Button fx:id="btnFetchData" layoutX="368.0" layoutY="54.0" mnemonicParsing="false" onAction="#fetchData" text="Fetch Data" />
      <ScrollPane fx:id="scrollPane" layoutX="46.0" layoutY="131.0" prefHeight="384.0" prefWidth="682.0">
         <content>
            <TextFlow fx:id="txtFlow" prefHeight="381.0" prefWidth="680.0" />
         </content>
      </ScrollPane>
   </children>
</AnchorPane>

This contains a Button (btnFetchData), a ScrollPane and a TextFlow (txtFlow).

The controller is TestFetch1Controller:

public class TestFetch1Controller {
    
    @FXML
    private Button btnFetchData;
    
    @FXML
    private ScrollPane scrollPane;
    
    @FXML
    private TextFlow txtFlow;
    
    public void fetchData(ActionEvent event) throws IOException {
        final String url = "https://repo.maven.apache.org/maven2/avalon/";
        
        Document docConnect = Jsoup.connect(url).get();
        
        Elements links = docConnect.getElementsByTag("a");
        
        for (Element link : links) {
            String linkString = link.attr("href").toString();
            System.out.println(linkString);
            
            Hyperlink hyperlink = new Hyperlink(linkString);
            hyperlink.setOnAction(e -> {
                try {
                    goToLink(event, hyperlink.getText());
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            });
            txtFlow.getChildren().add(hyperlink);
        }
    }

    public void goToLink(ActionEvent event, String hyperlinkURL) throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("TestFetch2.fxml"));
        Parent root2 = loader.load();
        TestFetch2Controller testFetch2Controller = loader.getController();
        testFetch2Controller.addHyperlinksToScrollPane(txtFlow);
        
        
        Stage stage2 = (Stage) ((Node) event.getSource()).getScene().getWindow();
        Scene scene2 = new Scene(root2);
        stage2.setScene(scene2);
        stage2.show();
    }
}

Here I grabbed a public Maven repository, which I filter by a-tags. These then become "hyperlink" which are all added to txtFlow. Now I would like to have the same process again when I click on one of the hyperlinks. So that the a-tags of the hyperlink, which was clicked, are shown to me in the txtFlow.

Since I don't know how to reset the txtFlow, I created a new FXML, the TestFetch2.fxml:

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="476.0" prefWidth="778.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.TestFetch2Controller">
   <children>
      <ScrollPane fx:id="scrollPane2" layoutX="68.0" layoutY="22.0" prefHeight="432.0" prefWidth="642.0" />
   </children>
</AnchorPane>

Here I have only one ScrollPane "scrollPane2". Using "setOnAction" ["goToLink()" in TestFetch1Controller] on hyperlink I wanted to tell each hyperlink to open the "TestFetch2.fxml" when clicked. This works once and then there is a NullPointerException ("stage2" is null). For your information, this is the TestFetch2Controller:

public class TestFetch2Controller {

    @FXML
    private ScrollPane scrollPane2;
    
    public void addHyperlinksToScrollPane(TextFlow textFlow) {
        scrollPane2.setContent(textFlow);
    }
}

So basically my problem is that I don't know how to extract the data (a-tags) of the hyperlink to the next window. I want to be able to keep clicking on the hyperlinks till I came to the .jar.

Please ask any question if something is unclear.

James_D
  • 201,275
  • 16
  • 291
  • 322
Katapa
  • 21
  • 4
  • 1
    [mcve] please .. mind the __M__! f.i. hard-code a static text with a single Hyperlink to demonstrate what you are trying to do and how it doesn't work. Note that the example has to be runnable as-is – kleopatra Jul 28 '22 at 08:25
  • @kleopatra Sorry, I don't really understand that. You mean to write a more simple example? – Katapa Jul 28 '22 at 08:37
  • 1
    exactly - you don't need to parse a real text, just use a simple list or something (that removes the external dependency to jsoup) forgot: add the complete stacktrace (best formatted as code) – kleopatra Jul 28 '22 at 08:42
  • to be honest: I don't understand what you are after - you seem to create a new controller and a new scene on every click on any hyperlink? – kleopatra Jul 28 '22 at 08:48
  • @kleopatra Ah okay, so should I edit this page or should I make a new one? Regarding the next question: Basically I want to clink on a maven-repository hyperlink and I want the content of that hyperlink (i.e. the snapshots) displayed on the same "window". These are also hyperlinks and they should work the same – Katapa Jul 28 '22 at 08:54
  • edit this (and make sure the fxmls are complete, including headers and imports :) But why create a new controller/scene? you will have as many of them as you have hyperlinks.. or what am I missing .. but then: waiting for the example, that's better then words :) – kleopatra Jul 28 '22 at 08:56
  • You are missing nothing, I am sure :P I am a beginner and I just tried to get something to work. There is a lot of redundant or not working code^^ So, it took me some time to work on the code at the top, so I have to see how I can simplify the code without the jsoup – Katapa Jul 28 '22 at 09:05
  • “I don't know how to reset the txtFlow” -> `txtFlow.getChildren().clear()`? – jewelsea Jul 28 '22 at 09:06
  • Can you load the maven url into a WebView instead of doing what you are doing? – jewelsea Jul 28 '22 at 09:08
  • @jewelsea I read about it and maybe it is something I can do, but I have chosen the other way. Maybe I will do it with a WebView if I can't get it to work the other way – Katapa Jul 28 '22 at 09:14
  • darn, that took me a while to detect ;) Technically, the NPE is thrown because you pass around the event received by the _fetch button_ (vs. the event received by the hyperlink) - when clicking the hyperlink after having moved it into a new scene and replaced the original's scene with it, the fetch button no longer has a window .. voting to close as typo. To really fix, it's probably best to re-think your design (there is no need to replace the scene if all you want is to reset source page of the references and re-fill the list of contained references) – kleopatra Jul 28 '22 at 10:42
  • @kleopatra Thank you for your effort! For hours I am trying to re-think my design and I managed to get my code working with hard-code but not with my "jsoup-way". With hard-code I simply created a String and Hyperlink for each URL and gave every Hyperlink a setOnAction(), where I added the correct Hyperlink to the txtFlow. But to do this dynamically with changing URLs is hard^^ Till now, I didn't manage to get a minimal reproducible example of this to post it here without jsoup. Because with jsoup it gives me a headache^^ – Katapa Jul 28 '22 at 12:08
  • PS (becauce no more space): The new code hast just 1 controller and 1 fxml, where the content is displayed on 1 TextFlow – Katapa Jul 28 '22 at 12:12
  • This `WebView` [example](https://stackoverflow.com/a/33824164/230513) works well with your repository link. – trashgod Aug 26 '22 at 11:39

1 Answers1

1

Here is some code that may help you get going.

I altered the code from Jsoup - Printing out info from HTML element SPAN not working?


Main

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

/**
 * JavaFX App
 */
public class App extends Application
{

    String rootDirectory = "https://repo.maven.apache.org/maven2/avalon/";
    
    @Override
    public void start(Stage primaryStage)
    {
        TableView<JSoupData> tvMain = new TableView();
        ObservableList<JSoupData> tableItems = FXCollections.observableArrayList();
        tvMain.setItems(tableItems);

        TableColumn<JSoupData, String> tcTagName = new TableColumn<>("Tag Name");
        tcTagName.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getTagName()));
        TableColumn<JSoupData, String> tcText = new TableColumn<>("Text");
        tcText.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getText()));
        tvMain.getColumns().add(tcTagName);
        tvMain.getColumns().add(tcText);
        tcTagName.prefWidthProperty().bind(tvMain.widthProperty().multiply(0.3));
        tcText.prefWidthProperty().bind(tvMain.widthProperty().multiply(0.7));

        
        TextField tfUrl = new TextField(rootDirectory);
        tfUrl.setPromptText("Enter URL Here!");

        Button btnProcess = new Button("Process URL");
        btnProcess.setOnAction((t) -> {
            btnProcess.setDisable(true);
            tvMain.setDisable(true);
            
            Task<List<JSoupData>> task = scrapper(tfUrl.getText());
            task.setOnSucceeded((WorkerStateEvent t1) -> {
                List<JSoupData> tempList = task.getValue();
                tableItems.setAll(tempList);
                btnProcess.setDisable(false);
                tvMain.setDisable(false);
            });
            Thread thread = new Thread(task);
            thread.setDaemon(true);
            thread.start();
        });
        
        
        tvMain.getSelectionModel().selectedItemProperty().addListener((ov, oldSelection, newSelection) -> {
            if(newSelection != null)
            {
                rootDirectory = rootDirectory + newSelection.getText();
                System.out.println("New Selection: " + rootDirectory);
                tfUrl.setText(rootDirectory);
                
                btnProcess.setDisable(true);
                tvMain.setDisable(true);
            
                Task<List<JSoupData>> task = scrapper(rootDirectory);
                task.setOnSucceeded((WorkerStateEvent t1) -> {
                    List<JSoupData> tempList = task.getValue();
                    tableItems.setAll(tempList);
                    btnProcess.setDisable(false);
                    tvMain.setDisable(false);
                });
                Thread thread = new Thread(task);
                thread.setDaemon(true);
                thread.start();
            }
        });        
    
        VBox root = new VBox(tvMain, tfUrl, btnProcess);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args)
    {
        launch();
    }

    public Task<List<JSoupData>> scrapper(String url)
    {
        Task<List<JSoupData>> scrapperTask = new Task<List<JSoupData>>()
        {
            @Override
            protected List<JSoupData> call()
            {
                List<JSoupData> jSoupDatas = new ArrayList();
                try {
                    System.out.println("url: " + url);
                    Document document = Jsoup.connect(url).get();
                    System.out.println("Title: " + document.title());
                    Elements gamesNames = document.select("a[href]");
                    System.out.println("a[href]");
                    for (Element element : gamesNames) {
                        jSoupDatas.add(new JSoupData(element.tagName(), element.text()));
                        System.out.println("Tag Name: " + element.tagName() + " - Text: " + element.text());
                    }
                }
                catch (IOException e) {
                    System.out.println(e.toString());
                }

                return jSoupDatas;
            }
        };

        return scrapperTask;
    }
}

JSoupData

/*
 * @author sedrick (sedj601)
 */

public class JSoupData
{

    private String tagName;
    private String text;

    public JSoupData(String tagName, String text)
    {
        this.tagName = tagName;
        this.text = text;
    }

    public String getText()
    {
        return text;
    }

    public void setText(String text)
    {
        this.text = text;
    }

    public String getTagName()
    {
        return tagName;
    }

    public void setTagName(String tagName)
    {
        this.tagName = tagName;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("JSoupData{tagName=").append(tagName);
        sb.append(", text=").append(text);
        sb.append('}');
        return sb.toString();
    }
}

Output

enter image description here


Note: This does not account for moving up the directory structure.

SedJ601
  • 12,173
  • 3
  • 41
  • 59