edu.ku.brc.specify.tasks.subpane.wb.wbuploader.Uploader.java Source code

Java tutorial

Introduction

Here is the source code for edu.ku.brc.specify.tasks.subpane.wb.wbuploader.Uploader.java

Source

/* Copyright (C) 2015, University of Kansas Center for Research
 * 
 * Specify Software Project, specify@ku.edu, Biodiversity Institute,
 * 1345 Jayhawk Boulevard, Lawrence, Kansas, 66045, USA
 * 
 * This program 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 2
 * of the License, or (at your option) any later version.
 * 
 * This program 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package edu.ku.brc.specify.tasks.subpane.wb.wbuploader;

import static edu.ku.brc.ui.UIHelper.createLabel;
import static edu.ku.brc.ui.UIRegistry.getResourceString;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.SoftBevelBorder;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;

import edu.ku.brc.af.core.AppContextMgr;
import edu.ku.brc.af.core.ServiceInfo;
import edu.ku.brc.af.core.Taskable;
import edu.ku.brc.af.core.db.DBFieldInfo;
import edu.ku.brc.af.core.db.DBTableIdMgr;
import edu.ku.brc.af.core.db.DBTableInfo;
import edu.ku.brc.af.prefs.AppPreferences;
import edu.ku.brc.af.ui.forms.BusinessRulesIFace;
import edu.ku.brc.dbsupport.DataProviderFactory;
import edu.ku.brc.dbsupport.DataProviderSessionIFace;
import edu.ku.brc.dbsupport.RecordSetItemIFace;
import edu.ku.brc.helpers.ImageMetaDataHelper;
import edu.ku.brc.specify.SpecifyUserTypes;
import edu.ku.brc.specify.conversion.BasicSQLUtils;
import edu.ku.brc.specify.datamodel.Accession;
import edu.ku.brc.specify.datamodel.Address;
import edu.ku.brc.specify.datamodel.Agent;
import edu.ku.brc.specify.datamodel.Attachment;
import edu.ku.brc.specify.datamodel.AttachmentOwnerIFace;
import edu.ku.brc.specify.datamodel.CollectingEvent;
import edu.ku.brc.specify.datamodel.CollectingTrip;
import edu.ku.brc.specify.datamodel.Collection;
import edu.ku.brc.specify.datamodel.CollectionObject;
import edu.ku.brc.specify.datamodel.DataModelObjBase;
import edu.ku.brc.specify.datamodel.Determination;
import edu.ku.brc.specify.datamodel.FieldNotebook;
import edu.ku.brc.specify.datamodel.FieldNotebookPage;
import edu.ku.brc.specify.datamodel.FieldNotebookPageSet;
import edu.ku.brc.specify.datamodel.GeoCoordDetail;
import edu.ku.brc.specify.datamodel.Locality;
import edu.ku.brc.specify.datamodel.LocalityDetail;
import edu.ku.brc.specify.datamodel.ObjectAttachmentIFace;
import edu.ku.brc.specify.datamodel.RecordSet;
import edu.ku.brc.specify.datamodel.SpecifyUser;
import edu.ku.brc.specify.datamodel.Taxon;
import edu.ku.brc.specify.datamodel.Treeable;
import edu.ku.brc.specify.datamodel.Workbench;
import edu.ku.brc.specify.datamodel.WorkbenchDataItem;
import edu.ku.brc.specify.datamodel.WorkbenchRow;
import edu.ku.brc.specify.datamodel.WorkbenchRowExportedRelationship;
import edu.ku.brc.specify.datamodel.WorkbenchRowImage;
import edu.ku.brc.specify.datamodel.WorkbenchTemplateMappingItem;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgr;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgr.SCOPE;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgr.USER_ACTION;
import edu.ku.brc.specify.tasks.DataEntryTask;
import edu.ku.brc.specify.tasks.InteractionsTask;
import edu.ku.brc.specify.tasks.RecordSetTask;
import edu.ku.brc.specify.tasks.ReportsBaseTask;
import edu.ku.brc.specify.tasks.WorkbenchTask;
import edu.ku.brc.specify.tasks.subpane.wb.WorkbenchPaneSS;
import edu.ku.brc.specify.tasks.subpane.wb.graph.DirectedGraph;
import edu.ku.brc.specify.tasks.subpane.wb.graph.DirectedGraphException;
import edu.ku.brc.specify.tasks.subpane.wb.graph.Edge;
import edu.ku.brc.specify.tasks.subpane.wb.graph.Vertex;
import edu.ku.brc.specify.tasks.subpane.wb.schema.Field;
import edu.ku.brc.specify.tasks.subpane.wb.schema.Relationship;
import edu.ku.brc.specify.tasks.subpane.wb.schema.Table;
import edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMappingDefRel.ImportMappingRelFld;
import edu.ku.brc.ui.CommandAction;
import edu.ku.brc.ui.CommandDispatcher;
import edu.ku.brc.ui.CustomDialog;
import edu.ku.brc.ui.JStatusBar;
import edu.ku.brc.ui.UIHelper;
import edu.ku.brc.ui.UIRegistry;
import edu.ku.brc.util.AttachmentUtils;
import edu.ku.brc.util.Pair;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;

/**
 * @author timo
 * 
 */
public class Uploader implements ActionListener, KeyListener {
    private static boolean debugging = false;

    // Phases in the upload process...
    protected final static String INITIAL_STATE = "WB_UPLOAD_INITIAL_STATE";
    protected final static String CHECKING_REQS = "WB_UPLOAD_CHECKING_REQS";
    protected final static String VALIDATING_DATA = "WB_UPLOAD_VALIDATING_DATA";
    public final static String READY_TO_UPLOAD = "WB_UPLOAD_READY_TO_UPLOAD";
    protected final static String UPLOADING = "WB_UPLOAD_UPLOADING";
    public final static String SUCCESS = "WB_UPLOAD_SUCCESS";
    protected final static String SUCCESS_PARTIAL = "WB_UPLOAD_SUCCESS_PARTIAL";
    protected final static String RETRIEVING_UPLOADED_DATA = "WB_RETRIEVING_UPLOADED_DATA";
    protected final static String FAILURE = "WB_UPLOAD_FAILURE";
    protected final static String USER_INPUT = "WB_UPLOAD_USER_INPUT";
    protected final static String UNDOING_UPLOAD = "WB_UPLOAD_UNDO";
    protected final static String CLEANING_UP = "WB_UPLOAD_CLEANUP";

    /**
     * Resource string Ids
     */
    protected final static String WB_CELL_LENGTH_EXCEPTION = "WB_CELL_LENGTH_EXCEPTION";
    protected final static String WB_TOO_MANY_ERRORS = "WB_TOO_MANY_ERRORS";
    protected final static String WB_UPLOAD_FORM_TITLE = "WB_UPLOAD_FORM_TITLE";
    protected final static String WB_UPLOAD_ROW_SKIPPED = "WB_UPLOAD_ROW_SKIPPED";
    protected final static String WB_UPLOAD_VIEW_RESULTS_TITLE = "WB_UPLOAD_VIEW_RESULTS_TITLE";

    //Upload lock check results
    public final static int NO_LOCK = 0;
    public final static int LOCK_REMOVED = 1;
    public final static int LOCK_IGNORED = 2;
    public final static int LOCKED = 3;
    public final static int LOCK_FAILED = 4;

    /**
     * Maximum number of messages (validation errors) to display. Prevents un-responsiveness when a workbench is REALLY messed up.
     */
    protected final static int MAX_MSG_DISPLAY_COUNT = 800;

    /**
     * one of above statics
     */
    protected String currentOp;

    /**
     * The operation that preceded the currentOp
     */
    protected String previousOp = null;

    /**
     * the exception that killed the most recent op. null if most recent op was not murdered.
     * locked by this. Use setOpKiller and getOpKiller for access.
     */
    protected Exception opKiller;

    /**
     * used by bogusViewer
     */
    //Map<String, Vector<Vector<String>>>             bogusStorages            = null;
    /**
     * Displays uploaded data. Roughly.
     */
    protected DB.BogusViewer bogusViewer = null;

    protected DB db;

    protected UploadData uploadData;

    /**
     * The WorkbenchPane for the uploading dataset.
     */
    protected WorkbenchPaneSS wbSS;
    protected Workbench theWb;

    protected java.util.Collection<WorkbenchTemplateMappingItem> workbenchTemplateMappingItems = null;

    protected Vector<UploadField> uploadFields;

    protected Vector<UploadTable> uploadTables;

    protected DirectedGraph<Table, Relationship> uploadGraph;

    boolean verbose = false;

    boolean dataValidated = false;

    protected UploadMainPanel mainPanel;

    /**
     * While editing invalid cells, this is added as a listener to the WorkbenchPaneSS's spreadsheet cell editor component.
     * The cell editor component is currently always the same object.
     */
    protected List<Component> keyListeningTo = new LinkedList<Component>();

    /**
     * Problems with contents of cells in dataset.
     */
    protected Vector<UploadTableInvalidValue> validationIssues = null;
    /**
     * This object assigns default values for missing required fields and foreign keys. And provides
     * UI for viewing and changing the defaults.
     */
    MissingDataResolver resolver;

    /**
     * Required related classes that are not available in the dataset.
     */
    protected Vector<RelatedClassSetter> missingRequiredClasses;
    /**
     * Required fields not present in the dataset.
     */
    protected Vector<DefaultFieldEntry> missingRequiredFields;

    /**
     * A list of recordsets containing the keys of all objects uploaded. A separate RecordSet will be
     * stored for each UploadTable.
     */
    protected Vector<RecordSet> recordSets = null;
    /**
     * While an upload is underway, this member will be provide access to the uploader.
     */
    protected static Uploader currentUpload = null;

    /**
     * A unique identifier currently used to identify the upload. NOTE: Would it
     * be desirable to store info on imports - dataset imported, date, user, basic stats ???
     * 
     */
    protected String identifier = "uploader";

    /**
     * The time of the upload. Used to create the identifier.
     */
    protected Calendar uploadTime = null;

    /**
     * The currently executing upload task.
     */
    protected UploaderTask currentTask = null;

    /**
     * the index of the currently processing row in the dataset.
     */
    protected int rowUploading;

    /**
     * the index of the row to start uploading at 
     */
    protected int uploadStartRow;

    protected AtomicInteger updateTableId = new AtomicInteger(-1);

    /**
     * The workbenchrowimages for the current workbench row
     */
    protected List<WorkbenchRowImage> imagesForRow = new Vector<WorkbenchRowImage>();

    /**
     * Attachments created during the upload
     */
    protected List<UploadedRecordInfo> newAttachments = new Vector<UploadedRecordInfo>();

    protected UploadRetriever uploadedObjectViewer = null;

    protected boolean additionalLocksSet = false;
    protected static final Logger log = Logger.getLogger(Uploader.class);

    private class SkippedAttachment extends BaseUploadMessage {
        protected int row;

        public SkippedAttachment(String msg, int row) {
            super(msg);
            this.row = row;
        }

        /* (non-Javadoc)
         * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.BaseUploadMessage#getRow()
         */
        @Override
        public int getRow() {
            return row;
        }

    }

    /**
     * @author timbo
     *
     * @code_status Alpha
     *
     * Stores information about rows that were not uploaded during an upload.
     */
    @SuppressWarnings("unused")
    private class SkippedRow extends BaseUploadMessage {
        protected UploaderException cause;
        protected int row;

        /**
         * @param cause
         * @param row
         */
        public SkippedRow(UploaderException cause, int row) {
            super(null);
            this.cause = cause;
            this.row = row;
        }

        /**
         * @return the cause
         */
        public UploaderException getCause() {
            return cause;
        }

        /*
         * (non-Javadoc)
         * 
         * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMessage#getRow()
         */
        @Override
        public int getRow() {
            return row;
        }

        /*
         * (non-Javadoc)
         * 
         * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMessage#getMsg()
         */
        @Override
        public String getMsg() {
            return String.format(getResourceString(WB_UPLOAD_ROW_SKIPPED), String.valueOf(getRow() + 1)) + ": "
                    + cause.getMessage();
        }

        /*
         * (non-Javadoc)
         * 
         * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.UploadMessage#getData()
         */
        @Override
        public Object getData() {
            return cause;
        }
    }

    /**
     * Stores skipped row information for the most recent upload
     */
    protected Vector<SkippedRow> skippedRows;

    /**
     * Stores messages that are generated during an upload
     */
    protected Vector<UploadMessage> messages;

    /**
     * Stores messages that have been generated since the last time the message ui was updated.
     */
    protected Vector<UploadMessage> newMessages;

    /**
     * @return dataValidated
     */
    public boolean getDataValidated() {
        return dataValidated;
    }

    /**
     * @return the currentUpload;
     */
    public static Uploader getCurrentUpload() {
        return currentUpload;
    }

    /**
     * @param services
     * @return list of services with services modified or deleted according to
     * requirements of upload status.
     */
    public List<ServiceInfo> filterServices(final List<ServiceInfo> services) {
        List<ServiceInfo> result = new Vector<ServiceInfo>();
        for (int s = 0; s < services.size(); s++) {
            ServiceInfo service = services.get(s);
            if (includeService(service)) {
                if (service.getTask() instanceof DataEntryTask || service.getTask() instanceof InteractionsTask) {
                    try {
                        ServiceInfo newService = (ServiceInfo) service.clone();
                        newService.getCommandAction().setProperty("readonly", true);
                        service = newService;
                    } catch (CloneNotSupportedException ex) {
                        log.error(ex);
                        continue;
                    }
                }
                result.add(service);
            }
        }
        return result;
    }

    protected boolean includeService(final ServiceInfo service) {
        return !(service.getTask() instanceof RecordSetTask);
    }

    /**
     * @return rowUploading
     */
    public int getRow() {
        return rowUploading;
    }

    /**
     * @return the identifier.
     */
    public final String getIdentifier() {
        return identifier;
    }

    /**
     * @return the uploadTime
     */
    public Calendar getUploadTime() {
        return uploadTime;
    }

    /**
     * creates an identifier for an importer
     * 
     */
    protected void buildIdentifier() {
        uploadTime = new GregorianCalendar();
        identifier = uploadData.getWbRow(0).getWorkbench().getName() + "_" + uploadTime.get(Calendar.YEAR) + "-"
                + (uploadTime.get(Calendar.MONTH) + 1) + "-" + uploadTime.get(Calendar.DAY_OF_MONTH) + "_"
                + uploadTime.get(Calendar.HOUR_OF_DAY) + ":" + uploadTime.get(Calendar.SECOND);
    }

    /**
     * @param f
     * @return an existing importTable that contains f
     */
    protected UploadTable getUploadTable(UploadField f) {
        for (UploadTable result : uploadTables) {
            if (result.getTable().getName().equals(f.getField().getTable().getName())
                    && (result.getRelationship() == null && f.getRelationship() == null
                            || (result.getRelationship() != null && f.getRelationship() != null
                                    && result.getRelationship().equals(f.getRelationship())))) {
                return result;
            }
        }
        return null;
    }

    /**
     * @param name
     * @return UploadTable named name.
     */
    public UploadTable getUploadTableByName(final String name) {
        for (UploadTable result : uploadTables) {
            if (result.getTable().getName().equalsIgnoreCase(name)) {
                return result;
            }
        }
        return null;
    }

    /**
     * @param name
     * @return UploadTable named name.
     */
    public UploadTable getUploadTableByNameRel(final String name, final String relTblName) {
        for (UploadTable result : uploadTables) {
            if (result.getTable().getName().equalsIgnoreCase(name)) {
                if (relTblName == null) {
                    return result;
                }
                Relationship rel = result.getRelationship();
                if (rel != null) {
                    if (rel.getRelatedField().getTable().getTableInfo().getName().equalsIgnoreCase(relTblName)) {
                        return result;
                    }
                }
            }
        }
        return null;
    }

    /**
     * @throws UploaderException
     * 
     * builds uploadTables member based on uploadFields' contents
     */
    protected void buildUploadTables() throws UploaderException {
        uploadTables = new Vector<UploadTable>();
        for (UploadField f : uploadFields) {
            logDebug(f.getWbFldName());
            if (f.getField() != null) {
                UploadTable it = getUploadTable(f);
                boolean addIt = it == null;
                if (addIt) {
                    it = new UploadTable(this, f.getField().getTable(), f.getRelationship());
                    if (it != null) // ??
                    {
                        it.init();
                    }
                }
                if (it == null) {
                    throw new UploaderException(getResourceString("WB_UPLOAD_UPLOADTBL_BUILD_FAIL"),
                            UploaderException.ABORT_IMPORT);
                }
                it.addField(f);
                if (addIt) {
                    uploadTables.add(it);
                }
            }
        }
        for (UploadTable ut : uploadTables) {
            ut.findPrecisionDateFields();
        }
    }

    protected void logDebug(Object toLog) {
        if (debugging) {
            log.debug(toLog);
        }
    }

    /**
     * @param mapping
     * @throws UploaderException
     * 
     * Adds elements to uploadFields as required for relationship described in mapping.
     */
    protected void addMappingRelFlds(UploadMappingDefRel mapping) throws UploaderException {
        if (mapping.getSequenceFld() != null) {
            Field fld = db.getSchema().getField(mapping.getTable(), mapping.getSequenceFld());
            if (fld == null) {
                logDebug("could not find field in db: " + mapping.getTable() + "." + mapping.getField());
            }
            UploadField newFld = new UploadField(fld, -1, mapping.getWbFldName(), null);
            newFld.setSequence(mapping.getSequence());
            newFld.setValue(mapping.getSequence().toString());
            uploadFields.add(newFld);
        }
        Table t1 = db.getSchema().getTable(mapping.getTable());
        Table t2 = db.getSchema().getTable(mapping.getRelatedTable());
        for (ImportMappingRelFld fld : mapping.getLocalFields()) {
            Field dbFld = t1.getField(fld.getFieldName());
            if (dbFld == null) {
                logDebug("could not find field in db: " + t1.getName() + "." + fld.getFieldName());
            }
            UploadField newFld = new UploadField(dbFld, fld.getFldIndex(), fld.getWbFldName(), null);
            newFld.setSequence(mapping.getSequence());
            uploadFields.add(newFld);
        }
        if (mapping.getRelatedFields().size() > 0) {
            //Relationship r = null;
            Vector<Relationship> rs;
            try {
                rs = db.getGraph().getAllEdgeData(t1, t2);
                if (rs.size() == 0) {
                    rs = db.getGraph().getAllEdgeData(t2, t1);
                }
            } catch (DirectedGraphException ex) {
                throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
            }
            // find the 'right' rel. ie: discard Agent ->> ModifiedByAgentID/CreatedByAgentID
            //Actually it seems the modifiedby and createdby 'system' relationships get filtered out during graph creation,
            //so the filtering isn't necessarily necessary.
            for (int r = rs.size() - 1; r > -1; r--) {
                if (rs.get(r).getRelatedField().getName().equalsIgnoreCase("modifiedbyagentid")
                        || rs.get(r).getRelatedField().getName().equalsIgnoreCase("createdbyagentid")) {
                    rs.remove(r);
                }
            }

            //            for (Relationship rel : rs)
            //            {
            //               if (!rel.getRelatedField().getName().equalsIgnoreCase("modifiedbyagentid")
            //                        && !rel.getRelatedField().getName().equalsIgnoreCase("createdbyagentid"))
            //                {
            //                    r = rel;
            //                    break;
            //                }
            //            }
            if (rs.size() > 0) {
                for (Relationship r : rs) {
                    if (r.getRelatedField().getName().equalsIgnoreCase(mapping.getField())) {
                        Vector<ImportMappingRelFld> relFlds = mapping.getRelatedFields();
                        for (int relF = 0; relF < relFlds.size(); relF++) {
                            Field fld = db.getSchema().getField(t2.getName(), relFlds.get(relF).getFieldName());
                            int fldIdx = relFlds.get(relF).getFldIndex();
                            String wbFldName = relFlds.get(relF).getWbFldName();
                            UploadField newFld = new UploadField(fld, fldIdx, wbFldName, r);
                            newFld.setSequence(mapping.getSequence());
                            uploadFields.add(newFld);
                        }
                        return;
                    }
                }
            }
            throw new UploaderException("could not find relationship for mapping.", UploaderException.ABORT_IMPORT);
        }
    }

    /**
     * @throws UploaderException
     * 
     * builds ImportFields required for import and adds them to uploadFields member.
     */
    protected void buildUploadFields() throws UploaderException {
        for (int f = 0; f < uploadData.getCols(); f++) {
            UploadMappingDef m = uploadData.getMapping(f);
            if (m.getClass() != UploadMappingDefTree.class) {
                Field fld = this.db.getSchema().getField(m.getTable(), m.getField());
                if (fld == null) {
                    logDebug("could not find field in db: " + m.getTable() + "." + m.getField());
                }
                UploadField newFld = new UploadField(fld, m.getIndex(), m.getWbFldName(), null);
                uploadFields.add(newFld);
                if (m.getClass() == UploadMappingDefRel.class) {
                    UploadMappingDefRel relM = (UploadMappingDefRel) m;
                    newFld.setSequence(relM.getSequence());
                    try {
                        addMappingRelFlds(relM);
                        newFld.setIndex(-1);
                    } catch (UploaderException ex) {
                        throw ex;
                    }
                }
            }
        }
    }

    /**
     * @return a "printout" of the uploadFields member.
     */
    public Vector<String> printUploadFields() {
        Vector<String> lines = new Vector<String>();
        for (UploadField impF : uploadFields) {
            lines.add(impF.getField().getTable().getName() + "." + impF.getField().getName() + " ["
                    + Integer.toString(impF.getIndex()) + "] "
                    + (impF.getSequence() == null ? "" : " (" + impF.getSequence().toString() + ")"));
        }
        return lines;
    }

    /**
     * @return a printout of info about the uploadGraph
     * 
     * @throws DirectedGraphException
     */
    public Vector<String> printGraphInfo() throws DirectedGraphException {
        Vector<String> lines = new Vector<String>();
        lines.add("vertices:");
        for (Vertex<Table> v : uploadGraph.getVertices()) {
            lines.add("   " + v.getLabel());
        }
        Vector<String> graphEdges = uploadGraph.listEdges();
        lines.add("");
        lines.add("edges:");
        for (String e : graphEdges) {
            lines.add(e);
        }
        lines.add("");
        lines.add("Graph sources:");
        Set<Vertex<Table>> sources = uploadGraph.sources();
        for (Vertex<Table> tbl : sources) {
            lines.add("   " + tbl.getLabel());
        }
        lines.add("");
        if (uploadGraph.isStronglyConnected()) {
            lines.add("graph is strongly connected.");
        } else {
            lines.add("graph is not strongly connected.");
        }
        return lines;
    }

    /**
     * @return a "printout" of the uploadTables member.
     */
    public Vector<String> printUploadTables() {
        Vector<String> lines = new Vector<String>();
        for (UploadTable impT : uploadTables) {
            lines.add(impT.getTable().getName());
            for (Vector<UploadField> seq : impT.getUploadFields()) {
                for (UploadField f : seq) {
                    lines.add("       " + f.getField().getName()
                            + (f.getSequence() == null ? "" : " (" + f.getSequence().toString() + ")"));
                }
            }
        }
        return lines;
    }

    public Uploader(DB db, UploadData importData, final WorkbenchPaneSS wbSS, boolean isValidator)
            throws UploaderException {
        this(db, importData, wbSS, wbSS.getWorkbench(),
                wbSS.getWorkbench().getWorkbenchTemplate().getWorkbenchTemplateMappingItems(), isValidator);
    }

    public Uploader(DB db, UploadData importData, final WorkbenchPaneSS wbSS,
            final java.util.Collection<WorkbenchTemplateMappingItem> wbItems, boolean isValidator)
            throws UploaderException {
        this(db, importData, wbSS, wbSS.getWorkbench(), wbItems, isValidator);
    }

    /**
     * @param db
     * @param uploadData
     * @throws UploaderException
     */
    public Uploader(DB db, UploadData importData, final WorkbenchPaneSS wbSS, final Workbench theWb,
            final java.util.Collection<WorkbenchTemplateMappingItem> wbItems, boolean isValidator)
            throws UploaderException {
        this.db = db;
        this.uploadData = importData;
        this.wbSS = wbSS;
        this.theWb = theWb;
        this.workbenchTemplateMappingItems = wbItems;
        this.uploadFields = new Vector<UploadField>(importData.getCols());
        this.missingRequiredClasses = new Vector<RelatedClassSetter>();
        this.missingRequiredFields = new Vector<DefaultFieldEntry>();
        this.skippedRows = new Vector<SkippedRow>();
        this.messages = new Vector<UploadMessage>();
        this.newMessages = new Vector<UploadMessage>();
        buildUploadFields();
        buildUploadTables();
        addEmptyUploadTables();
        addRequiredUploadTables();
        buildUploadGraph();
        processTreeMaps();
        for (UploadTable ut : uploadTables) {
            logDebug("assigningFldSetters: " + ut.getTable().getName());
            ut.assignFldSetters();
        }
        orderUploadTables();
        buildUploadTableParents();
        reOrderUploadTables();
        if (!isValidator) {
            currentUpload = this;
        }
        for (UploadTable ut : uploadTables) {
            ut.adjustPlugHoles();
        }

        //        for (UploadTable ut : uploadTables){
        //           System.out.print(ut + " " + ut.isMatchChild() + " ");
        //           if (ut.getParentTables() != null && ut.getParentTables().size() > 0)
        //           {
        //              System.out.println(ut.getParentTables().get(0).size());
        //           } else
        //           {
        //              System.out.println("0");
        //           }
        //        }
        //        System.out.println("end of upload tables");
        //        System.out.println(getRootTable());
    }

    /**
     * @param ut child whose parents will be traced
     * @param rps current list of parents
     */
    protected void processParentsForRootTableSearch(UploadTable ut, Set<UploadTable> rps) {
        for (Vector<ParentTableEntry> ptes : ut.getParentTables()) {
            for (ParentTableEntry pte : ptes) {
                UploadTable p = pte.getImportTable();
                if (!p.getSpecialChildren().contains(ut)) {
                    rps.add(p);
                    processParentsForRootTableSearch(p, rps);
                }
            }
        }

    }

    /**
     * @return the 'root' or 'main' table for a dataset
     * 
     * This will be the table that has no one-to-many or one-to-one child tables or 'owns' all its child tables. 
     */
    protected UploadTable getRootTable() {
        //First, construct all tables that are 'owned' by other tables and their non-'owner' parents.
        HashSet<UploadTable> ruledChildrenAndTheirReqs = new HashSet<UploadTable>();
        for (UploadTable ut : uploadTables) {
            if (ut.isMatchChild()) {
                ruledChildrenAndTheirReqs.add(ut);
                processParentsForRootTableSearch(ut, ruledChildrenAndTheirReqs);
            }
        }
        //Then move backwards through the ALREADY ordered upload tables until we get to a table 
        //that is not in the list constructed above 
        for (int t = uploadTables.size() - 1; t >= 0; t--) {
            if (!uploadTables.get(t).isMatchChild() && !ruledChildrenAndTheirReqs.contains(uploadTables.get(t))) {
                return uploadTables.get(t);
            }
        }
        return null;
    }

    /**
     * @param mapI
     * @return true if mapI maps a taxonomic level for determination
     * 
     */
    protected boolean isDetTaxLevelMapping(WorkbenchTemplateMappingItem mapI) {
        //XXX this is pretty dumb. Probably would be better to add original table
        //info when UploadMappingDefTree objects are created.
        //This code will have to be changed if/when tree levels are created on-the-fly
        //from TreeDefinitions.
        if (!mapI.getTableName().equalsIgnoreCase("determination")) {
            return false;
        }
        String fldName = mapI.getFieldName();
        return fldName.startsWith("kingdom") || fldName.startsWith("phylum") || fldName.startsWith("subphylum")
                || fldName.startsWith("superclass") || fldName.startsWith("class") || fldName.startsWith("subclass")
                || fldName.startsWith("infraclass") || fldName.startsWith("superorder")
                || fldName.startsWith("order") || fldName.startsWith("suborder") || fldName.startsWith("infraorder")
                || fldName.startsWith("superfamily") || fldName.startsWith("family")
                || fldName.startsWith("subfamily") || fldName.startsWith("tribe") || fldName.startsWith("subtribe")
                || fldName.startsWith("genus") || fldName.startsWith("subgenus") || fldName.startsWith("species")
                || fldName.startsWith("subspecies") || fldName.startsWith("variety") || fldName.startsWith("forma");
    }

    /**
     * Adds extra upload tables. Currently only adds Determination if necessary when Genus/Species
     * are selected. Also should add CollectingEvent if Locality and CollectionObject are present.
     * And others???
     */
    protected void addEmptyUploadTables() throws UploaderException {

        boolean genSpPresent = false, detPresent = false, locPresent = false, coPresent = false, cePresent = false;
        for (UploadTable ut : uploadTables) {
            if (ut.getTblClass().equals(Determination.class)) {
                detPresent = true;
            }
            if (ut.getTblClass().equals(Locality.class)) {
                locPresent = true;
            }
            if (ut.getTblClass().equals(CollectionObject.class)) {
                coPresent = true;
            }
            if (ut.getTblClass().equals(CollectingEvent.class)) {
                cePresent = true;
            }
        }
        if (!detPresent) {
            int maxSeq = 0;
            for (WorkbenchTemplateMappingItem mapI : workbenchTemplateMappingItems) {
                if (isDetTaxLevelMapping(mapI)) {
                    genSpPresent = true;
                    try {
                        String fldName = mapI.getFieldName();
                        if (Integer.valueOf(fldName.substring(fldName.length() - 1)) > maxSeq) {
                            maxSeq = Integer.valueOf(fldName.substring(fldName.length() - 1));
                        }
                    } catch (NumberFormatException e) {
                        genSpPresent = false;
                    }
                }
            }
            if (genSpPresent) {
                UploadTable det = new UploadTable(this, db.getSchema().getTable("Determination"), null);
                for (int seq = 0; seq < maxSeq; seq++) {
                    UploadField fld = new UploadField(
                            db.getSchema().getField("determination", "collectionobjectid"), -1, null, null);
                    fld.setSequence(seq);
                    det.addField(fld);
                }
                det.init();
                uploadTables.add(det);
            }
        }
        if (!cePresent && locPresent && coPresent) {
            UploadTable ce = new UploadTable(this, db.getSchema().getTable("CollectingEvent"), null);
            ce.init();
            ce.addField(new UploadField(db.getSchema().getField("collectingevent", "stationfieldnumber"), -1, null,
                    null));
            uploadTables.add(ce);
        }
    }

    /**
     * Imposes additional ordering constraints created by the matchChildren property of UploadTable.
     * I.e. if A precedes B and B is in C.matchChildren, then A must precede C.
     */
    protected void reOrderUploadTables() throws UploaderException {
        SortedSet<Pair<UploadTable, UploadTable>> moves = new TreeSet<Pair<UploadTable, UploadTable>>(
                new Comparator<Pair<UploadTable, UploadTable>>() {
                    private boolean isAncestorOf(UploadTable t1, UploadTable t2) {
                        logDebug("isAncestorOf(" + t1 + ", " + t2 + ")");
                        if (t1.equals(t2)) {
                            return true;
                        }
                        for (Vector<ParentTableEntry> ptes : t2.getParentTables()) {
                            for (ParentTableEntry pte : ptes) {
                                if (isAncestorOf(t1, pte.getImportTable())) {
                                    return true;
                                }
                            }
                        }
                        return false;
                    }

                    public int compare(Pair<UploadTable, UploadTable> p1, Pair<UploadTable, UploadTable> p2) {
                        if (p1.getFirst() == p2.getFirst() && p1.getSecond() == p2.getSecond()) {
                            return 0;
                        }
                        if (isAncestorOf(p1.getSecond(), p2.getSecond())) {
                            return -1;
                        }
                        if (isAncestorOf(p2.getSecond(), p1.getSecond())) {
                            return 1;
                        }
                        return 0;
                    }
                });
        if (debugging) {
            logDebug("reOrderUploadTables(): initial order:");
            for (UploadTable ut : uploadTables) {
                logDebug("   " + ut.getTable().getName());
            }
        }
        for (UploadTable ut : uploadTables) {
            logDebug("Table: " + ut.getTable().getName());
            for (UploadTable mc : ut.getSpecialChildren()) {
                if (ut.needToMatchChild(mc.getTblClass())) {
                    logDebug("  Child: " + mc.getTable().getName());
                    for (ParentTableEntry pte : mc.getAncestors()) {
                        if (uploadTables.indexOf(ut) < uploadTables.indexOf(pte.getImportTable())) {
                            logDebug("reordering: " + pte.getImportTable().getTable().getName() + " must precede "
                                    + ut.getTable().getName());
                            moves.add(new Pair<UploadTable, UploadTable>(ut, pte.getImportTable()));
                        }
                    }
                }
            }
        }
        for (Pair<UploadTable, UploadTable> move : moves) {
            int fromIdx = uploadTables.indexOf(move.getSecond());
            int toIdx = uploadTables.indexOf(move.getFirst());
            this.logDebug("reording: " + move.getSecond().getTable().getName() + "(" + fromIdx + ") -> "
                    + move.getFirst().getTable().getName() + "(" + toIdx + ")");
            if (toIdx > fromIdx) {
                log.error("Can't meet ordering constraints: " + move.getSecond().getTable().getName() + ","
                        + move.getFirst().getTable().getName());
                throw new UploaderException("The Dataset is not uploadable.", UploaderException.ABORT_IMPORT);
            }
            uploadTables.remove(fromIdx);
            uploadTables.insertElementAt(move.getSecond(), toIdx);
        }
    }

    /**
     * @throws UploaderException builds the uploadGraph.
     */
    protected void buildUploadGraph() throws UploaderException {
        uploadGraph = new DirectedGraph<Table, Relationship>();
        try {
            for (UploadTable t : uploadTables) {
                String label = t.getTable().getName();
                if (uploadGraph.getVertexByLabel(label) == null) {
                    uploadGraph.addVertex(new Vertex<Table>(label, t.getTable()));
                }
            }
            for (Edge<Table, Relationship> edge : db.getGraph().getEdges()) {
                Vector<UploadTable> its1 = getUploadTable(edge.getPointA().getData());
                Vector<UploadTable> its2 = getUploadTable(edge.getPointB().getData());
                if (its1.size() > 0 && its2.size() > 0) {
                    uploadGraph.addEdge(edge.getPointA().getLabel(), edge.getPointB().getLabel(), edge.getData());
                }
            }
        } catch (DirectedGraphException e) {
            logDebug(e);
            throw new UploaderException(e, UploaderException.ABORT_IMPORT);
        }
    }

    /**
     * @param treeMap
     * @param level
     * @return a name for table representing data with rank represented by level param.
     */
    protected String getTreeTableName(final UploadMappingDefTree treeMap, final int level) {
        return treeMap.getTable() + Integer.toString(treeMap.getLevels().get(level).getRank());
    }

    /**
     * @param treeMap
     * @throws UploaderException adds Tables, ImportTables, ImportFields required by heirarchy
     *             represented by treeMap param.
     */
    protected void processTreeMap(UploadMappingDefTree treeMap) throws UploaderException {
        Table baseTbl = db.getSchema().getTable(treeMap.getTable());
        if (baseTbl == null) {
            throw new UploaderException("Could not find base table for tree mapping.",
                    UploaderException.ABORT_IMPORT);
        }
        Table parentTbl = null;
        UploadTableTree parentImpTbl = null;
        for (int level = 0; level < treeMap.getLevels().size(); level++) {
            logDebug(treeMap.getLevels().get(level).getWbFldName());
            // add new table to import graph for rank
            Table rankTbl = new Table(getTreeTableName(treeMap, level), baseTbl);
            try {
                uploadGraph.addVertex(new Vertex<Table>(rankTbl.getName(), rankTbl));
                if (parentTbl != null) {
                    Relationship rankRel = new Relationship(parentTbl.getKey(),
                            rankTbl.getField(treeMap.getParentField()), Relationship.REL_ONETOMANY);
                    uploadGraph.addEdge(parentTbl.getName(), rankTbl.getName(), rankRel);
                }
            } catch (DirectedGraphException ex) {
                throw new UploaderException(ex);
            }
            parentTbl = rankTbl;

            // create UploadTable for new table

            UploadTableTree it = new UploadTableTree(this, rankTbl, baseTbl, parentImpTbl,
                    treeMap.getLevels().get(level).isRequired(), treeMap.getLevels().get(level).getRank(),
                    treeMap.getLevels().get(level).getWbFldName(), treeMap.getLevels().get(level).isLowerSubTree());
            it.init();

            // add ImportFields for new table
            //for (int seq = 0; seq < treeMap.getLevels().get(level).size(); seq++)
            int prevSeq = -1;
            //for (TreeMapElement item : treeMap.getLevels().get(level))
            for (int i = 0; i < treeMap.getLevels().get(level).size(); i++) {
                TreeMapElement item = treeMap.getLevels().get(level).getElement(i);
                int seq = item.getSequence();
                Field fld = rankTbl.getField(item.getFldName());
                int fldIdx = item.getIndex();
                String wbFldName = item.getWbFldName();
                UploadField newFld1 = new UploadField(fld, fldIdx, wbFldName, null);
                if (item.isRequired()) {
                    newFld1.setRequired(true);
                }
                newFld1.setSequence(seq);
                uploadFields.add(newFld1);

                UploadField newFld2 = null;
                if (prevSeq != seq) {
                    newFld2 = new UploadField(rankTbl.getField("rankId"), -1, null, null);
                    newFld2.setRequired(true);
                    newFld2.setValue(Integer.toString(item.getRank()));
                    newFld2.setSequence(seq);
                    uploadFields.add(newFld2);
                }

                it.addField(newFld1);
                if (newFld2 != null) {
                    it.addField(newFld2);
                }

                prevSeq = seq;
            }
            uploadTables.add(it);
            parentImpTbl = it;
        }

        // add relationships from base table to other tables

        if (parentTbl != null) {
            for (Edge<Table, Relationship> e : db.getGraph().getEdges()) {
                if (e.getPointA().getData().equals(baseTbl)) {
                    Vertex<Table> relTblVertex = uploadGraph.getVertexByLabel(e.getPointB().getLabel());
                    if (relTblVertex != null) {
                        String relFld1Name = e.getData().getField().getName();
                        Relationship rel = new Relationship(parentTbl.getField(relFld1Name),
                                e.getData().getRelatedField(), e.getData().getRelType());
                        if (isRelationshipImplemented(rel, relTblVertex.getData())) {
                            try {
                                uploadGraph.addEdge(parentTbl.getName(), relTblVertex.getLabel(), rel);
                            } catch (DirectedGraphException ex) {
                                throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * @throws UploaderException
     * 
     * processes UploadMappingDefTree objects in uploadData.
     */
    protected void processTreeMaps() throws UploaderException {
        for (int m = 0; m < uploadData.getCols(); m++) {
            if (uploadData.getMapping(m).getClass() == UploadMappingDefTree.class) {
                UploadMappingDefTree treeMap = (UploadMappingDefTree) uploadData.getMapping(m);
                processTreeMap(treeMap);
            }
        }
    }

    /**
     * @param t
     * @return ImportTables with Table equal to t.
     */
    protected Vector<UploadTable> getUploadTable(Table t) {
        SortedSet<UploadTable> its = new TreeSet<UploadTable>();
        for (UploadTable it : uploadTables) {
            if (it.getTable().equals(t)) {
                its.add(it);
            }
        }
        return new Vector<UploadTable>(its);
    }

    /**
     * @author timbo
     * 
     * @code_status Alpha
     * 
     * Handles 'parent-child' relationships between UploadTables
     */
    /**
     * @author timbo
     *
     * @code_status Alpha
     *
     */
    /**
     * @author timo
     *
     */
    public class ParentTableEntry {
        /**
         * The parent UploadTable
         */
        protected UploadTable importTable;
        /**
         * The relationship to the parent
         */
        protected Relationship parentRel;
        /**
         * The hibernate property name of the foreign key.
         */
        protected String propertyName;
        /**
         * the methods used to set and get objects of importTable's class to children.
         */
        protected Method setter;
        protected Method getter;

        /**
         * true if the parent is required.
         */
        protected boolean required = false;

        /**
         * @param importTable
         * @param parentRel
         */
        public ParentTableEntry(UploadTable importTable, Relationship parentRel) {
            super();
            this.importTable = importTable;
            this.parentRel = parentRel;
            if (parentRel != null) {
                required = parentRel.getRelatedField().isForeignKey() && parentRel.getRelatedField().isRequired();
            }
        }

        /**
         * @return the importTable
         */
        public final UploadTable getImportTable() {
            return importTable;
        }

        /**
         * @return the parentRel
         */
        public final Relationship getParentRel() {
            return parentRel;
        }

        /**
         * @return the setter
         */
        public final Method getSetter() {
            return setter;
        }

        /**
         * @param setter the setter to set Also sets the propertyName.
         */
        public final void setSetter(Method setter) {
            this.setter = setter;
            if (this.setter.getName().startsWith("set")) {
                this.propertyName = UploadTable.deCapitalize(this.setter.getName().substring(3));
            } else {
                this.propertyName = UploadTable.deCapitalize(this.setter.getName());
            }
        }

        /**
         * @param getter
         */
        public final void setGetter(Method getter) {
            this.getter = getter;

        }

        /**
         * @return the getter
         */
        public final Method getGetter() {
            return this.getter;

        }

        public final String getForeignKey() {
            if (parentRel == null) {
                return importTable.getTblClass().getSimpleName();
            }
            return parentRel.getRelatedField().getName();
        }

        /**
         * @return the propertyName
         */
        public final String getPropertyName() {
            return propertyName;
        }

        /**
         * @return the required
         */
        public boolean isRequired() {
            return required;
        }
    }

    public UploadTable getOneToOneParent(UploadTable ut) {
        if (ut.isOneToOneChild()) {
            for (UploadTable poop : uploadTables) {
                if (poop.getSpecialChildren() != null && poop.getSpecialChildren().indexOf(ut) >= 0) {
                    return poop;
                }
            }
        }
        return null;
    }

    /**
     * @param r
     * @return true is r is working
     */
    protected boolean isRelationshipImplemented(Relationship r, Table t) {
        boolean result = true;
        if (t.getName().equalsIgnoreCase("collectingeventattribute")) {
            if (r.getRelatedField().getName().equalsIgnoreCase("HostTaxonID")) {
                result = false;
            }
        } else if (t.getName().equalsIgnoreCase("paleocontext")) {
            if (r.getRelatedField().getName().equalsIgnoreCase("ChronosStratEndID")) {
                result = false;
            } else if (r.getRelatedField().getName().equalsIgnoreCase("BioStratID")) {
                result = false;
            }

        } else if (t.getName().equalsIgnoreCase("determination")) {
            if (r.getRelatedField().getName().equalsIgnoreCase("PreferredTaxonID")) {
                result = false;
            }
        }
        if (!result) {
            log.debug("Ignoring relationship: " + r.getField().getName() + ":" + r.getRelatedField().getName());
        }
        return result;
    }

    /**
     * @throws UploaderException
     * 
     * Determines 'parent' UploadTables for each UploadTable
     */
    protected void buildUploadTableParents() throws UploaderException {
        for (UploadTable it : uploadTables) {
            Vector<Vector<ParentTableEntry>> parentTables = new Vector<Vector<ParentTableEntry>>();
            Set<Vertex<Table>> tbls = uploadGraph.into(it.getTable());
            for (Vertex<Table> tv : tbls) {
                try {
                    Vector<Relationship> rs = uploadGraph.getAllEdgeData(tv.getData(), it.getTable());
                    for (Relationship r : rs) {
                        if (isRelationshipImplemented(r, tv.getData())) {
                            Vector<UploadTable> impTs = getUploadTable(tv.getData());
                            Vector<ParentTableEntry> entries = new Vector<ParentTableEntry>();
                            for (UploadTable impT : impTs) {
                                if (impT.getRelationship() == null || r.equals(impT.getRelationship())) {
                                    impT.setHasChildren(true);
                                    entries.add(new ParentTableEntry(impT, r));
                                }
                            }
                            parentTables.add(entries);
                        }
                    }
                } catch (DirectedGraphException ex) {
                    throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
                }
            }
            try {
                it.setParentTables(parentTables);
            } catch (ClassNotFoundException ex) {
                throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
            } catch (NoSuchMethodException ex) {
                throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
            }
        }
    }

    /**
     * @throws UploaderException
     * 
     * Orders uploadTables according to dependencies in uploadGraph.
     */
    protected void orderUploadTables() throws UploaderException {
        try {
            Vector<Vertex<Table>> topoSort = uploadGraph.getTopoSort();
            Vector<UploadTable> newTables = new Vector<UploadTable>();
            for (Vertex<Table> v : topoSort) {
                Vector<UploadTable> its = getUploadTable(v.getData());
                for (UploadTable it : its) {
                    newTables.add(it);
                    uploadTables.remove(it);
                }
                if (uploadTables.size() == 0) {
                    break;
                }
            }
            uploadTables = newTables;
        } catch (DirectedGraphException ex) {
            throw new UploaderException(ex);
        }
    }

    /**
     * Currently this just adds CollectingEvent if it is not present if the current collection
     * has embedded CollectingEvents
     */
    protected void addRequiredUploadTables() throws UploaderException {
        if (AppContextMgr.getInstance().getClassObject(Collection.class).getIsEmbeddedCollectingEvent()) {
            boolean hasCO = false;
            boolean hasCE = false;
            for (UploadTable t : uploadTables) {
                if (t.getTblClass().equals(CollectionObject.class)) {
                    hasCO = true;
                } else if (t.getTblClass().equals(CollectingEvent.class)) {
                    hasCE = true;
                }
            }
            if (!hasCE && hasCO) {
                UploadTable ce = new UploadTable(this, db.getSchema().getTable("CollectingEvent"), null);
                ce.init();
                ce.addField(new UploadField(db.getSchema().getField("collectingevent", "stationfieldnumber"), -1,
                        null, null));
                uploadTables.add(ce);
            }
        }
    }

    /**
     * @param fld
     * @return msg describing current cell contents length and max length for the mapped fld
     */
    protected String getInvalidLengthErrMsg(String value, int maxlen) {
        return String.format(UIRegistry.getResourceString("UploadTable.FieldHasTooManyChars"), value.length(),
                maxlen);
    }

    /**
     * @param uploadTable
     * @param rowToValidate
     * @param colToValidate
     * @return
     */
    protected Vector<UploadTableInvalidValue> validateLengths(final UploadTable uploadTable,
            final int rowToValidate, final int colToValidate) {
        Vector<UploadTableInvalidValue> result = new Vector<UploadTableInvalidValue>();
        int startRow = rowToValidate != -1 ? rowToValidate : 0;
        int endRow = rowToValidate != -1 ? rowToValidate + 1 : uploadData.getRows();
        Integer[] colWidths = wbSS != null ? null : WorkbenchPaneSS.getMaxColWidths(theWb);
        for (Vector<UploadField> ufs : uploadTable.getUploadFields()) {
            for (UploadField uf : ufs) {
                if (uf.getIndex() != -1) {
                    for (int r = startRow; r < endRow; r++) {
                        String value = wbSS != null ? wbSS.getSpreadSheet().getValueAt(r, uf.getIndex()).toString()
                                : uploadData.get(r, uf.getIndex()).toString();
                        int maxlen = wbSS != null ? wbSS.getColumnMaxWidth(uf.getIndex())
                                : colWidths[uf.getIndex()];
                        if (uf.getField().getFieldInfo() != null) {
                            maxlen = uf.getField().getFieldInfo().getLength();
                        }
                        if (maxlen != -1 && value.length() > maxlen) {
                            result.add(new UploadTableInvalidValue(null, uploadTable, uf, r,
                                    new UploaderException(getInvalidLengthErrMsg(value, maxlen),
                                            //getResourceString(WB_CELL_LENGTH_EXCEPTION),
                                            UploaderException.INVALID_DATA)));
                        }
                    }
                }
            }
        }
        return result;
    }

    /**
     * @param row
     * @param col
     * @return list of invalid values
     */
    public Vector<UploadTableInvalidValue> validateData(int row, int col) {
        Vector<UploadTableInvalidValue> result = new Vector<UploadTableInvalidValue>();
        //XXX figure out which table is associated with col.
        for (UploadTable tbl : uploadTables) {
            tbl.clearBlankness();
        }
        for (UploadTable tbl : uploadTables) {
            result.addAll(validateLengths(tbl, row, col));
            tbl.validateRowValues(row, uploadData, result);
        }
        //XXX not the right place!!!!!!!!
        //        try 
        //        {
        //           checkChangedData(row);
        //        } catch (Exception ex)
        //        {
        //           ex.printStackTrace();
        //        }
        return result;
    }

    public Vector<UploadField> checkChangedData(int row) throws Exception {

        getRootTable().loadExportedRecord(/*row, */theWb.getRow(row).getRecordId());

        Vector<UploadField> result = new Vector<UploadField>();
        for (UploadTable ut : uploadTables) {
            result.addAll(ut.getChangedFields(row));
        }
        if (result.size() > 0) {
            //for (UploadField fld : result)
            //{
            //System.out.println(fld.getWbFldName());
            //}
        }
        return result;
    }

    /**
     * @param ut
     * @param recID primary key value for record in ut
     * @return true if other records than the current exported rec for child tables use 
     * the specified ut record.
     */
    public boolean recIsShared(final UploadTable ut, final Integer recID) {
        int utIndex = -1;
        boolean result = false;
        for (int t = 0; t < uploadTables.size(); t++) {
            if (ut == uploadTables.get(t)) {
                utIndex = t;
                break;
            }
        }
        if (utIndex != -1) {
            for (int t = utIndex + 1; t < uploadTables.size(); t++) {
                UploadTable tbl = uploadTables.get(t);
                ParentTableEntry pte = tbl.getParentTableEntry(ut);
                if (pte != null) {
                    String sql = "select count(*) from " + tbl.getTblClass().getSimpleName().toLowerCase()
                            + " where " + pte.getParentRel().getRelatedField() + " = "
                            + ut.getExportedRecord().getId() + " and "
                            + tbl.getTable().getTableInfo().getPrimaryKeyName() + " != "
                            + tbl.getExportedRecord().getId();
                    Integer cnt = BasicSQLUtils.getCount(sql);
                    if (cnt != null && cnt > 0) {
                        return true;
                    } else if (recIsShared(tbl, tbl.getExportedRecord().getId())) {
                        return true;
                    }
                }
            }
        }

        return result;
    }

    /**
     * @param col
     * @return
     */
    protected Vector<Pair<Integer, UploadTable>> getUploadTablesForColForMatch(int col,
            List<UploadTableInvalidValue> invalidCols) {
        Vector<Pair<Integer, UploadTable>> result = new Vector<Pair<Integer, UploadTable>>();
        Set<Integer> badCols = new HashSet<Integer>();
        for (UploadTableInvalidValue utiv : invalidCols) {
            badCols.add(utiv.getCol());
        }
        if (col == -1) {
            for (UploadTable ut : uploadTables) {
                for (int seq = 0; seq < ut.getUploadFields().size(); seq++) {
                    if (ut.isMatchable(badCols, seq)) {
                        //only need to add lowest rank for trees
                        if (!(ut instanceof UploadTableTree) || ((UploadTableTree) ut).getChild() == null) {
                            result.add(new Pair<Integer, UploadTable>(seq, ut));
                        }
                    }
                }
            }
            return result;
        } else {
            for (UploadTable ut : uploadTables) {
                int seq = 0;
                for (Vector<UploadField> flds : ut.getUploadFields()) {
                    if (ut.isMatchable(badCols, seq)) {
                        for (UploadField fld : flds) {
                            if (fld.getIndex() == col) {
                                result.add(new Pair<Integer, UploadTable>(seq, ut));
                                return result;
                            }
                        }
                    }
                    seq++;
                }
            }
            return result;
        }
    }

    /**
     * @param row
     * @param col -1 -> match all cols
     * @return
     */
    public List<UploadTableMatchInfo> matchData(int row, int col, List<UploadTableInvalidValue> invalidCols)
            throws UploaderException, InvocationTargetException, IllegalAccessException, ParseException,
            NoSuchMethodException, InstantiationException, SQLException {
        //Whereas, a match or non-match in any column X depends on matches or non-matches for columns whose tables are parents to X's table, and
        //whereas, a match or non-match in X can change the match status for columns whose tables are children of X's tables, and
        //whereas, a match or non-match in X can even change the match status for columns whose tables are parents of the children of X's tables
        //for example a change to species can affect determination which can effect collection object,
        //therefore it is best simply not to try to do matching on a col by col basis.
        int matchCol = -1; //match entire row

        Set<Integer> invalidColNums = new HashSet<Integer>();
        for (UploadTableInvalidValue iv : invalidCols) {
            invalidColNums.add(iv.getCol());
        }

        //       boolean matchAborted = false;
        if (matchCol == -1) {
            //          if (invalidColNums.size() == 0) {
            //            for (UploadTable t : uploadTables) {
            //               try {
            //                  if (theWb.getRow(rowUploading).getUploadStatus() != WorkbenchRow.UPLD_SUCCESS) {
            //                     uploadRowSavelessly(t, rowUploading);
            //                  } else {
            //                     throw new UploaderException(getResourceString("WB_UPLOAD_ROW_ALREADY_UPLOADED"), UploaderException.ABORT_ROW);
            //                  }
            //               } catch (UploaderException ex) {
            //                  if (ex.getStatus() == UploaderException.ABORT_ROW) {
            //                     ex.printStackTrace();
            //                     abortRow(ex, rowUploading);
            //                     matchAborted = true;
            //                     break;
            //                  }
            //                  throw ex;
            //               }
            //            }
            //
            //          }

            List<UploadTableMatchInfo> result = getRootTable().getMatchInfo(row, 0, invalidColNums);
            return result;
        }

        return null;

        //       Vector<UploadTableMatchInfo> result = new Vector<UploadTableMatchInfo>();
        //       for (Pair<Integer, UploadTable> utSeq : getUploadTablesForColForMatch(matchCol, invalidCols))
        //       {
        //          result.add(utSeq.getSecond().getMatchInfo(row, utSeq.getFirst()));
        //       }
        //       return result;
    }

    /**
     * @param col
     * @return
     */
    public DBFieldInfo getFieldInfoForCol(int col) {
        for (UploadTable ut : uploadTables) {
            for (Vector<UploadField> ufs : ut.getUploadFields()) {
                for (UploadField uf : ufs) {
                    if (uf.getIndex() == col && uf.getField() != null) {
                        return uf.getField().getFieldInfo();
                    }
                }
            }
        }
        return null;
    }

    /**
     * Sets default match status display settings.
     * 
     */
    //XXX Need to base the 'checkMatchInfo' settings on user-prefs.
    public void setDefaultMatchStatus() {
        for (UploadTable t : uploadTables) {
            if (t instanceof UploadTableTree) {
                t.setCheckMatchInfo(true);
            } else if (Agent.class.isAssignableFrom(t.getTblClass())) {
                t.setCheckMatchInfo(true);
            } else if (Locality.class.isAssignableFrom(t.getTblClass())
                    || LocalityDetail.class.isAssignableFrom(t.getTblClass())
                    || GeoCoordDetail.class.isAssignableFrom(t.getTblClass())) {
                //XXX testing!
                if (AppPreferences.getRemote().getBoolean("WB_HighlightNewLocRecs", true)
                        || AppPreferences.getRemote().getBoolean("WB_HighlightNewCERecs", false)) {
                    t.setCheckMatchInfo(true);
                }
            } else if (CollectingEvent.class.isAssignableFrom(t.getTblClass())
                    || CollectingTrip.class.isAssignableFrom(t.getTblClass())) {
                if (AppPreferences.getRemote().getBoolean("WB_HighlightNewCERecs", false)) {
                    t.setCheckMatchInfo(true);
                }

            } else {
                t.setCheckMatchInfo(false);
            }
        }
    }

    //    public void getMatchPrefs()
    //    {
    //       List<Class<?>>
    //    }

    /**
     * Validates contents of all cells in dataset.
     */
    public boolean validateData(boolean doInBackground) {
        dataValidated = false;
        setOpKiller(null);

        final Vector<UploadTableInvalidValue> issues = new Vector<UploadTableInvalidValue>();

        final UploaderTask validateTask = new UploaderTask(true, "WB_UPLOAD_CANCEL_MSG") {
            @Override
            public Object doInBackground() {
                start();
                try {
                    int progress = 0;
                    initProgressBar(0, uploadTables.size(), true,
                            getResourceString("WB_UPLOAD_VALIDATING") + " " + getResourceString("ERD_TABLE"),
                            false);
                    for (UploadTable tbl : uploadTables) {
                        tbl.clearBlankness();
                    }
                    for (UploadTable tbl : uploadTables) {
                        setCurrentOpProgress(++progress);
                        issues.addAll(validateLengths(tbl, -1, -1));
                        issues.addAll(tbl.validateValues(uploadData));
                    }
                    Collections.sort(issues);
                    dataValidated = issues.size() == 0;
                    return dataValidated;
                } catch (Exception ex) {
                    setOpKiller(ex);
                    return false;
                }
            }

            @Override
            public void done() {
                super.done();
                validationIssues = issues;
                statusBar.setText("");
                if (cancelled) {
                    setCurrentOp(Uploader.INITIAL_STATE);
                } else if (dataValidated && resolver.isResolved() && mainPanel != null) {
                    mainPanel.addMsg(new BaseUploadMessage(getResourceString("WB_DATASET_VALIDATED")));
                    setCurrentOp(Uploader.READY_TO_UPLOAD);
                } else if (mainPanel != null) {
                    mainPanel.addMsg(new BaseUploadMessage(getResourceString("WB_INVALID_DATASET")));
                    setCurrentOp(Uploader.USER_INPUT);
                }
            }

            //         /* (non-Javadoc)
            //          * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.Uploader.UploaderTask#cancelTask()
            //          */
            //         @Override
            //         public synchronized void cancelTask()
            //         {
            //            super.cancelTask();
            //            interrupt();
            //         }

        };

        SwingUtilities.invokeLater(new Runnable() {

            /* (non-Javadoc)
             * @see java.lang.Runnable#run()
             */
            @Override
            public void run() {
                UIRegistry.getStatusBar().setText(getResourceString(Uploader.VALIDATING_DATA));
            }
        });
        boolean result = true;
        validateTask.execute();
        if (!doInBackground) {
            try {
                result = (Boolean) validateTask.get();
            } catch (ExecutionException ex) {
                //hopefully it will be clear to caller that something went wrong?
            } catch (InterruptedException ex) {
                //hopefully it will be clear to caller that something went wrong?
            }
            //validateTask.finished();
            //validateTask.
        }
        return result;
    }

    protected synchronized void cancelTask(final UploaderTask task) {
        boolean tooLate = true;
        if (!task.isDone()) {
            tooLate = false;
            //            boolean undo = UIRegistry.displayConfirm(getResourceString("WB_UPLOAD_FORM_TITLE"),
            //                getResourceString(task.getCancelMsg()), getResourceString("YES"),
            //                getResourceString("NO"), JOptionPane.QUESTION_MESSAGE);
            if (!task.isDone()) {
                task.setUndo(false);
                task.cancelTask();
            } else {
                tooLate = true;
            }
        }
        if (tooLate) {
            UIRegistry.displayErrorDlg(getResourceString("WB_UPLOAD_TASK_ALREADY_COMPLETE"));
        }
    }

    /**
     * @return a set of tables for which no fields are being imported, but which provide foreign
     *         keys for tables that do have fields being imported.
     * 
     * lots more to do here i think re agents (can occur in so many roles) and recursive tables.
     * also needs to distinguish between collectionObject -> CollectingEvent (missing) -> Locality
     * which is kind of bad and CollectionObject -> CollectingEvent (missing) -> Locality (missing)
     * which is useless but maybe ok.
     */
    public Set<Table> checkForMissingTables() {
        Set<Table> result = new HashSet<Table>();
        for (UploadTable t : uploadTables) {
            Set<Vertex<Table>> ins = uploadGraph.into(t.getTable());
            for (Vertex<Table> in : ins) {
                if (!uploadTableIsPresent(in.getData())) {
                    result.add(in.getData());
                }
            }
        }
        return result;
    }

    /**
     * @return the uploadData
     */
    public UploadData getUploadData() {
        return uploadData;
    }

    /**
     * @param t
     * @return true if there is an UploadTable defined for t.
     */
    protected boolean uploadTableIsPresent(final Table t) {
        for (UploadTable it : uploadTables) {
            if (it.getTable() == t) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param tbl
     * @return true if tbl is represented in the Workbench schema.
     */
    protected boolean isInWBSchema(final Table tbl) {
        return WorkbenchTask.getDatabaseSchema().getInfoById(tbl.getTableInfo().getTableId()) != null;
    }

    /**
     * @param tbl
     * @return true if tbl is in uploadGraph.
     */
    protected boolean isInUploadGraph(final Vertex<Table> tbl) {
        return uploadGraph.getVertexByLabel(tbl.getLabel()) != null;
    }

    /**
     * @param tbl
     * @return true if tbl's class implements Treeable.
     */
    protected boolean isTreeable(final Table tbl) {
        return Treeable.class.isAssignableFrom(tbl.getTableInfo().getClassObj());
        //return false;
    }

    /**
     * @param toMatch
     * @return matching vertex in uploadGraph.
     * Performs extra processing when Table represents a Treeable class.
     */
    protected Vertex<Table> getMatchingVertexInUpload(final Table toMatch) {
        if (isTreeable(toMatch)) {
            UploadTableTree treeTbl = null;
            for (UploadTable ut : uploadTables) {
                if (ut instanceof UploadTableTree) {
                    UploadTableTree utt = (UploadTableTree) ut;
                    if (utt.getBaseTable().equals(toMatch)) {
                        if (treeTbl == null || treeTbl.getRank() < utt.getRank())
                            treeTbl = utt;
                    }
                }
            }
            if (treeTbl == null) {
                return null;
            }
            return uploadGraph.getVertexByData(treeTbl.getTable());
        }
        //else
        return uploadGraph.getVertexByData(toMatch);
    }

    /**
     * @param depth
     * @return groups of tables that can be added to the upload graph to make it a connected graph.
     * @throws DirectedGraphException
     */
    protected Vector<Vector<Table>> connectUploadGraph(final int depth) throws DirectedGraphException {
        //no rocket science whatsoever.
        //just try all ways to add one table. If no luck, then try all ways to add 2, 3, 4 up to depth tables.
        //This could become very very very inefficient if the number of tables in the Workbench Schema gets larger.
        Vector<Vector<Table>> result = new Vector<Vector<Table>>();
        DirectedGraph<Table, Relationship> dbGraph = this.db.getSchema().getGraph();
        for (Vertex<Table> newTbl : dbGraph.getVertices()) {
            if (!isTreeable(newTbl.getData()) && isInWBSchema(newTbl.getData())) {
                if (!isInUploadGraph(newTbl)) {
                    uploadGraph.addVertex(newTbl);
                    for (Vertex<Table> adj : dbGraph.getAdjacentVertices(newTbl)) {
                        //Vertex<Table> endPt = uploadGraph.getVertexByData(adj.getData());
                        Vertex<Table> endPt = getMatchingVertexInUpload(adj.getData());
                        if (endPt != null) {
                            uploadGraph.addEdge(newTbl.getLabel(), endPt.getLabel());
                        }
                    }
                    for (Vertex<Table> adj : dbGraph.into(newTbl.getData())) {
                        Vertex<Table> endPt = getMatchingVertexInUpload(adj.getData());
                        if (endPt != null) {
                            uploadGraph.addEdge(endPt.getLabel(), newTbl.getLabel());
                        }
                    }
                    if (uploadGraph.isConnected()) {
                        Vector<Table> newTblResult = new Vector<Table>();
                        newTblResult.add(newTbl.getData());
                        result.add(newTblResult);
                    } else if (depth - 1 > 0) {
                        Vector<Vector<Table>> results = connectUploadGraph(depth - 1);
                        for (Vector<Table> tbls : results) {
                            tbls.add(newTbl.getData());
                            result.add(tbls);
                        }
                    }
                    uploadGraph.removeVertex(newTbl);
                }
            }
        }
        return result;
    }

    /**
     * @return true if the upload is updating existing records
     */
    public boolean isUpdateUpload() {
        UploadTable root = getRootTable();
        return theWb.getExportedFromTableName() != null && root != null && root.isUpdateMatches();
    }

    /**
     * @return true if the update upload can be performed.
     */
    protected boolean isValidUpdateUpload() {
        //currently this just checks for 1-many tables, which is a temporary restriction, but
        //it is possible that some tables just won't be updatable.
        //       for (UploadTable ut : uploadTables)
        //       {
        //          if (ut.getUploadFields().size() > 1)
        //          {
        //             return false;
        //          } else 
        //          {
        //             if (ut.getTblClass().equals(Determination.class)
        //                   || ut.getTblClass().equals(Collector.class)
        //                   || ut.getTblClass().equals(Preparation.class)
        //                   || ut.getTblClass().equals(CollectionObjectAttribute.class)
        //                   || ut.getTblClass().equals(CollectingEventAttribute.class)
        //                   || ut.getTblClass().equals(AccessionAuthorization.class)
        //                   || ut.getTblClass().equals(CollectionObjectCitation.class)
        //                   || ut.getTblClass().equals(LocalityCitation.class)
        //                   || ut.getTblClass().equals(DNASequence.class)
        //                   )
        //             {
        //                return false;
        //             }      
        //          }
        //       }
        return true;
    }

    /**
     * @return groups tables that, if added to the dataset, will probably make the dataset
     *         structurally sufficient for upload.
     */
    protected Vector<Vector<Table>> getMissingTbls() throws DirectedGraphException {
        Vector<Vector<Table>> result = new Vector<Vector<Table>>();
        int depth = 1;
        while (result.size() == 0 && depth < 4) {
            result.addAll(connectUploadGraph(depth++));
        }

        if (result.size() == 0) {
            result.add(null);
        }

        //It would make more sense to fix connectUploadGraph to not generate duplicate solutions,
        //but that would be hard.
        return removeDuplicateSolutions(result);

    }

    /**
     * @param solutions
     * @return a copy of solutions with only duplicate solutions (i.e same tables, different order) removed.
     */
    protected Vector<Vector<Table>> removeDuplicateSolutions(Vector<Vector<Table>> solutions) {
        SortedSet<Vector<Table>> sorted = new TreeSet<Vector<Table>>(new Comparator<Vector<Table>>() {
            /*
             * (non-Javadoc)
             * 
             * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
             */
            public int compare(Vector<Table> o1, Vector<Table> o2) {
                int size1 = o1 == null ? 0 : o1.size();
                int size2 = o2 == null ? 0 : o2.size();
                if (o1 != null && o2 != null && size1 == size2) {
                    if (o1.containsAll(o2)) {
                        return 0;
                    }
                    // else
                    if (o1.size() == 0)
                        return 0;
                    int id1 = o1.get(0).getTableInfo().getTableId();
                    int id2 = o2.get(0).getTableInfo().getTableId();
                    return id1 < id2 ? -1 : (id1 == id2 ? 0 : 1);
                }
                return size1 < size2 ? -1 : 1;

            }
        });
        for (Vector<Table> solution : solutions) {
            sorted.add(solution);
        }
        Vector<Vector<Table>> result = new Vector<Vector<Table>>();
        result.addAll(sorted);
        return result;
    }

    /**
     * @return true if images are attached to any row
     */
    protected boolean attachmentsPresent() {
        for (int r = 0; r < this.uploadData.getRows(); r++) {
            Set<WorkbenchRowImage> images = uploadData.getWbRow(r).getWorkbenchRowImages();
            if (images != null && images.size() > 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return list of attachable table classes in order of precedence.
     */
    protected Vector<Class<?>> getAttachableTables() {
        Vector<Class<?>> result = new Vector<Class<?>>(8);
        result.add(FieldNotebook.class);
        result.add(FieldNotebookPageSet.class);
        result.add(FieldNotebookPage.class);
        result.add(Taxon.class);
        result.add(Accession.class);
        result.add(Locality.class);
        result.add(CollectingEvent.class);
        result.add(CollectionObject.class);
        return result;
    }

    /**
     * @return a list of uploadtables that can have attachments in the current upload.
     * 
     *  Not currently used, but will be when/if we allow users to choose which tables
     *  attachments apply to.
     */
    public List<UploadTable> getAttachableTablesInUse() {
        List<UploadTable> result = new Vector<UploadTable>();
        for (UploadTable ut : uploadTables) {
            if (AttachmentOwnerIFace.class.isAssignableFrom(ut.getTblClass())) {
                //System.out.println(ut.getTblTitle());
                if (attachmentsSupported(ut)) {
                    //System.out.println("   attachable: " + ut.getTblTitle());
                    result.add(ut);
                }
            }
        }
        return result;
    }

    /**
     * @param tblClass
     * @return true if attachments can be uploaded for tblClass
     * 
     * Lots of UI work needs to be done before we can support attachments
     * for 1-many situations (collectors, determiners, preps, determinations)
     */
    protected boolean attachmentsSupported(UploadTable uploadTable) {
        //Only allow attachments to agent for agent-only dataset
        Class<?> tblClass = uploadTable.getTblClass();
        if (tblClass.equals(Agent.class)) {
            for (UploadTable ut : uploadTables) {
                if (!ut.getTblClass().equals(Agent.class) && !ut.getTblClass().equals(Address.class)) {
                    return false;
                }
            }
            return true;
        }

        //Only allow attachments to taxon for taxon-only dataset
        if (tblClass.equals(Taxon.class)) {
            for (UploadTable ut : uploadTables) {
                if (!ut.getTblClass().equals(Taxon.class)) {
                    return false;
                }
            }
            return true;
        }

        //Exclude tables that can have multiple records per row in a dataset 
        for (List<ParentTableEntry> ptes : uploadTable.getParentTables()) {
            for (ParentTableEntry pte : ptes) {
                if (pte.getImportTable().needToMatchChild(tblClass) && uploadTable.getUploadFields().size() > 1) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @return list of attachable table names.
     */
    protected String getAttachableStr() {
        String result = "";
        Set<Class<?>> clss = new HashSet<Class<?>>();
        List<UploadTable> uts = getAttachableTablesInUse();
        for (UploadTable ut : uts) {
            clss.add(ut.getTblClass());
        }
        for (Class<?> cls : clss) {
            DBTableInfo info = DBTableIdMgr.getInstance().getByClassName(cls.getName());
            if (info != null) {
                if (!StringUtils.isBlank(result)) {
                    result += ", ";
                }
                result += info.getTitle();
            }
        }
        return result;
    }

    /**
     * @return the table to attach images to.
     */
    public UploadTable getAttachToTable() {
        int priority = -1;
        Vector<Class<?>> attachable = getAttachableTables();
        UploadTable result = null;
        boolean isAgentOnly = true;
        for (UploadTable ut : uploadTables) {
            if (!ut.getTblClass().equals(Agent.class) || ut.getTblClass().equals(Address.class)) {
                isAgentOnly = false;
            }
            int newPriority = attachable.indexOf(ut.getTblClass());
            if (newPriority > priority) {
                priority = newPriority;
                result = ut;
            }
        }
        if (result instanceof UploadTableTree) {
            while (((UploadTableTree) result).getChild() != null) {
                result = ((UploadTableTree) result).getChild();
            }
        }
        if (result == null && isAgentOnly) {
            for (UploadTable ut : uploadTables) {
                if (ut.getTblClass().equals(Agent.class)) {
                    return ut;
                }
            }
        }
        return result;
    }

    /**
     * @param wri
     * @return table to attach wri to
     */
    public UploadTable getAttachToTable(WorkbenchRowImage wri) {
        if (wri.getAttachToTableName() == null) {
            return getAttachToTable();
        }

        String attachToStrs[] = wri.getAttachToTableName().split("\\.");
        String tblName = attachToStrs[0];
        String relTblName = attachToStrs.length > 1 ? attachToStrs[1] : null;

        UploadTable result = getUploadTableByNameRel(tblName, relTblName);
        if (result == null) {
            result = getAttachToTable();
            String msg = "row " + rowUploading + ": " + wri.getAttachToTableName() + " is not mapped."
                    + " Attaching image to "
                    + (result != null ? result.toString() : /*but this CANNOT happen*/ "default") + " table.";
            log.warn(msg);
        }
        return result;
    }

    /**
     * @return true if attachments are OK.
     */
    protected boolean verifyAttachments() {
        if (!attachmentsPresent()) {
            return true;
        }

        return getAttachToTable() != null;
    }

    /**
     * @return true if the dataset can be uploaded.
     * 
     * Checks that the import mapping and graph are OK. Checks that all required data (TreeDefs,
     * TreeDefItems, DeterminationStatuses, etc) is present in the database.
     * 
     * Saves messages for each problem.
     */
    public Vector<UploadMessage> verifyUploadability() throws UploaderException, ClassNotFoundException {
        Vector<UploadMessage> errors = new Vector<UploadMessage>();
        try {
            Vector<Vector<Table>> missingTbls = new Vector<Vector<Table>>();

            //check that parents exist for one-to-one children (which are required to be defined as many-to-one parents 
            //in hibernate)
            for (UploadTable t : uploadTables) {
                if (t.isOneToOneChild() && !t.getHasChildren() && !(t.getTblClass().equals(LocalityDetail.class)
                        || t.getTblClass().equals(GeoCoordDetail.class))) {
                    Vector<Vertex<Table>> vs = db.getGraph()
                            .getAdjacentVertices(new Vertex<Table>(t.getTable().getName(), t.getTable()));
                    Vector<Table> tbls = new Vector<Table>();
                    for (Vertex<Table> vertex : vs) {
                        tbls.add(vertex.getData());
                    }
                    missingTbls.add(tbls);
                }
            }
            if (!uploadGraph.isConnected()) {
                missingTbls.addAll(getMissingTbls());
            }
            if (missingTbls.size() > 0) {
                Vector<Pair<String, Vector<Table>>> missingTblHints = new Vector<Pair<String, Vector<Table>>>();
                int h = 1;
                for (Vector<Table> tbls : missingTbls) {
                    String msg = "";
                    if (tbls != null && tbls.size() > 0) {
                        msg += " ";
                        for (int t = 0; t < tbls.size(); t++) {
                            if (t > 0) {
                                msg += ", ";
                            }
                            msg += tbls.get(t).getTableInfo().getTitle();
                        }
                    }
                    if (!msg.equals("")) {
                        missingTblHints.add(new Pair<String, Vector<Table>>(
                                String.format(getResourceString("WB_UPLOAD_MISSING_TBL_HINT"), h++, msg), tbls));
                    }
                }
                if (missingTblHints.size() > 0) {
                    errors.add(new BaseUploadMessage(getResourceString("WB_UPLOAD_MISSING_TBL_HINTS")));
                    for (Pair<String, Vector<Table>> hint : missingTblHints) {
                        errors.add(new InvalidStructure("   " + hint.getFirst(), hint.getSecond()));
                    }
                } else {
                    errors.add(new BaseUploadMessage(getResourceString("WB_UPLOAD_MISSING_TBL_NO_HINTS")));
                }
            }
        } catch (DirectedGraphException ex) {
            throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
        }

        errors.addAll(validateConsistency());

        if (!verifyAttachments()) {
            String msg = String.format(UIRegistry.getResourceString("WB_UPLOAD_NO_ATTACHABLES"),
                    getAttachableStr());
            errors.add(new BaseUploadMessage(msg));
        }

        //if tables are missing return now, because spurious errors may be generated.
        if (errors.size() != 0) {
            return errors;
        }

        // now find out what data is not available in the dataset and not available in the database
        // Considering such issues 'structural' for now.
        missingRequiredClasses.clear();
        missingRequiredFields.clear();
        Iterator<RelatedClassSetter> rces;
        Iterator<DefaultFieldEntry> dfes;
        for (UploadTable t : uploadTables) {
            try {
                rces = t.getRelatedClassDefaults();
            } catch (ClassNotFoundException ex) {
                log.error(ex);
                return null;
            }
            while (rces.hasNext()) {
                missingRequiredClasses.add(rces.next());
            }

            try {
                dfes = t.getMissingRequiredFlds();
            } catch (NoSuchMethodException ex) {
                log.error(ex);
                return null;
            }
            while (dfes.hasNext()) {
                missingRequiredFields.add(dfes.next());
            }
        }
        resolver = new MissingDataResolver(missingRequiredClasses, missingRequiredFields);
        for (RelatedClassSetter rcs : missingRequiredClasses) {
            if (!rcs.isDefined()) {
                // Assume it is undefined because no related data exists in the database.
                // Also assuming (currently erroneously) that definition problems related to
                // choosing
                // from multiple existing related data have been resolved through user interaction.
                String tblName = DBTableIdMgr.getInstance()
                        .getByShortClassName(rcs.getRelatedClass().getSimpleName()).getTitle();
                // a very vague message...
                String msg = getResourceString("WB_UPLOAD_MISSING_DBDATA") + ": " + tblName;
                errors.add(new InvalidStructure(msg, this));
            }
        }

        Vector<DefaultFieldEntry> undefinedDfes = new Vector<DefaultFieldEntry>();
        for (DefaultFieldEntry dfe : missingRequiredFields) {
            if (!dfe.isDefined()) {
                undefinedDfes.add(dfe);
            }
        }
        //now remove possibly confusing or redundant dfes.
        Collections.sort(undefinedDfes, new Comparator<DefaultFieldEntry>() {

            /* (non-Javadoc)
             * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
             */
            @Override
            public int compare(DefaultFieldEntry o1, DefaultFieldEntry o2) {
                int result = o1.getUploadTbl().getTable().getName()
                        .compareTo(o2.getUploadTbl().getTable().getName());
                if (result != 0) {
                    return result;
                }
                boolean o1IsUserFld = o1.getUploadFld() == null || o1.getUploadFld().getIndex() != -1;
                boolean o2IsUserFld = o2.getUploadFld() == null || o2.getUploadFld().getIndex() != -1;
                if (o1IsUserFld == o2IsUserFld) {
                    return (o1.getFldName().compareTo(o2.getFldName()));
                }
                if (o1IsUserFld) {
                    return -1;
                }
                return 1;
            }

        });
        UploadTable currentTbl = null;
        Vector<DefaultFieldEntry> dfes4Tbl = new Vector<DefaultFieldEntry>();
        Vector<DefaultFieldEntry> dfes2Remove = new Vector<DefaultFieldEntry>();
        for (DefaultFieldEntry dfe : undefinedDfes) {
            if (dfe.getUploadTbl() != currentTbl) {
                if (dfes4Tbl.size() > 1) {
                    boolean gotAUserFld = false;
                    for (DefaultFieldEntry tblDfe : dfes4Tbl) {
                        boolean isAUserFld = tblDfe.getUploadFld() == null
                                || tblDfe.getUploadFld().getIndex() != -1;
                        gotAUserFld = gotAUserFld || isAUserFld;
                        if (!isAUserFld && gotAUserFld) {
                            //remove weird fields if there are other non-weird fields from the table
                            dfes2Remove.add(tblDfe);
                        }
                    }
                }
                dfes4Tbl.clear();
                currentTbl = dfe.getUploadTbl();
            }
            dfes4Tbl.add(dfe);
        }
        if (dfes4Tbl.size() > 1) {
            boolean gotAUserFld = false;
            for (DefaultFieldEntry tblDfe : dfes4Tbl) {
                boolean isAUserFld = tblDfe.getUploadFld() == null || tblDfe.getUploadFld().getIndex() != -1;
                gotAUserFld = gotAUserFld || isAUserFld;
                if (!isAUserFld && gotAUserFld) {
                    //remove weird fields if there are other non-weird(or weird) fields from the table
                    dfes2Remove.add(tblDfe);
                }
            }
        }
        for (DefaultFieldEntry dfe : dfes2Remove) {
            undefinedDfes.remove(dfe);
        }
        for (DefaultFieldEntry dfe : undefinedDfes) {
            // see note above for missignRequiredClasses iteration
            // another very vague message...
            String msg = getResourceString("WB_UPLOAD_MISSING_DBDATA") + ": "
                    + dfe.getUploadTbl().getTable().getTableInfo().getTitle() + "." + dfe.getFldName(); // i18n (dfe.getFldName() is not using title nor wb
                                                                                                                                                                          // column header)
            errors.add(new InvalidStructure(msg, this));
        }

        for (UploadTable t : uploadTables) {
            errors.addAll(t.verifyUploadability());
        }

        return errors;
    }

    /**
     * @return Vector containing messages for detected inconsistencies.
     * 
     * 
     */
    public Vector<UploadMessage> validateConsistency() {
        Vector<UploadMessage> result = new Vector<UploadMessage>();

        try {
            // Make sure that the tax tree levels included for each determination are consistent.
            // All need to include the same levels, for example [Genus 1, Species1, SubSpecies 1,
            // Genus
            // 2, Species 2] is inconsistent because SubSpecies 2 is missing.
            // Since, right now, tax trees are the only trees that require this test.
            // Non-tree 1-manys like Collector are ok (but possibly not desirable?) if inconsistent:
            // [FirstName 1, LastName 1, LastName 2] works.
            TreeMapElements maxTaxSeqLevel = null;
            for (int m = 0; m < uploadData.getCols(); m++) {
                if (uploadData.getMapping(m).getTable().equalsIgnoreCase("taxon")) {
                    UploadMappingDefTree tmap = (UploadMappingDefTree) uploadData.getMapping(m);
                    boolean seqSizeInconsistent = false;
                    for (int l = 0; l < tmap.getLevels().size(); l++) {
                        TreeMapElements tmes = tmap.getLevels().get(l);
                        if (tmes.getMaxSeq() > 0
                                || maxTaxSeqLevel != null && maxTaxSeqLevel.getRank() < tmes.getRank()) {
                            if (maxTaxSeqLevel == null) {
                                maxTaxSeqLevel = tmes;
                            } else if (maxTaxSeqLevel.getMaxSeq() != tmes.getMaxSeq()) {
                                seqSizeInconsistent = true;
                                if (maxTaxSeqLevel.getMaxSeq() < tmes.getMaxSeq()) {
                                    maxTaxSeqLevel = tmes;
                                }
                            }

                        }

                        boolean[] seqs = tmes.getSeqs();
                        for (int s = 0; s < seqs.length; s++) {
                            if (!seqs[s]) {
                                String msg = String.format(getResourceString("WB_UPLOAD_MISSING_SEQ"),
                                        tmes.getWbFldName());
                                result.add(new InvalidStructure(msg, null));
                            }
                        }
                    }

                    if (seqSizeInconsistent && maxTaxSeqLevel != null) {
                        for (int l = 0; l < tmap.getLevels().size(); l++) {
                            TreeMapElements tmes = tmap.getLevels().get(l);
                            if (tmes.getRank() > maxTaxSeqLevel.getRank()
                                    && tmes.getMaxSeq() < maxTaxSeqLevel.getMaxSeq()) {
                                for (int s = tmes.getMaxSeq() + 1; s <= maxTaxSeqLevel.getMaxSeq(); s++) {
                                    String msg = String.format(getResourceString("WB_UPLOAD_MISSING_SEQ"),
                                            tmes.getWbFldName());
                                    result.add(new InvalidStructure(msg, null));
                                }
                            }
                        }
                    }
                }
            }

            if (maxTaxSeqLevel != null && maxTaxSeqLevel.size() > 1) {
                // check to see if Determination.isCurrent is either not present or always present.
                int isCurrentCount = 0;
                boolean[] isCurrentPresent = new boolean[maxTaxSeqLevel.size()];
                for (int b = 0; b < isCurrentPresent.length; b++)
                    isCurrentPresent[b] = false;
                String isCurrentCaptionSample = null;
                for (int m = 0; m < uploadData.getCols(); m++) {
                    if (uploadData.getMapping(m).getTable().equalsIgnoreCase("determination")
                            && uploadData.getMapping(m).getField().equalsIgnoreCase("iscurrent")) {
                        isCurrentCount++;
                        isCurrentPresent[((UploadMappingDefRel) uploadData.getMapping(m)).getSequence()] = true;
                        isCurrentCaptionSample = uploadData.getMapping(m).getWbFldName();
                    }
                }
                if (isCurrentCount != 0 && isCurrentCount != maxTaxSeqLevel.size()) {
                    for (int c = 0; c < isCurrentPresent.length; c++) {
                        if (!isCurrentPresent[c]) {
                            String fldName;
                            // strip off trailing number (assuming we will never allow it to be >
                            // 10)
                            // and trim.
                            if (isCurrentCaptionSample != null) {
                                fldName = isCurrentCaptionSample.substring(0, isCurrentCaptionSample.length() - 2)
                                        .trim();
                            } else {
                                fldName = "Is Current"; // i18n (but isCurrentCaptionSample can't be
                                // null. Right.)
                            }
                            String msg = getResourceString("WB_UPLOAD_MISSING_FLD") + ": " + fldName + " "
                                    + Integer.toString(c + 1);
                            result.add(new InvalidStructure(msg, null));
                        }
                    }
                }
            }

            return result;
        } finally {
            if (result.size() > 0) {
                currentUpload = null;
            }
        }
    }

    /**
     * @param row
     * @return
     */
    public synchronized Integer getRowRecordId(int row) throws UploaderException {
        try {
            return theWb.getRow(row).getRecordId();
        } catch (Exception ex) {
            throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
        }
    }

    protected void setUpdateTableId(Integer updateTblId) {
        this.updateTableId.set(updateTblId == null ? -1 : updateTblId);
    }

    protected Integer getUpdateTableId() {
        int got = this.updateTableId.get();
        return got >= 0 ? got : null;
    }

    /**
     * @throws UploaderException
     * 
     * Sets up for upload.
     */
    protected void prepareToUpload(DataProviderSessionIFace theSession) throws UploaderException {
        //XXX What is the dbTableId field in workbench for? ExportedFromTableName may not even be necessary. Based on use of dbTableId in recordset
        //it looks like it was designed to for exported records...
        Integer updateTblId = isUpdateUpload()
                ? DBTableIdMgr.getInstance().getIdByClassName(theWb.getExportedFromTableName())
                : null;
        setUpdateTableId(updateTblId);
        if (currentOp != SUCCESS_PARTIAL) {
            for (UploadTable t : uploadTables) {
                t.setTblSession(theSession);
                t.setMatchRecordId(t.isMatchRecordId()
                        || (updateTblId != null && updateTblId == t.getTable().getTableInfo().getTableId()));
                if (updateTblId != null && updateTblId == CollectionObject.getClassTableId()
                        && t.getTable().getTableInfo().getTableId() == CollectingEvent.getClassTableId()
                        && AppContextMgr.getInstance().getClassObject(Collection.class)
                                .getIsEmbeddedCollectingEvent()) {
                    t.setMatchRecordId(true);
                }
                t.setUpdateMatches(isUpdateUpload());
                t.prepareToUpload();
            }
            newAttachments.clear();
            // But may want option to ONLY upload rows that were skipped...
            skippedRows.clear();
            messages.clear();
            uploadStartRow = 0;
        } else {
            uploadStartRow = rowUploading;
            if (theWb.getRow(rowUploading).getUploadStatus() == WorkbenchRow.UPLD_SUCCESS) {
                uploadStartRow++; //never should be more than row behind.
            }
        }
        if (uploadedObjectViewer != null) {
            uploadedObjectViewer.closeView();
        }
        newMessages.clear();
    }

    /**
     * @param opName
     * 
     * Puts UI into correct state for current upload phase.
     */
    public synchronized void setCurrentOp(final String opName) {
        previousOp = currentOp;
        currentOp = opName;
        if (mainPanel == null) {
            log.error("UI does not exist.");
            return;
        }
        setupUI(currentOp);
        setOpKiller(null);
    }

    protected synchronized void setOpKiller(final Exception killer) {
        opKiller = killer;
    }

    protected synchronized Exception getOpKiller() {
        return opKiller;
    }

    protected boolean indeterminateProgress = false;
    protected boolean useAppStatBar = false;
    protected int maxProgVal = 0;
    protected int minProgVal = 0;

    /**
     * @param min
     * @param max
     * @param paintString - true if the progress bar should display string description of progress
     * @param itemName - string description will be: "itemName x of max" (using English resource).
     * 
     * Initializes progress bar for upload actions. If min and max = 0, sets progress bar is
     * indeterminate.
     */
    protected void initProgressBar(final int min, final int max, final boolean paintString, final String itemName,
            final boolean useAppProgress) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (!useAppStatBar && mainPanel == null) {
                    log.error("UI does not exist.");
                    return;
                }
                minProgVal = min;
                maxProgVal = max;
                indeterminateProgress = minProgVal == 0 && maxProgVal == 0;
                useAppStatBar = useAppProgress;
                if (useAppStatBar) {
                    if (indeterminateProgress) {
                        UIRegistry.getStatusBar().setIndeterminate("UPLOADER", indeterminateProgress);
                    } else {
                        UIRegistry.getStatusBar().setProgressRange("UPLOADER", minProgVal, maxProgVal);
                    }
                } else {
                    JProgressBar pb = mainPanel.getCurrOpProgress();
                    pb.setVisible(true);
                    if (indeterminateProgress) {
                        pb.setIndeterminate(true);
                        pb.setString("");
                    } else {
                        if (pb.isIndeterminate()) {
                            pb.setIndeterminate(false);
                        }
                        pb.setStringPainted(paintString);
                        if (paintString) {
                            pb.setName(itemName);
                        }
                        pb.setMinimum(minProgVal);
                        pb.setMaximum(maxProgVal);
                        pb.setValue(minProgVal);
                    }
                }
            }
        });
    }

    /**
     * @param val
     * 
     * Sets progress bar progress.
     */
    protected void setCurrentOpProgress(final int val) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {

                if (mainPanel == null && !useAppStatBar) {
                    log.error("UI does not exist.");
                    return;
                }
                if (!indeterminateProgress) {
                    if (useAppStatBar && !indeterminateProgress) {
                        if (val == -1) {
                            UIRegistry.getStatusBar().incrementValue("UPLOADER");
                        } else {
                            UIRegistry.getStatusBar().setValue("UPLOADER", val);
                        }
                    } else {
                        JProgressBar pb = mainPanel.getCurrOpProgress();
                        int newVal = val == -1 ? Math.min(pb.getValue() + 1, pb.getMaximum()) : val;
                        pb.setValue(newVal);
                        if (pb.isStringPainted()) {
                            pb.setString(String.format(getResourceString("WB_UPLOAD_PROGRESSBAR_TEXT"),
                                    new Object[] { pb.getName(), Integer.toString(newVal),
                                            Integer.toString(pb.getMaximum()) }));
                        }
                    }
                }
            }
        });
    }

    protected void showUploadProgress(final int val) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (mainPanel == null) {
                    log.error("UI does not exist.");
                    return;
                }
                setCurrentOpProgress(val);
                synchronized (Uploader.this) {
                    for (UploadMessage newMsg : newMessages) {
                        mainPanel.addMsg(newMsg);
                        messages.add(newMsg);
                    }
                    newMessages.clear();
                }
            }
        });
    }

    public void undoStep() {
        setCurrentOpProgress(-1);
    }

    protected void updateObjectsCreated() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                mainPanel.updateObjectsCreated();
            }
        });
    }

    /**
     * @param initOp
     * 
     * builds upload ui with initial phase of initOp
     */
    protected void initUI(final String initOp) {
        buildMainUI();
        setCurrentOp(initOp);
    }

    public void startUI() {
        if (mainPanel == null) {
            log.error("Upload ui is null");
            return;
        }
        mainPanel.clearMsgs(new Class<?>[] { BaseUploadMessage.class });
        setCurrentOp(Uploader.INITIAL_STATE);
    }

    /**
     * Gets default values for all missing required classes (foreign keys) and local fields.
     */
    public void getDefaultsForMissingRequirements() {
        setOpKiller(null);
        final UploaderTask uploadTask = new UploaderTask(false, "") {
            boolean success = false;

            @Override
            public Object doInBackground() {
                start();
                try {
                    missingRequiredClasses.clear();
                    missingRequiredFields.clear();
                    Iterator<RelatedClassSetter> rces;
                    Iterator<DefaultFieldEntry> dfes;
                    for (UploadTable t : uploadTables) {
                        try {
                            rces = t.getRelatedClassDefaults();
                        } catch (ClassNotFoundException ex) {
                            log.error(ex);
                            return null;
                        }
                        while (rces.hasNext()) {
                            missingRequiredClasses.add(rces.next());
                        }

                        try {
                            dfes = t.getMissingRequiredFlds();
                        } catch (NoSuchMethodException ex) {
                            log.error(ex);
                            return null;
                        }
                        while (dfes.hasNext()) {
                            missingRequiredFields.add(dfes.next());
                        }
                        success = true;
                    }
                    resolver = new MissingDataResolver(missingRequiredClasses, missingRequiredFields);
                    return null;
                } catch (Exception ex) {
                    setOpKiller(ex);
                    return false;
                }
            }

            @Override
            public void done() {
                super.done();
                statusBar.setText("");
                if (success) {
                    mainPanel.addMsg(new BaseUploadMessage(getResourceString("WB_REQUIRED_RETRIEVED")));
                } else {
                    mainPanel.addMsg(new BaseUploadMessage(getResourceString("WB_REQUIRED_RETRIEVED")));
                    setCurrentOp(Uploader.FAILURE);
                }
            }

        };

        UIRegistry.getStatusBar().setText(getResourceString("WB_UPLOAD_CHECKING_REQS"));
        uploadTask.execute();
        initUI(Uploader.CHECKING_REQS);
    }

    /**
     * Called when dataset is saved.
     */
    public void refresh() {
        //theWb.forceLoad();
        if (currentOp.equals(USER_INPUT)) {
            uploadData.refresh(theWb.getWorkbenchRowsAsList());
            validateData(true);
        }
    }

    /**
     * Called when dataset is closing. If shutter is not the WorkbenchPane for the dataSet
     * being uploaded, the call is ignored.
     * 
     * @param shutter
     * 
     * @return true if closed, else false.
     */
    public boolean closing(final WorkbenchPaneSS shutter) {
        if (shutter != wbSS) {
            return false;
        }
        if (mainPanel != null) {
            closeMainForm(false);
        }
        return true;
    }

    /**
     * @return count of total number of objects uploaded
     */
    protected Integer getUploadedObjects() {
        Integer result = 0;
        for (UploadTable ut : uploadTables) {
            //NOTE the uploadedRecs structure in UploadTable is cleared at the beginning up each upload,
            //after an undo, uploadedRecs will still contain the records that were 'undone'.
            result += ut.getUploadedRecs().size();
        }
        return result;
    }

    /**
     * Shuts down upload UI.
     * @param notifyWB - If true, notify this Uploader's WorkBench.
     */
    public void closeMainForm(boolean notifyWB) {
        try {
            logDebug("closing main form");
            mainPanel.setVisible(false);
            mainPanel = null;
            closeUploadedDataViewers();

            for (Component c : keyListeningTo) {
                logDebug("removing key listener");
                c.removeKeyListener(this);
            }
            keyListeningTo.clear();

            if (notifyWB) {
                wbSS.uploadDone();
            }
        } finally {
            currentUpload = null;
        }
    }

    public UploadMainPanel getMainPanel() {
        if (mainPanel == null) {
            initUI(INITIAL_STATE);
        }
        return mainPanel;
    }

    /**
     * Closes views of uploaded data.
     */
    protected void closeUploadedDataViewers() {
        if (bogusViewer != null) {
            bogusViewer.closeViewers();
            bogusViewer = null;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     * 
     * Responds to user actions in UI.
     */
    public void actionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals(UploadMainPanel.VALIDATE_CONTENT)) {
            validateData(true);
        } else if (e.getActionCommand().equals(UploadMainPanel.DO_UPLOAD)) {
            if (isUpdateUpload()) {
                if (!isValidUpdateUpload()) {
                    UIRegistry.showError("This dataset contains mappings which are not updateable");
                    return;
                }
                if (!UIRegistry.displayConfirm("Ready to Update",
                        "This update cannot be undone. Are you sure you want to continue?", "Yes", "No",
                        JOptionPane.WARNING_MESSAGE)) {
                    return;
                }
            }
            uploadIt(true);
        } else if (e.getActionCommand().equals(UploadMainPanel.VIEW_UPLOAD)) {
            if (currentOp.equals(Uploader.SUCCESS) || currentOp.equals(Uploader.SUCCESS_PARTIAL)) {
                viewAllObjectsCreatedByUpload();
            }
        } else if (e.getActionCommand().equals(UploadMainPanel.VIEW_SETTINGS)) {
            showSettings();
            if (currentOp.equals(Uploader.READY_TO_UPLOAD) && !resolver.isResolved()) {
                setCurrentOp(Uploader.USER_INPUT);
            } else if (currentOp.equals(Uploader.USER_INPUT) && resolver.isResolved()) {
                setCurrentOp(Uploader.READY_TO_UPLOAD);
            }
        } else if (e.getActionCommand().equals(UploadMainPanel.CLOSE_UI)) {
            if (aboutToShutdown(null)) {
                closeMainForm(true);
            }
        } else if (e.getActionCommand().equals(UploadMainPanel.UNDO_UPLOAD)) {
            if (UIRegistry.displayConfirm(getResourceString("WB_UPLOAD_FORM_TITLE"),
                    getResourceString("WB_UNDO_UPLOAD_MSG"), getResourceString("OK"), getResourceString("CANCEL"),
                    JOptionPane.QUESTION_MESSAGE)) {
                undoUpload(true, false, true);
            }
        } else if (e.getActionCommand().equals(UploadMainPanel.TBL_DBL_CLICK)) {
            mainPanel.getViewUploadBtn().setEnabled(canViewUpload(currentOp));
            if (currentOp.equals(Uploader.SUCCESS) || currentOp.equals(Uploader.SUCCESS_PARTIAL)) {
                viewAllObjectsCreatedByUpload();
            }
        } else if (e.getActionCommand().equals(UploadMainPanel.TBL_CLICK)) {
            mainPanel.getViewUploadBtn().setEnabled(canViewUpload(currentOp));
        } else if (e.getActionCommand().equals(UploadMainPanel.MSG_CLICK)) {
            goToMsgWBCell((Component) e.getSource(), false);
        } else if (e.getActionCommand().equals(UploadMainPanel.PRINT_INVALID)) {
            printInvalidValReport();
        } else if (e.getActionCommand().equals(UploadMainPanel.CANCEL_OPERATION)) {
            if (currentTask != null && currentTask.isCancellable()) {
                cancelTask(currentTask);
            } else {
                log.info("ignoring action: " + e.getActionCommand());
            }
        } else
            log.error("Unrecognized action: " + e.getActionCommand());
    }

    /**
     * Called when the WorkbenchPaneSS for the uploaded dataset is shutting down, and when
     * the Upload UI 'Close' button is clicked.
     * 
     * @param shuttingDownSS - the dataset that is shutting down.
     * @return true if the Uploader can be closed, otherwise false.
     */
    public boolean aboutToShutdown(final WorkbenchPaneSS shuttingDownSS) {
        if (shuttingDownSS != null && shuttingDownSS != wbSS) {
            return true;
        }
        if (currentTask != null || (shuttingDownSS != null
                && (currentOp.equals(Uploader.SUCCESS) || currentOp.equals(Uploader.SUCCESS_PARTIAL))
                && getUploadedObjects() > 0)) {
            JOptionPane.showMessageDialog(UIRegistry.getTopWindow(),
                    getResourceString("WB_UPLOAD_BUSY_CANNOT_CLOSE"));
            return false;
        }

        boolean result = true;

        if (uploadedObjectViewer != null) {
            uploadedObjectViewer.closeView();
        }

        if (result && shuttingDownSS == null
                && (currentOp.equals(Uploader.SUCCESS) || currentOp.equals(Uploader.SUCCESS_PARTIAL))
                && getUploadedObjects() > 0 && !isUpdateUpload()) {
            result = false;
            String msg = String.format(getResourceString("WB_UPLOAD_CONFIRM_SAVE"), theWb.getName());
            JFrame topFrame = (JFrame) UIRegistry.getTopWindow();
            int rv = JOptionPane.showConfirmDialog(topFrame, msg, getResourceString("WB_UPLOAD_FORM_TITLE"),
                    JOptionPane.YES_NO_CANCEL_OPTION);

            if (rv == JOptionPane.YES_OPTION) {
                saveRecordSets();
                result = true;
                wbSS.saveObject();
            } else if (rv == JOptionPane.NO_OPTION) {
                undoUpload(shuttingDownSS == null, true, true);
                result = true;
            }
            //else rv equals JOptionPane.CANCEL_OPTION or CLOSED_OPTION
            if (result) {
                for (UploadTable ut : uploadTables) {
                    try {
                        ut.shutdown();
                    } catch (UploaderException ex) {
                        edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
                        edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(Uploader.class, ex);
                        throw new RuntimeException(ex);
                    }
                }
            }
        }
        if (additionalLocksSet) {
            freeAdditionalLocks();
        }
        return result;
    }

    protected void showSettings() {
        boolean readOnly = !(currentOp.equals(Uploader.READY_TO_UPLOAD)
                || currentOp.equals(Uploader.SUCCESS_PARTIAL)) && !currentOp.equals(Uploader.USER_INPUT);
        UploadSettingsPanel usp = new UploadSettingsPanel(uploadTables);
        usp.buildUI(resolver, readOnly);
        CustomDialog cwin;
        if (!readOnly) {
            cwin = new CustomDialog((Frame) UIRegistry.getTopWindow(), getResourceString("WB_UPLOAD_SETTINGS"),
                    true, usp);
        } else {
            cwin = new CustomDialog((Frame) UIRegistry.getTopWindow(), getResourceString("WB_UPLOAD_SETTINGS"),
                    true, CustomDialog.OK_BTN, usp, CustomDialog.OK_BTN);
        }

        cwin.setModal(true);
        UIHelper.centerAndShow(cwin);
        if (!cwin.isCancelled()) {
            usp.getMatchPanel().apply();
        }
        cwin.dispose();
    }

    /**
     * @author timbo
     * 
     * @code_status Alpha
     * 
     * Datasource for printing validation issues for current upload.
     * 
     */
    class InvalidValueJRDataSource implements JRDataSource {
        protected int rowIndex;
        protected final Vector<UploadTableInvalidValue> rows;

        public InvalidValueJRDataSource(final Vector<UploadTableInvalidValue> rows) {
            this.rows = rows;
            rowIndex = -1;
        }

        /*
         * (non-Javadoc)
         * 
         * @see net.sf.jasperreports.engine.JRDataSource#getFieldValue(net.sf.jasperreports.engine.JRField)
         */
        public Object getFieldValue(final JRField field) throws JRException {
            if (field.getName().equals("row")) {
                return String.valueOf(rows.get(rowIndex).getRow());
            }
            if (field.getName().equals("col")) {
                return String.valueOf(rows.get(rowIndex).getUploadFld().getWbFldName());
            }
            if (field.getName().equals("description")) {
                return rows.get(rowIndex).getDescription();
            }
            if (field.getName().equals("datasetName")) {
                return theWb.getName();
            }
            if (field.getName().equals("cellData")) {
                return uploadData.get(rows.get(rowIndex).getRow(), rows.get(rowIndex).getUploadFld().getIndex());
            }
            log.error("Unrecognized field Name: " + field.getName());
            return null;
        }

        /*
         * (non-Javadoc)
         * 
         * @see net.sf.jasperreports.engine.JRDataSource#next()
         */
        public boolean next() throws JRException {
            if (rowIndex >= rows.size() - 1) {
                return false;
            }
            rowIndex++;
            return true;
        }
    }

    /**
     * Launches ReportTask to display report of validation issues.
     */
    protected void printInvalidValReport() {
        if (validationIssues == null || validationIssues.size() == 0) {
            // this should never be called but just in case.
            log.error("no validationIssues");
            return;
        }
        InvalidValueJRDataSource src = new InvalidValueJRDataSource(validationIssues);
        final CommandAction cmd = new CommandAction(ReportsBaseTask.REPORTS, ReportsBaseTask.PRINT_REPORT, src);
        cmd.setProperty("title", "Validation Issues");
        cmd.setProperty("file", "upload_problem_report.jrxml");
        CommandDispatcher.dispatch(cmd);
    }

    /**
     * Moves to dataset cell corresponding to currently selected validation issue and starts editor.
     */
    protected void goToMsgWBCell(final Component c, boolean stopEdit) {
        if (mainPanel == null) {
            throw new RuntimeException("Upload form does not exist.");
        }
        if (wbSS != null) {
            UploadMessage msg;
            if (c == mainPanel.getValidationErrorList()) {
                msg = (UploadMessage) mainPanel.getValidationErrorList().getSelectedValue();
            } else {
                msg = (UploadMessage) mainPanel.getMsgList().getSelectedValue();
            }

            if (msg == null) {
                logDebug("gotToMsgWBCell: null message");
                return;
            }

            if (msg.getRow() != -1) {
                if (msg.getCol() == -1) {
                    wbSS.getSpreadSheet().scrollToRow(msg.getRow());
                    wbSS.getSpreadSheet().getSelectionModel().clearSelection();
                    wbSS.getSpreadSheet().getSelectionModel().setSelectionInterval(msg.getRow(), msg.getRow());
                } else {
                    wbSS.getSpreadSheet().getSelectionModel().clearSelection();
                    Rectangle rect = wbSS.getSpreadSheet().getCellRect(msg.getRow(), msg.getCol(), false);
                    wbSS.getSpreadSheet().scrollRectToVisible(rect);
                    if (msg instanceof UploadTableInvalidValue && msg.getCol() != -1) {
                        if (!stopEdit) {

                            wbSS.getSpreadSheet().editCellAt(msg.getRow(), msg.getCol(), null);

                            // Now, if necessary, add this as a listener to the editorComponent to
                            // allow moving to
                            // next/prev
                            // invalid cell after ENTER/TAB/UP/DOWN
                            Component editor = wbSS.getSpreadSheet().getEditorComponent();
                            boolean addListener = true;
                            if (editor != null) {
                                KeyListener[] listeners = editor.getKeyListeners();
                                for (int k = 0; k < listeners.length; k++) {
                                    if (listeners[k] instanceof Uploader) {
                                        if (listeners[k] == this) {
                                            logDebug("already listening to spreadsheet editor");
                                            addListener = false;
                                            break;
                                        }
                                        // should never get here, but just in case:
                                        logDebug("removing previous listener");
                                        editor.removeKeyListener(listeners[k]);
                                    }
                                }
                                if (addListener) {
                                    logDebug("adding this as listener to spreadsheet editor");
                                    editor.addKeyListener(this);
                                    this.keyListeningTo.add(editor);
                                }
                                editor.requestFocusInWindow();
                            }
                        } else {
                            if (wbSS.getSpreadSheet().getCellEditor() != null) {
                                wbSS.getSpreadSheet().getCellEditor().stopCellEditing();
                            }
                        }
                    }
                }
            }
        }
        if (mainPanel == null) {
            throw new RuntimeException("Upload form does not exist.");
        }
    }

    /**
     * Moves to and begins editing the next invalid WorkBench cell.
     * 
     * Not complete. Needs to limit selections to UploadTableInvalidValue objects.
     * 
     * See note in goToMsgWBCell re addKeyListener().
     */
    protected void goToNextInvalidCell() {
        logDebug("goToNextInvalidCell");
        int sel = mainPanel.getValidationErrorList().getSelectedIndex() + 1;
        if (sel >= mainPanel.getValidationErrorList().getModel().getSize())
            sel = 1; // first msg is explanatory
        if (sel != -1) {
            boolean stopEditing = sel == mainPanel.getValidationErrorList().getSelectedIndex();
            mainPanel.getValidationErrorList().setSelectedIndex(sel);
            logDebug("Going to msg " + sel);
            goToMsgWBCell(mainPanel.getValidationErrorList(), stopEditing);
        }
    }

    /**
     * Moves to and begins editing the previous invalid WorkBench cell.
     *
     * Not complete. Needs to limit selections to UploadTableInvalidValue objects.
     * 
     * See note in goToMsgWBCell re addKeyListener().
    */
    protected void goToPrevInvalidCell() {
        int sel = mainPanel.getValidationErrorList().getSelectedIndex() - 1;
        if (sel < 1) //first msg is explanatory
            sel = mainPanel.getValidationErrorList().getModel().getSize() - 1;
        if (sel != -1) {
            boolean stopEdit = sel == mainPanel.getValidationErrorList().getSelectedIndex();
            mainPanel.getValidationErrorList().setSelectedIndex(sel);
            goToMsgWBCell(mainPanel.getValidationErrorList(), stopEdit);
        }
    }

    /**
     * Moves to and begins editing the first invalid WorkBench cell.
     *
     * Not complete. Needs to limit selections to UploadTableInvalidValue objects.
     * 
     * See note in goToMsgWBCell re addKeyListener().
     */
    protected void goToFirstInvalidCell() {
        if (mainPanel.getValidationErrorList().getModel().getSize() > 0) {
            mainPanel.getValidationErrorList().setSelectedIndex(0);
            goToMsgWBCell(mainPanel.getValidationErrorList(), false);
        }
    }

    /**
     * Moves to and begins editing the last invalid WorkBench cell.
     *
     * Not complete. Needs to limit selections to UploadTableInvalidValue objects.
     * 
     * See note in goToMsgWBCell re addKeyListener().
     */
    protected void goToLastInvalidCell() {
        if (mainPanel.getValidationErrorList().getModel().getSize() > 0) {
            mainPanel.getValidationErrorList()
                    .setSelectedIndex(mainPanel.getValidationErrorList().getModel().getSize() - 1);
            goToMsgWBCell(mainPanel.getValidationErrorList(), false);
        }
    }

    /**
     * Builds form for upload UI.
     */
    protected void buildMainUI() {
        mainPanel = new UploadMainPanel(isUpdateUpload());

        SortedSet<UploadInfoRenderable> uts = new TreeSet<UploadInfoRenderable>();
        for (UploadTable ut : uploadTables) {
            UploadInfoRenderable render = new UploadInfoRenderable(ut);
            if (uts.contains(render)) {
                for (UploadInfoRenderable r : uts) {
                    if (r.equals(render)) {
                        r.addTable(ut);
                        break;
                    }
                }
            } else {
                uts.add(new UploadInfoRenderable(ut));
            }
        }
        mainPanel.addAffectedTables(uts.iterator());
        mainPanel.setActionListener(this);
    }

    //XXX debugging junk!
    int thirdTime = 0;

    /**
     * @param op
     * 
     * Sets up mainPanel for upload phase for op.
     */
    protected void setupUI(final String op) {
        //        SwingUtilities.invokeLater(new Runnable() {
        //           public void run()
        //           {
        if (mainPanel == null) {
            log.error("UI does not exist.");
            return;
        }

        if (op.equals(Uploader.RETRIEVING_UPLOADED_DATA)) {
            //There's really nothing to do in this case anymore
            return;
        }

        int uploadedObjects = getUploadedObjects();

        if (op.equals(Uploader.SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL)) {
            if (mainPanel.getUploadTbls().getSelectedIndex() == -1) {
                // assuming list is not empty
                mainPanel.getUploadTbls().setSelectedIndex(0);
            }
        }

        if (op.equals(Uploader.SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL)) {
            if (uploadedObjects > 0 && !isUpdateUpload()) {
                mainPanel.closeBtn.setText(getResourceString("WB_UPLOAD.COMMIT"));
            } else {
                mainPanel.closeBtn.setText(getResourceString("CLOSE"));
            }
        }

        if (op.equals(Uploader.READY_TO_UPLOAD)) {
            mainPanel.closeBtn.setText(getResourceString("CLOSE"));
        }

        if (op.equals(UPLOADING) || op.equals(SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL)) {
            mainPanel.showUploadTblTbl();
        } else if (uploadedObjects == 0 || op.equals(Uploader.READY_TO_UPLOAD)) {
            mainPanel.showUploadTblList();
        }

        mainPanel.getValidateContentBtn().setEnabled(canValidateContent(op));

        mainPanel.getCancelBtn().setEnabled(canCancel(op));
        if (mainPanel.getCancelBtn().isEnabled()) {
            if (op.equals(UPLOADING)) {
                mainPanel.getCancelBtn().setText(getResourceString("WB_UPLOAD_PAUSE"));
            } else {
                mainPanel.getCancelBtn().setText(getResourceString("WB_UPLOAD_CANCEL"));
            }
        }

        mainPanel.getCancelBtn().setVisible(mainPanel.getCancelBtn().isEnabled());

        mainPanel.getDoUploadBtn().setEnabled(canUpload(op));

        mainPanel.getViewSettingsBtn().setEnabled(canViewSettings(op));

        mainPanel.getViewUploadBtn().setEnabled(canViewUpload(op) && uploadedObjects > 0);
        mainPanel.getViewUploadBtn().setVisible(mainPanel.getViewUploadBtn().isEnabled());

        mainPanel.getUndoBtn().setEnabled(canUndo(op) && uploadedObjects > 0);
        mainPanel.getUndoBtn().setVisible(mainPanel.getUndoBtn().isEnabled());

        mainPanel.getCloseBtn().setEnabled(canClose(op));

        mainPanel.getCurrOpProgress().setVisible(mainPanel.getCancelBtn().isVisible());

        String statText;
        if (previousOp != null && previousOp.equals(UNDOING_UPLOAD) && op.equals(FAILURE)) {
            statText = getResourceString("WB_UPLOAD_UNDO_FAILURE");
        } else {
            statText = getResourceString(op);
        }
        Exception killer = getOpKiller();
        if (op.equals(Uploader.SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL)) {
            statText += ". " + getUploadedObjects().toString() + " " + getResourceString("WB_UPLOAD_OBJECT_COUNT")
                    + ".";
            if (killer != null) {
                logDebug("Hey. Wait a minute. The operation succeeded while dead. Is that not creepy?");
            }
        } else if (op.equals(Uploader.FAILURE) && killer != null) {
            if (StringUtils.isNotEmpty(killer.getLocalizedMessage())) {
                statText += ": " + killer;
            } else {
                statText += ": " + killer.getClass().getName();
            }
        }
        if (dotDotDot(op)) {
            statText += "...";
        }

        mainPanel.clearMsgs(new Class<?>[] { UploadTableInvalidValue.class });
        if (op.equals(USER_INPUT)) {
            mainPanel.addMsg(new UploadTableInvalidValue(statText, null, -1, null));
        } else {
            mainPanel.addMsg(new BaseUploadMessage(statText));
        }

        if (validationIssues != null) {
            for (int m = 0; m < validationIssues.size() && m < MAX_MSG_DISPLAY_COUNT; m++) {
                mainPanel.addMsg(validationIssues.get(m));
            }
            if (validationIssues.size() > MAX_MSG_DISPLAY_COUNT) {
                log.info("Only displaying " + String.valueOf(MAX_MSG_DISPLAY_COUNT) + " of "
                        + String.valueOf(validationIssues.size()) + " validation errors ");
                thirdTime = thirdTime + 1;
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        JOptionPane.showMessageDialog(UIRegistry.getTopWindow(),
                                String.format(getResourceString(WB_TOO_MANY_ERRORS),
                                        String.valueOf(MAX_MSG_DISPLAY_COUNT),
                                        String.valueOf(validationIssues.size())),
                                getResourceString(WB_UPLOAD_FORM_TITLE), JOptionPane.WARNING_MESSAGE, null);
                    }
                });
            }
        }
        mainPanel.getPrintBtn().setEnabled(validationIssues != null && validationIssues.size() > 0);
        //           }
        //        });
    }

    /**
     * Opens view of uploaded (newly created) objects for all tables. 
     * NOTE: Currently does not include attachments.
     */
    protected void viewAllObjectsCreatedByUpload() {
        if (currentOp.equals(Uploader.SUCCESS) || currentOp.equals(Uploader.SUCCESS_PARTIAL)
                || currentOp.equals(Uploader.RETRIEVING_UPLOADED_DATA)) {
            viewUploadsAll();
        }
    }

    protected void viewUploadsAll() {
        if (uploadedObjectViewer == null) {
            uploadedObjectViewer = new UploadRetriever();
        }
        uploadedObjectViewer.viewUploads(uploadTables, wbSS.getTask(),
                getResourceString(WB_UPLOAD_VIEW_RESULTS_TITLE));
    }

    protected void viewUpload(final UploadTable uploadTable) {
        List<UploadTable> lst = new LinkedList<UploadTable>();
        lst.add(uploadTable);
        new UploadRetriever().viewUploads(lst, wbSS.getTask(), getResourceString(WB_UPLOAD_VIEW_RESULTS_TITLE));
    }

    protected boolean dotDotDot(final String op) {
        return op.equals(Uploader.UPLOADING) || op.equals(Uploader.CHECKING_REQS) || op.equals(Uploader.CLEANING_UP)
                || op.equals(Uploader.RETRIEVING_UPLOADED_DATA) || op.equals(Uploader.VALIDATING_DATA)
                || op.equals(Uploader.USER_INPUT) || op.equals(Uploader.UNDOING_UPLOAD);
    }

    /**
     * @param op
     * @return true if canUndo in phase op.
     */
    protected boolean canUndo(final String op) {
        return (op.equals(Uploader.SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL)) && !isUpdateUpload(); //can't currently undo update uploads
    }

    /**
     * @param op
     * @return true if canCancel in phase op.
     */
    protected boolean canCancel(final String op) {
        return op.equals(Uploader.UPLOADING) || op.equals(Uploader.CHECKING_REQS)
                || op.equals(Uploader.VALIDATING_DATA) || op.equals(Uploader.RETRIEVING_UPLOADED_DATA);
    }

    /**
     * @param op
     * @return true if canUpload in phase op.
     */
    protected boolean canUpload(final String op) {
        return op.equals(Uploader.READY_TO_UPLOAD) || op.equals(Uploader.SUCCESS_PARTIAL);
    }

    /**
     * @param op
     * @return true if canClose in phase op.
     */
    protected boolean canValidateContent(final String op) {
        return /*op.equals(Uploader.USER_INPUT) ||*/ op.equals(Uploader.INITIAL_STATE)
                || op.equals(Uploader.FAILURE);
    }

    /**
     * @param op
     * @return true if Close button is applicable in phase op.
     */
    protected boolean canClose(final String op) {
        return op.equals(Uploader.READY_TO_UPLOAD) || op.equals(Uploader.USER_INPUT) || op.equals(Uploader.SUCCESS)
                || op.equals(Uploader.SUCCESS_PARTIAL) || op.equals(Uploader.INITIAL_STATE)
                || op.equals(Uploader.FAILURE);
    }

    /**
     * @param op
     * @return true if canViewSettings in phase op.
     */
    protected boolean canViewSettings(final String op) {
        return op.equals(Uploader.READY_TO_UPLOAD) || op.equals(Uploader.USER_INPUT)
                || op.equals(Uploader.SUCCESS_PARTIAL)
                // || op.equals(Uploader.SUCCESS)
                // || op.equals(Uploader.INITIAL_STATE)
                || op.equals(Uploader.FAILURE);
    }

    /**
     * @return all tables whose ParentTableEntries contain parent
     */
    public List<UploadTable> getChildren(final UploadTable parent) {
        Vector<UploadTable> result = new Vector<UploadTable>();
        for (UploadTable ut : uploadTables) {
            //Assuming this method is called by UploadTable created by this Uploader
            //so pointer comparisons are OK.
            if (ut != parent) {
                for (Vector<ParentTableEntry> ptes : ut.getParentTables()) {
                    boolean broke = false;
                    for (ParentTableEntry pte : ptes) {
                        if (pte.getImportTable() == parent) {
                            result.add(ut);
                            broke = true;
                            break;
                        }
                    }
                    if (broke) {
                        break;
                    }
                }
            }
        }
        return result;
    }

    /**
     * @param op
     * @return true if canViewUpload in phase op.
     */
    protected boolean canViewUpload(final String op) {
        return (op.equals(Uploader.SUCCESS) || op.equals(Uploader.SUCCESS_PARTIAL))
                && mainPanel.getUploadTbls().getSelectedIndex() != -1;
    }

    /**
     * Uploads dataset.
     */
    public void uploadIt(boolean doInBackground) {
        try {
            buildIdentifier();
            setOpKiller(null);
            prepareToUpload(null);

            final UploaderTask uploadTask = new UploaderTask(true, "WB_CANCEL_UPLOAD_MSG") {
                boolean success = false;
                boolean paused = false;
                Integer updateTblId = null;
                UploadTable exportedTable = null;

                /**
                 * @throws UploaderException
                 */
                protected void setupExportedTable() throws UploaderException {
                    if (updateTblId != null) {
                        for (UploadTable ut : uploadTables) {
                            if (ut.getTable().getTableInfo().getTableId() == updateTblId) {
                                if (exportedTable == null) {
                                    exportedTable = ut;
                                } else {
                                    throw new UploaderException("Unable to determine base exported table",
                                            UploaderException.ABORT_IMPORT);
                                }
                            }
                        }
                    }
                }

                /**
                 * 
                 */
                protected void setExportedRecordIds() throws Exception {
                    DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();
                    Class<?> cls = DBTableIdMgr.getInstance().getInfoById(updateTblId).getClassObj();
                    try {
                        DataModelObjBase obj = (DataModelObjBase) session.get(cls, getRowRecordId(rowUploading));
                        if (obj != null) {
                            obj.forceLoad();
                        }
                        exportedTable.setExportedRecordId(obj);
                    } finally {
                        session.close();
                    }

                }

                @SuppressWarnings("synthetic-access")
                @Override
                public Object doInBackground() {
                    start();
                    initProgressBar(0, uploadData.getRows(), true,
                            getResourceString("WB_UPLOAD_UPLOADING") + " " + getResourceString("WB_ROW"), false);
                    try {
                        updateTblId = getUpdateTableId();
                        setupExportedTable();
                        for (rowUploading = uploadStartRow; rowUploading < uploadData.getRows();) {
                            boolean rowAborted = false;
                            if (cancelled) {
                                paused = true;
                                break;
                            }
                            logDebug("uploading row " + String.valueOf(rowUploading));

                            if (rowUploading == 0) {
                                showUploadProgress(1);
                            }

                            if (!uploadData.isEmptyRow(rowUploading)) {
                                imagesForRow.clear();
                                if (updateTblId != null) {
                                    setExportedRecordIds();
                                }
                                imagesForRow.addAll(uploadData.getWbRow(rowUploading).getWorkbenchRowImages());
                                for (UploadTable t : uploadTables) {
                                    if (cancelled) {
                                        break;
                                    }
                                    try {
                                        if (theWb.getRow(rowUploading)
                                                .getUploadStatus() != WorkbenchRow.UPLD_SUCCESS) {
                                            uploadRow(t, rowUploading);
                                        } else {
                                            throw new UploaderException(
                                                    getResourceString("WB_UPLOAD_ROW_ALREADY_UPLOADED"),
                                                    UploaderException.ABORT_ROW);
                                        }
                                    } catch (UploaderException ex) {
                                        if (ex.getStatus() == UploaderException.ABORT_ROW) {
                                            logDebug(ex.getMessage());
                                            abortRow(ex, rowUploading);
                                            rowAborted = true;
                                            break;
                                        }
                                        throw ex;
                                    }
                                    updateObjectsCreated();
                                }
                            }

                            if (!rowAborted) {
                                theWb.getRow(rowUploading).setUploadStatus(WorkbenchRow.UPLD_SUCCESS);
                            }
                            if (!cancelled) {
                                rowUploading++;
                            }
                            showUploadProgress(rowUploading);
                        }
                    } catch (Exception ex) {
                        setOpKiller(ex);
                        return false;
                    }
                    success = !cancelled || (cancelled && paused);
                    return success;
                }

                @Override
                public void done() {
                    try {
                        //for (UploadTable t : uploadTables)
                        for (int t = uploadTables.size() - 1; t >= 0; t--) {
                            uploadTables.get(t).finishUpload(cancelled && !paused, null);
                        }
                    } catch (Exception ex) {
                        success = false;
                        setOpKiller(ex);
                    }
                    super.done();
                    statusBar.setText("");
                    if (success) {
                        if (!paused) {
                            setCurrentOp(Uploader.SUCCESS);
                        } else {
                            setCurrentOp(Uploader.SUCCESS_PARTIAL);
                        }
                    } else {
                        mainPanel.clearObjectsCreated();
                        //undoUpload will clear opKiller, so save it and reassign, after call. (iffy?)
                        Exception savedOpKiller = getOpKiller();
                        undoUpload(false, false, undo);
                        setOpKiller(savedOpKiller);

                        if (!cancelled) {
                            setCurrentOp(Uploader.FAILURE);
                        }
                    }
                }

            };

            UIRegistry.getStatusBar().setText(getResourceString(Uploader.UPLOADING));
            uploadTask.execute();
            if (mainPanel == null) {
                initUI(Uploader.UPLOADING);
            } else {
                setCurrentOp(Uploader.UPLOADING);
            }

            if (!doInBackground) {
                try {
                    uploadTask.get();
                } catch (ExecutionException ex) {
                    //hopefully it will be clear to caller that something went wrong?
                } catch (InterruptedException ex) {
                    //hopefully it will be clear to caller that something went wrong?
                }
                //uploadTask.finished();
            }
        } catch (UploaderException ex) {
            setOpKiller(ex);
        }
    }

    //************************************************************************************************************************

    /**
     * @throws UploaderException
     */
    protected UploadTable setupExportedTableSansUI(Integer updateTblId) throws UploaderException {
        if (updateTblId != null) {
            for (UploadTable ut : uploadTables) {
                if (ut.getTable().getTableInfo().getTableId() == updateTblId) {
                    return ut;
                }
            }
        }
        //throw new UploaderException("Unable to determine base exported table", UploaderException.ABORT_IMPORT);
        return null;
    }

    protected void setExportedRecordIdsSansUI(Integer updateTblId, UploadTable exportedTable) throws Exception {
        DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();
        Class<?> cls = DBTableIdMgr.getInstance().getInfoById(updateTblId).getClassObj();
        try {
            DataModelObjBase obj = (DataModelObjBase) session.get(cls, getRowRecordId(rowUploading));
            if (obj != null) {
                obj.forceLoad();
            }
            exportedTable.setExportedRecordId(obj);
        } finally {
            session.close();
        }

    }

    public Pair<List<UploadTableInvalidValue>, List<Pair<Integer, List<UploadTableMatchInfo>>>> validateDataSansUI(
            boolean match) {
        List<UploadTableInvalidValue> invalids = new ArrayList<UploadTableInvalidValue>();
        List<Pair<Integer, List<UploadTableMatchInfo>>> matches = new ArrayList<Pair<Integer, List<UploadTableMatchInfo>>>();
        Pair<List<UploadTableInvalidValue>, List<Pair<Integer, List<UploadTableMatchInfo>>>> result = new Pair<List<UploadTableInvalidValue>, List<Pair<Integer, List<UploadTableMatchInfo>>>>(
                invalids, matches);
        boolean fail = false;
        try {
            for (UploadTable tbl : uploadTables) {
                tbl.clearBlankness();
            }
            for (UploadTable tbl : uploadTables) {
                invalids.addAll(validateLengths(tbl, -1, -1));
                invalids.addAll(tbl.validateValues(uploadData));
            }
            Collections.sort(invalids);
        } catch (Exception ex) {
            fail = true;
            invalids.clear();
            invalids.add(new UploadTableInvalidValue("Exception while validating. FAIL.", null, -1, ex));
        }
        if (match && invalids.size() > 0 /*if no-invalids then matching is done during upload*/ && !fail) {
            try {
                int invalidIdx = 0;
                List<UploadTableInvalidValue> invalidsForRow = new ArrayList<UploadTableInvalidValue>();
                for (int r = uploadStartRow; r < uploadData.getRows(); r++) {
                    boolean doMatch = true;
                    if (invalidIdx < invalids.size()) {
                        while (invalidIdx < invalids.size() && invalids.get(invalidIdx).getRow() < r) {
                            invalidIdx++;
                        }
                        doMatch = invalidIdx >= invalids.size() || invalids.get(invalidIdx).getRow() > r;
                    }
                    if (doMatch) {
                        List<UploadTableMatchInfo> m = matchData(r, -1, invalidsForRow);
                        if (m != null && m.size() > 0) {
                            matches.add(new Pair<Integer, List<UploadTableMatchInfo>>(r, m));
                        }
                    } else {
                        matches.add(new Pair<Integer, List<UploadTableMatchInfo>>(r, null));
                    }
                }
            } catch (Exception ex) {
                fail = true;
                invalids.clear();
                invalids.add(new UploadTableInvalidValue("Exception while validating. FAIL.", null, -1, ex));
            }
        }
        return result;
    }

    /**
     * Uploads dataset.
     */
    public boolean uploadItSansUI(boolean doCommit, boolean doMatches, String multipleMatchAction) {

        if (doMatches && doCommit) {
            System.out.println("Error: invalid arguments. doCommit must be false when doMatches is true.");
            return false;
        }

        List<UploadMessage> structureErrors = null;
        boolean success = false;
        try {
            structureErrors = verifyUploadability();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        if (structureErrors == null || structureErrors.size() > 0) {
            System.out.println("Error: dataset is not uploadable due to mapping issues");
            for (UploadMessage um : structureErrors) {
                System.out.println(um.getRow() + "," + um.getCol() + ":" + um.getMsg());
            }
            return false;
        }

        setDefaultMatchStatus();
        Pair<List<UploadTableInvalidValue>, List<Pair<Integer, List<UploadTableMatchInfo>>>> vms = validateDataSansUI(
                doMatches);
        List<UploadTableInvalidValue> invalidities = vms.getFirst();
        if (invalidities.size() != 0) {
            System.out.println("Error: dataset is not uploadable because it contains invalid cells.");
            for (UploadTableInvalidValue invalidity : invalidities) {
                for (Integer col : invalidity.getCols()) {
                    System.out
                            .println("[" + invalidity.getRow() + " [" + col + "]] " + invalidity.getDescription());
                }
                //System.out.println(invalidity.getMsg());
            }
        }
        List<Pair<Integer, List<UploadTableMatchInfo>>> matchInfos = vms.getSecond();
        if (matchInfos.size() > 0) {
            List<String> moreMsgs = new ArrayList<String>();
            for (Pair<Integer, List<UploadTableMatchInfo>> matchInfo : matchInfos) {
                if (matchInfo.getSecond() != null) {
                    for (UploadTableMatchInfo rowInfo : matchInfo.getSecond()) {
                        if (!rowInfo.isSkipped() && rowInfo.getNumberOfMatches() != 1) {
                            for (Integer col : rowInfo.getColIdxs()) {
                                System.out.println("mi[" + matchInfo.getFirst() + " [" + col + "]] "
                                        + rowInfo.getDescription());
                            }
                        }
                    }
                } else {
                    moreMsgs.add(
                            "Row " + (matchInfo.getFirst() + 1) + " was not matched because it contains errors.");
                }
            }
            for (String msg : moreMsgs) {
                System.out.println(msg);
            }
        }

        if (invalidities.size() > 0/* || doMatches*/) {
            return false;
        }

        //set non-interactive match options
        for (UploadTable t : uploadTables) {
            UploadMatchSetting matchSet = t.getMatchSetting();
            if (doMatches) {
                matchSet.setMode(UploadMatchSetting.PICK_FIRST_MODE);
            } else {
                if ("new".equalsIgnoreCase(multipleMatchAction)) {
                    matchSet.setMode(UploadMatchSetting.ADD_NEW_MODE);
                } else if ("pick".equalsIgnoreCase(multipleMatchAction)) {
                    matchSet.setMode(UploadMatchSetting.PICK_FIRST_MODE);
                } else {
                    matchSet.setMode(UploadMatchSetting.SKIP_ROW_MODE);
                }
            }
            matchSet.setRemember(true);
            matchSet.setMatchEmptyValues(true);
        }
        int toiletSize = 200;
        DataProviderSessionIFace theSession = DataProviderFactory.getInstance().createSession();
        try {
            theSession.beginTransaction();
            buildIdentifier();
            setOpKiller(null);
            prepareToUpload(theSession);
            HashMap<UploadTable, HashMap<Integer, Integer>> uploadedRecs = new HashMap<UploadTable, HashMap<Integer, Integer>>();
            for (UploadTable ut : uploadTables) {
                if (ut.isCheckMatchInfo()) {
                    uploadedRecs.put(ut, new HashMap<Integer, Integer>());
                }
            }
            int rowsSinceFlush = 0;
            try {
                Integer updateTblId = getUpdateTableId();
                UploadTable exportedTable = setupExportedTableSansUI(updateTblId);
                for (rowUploading = uploadStartRow; rowUploading < uploadData.getRows();) {
                    boolean rowAborted = false;
                    System.out.println("uploading row " + String.valueOf(rowUploading + 1));
                    if (!uploadData.isEmptyRow(rowUploading)) {
                        imagesForRow.clear();
                        if (updateTblId != null) {
                            setExportedRecordIdsSansUI(updateTblId, exportedTable);
                        }
                        imagesForRow.addAll(uploadData.getWbRow(rowUploading).getWorkbenchRowImages());
                        for (UploadTable t : uploadTables) {
                            try {
                                if (theWb.getRow(rowUploading).getUploadStatus() != WorkbenchRow.UPLD_SUCCESS) {
                                    uploadRow(t, rowUploading);
                                } else {
                                    throw new UploaderException(getResourceString("WB_UPLOAD_ROW_ALREADY_UPLOADED"),
                                            UploaderException.ABORT_ROW);
                                }
                            } catch (UploaderException ex) {
                                if (ex.getStatus() == UploaderException.ABORT_ROW) {
                                    if (!doMatches || !(ex instanceof UploaderMatchSkipException)) {
                                        ex.printStackTrace();
                                    }
                                    abortRow(ex, rowUploading);
                                    rowAborted = true;
                                    break;
                                }
                                throw ex;
                            }
                        }
                    }

                    if (!rowAborted) {
                        theWb.getRow(rowUploading).setUploadStatus(WorkbenchRow.UPLD_SUCCESS);
                        if (doMatches) {
                            doUploadSansUIMatchProcessingStuff(uploadedRecs, matchInfos);
                        } else {
                            if ("new".equalsIgnoreCase(multipleMatchAction)
                                    || "pick".equalsIgnoreCase(multipleMatchAction)) {
                                for (UploadTable t : uploadTables) {
                                    Integer[] m = t.getMatchCountForCurrentRow();
                                    for (int i = 0; i < m.length; i++) {
                                        if (m[i] > 1) {
                                            String msg = "row " + (rowUploading + 1) + ": multiple matches for " + t
                                                    + ". ";
                                            if ("new".equalsIgnoreCase(multipleMatchAction)) {
                                                msg += " A new record was created.";
                                            } else {
                                                msg += " Record " + t.getCurrentRecord(i).getId() + " was picked.";
                                            }
                                            System.out.println(msg);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    for (UploadTable t : uploadTables) {
                        t.clearRecords();
                    }
                    rowUploading++;
                    if (rowsSinceFlush++ >= toiletSize) {
                        theSession.flush();
                        theSession.clear();
                        rowsSinceFlush = 0;
                    }
                }
                success = true;
            } catch (Exception ex) {
                setOpKiller(ex);
                ex.printStackTrace();
            }
            if (success) {
                try {
                    for (int t = uploadTables.size() - 1; t >= 0; t--) {
                        uploadTables.get(t).finishUpload(false, theSession);
                    }
                } catch (Exception ex) {
                    success = false;
                    setOpKiller(ex);
                }
            }
            if (matchInfos.size() > 0) {
                for (Pair<Integer, List<UploadTableMatchInfo>> matchInfo : matchInfos) {
                    for (UploadTableMatchInfo rowInfo : matchInfo.getSecond()) {
                        if (!rowInfo.isSkipped() && rowInfo.getNumberOfMatches() != 1) {
                            for (Integer col : rowInfo.getColIdxs()) {
                                System.out.println("mi[" + matchInfo.getFirst() + " [" + col + "]] "
                                        + rowInfo.getDescription());
                            }
                        }
                    }
                }
            }
            currentTask = null;
            if (success) {
                if (doCommit) {
                    theSession.commit();
                } else {
                    theSession.rollback();
                }
                System.out.println("success");
            } else {
                Exception savedOpKiller = getOpKiller();
                theSession.rollback();
                //undoUpload(false, false, true);
                setOpKiller(savedOpKiller);
                System.out.println("failure");
            }
            ;
        } catch (Exception e) {
            e.printStackTrace();
            theSession.rollback();
        } finally {
            if (theSession != null) {
                theSession.close();
            }
        }
        return success;
    }

    protected boolean isSkippedMatchInfo(List<UploadTable> prevMatches, UploadTable t) {
        boolean isSkipped = false;
        for (UploadTable p : prevMatches) {
            if (t.getParentTableEntry(p) != null) {
                isSkipped = true;
                break;
            }
        }
        return isSkipped;
    }

    protected void addMatchInfo(List<UploadTableMatchInfo> mis, List<UploadTable> prevMatches, UploadTable t,
            int mCount, List<Integer> colIdxs) {
        mis.add(new UploadTableMatchInfo(t.getTblTitle(), mCount, colIdxs, false,
                isSkippedMatchInfo(prevMatches, t)));
        if (Treeable.class.isAssignableFrom(t.getTblClass()) || Locality.class.equals(t.getTblClass())) {
            prevMatches.add(t);
        }
    }

    protected void doUploadSansUIMatchProcessingStuff(HashMap<UploadTable, HashMap<Integer, Integer>> uploadedRecs,
            List<Pair<Integer, List<UploadTableMatchInfo>>> matchInfos) {
        List<UploadTableMatchInfo> mis = new ArrayList<UploadTableMatchInfo>();
        List<UploadTable> prevMatches = new ArrayList<UploadTable>();
        for (UploadTable t : uploadTables) {
            if (t.isCheckMatchInfo() && !t.isSkipMatching()) {
                Integer[] mCount = t.getMatchCountForCurrentRow();
                SortedSet<UploadedRecordInfo> urs = t.getUploadedRecs() == null || t.getUploadedRecs().size() == 0
                        ? new TreeSet<UploadedRecordInfo>()
                        : t.getUploadedRecs().tailSet(new UploadedRecordInfo(null, rowUploading, 0, null));
                if (urs.size() == 0) {
                    urs.add(new UploadedRecordInfo(null, rowUploading, 0, null));
                }
                for (UploadedRecordInfo ur : urs) {
                    Integer seq = ur == null ? 0 : ur.getSeq();
                    List<Integer> colIdxs = new ArrayList<Integer>();
                    for (UploadField uf : t.getUploadFields().get(seq)) {
                        if (uf.getIndex() != -1) {
                            colIdxs.add(uf.getIndex());
                        }
                    }
                    HashMap<Integer, Integer> recs = mCount[seq] > 1 ? null : uploadedRecs.get(t);
                    if ((mCount[seq] == 0 && t.getCurrentRecord(seq) != null) || mCount[seq] > 1) {
                        //a record was  added or multiple matches
                        addMatchInfo(mis, prevMatches, t, mCount[seq], colIdxs);
                        if (mCount[seq] == 0) {
                            if (recs != null && ur != null) {
                                recs.put(ur.getKey(), ur.getSeq());
                            } else {
                                System.out.println(
                                        "Error: " + t + " is not enhashed or beset for row " + rowUploading);
                            }
                        }
                    } else if (mCount[seq] == 1) {
                        //figure out if record was added earlier in the upload
                        if (recs != null && t.getCurrentRecord(seq) != null) {
                            Integer oseq = recs.get(t.getCurrentRecord(seq).getId());
                            if (oseq != null) {
                                addMatchInfo(mis, prevMatches, t, 0, colIdxs);
                            }
                        } else {
                            System.out.println("Error: " + t + " is not enhashed or beset for row " + rowUploading);
                        }
                    } else {
                        //what the hell?
                    }
                }
            }
        }
        if (mis.size() > 0) {
            matchInfos.add(new Pair<Integer, List<UploadTableMatchInfo>>(rowUploading, mis));
        }

    }

    /**
     * @param cause
     * @param row
     * @throws UploaderException
     */
    protected synchronized void abortRow(UploaderException cause, int row) throws UploaderException {
        //logDebug("NOT undoing writes which have already occurred while processing aborted row");
        logDebug("undoing writes which have already occurred while processing aborted row");
        try {
            List<UploadTable> fixedUp = reorderUploadTablesForUndo();
            boolean isEmbeddedCE = AppContextMgr.getInstance().getClassObject(Collection.class)
                    .getIsEmbeddedCollectingEvent();
            try {
                AppContextMgr.getInstance().getClassObject(Collection.class).setIsEmbeddedCollectingEvent(false);
                for (int ut = fixedUp.size() - 1; ut >= 0; ut--) {
                    // setCurrentOpProgress(fixedUp.size() - ut, false);
                    logDebug("aborting " + fixedUp.get(ut).getTable().getName());
                    fixedUp.get(ut).abortRow(row);
                }
                updateObjectsCreated();
            } finally {
                AppContextMgr.getInstance().getClassObject(Collection.class)
                        .setIsEmbeddedCollectingEvent(isEmbeddedCE);
            }
        } catch (Exception e) {
            throw new UploaderException(e, UploaderException.ABORT_IMPORT);
        }

        if (cause instanceof UploaderMatchSkipException) {
            theWb.getRow(rowUploading).setUploadStatus(WorkbenchRow.UPLD_SKIPPED);
        } else {
            WorkbenchRow wbr = theWb.getRow(rowUploading);
            if (wbr.getUploadStatus() != WorkbenchRow.UPLD_SUCCESS) {
                wbr.setUploadStatus(WorkbenchRow.UPLD_FAILED);
            }
        }
        SkippedRow sr = new SkippedRow(cause, row);
        skippedRows.add(sr);
        newMessages.add(sr);
    }

    /**
     * @param msg
     */
    public synchronized void addMsg(final UploadMessage msg) {
        newMessages.add(msg);
    }

    /**
     * @return uploadTables ordered such that unDo will work.
     * 
     * Currently this merely moves XXXAttributes tables to the end of the ordering.
     * This prevents problems caused by the DeleteOrphan annotation for the XXX->XXXAttribute relationships.
     * This is a horrible hack, but should work for now. If not, it could be achieved more effectively by
     * checking the annotations properties and re-ordering according.
     * 
     * 
     */
    protected List<UploadTable> reorderUploadTablesForUndo() {
        //It is currently not necessary to do any re-ordering.
        return uploadTables;
    }

    /**
     * Undoes the most recent upload.
     * 
     * Called in response to undo command from user, and by the program when an upload is cancelled
     * or fails.
     */
    public void undoUpload(final boolean isUserCmd, final boolean shuttingDown, final boolean completeUndo) {
        setOpKiller(null);

        final UploaderTask undoTask = new UploaderTask(false, "") {
            boolean success = false;
            boolean removeObjects = completeUndo;
            Vector<UploadTable> undone = new Vector<UploadTable>();

            @Override
            public Object doInBackground() {
                start();
                if (removeObjects) {
                    try {
                        if (isUserCmd) {
                            SwingUtilities.invokeAndWait(new Runnable() {
                                public void run() {
                                    initProgressBar(0, getUploadedObjects(), true,
                                            getResourceString("WB_UPLOAD_UNDOING") + " "
                                                    + getResourceString("WB_UPLOAD_OBJECT"),
                                            shuttingDown);
                                }
                            });
                        } else {
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    initProgressBar(0, getUploadedObjects(), true,
                                            getResourceString("WB_UPLOAD_CLEANING_UP") + " "
                                                    + getResourceString("WB_UPLOAD_OBJECT"),
                                            shuttingDown);
                                }
                            });
                        }
                        List<UploadTable> fixedUp = reorderUploadTablesForUndo();
                        boolean isEmbeddedCE = AppContextMgr.getInstance().getClassObject(Collection.class)
                                .getIsEmbeddedCollectingEvent();
                        undoAttachments();
                        try {
                            AppContextMgr.getInstance().getClassObject(Collection.class)
                                    .setIsEmbeddedCollectingEvent(false);
                            for (int ut = fixedUp.size() - 1; ut >= 0; ut--) {
                                // setCurrentOpProgress(fixedUp.size() - ut, false);
                                logDebug("undoing " + fixedUp.get(ut).getTable().getName());
                                fixedUp.get(ut).undoUpload(true);
                                undone.add(fixedUp.get(ut));
                            }
                            success = true;
                            return success;
                        } finally {
                            AppContextMgr.getInstance().getClassObject(Collection.class)
                                    .setIsEmbeddedCollectingEvent(isEmbeddedCE);
                        }
                    } catch (Exception ex) {
                        setOpKiller(ex);
                        return false;
                    }
                }
                //else
                success = true;
                return success;
            }

            @Override
            public void done() {
                if (removeObjects) {
                    try {
                        for (UploadTable ut : undone) {
                            ut.finishUndoUpload();
                        }
                    } catch (Exception ex) {
                        setOpKiller(ex);
                        success = false;
                    }
                }

                super.done();

                if (removeObjects) {
                    for (WorkbenchRow wbRow : theWb.getWorkbenchRows()) {
                        wbRow.setUploadStatus(WorkbenchRow.UPLD_NONE);
                    }
                    wbSS.setChanged(false);
                }

                statusBar.setText("");
                statusBar.setProgressDone("UPLOADER");
                if (shuttingDown) {
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            UIRegistry.clearSimpleGlassPaneMsg();

                        }
                    });
                }
                if (getOpKiller() != null) {
                    JOptionPane.showMessageDialog(UIRegistry.getTopWindow(), String.format(
                            getResourceString("WB_UPLOAD_CLEANUP_FAILED"),
                            new Object[] {
                                    getResourceString((isUserCmd ? "WB_UPLOAD_UNDO_BTN" : "WB_UPLOAD_CLEANUP")),
                                    theWb.getName(), theWb.getName() }),
                            getResourceString("WARNING"), JOptionPane.WARNING_MESSAGE);

                }
                if (mainPanel != null) {
                    mainPanel.clearObjectsCreated();
                    if (success) {
                        if (removeObjects) {
                            setCurrentOp(Uploader.READY_TO_UPLOAD);
                        } else {
                            setCurrentOp(Uploader.SUCCESS_PARTIAL);
                        }
                    } else {
                        setCurrentOp(Uploader.FAILURE);
                    }
                }
                if (shuttingDown && !isUserCmd) {
                    wbSS.decShutdownLock();
                    wbSS.shutdown();
                }
            }

        };
        if (recordSets != null) {
            recordSets.clear();
            recordSets = null;
        }
        UIRegistry.displayStatusBarText(
                getResourceString((isUserCmd ? Uploader.UNDOING_UPLOAD : Uploader.CLEANING_UP)));
        if (shuttingDown) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    UIRegistry.writeSimpleGlassPaneMsg(
                            String.format(getResourceString("WB_UPLOAD_CLEANING_UP") + "...", theWb.getName()),
                            WorkbenchTask.GLASSPANE_FONT_SIZE);

                }
            });
        }
        if (shuttingDown && !isUserCmd) {
            wbSS.incShutdownLock();
        }
        undoTask.execute();
        setCurrentOp(isUserCmd ? Uploader.UNDOING_UPLOAD : Uploader.CLEANING_UP);
    }

    /**
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * 
     * prints listing of uploaded data to System.out.
     */
    public void printUpload() throws IllegalAccessException, InvocationTargetException {
        for (UploadTable ut : uploadTables) {
            System.out.println(ut.getWriteTable().getName());
            Vector<Vector<String>> vals = ut.printUpload();
            for (Vector<String> row : vals) {
                for (String val : row) {
                    System.out.print(val + ", ");
                }
                System.out.println();
            }
        }
    }

    protected abstract class UploaderTask extends javax.swing.SwingWorker<Object, Object> {
        protected final JStatusBar statusBar = UIRegistry.getStatusBar();
        protected boolean cancelled = false;
        protected boolean undo = false;
        protected boolean done = false;
        protected String cancelMsg = null;
        protected boolean cancellable = false;
        protected long startTime;
        protected long endTime;

        public UploaderTask(boolean cancellable, String cancelMsg) {
            super();
            this.cancellable = cancellable;
            this.cancelMsg = cancelMsg;
            currentTask = this;
        }

        public void start() {
            startTime = System.nanoTime();
        }

        @Override
        public void done() {
            super.done();
            currentTask = null;
            done = true;
            endTime = System.nanoTime();
            logDebug("UploaderTask time elapsed: " + Long.toString((endTime - startTime) / 1000000000L));
        }

        /**
         * Cancels the task. Subclasses need to take specific action
         * such as interrupting thread or whatever.
         */
        public synchronized void cancelTask() {
            cancelled = true;
        }

        public synchronized boolean isCancellable() {
            return cancellable;
        }

        public synchronized void setCancellable(boolean val) {
            cancellable = val;
        }

        public synchronized String getCancelMsg() {
            return cancelMsg;
        }

        public synchronized void setCancelMsg(final String msg) {
            cancelMsg = msg;
        }

        /**
         * @return the undo
         */
        public synchronized boolean isUndo() {
            return undo;
        }

        /**
         * @param undo the undo to set
         */
        public synchronized void setUndo(boolean undo) {
            this.undo = undo;
        }
    }

    /**
     * Builds viewer for uploaded data.
     */
    public void retrieveUploadedData() {
        //bogusStorages = new HashMap<String, Vector<Vector<String>>>();
        final String savedOp = currentOp;
        setOpKiller(null);

        final UploaderTask retrieverTask = new UploaderTask(true, "WB_CANCEL_UPLOAD_MSG") {
            @Override
            public Object doInBackground() {
                start();
                try {
                    //
                    return true;
                } catch (Exception ex) {
                    setOpKiller(ex);
                    return false;
                }
            }

            @Override
            public void done() {
                super.done();
                statusBar.setText("");
                setCurrentOp(savedOp);
                if (!cancelled) {
                    viewAllObjectsCreatedByUpload();
                    mainPanel.addMsg(new BaseUploadMessage(getResourceString("WB_UPLOAD_DATA_FETCHED")));
                } else {
                    //bogusStorages = null;
                    mainPanel.addMsg(new BaseUploadMessage(
                            getResourceString("RetrievalWB_UPLOAD_FETCH_CANCELLED cancelled")));
                }
            }

            /* (non-Javadoc)
             * @see edu.ku.brc.specify.tasks.subpane.wb.wbuploader.Uploader.UploaderTask#cancelTask()
             */
            //         @Override
            //         public synchronized void cancelTask()
            //         {
            //            super.cancelTask();
            //            interrupt();
            //         }

        };

        if (mainPanel == null) {
            initUI(Uploader.RETRIEVING_UPLOADED_DATA);
        } else {
            setCurrentOp(Uploader.RETRIEVING_UPLOADED_DATA);
        }
        UIRegistry.getStatusBar().setText(getResourceString(Uploader.RETRIEVING_UPLOADED_DATA));
        retrieverTask.execute();
    }

    /**
     * @param t
     * @param row
     */
    public void loadRow(final UploadTable t, int row) {
        for (UploadField field : uploadFields) {
            logDebug("   uploading field: " + field.getWbFldName());
            //System.out.println("   uploading field: " + field.getWbFldName());
            if (field.getField().getTable().equals(t.getTable())) {
                if (field.getIndex() != -1) {
                    uploadCol(field, uploadData.get(row, field.getIndex()));
                }
            }
        }
    }

    /**
     * @param t
     * @param row
     * @throws UploaderException
     * 
     * imports data in row belonging to t's Table.
     */
    protected void uploadRow(final UploadTable t, int row) throws UploaderException {
        loadRow(t, row);
        try {
            writeRow(t, row);
        } catch (UploaderException ex) {
            //ex.getCause().printStackTrace();
            logDebug(ex.getMessage() + " (" + t.getTable().getName() + ", row " + Integer.toString(row) + ")");
            throw ex;
        }
    }

    protected void uploadRowSavelessly(final UploadTable t, int row) throws UploaderException {
        loadRow(t, row);
        try {
            writeRowSavelessly(t, row);
        } catch (UploaderException ex) {
            //ex.getCause().printStackTrace();
            logDebug(ex.getMessage() + " (" + t.getTable().getName() + ", row " + Integer.toString(row) + ")");
            throw ex;
        }
    }

    /**
     * @param f
     * @param val
     * 
     * imports val to f.
     */
    protected void uploadCol(final UploadField f, final String val) {
        if (f != null) {
            f.setValue(val);
        }
    }

    /**
     * @param t
     * @throws UploaderException
     * 
     * writes data (if necessary) for t.
     */
    protected void writeRow(final UploadTable t, int row) throws UploaderException {
        t.writeRow(row);
        Set<WorkbenchRowImage> imagesToAttach = new HashSet<WorkbenchRowImage>();
        for (int i = imagesForRow.size() - 1; i >= 0; i--) {
            WorkbenchRowImage wri = imagesForRow.get(i);
            if (getAttachToTable(wri) == t) {
                imagesToAttach.add(wri);
                imagesForRow.remove(i);
            }
        }
        attachImages(t, imagesToAttach);
    }

    /**
     * @param t
     * @throws UploaderException
     * 
     * writes data (if necessary) for t.
     */
    protected void writeRowSavelessly(final UploadTable t, int row) throws UploaderException {
        t.writeRowSavelessly(row);
        /*Set<WorkbenchRowImage> imagesToAttach = new HashSet<WorkbenchRowImage>();
        for (int i = imagesForRow.size() -1; i >= 0; i--)
        {
           WorkbenchRowImage wri = imagesForRow.get(i);
           if (getAttachToTable(wri) == t)
           {
          imagesToAttach.add(wri);
          imagesForRow.remove(i);
           }
        }
        attachImages(t, imagesToAttach);*/
    }

    /**
     * @param cls
     * @return an initialized instance of the appropriate OjbectAttachmentIFace implementation.
     */
    protected ObjectAttachmentIFace<? extends DataModelObjBase> getAttachmentObject(final Class<?> cls) {
        ObjectAttachmentIFace<? extends DataModelObjBase> result = null;

        // Redesigned to handle anytime of Attachment upload
        Exception ex = null;
        String className = cls.getName() + "Attachment";
        try {
            Class<?> createClass = Class.forName(className);
            result = (ObjectAttachmentIFace<?>) createClass.newInstance();
            if (result != null) {
                ((DataModelObjBase) result).initialize();
            }
        } catch (ClassNotFoundException e) {
            ex = e;
            e.printStackTrace();
        } catch (InstantiationException e) {
            ex = e;
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            ex = e;
            e.printStackTrace();
        }

        if (ex != null) {
            edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(Uploader.class, ex);
        }

        return result;
    }

    /**
     * @param t
     * @param images
     * @throws UploaderException
     * 
     * Attaches images to the current record in t.
     */
    @SuppressWarnings("unchecked")
    protected void attachImages(final UploadTable t, final Set<WorkbenchRowImage> images) throws UploaderException {
        if (images.size() == 0) {
            return;
        }

        AttachmentOwnerIFace<?> rec = (AttachmentOwnerIFace<?>) t.getCurrentRecord(0);

        //
        if (rec == null && t instanceof UploadTableTree) {
            rec = (AttachmentOwnerIFace<?>) t.getParentRecord(0, t);
        }

        if (rec /*still*/ == null) {
            String msg = String.format(UIRegistry.getResourceString("Uploader.AttachToRecordMissing"), getRow() + 1,
                    t.toString());
            addMsg(new SkippedAttachment(msg, getRow()));
            return; //maybe the row was not uploaded for some reason
        }

        DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();
        boolean tblTransactionOpen = false;
        try {
            session.attach(rec);
            session.beginTransaction();
            tblTransactionOpen = true;
            Set<ObjectAttachmentIFace<?>> attachees = (Set<ObjectAttachmentIFace<?>>) rec.getAttachmentReferences();
            Vector<ObjectAttachmentIFace<?>> currentAttachees = new Vector<ObjectAttachmentIFace<?>>();
            int ordinal = 0;
            for (WorkbenchRowImage image : images) {
                Attachment attachment = new Attachment();
                attachment.initialize();
                attachment.setOrigFilename(image.getCardImageFullPath());
                attachment.setTableId(rec.getAttachmentTableId());
                File dummy = new File(image.getCardImageFullPath());
                if (!dummy.exists()) {
                    addMsg(new SkippedAttachment(String.format(getResourceString("Uploader.AttachFileMissing"),
                            getRow() + 1, image.getCardImageFullPath()), getRow()));
                    continue;
                }
                String title = dummy.getName();
                if (title.length() > 64) {
                    title = title.substring(0, 64);
                }
                attachment.setTitle(title);
                attachment.setFileCreatedDate(ImageMetaDataHelper.getEmbeddedDateOrFileDate(dummy));

                ObjectAttachmentIFace<DataModelObjBase> oaif = (ObjectAttachmentIFace<DataModelObjBase>) getAttachmentObject(
                        rec.getClass());
                if (oaif == null) {
                    //this should never happen
                    log.error("couldn't create attachment interface object for " + rec.getClass().getName());
                    return;
                }
                oaif.setAttachment(attachment);
                oaif.setObject((DataModelObjBase) rec);
                oaif.setOrdinal(ordinal);
                attachees.add(oaif);
                currentAttachees.add(oaif);
            }
            BusinessRulesIFace busRule = DBTableIdMgr.getInstance().getBusinessRule(rec.getClass());
            if (busRule != null) {
                busRule.beforeSave(rec, session);
            }
            session.saveOrUpdate(rec);
            if (busRule != null) {
                if (!busRule.beforeSaveCommit(rec, session)) {
                    session.rollback();
                    throw new Exception("Business rules processing failed");
                }
            }
            for (ObjectAttachmentIFace<?> att : currentAttachees) {
                AttachmentUtils.getAttachmentManager().setStorageLocationIntoAttachment(att.getAttachment(), true);

                att.getAttachment().storeFile(false); // false means do not display an error dialog
            }
            session.commit();
            tblTransactionOpen = false;
            if (busRule != null) {
                busRule.afterSaveCommit(rec, session);
            }
            for (ObjectAttachmentIFace<?> att : currentAttachees) {
                newAttachments.add(new UploadedRecordInfo(att.getAttachment().getId(), -1, 0, null, false, null,
                        t.getWriteTable().getName()));
            }
        } catch (HibernateException he) {
            if (tblTransactionOpen) {
                session.rollback();
            }
            // XXX To avoid hibernate errors, it may be necessary to perform
            // a merge below but that REALLY slows down uploads.
            // (refresh is bad enough)
            {
                throw new UploaderException(he, UploaderException.ABORT_IMPORT);
            }
        } catch (Exception ex) {
            if (tblTransactionOpen) {
                session.rollback();
            }
            if (!(ex instanceof UploaderException)) {
                throw new UploaderException(ex, UploaderException.ABORT_IMPORT);
            } else {
                throw (UploaderException) ex;
            }
        } finally {
            session.close();
        }
    }

    /**
     * remove uploaded attachments
     */
    protected void undoAttachments() {
        for (UploadedRecordInfo uri : newAttachments) {
            BasicSQLUtils.update("delete from " + uri.getTblName().toLowerCase()
                    + "attachment where attachmentid = " + uri.getKey());
            BasicSQLUtils.update("delete from attachment where attachmentid = " + uri.getKey()); //assuming attachments aren't being shared
        }
    }

    /**
     * Creates a recordset for each UploadTable containing the keys of all objects
     * uploaded.
     */
    protected void createRecordSets() {
        if (recordSets != null) {
            recordSets.clear();
        } else {
            recordSets = new Vector<RecordSet>(uploadTables.size());
        }

        UploadTable root = getRootTable();
        for (UploadTable ut : uploadTables) {
            RecordSet rs = ut.getRecordSet(ut == root);
            if (rs.getNumItems() > 0) {
                recordSets.add(rs);
            }
        }
        //combine recordSets with identical names...
        Collections.sort(recordSets, new Comparator<RecordSet>() {
            public int compare(RecordSet rs1, RecordSet rs2) {
                return rs1.getName().compareTo(rs2.getName());
            }
        });
        for (int rs = recordSets.size() - 1; rs > 0; rs--) {
            if (recordSets.get(rs).getName().equals(recordSets.get(rs - 1).getName())) {
                recordSets.get(rs - 1).addAll(recordSets.get(rs).getItems());
                for (RecordSetItemIFace rsi : recordSets.get(rs).getItems()) {
                    recordSets.get(rs - 1).addItem(rsi);
                }
                recordSets.remove(rs);
            }
        }
        //...end combine recordSets.
    }

    /**
     * Saves recordSets to the database.
     */
    protected void saveRecordSets() {
        if (recordSets == null || recordSets.size() == 0) {
            createRecordSets();
        }

        DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();
        try {
            UploadTable root = getRootTable();
            for (RecordSet rs : recordSets) {
                BusinessRulesIFace busRule = DBTableIdMgr.getInstance().getBusinessRule(RecordSet.class);
                if (busRule != null) {
                    busRule.beforeSave(rs, session);
                }
                session.beginTransaction();
                try {
                    session.save(rs);
                    if (busRule != null) {
                        if (!busRule.beforeSaveCommit(rs, session)) {
                            session.rollback();
                            throw new Exception("Business rules processing failed");
                        }
                    }
                    session.commit();
                    if (busRule != null) {
                        busRule.afterSaveCommit(rs, session);
                    }
                    if (rs.getType() == RecordSet.GLOBAL
                            && rs.getDbTableId() == root.getTable().getTableInfo().getTableId()) {
                        final RecordSet mergedRs = session.merge(rs);
                        SwingUtilities.invokeLater(new Runnable() {

                            /* (non-Javadoc)
                             * @see java.lang.Runnable#run()
                             */
                            @Override
                            public void run() {
                                CommandAction cmd = new CommandAction(RecordSetTask.RECORD_SET,
                                        RecordSetTask.ADD_TO_NAV_BOX);
                                cmd.setData(mergedRs);
                                CommandDispatcher.dispatch(cmd);
                            }

                        });
                    }
                } catch (Exception ex) {
                    edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
                    edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(Uploader.class, ex);
                    session.rollback();
                }
            }
        } catch (Exception ex) {
            edu.ku.brc.af.core.UsageTracker.incrHandledUsageCount();
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(Uploader.class, ex);
            throw new RuntimeException(ex);
        } finally {
            session.close();
        }
    }

    /* (non-Javadoc)
     * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
     */
    //@Override
    public void keyPressed(KeyEvent e) {
        logDebug("keyPressed");
        int key = e.getKeyCode();
        if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_TAB || key == KeyEvent.VK_DOWN || key == KeyEvent.VK_UP
                || (key == KeyEvent.VK_TAB && e.isShiftDown()) || key == KeyEvent.VK_HOME
                || key == KeyEvent.VK_END) {
            editInvalidCell(e);
            e.consume();
        }
    }

    /* (non-Javadoc)
     * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
     */
    //@Override
    public void keyReleased(KeyEvent e) {
        // nuthin
        logDebug("keyReleased");
    }

    /* (non-Javadoc)
     * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
     */
    //@Override
    public void keyTyped(KeyEvent e) {
        logDebug("KeyTyped");
        //see note in goToMsgWBCell() re addKeyListener()
        //        int key = e.getKeyCode();
        //        if (key == KeyEvent.VK_ENTER || 
        //                key == KeyEvent.VK_TAB || 
        //                key == KeyEvent.VK_DOWN ||
        //                key == KeyEvent.VK_UP || (key == KeyEvent.VK_TAB && e.isShiftDown()) ||
        //                key == KeyEvent.VK_HOME ||
        //                key == KeyEvent.VK_END)
        //        {
        //            editInvalidCell(e);
        //            e.consume();
        //        }
    }

    /**
     * @param e
     * 
     * Moves to WB cell for appropriate InvalidValue and starts editing it.
     */
    protected void editInvalidCell(KeyEvent e) {
        logDebug("editing invalid cell");
        int key = e.getKeyCode();
        if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_TAB || key == KeyEvent.VK_DOWN)
            goToNextInvalidCell();
        else if (key == KeyEvent.VK_UP || (key == KeyEvent.VK_TAB && e.isShiftDown()))
            goToPrevInvalidCell();
        else if (key == KeyEvent.VK_HOME)
            goToFirstInvalidCell();
        else if (key == KeyEvent.VK_END)
            goToLastInvalidCell();
    }

    /**
     * @param fldConfigs
     */
    public void copyFldConfigs(UploadField[] fldConfigs) {
        if (fldConfigs != null) {
            for (UploadTable ut : uploadTables) {
                ut.copyFldConfigs(fldConfigs);
            }
        }
    }

    /*
     * Stuff to handling with preventing logins and disabling tasks during uploads.
     * 
     */

    /**
     * True if a lock has been overridden. Prevents multiple complaints about uploader lock.
     */
    //XXX Need to test to be sure lockout of other logins is maintaind if user who overrides opens her own upload session.
    protected static boolean isLockIgnored = false;

    /**
     * @return true if it is possible (i.e. user has permission, ??) to override an upload lock.
     * 
     * In this case, override means that the lock will remain in effect, and other users will be locked out,
     * but the current user will be not be locked out.
     */
    protected static boolean canOverrideLock() {
        return SpecifyUser.isCurrentUserType(SpecifyUserTypes.UserType.Manager);
    }

    /**
     * @return true if the lock can be removed - i.e. unlocked.
     */
    protected static boolean canRemoveLock() {
        return canOverrideLock();
    }

    /**
     * Checks to see if a lock has been set for the upload task. 
     * 
     * @return true if no lock is present or lock is overridden. Else return false.
     *  
     */
    public static int checkUploadLock(final Taskable caller) {
        if (isLockIgnored) {
            return NO_LOCK;
        }
        if (!TaskSemaphoreMgr.isLocked(getLockTitle(), "WORKBENCHUPLOAD", TaskSemaphoreMgr.SCOPE.Discipline)) {
            return NO_LOCK;
        }
        int result = lockUpload(caller, false);
        isLockIgnored = result == Uploader.LOCK_IGNORED;
        return result;
    }

    public static String getLockTitle() {
        return UIRegistry.getResourceString("Uploader.UploaderTask");
    }

    /**
     * Sets a lock for the upload task.
     * 
     * @return true is successful.
     */
    public static int lockUpload(final Taskable caller, boolean doLock) {
        boolean override = false;
        boolean unlocked = false;
        boolean isLocked = TaskSemaphoreMgr.isLocked(getLockTitle(), "WORKBENCHUPLOAD",
                TaskSemaphoreMgr.SCOPE.Discipline);
        USER_ACTION result = TaskSemaphoreMgr.lock(getLockTitle(), "WORKBENCHUPLOAD", null, SCOPE.Discipline, false,
                new UploadLocker(canOverrideLock(), canRemoveLock(), caller, doLock), false);
        if (result == USER_ACTION.Override) {
            override = true;
            unlocked = TaskSemaphoreMgr.unlock(getLockTitle(), "WORKBENCHUPLOAD", SCOPE.Discipline);
            if (unlocked && doLock) {
                result = TaskSemaphoreMgr.lock(getLockTitle(), "WORKBENCHUPLOAD", null, SCOPE.Discipline, false);
            }
        }
        if (doLock) {
            if (result == USER_ACTION.OK) {
                return LOCKED;
            }
            return LOCK_FAILED;
        }
        if (!doLock) {
            if (isLocked) {
                if (!override) {
                    if (result == USER_ACTION.OK) {
                        return LOCK_IGNORED;
                    }
                    return LOCKED;
                } else {
                    if (unlocked) {
                        return Uploader.LOCK_REMOVED;
                    }
                    return LOCKED;
                }
            } else {
                return NO_LOCK;
            }
        }
        return NO_LOCK;
    }

    /**
     * Unlocks the upload task.
     * 
     * @return true if successful.
     */
    public static boolean unlockUpload() {
        return TaskSemaphoreMgr.unlock(getLockTitle(), "WORKBENCHUPLOAD", TaskSemaphoreMgr.SCOPE.Discipline);
    }

    protected static void setAppLock(final boolean lock) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JMenuBar menuBar = (JMenuBar) UIRegistry.get(UIRegistry.MENUBAR);
                for (int m = 0; m < menuBar.getMenuCount(); m++) {
                    if (isSystemMenu(menuBar, m)) {
                        menuBar.getMenu(m).setEnabled(!lock);
                    } else if (isTabsMenu(menuBar, m)) {
                        menuBar.getMenu(m).setEnabled(!lock);
                    }
                }
            }
        });
    }

    public static void lockApp() {
        setAppLock(true);
    }

    /**
     * @param menuBar
     * @param menu
     * @return true if menuBar.getMenu(menu) is the Specify "System" menu.
     */
    protected static boolean isSystemMenu(JMenuBar menuBar, int menu) {
        //very cheap and dirty
        return menu == 3;
    }

    /**
     * @param menuBar
     * @param menu
     * @return true if menuBar.getMenu(menu) is the Specify "Tabs" menu.
     */
    protected static boolean isTabsMenu(JMenuBar menuBar, int menu) {
        //cheap. dirty. trash.
        return menu == 4;
    }

    public static void unlockApp() {
        setAppLock(false);
    }

    /**
     * @return the wbSS
     */
    public WorkbenchPaneSS getWbSS() {
        return wbSS;
    }

    /**
     * @return the wb
     */
    public Workbench getWb() {
        return theWb;
    }

    /**
     * @return list of tree classes involved in the upload.
     */
    protected List<Pair<UploadTableTree, Boolean>> getTreesToLock(final boolean defaultStatus) {
        List<Pair<UploadTableTree, Boolean>> trees = new LinkedList<Pair<UploadTableTree, Boolean>>();
        for (UploadTable t : uploadTables) {
            //does t represents the root of a tree?
            if (Treeable.class.isAssignableFrom(t.getTblClass()) && ((UploadTableTree) t).getParent() == null) {
                trees.add(new Pair<UploadTableTree, Boolean>((UploadTableTree) t, defaultStatus));
            }
        }
        return trees;
    }

    /**
     * @return true if all necessary locks were set.
     * 
     * Sets locks on Trees involved in the upload.
     */
    /**
     * @return
     */
    public boolean setAdditionalLocks() {
        //It is actually not necessary to set these locks. 
        return true;

        //       List<Pair<UploadTableTree, Boolean>> trees = getTreesToLock(false);
        //       
        //       if (trees.size() == 0)
        //       {
        //          //nothing to lock
        //          return true;
        //       }
        //              
        //       boolean result = true;
        //       for (Pair<UploadTableTree, Boolean> ttp : trees)
        //       {
        //          UploadTableTree utt = ttp.getFirst();
        //          final String title = utt.getTable().getTableInfo().getTitle();
        //           TaskSemaphoreMgrCallerIFace lockCallback = new TaskSemaphoreMgrCallerIFace(){
        //               @Override
        //              public TaskSemaphoreMgr.USER_ACTION resolveConflict(SpTaskSemaphore semaphore, 
        //                        boolean previouslyLocked,
        //                        String prevLockBy)
        //               {
        //                  UIRegistry.showLocalizedMsg("Uploader.AdditionalLockFailTitle", "Uploader.AdditionalLockFail", 
        //                        title, prevLockBy, title);
        //                  return TaskSemaphoreMgr.USER_ACTION.Error;
        //               }
        //              
        //           };
        //          
        //          TaskSemaphoreMgr.USER_ACTION action = TaskSemaphoreMgr.lock(title, 
        //                utt.getTblClass().getSimpleName() + "TreeDef", null, TaskSemaphoreMgr.SCOPE.Discipline, false, lockCallback);
        //          if (action == TaskSemaphoreMgr.USER_ACTION.OK)
        //          {
        //             ttp.setSecond(true);
        //          }
        //          else
        //          {
        //             result = false;
        //             break;
        //          }
        //       }
        //       
        //       if (!result)
        //       {
        //          unlockTrees(trees);
        //       }
        //       
        //       additionalLocksSet = true;
        //       return result;
    }

    /**
     * @param trees the trees to unlock.
     */
    protected void unlockTrees(final List<Pair<UploadTableTree, Boolean>> trees) {
        //No longer needed

        //       for (Pair<UploadTableTree, Boolean> ttp : trees)
        //       {
        //          if (ttp.getSecond())
        //          {
        //             UploadTableTree utt = ttp.getFirst();
        //             //XXX do something if unlock fails.
        //             TaskSemaphoreMgr.unlock(utt.getTable().getTableInfo().getTitle(), utt.getTblClass().getSimpleName() + "TreeDef", 
        //                TaskSemaphoreMgr.SCOPE.Discipline);
        //          }
        //       }
    }

    /**
     * Frees additional locks set on trees involved in the upload.
     */
    public void freeAdditionalLocks() {
        if (additionalLocksSet) {
            List<Pair<UploadTableTree, Boolean>> trees = getTreesToLock(true);
            unlockTrees(trees);
        }
    }

    /**
     * @param umsbp
     * 
     * Apply settings in umsbp to all tables.
     */
    public void applyMatchSettingsToAllTables(final UploadMatchSettingsBasicPanel umsbp) {
        umsbp.applySettingToAll(uploadTables);
    }

    public void loadRecordToWb(final DataModelObjBase rec, final Workbench wb) throws Exception {
        UploadTable t = null;
        for (UploadTable ut : uploadTables) {
            if (ut.getTblClass().equals(rec.getClass())) {
                t = ut;
            }
        }

        t.loadRecord(rec, 0);
        WorkbenchRow row = wb.addRow();
        row.setRecordId(rec.getId());
        for (UploadTable ut : uploadTables) {
            int seq = 0;
            boolean isOneToMany = ut.getUploadFields().size() > 1
                    || ut.getTable().getName().equalsIgnoreCase("address");
            for (Vector<UploadField> flds : ut.getUploadFields()) {
                if (ut.getCurrentRecord(seq) != null) {
                    if (isOneToMany) {
                        WorkbenchRowExportedRelationship wber = new WorkbenchRowExportedRelationship();
                        wber.initialize();
                        wber.setWorkbenchRow(row);
                        row.getWorkbenchRowExportedRelationships().add(wber);
                        wber.setTableName(ut.getTblClass().getSimpleName());
                        wber.setSequence(seq);
                        //wber.setIsDeleted(false);
                        wber.setRecordId(ut.getCurrentRecord(seq).getId());
                    }
                    //wber.setRelationshipName() ??? - skipping it: assuming that for uploader a table is only one-to-many'ed once from a parent.
                    for (UploadField fld : flds) {
                        if (fld.getIndex() != -1) {
                            Object value = fld.getGetter().invoke(ut.getCurrentRecord(seq));
                            if (value != null) {
                                WorkbenchTemplateMappingItem mi = wb.getMappingFromColumn((short) fld.getIndex());
                                WorkbenchDataItem di = new WorkbenchDataItem();
                                di.initialize();
                                di.setWorkbenchTemplateMappingItem(mi);
                                di.setWorkbenchRow(row);
                                di.setRowNumber(row.getRowNumber());
                                di.setRequired(mi.getIsRequired());
                                //XXX need to deal with formatting and stuff using fld.DBFieldInfo ...
                                di.setCellData(ut.getTextForFieldValue(fld, value, 0));
                                row.getWorkbenchDataItems().add(di);
                            }
                        }
                    }
                }
                seq++;
            }
        }
    }

    /**
     * @return the currentOp
     */
    public String getCurrentOp() {
        return currentOp;
    }

    /**
     * @return
     */
    public List<UploadField> getAutoAssignableFields() {
        List<UploadField> result = new ArrayList<UploadField>();
        for (UploadTable ut : uploadTables) {
            result.addAll(ut.getAutoAssignableFields());
        }
        return result;
    }

    /* @param structureErrors
    * 
    * Display a dialog listing the 'structural' problems with the dataset
    * that prevent uploading.
    */
    public static void showStructureErrors(Vector<UploadMessage> structureErrors) {
        JPanel pane = new JPanel(new BorderLayout());
        JLabel lbl = createLabel(getResourceString("WB_UPLOAD_BAD_STRUCTURE_MSG") + ":");
        lbl.setBorder(new EmptyBorder(3, 1, 2, 0));
        pane.add(lbl, BorderLayout.NORTH);
        JPanel lstPane = new JPanel(new BorderLayout());
        JList<?> lst = UIHelper.createList(structureErrors);
        lst.setBorder(new SoftBevelBorder(BevelBorder.LOWERED));
        lstPane.setBorder(new EmptyBorder(1, 1, 10, 1));
        lstPane.add(lst, BorderLayout.CENTER);
        pane.add(lstPane, BorderLayout.CENTER);
        CustomDialog dlg = new CustomDialog((Frame) UIRegistry.getTopWindow(),
                getResourceString("WB_UPLOAD_BAD_STRUCTURE_DLG"), true, CustomDialog.OKHELP, pane);
        UIHelper.centerAndShow(dlg);
        dlg.dispose();
    }

}