Simple tool to save a web page's source. A result of trying to learn and apply an MVC approach. The FXML is generated via Scene Builder, but included for completion.
I welcome any general feedback on how to improve anything but in particular:
- There are many exceptions, current impression is that current handling is not ideal. What is the conventional wisdom on handling a plethora of io/connection errors?
- API and methods utilized in reading and writing were changed several times. Thanks to @rolfl's answer to a related question, currently refactored to employ Java 8 streams, is this the most appropriate for purpose? Can it be cleaner? Faster?
- Any conventions that should be followed, especially in regards to MVC, FXML.
The following is the source, with a line in the main class commented out so this may be executed if desired.
Main Class:
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
public class HTMLSave extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
Parent root = null;
try {
root = FXMLLoader.load(getClass().getResource("HTML_Save.fxml"));
} catch (IOException ioe) {
System.err.println("Could not load FXML");
}
Scene scene = new Scene(root, Color.TRANSPARENT);
scene.getStylesheets().add("html-save.css");
stage.setScene(scene);
stage.setTitle("HTML Saver");
//stage.getIcons().add(new Image(getClass().getResourceAsStream("icon.png")));
stage.show();
}
}
Controller Class:
import java.awt.Desktop;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
public class HTMLSaveController implements Initializable {
@FXML
private Button saveButton;
@FXML
private Button openButton;
@FXML
private TextField htmlField;
@FXML
private TextField filenameField;
@FXML
private ProgressBar progressBar;
private String destinationFile;
@Override
public void initialize(URL location, ResourceBundle resources) {
System.out.println("HTML Saver view successfully loaded.");
}
public void handleSaveRequest() {
String filename = filenameField.getText();
String url = htmlField.getText();
if (url.isEmpty()) {
errorAlert("URL missing", "Please paste in website address");
} else if (filename.isEmpty()) {
errorAlert("No filename", "Please enter a file name to save html");
} else {
destinationFile = filename + ".html";
try (Writer destination = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(destinationFile), "UTF-8"))) {
savePageSource(url, destination);
} catch (IOException ioe) {
System.err.println("Could not write to " + filename + '.');
}
}
}
public void handleOpenRequest() {
try {
Desktop.getDesktop().open(new File(destinationFile));
} catch (IOException ioe) {
System.err.println(destinationFile + " not found.");
}
}
private void errorAlert(String message, String infoMessage) {
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(message);
alert.setContentText(infoMessage);
alert.showAndWait();
}
private void savePageSource(String url, Writer destination) throws IOException {
Task saveTask = new Task() {
@Override
protected Void call() throws IOException {
updateProgress(0, 100);
openButton.setDisable(true);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(url).openStream()))) {
progressBar.setCursor(Cursor.WAIT);
destination.write(reader.lines().collect(Collectors.joining("\n")));
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
progressBar.setCursor(Cursor.DEFAULT);
updateProgress(1, 1);
openButton.setDisable(false);
return null;
}
};
progressBar.progressProperty().bind(saveTask.progressProperty());
Thread savingThread = new Thread(saveTask);
savingThread.start();
try {
savingThread.join();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
CSS:
.label {
-fx-text-fill: rgb(166, 226, 46);
-fx-font-weight: bold;
}
.button {
-fx-text-fill: rgb(166, 226, 46);
-fx-background-color: rgb(39, 40, 34);
-fx-border-radius: 20;
-fx-background-radius: 20;
-fx-padding: 5;
}
.button:hover {
-fx-text-fill: rgb(249, 38, 114);
}
.root{
-fx-font-family: "Courier New";
-fx-background: rgb(61, 61, 59);
-fx-base: rgb(39, 40, 34);
}
.text-field:focused {
-fx-background-color: #a9a9a9 , white , white;
-fx-background-insets: 0 -1 -1 -1, 0 0 0 0, 0 -1 3 -1;
}
.progress-bar > .bar {
-fx-background-color: linear-gradient(
from 0px .75em to .75em 0px,
repeat,
rgb(253, 151, 31) 0%,
rgb(253, 151, 31) 49%,
derive(rgb(253, 151, 31), 30%) 50%,
derive(rgb(253, 151, 31), 30%) 99%
);
}
FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="174.0" prefWidth="474.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="HTMLSaveController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="309.0" minWidth="10.0" prefWidth="203.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="398.0" minWidth="10.0" prefWidth="271.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="121.0" minHeight="10.0" prefHeight="114.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="181.0" minHeight="10.0" prefHeight="129.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="293.0" minHeight="10.0" prefHeight="121.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Button fx:id="saveButton" defaultButton="true" mnemonicParsing="false" onAction="#handleSaveRequest" prefHeight="39.0" prefWidth="73.0" text="Save" textFill="#36d721" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="2" GridPane.valignment="TOP">
<font>
<Font size="18.0" />
</font>
<cursor>
<Cursor fx:constant="HAND" />
</cursor></Button>
<Label text="Paste Address: ">
<font>
<Font size="20.0" />
</font></Label>
<Label text="Enter Filename: " GridPane.rowIndex="1">
<font>
<Font size="20.0" />
</font></Label>
<TextField fx:id="htmlField" GridPane.columnIndex="1" />
<TextField fx:id="filenameField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<VBox prefHeight="123.0" prefWidth="205.0" GridPane.rowIndex="2">
<children>
<Button fx:id="openButton" disable="true" mnemonicParsing="false" onAction="#handleOpenRequest" prefHeight="39.0" prefWidth="81.0" text="Open" textFill="#6f2929">
<font>
<Font size="18.0" />
</font>
<cursor>
<Cursor fx:constant="HAND" />
</cursor>
</Button>
<ProgressBar fx:id="progressBar" prefWidth="200.0" progress="0.0">
<cursor>
<Cursor fx:constant="DEFAULT" />
</cursor>
</ProgressBar>
</children>
</VBox>
</children>
</GridPane>
The result: