ca.sqlpower.architect.swingui.ArchitectSwingSessionImpl.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.architect.swingui.ArchitectSwingSessionImpl.java

Source

/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of Power*Architect.
*
* Power*Architect is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* Power*Architect is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>. 
*/
package ca.sqlpower.architect.swingui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import javax.swing.Action;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;

import org.apache.log4j.Logger;

import ca.sqlpower.architect.ArchitectSession;
import ca.sqlpower.architect.ArchitectSessionImpl;
import ca.sqlpower.architect.CoreUserSettings;
import ca.sqlpower.architect.ProjectLoader;
import ca.sqlpower.architect.ProjectSettings;
import ca.sqlpower.architect.ProjectSettings.ColumnVisibility;
import ca.sqlpower.architect.UserSettings;
import ca.sqlpower.architect.ddl.DDLGenerator;
import ca.sqlpower.architect.ddl.LiquibaseSettings;
import ca.sqlpower.architect.enterprise.ArchitectClientSideSession;
import ca.sqlpower.architect.etl.kettle.KettleJob;
import ca.sqlpower.architect.olap.OLAPRootObject;
import ca.sqlpower.architect.olap.OLAPSession;
import ca.sqlpower.architect.profile.ProfileManager;
import ca.sqlpower.architect.profile.ProfileManagerImpl;
import ca.sqlpower.architect.swingui.action.AboutAction;
import ca.sqlpower.architect.swingui.action.AddDataSourceAction;
import ca.sqlpower.architect.swingui.action.NewDataSourceAction;
import ca.sqlpower.architect.swingui.action.OpenProjectAction;
import ca.sqlpower.architect.swingui.action.PreferencesAction;
import ca.sqlpower.architect.swingui.dbtree.DBTreeCellRenderer;
import ca.sqlpower.architect.swingui.olap.OLAPEditSession;
import ca.sqlpower.architect.swingui.olap.OLAPSchemaManager;
import ca.sqlpower.architect.undo.ArchitectUndoManager;
import ca.sqlpower.enterprise.AbstractNetworkConflictResolver;
import ca.sqlpower.object.AbstractPoolingSPListener;
import ca.sqlpower.object.AbstractSPListener;
import ca.sqlpower.object.SPChildEvent;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.object.SPObjectSnapshot;
import ca.sqlpower.sql.DataSourceCollection;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.Olap4jDataSource;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.sqlobject.SQLDatabase;
import ca.sqlpower.sqlobject.SQLObjectException;
import ca.sqlpower.sqlobject.SQLObjectRoot;
import ca.sqlpower.sqlobject.UserDefinedSQLType;
import ca.sqlpower.swingui.DialogUserPrompter;
import ca.sqlpower.swingui.RecentMenu;
import ca.sqlpower.swingui.SPSUtils;
import ca.sqlpower.swingui.SPSwingWorker;
import ca.sqlpower.swingui.SwingUIUserPrompterFactory;
import ca.sqlpower.swingui.db.DataSourceDialogFactory;
import ca.sqlpower.swingui.db.DataSourceTypeDialogFactory;
import ca.sqlpower.swingui.db.DatabaseConnectionManager;
import ca.sqlpower.swingui.event.SessionLifecycleEvent;
import ca.sqlpower.swingui.event.SessionLifecycleListener;
import ca.sqlpower.util.DefaultUserPrompterFactory;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.UserPrompter;
import ca.sqlpower.util.UserPrompter.UserPromptOptions;
import ca.sqlpower.util.UserPrompter.UserPromptResponse;
import ca.sqlpower.util.UserPrompterFactory;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;

public class ArchitectSwingSessionImpl implements ArchitectSwingSession {

    private static final Logger logger = Logger.getLogger(ArchitectSwingSessionImpl.class);

    private static final Executor saveExecutor = new ScheduledThreadPoolExecutor(1);

    private final ArchitectSwingSessionContext context;

    /**
     * This is the core session that some tasks are delegated to.
     */
    private final ArchitectSession delegateSession;

    /**
     * The frame that this project may appear in
     */
    private ArchitectFrame frame;

    /**
     * The panel where the GUI for this session appears
     */
    private JComponent projectPanel;

    private JScrollPane playPenScrollPane;

    private Saver saveBehaviour = new Saver() {
        public boolean save(ArchitectSwingSession session, boolean showChooser, boolean separateThread) {
            final boolean finalSeparateThread = separateThread;
            final ProgressMonitor pm = new ProgressMonitor(frame,
                    Messages.getString("ArchitectSwingSessionImpl.saveProgressDialogTitle"), "", 0, 100); //$NON-NLS-1$ //$NON-NLS-2$

            class SaverTask implements Runnable {
                boolean success;

                public void run() {
                    SwingUIProjectLoader project = getProjectLoader();
                    try {
                        success = false;
                        if (finalSeparateThread) {
                            SwingUtilities.invokeAndWait(new Runnable() {
                                public void run() {
                                    getArchitectFrame().setEnableSaveOption(false);
                                }
                            });
                        }
                        project.setSaveInProgress(true);
                        project.save(finalSeparateThread ? pm : null);
                        success = true;
                    } catch (Exception ex) {
                        success = false;
                        ASUtils.showExceptionDialog(ArchitectSwingSessionImpl.this,
                                Messages.getString("ArchitectSwingSessionImpl.cannotSaveProject") + ex.getMessage(), //$NON-NLS-1$
                                ex);
                    } finally {
                        project.setSaveInProgress(false);
                        if (finalSeparateThread) {
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    getArchitectFrame().setEnableSaveOption(true);
                                }
                            });
                        }
                    }
                }
            }
            SaverTask saveTask = new SaverTask();
            if (separateThread) {
                saveExecutor.execute(saveTask);
                return true; // this is an optimistic lie
            } else {
                saveTask.run();
                return saveTask.success;
            }
        }

        @Override
        public void saveToStream(ArchitectSwingSession session, OutputStream out) throws IOException {
            session.getProjectLoader().save(out, "utf-8");
        }
    };

    /**
     * The menu of recently-opened project files on this system.
     */
    private RecentMenu recent;

    /** the dialog that contains the small ProfileManagerView */
    private JDialog profileDialog;

    /**
     * Keeps track of whether or not the profile manager dialog has been packed yet.
     * We only want to do this the first time we make it visible, since doing it over
     * and over will annoy users.
     */
    private boolean profileDialogPacked = false;

    private final PlayPen playPen;

    /** the small dialog that lists the profiles */
    private ProfileManagerView profileManagerView;

    private CompareDMSettings compareDMSettings;

    private ArchitectUndoManager undoManager;

    private boolean isNew;

    private final DBTree dbTree;

    private KettleJob kettleJob;
    // END STUFF BROUGHT IN FROM SwingUIProject

    private final List<SessionLifecycleListener<ArchitectSession>> lifecycleListeners;

    private Set<SPSwingWorker> swingWorkers;

    private ProjectModificationWatcher projectModificationWatcher;

    private List<OLAPEditSession> olapEditSessions;

    /**
     * A GUI for adding, removing, or opening the OLAP schema edit sessions
     * that belong to this architect session.
     */
    private final OLAPSchemaManager olapSchemaManager;

    /**
     * This will store the properties of the print panel.
     */
    private final PrintSettings printSettings;

    /**
     * This user prompter factory will create all the necessary GUI user prompts
     * for Architect.
     */
    private UserPrompterFactory swinguiUserPrompterFactory;

    /**
     * A colour chooser used by the {@link RelationshipEditPanel}, and possibly
     * others, to set custom colours. It has been created within a swing session
     * to share recent colours amongst different objects.
     */
    private static final JColorChooser colourChooser = new JColorChooser();

    /**
     * The database connection manager GUI for this session (because all sessions
     * do not share the same set of connections, some get theirs locally, and others
     * get theirs from a server).
     */
    private final DatabaseConnectionManager dbConnectionManager;

    /**
     * The Preferences editor for this application.
     */
    private final PreferencesEditor prefsEditor;

    /**
     * This factory just passes the request through to the {@link ASUtils#showDbcsDialog(Window, SPDataSource, Runnable)}
     * method.
     */
    private static final DataSourceDialogFactory dsDialogFactory = new DataSourceDialogFactory() {

        public JDialog showDialog(Window parentWindow, JDBCDataSource dataSource, Runnable onAccept) {
            return ASUtils.showDbcsDialog(parentWindow, dataSource, onAccept);
        }

        public JDialog showDialog(Window parentWindow, Olap4jDataSource dataSource,
                DataSourceCollection<? super JDBCDataSource> dsCollection, Runnable onAccept) {
            throw new UnsupportedOperationException(
                    "There is no editor dialog for Olap4j connections in Architect.");
        }

    };

    /**
     * This factory just passes the request through to the {@link ASUtils#showDbcsDialog(Window, SPDataSource, Runnable)}
     * method.
     */
    private final DataSourceTypeDialogFactory dsTypeDialogFactory = new DataSourceTypeDialogFactory() {
        public Window showDialog(Window owner) {
            return prefsEditor.showJDBCDriverPreferences(owner, ArchitectSwingSessionImpl.this);
        }
    };

    /**
     * Creates a new, swing ready, architect session. It will not be displayed
     * anywhere until it is added to an {@link ArchitectFrame}.
     * 
     * @param context The {@link ArchitectSwingSessionContext} that owns this session.
     * @param name The User visible name of this session.
     * @throws SQLObjectException
     */
    ArchitectSwingSessionImpl(final ArchitectSwingSessionContext context, String name) throws SQLObjectException {
        this(context, new ArchitectSessionImpl(context, name, new ArchitectSwingProject()));
    }

    /**
     * Creates a new, swing ready, architect session. It will not be displayed
     * anywhere until it is added to an {@link ArchitectFrame}.
     * 
     * @param context
     *            The {@link ArchitectSwingSessionContext} that owns this
     *            session.
     * @param delegateSession
     *            An {@link ArchitectSession} that this session will delegate
     *            functionality to. This session must have a swing project in
     *            order to properly maintain the state of the swing components.
     */
    ArchitectSwingSessionImpl(final ArchitectSwingSessionContext context, ArchitectSession delegateSession) {

        if (!(delegateSession.getWorkspace() instanceof ArchitectSwingProject)) {
            throw new IllegalStateException("The delegate session must have a swing project"
                    + "as its workspace. If there is no way to pass in a delegate with a swing"
                    + "project we may need to make the reference non-final.");
        }
        swinguiUserPrompterFactory = new DefaultUserPrompterFactory();

        this.isNew = true;
        this.context = context;
        this.delegateSession = delegateSession;
        getWorkspace().setSession(this);
        ProfileManagerImpl profileManager = new ProfileManagerImpl();
        ((ArchitectSessionImpl) delegateSession).setProfileManager(profileManager);
        ((ArchitectSessionImpl) delegateSession).setUserPrompterFactory(this);
        this.recent = new RecentMenu(this.getClass()) {
            @Override
            public void loadFile(String fileName) throws IOException {
                File f = new File(fileName);
                try {
                    OpenProjectAction.getFileLoader().open(getContext().createSession(), f,
                            ArchitectSwingSessionImpl.this, true);
                } catch (SQLObjectException ex) {
                    SPSUtils.showExceptionDialogNoReport(getArchitectFrame(),
                            Messages.getString("ArchitectSwingSessionImpl.openProjectFileFailed"), ex); //$NON-NLS-1$
                }
            }
        };

        dbConnectionManager = new DatabaseConnectionManager(delegateSession.getDataSources(), dsDialogFactory,
                dsTypeDialogFactory);

        setProjectLoader(new SwingUIProjectLoader(this));

        compareDMSettings = new CompareDMSettings();

        kettleJob = new KettleJob(this);

        olapSchemaManager = new OLAPSchemaManager(this);

        this.dbTree = new DBTree(this);

        if (isEnterpriseSession()) {
            ((DBTreeCellRenderer) dbTree.getCellRenderer()).addIconFilter(new DomainCategorySnapshotIconFilter());
        }

        playPen = RelationalPlayPenFactory.createPlayPen(this, dbTree);
        this.getWorkspace().setPlayPenContentPane(playPen.getContentPane());
        UserSettings sprefs = getUserSettings().getSwingSettings();
        if (sprefs != null && playPen.getZoom() == 1.0) {
            playPen.setRenderingAntialiased(
                    sprefs.getBoolean(ArchitectSwingUserSettings.PLAYPEN_RENDER_ANTIALIASED, false));
            Object d = sprefs.getObject("zoom", new Double(1.0));
            if (!(d instanceof Double)) {
                throw new RuntimeException("Your playpen zoom must be a double");
            }
            playPen.setZoom((Double) d);

        }
        projectModificationWatcher = new ProjectModificationWatcher(playPen);

        getRootObject().addSPListener(new AbstractPoolingSPListener() {
            @Override
            public void propertyChangeImpl(PropertyChangeEvent e) {
                isNew = false;
            }

            @Override
            public void childRemovedImpl(SPChildEvent e) {
                isNew = false;
            }

            @Override
            public void childAddedImpl(SPChildEvent e) {
                isNew = false;
            }
        });
        undoManager = new ArchitectUndoManager(playPen);
        final PropertyChangeListener undoListener = undoManager.getEventAdapter();

        playPen.getContentPane().addComponentPropertyListener(
                new String[] { "topLeftCorner", "lengths", "pkConnection", "fkConnection", "backgroundColor",
                        "foregroundColor", "dashed", "rounded" },
                // Simple conversion from PropertyChangeListener to SPListener
                new AbstractSPListener() {
                    public void propertyChanged(PropertyChangeEvent evt) {
                        undoListener.propertyChange(evt);
                    }
                });

        getWorkspace().setPlayPenContentPane(playPen.getContentPane());

        lifecycleListeners = new ArrayList<SessionLifecycleListener<ArchitectSession>>();

        swingWorkers = new HashSet<SPSwingWorker>();

        olapEditSessions = new ArrayList<OLAPEditSession>();

        printSettings = new PrintSettings();

        prefsEditor = new PreferencesEditor();

        // Ensure the this swing session is listening to the project settings
        // so it can repaint and update the UI when it is changed.

        final SPListener settingsListener = new AbstractSPListener() {
            public void propertyChanged(PropertyChangeEvent evt) {
                getPlayPen().updateTablePanes();
                getPlayPen().repaint();
            }
        };
        getWorkspace().addSPListener(new AbstractSPListener() {
            public void childAdded(SPChildEvent evt) {
                if (evt.getChildType() == ProjectSettings.class) {
                    evt.getChild().addSPListener(settingsListener);
                }
            }

            public void childRemoved(SPChildEvent evt) {
                if (evt.getChildType() == ProjectSettings.class) {
                    evt.getChild().removeSPListener(settingsListener);
                }
            }
        });
        getWorkspace().getProjectSettings().addSPListener(settingsListener);

        getWorkspace().getCriticManager().registerStartingCritics();

    }

    /**
     * Creates the GUI components for this session, and parents them to the
     * given ArchitectFrame. This should only be called by the frame itself, and
     * then only if you need a GUI.
     */
    public void initGUI(ArchitectFrame parentFrame) {
        if (!SwingUtilities.isEventDispatchThread()) {
            throw new IllegalStateException("This method must be called on the Swing Event Dispatch Thread."); //$NON-NLS-1$
        }

        this.frame = parentFrame;

        ((ArchitectSessionImpl) delegateSession).setStatusInfo(getStatusInformation());

        // makes the tool tips show up on these components 
        ToolTipManager.sharedInstance().registerComponent(playPen);
        ToolTipManager.sharedInstance().registerComponent(dbTree);

        swinguiUserPrompterFactory = new SwingUIUserPrompterFactory(parentFrame);

        if (projectPanel == null) {
            playPenScrollPane = new JScrollPane(playPen);
            projectPanel = new JPanel();
            projectPanel.setLayout(new BorderLayout());
            projectPanel.add(playPenScrollPane, BorderLayout.CENTER);
        }

        profileDialog = new JDialog(frame, Messages.getString("ArchitectSwingSessionImpl.profilesDialogTitle")); //$NON-NLS-1$
        profileManagerView = new ProfileManagerView(delegateSession.getProfileManager());
        delegateSession.getProfileManager().addProfileChangeListener(profileManagerView);
        profileDialog.add(profileManagerView);

        // This has to be called after frame.init() because playPen gets the keyboard actions from frame,
        // which only get set up after calling frame.init().
        RelationalPlayPenFactory.setupKeyboardActions(playPen, this);
        dbTree.setupKeyboardActions();

        macOSXRegistration(frame);

        profileDialog.setLocationRelativeTo(frame);
    }

    public SwingUIProjectLoader getProjectLoader() {
        return (SwingUIProjectLoader) delegateSession.getProjectLoader();
    }

    public void setProjectLoader(ProjectLoader project) {
        delegateSession.setProjectLoader(project);
    }

    public CoreUserSettings getUserSettings() {
        return context.getUserSettings();
    }

    public ArchitectSwingSessionContext getContext() {
        return context;
    }

    public ArchitectFrame getArchitectFrame() {
        return frame;
    }

    /**
     * Registers this application in Mac OS X if we're running on that platform.
     *
     * <p>This code came from Apple's "OS X Java Adapter" example.
     */
    private void macOSXRegistration(ArchitectFrame frame) {

        Action exitAction = frame.getExitAction();
        PreferencesAction prefAction = frame.getPrefAction();
        AboutAction aboutAction = frame.getAboutAction();
        Action openProjectAction = frame.getOpenProjectAction();

        // Whether or not this is OS X, the three actions we're referencing must have been initialized by now.
        if (exitAction == null)
            throw new IllegalStateException("Exit action has not been initialized"); //$NON-NLS-1$
        if (prefAction == null)
            throw new IllegalStateException("Prefs action has not been initialized"); //$NON-NLS-1$
        if (aboutAction == null)
            throw new IllegalStateException("About action has not been initialized"); //$NON-NLS-1$
        if (openProjectAction == null)
            throw new IllegalStateException("Open Project action has not been initialized"); //$NON-NLS-1$

        if (context.isMacOSX()) {
            try {
                Class<?> osxAdapter = ArchitectSwingSessionImpl.class.getClassLoader()
                        .loadClass("ca.sqlpower.architect.swingui.OSXAdapter"); //$NON-NLS-1$

                // The main registration method.  Takes quitAction, prefsAction, aboutAction, openAction.
                Class<?>[] defArgs = { Action.class, Action.class, Action.class, Action.class };
                Method registerMethod = osxAdapter.getDeclaredMethod("registerMacOSXApplication", defArgs); //$NON-NLS-1$
                Object[] args = { exitAction, prefAction, aboutAction, openProjectAction };
                registerMethod.invoke(osxAdapter, args);

                // The enable prefs method.  Takes a boolean.
                defArgs = new Class[] { boolean.class };
                Method prefsEnableMethod = osxAdapter.getDeclaredMethod("enablePrefs", defArgs); //$NON-NLS-1$
                args = new Object[] { Boolean.TRUE };
                prefsEnableMethod.invoke(osxAdapter, args);
            } catch (NoClassDefFoundError e) {
                // This will be thrown first if the OSXAdapter is loaded on a system without the EAWT
                // because OSXAdapter extends ApplicationAdapter in its def
                System.err.println(
                        "This version of Mac OS X does not support the Apple EAWT.  Application Menu handling has been disabled (" //$NON-NLS-1$
                                + e + ")"); //$NON-NLS-1$
            } catch (ClassNotFoundException e) {
                // This shouldn't be reached; if there's a problem with the OSXAdapter we should get the
                // above NoClassDefFoundError first.
                System.err.println(
                        "This version of Mac OS X does not support the Apple EAWT.  Application Menu handling has been disabled (" //$NON-NLS-1$
                                + e + ")"); //$NON-NLS-1$
            } catch (Exception e) {
                System.err.println("Exception while loading the OSXAdapter:"); //$NON-NLS-1$
                e.printStackTrace();
            }
        }
    }

    /**
     * Checks if the project is modified, and if so presents the user with the option to save
     * the existing project.  This is useful to use in actions that are about to get rid of
     * the currently open project.
     *
     * @return True if the project can be closed; false if the project should remain open.
     */
    protected boolean promptForUnsavedModifications() {
        if (getProjectLoader().isModified() && frame != null) {
            int response = JOptionPane.showOptionDialog(frame,
                    Messages.getString("ArchitectSwingSessionImpl.projectHasUnsavedChanges"), //$NON-NLS-1$
                    Messages.getString("ArchitectSwingSessionImpl.unsavedChangesDialogTitle"), //$NON-NLS-1$
                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
                    new Object[] { Messages.getString("ArchitectSwingSessionImpl.doNotSaveOption"), //$NON-NLS-1$
                            Messages.getString("ArchitectSwingSessionImpl.cancelOption"), //$NON-NLS-1$
                            Messages.getString("ArchitectSwingSessionImpl.saveOption") }, //$NON-NLS-1$
                    Messages.getString("ArchitectSwingSessionImpl.saveOption")); //$NON-NLS-1$
            if (response == 0) {
                return true;
            } else if (response == JOptionPane.CLOSED_OPTION || response == 1) {
                return false;
            } else {
                return saveOrSaveAs(false, false);
            }
        } else {
            return true;
        }
    }

    /**
     * Condition the Model to save the project, showing a file chooser when appropriate.
     *
     * @param showChooser If true, a chooser will always be shown; otherwise a
     * chooser will only be shown if the project has no file associated with it
     * (this is usually because it has never been saved before).
     * @param separateThread If true, the (possibly lengthy) save operation
     * will be executed in a separate thread and this method will return immediately.
     * Otherwise, the save operation will proceed on the current thread.
     * @return True if the project was saved successfully; false otherwise.  If saving
     * on a separate thread, a result of <code>true</code> is just an optimistic guess,
     * and there is no way to discover if the save operation has eventually succeeded or
     * failed.
     */
    public boolean saveOrSaveAs(boolean showChooser, boolean separateThread) {
        SwingUIProjectLoader project = getProjectLoader();

        if (project.getFile() == null || showChooser) {
            JFileChooser chooser = new JFileChooser(project.getFile());
            chooser.addChoosableFileFilter(SPSUtils.ARCHITECT_FILE_FILTER);
            int response = chooser.showSaveDialog(frame);
            if (response != JFileChooser.APPROVE_OPTION) {
                return false;
            } else {
                File file = chooser.getSelectedFile();
                if (!file.getPath().endsWith(".architect")) { //$NON-NLS-1$
                    file = new File(file.getPath() + ".architect"); //$NON-NLS-1$
                }
                if (file.exists()) {
                    response = JOptionPane.showConfirmDialog(frame,
                            Messages.getString("ArchitectSwingSessionImpl.fileAlreadyExists", file.getPath()), //$NON-NLS-1$
                            Messages.getString("ArchitectSwingSessionImpl.fileAlreadyExistsDialogTitle"), //$NON-NLS-1$
                            JOptionPane.YES_NO_OPTION);
                    if (response == JOptionPane.NO_OPTION) {
                        return saveOrSaveAs(true, separateThread);
                    }
                }

                //creates an empty file if "file" does not exist 
                //so that the new file can be found by the recent menu
                try {
                    file.createNewFile();
                } catch (Exception e) {
                    ASUtils.showExceptionDialog(this,
                            Messages.getString("ArchitectSwingSessionImpl.couldNotCreateFile"), e); //$NON-NLS-1$
                    return false;
                }

                getRecentMenu().putRecentFileName(file.getAbsolutePath());
                project.setFile(file);
                project.clearFileVersion();
                String projName = file.getName().substring(0, file.getName().length() - ".architect".length()); //$NON-NLS-1$
                setName(projName);
            }
        }
        return saveBehaviour.save(this, showChooser, separateThread);
    }

    public Executor getSaveExecutor() {
        return saveExecutor;
    }

    // STUFF BROUGHT IN FROM SwingUIProject

    /**
     * This is a common handler for all actions that must occur when switching
     * projects, e.g., prompting to save any unsaved changes, disposing dialogs,
     * shutting down running threads, and so on.
     */
    public boolean close() {
        // IMPORTANT NOTE: If the GUI hasn't been initialized, frame will be null.

        if (!delegateSession.isEnterpriseSession()) {
            // Only prompt to save if it is not an enterprise session
            if (getProjectLoader().isSaveInProgress()) {
                // project save is in progress, don't allow exit
                if (frame != null) {
                    JOptionPane.showMessageDialog(frame,
                            Messages.getString("ArchitectSwingSessionImpl.cannotExitWhileSaving"), //$NON-NLS-1$
                            Messages.getString("ArchitectSwingSessionImpl.cannotExitWhileSavingDialogTitle"), //$NON-NLS-1$
                            JOptionPane.WARNING_MESSAGE);
                }
                return false;
            }

            if (!promptForUnsavedModifications()) {
                return false;
            }
        } else {
            getEnterpriseSession().putPref("zoom", playPen.getZoom());
        }

        if (!delegateSession.close()) {
            return false;
        }

        // If we still have ArchitectSwingWorker threads running, 
        // tell them to cancel, and then ask the user to try again later.
        // Note that it is not safe to force threads to stop, so we will
        // have to wait until the threads stop themselves.
        if (swingWorkers.size() > 0) {
            for (SPSwingWorker currentWorker : swingWorkers) {
                currentWorker.setCancelled(true);
            }

            Object[] options = { Messages.getString("ArchitectSwingSessionImpl.waitOption"), //$NON-NLS-1$
                    Messages.getString("ArchitectSwingSessionImpl.forceQuiteOption") }; //$NON-NLS-1$
            int n = JOptionPane.showOptionDialog(frame,
                    Messages.getString("ArchitectSwingSessionImpl.unfinishedTasksRemaining"), //$NON-NLS-1$
                    Messages.getString("ArchitectSwingSessionImpl.unfinishedTasksDialogTitle"), //$NON-NLS-1$
                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);

            if (n == 0) {
                return false;
            } else {
                for (SPSwingWorker currentWorker : swingWorkers) {
                    currentWorker.kill();
                    currentWorker.setCancelled(true);
                }
            }
        }

        if (profileDialog != null) {
            // XXX this could/should be done by the profile dialog with a session closing listener
            profileDialog.dispose();
        }

        // Write pl.ini data back
        try {
            if (!isEnterpriseSession())
                if (context.getPlDotIniPath() != null) {
                    getDataSources().write(new File(context.getPlDotIniPath()));
                } else {
                    getDataSources().write();
                }
        } catch (IOException e) {
            logger.error("Couldn't save PL.INI file!", e); //$NON-NLS-1$
        }

        // close connections
        for (SQLDatabase db : getRootObject().getChildren(SQLDatabase.class)) {
            logger.debug("closing connection: " + db.getName()); //$NON-NLS-1$
            db.disconnect();
        }

        // Clear the profile manager (the effect we want is just to cancel running profiles.. clearing is a harmless side effect)
        // XXX this could/should be done by the profile manager with a session closing listener
        delegateSession.getProfileManager().clear();

        fireSessionClosing();

        return true;
    }

    /**
     * Gets the value of sourceDatabases
     *
     * @return the value of sourceDatabases
     */
    public DBTree getDBTree() {
        return this.dbTree;
    }

    public void setSourceDatabaseList(List<SQLDatabase> databases) throws SQLObjectException {
        delegateSession.setSourceDatabaseList(databases);
    }

    /**
     * Gets the target database in the playPen.
     */
    public SQLDatabase getTargetDatabase() {
        return delegateSession.getTargetDatabase();
    }

    /**
     * The ProjectModificationWatcher watches a PlayPen's components and
     * business model for changes.  When it detects any, it marks the
     * project dirty.
     *
     * <p>Note: when we implement proper undo/redo support, this class should
     * be replaced with a hook into that system.
     */
    class ProjectModificationWatcher extends AbstractSPListener {

        /**
         * Sets up a new modification watcher on the given playpen.
         */
        public ProjectModificationWatcher(PlayPen pp) {
            SQLPowerUtils.listenToHierarchy(getTargetDatabase(), this);
            PlayPenContentPane ppcp = pp.getContentPane();
            ppcp.addComponentPropertyListener(this);
        }

        /** Marks project dirty, and starts listening to new kids. */
        public void childAdded(SPChildEvent e) {
            getProjectLoader().setModified(true);
            SQLPowerUtils.listenToHierarchy(e.getChild(), this);
            isNew = false;
        }

        /** Marks project dirty, and stops listening to removed kids. */
        public void childRemoved(SPChildEvent e) {
            getProjectLoader().setModified(true);
            SQLPowerUtils.unlistenToHierarchy(e.getChild(), this);
            isNew = false;
        }

        /** Marks project dirty. */
        public void propertyChanged(PropertyChangeEvent e) {
            getProjectLoader().setModified(true);
            isNew = false;
        }
    }

    public String getServerName() {
        if (delegateSession instanceof ArchitectClientSideSession) {
            return ((ArchitectClientSideSession) delegateSession).getProjectLocation().getServiceInfo().getName();
        } else {
            return null;
        }
    }

    /**
     * Gets the value of name
     *
     * @return the value of name
     */
    public String getName() {
        return delegateSession.getName();
    }

    /**
     * Sets the value of name
     *
     * @param argName Value to assign to this.name
     */
    public void setName(String argName) {
        delegateSession.setName(argName);
    }

    /**
     * Gets the value of playPen
     *
     * @return the value of playPen
     */
    public PlayPen getPlayPen() {
        return this.playPen;
    }

    /**
     * Gets the recent menu list
     * 
     * @return the recent menu
     */
    public RecentMenu getRecentMenu() {
        return this.recent;
    }

    public LiquibaseSettings getLiquibaseSettings() {
        return delegateSession.getLiquibaseSettings();
    }

    public void setLiquibaseSettings(LiquibaseSettings settings) {
        delegateSession.setLiquibaseSettings(settings);
    }

    public CompareDMSettings getCompareDMSettings() {
        return compareDMSettings;
    }

    public void setCompareDMSettings(CompareDMSettings compareDMSettings) {
        this.compareDMSettings = compareDMSettings;
    }

    public ArchitectUndoManager getUndoManager() {
        return undoManager;
    }

    public ProfileManager getProfileManager() {
        return delegateSession.getProfileManager();
    }

    public JDialog getProfileDialog() {
        if (!profileDialogPacked) {
            profileDialog.pack();
            profileDialogPacked = true;
        }
        return profileDialog;
    }

    public void setSaveBehaviour(Saver saveBehaviour) {
        this.saveBehaviour = saveBehaviour;
    }

    public Saver getSaveBehaviour() {
        return saveBehaviour;
    }

    /**
     * See {@link #savingEntireSource}.
     *
     * @return the value of savingEntireSource
     */
    public boolean isSavingEntireSource() {
        return getProjectSettings().isSavingEntireSource();
    }

    /**
     * See {@link #savingEntireSource}.
     *
     * @param argSavingEntireSource Value to assign to this.savingEntireSource
     */
    public void setSavingEntireSource(boolean argSavingEntireSource) {
        getProjectSettings().setSavingEntireSource(argSavingEntireSource);
    }

    public KettleJob getKettleJob() {
        return kettleJob;
    }

    public void setKettleJob(KettleJob kettleJob) {
        this.kettleJob = kettleJob;
    }
    // END STUFF BROUGHT IN FROM SwingUIProject

    public void addSessionLifecycleListener(SessionLifecycleListener<ArchitectSession> l) {
        lifecycleListeners.add(l);
    }

    public void removeSessionLifecycleListener(SessionLifecycleListener<ArchitectSession> l) {
        lifecycleListeners.remove(l);
    }

    public void fireSessionClosing() {
        SessionLifecycleEvent<ArchitectSession> evt = new SessionLifecycleEvent<ArchitectSession>(this);
        for (SessionLifecycleListener<ArchitectSession> listener : lifecycleListeners) {
            listener.sessionClosing(evt);
        }
    }

    public void registerSwingWorker(SPSwingWorker worker) {
        swingWorkers.add(worker);
    }

    public void removeSwingWorker(SPSwingWorker worker) {
        swingWorkers.remove(worker);
    }

    public boolean isNew() {
        return isNew;
    }

    /**
     * A package-private getter for the projectModificationWatcher.
     * This is currently used to run the event handler methods in
     * the unit tests.
     */
    ProjectModificationWatcher getProjectModificationWatcher() {
        return projectModificationWatcher;
    }

    public void setRelationshipLinesDirect(boolean relationshipLinesDirect) {
        getProjectSettings().setRelationshipLinesDirect(relationshipLinesDirect);
    }

    public boolean getRelationshipLinesDirect() {
        return getProjectSettings().isRelationshipLinesDirect();
    }

    public boolean isUsingLogicalNames() {
        return getProjectSettings().isUsingLogicalNames();
    }

    public void setUsingLogicalNames(boolean usingLogicalNames) {
        getProjectSettings().setUsingLogicalNames(usingLogicalNames);
    }

    public SQLObjectRoot getRootObject() {
        return delegateSession.getRootObject();
    }

    public DDLGenerator getDDLGenerator() {
        return delegateSession.getDDLGenerator();
    }

    public void setDDLGenerator(DDLGenerator generator) {
        delegateSession.setDDLGenerator(generator);
    }

    public OLAPRootObject getOLAPRootObject() {
        return getWorkspace().getOlapRootObject();
    }

    /**
     * Creates a new user prompter that uses a modal dialog to pose the given question.
     * 
     * @see DialogUserPrompter
     */
    public UserPrompter createUserPrompter(String question, UserPromptType responseType,
            UserPromptOptions optionType, UserPromptResponse defaultResponseType, Object defaultResponse,
            String... buttonNames) {
        return swinguiUserPrompterFactory.createUserPrompter(question, responseType, optionType,
                defaultResponseType, defaultResponse, buttonNames);

    }

    public ProjectSettings getProjectSettings() {
        return getWorkspace().getProjectSettings();
    }

    public boolean isShowPkTag() {
        return getProjectSettings().isShowPkTag();
    }

    public void setShowPkTag(boolean showPkTag) {
        getProjectSettings().setShowPkTag(showPkTag);
    }

    public boolean isShowFkTag() {
        return getProjectSettings().isShowFkTag();
    }

    public void setShowFkTag(boolean showFkTag) {
        getProjectSettings().setShowFkTag(showFkTag);
    }

    public boolean isShowAkTag() {
        return getProjectSettings().isShowAkTag();
    }

    public void setShowAkTag(boolean showAkTag) {
        getProjectSettings().setShowAkTag(showAkTag);
    }

    /**
     * Sets the visibility of columns in the playpen of this session.
     * 
     * @param option The new column visibility setting. If null, all columns will
     * be shown (equivalent to specifying {@link ColumnVisibility#ALL}).
     */
    public void setColumnVisibility(ColumnVisibility option) {
        getProjectSettings().setColumnVisibility(option);
    }

    public ColumnVisibility getColumnVisibility() {
        return getProjectSettings().getColumnVisibility();
    }

    public void showOLAPSchemaManager(Window owner) {
        olapSchemaManager.showDialog(owner);
    }

    public List<OLAPEditSession> getOLAPEditSessions() {
        return olapEditSessions;
    }

    public OLAPEditSession getOLAPEditSession(OLAPSession olapSession) {
        if (olapSession == null) {
            throw new NullPointerException(Messages.getString("ArchitectSwingSessionImpl.nullOlapSession")); //$NON-NLS-1$
        }
        for (OLAPEditSession editSession : getOLAPEditSessions()) {
            if (editSession.getOlapSession() == olapSession) {
                return editSession;
            }
        }
        return new OLAPEditSession(this, olapSession);
    }

    // docs inherit from interface
    public JMenu createDataSourcesMenu() {
        JMenu dbcsMenu = new JMenu(Messages.getString("DBTree.addSourceConnectionMenuName")); //$NON-NLS-1$
        dbcsMenu.add(new JMenuItem(new NewDataSourceAction(this)));
        dbcsMenu.addSeparator();

        // populate
        for (SPDataSource dbcs : getDataSources().getConnections()) {
            dbcsMenu.add(new JMenuItem(new AddDataSourceAction(dbTree, dbcs)));
        }
        SPSUtils.breakLongMenu(getArchitectFrame(), dbcsMenu);

        return dbcsMenu;
    }

    public PrintSettings getPrintSettings() {
        return printSettings;
    }

    public SQLDatabase getDatabase(JDBCDataSource ds) {
        return delegateSession.getDatabase(ds);
    }

    public boolean isDisplayRelationshipLabel() {
        return getProjectSettings().isDisplayRelationshipLabel();
    }

    public void setDisplayRelationshipLabel(boolean displayRelationshipLabel) {
        getProjectSettings().setDisplayRelationshipLabel(displayRelationshipLabel);
    }

    /**
     * This method will let users select a custom colour from a colour chooser
     * and then return the colour.
     * 
     * @param initial
     *            The initial colour to have selected in the colour chooser.
     * @return The colour selected or created by the user.
     */
    public static Color getCustomColour(Color initial, JComponent parent) {
        if (initial == null) {
            initial = Color.BLACK;
        }
        colourChooser.setColor(initial);
        ColorTracker ok = new ColorTracker(colourChooser);
        JDialog dialog = JColorChooser.createDialog(parent, "Choose a custom colour", true, colourChooser, ok,
                null);

        dialog.setVisible(true);

        return ok.getColor();
    }

    /**
     * Action Listener used by the custom colour dialog created in the
     * getCustomColour method.
     */
    private static class ColorTracker implements ActionListener, Serializable {
        JColorChooser chooser;
        Color color;

        public ColorTracker(JColorChooser c) {
            chooser = c;
        }

        public void actionPerformed(ActionEvent e) {
            color = chooser.getColor();
        }

        public Color getColor() {
            return color;
        }
    }

    public UserPrompter createDatabaseUserPrompter(String question, List<Class<? extends SPDataSource>> dsTypes,
            UserPromptOptions optionType, UserPromptResponse defaultResponseType, Object defaultResponse,
            DataSourceCollection<SPDataSource> dsCollection, String... buttonNames) {
        return swinguiUserPrompterFactory.createDatabaseUserPrompter(question, dsTypes, optionType,
                defaultResponseType, defaultResponse, dsCollection, buttonNames);
    }

    public ArchitectSwingProject getWorkspace() {
        return (ArchitectSwingProject) delegateSession.getWorkspace();
    }

    public boolean isForegroundThread() {
        //Until the GUI is initialized we may be running headless in which case
        //we will not be using the EDT.
        if (frame != null) {
            return SwingUtilities.isEventDispatchThread();
        } else {
            return true;
        }
    }

    public void runInBackground(Runnable runner) {
        runInBackground(runner, "worker");
    }

    public void runInBackground(final Runnable runner, String name) {
        new Thread(runner, name).start();
    }

    public void runInForeground(Runnable runner) {
        //Until the GUI is initialized we may be running headless in which case
        //we will not be using the EDT.
        if (frame == null || SwingUtilities.isEventDispatchThread()) {
            runner.run();
        } else {
            SwingUtilities.invokeLater(runner);
        }
    }

    public boolean isEnterpriseSession() {
        return delegateSession.isEnterpriseSession();
    }

    public ArchitectClientSideSession getEnterpriseSession() {
        if (isEnterpriseSession()) {
            return (ArchitectClientSideSession) delegateSession;
        } else {
            throw new RuntimeException("This swing session is not an enterprise session");
        }
    }

    public DataSourceCollection<JDBCDataSource> getDataSources() {
        return delegateSession.getDataSources();
    }

    public void showConnectionManager(Window owner) {
        dbConnectionManager.showDialog(owner);
    }

    public void showPreferenceDialog(Window owner) {
        prefsEditor.showPreferencesDialog(owner, ArchitectSwingSessionImpl.this);
    }

    /**
     * Protected method for setting the user prompter factory delegate to
     * something other than the UI user prompter factory. This allows
     * the tests to define a user prompter factory that does not block 
     * waiting for user response.
     */
    void setUserPrompterFactory(UserPrompterFactory newUPF) {
        swinguiUserPrompterFactory = newUPF;
    }

    public void refresh() {
        if (isEnterpriseSession()) {
            try {
                ArchitectSwingSession newSession = ((ArchitectSwingSessionContextImpl) getContext())
                        .createServerSession(((ArchitectClientSideSession) delegateSession).getProjectLocation(),
                                false);

                frame.addSession(newSession);

                JLabel messageLabel = new JLabel("Refreshing");
                JProgressBar progressBar = new JProgressBar();
                progressBar.setIndeterminate(true);

                final JDialog dialog = new JDialog(frame, "Refreshing");
                DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("pref:grow, 5dlu, pref"));
                builder.setDefaultDialogBorder();
                builder.append(messageLabel, 3);
                builder.nextLine();
                builder.append(progressBar, 3);
                dialog.add(builder.getPanel());

                dialog.pack();
                dialog.setLocation(frame.getX() + (frame.getWidth() - dialog.getWidth()) / 2,
                        frame.getY() + (frame.getHeight() - dialog.getHeight()) / 2);
                dialog.setAlwaysOnTop(true);
                dialog.setVisible(true);

                close();

                ((ArchitectClientSideSession) ((ArchitectSwingSessionImpl) newSession).getDelegateSession())
                        .getUpdater().addListener(new AbstractNetworkConflictResolver.UpdateListener() {
                            public boolean updatePerformed(AbstractNetworkConflictResolver updater) {
                                dialog.dispose();
                                return true; // true indicates that the listener should be removed
                            }

                            public boolean updateException(AbstractNetworkConflictResolver resolver, Throwable t) {
                                return false;
                            }

                            public void preUpdatePerformed(AbstractNetworkConflictResolver resolver) {
                                //do nothing
                            }

                            public void workspaceDeleted() {
                                // do nothing
                            }
                        });

                ((ArchitectClientSideSession) ((ArchitectSwingSessionImpl) newSession).getDelegateSession())
                        .startUpdaterThread();

            } catch (Exception e) {
                throw new RuntimeException("Failed to refresh", e);
            }
        }
    }

    public ArchitectSession getDelegateSession() {
        return delegateSession;
    }

    public List<UserDefinedSQLType> getSQLTypes() {
        return delegateSession.getSQLTypes();
    }

    public UserDefinedSQLType findSQLTypeByUUID(String uuid) {
        return delegateSession.findSQLTypeByUUID(uuid);
    }

    public UserDefinedSQLType findSQLTypeByJDBCType(int type) {
        return delegateSession.findSQLTypeByJDBCType(type);
    }

    public <T> UserPrompter createListUserPrompter(String question, List<T> responses, T defaultResponse) {
        return swinguiUserPrompterFactory.createListUserPrompter(question, responses, defaultResponse);
    }

    public List<UserDefinedSQLType> getDomains() {
        return delegateSession.getDomains();
    }

    public JScrollPane getPlayPenScrollPane() {
        return playPenScrollPane;
    }

    public void setPlayPenScrollPane(JScrollPane ppScrollPane) {
        playPenScrollPane = ppScrollPane;
    }

    public JComponent getProjectPanel() {
        return projectPanel;
    }

    public void setProjectPanel(JComponent panel) {
        projectPanel = panel;
    }

    @Override
    public ArchitectStatusBar getStatusInformation() {
        if (frame == null || frame.getStatusBar() == null)
            return null;
        return frame.getStatusBar();
    }

    @Override
    public Runnable createUpdateSnapshotRunnable(SPObjectSnapshot<?> snapshot) {
        return delegateSession.createUpdateSnapshotRunnable(snapshot);
    }

}