Added export to file button with CSV support.
This commit is contained in:
		
							parent
							
								
									3908515ca4
								
							
						
					
					
						commit
						408d5e415d
					
				| 
						 | 
					@ -3,6 +3,7 @@ package com.andrewlalis.perfin.control;
 | 
				
			||||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
 | 
					import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
 | 
				
			||||||
import com.andrewlalis.perfin.data.SavedQueryRepository;
 | 
					import com.andrewlalis.perfin.data.SavedQueryRepository;
 | 
				
			||||||
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
 | 
					import com.andrewlalis.perfin.data.impl.JdbcDataSource;
 | 
				
			||||||
 | 
					import com.andrewlalis.perfin.data.util.FileUtil;
 | 
				
			||||||
import com.andrewlalis.perfin.model.Profile;
 | 
					import com.andrewlalis.perfin.model.Profile;
 | 
				
			||||||
import javafx.fxml.FXML;
 | 
					import javafx.fxml.FXML;
 | 
				
			||||||
import javafx.scene.Node;
 | 
					import javafx.scene.Node;
 | 
				
			||||||
| 
						 | 
					@ -10,12 +11,17 @@ import javafx.scene.control.*;
 | 
				
			||||||
import javafx.scene.layout.AnchorPane;
 | 
					import javafx.scene.layout.AnchorPane;
 | 
				
			||||||
import javafx.scene.layout.HBox;
 | 
					import javafx.scene.layout.HBox;
 | 
				
			||||||
import javafx.scene.layout.VBox;
 | 
					import javafx.scene.layout.VBox;
 | 
				
			||||||
 | 
					import javafx.stage.FileChooser;
 | 
				
			||||||
import javafx.stage.Modality;
 | 
					import javafx.stage.Modality;
 | 
				
			||||||
import javafx.stage.StageStyle;
 | 
					import javafx.stage.StageStyle;
 | 
				
			||||||
import javafx.stage.Window;
 | 
					import javafx.stage.Window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.File;
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.PrintWriter;
 | 
				
			||||||
import java.nio.charset.StandardCharsets;
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.nio.file.Files;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
import java.sql.ResultSet;
 | 
					import java.sql.ResultSet;
 | 
				
			||||||
import java.sql.SQLException;
 | 
					import java.sql.SQLException;
 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
| 
						 | 
					@ -42,12 +48,7 @@ public class SqlConsoleViewController implements RouteSelectionListener {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @FXML public void executeQuery() {
 | 
					    @FXML public void executeQuery() {
 | 
				
			||||||
        String queryText = sqlEditorTextArea.getText().strip();
 | 
					        List<String> queries = getCurrentQueries();
 | 
				
			||||||
        String[] rawQueries = queryText.split("\\s*;\\s*");
 | 
					 | 
				
			||||||
        List<String> queries = Arrays.stream(rawQueries)
 | 
					 | 
				
			||||||
                .filter(s -> !s.isBlank())
 | 
					 | 
				
			||||||
                .filter(s -> !s.startsWith("#") && !s.startsWith("//"))
 | 
					 | 
				
			||||||
                .toList();
 | 
					 | 
				
			||||||
        outputTextArea.clear();
 | 
					        outputTextArea.clear();
 | 
				
			||||||
        JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
 | 
					        JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
 | 
				
			||||||
        try (
 | 
					        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() {
 | 
					    private void refreshSavedQueries() {
 | 
				
			||||||
        savedQueriesVBox.getChildren().clear();
 | 
					        savedQueriesVBox.getChildren().clear();
 | 
				
			||||||
        List<String> savedQueries = Profile.getCurrent().dataSource()
 | 
					        List<String> savedQueries = Profile.getCurrent().dataSource()
 | 
				
			||||||
| 
						 | 
					@ -147,6 +213,15 @@ public class SqlConsoleViewController implements RouteSelectionListener {
 | 
				
			||||||
        return pane;
 | 
					        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() {
 | 
					    @FXML public void showSchema() {
 | 
				
			||||||
        SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
 | 
					        SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
 | 
				
			||||||
        dialog.show();
 | 
					        dialog.show();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -114,4 +114,10 @@ public class FileUtil {
 | 
				
			||||||
            in.transferTo(out);
 | 
					            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("\"", "\"\"") + '"';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@
 | 
				
			||||||
            <Button text="Execute Query" onAction="#executeQuery"/>
 | 
					            <Button text="Execute Query" onAction="#executeQuery"/>
 | 
				
			||||||
            <Button text="View Schema" onAction="#showSchema"/>
 | 
					            <Button text="View Schema" onAction="#showSchema"/>
 | 
				
			||||||
            <Button text="Save Query" onAction="#saveQuery"/>
 | 
					            <Button text="Save Query" onAction="#saveQuery"/>
 | 
				
			||||||
 | 
					            <Button text="Export to File" onAction="#exportToFile"/>
 | 
				
			||||||
        </HBox>
 | 
					        </HBox>
 | 
				
			||||||
        <ScrollPane fitToHeight="true" fitToWidth="true">
 | 
					        <ScrollPane fitToHeight="true" fitToWidth="true">
 | 
				
			||||||
            <TextArea
 | 
					            <TextArea
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue