1
\$\begingroup\$

I wrote a small JavaFX Application to inspect Javascript within HTML pages. It loads and browses sites, shows the HTML source, shows an overview of the text of a page, provides a simple Javascript terminal, and runs Javscript files from disk. I'm thinking with some careful perusing it can be at least a little shorter and more succinct. How can this implementation be enhanced?

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.MenuItemBuilder;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class Browser extends Application {

    private static final String help = ""
        + "dir=function(x){"
        + "rv=\"\";"
        + "Object.keys(x).forEach(function(key){"
        + "rv+=key+\"\\n\";"
        + "});return rv;};"
        + "dir(%s);";

    @Override
    public void start(Stage primaryStage) {
        MenuBar menuBar = new MenuBar();
        menuBar.setVisible(true);
        Menu viewMenu = new Menu("Options");
        menuBar.getMenus().addAll(viewMenu);

        WebView webView = new WebView();
        WebEngine eng = webView.getEngine();
        eng.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    primaryStage.setTitle(eng.getTitle());
                }
            }
        });
        final MenuItem viewContent
            = MenuItemBuilder.create()
            .text("Content")
            .onAction((ActionEvent e) -> {
                showText("Content of " + eng.getTitle(), primaryStage,
                    (String) eng.executeScript("document.documentElement.innerText"));
            })
            .accelerator(
                new KeyCodeCombination(
                    KeyCode.V, KeyCombination.ALT_DOWN))
            .build();
        final MenuItem viewSource
            = MenuItemBuilder.create()
            .text("Source")
            .onAction((ActionEvent e) -> {
                showText("Source of " + eng.getTitle(), primaryStage,
                    (String) eng.executeScript("document.documentElement.outerHTML"));
            })
            .accelerator(
                new KeyCodeCombination(
                    KeyCode.S, KeyCombination.ALT_DOWN))
            .build();
        final MenuItem console
            = MenuItemBuilder.create()
            .text("Console")
            .onAction((ActionEvent e) -> {
                console(primaryStage, eng);
            })
            .accelerator(
                new KeyCodeCombination(
                    KeyCode.C, KeyCombination.ALT_DOWN))
            .build();
        final MenuItem openScript
            = MenuItemBuilder.create()
            .text("Open Script")
            .onAction((ActionEvent e) -> {
                File file = new FileChooser().showOpenDialog(primaryStage);
                if (file != null) {
                    String result;
                    try {
                        result = (String)eng.executeScript(readFile(file.getCanonicalPath(),
                            Charset.defaultCharset()));
                    } catch (Exception ex) {
                        result = ex.getMessage();
                    }
                    alert("Result", "Final Value", "Final Value of Script:", result);
                }
            })
            .accelerator(
                new KeyCodeCombination(
                    KeyCode.O, KeyCombination.ALT_DOWN))
            .build();
        viewMenu.getItems().addAll(viewContent, viewSource, console, openScript);

        TextField address = new TextField();
        address.setOnKeyTyped((KeyEvent ke) -> {
            String ch = ke.getCharacter();
            switch (ch.getBytes()[0]) {
                case 13:
                    ke.consume();
                    eng.load(address.getText());
            }
        });

        BorderPane root = new BorderPane();
        root.setTop(address);
        root.setCenter(webView);
        root.setBottom(menuBar);
        Scene scene = new Scene(root, 800, 600);

        primaryStage.setTitle("");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

    private void showText(String title, Stage primaryStage, String text) {
        TextArea root = new TextArea(text);
        Scene secondScene = new Scene(root, 600, 400);
        Stage secondStage = new Stage();
        secondStage.setTitle(title);
        secondStage.setScene(secondScene);
        secondStage.initOwner(primaryStage);
        secondStage.show();
    }

    ArrayList<String> history = new ArrayList<>();
    StringBuilder input = new StringBuilder();

    private void console(Stage primaryStage, WebEngine eng) {
        final String PROMPT = "> ";
        TextArea root = new TextArea(PROMPT);
        root.setStyle("-fx-font-family: monospace");
        root.setOnKeyTyped((KeyEvent ke) -> {
            String ch = ke.getCharacter();
            ke.consume();
            switch (ch.getBytes()[0]) {
                case 13:
                    try {
                        if (input.length() == 0 && history.size() > 0) {
                            input = new StringBuilder(history.get(history.size() - 2).substring(PROMPT.length()));
                            break;
                        } else if (input.toString().contains(" ")
                            && input.toString().split(" ")[0].equals("help")) {
                            System.out.println(String.format(help, input.toString().split(" ")[1]));
                            input = new StringBuilder(String.format(help, input.toString().split(" ")[1]));
                        }
                        history.add(PROMPT + input.toString());
                        history.add(eng.executeScript(input.toString()).toString());
                    } catch (Exception jse) {
                        history.add(jse.toString());
                    }
                    input = new StringBuilder();
                    break;
                case 8:
                    if (input.length() > 0) {
                        input.setLength(input.length() - 1);
                    }
                    break;
                default:
                    input.append(ch);
                    break;
            }
            root.setText("");
            // while (history.size() > 20) history.remove(0);
            history.stream().forEach((entry) -> {
                root.appendText(entry + "\n");
            });
            root.appendText(PROMPT);
            root.appendText(input.toString());
            root.positionCaret(root.getLength());
        });
        Scene secondScene = new Scene(root, 600, 400);
        Stage secondStage = new Stage();
        secondStage.setTitle("Javascript Console");
        secondStage.setScene(secondScene);
        secondStage.initOwner(primaryStage);
        secondStage.show();
    }

    static String readFile(String path, Charset encoding)
        throws IOException {
        byte[] encoded = Files.readAllBytes(Paths.get(path));
        return new String(encoded, encoding);
    }

    private void alert(String title, String header, String content, String extra) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle(title);
        alert.setHeaderText(header);
        alert.setContentText(content);
        TextArea textArea = new TextArea(extra);
        textArea.setEditable(false);
        textArea.setWrapText(true);

        textArea.setMaxWidth(Double.MAX_VALUE);
        textArea.setMaxHeight(Double.MAX_VALUE);
        GridPane.setVgrow(textArea, Priority.ALWAYS);
        GridPane.setHgrow(textArea, Priority.ALWAYS);

        GridPane expContent = new GridPane();
        expContent.setMaxWidth(Double.MAX_VALUE);
        expContent.add(textArea, 0, 1);

        alert.getDialogPane().setExpandableContent(expContent);

        alert.showAndWait();
    }
}

Tip: you can type help document, for example; in the console window. This command will be auto-replaced with a short script to output a listing of document. When running scripts just remember this program tries to convert and diplay the last statement as a string. You can also press Enter at the console to replay the last input. Lastly, I want to present a script that displays links from a given page:

var arr = [], l = document.links;

for(var i=0; i<l.length; i++) {

    arr.push(l[i].href);

};
print = function(item) {
    document.write("<a href=\"" + item + "\">" + item + "</a><br>");
    result += item + "\n";
}
result = "";
arr.forEach(print);
result

The project is available on github. Some screenshots:

main

test output

source output

console output

script output

\$\endgroup\$
1
\$\begingroup\$
case 13:

If you're going to add in hooks to keycodes like that, either

  1. Use the KeyCode class you were using earlier
  2. Make use of java's char to byte casting; use '\r'
  3. Define the value as a constant KEYCODE_CARRIAGE_RETURN
  4. Add a comment // \r carriage return, enter key.

I'd prefer 1 or 2.


I also think it would be handy if these objects

    final MenuItem viewContent
        = MenuItemBuilder.create()
        .text("Content")
        .onAction((ActionEvent e) -> {
            showText("Content of " + eng.getTitle(), primaryStage,
                (String) eng.executeScript("document.documentElement.innerText"));
        })
        .accelerator(
            new KeyCodeCombination(
                KeyCode.V, KeyCombination.ALT_DOWN))
        .build();
    final MenuItem viewSource
        = MenuItemBuilder.create()
        .text("Source")
        .onAction((ActionEvent e) -> {
            showText("Source of " + eng.getTitle(), primaryStage,
                (String) eng.executeScript("document.documentElement.outerHTML"));
        })
        .accelerator(
            new KeyCodeCombination(
                KeyCode.S, KeyCombination.ALT_DOWN))
        .build();
    final MenuItem console
        = MenuItemBuilder.create()
        .text("Console")
        .onAction((ActionEvent e) -> {
            console(primaryStage, eng);
        })
        .accelerator(
            new KeyCodeCombination(
                KeyCode.C, KeyCombination.ALT_DOWN))
        .build();
    final MenuItem openScript
        = MenuItemBuilder.create()
        .text("Open Script")
        .onAction((ActionEvent e) -> {
            File file = new FileChooser().showOpenDialog(primaryStage);
            if (file != null) {
                String result;
                try {
                    result = (String)eng.executeScript(readFile(file.getCanonicalPath(),
                        Charset.defaultCharset()));
                } catch (Exception ex) {
                    result = ex.getMessage();
                }
                alert("Result", "Final Value", "Final Value of Script:", result);
            }
        })
        .accelerator(
            new KeyCodeCombination(
                KeyCode.O, KeyCombination.ALT_DOWN))
        .build();

Were created each in their own function. Right now, populating the view is a messy place, and by stowing these elements into separate functions you can keep the menu items and the management of menu items apart.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.