Java tutorial
/* * ComplexQueryViewController * Copyright (C) 2017 Gaurav Vaidya * * This file is part of SciNames. * * SciNames is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * SciNames is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with SciNames. If not, see <http://www.gnu.org/licenses/>. * */ package com.ggvaidya.scinames.complexquery; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import com.ggvaidya.scinames.dataset.DatasetChangesView; import com.ggvaidya.scinames.model.Change; import com.ggvaidya.scinames.model.Dataset; import com.ggvaidya.scinames.model.DatasetColumn; import com.ggvaidya.scinames.model.DatasetRow; import com.ggvaidya.scinames.model.Name; import com.ggvaidya.scinames.model.NameCluster; import com.ggvaidya.scinames.model.NameClusterManager; import com.ggvaidya.scinames.model.Project; import com.ggvaidya.scinames.model.TaxonConcept; import com.ggvaidya.scinames.tabulardata.TabularDataViewController; import com.ggvaidya.scinames.ui.ProjectView; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ComboBox; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.input.Clipboard; import javafx.scene.input.DataFormat; import javafx.scene.input.MouseButton; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.CornerRadii; import javafx.scene.paint.Paint; import javafx.stage.FileChooser; /** * A ComplexQueryView is like a TabularDataView: it does a lot of the work, * so you don't have to. A complex query consists of a query in a text area, * which is a serialization of the query. A button can be used to call up a * list of available commands. * * @author Gaurav Vaidya <gaurav@ggvaidya.com> */ public class ComplexQueryViewController implements Initializable { private static final Logger LOGGER = Logger.getLogger(ComplexQueryViewController.class.getSimpleName()); public static ComplexQueryViewController createComplexQueryView(ProjectView pv) { Scene scene; FXMLLoader loader = new FXMLLoader( TabularDataViewController.class.getResource("/fxml/ComplexQueryView.fxml")); AnchorPane ap; try { ap = (AnchorPane) loader.load(); } catch (IOException e) { throw new RuntimeException("Could not load internal file 'ComplexQueryView.fxml': " + e); } scene = new Scene(ap); ComplexQueryViewController controller = loader.getController(); controller.scene = scene; controller.projectView = pv; return controller; } private ProjectView projectView; public ProjectView getProjectView() { return projectView; } private Scene scene; public Scene getScene() { return scene; } /** * Initializes the controller class. */ @Override public void initialize(URL url, ResourceBundle rb) { updatePrerecordedQueries(); initDataTable(); } private void initDataTable() { dataTableView.setOnMouseClicked(evt -> { if (evt.getButton().equals(MouseButton.PRIMARY) && evt.getClickCount() >= 2) { // Double-click on row! Object row = dataTableView.getSelectionModel().getSelectedItem(); // What is this? if (Change.class.isAssignableFrom(row.getClass())) { // Be the change you want to see in the world. Change ch = (Change) row; DatasetChangesView view = new DatasetChangesView(projectView, ch); view.getStage().show(); } else { LOGGER.warning("Could not find handler for double-clicking on " + row); } } }); } private void updatePrerecordedQueries() { ObservableList<String> items = prerecordedQueries.getItems(); items.clear(); items.add("Choose one of the pre-recorded queries, or enter your own below:"); prerecordedQueries.getSelectionModel().clearAndSelect(0); } private List<Function<ComplexQueryViewController, String>> listeners = new LinkedList<>(); public List<Function<ComplexQueryViewController, String>> getListeners() { return listeners; } public void addListener(Function<ComplexQueryViewController, String> listener) { listeners.add(listener); } @FXML private void executeQuery() { for (Function<ComplexQueryViewController, String> listener : listeners) { setQueryStatus(listener.apply(this)); } } public String getQuery() { return queryTextArea.getText(); } public void setQueryStatus(String status) { if (status.equals("ok")) { queryStatusTextField.setBackground( new Background(new BackgroundFill(Paint.valueOf("green"), CornerRadii.EMPTY, Insets.EMPTY))); queryStatusTextField.setAlignment(Pos.CENTER); queryStatusTextField.setText("Query executed successfully"); } else { queryStatusTextField.setBackground( new Background(new BackgroundFill(Paint.valueOf("red"), CornerRadii.EMPTY, Insets.EMPTY))); queryStatusTextField.setAlignment(Pos.CENTER); queryStatusTextField.setText(status); } } /** * Provide an export of the data in the TableView as a "table". In its * simplest Java representation, that is a list of columns, with each * column starting with a column header and then all the rest of the data. * * Warning: this can be a long-running function! * * @return A list of columns of data. */ public List<List<String>> getDataAsTable() { // What columns do we have? List<List<String>> result = new LinkedList<>(); List<TableColumn<NameCluster, ?>> columns = dataTableView.getColumns(); for (TableColumn<NameCluster, ?> col : columns) { List<String> column = new LinkedList<>(); // Add the header. column.add(col.getText()); // Add the data. for (int x = 0; x < dataTableView.getItems().size(); x++) { ObservableValue cellObservableValue = col.getCellObservableValue(x); column.add(cellObservableValue.getValue().toString()); } result.add(column); } ; return result; } private void fillCSVFormat(CSVFormat format, Appendable destination, List<List<String>> data) throws IOException { try (CSVPrinter printer = format.print(destination)) { List<List<String>> dataAsTable = data; if (dataAsTable.isEmpty()) return; for (int x = 0; x < dataAsTable.get(0).size(); x++) { for (int y = 0; y < dataAsTable.size(); y++) { String value = dataAsTable.get(y).get(x); printer.print(value); } printer.println(); } } } /* Actions */ @FXML private void copyToClipboard(ActionEvent evt) { try { StringWriter writer = new StringWriter(); List<List<String>> dataAsTable = getDataAsTable(); fillCSVFormat(CSVFormat.TDF, writer, getDataAsTable()); Clipboard clipboard = Clipboard.getSystemClipboard(); HashMap<DataFormat, Object> content = new HashMap<>(); content.put(DataFormat.PLAIN_TEXT, writer.getBuffer().toString()); clipboard.setContent(content); Alert window = new Alert(Alert.AlertType.CONFIRMATION, (dataAsTable.get(0).size() - 1) + " rows written to clipboard."); window.showAndWait(); } catch (IOException e) { Alert window = new Alert(Alert.AlertType.ERROR, "Could not save CSV to the clipboard: " + e); window.showAndWait(); } } @FXML private void exportToCSV(ActionEvent evt) { FileChooser chooser = new FileChooser(); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("CSV file", "*.csv"), new FileChooser.ExtensionFilter("Tab-delimited file", "*.txt")); File file = chooser.showSaveDialog(scene.getWindow()); if (file != null) { CSVFormat format = CSVFormat.RFC4180; String outputFormat = chooser.getSelectedExtensionFilter().getDescription(); if (outputFormat.equalsIgnoreCase("Tab-delimited file")) format = CSVFormat.TDF; try { List<List<String>> dataAsTable = getDataAsTable(); fillCSVFormat(format, new FileWriter(file), dataAsTable); Alert window = new Alert(Alert.AlertType.CONFIRMATION, "CSV file '" + file + "' saved with " + (dataAsTable.get(0).size() - 1) + " rows."); window.showAndWait(); } catch (IOException e) { Alert window = new Alert(Alert.AlertType.ERROR, "Could not save CSV to '" + file + "': " + e); window.showAndWait(); } } } /* FXML objects */ @FXML private ComboBox<String> prerecordedQueries; @FXML private TableView<String> propertiesTableView; @FXML private TextArea queryTextArea; @FXML private TextField queryStatusTextField; @FXML private TableView dataTableView; @FXML private TextField statusTextField; /* Return FXML objects */ public TableView<String> getPropertiesTableView() { return propertiesTableView; } /* * Complicated fun things that make ComplexQueryViewControllers easy to use. */ /* Part 1: Help */ private String queryHelp; public String getQueryHelp() { return queryHelp; } public void setQueryHelp(String help) { queryHelp = help; } @FXML private void displayQueryHelp() { new Alert(Alert.AlertType.INFORMATION, queryHelp).showAndWait(); } /* Part 2: Displaying data into the table */ public static final Dataset ALL = new Dataset(); private TableColumn<Change, String> createTableColumnFromChange(String colName, Function<Change, String> func) { TableColumn<Change, String> column = new TableColumn<>(colName); column.cellValueFactoryProperty().set(cvf -> new ReadOnlyStringWrapper(func.apply(cvf.getValue()))); column.setPrefWidth(100.0); column.setEditable(false); return column; } public void updateTableWithChanges(Project project, Set<Change> changesToDisplay, List<Dataset> datasets) { List<Change> changes = changesToDisplay.stream().sorted((a, b) -> a.getDataset().compareTo(b.getDataset())) .collect(Collectors.toList()); NameClusterManager ncm = project.getNameClusterManager(); // And add tablecolumns for the rest. dataTableView.getColumns().clear(); dataTableView.getColumns().addAll(createTableColumnFromChange("id", ch -> ch.getId().toString()), createTableColumnFromChange("dataset", ch -> ch.getDataset().getName()), createTableColumnFromChange("type", ch -> ch.getType().getType()), createTableColumnFromChange("from", ch -> ch.getFromString()), createTableColumnFromChange("from_name_cluster_ids", ch -> ncm.getClusters(ch.getFrom()).stream().map(cl -> cl.getId().toString()) .collect(Collectors.joining(" and "))), createTableColumnFromChange("from_name_clusters", ch -> ncm.getClusters(ch.getFrom()).stream() .map(cl -> cl.getNames().stream().map(n -> n.getFullName()) .collect(Collectors.joining("; "))) .collect(Collectors.joining(" and "))), createTableColumnFromChange("to", ch -> ch.getToString()), createTableColumnFromChange("to_name_cluster_ids", ch -> ncm.getClusters(ch.getTo()).stream().map(cl -> cl.getId().toString()) .collect(Collectors.joining(" and "))), createTableColumnFromChange("to_name_clusters", ch -> ncm.getClusters(ch.getTo()).stream() .map(cl -> cl.getNames().stream().map(n -> n.getFullName()) .collect(Collectors.joining("; "))) .collect(Collectors.joining(" and "))), createTableColumnFromChange("filter_status", ch -> project.getChangeFilter().test(ch) ? "retained" : "eliminated"), createTableColumnFromChange("properties", ch -> ch.getProperties().entrySet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()).sorted() .collect(Collectors.joining("; "))), createTableColumnFromChange("citations", ch -> ch.getCitationStream().map(cit -> cit.getCitation()) .sorted().collect(Collectors.joining("; ")))); dataTableView.getItems().clear(); dataTableView.getItems().addAll(changes); dataTableView.refresh(); // Fill in status text field. statusTextField.setText(dataTableView.getItems().size() + " changes across " + changes.stream().map(ch -> ch.getDataset()).distinct().count() + " distinct datasets"); } public void updateTableWithChangesUsingNameClusters(Project project, List<NameCluster> nameClusters, List<Dataset> datasets) { Set<Change> changesToDisplay = new HashSet<>(); for (NameCluster cluster : nameClusters) { // Yes, we want to use getAllChanges() here, because we'd like to match eliminated changes too. changesToDisplay .addAll(datasets.stream().flatMap(ds -> ds.getAllChanges()).collect(Collectors.toSet())); } List<Change> changes = changesToDisplay.stream().sorted((a, b) -> a.getDataset().compareTo(b.getDataset())) .collect(Collectors.toList()); NameClusterManager ncm = project.getNameClusterManager(); // And add tablecolumns for the rest. dataTableView.getColumns().clear(); dataTableView.getColumns().addAll(createTableColumnFromChange("id", ch -> ch.getId().toString()), createTableColumnFromChange("dataset", ch -> ch.getDataset().getName()), createTableColumnFromChange("type", ch -> ch.getType().getType()), createTableColumnFromChange("from", ch -> ch.getFromString()), createTableColumnFromChange("from_name_cluster_ids", ch -> ncm.getClusters(ch.getFrom()).stream().map(cl -> cl.getId().toString()) .collect(Collectors.joining(" and "))), createTableColumnFromChange("from_name_clusters", ch -> ncm.getClusters(ch.getFrom()).stream() .map(cl -> cl.getNames().stream().map(n -> n.getFullName()) .collect(Collectors.joining("; "))) .collect(Collectors.joining(" and "))), createTableColumnFromChange("to", ch -> ch.getToString()), createTableColumnFromChange("to_name_cluster_ids", ch -> ncm.getClusters(ch.getTo()).stream().map(cl -> cl.getId().toString()) .collect(Collectors.joining(" and "))), createTableColumnFromChange("to_name_clusters", ch -> ncm.getClusters(ch.getTo()).stream() .map(cl -> cl.getNames().stream().map(n -> n.getFullName()) .collect(Collectors.joining("; "))) .collect(Collectors.joining(" and "))), createTableColumnFromChange("filter_status", ch -> project.getChangeFilter().test(ch) ? "retained" : "eliminated"), createTableColumnFromChange("citations", ch -> ch.getCitationStream().map(cit -> cit.getCitation()) .collect(Collectors.joining("; ")))); dataTableView.getItems().clear(); dataTableView.getItems().addAll(changes); dataTableView.refresh(); // Fill in status text field. statusTextField.setText(dataTableView.getItems().size() + " changes across " + changes.stream().map(ch -> ch.getDataset()).distinct().count() + " distinct datasets"); } private TableColumn<NameCluster, String> createColumnFromPrecalc(String colName, Table<NameCluster, String, Set<String>> precalc) { TableColumn<NameCluster, String> column = new TableColumn<>(colName); column.cellValueFactoryProperty().set((TableColumn.CellDataFeatures<NameCluster, String> cdf) -> { NameCluster nc = cdf.getValue(); // There might be columns found in some dataset but not in others // so we detect those cases here and put in "NA"s instead. String output = "NA"; if (precalc.contains(nc, colName)) output = precalc.get(nc, colName).stream().collect(Collectors.joining("; ")); return new ReadOnlyStringWrapper(output); }); column.setPrefWidth(100.0); column.setEditable(false); return column; } public void updateTableWithNameClusters(Project project, List<NameCluster> nameClusters, List<Dataset> datasets) { Table<NameCluster, String, Set<String>> precalc = HashBasedTable.create(); if (nameClusters == null) { dataTableView.setItems(FXCollections.emptyObservableList()); return; } boolean flag_nameClustersAreTaxonConcepts = false; if (nameClusters.size() > 0 && TaxonConcept.class.isAssignableFrom(nameClusters.get(0).getClass())) flag_nameClustersAreTaxonConcepts = true; dataTableView.setItems(FXCollections.observableList(nameClusters)); // Precalculate. List<String> existingColNames = new ArrayList<>(); existingColNames.add("id"); existingColNames.add("name"); existingColNames.add("names_in_dataset"); existingColNames.add("all_names_in_cluster"); // If these are taxon concepts, there's three other columns we want // to emit. if (flag_nameClustersAreTaxonConcepts) { existingColNames.add("name_cluster_id"); existingColNames.add("starts_with"); existingColNames.add("ends_with"); existingColNames.add("is_ongoing"); } else { existingColNames.add("taxon_concept_count"); existingColNames.add("taxon_concepts"); } // Set<Name> recognizedNamesInDataset = namesDataset.getRecognizedNames(project).collect(Collectors.toSet()); for (NameCluster cluster : nameClusters) { precalc.put(cluster, "id", getOneElementSet(cluster.getId().toString())); // Okay, here's what we need to do: // - If names is ALL, then we can't do better than cluster.getName(). // if(namesDataset == ALL) { precalc.put(cluster, "names_in_dataset", cluster.getNames().stream().map(n -> n.getFullName()).collect(Collectors.toSet())); precalc.put(cluster, "name", getOneElementSet(cluster.getName().getFullName())); //} else { /* // hey, here's something cool we can do: figure out which name(s) // this dataset uses from this cluster! List<String> namesInDataset = cluster.getNames().stream() .filter(n -> recognizedNamesInDataset.contains(n)) .map(n -> n.getFullName()) .collect(Collectors.toList()); String firstName = ""; if(namesInDataset.size() > 0) firstName = namesInDataset.get(0); precalc.put(cluster, "names_in_dataset", new HashSet<>(namesInDataset)); precalc.put(cluster, "name", getOneElementSet(firstName)); }*/ precalc.put(cluster, "all_names_in_cluster", cluster.getNames().stream().map(n -> n.getFullName()).collect(Collectors.toSet())); // If it's a taxon concept, precalculate a few more columns. if (flag_nameClustersAreTaxonConcepts) { TaxonConcept tc = (TaxonConcept) cluster; precalc.put(cluster, "name_cluster_id", getOneElementSet(tc.getNameCluster().getId().toString())); precalc.put(cluster, "starts_with", tc.getStartsWith().stream().map(ch -> ch.toString()).collect(Collectors.toSet())); precalc.put(cluster, "ends_with", tc.getEndsWith().stream().map(ch -> ch.toString()).collect(Collectors.toSet())); precalc.put(cluster, "is_ongoing", getOneElementSet(tc.isOngoing(project) ? "yes" : "no")); } else { // If it's a true name cluster, then perhaps people will want // to know what taxon concepts are in here? Maybe for some sort // of PhD? List<TaxonConcept> tcs = cluster.getTaxonConcepts(project); precalc.put(cluster, "taxon_concept_count", getOneElementSet(String.valueOf(tcs.size()))); precalc.put(cluster, "taxon_concepts", tcs.stream().map(tc -> tc.toString()).collect(Collectors.toSet())); } // Okay, here's where we reconcile! for (Name n : cluster.getNames()) { // TODO: there's probably an optimization here, in which we should // loop on the smaller set (either loop on 'datasets' and compare // to cluster, or loop on cluster.foundIn and compare to 'datasets'). for (Dataset ds : datasets) { Map<Name, Set<DatasetRow>> rowsByName = ds.getRowsByName(); // Are we included in this name cluster? If not, skip! if (!cluster.getFoundIn().contains(ds)) continue; // Check to see if we have any rows for this name; if not, skip. if (!rowsByName.containsKey(n)) continue; Set<DatasetRow> matched = rowsByName.get(n); LOGGER.log(Level.FINER, "Adding {0} rows under name ''{1}''", new Object[] { matched.size(), n.getFullName() }); Map<Set<DatasetColumn>, List<DatasetRow>> rowsByCols = matched.stream() .collect(Collectors.groupingBy((DatasetRow row) -> row.getColumns())); for (Set<DatasetColumn> cols : rowsByCols.keySet()) { for (DatasetColumn col : cols) { String colName = col.getName(); if (existingColNames.contains(colName)) colName = "datasets." + colName; if (!precalc.contains(cluster, colName)) precalc.put(cluster, colName, new HashSet()); for (DatasetRow row : rowsByCols.get(cols)) { if (!row.hasColumn(col)) continue; precalc.get(cluster, colName).add(row.get(col)); } LOGGER.log(Level.FINER, "Added {0} rows under name ''{1}''", new Object[] { rowsByCols.get(cols).size(), n.getFullName() }); } } } } } dataTableView.getColumns().clear(); for (String colName : existingColNames) { dataTableView.getColumns().add(createColumnFromPrecalc(colName, precalc)); } // Get distinct column names. Stream<String> colNames = precalc.cellSet().stream().map(set -> set.getColumnKey()); // Eliminate columns that are in the existingColNames. colNames = colNames.filter(colName -> !existingColNames.contains(colName)); // And add tablecolumns for the rest. List<TableColumn<NameCluster, String>> cols = colNames.distinct().sorted() .map(colName -> createColumnFromPrecalc(colName, precalc)).collect(Collectors.toList()); dataTableView.getColumns().addAll(cols); dataTableView.refresh(); // Fill in status text field. statusTextField .setText(dataTableView.getItems().size() + " rows across " + cols.size() + " reconciled columns"); } private Set<String> getOneElementSet(String str) { HashSet hs = new HashSet<>(); hs.add(str); return hs; } }