All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.speedment.plugins.enums.internal.ui.AddRemoveStringItem Maven / Gradle / Ivy

/*
 *
 * Copyright (c) 2006-2019, Speedment, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); You may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.speedment.plugins.enums.internal.ui;

import static com.speedment.runtime.config.util.DocumentUtil.ancestor;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.CompletableFuture.supplyAsync;
import static java.util.stream.Collectors.joining;
import static javafx.application.Platform.requestNextPulse;
import static javafx.application.Platform.runLater;
import static javafx.scene.layout.Region.USE_PREF_SIZE;

import com.speedment.common.injector.Injector;
import com.speedment.common.injector.State;
import com.speedment.common.injector.annotation.Config;
import com.speedment.common.injector.annotation.ExecuteBefore;
import com.speedment.common.injector.annotation.Inject;
import com.speedment.common.injector.annotation.WithState;
import com.speedment.common.json.Json;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import com.speedment.common.singletonstream.SingletonStream;
import com.speedment.runtime.config.Column;
import com.speedment.runtime.config.Project;
import com.speedment.runtime.config.Schema;
import com.speedment.runtime.config.Table;
import com.speedment.runtime.config.identifier.ColumnIdentifier;
import com.speedment.runtime.config.identifier.TableIdentifier;
import com.speedment.runtime.config.trait.HasName;
import com.speedment.runtime.config.trait.HasParent;
import com.speedment.runtime.core.ApplicationMetadata;
import com.speedment.runtime.core.component.ManagerComponent;
import com.speedment.runtime.core.component.PasswordComponent;
import com.speedment.runtime.core.component.ProjectComponent;
import com.speedment.runtime.core.component.SqlAdapter;
import com.speedment.runtime.core.component.StreamSupplierComponent;
import com.speedment.runtime.core.db.DbmsMetadataHandler;
import com.speedment.runtime.core.db.SqlFunction;
import com.speedment.runtime.core.exception.SpeedmentException;
import com.speedment.runtime.core.internal.component.sql.SqlStreamSupplierComponentImpl;
import com.speedment.runtime.core.manager.HasLabelSet;
import com.speedment.runtime.core.manager.Manager;
import com.speedment.runtime.core.manager.Persister;
import com.speedment.runtime.core.manager.Remover;
import com.speedment.runtime.core.manager.Updater;
import com.speedment.runtime.core.stream.parallel.ParallelStrategy;
import com.speedment.runtime.core.util.ProgressMeasure;
import com.speedment.runtime.field.Field;
import com.speedment.runtime.field.StringField;
import com.speedment.runtime.typemapper.TypeMapper;
import com.speedment.tool.config.DbmsProperty;
import com.speedment.tool.config.trait.HasEnumConstantsProperty;
import com.speedment.tool.config.trait.HasTypeMapperProperty;
import com.speedment.tool.core.component.UserInterfaceComponent;
import com.speedment.tool.core.exception.SpeedmentToolException;
import com.speedment.tool.core.resource.FontAwesome;
import com.speedment.tool.propertyeditor.item.AbstractLabelTooltipItem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;

import java.sql.ResultSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

/**
 * Item for generating a comma-separated string.
 * 

* We parse what values an enum should be able to take from a string, where * each element is separated by a comma. This editor item allows the user * to easily edit such a string. * * @author Simon Jonasson * @since 3.0.0 */ public final class AddRemoveStringItem & HasName> extends AbstractLabelTooltipItem { private final static Logger LOGGER = LoggerManager.getLogger(AddRemoveStringItem.class); private final DOC column; private final ObservableList strings; private final ObservableBooleanValue enabled; @SuppressWarnings("FieldCanBeLocal") private final StringProperty cache; private final String DEFAULT_FIELD = "ENUM_CONSTANT_"; private final double SPACING = 10.0; private final int LIST_HEIGHT = 200; @Inject public ProjectComponent projects; @Inject public DbmsMetadataHandler metadata; @Inject public PasswordComponent passwords; @Inject public UserInterfaceComponent ui; @Inject public Injector injector; //////////////////////////////////////////////////////////////////////////// // Constructor // //////////////////////////////////////////////////////////////////////////// AddRemoveStringItem( final DOC column, final String label, final StringProperty value, final String tooltip, final ObservableBooleanValue enableThis) { super(label, tooltip, NO_DECORATOR); final String currentValue = value.get(); if (currentValue == null) { this.strings = FXCollections.observableArrayList(); } else { this.strings = FXCollections.observableArrayList( Stream.of(currentValue.split(",")) .filter(s -> !s.isEmpty()) .toArray(String[]::new) ); } this.column = requireNonNull(column); this.enabled = enableThis; this.cache = new SimpleStringProperty(); this.strings.addListener((ListChangeListener.Change c) -> { @SuppressWarnings("unchecked") final List list = (List) c.getList(); value.setValue(getFormatedString(list)); }); } //////////////////////////////////////////////////////////////////////////// // Public // //////////////////////////////////////////////////////////////////////////// @Override public Node createLabel() { final Node node = super.createLabel(); hideShowBehaviour(node); return node; } @Override protected Node createUndecoratedEditor() { final VBox container = new VBox(); final ListView listView = new ListView<>(strings); listView.setCellFactory(view -> new EnumCell(strings)); listView.setEditable(true); listView.setMaxHeight(USE_PREF_SIZE); listView.setPrefHeight(LIST_HEIGHT); final HBox controls = new HBox(SPACING); controls.setAlignment(Pos.CENTER); controls.getChildren().addAll( addButton(listView), removeButton(listView), populateButton(listView) ); container.setSpacing(SPACING); container.getChildren().addAll(listView, controls); hideShowBehaviour(container); return container; } //////////////////////////////////////////////////////////////////////////// // Private // //////////////////////////////////////////////////////////////////////////// /** * Removes any empty substrings and makes sure the entire string is either * {@code null} or non-empty. * * @return the formatted string */ private String getFormatedString(List newValue) { final String formated = newValue.stream() .filter(v -> !v.isEmpty()) .collect(joining(",")); if (formated == null || formated.isEmpty()) { return null; } else { return formated; } } private void setValue(String value) { if (value == null) { strings.clear(); } else { strings.setAll(value.split(",")); } } private void hideShowBehaviour(Node node){ node.visibleProperty().bind(enabled); node.managedProperty().bind(enabled); node.disableProperty().bind(Bindings.not(enabled)); } private Button removeButton(final ListView listView) { final Button button = new Button("Remove Selected", FontAwesome.TIMES.view()); button.setOnAction(e -> { final int selectedIdx = listView.getSelectionModel().getSelectedIndex(); if (selectedIdx != -1 && listView.getItems().size() > 1) { final int newSelectedIdx = (selectedIdx == listView.getItems().size() - 1) ? selectedIdx - 1 : selectedIdx; listView.getItems().remove(selectedIdx); listView.getSelectionModel().select(newSelectedIdx); } }); return button; } private Button addButton(final ListView listView) { final Button button = new Button("Add Item", FontAwesome.PLUS.view()); button.setOnAction(e -> { final int newIndex = listView.getItems().size(); final Set set = new HashSet<>(strings); final AtomicInteger i = new AtomicInteger(0); while (!set.add(DEFAULT_FIELD + i.incrementAndGet())) {} listView.getItems().add(DEFAULT_FIELD + i.get()); listView.scrollTo(newIndex); listView.getSelectionModel().select(newIndex); // There is a strange behavior in JavaFX if you try to start editing // a field on the same animation frame as another field lost focus. // Therefore, we wait one animation cycle before setting the field // into the editing state runLater(() -> listView.edit(newIndex)); }); return button; } private Button populateButton(final ListView listView) { final Button button = new Button("Populate", FontAwesome.DATABASE.view()); button.setOnAction(e -> { final Column col = column.getMappedColumn(); final DbmsProperty dbms = ancestor(col, DbmsProperty.class).get(); final String dbmsName = dbms.getName(); final String schemaName = ancestor(col, Schema.class).get().getName(); final String tableName = col.getParentOrThrow().getName(); final String columnName = col.getName(); final ProgressMeasure progress = ProgressMeasure.create(); if (!passwords.get(dbmsName).isPresent()) { ui.showPasswordDialog(dbms); } final char[] password = passwords.get(dbmsName).orElseThrow(() -> new SpeedmentToolException( "A password is required to populate enum constants field!" ) ); final CompletableFuture task = supplyAsync(() -> { try { // Hack to create a Manager for reading a single column from // the database. LOGGER.info("Creating Temporary Speedment..."); progress.setProgress(0.2); final Injector inj = injector.newBuilder() .withComponent(TempApplicationMetadata.class) .withComponent(SqlStreamSupplierComponentImpl.class) .withParam("temp.json", Json.toJson(dbms.getParentOrThrow().getData())) .withParam("temp.dbms", dbmsName) .withParam("temp.schema", schemaName) .withParam("temp.table", tableName) .withParam("temp.column", columnName) .withComponent(SingleColumnManager.class) .withComponent(SingleColumnSqlAdapter.class) .beforeInitialized(PasswordComponent.class, passw -> { LOGGER.info("Installing Password..."); passw.put(dbmsName, password); }) .build(); LOGGER.info("Temporary Speedment built. Streaming..."); progress.setProgress(0.4); final String constants = inj.getOrThrow(SingleColumnManager.class).stream() .distinct().sorted() .collect(joining(",")); LOGGER.info("Streaming complete!"); // Run in UI thread: runLater(() -> { column.enumConstantsProperty().setValue(constants); setValue(constants); progress.setProgress(1.0); }); return true; } catch (final InstantiationException ex) { LOGGER.error(ex); return false; } }).handleAsync((res, ex) -> { if (ex != null) { LOGGER.error(ex); return false; } progress.setProgress(1.0); return true; }); ui.showProgressDialog("Populating Enum Constants", progress, task); }); return button; } //////////////////////////////////////////////////////////////////////////// // Internal Class // //////////////////////////////////////////////////////////////////////////// private static final class TempApplicationMetadata implements ApplicationMetadata { private final String json; public TempApplicationMetadata(@Config(name="temp.json", value="") String json) { this.json = requireNonNull(json); } @Override public Project makeProject() { try { @SuppressWarnings("unchecked") final Map data = (Map) Json.fromJson(json); return Project.create(data); } catch (final ClassCastException ex) { throw new SpeedmentToolException( "Error deserializing temporary project JSON.", ex ); } } } private static final class TempColumnIdentifier implements ColumnIdentifier { private final String dbms; private final String schema; private final String table; private final String column; TempColumnIdentifier(String dbms, String schema, String table, String column) { this.dbms = requireNonNull(dbms); this.schema = requireNonNull(schema); this.table = requireNonNull(table); this.column = requireNonNull(column); } @Override public String getDbmsId() { return dbms; } @Override public String getTableId() { return table; } @Override public String getColumnId() { return column; } @Override public String getSchemaId() { return schema; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ColumnIdentifier)) return false; final ColumnIdentifier that = (ColumnIdentifier) o; return dbms.equals(that.getDbmsId()) && schema.equals(that.getSchemaId()) && table.equals(that.getTableId()) && column.equals(that.getColumnId()); } @Override public int hashCode() { int result = dbms.hashCode(); result = 31 * result + schema.hashCode(); result = 31 * result + table.hashCode(); result = 31 * result + column.hashCode(); return result; } @Override public String toString() { return "TempColumnIdentifier{" + "dbms='" + dbms + '\'' + ", schema='" + schema + '\'' + ", table='" + table + '\'' + ", column='" + column + '\'' + '}'; } } private static final class SingleColumnSqlAdapter implements SqlAdapter { private final TableIdentifier tableId; private final String column; public SingleColumnSqlAdapter( @Config(name = "temp.dbms", value = "") final String dbms, @Config(name = "temp.schema", value = "") final String schema, @Config(name = "temp.table", value = "") final String table, @Config(name = "temp.column", value = "") final String column ) { this.column = requireNonNull(column); this.tableId = TableIdentifier.of( requireNonNull(dbms), requireNonNull(schema), requireNonNull(table) ); } @Override public TableIdentifier identifier() { return tableId; } @Override public SqlFunction entityMapper() { return in -> in.getString(column); } @Override public SqlFunction entityMapper(int offset) { return entityMapper(); // We do not use index and offset } } private static final class SingleColumnManager implements Manager { private final StreamSupplierComponent streamSupplierComponent; private final StringField field; private final TableIdentifier tableId; SingleColumnManager( final StreamSupplierComponent streamSupplierComponent, @Config(name="temp.dbms", value="") final String dbms, @Config(name="temp.schema", value="") final String schema, @Config(name="temp.table", value="") final String table, @Config(name="temp.column", value="") final String column ) { this.streamSupplierComponent = requireNonNull(streamSupplierComponent); requireNonNull(dbms); requireNonNull(schema); requireNonNull(table); requireNonNull(column); this.tableId = TableIdentifier.of(dbms, schema, table); this.field = StringField.create( new TempColumnIdentifier(dbms, schema, table, column), e -> e, (e, s) -> {}, TypeMapper.identity(), false ); } @ExecuteBefore(State.INITIALIZED) public void configureManagerComponent(@WithState(State.INITIALIZED) ManagerComponent managerComponent) { managerComponent.put(this); } @Override public TableIdentifier getTableIdentifier() { return tableId; } @Override public Class getEntityClass() { return String.class; } @Override public Stream> fields() { return SingletonStream.of(field); } @Override public Stream> primaryKeyFields() { return SingletonStream.of(field); } @Override public Stream stream() { return streamSupplierComponent.stream( getTableIdentifier(), ParallelStrategy.computeIntensityDefault() ); } @Override public String create() { throw newUnsupportedOperationExceptionReadOnly(); } @Override public final String persist(String entity) throws SpeedmentException { throw newUnsupportedOperationExceptionReadOnly(); } @Override public Persister persister() { throw newUnsupportedOperationExceptionReadOnly(); } @Override public Persister persister(HasLabelSet fields) { throw newUnsupportedOperationExceptionReadOnly(); } @Override public final String update(String entity) throws SpeedmentException { throw newUnsupportedOperationExceptionReadOnly(); } @Override public Updater updater() { throw newUnsupportedOperationExceptionReadOnly(); } @Override public Updater updater(HasLabelSet fields) { throw newUnsupportedOperationExceptionReadOnly(); } @Override public final String remove(String entity) throws SpeedmentException { throw newUnsupportedOperationExceptionReadOnly(); } @Override public Remover remover() { throw newUnsupportedOperationExceptionReadOnly(); } @Override public String toString() { return getClass().getSimpleName() + "{tableId: " + tableId.toString() + "}"; } } private static final class EnumCell extends TextFieldListCell { private final ObservableList strings; private String labelString; private EnumCell(ObservableList strings) { super(); this.strings = requireNonNull(strings); setConverter(myConverter()); } @Override public void startEdit() { labelString = getText(); super.startEdit(); } private StringConverter myConverter() { return new StringConverter() { @Override public String toString(String value) { return (value != null) ? value : ""; } @Override public String fromString(String value) { // Avoid false positives (ie showing an error that we match ourselves) if (value.equalsIgnoreCase(labelString)) { return value; } else if (value.isEmpty()) { LOGGER.info("An enum field cannot be empty. Please remove the field instead."); return labelString; } // Make sure this is not a duplicate entry final AtomicBoolean duplicate = new AtomicBoolean(false); strings.stream() .filter(elem -> elem.equalsIgnoreCase(value)) .forEach(elem -> duplicate.set(true)); if (duplicate.get()){ LOGGER.info("Enum cannot contain the same constant twice"); return labelString; // Make sure this entry contains only legal characters } else if ( !value.matches("([\\w\\-\\_\\ ]+)")) { LOGGER.info("Enum should only contain letters, number, underscore and/or dashes"); return labelString; // Warn if it contains a space } else if (value.contains(" ")) { LOGGER.warn("Enum spaces will be converted to underscores in Java"); return value; } else { return value; } } }; } } private static UnsupportedOperationException newUnsupportedOperationExceptionReadOnly() { return new UnsupportedOperationException("This manager is read-only."); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy