Added export to file button with CSV support.

This commit is contained in:
Andrew Lalis 2024-07-20 14:57:34 -04:00
parent 3908515ca4
commit 408d5e415d
4 changed files with 104 additions and 6 deletions

View File

@ -3,6 +3,7 @@ package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.SavedQueryRepository;
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile;
import javafx.fxml.FXML;
import javafx.scene.Node;
@ -10,12 +11,17 @@ import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
@ -42,12 +48,7 @@ public class SqlConsoleViewController implements RouteSelectionListener {
}
@FXML public void executeQuery() {
String queryText = sqlEditorTextArea.getText().strip();
String[] rawQueries = queryText.split("\\s*;\\s*");
List<String> queries = Arrays.stream(rawQueries)
.filter(s -> !s.isBlank())
.filter(s -> !s.startsWith("#") && !s.startsWith("//"))
.toList();
List<String> queries = getCurrentQueries();
outputTextArea.clear();
JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
try (
@ -107,6 +108,71 @@ public class SqlConsoleViewController implements RouteSelectionListener {
}
}
@FXML public void exportToFile() {
if (sqlEditorTextArea.getText().isBlank()) {
Popups.message(sqlEditorTextArea, "Cannot export the results of an empty query.");
return;
}
if (getCurrentQueries().size() > 1) {
Popups.message(sqlEditorTextArea, "Note: Export to file will only export the results of your first query.");
}
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export to File");
fileChooser.setInitialFileName("export.csv");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
"CSV Files", ".csv"
));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
"JSON Files", ".json"
));
fileChooser.setInitialDirectory(Profile.getCurrent().dataSource().getContentDir().toFile());
File chosenFile = fileChooser.showSaveDialog(sqlEditorTextArea.getScene().getWindow());
if (chosenFile == null) return;
String name = chosenFile.getName().strip().toLowerCase();
if (!name.endsWith(".csv") && !name.endsWith(".json")) {
Popups.error(sqlEditorTextArea, "Invalid file format. Only CSV and JSON are permitted.");
return;
}
String query = getCurrentQueries().getFirst();
JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
try (
var conn = dataSource.getConnection();
var stmt = conn.createStatement()
) {
ResultSet rs = stmt.executeQuery(query);
if (name.endsWith(".csv")) {
writeQueryResultsToCsv(rs, chosenFile.toPath());
} else if (name.endsWith(".json")) {
// writeQueryResultsToJson(rs, chosenFile.toPath());
Popups.message(sqlEditorTextArea, "JSON not yet supported.");
}
} catch (Exception e) {
Popups.error(sqlEditorTextArea, e);
}
}
private void writeQueryResultsToCsv(ResultSet rs, Path file) throws SQLException, IOException {
try (var out = Files.newOutputStream(file); var writer = new PrintWriter(out)) {
final int columnCount = rs.getMetaData().getColumnCount();
// First write the header.
for (int i = 1; i <= columnCount; i++) {
writer.append(FileUtil.escapeCSVText(rs.getMetaData().getColumnLabel(i)));
if (i < columnCount) writer.append(',');
}
writer.println();
// Then write the body rows.
while (rs.next()) {
for (int i = 1; i <= columnCount; i++) {
writer.append(FileUtil.escapeCSVText(rs.getString(i)));
if (i < columnCount) writer.append(',');
}
writer.println();
}
}
}
private void refreshSavedQueries() {
savedQueriesVBox.getChildren().clear();
List<String> savedQueries = Profile.getCurrent().dataSource()
@ -147,6 +213,15 @@ public class SqlConsoleViewController implements RouteSelectionListener {
return pane;
}
private List<String> getCurrentQueries() {
String queryText = sqlEditorTextArea.getText().strip();
String[] rawQueries = queryText.split("\\s*;\\s*");
return Arrays.stream(rawQueries)
.filter(s -> !s.isBlank())
.filter(s -> !s.startsWith("#") && !s.startsWith("//"))
.toList();
}
@FXML public void showSchema() {
SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
dialog.show();

View File

@ -114,4 +114,10 @@ public class FileUtil {
in.transferTo(out);
}
}
public static String escapeCSVText(String raw) {
if (raw == null) return "NULL";
if (!raw.contains("\"") && !raw.contains(",") && !raw.contains(";")) return raw;
return '"' + raw.replaceAll("\"", "\"\"") + '"';
}
}

View File

@ -21,6 +21,7 @@
<Button text="Execute Query" onAction="#executeQuery"/>
<Button text="View Schema" onAction="#showSchema"/>
<Button text="Save Query" onAction="#saveQuery"/>
<Button text="Export to File" onAction="#exportToFile"/>
</HBox>
<ScrollPane fitToHeight="true" fitToWidth="true">
<TextArea

View File

@ -0,0 +1,16 @@
package com.andrewlalis.perfin.data.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class FileUtilTest {
@Test
public void testEscapeCSVText() {
assertEquals("regular_value", FileUtil.escapeCSVText("regular_value"));
assertEquals("\"\"\"\"", FileUtil.escapeCSVText("\""));
assertEquals("\"\"\",\"\"\"", FileUtil.escapeCSVText("\",\""));
assertEquals("\"and, this\"", FileUtil.escapeCSVText("and, this"));
assertEquals("\"1,345.43\"", FileUtil.escapeCSVText("1,345.43"));
}
}