/***************************************************************** * 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(); } }