/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cayenne.modeler.dialog.db.merge;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.configuration.event.DataMapEvent;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.dbsync.merge.DataMapMerger;
import org.apache.cayenne.dbsync.merge.context.MergeDirection;
import org.apache.cayenne.dbsync.merge.context.MergerContext;
import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactory;
import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactoryProvider;
import org.apache.cayenne.dbsync.merge.token.MergerToken;
import org.apache.cayenne.dbsync.merge.token.db.AbstractToDbToken;
import org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator;
import org.apache.cayenne.dbsync.naming.NoStemStemmer;
import org.apache.cayenne.dbsync.reverse.dbimport.DefaultDbImportAction;
import org.apache.cayenne.dbsync.reverse.dbload.DbLoader;
import org.apache.cayenne.dbsync.reverse.dbload.DbLoaderConfiguration;
import org.apache.cayenne.dbsync.reverse.dbload.DefaultModelMergeDelegate;
import org.apache.cayenne.dbsync.reverse.dbload.LoggingDbLoaderDelegate;
import org.apache.cayenne.dbsync.reverse.dbload.ModelMergeDelegate;
import org.apache.cayenne.dbsync.reverse.dbload.ProxyModelMergeDelegate;
import org.apache.cayenne.dbsync.reverse.filters.FiltersConfig;
import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
import org.apache.cayenne.dbsync.reverse.filters.TableFilter;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.event.MapEvent;
import org.apache.cayenne.modeler.Application;
import org.apache.cayenne.modeler.ProjectController;
import org.apache.cayenne.modeler.dialog.ValidationResultBrowser;
import org.apache.cayenne.modeler.pref.DBConnectionInfo;
import org.apache.cayenne.modeler.util.CayenneController;
import org.apache.cayenne.project.Project;
import org.apache.cayenne.resource.Resource;
import org.apache.cayenne.swing.BindingBuilder;
import org.apache.cayenne.swing.ObjectBinding;
import org.apache.cayenne.validation.ValidationResult;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Component;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class MergerOptions extends CayenneController {
protected MergerOptionsView view;
protected ObjectBinding sqlBinding;
protected DBConnectionInfo connectionInfo;
protected DataMap dataMap;
protected DbAdapter adapter;
protected String textForSQL;
protected MergerTokenSelectorController tokens;
protected String defaultCatalog;
protected String defaultSchema;
private MergerTokenFactoryProvider mergerTokenFactoryProvider;
public MergerOptions(ProjectController parent,
String title,
DBConnectionInfo connectionInfo,
DataMap dataMap,
String defaultCatalog,
String defaultSchema,
MergerTokenFactoryProvider mergerTokenFactoryProvider) {
super(parent);
this.mergerTokenFactoryProvider = mergerTokenFactoryProvider;
this.dataMap = dataMap;
this.tokens = new MergerTokenSelectorController(parent);
this.view = new MergerOptionsView(tokens.getView());
this.connectionInfo = connectionInfo;
this.defaultCatalog = defaultCatalog;
this.defaultSchema = defaultSchema;
this.view.setTitle(title);
initController();
prepareMigrator();
createSQL();
refreshView();
}
public Component getView() {
return view;
}
public String getTextForSQL() {
return textForSQL;
}
protected void initController() {
BindingBuilder builder = new BindingBuilder(
getApplication().getBindingFactory(),
this);
sqlBinding = builder.bindToTextArea(view.getSql(), "textForSQL");
builder.bindToAction(view.getGenerateButton(), "generateSchemaAction()");
builder.bindToAction(view.getSaveSqlButton(), "storeSQLAction()");
builder.bindToAction(view.getCancelButton(), "closeAction()");
// refresh SQL if different tables were selected
view.getTabs().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
if (view.getTabs().getSelectedIndex() == 1) {
// this assumes that some tables where checked/unchecked... not very
// efficient
refreshGeneratorAction();
}
}
});
}
/**
* check database and create the {@link List} of {@link MergerToken}s
*/
protected void prepareMigrator() {
try {
adapter = connectionInfo.makeAdapter(getApplication().getClassLoadingService());
MergerTokenFactory mergerTokenFactory = mergerTokenFactoryProvider.get(adapter);
tokens.setMergerTokenFactory(mergerTokenFactory);
FiltersConfig filters = FiltersConfig.create(defaultCatalog, defaultSchema, TableFilter.everything(),
PatternFilter.INCLUDE_NOTHING);
DataMapMerger merger = DataMapMerger.builder(mergerTokenFactory)
.filters(filters)
.build();
DbLoaderConfiguration config = new DbLoaderConfiguration();
config.setFiltersConfig(filters);
DataSource dataSource = connectionInfo.makeDataSource(getApplication().getClassLoadingService());
DataMap dbImport;
try (Connection conn = dataSource.getConnection();) {
dbImport = new DbLoader(adapter, conn,
config,
new LoggingDbLoaderDelegate(LoggerFactory.getLogger(DbLoader.class)),
new DefaultObjectNameGenerator(NoStemStemmer.getInstance()))
.load();
} catch (SQLException e) {
throw new CayenneRuntimeException("Can't doLoad dataMap from db.", e);
}
tokens.setTokens(merger.createMergeTokens(dataMap, dbImport));
} catch (Exception ex) {
reportError("Error loading adapter", ex);
}
}
/**
* Returns SQL statements generated for selected schema generation options.
*/
protected void createSQL() {
// convert them to string representation for display
StringBuilder buf = new StringBuilder();
Iterator<MergerToken> it = tokens.getSelectedTokens().iterator();
String batchTerminator = adapter.getBatchTerminator();
String lineEnd = batchTerminator != null ? "\n" + batchTerminator + "\n\n" : "\n\n";
while (it.hasNext()) {
MergerToken token = it.next();
if (token instanceof AbstractToDbToken) {
AbstractToDbToken tdb = (AbstractToDbToken) token;
for (String sql : tdb.createSql(adapter)) {
buf.append(sql);
buf.append(lineEnd);
}
}
}
textForSQL = buf.toString();
}
protected void refreshView() {
sqlBinding.updateView();
}
// ===============
// Actions
// ===============
/**
* Starts options dialog.
*/
public void startupAction() {
view.pack();
view.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
view.setModal(true);
makeCloseableOnEscape();
centerView();
view.setVisible(true);
}
public void refreshGeneratorAction() {
refreshSQLAction();
}
/**
* Updates a text area showing generated SQL.
*/
public void refreshSQLAction() {
createSQL();
sqlBinding.updateView();
}
/**
* Performs configured schema operations via DbGenerator.
*/
public void generateSchemaAction() {
refreshGeneratorAction();
// sanity check...
List<MergerToken> tokensToMigrate = tokens.getSelectedTokens();
if (tokensToMigrate.isEmpty()) {
JOptionPane.showMessageDialog(getView(), "Nothing to migrate.");
return;
}
DataSource dataSource;
try {
dataSource = connectionInfo.makeDataSource(getApplication()
.getClassLoadingService());
} catch (SQLException ex) {
reportError("Migration Error", ex);
return;
}
final Collection<ObjEntity> loadedObjEntities = new LinkedList<>();
MergerContext mergerContext = MergerContext.builder(dataMap)
.syntheticDataNode(dataSource, adapter)
.delegate(createDelegate(loadedObjEntities))
.build();
boolean modelChanged = applyTokens(tokensToMigrate, mergerContext);
DefaultDbImportAction.flattenManyToManyRelationships(
dataMap,
loadedObjEntities,
mergerContext.getNameGenerator());
notifyProjectModified(modelChanged);
reportFailures(mergerContext);
if(tokens.isReverse()) {
getApplication().getUndoManager().discardAllEdits();
}
}
private ModelMergeDelegate createDelegate(final Collection<ObjEntity> loadedObjEntities) {
return new ProxyModelMergeDelegate(new DefaultModelMergeDelegate()) {
@Override
public void objEntityAdded(ObjEntity ent) {
loadedObjEntities.add(ent);
super.objEntityAdded(ent);
}
};
}
private boolean applyTokens(List<MergerToken> tokensToMigrate, MergerContext mergerContext) {
boolean modelChanged = false;
try {
for (MergerToken tok : tokensToMigrate) {
int numOfFailuresBefore = getFailuresCount(mergerContext);
tok.execute(mergerContext);
if (!modelChanged && tok.getDirection().equals(MergeDirection.TO_MODEL)) {
modelChanged = true;
}
if (numOfFailuresBefore == getFailuresCount(mergerContext)) {
// looks like the token executed without failures
tokens.removeToken(tok);
}
}
} catch (Throwable th) {
reportError("Migration Error", th);
}
return modelChanged;
}
private int getFailuresCount(MergerContext mergerContext) {
return mergerContext.getValidationResult().getFailures().size();
}
private void reportFailures(MergerContext mergerContext) {
ValidationResult failures = mergerContext.getValidationResult();
if (failures == null || !failures.hasFailures()) {
JOptionPane.showMessageDialog(getView(), "Migration Complete.");
} else {
new ValidationResultBrowser(this).startupAction(
"Migration Complete",
"Migration finished. The following problem(s) were ignored.",
failures);
}
}
private void notifyProjectModified(boolean modelChanged) {
if(!modelChanged) {
return;
}
// mark the model as unsaved
Project project = getApplication().getProject();
project.setModified(true);
ProjectController projectController = getProjectController();
projectController.setDirty(true);
projectController.fireDataMapEvent(new DataMapEvent(Application.getFrame(),
dataMap, MapEvent.REMOVE));
projectController.fireDataMapEvent(new DataMapEvent(Application.getFrame(),
dataMap, MapEvent.ADD));
}
/**
* Allows user to save generated SQL in a file.
*/
public void storeSQLAction() {
JFileChooser fc = new JFileChooser();
fc.setDialogType(JFileChooser.SAVE_DIALOG);
fc.setDialogTitle("Save SQL Script");
Resource projectDir = getApplication().getProject().getConfigurationResource();
if (projectDir != null) {
fc.setCurrentDirectory(new File(projectDir.getURL().getPath()));
}
if (fc.showSaveDialog(getView()) == JFileChooser.APPROVE_OPTION) {
refreshGeneratorAction();
try {
File file = fc.getSelectedFile();
FileWriter fw = new FileWriter(file);
PrintWriter pw = new PrintWriter(fw);
pw.print(textForSQL);
pw.flush();
pw.close();
} catch (IOException ex) {
reportError("Error Saving SQL", ex);
}
}
}
private ProjectController getProjectController() {
return getApplication().getFrameController().getProjectController();
}
public void closeAction() {
view.dispose();
}
}