ca.sqlpower.matchmaker.swingui.MatchResultVisualizer.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.matchmaker.swingui.MatchResultVisualizer.java

Source

/*
 * Copyright (c) 2008, SQL Power Group Inc.
 *
 * This file is part of DQguru
 *
 * DQguru 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.
 *
 * DQguru 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.matchmaker.swingui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

import org.apache.log4j.Logger;

import ca.sqlpower.graph.BreadthFirstSearch;
import ca.sqlpower.graph.ConnectedComponentFinder;
import ca.sqlpower.graph.GraphModel;
import ca.sqlpower.matchmaker.AutoMatcher;
import ca.sqlpower.matchmaker.MatchPool;
import ca.sqlpower.matchmaker.PotentialMatchRecord;
import ca.sqlpower.matchmaker.PotentialMatchRecord.MatchType;
import ca.sqlpower.matchmaker.Project;
import ca.sqlpower.matchmaker.SourceTableRecord;
import ca.sqlpower.matchmaker.SourceTableRecordDisplayComparator;
import ca.sqlpower.matchmaker.graph.MatchPoolDotExport;
import ca.sqlpower.matchmaker.graph.MatchPoolGraphModel;
import ca.sqlpower.matchmaker.graph.NonDirectedUserValidatedMatchPoolGraphModel;
import ca.sqlpower.matchmaker.munge.MungeProcess;
import ca.sqlpower.matchmaker.swingui.engine.MungeProcessSelectionList;
import ca.sqlpower.matchmaker.swingui.graphViewer.DefaultGraphLayoutCache;
import ca.sqlpower.matchmaker.swingui.graphViewer.GraphNodeRenderer;
import ca.sqlpower.matchmaker.swingui.graphViewer.GraphSelectionListener;
import ca.sqlpower.matchmaker.swingui.graphViewer.GraphViewer;
import ca.sqlpower.matchmaker.undo.AbstractUndoableEditorPane;
import ca.sqlpower.object.AbstractSPListener;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.sqlobject.SQLColumn;
import ca.sqlpower.sqlobject.SQLDatabase;
import ca.sqlpower.sqlobject.SQLObjectException;
import ca.sqlpower.swingui.JDefaultButton;
import ca.sqlpower.swingui.ProgressWatcher;
import ca.sqlpower.swingui.SPSUtils;
import ca.sqlpower.swingui.SPSwingWorker;
import ca.sqlpower.swingui.SwingWorkerRegistry;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.factories.ButtonBarFactory;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

/**
 * The MatchResultVisualizer produces graphical representations of the matches
 * in a Match Result Table.
 */
public class MatchResultVisualizer extends AbstractUndoableEditorPane<MatchPool> {

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

    /**
     * Icons used for the match validation actions.
     */
    final private Icon masterIcon = new ImageIcon(getClass().getResource("/icons/master.png"));
    final private Icon duplicateIcon = new ImageIcon(getClass().getResource("/icons/duplicate.png"));
    final private Icon nomatchIcon = new ImageIcon(getClass().getResource("/icons/nomatch.png"));
    final private Icon unmatchIcon = new ImageIcon(getClass().getResource("/icons/unmatch.png"));

    private static final int RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING = 4;

    /**
     * Pops up a save dialog and saves the match pool to the chosen DOT file.
     */
    private final Action exportDotFileAction = new AbstractAction("Export as Dot file...") {
        public void actionPerformed(ActionEvent e) {
            try {
                File dotFile = new File(System.getProperty("user.home"),
                        "dqguru_graph_" + project.getName() + ".dot");
                JFileChooser fc = new JFileChooser(dotFile);
                int choice = fc.showSaveDialog(getPanel());
                if (choice == JFileChooser.APPROVE_OPTION) {
                    MatchPoolDotExport exporter = new MatchPoolDotExport(project);
                    exporter.setDotFile(fc.getSelectedFile());
                    exporter.exportDotFile();
                }
            } catch (Exception ex) {
                SPSUtils.showExceptionDialogNoReport(getPanel(), "Couldn't export dot file!", ex);
            }
        }
    };

    /**
    * This action calls the reset method on the pool. For more information
    * see {@link MatchPool#resetPool()}.
    */
    private final Action resetPoolAction = new AbstractAction("Reset All") {
        public void actionPerformed(ActionEvent e) {
            if (JOptionPane.showConfirmDialog(getPanel(),
                    "You are about to reset the entire match pool! Do you really wish to do this?",
                    "Reset Entire Match Pool", JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE) == JOptionPane.NO_OPTION)
                return;
            pool.begin("Reseting the match pool.");
            pool.resetPool();
            pool.commit();
            graph.repaint();
        }
    };

    /**
    * An action that calls
    * {@link MatchPoolGraphModel#resetCluster(SourceTableRecord)} on
    * {@link #selectedNode}, and then saves the changes into the database.
    */
    private final Action resetClusterAction = new AbstractAction("Reset Cluster") {
        public void actionPerformed(ActionEvent e) {
            graphModel.resetCluster(selectedNode);
            graph.repaint();
        }
    };

    /**
    * A button associated with {@link #resetClusterAction}. It's made into a
    * field so that we can enable/disable it according to whether or not a node
    * has been selected.
    */
    private JButton resetClusterButton;

    /**
    * A button associated with refreshAction so that we can reload the MatchScreen with the new
    * number of matches to display.
    */
    private JButton refreshButton;

    /**
     * Warning! Will throw a ClassCastException if the node renderer in graph
     * is not a SourceTableRecordRenderer.
     */
    private final Action chooseDisplayedValueAction = new AbstractAction("Choose Displayed Value...") {

        private JDialog dialog;
        private DisplayedNodeValueChooser chooser;
        private List<SQLColumn> revertList = new ArrayList<SQLColumn>();

        private final AbstractAction okAction = new AbstractAction("OK") {
            public void actionPerformed(ActionEvent e) {

                List<SQLColumn> chosenColumns = chooser.getChosenColumns();

                try {
                    // Refresh the display column preferences with the newly chosen columns
                    Preferences displayColumnPrefs = matchValidationPrefs.node("displayColumns");
                    displayColumnPrefs.clear();
                    for (int i = 0; i < chosenColumns.size(); i++) {
                        SQLColumn column = chosenColumns.get(i);
                        displayColumnPrefs.putInt(column.getName(), i);
                    }
                } catch (BackingStoreException bsEx) {
                    logger.error("Exception while storing columns in Preferences", bsEx);
                }

                try {
                    MatchResultVisualizer.this.displayColumns = chosenColumns;
                    MatchResultVisualizer.this.getPool().find(chosenColumns);
                    MatchResultVisualizer.this.getPanel().repaint();
                    MatchResultVisualizer.this.doAutoLayout();
                } catch (SQLObjectException ex) {
                    MMSUtils.showExceptionDialog((Component) e.getSource(), ex.getMessage(), ex);
                } catch (SQLException sqlEx) {
                    MMSUtils.showExceptionDialog((Component) e.getSource(), sqlEx.getMessage(), sqlEx);
                } finally {
                    dialog.dispose();
                }
            }
        };

        private final AbstractAction cancelAction = new AbstractAction("Cancel") {
            public void actionPerformed(ActionEvent e) {
                chooser.setChosenColumns(revertList);
                dialog.dispose();
            }
        };

        private final AbstractAction selectAction = new AbstractAction("Select All") {
            public void actionPerformed(ActionEvent e) {
                chooser.setAllDefaultChosen(true);
            }
        };

        private final AbstractAction deselectAction = new AbstractAction("Deselect All") {
            public void actionPerformed(ActionEvent e) {
                chooser.setAllDefaultChosen(false);
            }
        };

        public void actionPerformed(ActionEvent e) {
            try {
                dialog = SPSUtils.makeOwnedDialog(getPanel(), "Select Graph Display Values");
                if (chooser == null) {
                    chooser = new DisplayedNodeValueChooser((SourceTableNodeRenderer) graph.getNodeRenderer(),
                            project, displayColumns);
                }

                revertList = chooser.getChosenColumns();

                JDefaultButton okButton = new JDefaultButton(okAction);
                JPanel buttonPanelRight = ButtonBarFactory.buildOKCancelBar(okButton, new JButton(cancelAction));
                JPanel buttonPanelLeft = ButtonBarFactory.buildLeftAlignedBar(new JButton(selectAction),
                        new JButton(deselectAction));
                JPanel buttonPanel = new JPanel(new BorderLayout());
                buttonPanel.add(buttonPanelLeft, BorderLayout.WEST);
                buttonPanel.add(buttonPanelRight, BorderLayout.EAST);
                JPanel panel = new JPanel(new BorderLayout());
                panel.add(chooser.makeGUI(), BorderLayout.CENTER);
                panel.add(buttonPanel, BorderLayout.SOUTH);
                dialog.getContentPane().add(panel);
                dialog.pack();
                dialog.getRootPane().setDefaultButton(okButton);
                SPSUtils.makeJDialogCancellable(dialog, cancelAction, false);
                dialog.setLocationRelativeTo((Component) e.getSource());
                dialog.setVisible(true);
            } catch (SQLObjectException ex) {
                MMSUtils.showExceptionDialog((Component) e.getSource(), ex.getMessage(), ex);
            }
        }
    };

    /**
     * The action for showing and hiding rows on the validation chart.
     */
    private final Action chooseDisplayedColumnAction = new AbstractAction("Columns...") {

        private JDialog dialog;
        private DisplayedNodeValueChooser chooser;
        private List<SQLColumn> revertList = new ArrayList<SQLColumn>();

        private final AbstractAction okAction = new AbstractAction("OK") {
            public void actionPerformed(ActionEvent e) {
                if (chooser.getChosenColumns().isEmpty()) {
                    JOptionPane.showMessageDialog(dialog, "Please select at least one column!",
                            "No columns selected", JOptionPane.INFORMATION_MESSAGE);
                } else {
                    List<SQLColumn> chosenColumns = chooser.getChosenColumns();

                    try {
                        // Refresh the display column preferences with the newly chosen columns
                        Preferences shownColumnPrefs = matchValidationPrefs.node("shownColumns");
                        shownColumnPrefs.clear();
                        for (int i = 0; i < chosenColumns.size(); i++) {
                            SQLColumn column = chosenColumns.get(i);
                            shownColumnPrefs.putInt(column.getName(), i);
                        }
                    } catch (BackingStoreException ex) {
                        logger.error("Exception while storing columns in Preferences", ex);
                    }

                    shownColumns = chosenColumns;
                    updateMatchTable();
                    dialog.dispose();
                }
            }
        };

        private final AbstractAction cancelAction = new AbstractAction("Cancel") {
            public void actionPerformed(ActionEvent e) {
                chooser.setChosenColumns(revertList);
                dialog.dispose();
            }
        };

        private final AbstractAction selectAction = new AbstractAction("Select All") {
            public void actionPerformed(ActionEvent e) {
                chooser.setAllDefaultChosen(true);
            }
        };

        private final AbstractAction deselectAction = new AbstractAction("Deselect All") {
            public void actionPerformed(ActionEvent e) {
                chooser.setAllDefaultChosen(false);
            }
        };

        public void actionPerformed(ActionEvent e) {
            try {
                dialog = SPSUtils.makeOwnedDialog(getPanel(), "Select Chart Display Values");
                if (chooser == null) {
                    if (shownColumns == null) {
                        shownColumns = new ArrayList<SQLColumn>();
                        chooser = new DisplayedNodeValueChooser((SourceTableNodeRenderer) graph.getNodeRenderer(),
                                project, shownColumns);
                        // Defaults the shown value to true
                        chooser.setAllDefaultChosen(true);
                        shownColumns = chooser.getChosenColumns();
                    } else {
                        chooser = new DisplayedNodeValueChooser((SourceTableNodeRenderer) graph.getNodeRenderer(),
                                project, shownColumns);
                    }
                }

                revertList = chooser.getChosenColumns();

                JDefaultButton okButton = new JDefaultButton(okAction);
                JPanel buttonPanelRight = ButtonBarFactory.buildOKCancelBar(okButton, new JButton(cancelAction));
                JPanel buttonPanelLeft = ButtonBarFactory.buildLeftAlignedBar(new JButton(selectAction),
                        new JButton(deselectAction));
                JPanel buttonPanel = new JPanel(new BorderLayout());
                buttonPanel.add(buttonPanelLeft, BorderLayout.WEST);
                buttonPanel.add(buttonPanelRight, BorderLayout.EAST);
                JPanel panel = new JPanel(new BorderLayout());
                panel.add(chooser.makeGUI(), BorderLayout.CENTER);
                panel.add(buttonPanel, BorderLayout.SOUTH);
                dialog.getContentPane().add(panel);
                dialog.pack();
                dialog.getRootPane().setDefaultButton(okButton);
                SPSUtils.makeJDialogCancellable(dialog, cancelAction, false);
                dialog.setLocationRelativeTo((Component) e.getSource());
                dialog.setVisible(true);
            } catch (SQLObjectException ex) {
                MMSUtils.showExceptionDialog((Component) e.getSource(), ex.getMessage(), ex);
            }
        }
    };

    /**
    * When this action is fired the nodes will become unrelated.
    */
    class SetNoMatchAction extends AbstractAction {
        private final SourceTableRecord record1;
        private final SourceTableRecord record2;

        protected SetNoMatchAction(String name, SourceTableRecord record1, SourceTableRecord record2) {
            super("", nomatchIcon);
            super.putValue(AbstractAction.SHORT_DESCRIPTION, name);
            this.record1 = record1;
            this.record2 = record2;
        }

        public void actionPerformed(ActionEvent e) {
            try {
                pool.begin("setting a no-match");
                pool.defineNoMatch(record1, record2);
                pool.commit();
            } catch (SQLObjectException ex) {
                pool.rollback("rolling back no-match setting");
                MMSUtils.showExceptionDialog(getPanel(), "An exception occurred while trying to " + "define "
                        + record1 + " and " + record2 + " to not be duplicates.", ex);
            } catch (Exception ex) {
                pool.rollback("rolling back no-match setting");
                throw new RuntimeException(ex);
            }
            selectionListener.nodeSelected(record1);
            graph.repaint();
        }
    }

    /**
    * When this action is fired the nodes will have the edge directly relating
    * them set to undecided if the edge exists.
    */
    class SetUnmatchAction extends AbstractAction {
        private final SourceTableRecord record1;
        private final SourceTableRecord record2;

        protected SetUnmatchAction(String name, SourceTableRecord record1, SourceTableRecord record2) {
            super("", unmatchIcon);
            super.putValue(AbstractAction.SHORT_DESCRIPTION, name);
            this.record1 = record1;
            this.record2 = record2;
        }

        public void actionPerformed(ActionEvent e) {
            try {
                pool.defineUnmatched(record1, record2);
            } catch (SQLObjectException ex) {
                MMSUtils.showExceptionDialog(getPanel(),
                        "An exception occurred when trying to " + "unmatch " + record1 + " and " + record2, ex);
            }
            selectionListener.nodeSelected(record1);
            graph.repaint();
        }
    }

    /**
    * When this action is fired the master given to the constructor will become
    * the master of the given duplicate.
    */
    class SetMasterAction extends AbstractAction {

        private final SourceTableRecord master;
        private final SourceTableRecord duplicate;

        SetMasterAction(String name, SourceTableRecord master, SourceTableRecord duplicate) {
            super("", masterIcon);
            super.putValue(AbstractAction.SHORT_DESCRIPTION, name);
            this.master = master;
            this.duplicate = duplicate;
        }

        public void actionPerformed(ActionEvent e) {
            try {
                pool.defineMaster(master, duplicate);
            } catch (SQLObjectException ex) {
                pool.rollback("rolling back master setting");
                MMSUtils.showExceptionDialog(getPanel(), "An exception occurred when trying " + "to set " + master
                        + " to be the master of " + duplicate, ex);
            } catch (Exception ex) {
                pool.rollback("rolling back master setting");
                throw new RuntimeException(ex);
            }
            selectionListener.nodeSelected(duplicate);
        }
    }

    /**
    * When this action is fired the master given to the constructor will become
    * the master of the given duplicate.
    */
    class SetDuplicateAction extends AbstractAction {

        private final SourceTableRecord master;
        private final SourceTableRecord duplicate;

        SetDuplicateAction(String name, SourceTableRecord master, SourceTableRecord duplicate) {
            super("", duplicateIcon);
            super.putValue(AbstractAction.SHORT_DESCRIPTION, name);
            this.master = master;
            this.duplicate = duplicate;
        }

        public void actionPerformed(ActionEvent e) {
            try {
                pool.defineMaster(master, duplicate);
            } catch (SQLObjectException ex) {
                MMSUtils.showExceptionDialog(getPanel(), "An exception occurred when trying " + "to set "
                        + duplicate + " to be a duplicate of " + master, ex);
            }
            selectionListener.nodeSelected(master);
        }
    }

    private class MyGraphSelectionListener
            implements GraphSelectionListener<SourceTableRecord, PotentialMatchRecord> {

        public void nodeDeselected(SourceTableRecord node) {
            selectedNode = null;
            recordViewerPanel.removeAll();
            recordViewerPanel.add(SourceTableRecordViewer.getNoNodeSelectedLabel());
            recordViewerPanel.revalidate();
            recordViewerColumnHeader.removeAll();
            recordViewerColumnHeader.revalidate();
            recordViewerRowHeader.removeAll();
            recordViewerRowHeader.revalidate();
            recordViewerCornerPanel.removeAll();
            recordViewerCornerPanel.revalidate();
            resetClusterButton.setEnabled(false);
        }

        public void nodeSelected(final SourceTableRecord node) {
            selectedNode = node;
            updateMatchTable();
            resetClusterButton.setEnabled(true);
        }
    }

    /**
     * An action to get at the *shudder* Auto-Match feature.
     */
    private class AutoMatchAction extends AbstractAction {
        /**
         * A message that expresses our concerns about the auto-layout feature
         * to the user.
         */
        private final String warningMessage = "WARNING: Performing an the auto-match operation will create\n"
                + "matches between all records that were matched according to the\n"
                + "selected rule set. It is imperative that you review these\n"
                + "matches carefully before merging records because this operation\n"
                + "does NOT rank records based on their perceived usefulness.";

        private MatchMakerSwingSession session;

        public AutoMatchAction(GraphModel<SourceTableRecord, PotentialMatchRecord> model,
                MatchMakerSwingSession session) {
            super("Auto-Match");
            this.session = session;
        }

        public void actionPerformed(ActionEvent e) {
            int response = JOptionPane.showConfirmDialog(getPanel(), warningMessage, "WARNING",
                    JOptionPane.OK_CANCEL_OPTION);
            if (response == JOptionPane.OK_OPTION) {
                try {
                    MungeProcess selectedProcess = (MungeProcess) mungeProcessComboBox.getSelectedItem();
                    AutoMatchWorker worker = new AutoMatchWorker(pool, selectedProcess, session);
                    ProgressMonitor monitor = new ProgressMonitor(session.getFrame(), "Auto-matching...", null, 0,
                            100);
                    ProgressWatcher watcher = new ProgressWatcher(monitor, worker);
                    watcher.start();
                    new Thread(worker).start();
                } catch (Exception ex) {
                    MMSUtils.showExceptionDialog(getPanel(),
                            "Auto-Match failed, most likely a database connection error", ex);
                }
            }
        }
    }

    private class AutoMatchWorker extends SPSwingWorker {

        private AutoMatcher autoMatcher;
        private MungeProcess mungeProcess;
        private MatchPool pool;

        public AutoMatchWorker(MatchPool pool, MungeProcess mungeProcess, SwingWorkerRegistry registry) {
            super(registry);
            autoMatcher = new AutoMatcher(pool);
            this.mungeProcess = mungeProcess;
            this.pool = pool;
        }

        @Override
        public void cleanup() throws Exception {
            if (getDoStuffException() != null) {
                pool.clearRecords();
                pool.find(displayColumns);
                if (!(getDoStuffException() instanceof CancellationException)) {
                    MMSUtils.showExceptionDialog(getPanel(), "Error during auto-match", getDoStuffException());
                    logger.error("Error during auto-match", getDoStuffException());
                }
            }
            graph.repaint();
        }

        @Override
        public void doStuff() throws Exception {
            pool.begin("Doing automatch");
            autoMatcher.doAutoMatch(mungeProcess);
            pool.setDebug(false);
            pool.setUseBatchUpdates(false);
            pool.commit();
        }

        @Override
        protected Integer getJobSizeImpl() {
            return autoMatcher.getJobSize() + pool.getJobSize();
        }

        @Override
        protected String getMessageImpl() {
            return autoMatcher.getMessage();
        }

        @Override
        protected int getProgressImpl() {
            int amProgress = autoMatcher.getProgress();
            if (!autoMatcher.isFinished()) {
                return amProgress;
            } else {
                return amProgress + pool.getProgress();
            }
        }

        @Override
        protected boolean hasStartedImpl() {
            return autoMatcher.hasStarted();
        }

        @Override
        protected boolean isFinishedImpl() {
            return (autoMatcher.isFinished() && pool.isFinished()) || isCancelled();
        }

        @Override
        public synchronized void setCancelled(boolean cancelled) {
            super.setCancelled(cancelled);
            autoMatcher.setCancelled(cancelled);
            pool.setCancelled(cancelled);
        }
    }

    private final Action refreshAction = new AbstractAction("Refresh Matches") {

        public void actionPerformed(ActionEvent e) {
            try {
                pool.setLimit(Integer.parseInt(limitField.getText()));
                pool.clearRecords();
                pool.find(displayColumns);
                setViewLabelText();
                refreshGraph();
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            } catch (SQLObjectException ex) {
                throw new RuntimeException(ex);
            }
        }
    };

    /**
     * This is the match whose result table we're visualizing.
     */
    private final Project project;

    /**
     * The visual component that actually displays the match results graph.
     */
    private GraphViewer<SourceTableRecord, PotentialMatchRecord> graph;

    /**
     * The component that is used to layout the RecordViewer objects.
     */
    private final JPanel recordViewerPanel = new JPanel(
            new RecordViewerLayout(RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING));

    /**
     * The header component that is fixed above the record viewer panel (it
     * has the match/no match buttons and stuff).
     * <p>
     * The layout strategy here is iffy. It would be better if this header and the
     * recordViewerPanel itself were managed by the same LayoutManager instance,
     * which would ingore all layout requests for this header, and set the sizes
     * for both the record viewer body panel and related header component at the
     * same time (when laying out the body).  The main problem with the current approach
     * is that it's not possible to ensure there will be enough room for the buttons
     * in the header when laying out the body, but we can't have the two layout
     * managers wrestle each other.
     */
    private final JPanel recordViewerColumnHeader = new JPanel(new RecordViewerLayout(0));

    /**
     * The row header component that is placed on the left side of the record viewer panel.
     * If no node is selected, it would contain only one panel with the names of the columns
     * of the source table record to be displayed. If a node is selected, it will also contain
     * in a second column the values of the source table record from the selected node. This is
     * to prevent the selected record from scrolling out of view when scrolling through the records.
     */
    private final JPanel recordViewerRowHeader = new JPanel(
            new RecordViewerLayout(RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING));

    /**
     * The component that is placed in the upper left corner of the record viewer.
     * Typically, if no node is selected, then it will be an empty panel. Otherwise, it
     * would contain a toolbar for the source table record column of the selected node. 
     */
    private final JPanel recordViewerCornerPanel = new JPanel(new RecordViewerLayout(0));

    private final MyGraphSelectionListener selectionListener = new MyGraphSelectionListener();

    private final MatchPool pool;

    private MatchPoolGraphModel graphModel;

    private JComboBox mungeProcessComboBox;

    private MungeProcessSelectionList selectionButton;

    private JButton autoMatchButton;

    private SPListener updaterListener;

    private JTextField limitField;

    private final JButton nextSet;

    private final JButton previousSet;

    /**
     * Displays to the user what number of matches we are currently viewing.
     */
    private final JLabel viewLabel;

    /**
     * The node on the graph that is currently selected.
     */
    private SourceTableRecord selectedNode;

    /**
     * A list of the SQLColumns that we want to display for each
     * SourceTableRecord in the match validation table.
     */
    private List<SQLColumn> shownColumns;

    /**
     * A list of the SQLColumns that we want to display in each SourceTableRecord
     * node in the match validation graph.
     */
    private List<SQLColumn> displayColumns = new ArrayList<SQLColumn>();

    /**
    * A {@link Preferences} node containing user and project-specific
    * preferences for the match validation screen.
    * The pathname used to retrieve it from the user root preference node is as
    * follows:
    * 
    * /ca/sqlpower/matchmaker/projectSettings/$<repository connection name>/$<project oid>/matchValidation";
    */
    private final Preferences matchValidationPrefs;

    private final MatchMakerSwingSession session;

    public MatchResultVisualizer(Project project, MatchMakerSwingSession session)
            throws SQLException, SQLObjectException {
        super(session, project.getMatchPool());
        this.project = project;
        this.session = session;
        this.pool = project.getMatchPool();

        if (!project.doesSourceTableExist() || !project.verifySourceTableStructure()) {
            String errorText = "The DQguru has detected changes in the source table structure.\n"
                    + "Your project will require modifications before the engines can run properly.\n"
                    + "The DQguru can automatically modify the project but the changes may not be reversible.\n"
                    + "Would you like the DQguru to modify the project now?";
            int response = JOptionPane.showOptionDialog(session.getFrame(), errorText, "Source Table Changed",
                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null,
                    new String[] { "Fix Now", "Not Now" }, "Fix Now");
            if (response == JOptionPane.YES_OPTION) {
                try {
                    SQLDatabase db = project.getSourceTable().getParentDatabase();
                    db.refresh();
                } catch (Exception ex1) {
                    MMSUtils.showExceptionDialog(getPanel(), "Failed to fix project!", ex1);
                }
            }
        }

        pool.clearRecords();
        pool.find(displayColumns);

        FormLayout topLayout = new FormLayout("pref", "pref, pref");
        JPanel topPanel = new JPanel(topLayout);
        PanelBuilder pb = new PanelBuilder(topLayout, topPanel);
        CellConstraints cc = new CellConstraints();
        JPanel buttonPanel = new JPanel(new GridLayout(8, 1));
        buttonPanel.add(new JButton(chooseDisplayedValueAction));

        selectionButton = new MungeProcessSelectionList(project) {

            @Override
            public boolean getValue(MungeProcess mp) {
                return mp.isValidate();
            }

            @Override
            public void setValue(MungeProcess mp, boolean value) {
                mp.setValidate(value);
            }

        };
        selectionButton.setCloseAction(new Runnable() {
            public void run() {
                graph.setSelectedNode(null);
                graph.setFocusedNode(null);
                pool.clearRecords();
                pool.clearCache();
                try {
                    pool.find(displayColumns);
                } catch (SQLObjectException ex) {
                    MMSUtils.showExceptionDialog(getPanel(), ex.getMessage(), ex);
                } catch (SQLException sqlEx) {
                    MMSUtils.showExceptionDialog(getPanel(), sqlEx.getMessage(), sqlEx);
                }
                ((DefaultGraphLayoutCache) graph.getLayoutCache()).clearNodes();
                updateAutoMatchComboBox();
                doAutoLayout();
                graph.repaint();
            }
        });

        buttonPanel.add(selectionButton);
        buttonPanel.add(new JButton(resetPoolAction));
        resetClusterButton = new JButton(resetClusterAction);
        resetClusterButton.setEnabled(selectedNode != null);
        buttonPanel.add(resetClusterButton);
        JPanel limitPanel = new JPanel(new GridLayout(1, 2));
        limitPanel.add(new JLabel("# of clusters to view: "));
        limitField = new JTextField(20);

        limitPanel.add(limitField);
        buttonPanel.add(limitPanel);
        refreshButton = new JButton(refreshAction);
        buttonPanel.add(refreshButton);
        JPanel setPanel = new JPanel(new GridLayout(1, 2));
        previousSet = new JButton(new AbstractAction("Previous Set") {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                pool.setLimit(Integer.parseInt(limitField.getText()));
                int limit = pool.getLimit();
                int n = pool.getCurrentMatchNumber();
                if (n < limit) {
                    n = 0;
                } else {
                    n = n - limit;
                }
                try {
                    pool.setCurrentMatchNumber(n);
                    pool.clearRecords();
                    pool.find(displayColumns);
                    setViewLabelText();
                    refreshGraph();
                } catch (SQLException ex) {
                    throw new RuntimeException(ex);
                } catch (SQLObjectException ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
        if (pool.getLimit() == 0) {
            previousSet.setEnabled(false);
        }
        setPanel.add(previousSet);
        nextSet = new JButton(new AbstractAction("Next Set") {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                pool.setLimit(Integer.parseInt(limitField.getText()));
                int limit = pool.getLimit();
                int n = pool.getCurrentMatchNumber();
                n += limit;
                try {
                    pool.setCurrentMatchNumber(n);
                    pool.clearRecords();
                    pool.find(displayColumns);
                    setViewLabelText();
                    refreshGraph();
                } catch (SQLException ex) {
                    throw new RuntimeException(ex);
                } catch (SQLObjectException ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
        setPanel.add(nextSet);
        buttonPanel.add(setPanel);
        viewLabel = new JLabel();
        setViewLabelText();
        buttonPanel.add(viewLabel);
        buttonPanel.setPreferredSize(new Dimension(350, 200));

        String prefNodePath = "ca/sqlpower/matchmaker/projectSettings/$" + project.getUUID() + "/matchValidation";
        matchValidationPrefs = Preferences.userRoot().node(prefNodePath);

        try {
            final Preferences displayColumnPrefs = matchValidationPrefs.node("displayColumns");
            String[] keys = displayColumnPrefs.keys();
            for (String key : keys) {
                SQLColumn column = project.getSourceTable().getColumnByName(key);
                if (column != null) {
                    displayColumns.add(column);
                }
            }
            Collections.sort(displayColumns, new Comparator<SQLColumn>() {
                public int compare(SQLColumn o1, SQLColumn o2) {
                    int o1Index = displayColumnPrefs.getInt(o1.getName(), -1);
                    int o2Index = displayColumnPrefs.getInt(o2.getName(), -1);
                    if (o1Index < o2Index)
                        return -1;
                    if (o1Index > o2Index)
                        return 1;
                    return 0;
                }
            });
        } catch (BackingStoreException e) {
            logger.error("Error loading preferred display values for graph. Defaulting to primary key values", e);
            displayColumns.clear();
        }

        try {
            shownColumns = new ArrayList<SQLColumn>();
            final Preferences shownColumnPrefs = matchValidationPrefs.node("shownColumns");
            String[] keys = shownColumnPrefs.keys();

            if (keys.length == 0) {
                shownColumns = null;
            } else {
                for (String key : keys) {
                    SQLColumn column = project.getSourceTable().getColumnByName(key);
                    if (column != null) {
                        shownColumns.add(column);
                    }
                }
                Collections.sort(shownColumns, new Comparator<SQLColumn>() {
                    public int compare(SQLColumn o1, SQLColumn o2) {
                        int o1Index = shownColumnPrefs.getInt(o1.getName(), -1);
                        int o2Index = shownColumnPrefs.getInt(o2.getName(), -1);
                        if (o1Index < o2Index)
                            return -1;
                        if (o1Index > o2Index)
                            return 1;
                        return 0;
                    }
                });
            }
        } catch (BackingStoreException e) {
            logger.error("Error loading preferred display columns for table. Defaulting to all columns", e);
            shownColumns = null;
        }

        for (PotentialMatchRecord pmr : pool.getPotentialMatchRecords()) {
            updaterListener = new AbstractSPListener() {
                @Override
                public void propertyChanged(PropertyChangeEvent evt) {
                    if (evt.getPropertyName().equals("master") || evt.getPropertyName().equals("matchStatus")) {
                        graph.repaint();
                    }
                }
            };
            pmr.addSPListener(updaterListener);
        }
        graphModel = new MatchPoolGraphModel(pool);
        graph = new GraphViewer<SourceTableRecord, PotentialMatchRecord>(graphModel);
        graph.setNodeRenderer(new SourceTableNodeRenderer());
        graph.setEdgeRenderer(new PotentialMatchEdgeRenderer(graph));
        graph.addSelectionListener(selectionListener);

        JPanel autoMatchPanel = new JPanel(new FlowLayout());
        mungeProcessComboBox = new JComboBox();
        mungeProcessComboBox.setRenderer(new MatchMakerObjectComboBoxCellRenderer());
        autoMatchButton = new JButton(new AutoMatchAction(graph.getModel(), session));
        autoMatchPanel.add(autoMatchButton);
        updateAutoMatchComboBox();
        autoMatchPanel.add(new JLabel(":"));
        autoMatchPanel.add(mungeProcessComboBox);

        pb.add(buttonPanel, cc.xy(1, 1));
        limitField.setText(pool.getLimit() + "");
        pb.add(autoMatchPanel, cc.xy(1, 2));
        doAutoLayout();

        JPanel graphPanel = new JPanel(new BorderLayout());
        graphPanel.add(pb.getPanel(), BorderLayout.NORTH);
        graphPanel.add(new JScrollPane(graph), BorderLayout.CENTER);

        recordViewerPanel.add(SourceTableRecordViewer.getNoNodeSelectedLabel());
        final JScrollPane recordViewerScrollPane = new JScrollPane(recordViewerPanel,
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

        recordViewerScrollPane.getHorizontalScrollBar().setBlockIncrement(100);
        recordViewerScrollPane.getHorizontalScrollBar().setUnitIncrement(15);
        recordViewerScrollPane.getVerticalScrollBar().setBlockIncrement(100);
        recordViewerScrollPane.getVerticalScrollBar().setUnitIncrement(15);

        recordViewerScrollPane.setColumnHeaderView(recordViewerColumnHeader);
        recordViewerScrollPane.setRowHeaderView(recordViewerRowHeader);
        recordViewerScrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, recordViewerCornerPanel);

        // put it all together
        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, graphPanel, recordViewerScrollPane);

        JPanel panel = new JPanel(new BorderLayout(12, 12));
        panel.add(splitPane, BorderLayout.CENTER);
        super.setPanel(panel);
    }

    /**
     * Performs auto-layout on the visible graph of matches managed
     * by this component.
     */
    public void doAutoLayout() {
        final GraphModel<SourceTableRecord, PotentialMatchRecord> model = graph.getModel();
        final GraphNodeRenderer<SourceTableRecord> renderer = graph.getNodeRenderer();
        final int hgap = 10;
        final int vgap = 10;
        final int targetHeight = graph.getPreferredGraphLayoutHeight();

        ConnectedComponentFinder<SourceTableRecord, PotentialMatchRecord> ccf = new ConnectedComponentFinder<SourceTableRecord, PotentialMatchRecord>(
                new SourceTableRecordDisplayComparator());
        Set<Set<SourceTableRecord>> components = ccf.findConnectedComponents(model);

        int y = 0;
        int x = 0;
        int nextx = 0;

        for (Set<SourceTableRecord> component : components) {

            // lay out the nodes of this component in a circle
            double angleStep = Math.PI * 2.0 / component.size();
            double currentAngle = 0.0;
            double radius = 100.0;
            Rectangle componentBounds = null;
            Map<SourceTableRecord, Rectangle> componentNodeBounds = new HashMap<SourceTableRecord, Rectangle>();

            for (SourceTableRecord node : component) {
                double xx = radius * Math.cos(currentAngle);
                double yy = radius * Math.sin(currentAngle);

                Dimension prefSize = renderer.getGraphNodeRendererComponent(node, false, false).getPreferredSize();

                final Rectangle nodeBounds = new Rectangle((int) xx, (int) yy, prefSize.width, prefSize.height);
                if (componentBounds == null) {
                    componentBounds = new Rectangle(nodeBounds);
                } else {
                    componentBounds.add(nodeBounds);
                }
                componentNodeBounds.put(node, nodeBounds);

                currentAngle += angleStep;
            }

            logger.debug("Nodes of a graph and their bounds are " + componentNodeBounds);
            logger.debug("Overall bounds on the above graph are " + componentBounds);

            // fit the laid out component under previous component, or in a
            // new column if nodes won't fit in current column
            Point translate = new Point(-componentBounds.x, -componentBounds.y);
            if (y + componentBounds.height > targetHeight) {
                y = 0;
                x = nextx + hgap;
            }

            for (SourceTableRecord node : component) {
                final Rectangle bounds = componentNodeBounds.get(node);
                bounds.translate(x + translate.x, y + translate.y);
                graph.setNodeBounds(node, bounds);
                nextx = Math.max(nextx, bounds.x + bounds.width);
            }

            y += componentBounds.height + vgap;
        }
        graph.repaint();
        graph.revalidate();
    }

    /**
    * This method will return the actions allowed between the two given nodes
    * in a graph.
    * 
    * @param lhs
    *            The node that should be considered on the left for the methods
    *            that the actions will run.
    * @param rhs
    *            The node that should be considered on the right for the
    *            methods that the actions will run.
    */
    public List<Action> getActions(SourceTableRecord lhs, SourceTableRecord rhs) {
        List<Action> actionsAllowed = new ArrayList<Action>();
        if (lhs == rhs) {
            actionsAllowed.add(new SetMasterAction("Master of All", lhs, rhs));
            actionsAllowed.add(new SetNoMatchAction("No Match to Any", lhs, rhs));
            actionsAllowed.add(new SetUnmatchAction("Unmatch All", lhs, rhs));
            return actionsAllowed;
        }

        GraphModel<SourceTableRecord, PotentialMatchRecord> nonDirectedGraph = new NonDirectedUserValidatedMatchPoolGraphModel(
                pool, new HashSet<PotentialMatchRecord>());
        BreadthFirstSearch<SourceTableRecord, PotentialMatchRecord> bfs = new BreadthFirstSearch<SourceTableRecord, PotentialMatchRecord>();
        Set<SourceTableRecord> reachable = new HashSet<SourceTableRecord>(bfs.performSearch(nonDirectedGraph, lhs));

        logger.debug("lhs record is " + lhs.toString());
        logger.debug("rhs record is " + rhs.toString());

        PotentialMatchRecord pmr = pool.getPotentialMatchFromOriginals(lhs, rhs);
        if (reachable.contains(rhs)) {
            logger.debug("reachable contains rhs");
            actionsAllowed.add(new SetUnmatchAction("Unmatch", lhs, rhs));
            actionsAllowed.add(new SetNoMatchAction("No Match", lhs, rhs));
            return actionsAllowed;
        }

        actionsAllowed.add(new SetMasterAction("Master", rhs, lhs));
        actionsAllowed.add(new SetDuplicateAction("Duplicate", lhs, rhs));
        if (pmr != null && pmr.getMatchStatus() == MatchType.NOMATCH) {
            actionsAllowed.add(new SetUnmatchAction("Unmatch", lhs, rhs));
        } else {
            actionsAllowed.add(new SetNoMatchAction("No Match", lhs, rhs));
        }

        return actionsAllowed;
    }

    MatchPool getPool() {
        return pool;
    }

    /**
     * Updates the Auto Match combo box to remove munge processes that are not currently shown
     */
    private void updateAutoMatchComboBox() {
        mungeProcessComboBox.removeAllItems();
        for (MungeProcess mp : project.getValidatingMungeProcesses()) {
            mungeProcessComboBox.addItem(mp);
        }
        mungeProcessComboBox.setEnabled(mungeProcessComboBox.getItemCount() > 0);
        autoMatchButton.setEnabled(mungeProcessComboBox.getItemCount() > 0);
    }

    /**
     * Whenever we load a new set of matches in the visualizer we need to call this.
     */
    private void refreshGraph() {
        graph.setLayoutCache(new DefaultGraphLayoutCache<SourceTableRecord, PotentialMatchRecord>());
        doAutoLayout();
        for (PotentialMatchRecord pmr : pool.getPotentialMatchRecords()) {
            updaterListener = new AbstractSPListener() {
                @Override
                public void propertyChanged(PropertyChangeEvent evt) {
                    if (evt.getPropertyName().equals("master") || evt.getPropertyName().equals("matchStatus")) {
                        graph.repaint();
                    }
                }
            };
            pmr.addSPListener(updaterListener);
        }
        graph.repaint();
    }

    private void setViewLabelText() {
        int viewTo = (pool.getCurrentMatchNumber() + pool.getLimit());
        int cmn = pool.getCurrentMatchNumber();
        if (pool.getClusterCount() == 0) {
            nextSet.setEnabled(false);
            previousSet.setEnabled(false);
        } else if (viewTo >= pool.getClusterCount()) {
            viewTo = pool.getClusterCount();
            nextSet.setEnabled(false);
            previousSet.setEnabled(true);
        } else if (cmn == 0) {
            nextSet.setEnabled(true);
            previousSet.setEnabled(false);
        } else {
            previousSet.setEnabled(true);
            nextSet.setEnabled(true);
        }

        viewLabel.setText(
                "Viewing " + ((cmn == 0) ? 0 : (cmn + 1)) + " to " + viewTo + " out of " + pool.getClusterCount());
    }

    /**
     * Updates the match table based on shownColumns and the current selectedNode.
     */
    private void updateMatchTable() {
        try {
            recordViewerPanel.removeAll();
            recordViewerColumnHeader.removeAll();
            recordViewerRowHeader.removeAll();
            recordViewerCornerPanel.removeAll();

            JButton displayColumnButton = new JButton(chooseDisplayedColumnAction);
            recordViewerCornerPanel.add(displayColumnButton);

            // if there's no node selected, then there will not be any table
            if (selectedNode == null) {
                recordViewerPanel.add(SourceTableRecordViewer.getNoNodeSelectedLabel());
            }
            // if none of the columns are shown, then there will be no table
            else if (shownColumns != null && shownColumns.isEmpty()) {
                recordViewerPanel.add(SourceTableRecordViewer.getNoColumnSelectedLabel());
            } else {
                // if shownColumnes is null, then all the columns will be shown
                JPanel headerPanel = SourceTableRecordViewer.headerPanel(project, shownColumns);
                headerPanel.setPreferredSize(new Dimension(Math.max(displayColumnButton.getPreferredSize().width,
                        headerPanel.getPreferredSize().width), headerPanel.getPreferredSize().height));
                recordViewerRowHeader.add(headerPanel);
                BreadthFirstSearch<SourceTableRecord, PotentialMatchRecord> bfs = new BreadthFirstSearch<SourceTableRecord, PotentialMatchRecord>();
                bfs.setComparator(new SourceTableRecordsComparator(selectedNode));
                List<SourceTableRecord> reachableNodes = bfs.performSearch(graphModel, selectedNode);
                for (SourceTableRecord rec : reachableNodes) {
                    if (rec.equals(selectedNode)) {
                        // if shownColumnes is null, then all the columns will be shown

                        SourceTableRecordViewer recordViewer = new SourceTableRecordViewer(rec, selectedNode,
                                getActions(selectedNode, rec), shownColumns,
                                RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING);
                        JToolBar toolBar = recordViewer.getToolBar();
                        EmptyBorder emptyBorder = new EmptyBorder(0,
                                headerPanel.getPreferredSize().width + RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING
                                        - displayColumnButton.getPreferredSize().width,
                                0, 0);
                        toolBar.setBorder(emptyBorder);
                        recordViewerCornerPanel.add(toolBar);
                        recordViewerRowHeader.add(recordViewer.getPanel());
                    } else {
                        final SourceTableRecord str = rec;
                        // if shownColumnes is null, then all the columns will be shown
                        SourceTableRecordViewer recordViewer = new SourceTableRecordViewer(str, selectedNode,
                                getActions(selectedNode, str), shownColumns,
                                RECORD_VIEWER_ROW_HEADER_LAYOUT_PADDING);
                        recordViewer.getPanel().addMouseListener(new MouseAdapter() {
                            @Override
                            public void mousePressed(MouseEvent e) {
                                if (SwingUtilities.isLeftMouseButton(e)) {
                                    if (e.getClickCount() == 1) {
                                        graph.setFocusedNode(str);
                                        graph.scrollNodeToVisible(str);
                                    } else if (e.getClickCount() == 2) {
                                        graph.setSelectedNode(str);
                                        graph.scrollNodeToVisible(str);
                                    }
                                }
                            }
                        });
                        recordViewerPanel.add(recordViewer.getPanel());
                        recordViewerColumnHeader.add(recordViewer.getToolBar());
                    }
                }
            }
        } catch (Exception ex) {
            MMSUtils.showExceptionDialog(getPanel(), "Couldn't show potential matches", ex);
        }
        recordViewerPanel.revalidate();
        recordViewerColumnHeader.revalidate();
        recordViewerRowHeader.revalidate();
        recordViewerCornerPanel.revalidate();
    }

    /**
     * A comparator that compares SourceTableRecords based on match status and match priority.
     */
    private class SourceTableRecordsComparator implements Comparator<SourceTableRecord> {
        private SourceTableRecord master;
        private MatchTypeComparator matchTypeComp;

        /**
         *   The master is the basis for comparison, mostly just used
         *   to get the match priority. 
         */
        public SourceTableRecordsComparator(SourceTableRecord master) {
            this.master = master;
            matchTypeComp = new MatchTypeComparator();
        }

        public int compare(SourceTableRecord o1, SourceTableRecord o2) {
            // Assumes the basis as the smallest
            if (o1 == master) {
                return -1;
            } else if (o2 == master) {
                return 1;
            }

            // Finds the "highest" match status related to each SourceTableRecords
            List<PotentialMatchRecord> pmrs1 = new ArrayList<PotentialMatchRecord>(o1.getOriginalMatchEdges());
            MatchType t1 = MatchType.UNMATCH;
            for (PotentialMatchRecord pmr : pmrs1) {
                if (matchTypeComp.compare(pmr.getMatchStatus(), t1) > 0) {
                    t1 = pmr.getMatchStatus();
                }
            }
            List<PotentialMatchRecord> pmrs2 = new ArrayList<PotentialMatchRecord>(o2.getOriginalMatchEdges());
            MatchType t2 = MatchType.UNMATCH;
            for (PotentialMatchRecord pmr : pmrs2) {
                if (matchTypeComp.compare(pmr.getMatchStatus(), t2) > 0) {
                    t2 = pmr.getMatchStatus();
                }
            }

            // Compares the SourceTableRecords based on the higher match status
            if (matchTypeComp.compare(t1, t2) != 0) {
                return matchTypeComp.compare(t1, t2);
            }

            // Assumes any SourceTableRecords not directly adjacent to the master to be larger
            PotentialMatchRecord pmr1 = o1.getMatchRecordByOriginalAdjacentSourceTableRecord(master);
            PotentialMatchRecord pmr2 = o2.getMatchRecordByOriginalAdjacentSourceTableRecord(master);
            if (pmr1 == null) {
                return 1;
            } else if (pmr2 == null) {
                return -1;
            }

            // Compares based on the match priority of the corresponding munge process 
            int percent1 = 0;
            if (pmr1.getMungeProcess().getMatchPriority() != null) {
                percent1 = pmr1.getMungeProcess().getMatchPriority().shortValue();
            }
            int percent2 = 0;
            if (pmr2.getMungeProcess().getMatchPriority() != null) {
                percent2 = pmr2.getMungeProcess().getMatchPriority().shortValue();
            }
            return percent1 - percent2;
        }

        /**
         * A comparator that compares MatchTypes in the following order:
         * UNMATCH < AUTOMATCH < MATCH < NOMATCH < MERGED
         */
        private class MatchTypeComparator implements Comparator<MatchType> {

            private List<MatchType> types = new ArrayList<MatchType>();

            public MatchTypeComparator() {
                types.add(MatchType.UNMATCH);
                types.add(MatchType.AUTOMATCH);
                types.add(MatchType.MATCH);
                types.add(MatchType.NOMATCH);
                types.add(MatchType.MERGED);
            }

            public int compare(MatchType o1, MatchType o2) {
                return types.indexOf(o1) - types.indexOf(o2);
            }
        }
    }

    @Override
    public void undoEventFired(PropertyChangeEvent evt) {
        // TODO Auto-generated method stub
        logger.debug("Stub call: AbstractUndoableEditorPane<MatchPool>.undoEventFired()");

    }
}