public interface MessageByLocaleService {
    String getMessage(String id);
} public class BackgroundTaskExecutor {
    private final Map<UUID, BackgroundTask> runningTasks = new HashMap<>();

    public UUID submit(BackgroundTask task) {
        UUID taskId = UUID.randomUUID();
        runningTasks.put(taskId, task);
        task.run();
        return taskId;
    }

    public Optional<TaskStatistics> findTaskStatistics(UUID taskId) {
        return Optional.ofNullable(runningTasks.get(taskId)).map(BackgroundTask::getStatistics);
    }

    @SneakyThrows
    public void stop(UUID taskId) {
        BackgroundTask task = runningTasks.remove(taskId);
        if (task != null) {
            task.dispose();
            while(!task.isDisposed()) {
                Thread.sleep(100);
            }
        }
    }
} @Slf4j
@Service
@RequiredArgsConstructor
public class CassandraClientAdapter {
    private final AtomicReference<CassandraAdminTemplate> template = new AtomicReference<>();
    private final BeanFactory beanFactory;

    /**
     * Create new session factory bean for provided cassandra endpoint
     *
     * @param connectionData connection metadata
     * @return future providing new instance of {@link CassandraAdminTemplate template} to manage cassandra data
     */
    public CompletableFuture<CassandraAdminTemplate> connect(ConnectionData connectionData) {
        return CompletableFuture.supplyAsync(() -> {
            if (isNullOrEmpty(connectionData.getKeyspace()) || isNullOrEmpty(connectionData.getUrl())) {
                throw new UrlSyntaxException("Both url and keyspace are required");
            }
            String[] urlParts = connectionData.getUrl().split(":");
            if (urlParts.length != 2) {
                throw new UrlSyntaxException("Url should contain host:port");
            }
            int port;
            try {
                port = Integer.parseInt(urlParts[1]);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new UrlSyntaxException("Invalid port : " + urlParts[1]);
            }

            Cluster.Builder builder = Cluster.builder()
                    .withoutJMXReporting()
                    .addContactPoint(urlParts[0])
                    .withPort(port);

            if (StringUtils.isNoneBlank(connectionData.getUsername(), connectionData.getPassword())) {
                builder.withCredentials(connectionData.getUsername(), connectionData.getPassword());
            }

            Cluster cluster = builder.build();

            CassandraSessionFactoryBean factory =
                    beanFactory.getBean(CassandraSessionFactoryBean.class, cluster, connectionData.getKeyspace());
            Field adminTemplateField = findField(CassandraSessionFactoryBean.class, "admin");
            adminTemplateField.setAccessible(true);
            CassandraAdminTemplate template = (CassandraAdminTemplate) getField(adminTemplateField, factory);
            this.template.set(template);
            return template;
        });
    }

    public CompletableFuture<List<String>> getAllKeyspaces(String url, @Nullable String username,
                                                           @Nullable String password) {
        ConnectionData connectionData = new ConnectionData(url, "system_schema", username, password);
        return connect(connectionData)
                .thenApply(template -> template.select("SELECT keyspace_name FROM keyspaces", String.class));
    }

    public CompletableFuture<ResultSet> getAll(String table) {
        return CompletableFuture.supplyAsync(() -> template.get().getCqlOperations()
                .queryForResultSet(format("select * from {}", table)));
    }

    public CompletableFuture<ResultSet> executeStatement(Statement statement) {
        return CompletableFuture.supplyAsync(() -> {
            SessionCallback<ResultSet> callback = session -> session.execute(statement);
            return template.get().getCqlOperations().execute(callback);
        });
    }

    public CompletableFuture<Void> update(TableMetadata metadata, TableColumn.CellEditEvent<DataObject, Object> event) {
        return CompletableFuture.supplyAsync(() -> {
            Stream<ColumnMetadata> primaryKey = metadata.getPrimaryKey().stream();
            Stream<ColumnMetadata> partitionKey = metadata.getPartitionKey().stream();

            Set<String> keyNames = Stream.concat(primaryKey, partitionKey)
                    .map(ColumnMetadata::getName)
                    .collect(toSet());

            int column = event.getTablePosition().getColumn() - 1;
            String updatedColumn = metadata.getColumns().get(column).getName();

            if (keyNames.contains(updatedColumn)) {
                log.warn("Can't update primary key : {}", updatedColumn);
            }

            Update update = QueryBuilder.update(metadata);
            update.with(QueryBuilder.set(updatedColumn, event.getNewValue()));

            for (String columnName : keyNames) {
                update.where(QueryBuilder.eq(columnName, event.getRowValue().get(columnName)));
            }

            log.info("Executing update : {}", update);

            template.get().getCqlOperations().execute(update);
            event.getRowValue().set(updatedColumn, event.getNewValue());
            return null;
        });
    }

    public Mono<Long> count(String table) {
        return Mono.fromFuture(execute(format("select count(*) from {} limit 100000000", table)))
                .map(ResultSet::one)
                .map(row -> row.getLong(0));
    }

    public CompletableFuture<ResultSet> execute(String cql) {
        return CompletableFuture.supplyAsync(() -> template.get().getCqlOperations().queryForResultSet(cql));
    }
} @Slf4j
public class TableContext {

    private final Map<Integer, String> pagingStates = new HashMap<>();
    @Getter
    private final String table;
    private final String query;
    @Getter
    private final TableMetadata tableMetadata;
    @Getter
    private final List<TableColumn<DataObject, Object>> columns;
    private final CassandraClientAdapter clientAdapter;
    private final int pageSize;

    private int page;
    private CompletableFuture<ObservableList<DataObject>> currentPage;
    private volatile ResultSet resultSet;

    private TableContext(String table, String query, TableMetadata tableMetadata, CassandraClientAdapter clientAdapter,
            int pageSize) {
        this.table = table;
        this.tableMetadata = tableMetadata;
        this.clientAdapter = clientAdapter;
        this.pageSize = pageSize;
        this.query = query;
        columns = buildColumns();
    }

    public static TableContext fullTable(String table, TableMetadata tableMetadata,
            CassandraClientAdapter clientAdapter, int pageSize) {
        return new TableContext(table, "select * from " + table, tableMetadata, clientAdapter, pageSize);
    }

    public static TableContext customView(String table, String query, TableMetadata tableMetadata,
            CassandraClientAdapter clientAdapter, int pageSize) {
        return new TableContext(table, query, tableMetadata, clientAdapter, pageSize);
    }

    @Synchronized
    public CompletableFuture<ObservableList<DataObject>> nextPage() {
        if (!hasNextPage()) {
            return currentPage;
        }
        page++;
        currentPage = toCompletable(resultSet.fetchMoreResults())
                .thenApply(this::parseResultSet)
                .thenApply(FXCollections::observableList);
        return currentPage;
    }

    @Synchronized
    public boolean hasPreviousPage() {
        return page > 0;
    }

    @Synchronized
    public boolean hasNextPage() {
        return !resultSet.isExhausted();
    }

    @Synchronized
    public CompletableFuture<ObservableList<DataObject>> previousPage() {
        page = Math.max(0, page - 1);
        currentPage = loadPage(page);
        return currentPage;
    }

    @Synchronized
    private CompletableFuture<ObservableList<DataObject>> loadPage(int page) {
        log.debug("Loading page {}", page);
        Statement statement = getStatement();
        String rawPagingState = pagingStates.get(page);
        if (rawPagingState != null) {
            PagingState pagingState = PagingState.fromString(rawPagingState);
            log.debug("Loading page {} with pagination state {}", page, rawPagingState);
            statement.setPagingState(pagingState);
        }
        return clientAdapter.executeStatement(statement)
                .thenApply(result -> {
                    ExecutionInfo executionInfo = result.getExecutionInfo();
                    executionInfo.getWarnings().forEach(log::warn);
                    printQueryTrace(page, executionInfo.getQueryTraceAsync());
                    this.resultSet = result;
                    return result;
                })
                .thenApply(this::parseResultSet)
                .thenApply(FXCollections::observableList)
                .whenComplete(logErrorIfPresent(format("Page {} load failed", page)));
    }

    private Statement getStatement() {
        Statement statement = new SimpleStatement(query);
        statement.setFetchSize(pageSize);
        return statement.enableTracing();
    }

    private List<DataObject> parseResultSet(ResultSet resultSet) {
        AtomicInteger start = new AtomicInteger(pageSize * page);
        int availableWithoutFetching = resultSet.getAvailableWithoutFetching();
        int currentPageSize = Math.min(availableWithoutFetching, pageSize);
        PagingState nextPageState = resultSet.getExecutionInfo().getPagingState();
        log.debug("Page {} size {}", page, currentPageSize);
        if (nextPageState != null) {
            String value = nextPageState.toString();
            log.debug("Page {} paging state {}", page + 1, value);
            pagingStates.put(page + 1, value);
        } else {
            log.info("No pagination state for remaining entries. Fetching them within current page.");
            currentPageSize = availableWithoutFetching;
        }
        return StreamSupport.stream(resultSet.spliterator(), false)
                .limit(currentPageSize)
                .map(row -> parseRow(start.getAndIncrement(), row))
                .collect(Collectors.toList());
    }

    private DataObject parseRow(int index, Row row) {
        DataObject dataObject = new DataObject(index);
        for (ColumnDefinitions.Definition definition : row.getColumnDefinitions()) {
            TypeCodec<Object> codec = CodecRegistry.DEFAULT_INSTANCE.codecFor(definition.getType());
            dataObject.set(definition.getName(), row.get(definition.getName(), codec));
        }
        return dataObject;
    }

    private List<TableColumn<DataObject, Object>> buildColumns() {
        List<TableColumn<DataObject, Object>> columns = new ArrayList<>();

        TableColumn<DataObject, Object> counterColumn = buildColumn(DataType.cint(), "#");
        counterColumn.setCellValueFactory(param -> {
            Integer object = param.getValue().getPosition();
            return new SimpleObjectProperty<>(object);
        });
        counterColumn.setEditable(false);

        columns.add(counterColumn);

        tableMetadata.getColumns().forEach(columnMetadata -> {
            DataType type = columnMetadata.getType();
            TableColumn<DataObject, Object> column = buildColumn(type, columnMetadata.getName());
            column.setCellValueFactory(param -> {
                Object object = param.getValue().get(columnMetadata.getName());
                return new SimpleObjectProperty<>(object);
            });
            column.setOnEditCommit(event -> {
                log.debug("Updating row value with {}", event.getRowValue());
                clientAdapter.update(tableMetadata, event).whenComplete(loggingConsumer(aVoid -> event.getRowValue()));
            });
            columns.add(column);
        });
        return columns;
    }

    private void printQueryTrace(int page, ListenableFuture<QueryTrace> listenableFuture) {
        EvenMoreFutures.toCompletable(listenableFuture)
                .whenCompleteAsync((trace, throwable) -> {
                    if (trace != null) {
                        log.info("Executed page {} query trace : [{}] " +
                                        "\n\t started at {} and took {} μs " +
                                        "\n\t coordinator {}" +
                                        "\n\t request type {}" +
                                        "\n\t parameters {}" +
                                        "\n\t events",
                                page, trace.getTraceId(), trace.getStartedAt(), trace.getDurationMicros(),
                                trace.getCoordinator(), trace.getRequestType(), trace.getParameters(),
                                trace.getEvents());
                    }
                    if (throwable != null) {
                        log.error("Query trace could not be read", throwable);
                    }
                });

    }
} @Slf4j
@RequiredArgsConstructor
public class CsvWriteBackgroundTask extends AbstractTask {
    private final CsvTargetMetadata metadata;
    private final TableMetadata tableMetadata;
    private final CassandraClientAdapter clientAdapter;

    @Override
    Flux<TaskStatistics> innerRun() {
        Instant start = Instant.now();
        return clientAdapter.count(metadata.getTable())
                .flatMapMany(totalRows -> {
                    long percentRows = Math.max(totalRows / 100, 1);
                    percentRows = Math.min(percentRows, Integer.MAX_VALUE);
                    AtomicLong processedRows = new AtomicLong();
                    List<String> columns = tableMetadata.getColumns().stream()
                            .map(ColumnMetadata::getName).collect(Collectors.toList());
                    CSVPrinter printer = getPrinter(columns);
                    TableContext tableContext = fullTable(metadata.getTable(), tableMetadata, clientAdapter, 3000);
                    return Flux.<CompletableFuture<ObservableList<DataObject>>>create(
                            fluxSink -> {
                                fluxSink.next(tableContext.previousPage());
                                while (tableContext.hasNextPage()) {
                                    fluxSink.next(tableContext.nextPage());
                                }
                                fluxSink.complete();
                            })
                            .map(this::extractValue)
                            .flatMapIterable(Function.identity())
                            .doOnNext(dataObject -> printRow(dataObject, columns, printer))
                            .buffer((int) percentRows)
                            .map(dataObjects -> {
                                Duration duration = Duration.between(start, Instant.now());
                                long processed = processedRows.addAndGet(dataObjects.size());
                                double progress = (double) processed / totalRows;
                                return new TaskStatistics(ACTIVE, start.toEpochMilli(), duration, progress);
                            })
                            .doFinally(signalType -> closePrinter(printer));
                })
                .doOnError(throwable -> log.error("Could not write CSV", throwable));
    }

    @SneakyThrows
    private CSVPrinter getPrinter(List<String> columns) {
        CSVFormat format = metadata.getFormat().withHeader(columns.toArray(new String[0]));
        return new CSVPrinter(new FileWriter(metadata.getTarget()), format);
    }

    @SneakyThrows
    private void closePrinter(CSVPrinter printer) {
        printer.close(true);
    }

    @SneakyThrows
    private ObservableList<DataObject> extractValue(CompletableFuture<ObservableList<DataObject>> future) {
        return future.get();
    }

    @SneakyThrows
    private void printRow(DataObject dataObject, List<String> header, CSVPrinter printer) {
        Object[] fields = header.stream().map(dataObject::get).toArray();
        printer.printRecord(fields);
    }
} @RequiredArgsConstructor
public class TextFieldButtonWatcher implements ChangeListener<String> {
    private final Button button;

    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        button.setDisable(newValue.isEmpty());
    }

    public static TextFieldButtonWatcher wrap(Button button) {
        return new TextFieldButtonWatcher(button);
    }
} @Slf4j
public class DataExportWidget extends Stage {
    private static final List<String> CSV_FORMATS = Arrays.stream(CSVFormat.Predefined.values())
            .map(Object::toString)
            .collect(Collectors.toList());

    private final MonoProcessor<CsvTargetMetadata> metadataProcessor = MonoProcessor.create();
    private final String table;
    private final UIProperties properties;

    public DataExportWidget(Stage parent, String table, MessageByLocaleService localeService, UIProperties properties) {
        this.table = table;
        this.properties = properties;
        setTitle(localeService.getMessage("ui.editor.export.title"));
        getIcons().add(new Image("cassandra_ico.png"));
        initModality(Modality.APPLICATION_MODAL);
        initOwner(parent);
        setScene(buildScene());
        setMinWidth(properties.getExportWidth());
        setMinHeight(properties.getExportHeight());
    }

    public Mono<CsvTargetMetadata> getMetadata() {
        show();
        return metadataProcessor;
    }

    @SneakyThrows
    private Scene buildScene() {
        AnchorPane container = new AnchorPane();
        VBox editor = new VBox();
        editor.setSpacing(properties.getExportSpacing());
        editor.setAlignment(Pos.TOP_CENTER);
        container.getChildren().add(editor);
        fillParent(editor);

        TextField pathField = new TextField();
        URL location = this.getClass().getProtectionDomain().getCodeSource().getLocation();
        File defaultDir = new File(location.toURI()).getParentFile();
        pathField.setText(defaultDir + "/" + table + ".csv");
        Button startButton = new Button("Start export");

        HBox settingsContainer = new HBox();
        settingsContainer.prefWidthProperty().bind(editor.widthProperty());

        Label formatLabel = new Label("CSV format");
        ComboBox<String> formats = buildFormatBox(200);

        CheckBox customizeCheckbox = new CheckBox("override format settings");
        customizeCheckbox.selectedProperty().addListener(create(
                () -> {
                    FormatSettingsBox formatSettingsBox = buildFormatSettingsBox(formats);
                    formatSettingsBox.prefWidthProperty().bind(settingsContainer.widthProperty());
                    settingsContainer.getChildren().add(formatSettingsBox);
                    startButton.setOnAction(event -> {
                        if (customizeCheckbox.isSelected()) {
                            CSVFormat csvFormat = formatSettingsBox.build();
                            File target = new File(pathField.getText());
                            metadataProcessor.onNext(new CsvTargetMetadata(table, csvFormat, target));
                            hide();
                        }
                    });
                },
                () -> settingsContainer.getChildren().clear())
        );

        startButton.setOnAction(event -> {
            if (!customizeCheckbox.isSelected()) {
                FormatSettingsBox formatSettingsBox = buildFormatSettingsBox(formats);
                CSVFormat csvFormat = formatSettingsBox.build();
                File target = new File(pathField.getText());
                metadataProcessor.onNext(new CsvTargetMetadata(table, csvFormat, target));
                hide();
            }
        });

        formats.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (!Objects.equals(oldValue, newValue)) {
                customizeCheckbox.setSelected(!customizeCheckbox.isSelected());
                customizeCheckbox.setSelected(!customizeCheckbox.isSelected());
            }
        });
        HBox format = new HBox(formatLabel, formats, customizeCheckbox);
        format.setAlignment(Pos.CENTER);
        format.setSpacing(properties.getExportSpacing());

        pathField.prefWidthProperty().bind(editor.widthProperty().multiply(0.6));
        Button destinationButton = new Button("Choose destination");
        HBox savePath = new HBox(pathField, destinationButton);
        savePath.setAlignment(Pos.CENTER);
        savePath.setSpacing(properties.getExportSpacing());

        destinationButton.setOnAction(event -> {
            FileChooser savePathProvider = new FileChooser();
            savePathProvider.setTitle("Save CSV");
            savePathProvider.setInitialFileName(table + ".csv");
            savePathProvider.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("CSV", "csv"));
            File file = savePathProvider.showSaveDialog(this);
            if (file != null) {
                pathField.setText(file.getAbsolutePath());
            }
        });

        editor.getChildren().addAll(format, savePath, settingsContainer, startButton);
        return new Scene(container, properties.getExportWidth(), properties.getExportHeight());
    }

    private ComboBox<String> buildFormatBox(int width) {
        ComboBox<String> box = new ComboBox<>(FXCollections.observableList(CSV_FORMATS));
        box.getSelectionModel().select(CSVFormat.Predefined.Default.toString());
        box.setMinWidth(width - 20);
        box.setMaxWidth(width - 20);
        return box;
    }

    private FormatSettingsBox buildFormatSettingsBox(ComboBox<String> formatBox) {
        String selectedFormat = formatBox.getSelectionModel().getSelectedItem();
        return new FormatSettingsBox(CSVFormat.valueOf(selectedFormat), properties.getExportSpacing());
    }

    static class FormatSettingsBox extends HBox {
        private final CSVFormat format;
        private final TextField delimiter;
        private final TextField quoteCharacter;
        private final ComboBox<QuoteMode> quoteModeComboBox;
        private final TextField commentStart;
        private final TextField escapeCharacter;
        private final CheckBox ignoreSurroundingSpaces;
        private final CheckBox allowMissingColumnNames;
        private final CheckBox ignoreEmptyLines;
        private final TextField recordSeparator;
        private final TextField nullString;
        private final TextField headerComments;
        private final TextField headers;
        private final CheckBox skipHeaderRecord;
        private final CheckBox ignoreHeaderCase;
        private final CheckBox trailingDelimiter;
        private final CheckBox trim;

        FormatSettingsBox(CSVFormat format, Integer spacing) {
            this.format = format;
            delimiter = new TextField(String.valueOf(format.getDelimiter()));
            quoteCharacter = new TextField(String.valueOf(format.getQuoteCharacter()));
            quoteModeComboBox = new ComboBox<>(FXCollections.observableList(Arrays.asList(QuoteMode.values())));
            quoteModeComboBox.getSelectionModel().select(format.getQuoteMode());
            commentStart = new TextField(String.valueOf(format.getCommentMarker()));
            escapeCharacter = new TextField(String.valueOf(format.getEscapeCharacter()));
            ignoreSurroundingSpaces = new CheckBox();
            ignoreSurroundingSpaces.selectedProperty().setValue(format.getIgnoreSurroundingSpaces());
            allowMissingColumnNames = new CheckBox();
            allowMissingColumnNames.selectedProperty().setValue(format.getAllowMissingColumnNames());
            ignoreEmptyLines = new CheckBox();
            ignoreEmptyLines.selectedProperty().setValue(format.getIgnoreEmptyLines());
            recordSeparator = new TextField(encloseRecordSeparator(format.getRecordSeparator()));
            nullString = new TextField(format.getNullString());
            headerComments = new TextField(Arrays.toString(format.getHeaderComments()));
            headers = new TextField(Arrays.toString(format.getHeader()));
            skipHeaderRecord = new CheckBox();
            skipHeaderRecord.selectedProperty().setValue(format.getSkipHeaderRecord());
            ignoreHeaderCase = new CheckBox();
            ignoreHeaderCase.selectedProperty().setValue(format.getIgnoreHeaderCase());
            trailingDelimiter = new CheckBox();
            trailingDelimiter.selectedProperty().setValue(format.getTrailingDelimiter());
            trim = new CheckBox();
            trim.selectedProperty().setValue(format.getTrailingDelimiter());

            UIUtil.setWidth(delimiter, 50);
            UIUtil.setWidth(quoteCharacter, 50);
            UIUtil.setWidth(commentStart, 50);
            UIUtil.setWidth(escapeCharacter, 50);
            UIUtil.setWidth(recordSeparator, 50);
            UIUtil.setWidth(nullString, 50);
            UIUtil.setWidth(headerComments, 100);
            UIUtil.setWidth(headers, 100);

            LabeledComponentColumn firstColumn = createColumn(spacing);
            firstColumn.addLabeledElement("Delimiter", delimiter);
            firstColumn.addLabeledElement("Quote Character", quoteCharacter);
            firstColumn.addLabeledElement("Quote Mode", quoteModeComboBox);
            firstColumn.addLabeledElement("Comment Marker", commentStart);
            firstColumn.addLabeledElement("Escape Character", escapeCharacter);
            firstColumn.addLabeledElement("Ignore Surrounding Spaces", ignoreSurroundingSpaces);
            firstColumn.addLabeledElement("Allow Missing Column Names", allowMissingColumnNames);
            firstColumn.addLabeledElement("Ignore Empty Lines", ignoreEmptyLines);

            LabeledComponentColumn secondColumn = createColumn(spacing);
            secondColumn.addLabeledElement("Record Separator", recordSeparator);
            secondColumn.addLabeledElement("Null String", nullString);
            secondColumn.addLabeledElement("Header Comments", headerComments);
            secondColumn.addLabeledElement("Headers", headers);
            secondColumn.addLabeledElement("Skip Header Record", skipHeaderRecord);
            secondColumn.addLabeledElement("Ignore Header Case", ignoreHeaderCase);
            secondColumn.addLabeledElement("Trailing delimiter", trailingDelimiter);
            secondColumn.addLabeledElement("Trim", trim);

            HBox.setMargin(firstColumn, new Insets(0, 20, 0, 20));
            HBox.setMargin(secondColumn, new Insets(0, 20, 0, 20));
            getChildren().addAll(firstColumn, secondColumn);
        }

        CSVFormat build() {
            return format;
        }

        private LabeledComponentColumn createColumn(Integer spacing) {
            LabeledComponentColumn column = new LabeledComponentColumn(spacing);
            column.prefWidthProperty().bind(widthProperty().multiply(0.4));
            column.setAlignment(Pos.CENTER);
            return column;
        }

        private String encloseRecordSeparator(String recordSeparator) {
            switch (recordSeparator) {
                case StringUtils.LF:
                    return "\\n";
                case StringUtils.CR:
                    return "\\r";
                case "\r\n":
                    return "\\r\\n";
            }
            return recordSeparator;
        }
    }
} @Slf4j
public class BackgroundTaskMonitor extends Pane {
    private static final String TEMPLATE = "{} ⏲";
    private final AtomicInteger counter = new AtomicInteger();
    private final Map<UUID, TaskStatistics> taskStatistics = new HashMap<>();

    private final Label label;

    public BackgroundTaskMonitor(UIProperties properties) {
        label = new Label(format(TEMPLATE, counter.get()));
        label.alignmentProperty().setValue(Pos.BASELINE_CENTER);
        UIUtil.setWidth(this, 20);
        getChildren().add(label);
    }

    public void addTask(UUID id) {
        taskStatistics.put(id, null);
        label.setText(format(TEMPLATE, counter.incrementAndGet()));
    }

    public void update(UUID id, TaskStatistics statistics) {
        taskStatistics.put(id, statistics);
    }

    public void remove(UUID id) {
        taskStatistics.remove(id);
        label.setText(format(TEMPLATE, counter.decrementAndGet()));
    }

    public void showTasks() {
        log.debug("!!!!!!!!!!");
    }
} @Slf4j
public class EventLogger extends GridPane {
    private final Label eventLabel;
    private final Label serverLabel;

    public EventLogger() {
        setPrefHeight(30);

        ColumnConstraints column1 = new ColumnConstraints();
        column1.setPercentWidth(65);
        column1.setHgrow(Priority.SOMETIMES);

        ColumnConstraints column2 = new ColumnConstraints();
        column2.setPercentWidth(20);
        column2.setHgrow(Priority.SOMETIMES);

        getColumnConstraints().clear();
        getColumnConstraints().addAll(column1, column2);

        RowConstraints row = new RowConstraints(10, 40, 40);
        row.setVgrow(Priority.SOMETIMES);

        getRowConstraints().clear();
        getRowConstraints().add(row);

        eventLabel = buildEventLabel();
        setConstraints(eventLabel, 0, 0);
        GridPane.setMargin(eventLabel, new Insets(0, 10, 0, 10));

        serverLabel = new Label();
        setConstraints(serverLabel, 1, 0);
        GridPane.setMargin(serverLabel, new Insets(0, 10, 0, 10));
        getChildren().addAll(eventLabel, serverLabel);
    }

    public void fireLogEvent(String template, Object... args) {
        runLater(() -> {
            String message = format(template, args);
            log.info(message);
            eventLabel.setText(message);
        });
    }

    public void clear() {
        runLater(() -> {
            eventLabel.setText("");
            serverLabel.setText("");
        });
    }

    public void printServerName(String url, String keyspace) {
        runLater(() -> serverLabel.setText(url + "/" + keyspace));
    }

    private Label buildEventLabel() {
        Label label = new Label();
        //to create new
        label.opaqueInsetsProperty();
        return label;
    }
} public class TableDataGridPane extends GridPane {
    private final EmitterProcessor<DataObject> processor = EmitterProcessor.create();
    private final FluxSink<DataObject> sink = processor.sink();
    private final FilterGrid filterGrid;
    private final DataTableView dataTableView;
    private final PaginationPanel paginationPanel;

    public TableDataGridPane(FilterGrid filterGrid, DataTableView dataTableView, PaginationPanel paginationPanel) {
        this.filterGrid = filterGrid;
        this.dataTableView = dataTableView;
        this.paginationPanel = paginationPanel;
        setLayoutX(78);
        setLayoutY(131);
        setVisible(false);

        ColumnConstraints column = new ColumnConstraints();
        column.setHgrow(Priority.SOMETIMES);
        column.setMinWidth(10);
        column.setPrefWidth(100);

        getColumnConstraints().clear();
        getColumnConstraints().add(column);

        RowConstraints row1 = new RowConstraints();
        row1.setMaxHeight(40);
        row1.setMinHeight(40);
        row1.setPrefHeight(40);
        row1.setVgrow(Priority.SOMETIMES);

        RowConstraints row2 = new RowConstraints();
        row2.setVgrow(Priority.SOMETIMES);

        RowConstraints row3 = new RowConstraints();
        row3.setMaxHeight(40);
        row3.setMinHeight(40);
        row3.setPrefHeight(40);
        row3.setVgrow(Priority.SOMETIMES);

        getRowConstraints().clear();
        getRowConstraints().addAll(row1, row2, row3);

        add(filterGrid, 0, 0);
        add(dataTableView, 0, 1);
        add(paginationPanel, 0, 2);

        fillParent(this);
    }

    public void updateTableMetadata(TableMetadata metadata) {
        dataTableView.onTableSelected(metadata);
        filterGrid.setTableMetadata(metadata);
    }

    public void setDataColumns(List<TableColumn<DataObject, Object>> columns) {
        dataTableView.setDataColumns(columns);
    }

    public void setData(TableContext context, ObservableList<DataObject> data) {
        dataTableView.setItems(data);
        dataTableView.refresh();
        filterGrid.onDataUpdated(data);

        dataTableView.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.DELETE) {
                TableView.TableViewSelectionModel<DataObject> selectionModel = dataTableView.getSelectionModel();
                DataObject selectedItem = selectionModel.getSelectedItem();
                int selectedIndex = selectionModel.getSelectedIndex();

                data.remove(selectedIndex);
                sink.next(selectedItem);
            }
        });

        paginationPanel.applyOnTable(context, dataObjects -> runLater(() -> {
            dataTableView.setItems(dataObjects);
            dataTableView.refresh();
        }));

        setVisible(true);
    }

    public Flux<DataObject> objectsToRemoveStream() {
        return processor;
    }

    public TableCellCopyHandler buildCopyHandler() {
        return dataTableView.buildCopyHandler();
    }

    public void suggestCompletion() {
        filterGrid.suggestCompletion();
    }
} public class DdlTextArea extends TextArea {
    public DdlTextArea() {
        setEditable(false);
        setPrefSize(200, 200);
        setVisible(false);
        UIUtil.fillParent(this);
    }

    public void showTableDDL(TableMetadata tableMetadata) {
        setText(formatDDL(tableMetadata.toString()));
        setVisible(true);
    }

    public void showException(Throwable throwable) {
        setText(ExceptionUtils.getStackTrace(throwable));
        setVisible(true);
    }
} public class TablePanel extends AnchorPane implements BeanFactoryAware {
    private final TableListView tableListView;
    private final TableButtons buttons;
    private final TableListContext tableContext;
    private final MainController controller;

    public TablePanel(UIProperties uiProperties, MessageByLocaleService localeService, MainController controller) {
        this.controller = controller;
        setPrefHeight(uiProperties.getTablesPrefHeight());
        setPrefWidth(uiProperties.getTablesPrefWidth());

        buttons = new TableButtons();
        tableListView = new TableListView();
        tableListView.onMouseClick(buttons::enableButtons, buttons::disableButtons, this::tryLoadData);
        tableListView.setOnContextMenuRequested(this::onTableContextMenu);

        SplitPane splitPane = splitPane(uiProperties);
        UIUtil.fillParent(splitPane);
        getChildren().add(splitPane);

        tableContext = new TableListContext(localeService);
        tableContext.onDataAction(this::tryLoadData);
        tableContext.onDdlAction(this::tryLoadDDL);
        tableContext.onExportAction(this::tryExportData);
    }

    public void showTables(ObservableList<String> tables) {
        tableListView.showTables(tables);
    }

    public void clear() {
        tableContext.hide();
        tableListView.clear();
    }

    public void setNewValueListener(Consumer<String> newSelectedTableListener) {
        tableListView.onNewValueSelected((observable, oldValue, newValue) -> {
            if (newValue != null) {
                newSelectedTableListener.accept(newValue);
            }
            tableContext.hide();
        });
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        buttons.initActions(beanFactory);
    }

    private SplitPane splitPane(UIProperties uiProperties) {
        SplitPane splitPane = new SplitPane(tableListView, buttons);
        splitPane.setDividerPositions(uiProperties.getTablesDividerPosition());
        splitPane.setFocusTraversable(false);
        splitPane.setMaxHeight(40);
        splitPane.setPrefHeight(40);
        splitPane.setPrefWidth(160);
        splitPane.setScaleShape(false);
        splitPane.setOrientation(Orientation.VERTICAL);
        return splitPane;
    }

    private void tryLoadDDL() {
        tableListView.getSelectedTable().ifPresent(controller::showDDLForTable);
    }

    private void tryLoadData() {
        tableListView.getSelectedTable().ifPresent(controller::showDataForTable);
    }

    private void tryExportData() {
        tableListView.getSelectedTable().ifPresent(controller::exportDataForTable);
    }

    private void onTableContextMenu(ContextMenuEvent event) {
        tableListView.getSelectedTable()
                .ifPresent(table -> tableContext.show(this, event.getScreenX(), event.getScreenY()));
    }
} public static TableContext customView(String table, String query, TableMetadata tableMetadata,
        CassandraClientAdapter clientAdapter, int pageSize) {
    return new TableContext(table, query, tableMetadata, clientAdapter, pageSize);
} public static TableContext fullTable(String table, TableMetadata tableMetadata,
        CassandraClientAdapter clientAdapter, int pageSize) {
    return new TableContext(table, "select * from " + table, tableMetadata, clientAdapter, pageSize);
} public static Optional<String> getSelectTable(String cql) {
    Matcher matcher = select.matcher(cql);
    if (matcher.find()) {
        return Optional.of(matcher.group(4));
    }
    return Optional.empty();
} public static boolean isSelect(String cql) {
    return select.matcher(cql).find();
} public static <O, T extends Throwable> BiConsumer<? super O, ? super T> handleErrorIfPresent(Consumer<T> handler) {
    return (obj, throwable) -> {
        if (throwable != null) {
            handler.accept(throwable);
        }
    };
} public static <K, E> Map<K, E> toMap(Collection<E> collection, Function<E, K> keyMapper) {
    return toMap(collection, keyMapper, Function.identity());
} public static void disable(Node... nodes) {
    for (Node node : nodes) {
        node.setDisable(true);
    }
} public static void enable(Node... nodes) {
    for (Node node : nodes) {
        node.setDisable(false);
    }
}
import com.datastax.driver.core.AbstractTableMetadata;
import com.datastax.driver.core.KeyspaceMetadata;
import com.datastax.driver.core.TableMetadata;
import com.github.kindrat.cassandra.client.i18n.MessageByLocaleService;
import com.github.kindrat.cassandra.client.service.BackgroundTaskExecutor;
import com.github.kindrat.cassandra.client.service.CassandraClientAdapter;
import com.github.kindrat.cassandra.client.service.TableContext;
import com.github.kindrat.cassandra.client.service.tasks.CsvWriteBackgroundTask;
import com.github.kindrat.cassandra.client.ui.listener.TextFieldButtonWatcher;
import com.github.kindrat.cassandra.client.ui.widget.DataExportWidget;
import com.github.kindrat.cassandra.client.ui.window.editor.main.BackgroundTaskMonitor;
import com.github.kindrat.cassandra.client.ui.window.editor.main.EventLogger;
import com.github.kindrat.cassandra.client.ui.window.editor.main.TableDataGridPane;
import com.github.kindrat.cassandra.client.ui.window.editor.main.table.DdlTextArea;
import com.github.kindrat.cassandra.client.ui.window.editor.tables.TablePanel;
import javafx.collections.ObservableMap;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.cassandra.core.CassandraAdminTemplate;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.Optional;
import static com.github.kindrat.cassandra.client.service.TableContext.customView;
import static com.github.kindrat.cassandra.client.service.TableContext.fullTable;
import static com.github.kindrat.cassandra.client.util.CqlUtil.getSelectTable;
import static com.github.kindrat.cassandra.client.util.CqlUtil.isSelect;
import static com.github.kindrat.cassandra.client.util.EvenMoreFutures.handleErrorIfPresent;
import static com.github.kindrat.cassandra.client.util.StreamUtils.toMap;
import static com.github.kindrat.cassandra.client.util.UIUtil.disable;
import static com.github.kindrat.cassandra.client.util.UIUtil.enable;
import static javafx.application.Platform.runLater;
import static javafx.collections.FXCollections.observableArrayList;
import static javafx.scene.input.KeyCode.C;
import static javafx.scene.input.KeyCode.SPACE;
import static javafx.scene.input.KeyCombination.CONTROL_ANY;
package com.github.kindrat.cassandra.client.ui;




@Slf4j
public class MainController {

    private final int pageSize = 1000;
    @Autowired
    private BeanFactory beanFactory;
    @Autowired
    private MessageByLocaleService localeService;
    @Autowired
    private CassandraClientAdapter clientAdapter;
    @Autowired
    private BackgroundTaskExecutor backgroundTaskExecutor;
    @Autowired
    private View view;
    @Autowired
    private TablePanel tablePanel;
    @Autowired
    private EventLogger eventLogger;
    @Autowired
    private BackgroundTaskMonitor backgroundTaskMonitor;

    private Map<String, TableMetadata> tableMetadata;
    private DdlTextArea ddlTextArea;

    @FXML
    private MenuBar menu;
    @FXML
    private SplitPane mainWindow;
    @FXML
    private Button runButton;
    @FXML
    private TextField queryTextField;
    @FXML
    private AnchorPane editorAnchor;

    @Autowired
    private TableDataGridPane tableDataGridPane;