/**
*
* Copyright (c) 2006-2017, 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.tool.core.internal.component;
import com.speedment.common.injector.InjectBundle;
import com.speedment.common.injector.Injector;
import com.speedment.common.injector.annotation.Inject;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import com.speedment.common.mapstream.MapStream;
import com.speedment.generator.translator.TranslatorSupport;
import com.speedment.runtime.config.Dbms;
import com.speedment.runtime.config.Project;
import com.speedment.runtime.config.Schema;
import com.speedment.runtime.config.internal.immutable.ImmutableProject;
import com.speedment.runtime.config.trait.HasMainInterface;
import com.speedment.runtime.core.component.PasswordComponent;
import com.speedment.runtime.core.component.ProjectComponent;
import com.speedment.runtime.core.internal.util.Settings;
import com.speedment.runtime.core.util.ProgressMeasure;
import com.speedment.tool.config.DbmsProperty;
import com.speedment.tool.config.DocumentProperty;
import com.speedment.tool.config.ProjectProperty;
import com.speedment.tool.config.component.DocumentPropertyComponent;
import com.speedment.tool.config.internal.component.DocumentPropertyComponentImpl;
import com.speedment.tool.core.MainApp;
import com.speedment.tool.core.brand.Palette;
import com.speedment.tool.core.component.RuleComponent;
import com.speedment.tool.core.component.UserInterfaceComponent;
import com.speedment.tool.core.internal.brand.SpeedmentBrand;
import com.speedment.tool.core.internal.notification.NotificationImpl;
import com.speedment.tool.core.internal.util.ConfigFileHelper;
import com.speedment.tool.core.internal.util.InjectionLoader;
import com.speedment.tool.core.notification.Notification;
import com.speedment.tool.core.resource.FontAwesome;
import com.speedment.tool.core.resource.Icon;
import com.speedment.tool.core.util.BrandUtil;
import com.speedment.tool.core.util.OutputUtil;
import com.speedment.tool.propertyeditor.PropertyEditor;
import com.speedment.tool.propertyeditor.internal.component.PropertyEditorComponentImpl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Pair;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import static com.speedment.common.invariant.NullUtil.requireNonNulls;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static javafx.application.Platform.runLater;
/**
*
* @author Emil Forslund
*/
public final class UserInterfaceComponentImpl implements UserInterfaceComponent {
private static final Logger LOGGER = LoggerManager.getLogger(UserInterfaceComponentImpl.class);
private static final String GITHUB_URI = "https://github.com/speedment/speedment/";
private static final String GITTER_URI = "https://gitter.im/speedment/speedment/";
private static final Predicate<File> OPEN_DIRECTORY_CONDITIONS = file
-> file != null
&& file.exists()
&& file.isDirectory();
private static final Predicate<Optional<char[]>> NO_PASSWORD_SPECIFIED
= pass -> !pass.isPresent() || pass.get().length == 0;
private final ObjectProperty<StoredNode> hiddenProjectTree = new SimpleObjectProperty<>();
private final ObjectProperty<StoredNode> hiddenWorkspace = new SimpleObjectProperty<>();
private final ObjectProperty<StoredNode> hiddenOutput = new SimpleObjectProperty<>();
private final ObservableList<Notification> notifications;
private final ObservableList<Node> outputMessages;
private final ObservableList<TreeItem<DocumentProperty>> selectedTreeItems;
private final ObservableList<PropertyEditor.Item> properties;
private final Map<Class<?>, List<UserInterfaceComponent.ContextMenuBuilder<?>>> contextMenuBuilders;
private final AtomicBoolean canGenerate;
@Inject private DocumentPropertyComponent documentPropertyComponent;
@Inject private PasswordComponent passwordComponent;
@Inject private ProjectComponent projectComponent;
@Inject private ConfigFileHelper configFileHelper;
@Inject private InjectionLoader loader;
@Inject private RuleComponent rules;
@Inject private Injector injector;
private Stage stage;
private Application application;
private ProjectProperty project;
private UserInterfaceComponentImpl() {
notifications = FXCollections.observableArrayList();
outputMessages = FXCollections.observableArrayList();
selectedTreeItems = FXCollections.observableArrayList();
properties = FXCollections.observableArrayList();
contextMenuBuilders = new ConcurrentHashMap<>();
canGenerate = new AtomicBoolean(true);
}
public static InjectBundle include() {
return InjectBundle.of(
DocumentPropertyComponentImpl.class,
SpeedmentBrand.class,
InjectionLoader.class,
ConfigFileHelper.class,
PropertyEditorComponentImpl.class,
RuleComponentImpl.class,
IssueComponentImpl.class
);
}
public void start(Application application, Stage stage) {
this.stage = requireNonNull(stage);
this.application = requireNonNull(application);
this.project = new ProjectProperty();
LoggerManager.getFactory().addListener(ev -> {
switch (ev.getLevel()) {
case DEBUG : case TRACE : case INFO : {
addToOutputMessages(OutputUtil.info(ev.getMessage()));
break;
}
case WARN : {
addToOutputMessages(OutputUtil.warning(ev.getMessage()));
showNotification(ev.getMessage(), Palette.WARNING);
break;
}
case ERROR : case FATAL : {
addToOutputMessages(OutputUtil.error(ev.getMessage()));
// Hack to remove stack trace from message.
String msg = ev.getMessage();
if (msg.contains("\tat ")) {
msg = msg.substring(0, msg.indexOf("\tat "));
}
break;
}
}
});
final Project loaded = projectComponent.getProject();
if (loaded != null) {
project.merge(documentPropertyComponent, loaded);
}
}
private void addToOutputMessages(Node node) {
runLater(() -> outputMessages.add(node));
}
/*************************************************************/
/* Global properties */
/*************************************************************/
@Override
public ProjectProperty projectProperty() {
return project;
}
@Override
public Application getApplication() {
return application;
}
@Override
public Stage getStage() {
return stage;
}
@Override
public ObservableList<Notification> notifications() {
return notifications;
}
@Override
public ObservableList<Node> outputMessages() {
return outputMessages;
}
@Override
public ObservableList<TreeItem<DocumentProperty>> getSelectedTreeItems() {
return selectedTreeItems;
}
@Override
public ObservableList<PropertyEditor.Item> getProperties() {
return properties;
}
/*************************************************************/
/* Menubar actions */
/*************************************************************/
@Override
public void newProject() {
try {
MainApp.setInjector(injector.newBuilder().build());
final MainApp app = new MainApp();
final Stage newStage = new Stage();
app.start(newStage);
} catch (final Exception e) {
LOGGER.error(e);
showError("Could not create empty project", e.getMessage(), e);
}
}
@Override
public void openProject() {
openProject(ReuseStage.CREATE_A_NEW_STAGE);
}
@Override
public void openProject(ReuseStage reuse) {
final FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open .json File");
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("JSON files (*.json)", "*.json"));
Optional.ofNullable(Settings.inst().get("project_location"))
.map(File::new)
.filter(OPEN_DIRECTORY_CONDITIONS)
.ifPresent(fileChooser::setInitialDirectory);
final File file = fileChooser.showOpenDialog(stage);
if (file != null) {
configFileHelper.loadConfigFile(file, reuse);
}
}
@Override
public void saveProject() {
if (configFileHelper.isFileOpen()) {
configFileHelper.saveCurrentlyOpenConfigFile();
} else {
configFileHelper.saveConfigFile();
}
}
@Override
public void saveProjectAs() {
configFileHelper.saveConfigFile();
}
@Override
public void quit() {
stage.close();
}
@Override
public void reload() {
if (showWarning(
"Do you really want to do this?",
"Reloading the project will remove any changes you have done "
+ "to the project. Are you sure you want to continue?"
).filter(ButtonType.OK::equals).isPresent()) {
project.dbmses()
.filter(dbms -> NO_PASSWORD_SPECIFIED.test(passwordComponent.get(dbms)))
.forEach(this::showPasswordDialog);
final Optional<String> schemaName = project
.dbmses().flatMap(Dbms::schemas)
.map(Schema::getId)
.findAny();
if (schemaName.isPresent()) {
project.dbmses()
.map(DbmsProperty.class::cast)
.forEach(dbms -> configFileHelper.loadFromDatabase(dbms, schemaName.get()));
} else {
showError(
"No Schema Found",
"Can not connect to the database without at least one schema specified."
);
}
}
}
@Override
public void generate() {
//Make sure that no more than one attemp of generating occurs concurrently
final boolean allowed = canGenerate.getAndSet(false);
if( !allowed ){
return;
}
clearLog();
final TranslatorSupport<Project> support = new TranslatorSupport<>(injector, project);
log(OutputUtil.info("Prepairing for generating classes " + support.basePackageName() + "." + project.getId() + ".*"));
log(OutputUtil.info("Target directory is " + project.getPackageLocation()));
log(OutputUtil.info("Performing rule verifications..."));
final Project immutableProject = ImmutableProject.wrap(project);
projectComponent.setProject(immutableProject);
CompletableFuture<Boolean> future = rules.verify();
future.handleAsync((bool, ex) -> {
if (ex != null) {
final String err =
"An error occured while the error checker was looking " +
"for issues in the project configuration.";
LOGGER.error(ex, err);
runLater(() -> {
showError("Error Creating Report", err, ex);
canGenerate.set(true);
});
} else {
if (!bool) {
runLater( () -> {
showIssues();
canGenerate.set(true);
} );
} else {
runLater(() -> log(OutputUtil.info("Rule verifications completed")));
if (!configFileHelper.isFileOpen()) {
configFileHelper.setCurrentlyOpenFile(
new File(ConfigFileHelper.DEFAULT_CONFIG_LOCATION)
);
}
configFileHelper.saveCurrentlyOpenConfigFile();
configFileHelper.generateSources();
canGenerate.set(true);
}
}
return bool;
});
}
@Override
public void prepareToggleProjectTree(BooleanProperty checked) {
toggle(checked, "projectTree", hiddenProjectTree, StoredNode.InsertAt.BEGINNING);
}
@Override
public void prepareToggleWorkspace(BooleanProperty checked) {
toggle(checked, "workspace", hiddenWorkspace, StoredNode.InsertAt.BEGINNING);
}
@Override
public void prepareToggleOutput(BooleanProperty checked) {
toggle(checked, "output", hiddenOutput, StoredNode.InsertAt.END);
}
@Override
public void showGitter() {
browse(GITTER_URI);
}
@Override
public void showGithub() {
browse(GITHUB_URI);
}
private void toggle(
BooleanProperty checked,
String cssId,
ObjectProperty<StoredNode> hidden,
StoredNode.InsertAt insertAt) {
final Runnable toggler = () -> {
final SplitPane parent;
final Node node;
if (hidden.get() == null) {
node = this.stage.getScene().lookup("#" + cssId);
if (node != null) {
Node n = node;
while (!((n = n.getParent()) instanceof SplitPane) && n != null) {
}
parent = (SplitPane) n;
if (parent != null) {
parent.getItems().remove(node);
hidden.set(new StoredNode(node, parent));
} else {
LOGGER.error("Found no SplitPane ancestor of #" + cssId + ".");
}
} else {
LOGGER.error("Non-existing node #" + cssId + " was toggled.");
}
} else {
parent = hidden.get().parent;
if (parent != null) {
node = hidden.get().node;
switch (insertAt) {
case BEGINNING:
parent.getItems().add(0, node);
break;
case END:
parent.getItems().add(node);
break;
default:
throw new UnsupportedOperationException(
"Unknown InsertAt enum constant '" + insertAt + "'."
);
}
hidden.set(null);
} else {
LOGGER.error("Found no parent to node #" + cssId + " that was toggled.");
}
}
};
checked.addListener((ob, o, isChecked) ->
toggler.run()
);
// If the item is unchecked, toggle the component initially.
if (!checked.get()) {
runLater(toggler);
}
}
/*************************************************************/
/* Dialog messages */
/*************************************************************/
@Override
public void showError(String title, String message) {
showError(title, message, null);
}
@Override
public void showError(String title, String message, Throwable ex) {
final Alert alert = new Alert(Alert.AlertType.ERROR);
final Scene scene = alert.getDialogPane().getScene();
BrandUtil.applyBrand(injector, stage, scene);
alert.setHeaderText(title);
alert.setContentText(message);
alert.setGraphic(FontAwesome.EXCLAMATION_TRIANGLE.view());
if (ex == null) {
alert.setTitle("Error");
} else {
alert.setTitle(ex.getClass().getSimpleName());
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
final Label label = new Label("The exception stacktrace was:");
final String exceptionText = sw.toString();
final TextArea textArea = new TextArea(exceptionText);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
final GridPane expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
alert.getDialogPane().setExpandableContent(expContent);
}
alert.showAndWait();
}
@Override
public Optional<ButtonType> showWarning(String title, String message) {
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
final Scene scene = alert.getDialogPane().getScene();
BrandUtil.applyBrand(injector, stage, scene);
alert.setTitle("Confirmation");
alert.setHeaderText(title);
alert.setContentText(message);
alert.setGraphic(FontAwesome.EXCLAMATION_TRIANGLE.view());
return alert.showAndWait();
}
@Override
public void showPasswordDialog(DbmsProperty dbms) {
final Dialog<Pair<String, char[]>> dialog = new Dialog<>();
dialog.setTitle("Authentication Required");
dialog.setHeaderText("Enter password for " + dbms.getName());
dialog.setGraphic(FontAwesome.LOCK.view());
final DialogPane pane = dialog.getDialogPane();
pane.getStyleClass().add("authentication");
final Scene scene = pane.getScene();
BrandUtil.applyBrand(injector, stage, scene);
final ButtonType authButtonType = new ButtonType("OK", ButtonData.OK_DONE);
pane.getButtonTypes().addAll(ButtonType.CANCEL, authButtonType);
final GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10));
final TextField username = new TextField(dbms.getUsername().orElse("Root"));
username.setPromptText("Username");
final PasswordField password = new PasswordField();
password.setPromptText("Password");
grid.add(new Label("Username:"), 0, 0);
grid.add(username, 1, 0);
grid.add(new Label("Password:"), 0, 1);
grid.add(password, 1, 1);
final Node loginButton = pane.lookupButton(authButtonType);
username.textProperty().addListener((ob, o, n)
-> loginButton.setDisable(n.trim().isEmpty())
);
pane.setContent(grid);
Platform.runLater(username::requestFocus);
dialog.setResultConverter(dialogButton -> {
if (dialogButton == authButtonType) {
return new Pair<>(username.getText(), password.getText().toCharArray());
}
return null;
});
Optional<Pair<String, char[]>> result = dialog.showAndWait();
result.ifPresent(usernamePassword -> {
dbms.mutator().setUsername(usernamePassword.getKey());
passwordComponent.put(dbms, usernamePassword.getValue());
});
}
@Override
public void showProgressDialog(String title, ProgressMeasure progress, CompletableFuture<Boolean> task) {
final Dialog<Boolean> dialog = new Dialog<>();
dialog.setTitle("Progress Tracker");
dialog.setHeaderText(title);
dialog.setGraphic(FontAwesome.SPINNER.view());
final DialogPane pane = dialog.getDialogPane();
pane.getStyleClass().add("progress");
final VBox box = new VBox();
final ProgressBar bar = new ProgressBar();
final Label message = new Label();
final Button cancel = new Button("Cancel", FontAwesome.TIMES.view());
box.getChildren().addAll(bar, message, cancel);
box.setMaxWidth(Double.MAX_VALUE);
bar.setMaxWidth(Double.MAX_VALUE);
message.setMaxWidth(Double.MAX_VALUE);
cancel.setMaxWidth(128);
VBox.setVgrow(message, Priority.ALWAYS);
box.setFillWidth(false);
box.setSpacing(8);
progress.addListener(measure -> {
final String msg = measure.getCurrentAction();
final double prg = measure.getProgress();
final boolean done = measure.isDone();
runLater(() -> {
if (done) {
dialog.setResult(true);
dialog.close();
} else {
message.setText(msg);
bar.setProgress(prg);
}
});
});
cancel.setOnAction(ev -> {
if (!task.cancel(true)) {
LOGGER.error("Failed to cancel task.");
}
progress.setCurrentAction("Cancelling...");
progress.setProgress(ProgressMeasure.DONE);
});
pane.setContent(box);
pane.setMaxWidth(Double.MAX_VALUE);
final Scene scene = pane.getScene();
BrandUtil.applyBrand(injector, stage, scene);
if (!progress.isDone()) {
dialog.showAndWait();
}
}
@Override
public void showIssues() {
loader.loadAsModal("ProjectProblem");
}
@Override
public void showNotification(String message) {
showNotification(message, FontAwesome.EXCLAMATION_CIRCLE);
}
@Override
public void showNotification(String message, Icon icon) {
showNotification(message, icon, Palette.INFO);
}
@Override
public void showNotification(String message, Runnable action) {
showNotification(message, FontAwesome.EXCLAMATION_CIRCLE, Palette.INFO, action);
}
@Override
public void showNotification(String message, Palette palette) {
showNotification(message, FontAwesome.EXCLAMATION_CIRCLE, palette);
}
@Override
public void showNotification(String message, Icon icon, Palette palette) {
showNotification(message, icon, palette, () -> {});
}
@Override
public void showNotification(String message, Icon icon, Palette palette, Runnable action) {
runLater(() ->
notifications.add(new NotificationImpl(message, icon, palette, action))
);
}
/*************************************************************/
/* Context Menues */
/*************************************************************/
@Override
public <DOC extends DocumentProperty & HasMainInterface> void
installContextMenu(Class<? extends DOC> nodeType, ContextMenuBuilder<DOC> menuBuilder) {
contextMenuBuilders.computeIfAbsent(nodeType, k -> new CopyOnWriteArrayList<>()).add(menuBuilder);
}
@Override
public <DOC extends DocumentProperty & HasMainInterface> Optional<ContextMenu>
createContextMenu(TreeCell<DocumentProperty> treeCell, DOC doc) {
requireNonNulls(treeCell, doc);
@SuppressWarnings("unchecked")
final List<UserInterfaceComponent.ContextMenuBuilder<DOC>> builders =
MapStream.of(contextMenuBuilders)
.filterKey(clazz -> clazz.isAssignableFrom(doc.getClass()))
.values()
.flatMap(List::stream)
.map(builder -> (UserInterfaceComponent.ContextMenuBuilder<DOC>) builder)
.collect(toList());
final ContextMenu menu = new ContextMenu();
for (int i = 0; i < builders.size(); i++) {
final List<MenuItem> items = builders.get(i)
.build(treeCell, doc)
.collect(toList());
if (i > 0 && !items.isEmpty()) {
menu.getItems().add(new SeparatorMenuItem());
}
items.forEach(menu.getItems()::add);
}
if (menu.getItems().isEmpty()) {
return Optional.empty();
} else {
return Optional.ofNullable(menu);
}
}
/*************************************************************/
/* Other */
/*************************************************************/
@Override
public void clearLog() {
outputMessages.clear();
}
@Override
public void log(Label line) {
outputMessages.add(line);
}
@Override
public void browse(String url) {
application.getHostServices().showDocument(url);
}
private static final class StoredNode {
private enum InsertAt {
BEGINNING, END
}
private final Node node;
private final SplitPane parent;
private StoredNode(Node node, SplitPane parent) {
this.node = requireNonNull(node);
this.parent = requireNonNull(parent);
}
}
}