/**
*
* 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.util;
import com.speedment.common.injector.Injector;
import com.speedment.common.injector.InjectorBuilder;
import com.speedment.common.injector.annotation.Inject;
import com.speedment.common.injector.annotation.InjectKey;
import com.speedment.common.json.Json;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import com.speedment.generator.translator.TranslatorManager;
import com.speedment.runtime.config.Dbms;
import com.speedment.runtime.config.Project;
import com.speedment.runtime.config.Schema;
import com.speedment.runtime.config.internal.DbmsImpl;
import com.speedment.runtime.config.internal.immutable.ImmutableProject;
import com.speedment.runtime.config.util.DocumentTranscoder;
import com.speedment.runtime.core.Speedment;
import com.speedment.runtime.core.component.DbmsHandlerComponent;
import com.speedment.runtime.core.component.ProjectComponent;
import com.speedment.runtime.core.db.DbmsMetadataHandler;
import com.speedment.runtime.core.db.DbmsType;
import com.speedment.runtime.core.internal.DefaultApplicationBuilder;
import com.speedment.runtime.core.internal.util.ProgressMeasurerImpl;
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.ProjectProperty;
import com.speedment.tool.config.component.DocumentPropertyComponent;
import com.speedment.tool.core.MainApp;
import com.speedment.tool.core.brand.Palette;
import com.speedment.tool.core.component.UserInterfaceComponent;
import com.speedment.tool.core.component.UserInterfaceComponent.ReuseStage;
import com.speedment.tool.core.exception.SpeedmentToolException;
import com.speedment.tool.core.resource.FontAwesome;
import com.speedment.tool.core.resource.SpeedmentIcon;
import com.speedment.tool.core.util.OutputUtil;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import static com.speedment.runtime.core.internal.DefaultApplicationMetadata.METADATA_LOCATION;
import static com.speedment.runtime.core.internal.util.TextUtil.alignRight;
import static com.speedment.tool.core.util.OutputUtil.error;
import static com.speedment.tool.core.util.OutputUtil.success;
import static java.util.stream.Collectors.toSet;
import static javafx.application.Platform.runLater;
/**
*
* @author Emil Forslund
* @since 3.0.0
*/
@InjectKey(ConfigFileHelper.class)
public final class ConfigFileHelper {
private static final Logger LOGGER = LoggerManager.getLogger(ConfigFileHelper.class);
public static final String DEFAULT_CONFIG_LOCATION = "src/main/json/speedment.json";
private static final Predicate<File> OPEN_FILE_CONDITIONS = file
-> file != null
&& file.exists()
&& file.isFile()
&& file.canRead()
&& file.getName().toLowerCase().endsWith(".json");
@Inject private DocumentPropertyComponent documentPropertyComponent;
@Inject private UserInterfaceComponent userInterfaceComponent;
@Inject private DbmsHandlerComponent dbmsHandlerComponenet;
@Inject private TranslatorManager translatorManager;
@Inject private ProjectComponent projectComponent;
@Inject private Injector injector;
private File currentlyOpenFile;
public boolean isFileOpen() {
return currentlyOpenFile != null;
}
public File getCurrentlyOpenFile() {
return currentlyOpenFile;
}
public void setCurrentlyOpenFile(File currentlyOpenFile) {
this.currentlyOpenFile = currentlyOpenFile; // Nullable
}
public void loadFromDatabaseAndSaveToFile() {
final ProjectProperty project = new ProjectProperty();
final Project loaded = projectComponent.getProject();
if (loaded != null) {
project.merge(documentPropertyComponent, loaded);
} else {
throw new SpeedmentToolException(
"Can't load from database unless either a dbms and schema "
+ "is specified or a config file is present."
);
}
project.dbmses().map(dbms -> {
final DbmsType dbmsType = dbmsHandlerComponenet.findByName(dbms.getTypeName())
.orElseThrow(() -> new SpeedmentToolException(
"Could not find dbms type with name '" + dbms.getTypeName() + "'."
));
LOGGER.info(String.format(
"Reloading from dbms '%s' on %s:%d.",
dbms.getName(),
dbms.getIpAddress().orElse("127.0.0.1"),
dbms.getPort().orElseGet(dbmsType::getDefaultPort)
));
final Predicate<String> schemaFilter;
final Set<String> schemaNames = dbms.schemas()
.map(Schema::getName).collect(toSet());
if (schemaNames.isEmpty()) {
schemaFilter = s -> true;
} else {
schemaFilter = schemaNames::contains;
}
return dbmsType.getMetadataHandler()
.readSchemaMetadata(dbms, new ProgressMeasurerImpl(), schemaFilter);
}).forEachOrdered(fut -> {
try {
final Project newProject = fut.get();
synchronized (project) {
project.merge(documentPropertyComponent, newProject);
}
} catch (final ExecutionException ex) {
throw new SpeedmentToolException("Error in execution of reload sequence.", ex);
} catch (final InterruptedException ex) {
throw new SpeedmentToolException("Reload sequence was interrupted.", ex);
}
});
saveConfigFile(
currentlyOpenFile == null
? new File(DEFAULT_CONFIG_LOCATION)
: currentlyOpenFile,
project,
false
);
}
public boolean loadFromDatabase(DbmsProperty dbms, String schemaName) {
try {
// Create an immutable copy of the tree and store in the ProjectComponent
final Project projectCopy = ImmutableProject.wrap(userInterfaceComponent.projectProperty());
projectComponent.setProject(projectCopy);
// TODO: This method needs to be refactored. We create multiple
// copies of the config tree, but most of the copies are not deep
// but shallow, meaning that as soon as you start traversing them
// you risk changing the original map. Not only that, when the new
// nodes are created, they are given a reference to the old mutable
// parent instance, meaning that they can mutate the existing tree.
// It seems to be working for now, mainly because the metadata
// handler already does a second deep safe-copy of the given tree,
// but that is both unnescessary and very bad for load performance.
// We should try to limit the method to a maximum of one deep copy.
// Create a copy of everything in Dbms EXCEPT the schema key. This
// is a hack to prevent duplication errors that would otherwise
// occure if you change name of a node and reload.
final Map<String, Object> dbmsData
= new ConcurrentSkipListMap<>(dbms.getData());
dbmsData.remove(Dbms.SCHEMAS);
final Dbms dbmsCopy = new DbmsImpl(dbms.getParentOrThrow(), dbmsData);
// Find the DbmsHandler to use when loading the metadata
final DbmsMetadataHandler dh = dbmsHandlerComponenet.findByName(dbmsCopy.getTypeName())
.map(DbmsType::getMetadataHandler)
.orElseThrow(() -> new SpeedmentToolException(
"Could not find metadata handler for DbmsType '" + dbmsCopy.getTypeName() + "'."
));
// Begin executing the loading with a progress measurer
final ProgressMeasure progress = ProgressMeasure.create();
final CompletableFuture<Boolean> future
= dh.readSchemaMetadata(dbmsCopy, progress, schemaName::equalsIgnoreCase)
.handleAsync((p, ex) -> {
progress.setProgress(ProgressMeasure.DONE);
// If the loading was successfull
if (ex == null && p != null) {
// Make sure any old data is cleared before merging in
// the new state from the database.
dbms.schemasProperty().clear();
userInterfaceComponent.projectProperty().merge(documentPropertyComponent, p);
return true;
} else {
runLater(() -> {
userInterfaceComponent.showError("Error Connecting to Database",
"A problem occured with establishing the database connection.", ex
);
});
return false;
}
});
userInterfaceComponent.showProgressDialog("Loading Database Metadata", progress, future);
final boolean status = future.get();
if (status) {
userInterfaceComponent.showNotification(
"Database metadata has been loaded.",
FontAwesome.DATABASE,
Palette.INFO
);
}
return status;
} catch (final InterruptedException | ExecutionException ex) {
userInterfaceComponent.showError("Error Executing Connection Task",
"The execution of certain tasks could not be completed.", ex
);
}
return false;
}
public void loadConfigFile(File file, ReuseStage reuse) {
if (OPEN_FILE_CONDITIONS.test(file)) {
try {
switch (reuse) {
case CREATE_A_NEW_STAGE:
final Stage newStage = new Stage();
final InjectorBuilder injectorBuilder = injector.newBuilder()
.withParam(METADATA_LOCATION, DEFAULT_CONFIG_LOCATION);
final Speedment newSpeedment
= new DefaultApplicationBuilder(injectorBuilder)
.build();
MainApp.setInjector(newSpeedment.getOrThrow(Injector.class));
final MainApp main = new MainApp();
try {
main.start(newStage);
} catch (final Exception ex) {
throw new SpeedmentToolException(ex);
}
break;
case USE_EXISTING_STAGE:
final Project p = DocumentTranscoder.load(file.toPath(), this::fromJson);
userInterfaceComponent.projectProperty().merge(documentPropertyComponent, p);
break;
default:
throw new IllegalStateException(
"Unknown enum constant '" + reuse + "'."
);
}
currentlyOpenFile = file;
} catch (final SpeedmentToolException ex) {
LOGGER.error(ex);
userInterfaceComponent.log(error(ex.getMessage()));
userInterfaceComponent.showError("Could not load project", ex.getMessage(), ex);
}
} else {
userInterfaceComponent.showError(
"Could not read .json file",
"The file '" + file.getAbsoluteFile().getName()
+ "' could not be read.",
null
);
}
}
private Map<String, Object> fromJson(String json) {
@SuppressWarnings("unchecked")
final Map<String, Object> parsed = (Map<String, Object>) Json.fromJson(json);
return parsed;
}
public void saveConfigFile() {
final FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save JSON File");
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("JSON files (*.json)", "*.json"));
if (currentlyOpenFile == null) {
final Path path = Paths.get(DEFAULT_CONFIG_LOCATION);
final Path parent = path.getParent();
if (parent == null) {
throw new SpeedmentToolException("Unable to save " + path.toString() + " (no parent).");
}
try {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
} catch (IOException ex) {/*
Do nothing. Creating the parent directory is purely for
the convenience of the user.
*/
}
fileChooser.setInitialDirectory(parent.toFile());
fileChooser.setInitialFileName(Optional.ofNullable(path.getFileName()).map(Path::toString).orElse(""));
} else {
fileChooser.setInitialDirectory(currentlyOpenFile.getParentFile());
fileChooser.setInitialFileName(currentlyOpenFile.getName());
}
File file = fileChooser.showSaveDialog(userInterfaceComponent.getStage());
if (file != null) {
if (!file.getName().endsWith(".json")) {
file = new File(file.getAbsolutePath() + ".json");
}
saveConfigFile(file);
}
}
public void saveCurrentlyOpenConfigFile() {
saveConfigFile(currentlyOpenFile);
}
public void saveConfigFile(File file) {
saveConfigFile(file, userInterfaceComponent.projectProperty(), true);
}
private void saveConfigFile(File file, ProjectProperty project, boolean isGraphical) {
final Path path = file.toPath();
final Path parent = path.getParent();
if (parent == null) {
throw new SpeedmentToolException("Unable to save " + file.toString() + " (no parent).");
}
try {
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
DocumentTranscoder.save(project, path, Json::toJson);
final String absolute = file.getAbsolutePath();
Settings.inst().set("project_location", absolute);
if (isGraphical) {
userInterfaceComponent.log(success("Saved project file to '" + absolute + "'."));
userInterfaceComponent.showNotification("Configuration saved.", FontAwesome.DOWNLOAD, Palette.INFO);
}
currentlyOpenFile = file;
} catch (final IOException ex) {
if (isGraphical) {
userInterfaceComponent.showError("Could not save file", ex.getMessage(), ex);
} else {
throw new SpeedmentToolException(ex);
}
}
}
public void generateSources() {
try {
translatorManager.accept(projectComponent.getProject());
// stopwatch.stop();
runLater(() -> {
userInterfaceComponent.log(OutputUtil.success(
"+------------: Generation completed! :------------+" + "\n"
// + "| Total time " + alignRight(stopwatch.toString(), 30) + " |\n"
+ "| Files generated " + alignRight("" + Integer.toString(translatorManager.getFilesCreated()), 30) + " |\n"
+ "+-------------------------------------------------+"
));
userInterfaceComponent.showNotification(
"Generation completed! " + translatorManager.getFilesCreated()
+ " files created.",
FontAwesome.STAR,
Palette.SUCCESS
);
});
} catch (final Exception ex) {
// if (!stopwatch.isStopped()) {
// stopwatch.stop();
// }
runLater(() -> {
userInterfaceComponent.log(OutputUtil.error(
"+--------------: Generation failed! :-------------+" + "\n"
// + "| Total time " + alignRight(stopwatch.toString(), 30) + " |\n"
+ "| Files generated " + alignRight("" + Integer.toString(translatorManager.getFilesCreated()), 30) + " |\n"
+ "| Exception Type " + alignRight(ex.getClass().getSimpleName(), 30) + " |\n"
+ "+-------------------------------------------------+"
));
final String msg = "Error! Failed to generate code. A " + ex.getClass().getSimpleName() + " was thrown.";
LOGGER.error(ex, msg);
userInterfaceComponent.showError("Failed to generate code", ex.getMessage(), ex);
});
}
}
}