edu.ku.brc.specify.datamodel.busrules.BaseTreeBusRules.java Source code

Java tutorial

Introduction

Here is the source code for edu.ku.brc.specify.datamodel.busrules.BaseTreeBusRules.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.datamodel.busrules;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.text.DateFormat;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

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

import edu.ku.brc.af.core.AppContextMgr;
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.core.expresssearch.QueryAdjusterForDomain;
import edu.ku.brc.af.ui.forms.FormViewObj;
import edu.ku.brc.af.ui.forms.Viewable;
import edu.ku.brc.af.ui.forms.persist.AltViewIFace.CreationMode;
import edu.ku.brc.af.ui.forms.validation.UIValidator;
import edu.ku.brc.af.ui.forms.validation.ValComboBox;
import edu.ku.brc.af.ui.forms.validation.ValComboBoxFromQuery;
import edu.ku.brc.af.ui.forms.validation.ValTextField;
import edu.ku.brc.dbsupport.DataProviderFactory;
import edu.ku.brc.dbsupport.DataProviderSessionIFace;
import edu.ku.brc.dbsupport.DataProviderSessionIFace.QueryIFace;
import edu.ku.brc.specify.config.SpecifyAppContextMgr;
import edu.ku.brc.specify.conversion.BasicSQLUtils;
import edu.ku.brc.specify.datamodel.CollectionMember;
import edu.ku.brc.specify.datamodel.Discipline;
import edu.ku.brc.specify.datamodel.SpTaskSemaphore;
import edu.ku.brc.specify.datamodel.TreeDefIface;
import edu.ku.brc.specify.datamodel.TreeDefItemIface;
import edu.ku.brc.specify.datamodel.TreeDefItemStandardEntry;
import edu.ku.brc.specify.datamodel.Treeable;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgr;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgr.USER_ACTION;
import edu.ku.brc.specify.dbsupport.TaskSemaphoreMgrCallerIFace;
import edu.ku.brc.specify.dbsupport.TreeDefStatusMgr;
import edu.ku.brc.specify.treeutils.TreeDataService;
import edu.ku.brc.specify.treeutils.TreeDataServiceFactory;
import edu.ku.brc.specify.treeutils.TreeHelper;
import edu.ku.brc.ui.GetSetValueIFace;
import edu.ku.brc.ui.UIRegistry;

/**
 * @author rod 
 *
 * (original author was JDS)
 *
 * @code_status Alpha
 *
 * Jan 10, 2008
 *
 * @param <T>
 * @param <D>
 * @param <I>
 */
public abstract class BaseTreeBusRules<T extends Treeable<T, D, I>, D extends TreeDefIface<T, D, I>, I extends TreeDefItemIface<T, D, I>>
        extends AttachmentOwnerBaseBusRules {
    public static final boolean ALLOW_CONCURRENT_FORM_ACCESS = true;
    public static final long FORM_SAVE_LOCK_MAX_DURATION_IN_MILLIS = 60000;
    private static final Logger log = Logger.getLogger(BaseTreeBusRules.class);

    private boolean processedRules = false;

    /**
     * Constructor.
     * 
     * @param dataClasses a var args list of classes that this business rules implementation handles
     */
    public BaseTreeBusRules(Class<?>... dataClasses) {
        super(dataClasses);
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.ui.forms.BaseBusRules#initialize(edu.ku.brc.ui.forms.Viewable)
     */
    @Override
    public void initialize(Viewable viewableArg) {
        super.initialize(viewableArg);

        GetSetValueIFace parentField = (GetSetValueIFace) formViewObj.getControlByName("parent");
        Component comp = formViewObj.getControlByName("definitionItem");
        if (comp instanceof ValComboBox) {
            final ValComboBox rankComboBox = (ValComboBox) comp;

            final JCheckBox acceptedCheckBox = (JCheckBox) formViewObj.getControlByName("isAccepted");
            Component apComp = formViewObj.getControlByName("acceptedParent");
            final ValComboBoxFromQuery acceptedParentWidget = apComp instanceof ValComboBoxFromQuery
                    ? (ValComboBoxFromQuery) apComp
                    : null;

            if (parentField instanceof ValComboBoxFromQuery) {
                final ValComboBoxFromQuery parentCBX = (ValComboBoxFromQuery) parentField;
                if (parentCBX != null && rankComboBox != null) {
                    parentCBX.addListSelectionListener(new ListSelectionListener() {
                        public void valueChanged(ListSelectionEvent e) {
                            if (e == null || !e.getValueIsAdjusting()) {
                                parentChanged(formViewObj, parentCBX, rankComboBox, acceptedCheckBox,
                                        acceptedParentWidget);
                            }
                        }
                    });
                    rankComboBox.getComboBox().addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            rankChanged(formViewObj, parentCBX, rankComboBox, acceptedCheckBox,
                                    acceptedParentWidget);
                        }
                    });
                }
            }

            if (acceptedCheckBox != null && acceptedParentWidget != null) {
                acceptedCheckBox.addItemListener(new ItemListener() {
                    public void itemStateChanged(ItemEvent e) {
                        if (acceptedCheckBox.isSelected()) {
                            acceptedParentWidget.setValue(null, null);
                            acceptedParentWidget.setChanged(true); // This should be done automatically
                            acceptedParentWidget.setEnabled(false);
                        } else {
                            acceptedParentWidget.setEnabled(true);
                        }
                    }
                });
            }
        }
    }

    /**
     * @return list of foreign key relationships for purposes of checking
     * if a record can be deleted. 
     * The list contains two entries for each relationship. The first entry
     * is the related table name. The second is the name of the foreign key field in the related table.
     */
    public abstract String[] getRelatedTableAndColumnNames();

    /**
    * @return list of ass foreign key relationships. 
    * The list contains two entries for each relationship. The first entry
    * is the related table name. The second is the name of the foreign key field in the related table.
    */
    public String[] getAllRelatedTableAndColumnNames() {
        return getRelatedTableAndColumnNames();
    }

    /* (non-Javadoc)
    * @see edu.ku.brc.af.ui.forms.BaseBusRules#okToEnableDelete(java.lang.Object)
    */
    @SuppressWarnings("unchecked")
    @Override
    public boolean okToEnableDelete(Object dataObj) {
        // This is a little weak and chessey, but it gets the job done.
        // Becase both the Tree and Definition want/need to share Business Rules.
        String viewName = formViewObj.getView().getName();
        if (StringUtils.contains(viewName, "TreeDef")) {
            final I treeDefItem = (I) dataObj;
            if (treeDefItem != null && treeDefItem.getTreeDef() != null) {
                return treeDefItem.getTreeDef().isRequiredLevel(treeDefItem.getRankId());
            }
        }
        return super.okToEnableDelete(dataObj);
    }

    /**
      * @param node
      * @return
      */
    @SuppressWarnings("unchecked")
    public boolean okToDeleteNode(T node) {
        if (node.getDefinition() != null && !node.getDefinition().getNodeNumbersAreUpToDate()
                && !node.getDefinition().isUploadInProgress()) {
            //Scary. If nodes are not up to date, tree rules may not work.
            //The application should prevent edits to items/trees whose tree numbers are not up to date except while uploading
            //workbenches.
            throw new RuntimeException(node.getDefinition().getName() + " has out of date node numbers.");
        }

        if (node.getDefinition() != null && node.getDefinition().isUploadInProgress()) {
            //don't think this will ever get called during an upload/upload-undo, but just in case.
            return true;
        }

        Integer id = node.getTreeId();
        if (id == null) {
            return true;
        }
        String[] relationships = getRelatedTableAndColumnNames();

        // if the given node can't be deleted, return false
        if (!super.okToDelete(relationships, node.getTreeId())) {
            return false;
        }

        // now check the children

        // get a list of all descendent IDs
        DataProviderSessionIFace session = null;
        List<Integer> childIDs = null;
        try {
            session = DataProviderFactory.getInstance().createSession();
            String queryStr = "SELECT n.id FROM " + node.getClass().getName()
                    + " n WHERE n.nodeNumber <= :highChild AND n.nodeNumber > :nodeNum ORDER BY n.rankId DESC";
            QueryIFace query = session.createQuery(queryStr, false);
            query.setParameter("highChild", node.getHighestChildNodeNumber());
            query.setParameter("nodeNum", node.getNodeNumber());
            childIDs = (List<Integer>) query.list();

        } catch (Exception ex) {
            edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(BaseTreeBusRules.class, ex);
            // Error Dialog
            ex.printStackTrace();

        } finally {
            if (session != null) {
                session.close();
            }
        }

        // if there are no descendent nodes, return true
        if (childIDs != null && childIDs.size() == 0) {
            return true;
        }

        // break the descendent checks up into chunks or queries

        // This is an arbitrary number.  Trial and error will determine a good value.  This determines
        // the number of IDs that wind up in the "IN" clause of the query run inside okToDelete().
        int chunkSize = 250;
        int lastRecordChecked = -1;

        boolean childrenDeletable = true;
        while (lastRecordChecked + 1 < childIDs.size() && childrenDeletable) {
            int startOfChunk = lastRecordChecked + 1;
            int endOfChunk = Math.min(lastRecordChecked + 1 + chunkSize, childIDs.size());

            // grabs selected subset, exclusive of the last index
            List<Integer> chunk = childIDs.subList(startOfChunk, endOfChunk);

            Integer[] idChunk = chunk.toArray(new Integer[1]);
            childrenDeletable = super.okToDelete(relationships, idChunk);

            lastRecordChecked = endOfChunk - 1;
        }
        return childrenDeletable;
    }

    @Override
    protected String getExtraWhereColumns(DBTableInfo tableInfo) {
        String result = super.getExtraWhereColumns(tableInfo);
        if (CollectionMember.class.isAssignableFrom(tableInfo.getClassObj())) {
            Vector<Object> cols = BasicSQLUtils
                    .querySingleCol("select distinct CollectionID from collection " + "where DisciplineID = "
                            + AppContextMgr.getInstance().getClassObject(Discipline.class).getId());
            if (cols != null) {
                String colList = "";
                for (Object col : cols) {
                    if (!"".equals(colList)) {
                        colList += ",";
                    }
                    colList += col;
                }
                if (!"".equals(colList)) {
                    result = "((" + result + ") or " + tableInfo.getAbbrev() + ".CollectionMemberID in(" + colList
                            + "))";
                }
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    protected void rankChanged(final FormViewObj form, final ValComboBoxFromQuery parentComboBox,
            final ValComboBox rankComboBox, final JCheckBox acceptedCheckBox,
            final ValComboBoxFromQuery acceptedParentWidget) {
        if (form.getAltView().getMode() != CreationMode.EDIT) {
            return;
        }

        //log.debug("form was validated: calling adjustRankComboBoxModel()");

        Object objInForm = form.getDataObj();
        //log.debug("form data object = " + objInForm);
        if (objInForm == null) {
            return;
        }

        final T formNode = (T) objInForm;
        T parent = null;
        if (parentComboBox.getValue() instanceof String) {
            // the data is still in the VIEW mode for some reason
            log.debug("Form is in mode (" + form.getAltView().getMode() + ") but the parent data is a String");
            parentComboBox.getValue();
            parent = formNode.getParent();
        } else {
            parent = (T) parentComboBox.getValue();
        }

        final T theParent = parent;
        I rankObj = (I) rankComboBox.getValue();
        final int rank = rankObj == null ? -2 : rankObj.getRankId();
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                boolean canSynonymize = false;
                if (canAccessSynonymy(formNode, rank)) {
                    canSynonymize = formNode.getDefinition() != null
                            && formNode.getDefinition().getSynonymizedLevel() <= rank
                            && formNode.getDescendantCount() == 0;

                }
                if (acceptedCheckBox != null && acceptedParentWidget != null) {
                    acceptedCheckBox.setEnabled(canSynonymize && theParent != null);
                    if (acceptedCheckBox.isSelected() && acceptedCheckBox.isEnabled()) {
                        acceptedParentWidget.setValue(null, null);
                        acceptedParentWidget.setChanged(true); // This should be done automatically
                        acceptedParentWidget.setEnabled(false);
                    }
                }
                form.getValidator().validateForm();
            }
        });
    }

    @SuppressWarnings("unchecked")
    protected void parentChanged(final FormViewObj form, final ValComboBoxFromQuery parentComboBox,
            final ValComboBox rankComboBox, final JCheckBox acceptedCheckBox,
            final ValComboBoxFromQuery acceptedParentWidget) {
        if (form.getAltView().getMode() != CreationMode.EDIT) {
            return;
        }

        //log.debug("form was validated: calling adjustRankComboBoxModel()");

        Object objInForm = form.getDataObj();
        //log.debug("form data object = " + objInForm);
        if (objInForm == null) {
            return;
        }

        final T formNode = (T) objInForm;

        // set the contents of this combobox based on the value chosen as the parent
        adjustRankComboBoxModel(parentComboBox, rankComboBox, formNode);

        T parent = null;
        if (parentComboBox.getValue() instanceof String) {
            // the data is still in the VIEW mode for some reason
            log.debug("Form is in mode (" + form.getAltView().getMode() + ") but the parent data is a String");
            parentComboBox.getValue();
            parent = formNode.getParent();
        } else {
            parent = (T) parentComboBox.getValue();
        }

        // set the tree def for the object being edited by using the parent node's tree def
        // set the parent too??? (lookups for the AcceptedParent QueryComboBox need this) 
        if (parent != null) {
            formNode.setDefinition(parent.getDefinition());
            formNode.setParent(parent);
        }

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                boolean rnkEnabled = rankComboBox.getComboBox().getModel().getSize() > 0;
                rankComboBox.setEnabled(rnkEnabled);
                JLabel label = form.getLabelFor(rankComboBox);
                if (label != null) {
                    label.setEnabled(rnkEnabled);
                }
                if (rankComboBox.hasFocus() && !rnkEnabled) {
                    parentComboBox.requestFocus();
                }
                rankChanged(formViewObj, parentComboBox, rankComboBox, acceptedCheckBox, acceptedParentWidget);
                form.getValidator().validateForm();
            }
        });

    }

    /**
     * @param parentField
     * @param rankComboBox
     * @param nodeInForm
     */
    @SuppressWarnings("unchecked")
    protected void adjustRankComboBoxModel(final GetSetValueIFace parentField, final ValComboBox rankComboBox,
            final T nodeInForm) {
        log.debug("Adjusting the model for the 'rank' combo box in a tree node form");
        if (nodeInForm == null) {
            return;
        }
        log.debug("nodeInForm = " + nodeInForm.getName());

        DefaultComboBoxModel<I> model = (DefaultComboBoxModel<I>) rankComboBox.getModel();
        model.removeAllElements();

        // this is the highest rank the edited item can possibly be
        I topItem = null;
        // this is the lowest rank the edited item can possibly be
        I bottomItem = null;

        Object value = parentField.getValue();
        T parent = null;
        if (value instanceof String) {
            // this happens when the combobox is in view mode, which means it's really a textfield
            // in that case, the parent of the node in the form will do, since the user can't change the parents
            parent = nodeInForm.getParent();
        } else {
            parent = (T) parentField.getValue();
        }

        if (parent == null) {
            return;
        }

        // grab all the def items from just below the parent's item all the way to the next enforced level
        // or to the level of the highest ranked child
        topItem = parent.getDefinitionItem().getChild();
        log.debug("highest valid tree level: " + topItem);

        if (topItem == null) {
            // this only happens if a parent was chosen that cannot have children b/c it is at the
            // lowest defined level in the tree
            log.warn("Chosen node cannot be a parent node.  It is at the lowest defined level of the tree.");
            return;
        }

        // find the child with the highest rank and set that child's def item as the bottom of the range
        if (!nodeInForm.getChildren().isEmpty()) {
            for (T child : nodeInForm.getChildren()) {
                if (bottomItem == null || child.getRankId() > bottomItem.getRankId()) {
                    bottomItem = child.getDefinitionItem().getParent();
                }
            }
        }

        log.debug("lowest valid tree level:  " + bottomItem);

        I item = topItem;
        boolean done = false;
        while (!done) {
            model.addElement(item);

            if (item.getChild() == null || item.getIsEnforced() == Boolean.TRUE
                    || (bottomItem != null && item.getRankId().intValue() == bottomItem.getRankId().intValue())) {
                done = true;
            }
            item = item.getChild();
        }

        if (nodeInForm.getDefinitionItem() != null) {
            I defItem = nodeInForm.getDefinitionItem();
            for (int i = 0; i < model.getSize(); ++i) {
                I modelItem = (I) model.getElementAt(i);
                if (modelItem.getRankId().equals(defItem.getRankId())) {
                    log.debug("setting rank selected value to " + modelItem);
                    model.setSelectedItem(modelItem);
                }
            }
            //            if (model.getIndexOf(defItem) != -1)
            //            {
            //                model.setSelectedItem(defItem);
            //            }
        } else if (model.getSize() == 1) {
            Object defItem = model.getElementAt(0);
            log.debug("setting rank selected value to the only available option: " + defItem);
            model.setSelectedItem(defItem);
        }
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.ui.forms.BaseBusRules#afterFillForm(java.lang.Object)
     */
    @SuppressWarnings("unchecked")
    @Override
    public void afterFillForm(final Object dataObj) {
        // This is a little weak and cheesey, but it gets the job done.
        // Because both the Tree and Definition want/need to share Business Rules.
        String viewName = formViewObj.getView().getName();
        if (StringUtils.contains(viewName, "TreeDef")) {
            if (formViewObj.getAltView().getMode() != CreationMode.EDIT) {
                // when we're not in edit mode, we don't need to setup any listeners since the user can't change anything
                //log.debug("form is not in edit mode: no special listeners will be attached");
                return;
            }

            if (!StringUtils.contains(viewName, "TreeDefItem")) {
                return;
            }

            final I nodeInForm = (I) formViewObj.getDataObj();
            //disable FullName -related fields if TreeDefItem is used by nodes in the tree
            //NOTE: Can remove the edit restriction. Tree rebuilds now update fullname fields. Need to add tree rebuild after fullname def edits.
            if (nodeInForm != null && nodeInForm.getTreeDef() != null) {
                //               boolean canNOTEditFullNameFlds = nodeInForm.hasTreeEntries();
                //               if (canNOTEditFullNameFlds)
                //               {
                //                  ValTextField ftCtrl = (ValTextField )formViewObj.getControlByName("textAfter");
                //                  if (ftCtrl != null)
                //                  {
                //                     ftCtrl.setEnabled(false);
                //                  }
                //                  ftCtrl = (ValTextField )formViewObj.getControlByName("textBefore");
                //                  if (ftCtrl != null)
                //                  {
                //                     ftCtrl.setEnabled(false);
                //                  }
                //                  ftCtrl = (ValTextField )formViewObj.getControlByName("fullNameSeparator");
                //                  if (ftCtrl != null)
                //                  {
                //                     ftCtrl.setEnabled(false);
                //                  }
                //                  ValCheckBox ftBox = (ValCheckBox )formViewObj.getControlByName("isInFullName");
                //                  if (ftBox != null)
                //                  {
                //                     ftBox.setEnabled(false);
                //                  }
                //               }

                if (!viewName.endsWith("TreeDefItem")) {
                    return;
                }

                //disabling editing of name and rank for standard levels.
                List<TreeDefItemStandardEntry> stds = nodeInForm.getTreeDef().getStandardLevels();
                TreeDefItemStandardEntry stdLevel = null;
                for (TreeDefItemStandardEntry std : stds) {
                    //if (std.getTitle().equals(nodeInForm.getName()) && std.getRank() == nodeInForm.getRankId())
                    if (std.getRank() == nodeInForm.getRankId()) {
                        stdLevel = std;
                        break;
                    }
                }
                if (stdLevel != null) {
                    ValTextField nameCtrl = (ValTextField) formViewObj.getControlByName("name");
                    Component rankCtrl = formViewObj.getControlByName("rankId");
                    if (nameCtrl != null) {
                        nameCtrl.setEnabled(false);
                    }
                    if (rankCtrl != null) {
                        rankCtrl.setEnabled(false);
                    }
                    if (nodeInForm.getTreeDef().isRequiredLevel(stdLevel.getRank())) {
                        Component enforcedCtrl = formViewObj.getControlByName("isEnforced");
                        if (enforcedCtrl != null) {
                            enforcedCtrl.setEnabled(false);
                        }
                    }
                }
            }
            return;
        }

        final T nodeInForm = (T) formViewObj.getDataObj();

        if (formViewObj.getAltView().getMode() != CreationMode.EDIT) {
            if (nodeInForm != null) {
                //XXX this MAY be necessary due to a bug with TextFieldFromPickListTable??
                // TextFieldFromPickListTable.setValue() does nothing because of a null adapter member.
                Component comp = formViewObj.getControlByName("definitionItem");
                if (comp instanceof JTextField) {
                    ((JTextField) comp).setText(nodeInForm.getDefinitionItem().getName());
                }
            }
        } else {
            processedRules = false;
            GetSetValueIFace parentField = (GetSetValueIFace) formViewObj.getControlByName("parent");
            Component comp = formViewObj.getControlByName("definitionItem");
            if (comp instanceof ValComboBox) {
                final ValComboBox rankComboBox = (ValComboBox) comp;

                if (parentField instanceof ValComboBoxFromQuery) {
                    final ValComboBoxFromQuery parentCBX = (ValComboBoxFromQuery) parentField;
                    if (parentCBX != null && rankComboBox != null && nodeInForm != null) {
                        parentCBX.registerQueryBuilder(new TreeableSearchQueryBuilder(nodeInForm, rankComboBox,
                                TreeableSearchQueryBuilder.PARENT));
                    }
                }

                if (nodeInForm != null && nodeInForm.getDefinitionItem() != null) {
                    // log.debug("node in form already has a set rank: forcing a call to
                    // adjustRankComboBoxModel()");
                    UIValidator.setIgnoreAllValidation(this, true);
                    adjustRankComboBoxModel(parentField, rankComboBox, nodeInForm);
                    UIValidator.setIgnoreAllValidation(this, false);
                }

                // TODO: the form system MUST require the accepted parent widget to be present if
                // the
                // isAccepted checkbox is present
                final JCheckBox acceptedCheckBox = (JCheckBox) formViewObj.getControlByName("isAccepted");
                final ValComboBoxFromQuery acceptedParentWidget = (ValComboBoxFromQuery) formViewObj
                        .getControlByName("acceptedParent");
                if (canAccessSynonymy(nodeInForm)) {
                    if (acceptedCheckBox != null && acceptedParentWidget != null) {
                        if (acceptedCheckBox.isSelected() && nodeInForm != null
                                && nodeInForm.getDefinition() != null) {
                            // disable if necessary
                            boolean canSynonymize = nodeInForm.getDefinition().getSynonymizedLevel() <= nodeInForm
                                    .getRankId() && nodeInForm.getDescendantCount() == 0;
                            acceptedCheckBox.setEnabled(canSynonymize);
                        }
                        acceptedParentWidget
                                .setEnabled(!acceptedCheckBox.isSelected() && acceptedCheckBox.isEnabled());
                        if (acceptedCheckBox.isSelected()) {
                            acceptedParentWidget.setValue(null, null);
                        }

                        if (nodeInForm != null && acceptedParentWidget != null && rankComboBox != null) {
                            acceptedParentWidget.registerQueryBuilder(new TreeableSearchQueryBuilder(nodeInForm,
                                    rankComboBox, TreeableSearchQueryBuilder.ACCEPTED_PARENT));
                        }
                    }
                } else {
                    if (acceptedCheckBox != null) {
                        acceptedCheckBox.setEnabled(false);
                    }
                    if (acceptedParentWidget != null) {
                        acceptedParentWidget.setEnabled(false);
                    }
                }

                if (parentField instanceof ValComboBoxFromQuery) {
                    parentChanged(formViewObj, (ValComboBoxFromQuery) parentField, rankComboBox, acceptedCheckBox,
                            acceptedParentWidget);
                }
            }
        }
    }

    /**
     * @param tableInfo
     * 
     * @return Select (i.e. everything before where clause) of sqlTemplate
     */
    protected String getSqlSelectTemplate(final DBTableInfo tableInfo) {
        StringBuilder sb = new StringBuilder();
        sb.append("select %s1 FROM "); //$NON-NLS-1$
        sb.append(tableInfo.getClassName());
        sb.append(" as "); //$NON-NLS-1$
        sb.append(tableInfo.getAbbrev());

        String joinSnipet = QueryAdjusterForDomain.getInstance().getJoinClause(tableInfo, true, null, false); //arg 2: false means SQL
        if (joinSnipet != null) {
            sb.append(' ');
            sb.append(joinSnipet);
        }
        sb.append(' ');
        return sb.toString();
    }

    /**
     * @param dataObj
     * 
     * return true if acceptedParent and accepted fields should be enabled on data forms.
     */
    @SuppressWarnings("unchecked")
    protected boolean canAccessSynonymy(final T dataObj) {
        if (dataObj == null) {
            return false; //??
        }

        if (dataObj.getChildren().size() > 0) {
            return false;
        }

        TreeDefItemIface<?, ?, ?> defItem = dataObj.getDefinitionItem();
        if (defItem == null) {
            return false; //??? 
        }
        TreeDefIface<?, ?, ?> def = dataObj.getDefinition();
        if (def == null) {
            def = ((SpecifyAppContextMgr) AppContextMgr.getInstance())
                    .getTreeDefForClass((Class<? extends Treeable<?, ?, ?>>) dataObj.getClass());
        }

        if (!def.isSynonymySupported()) {
            return false;
        }

        return defItem.getRankId() >= def.getSynonymizedLevel();
    }

    /**
     * @param dataObj
     * @param rank
     * @return true if the rank is synonymizable according to the relevant TreeDefinition
     * 
     * For use when dataObj's rank has not yet been assigned or updated.
     */
    @SuppressWarnings("unchecked")
    protected boolean canAccessSynonymy(final T dataObj, final int rank) {
        if (dataObj == null) {
            return false; //??
        }

        if (dataObj.getChildren().size() > 0) {
            return false;
        }
        TreeDefIface<?, ?, ?> def = ((SpecifyAppContextMgr) AppContextMgr.getInstance())
                .getTreeDefForClass((Class<? extends Treeable<?, ?, ?>>) dataObj.getClass());

        if (!def.isSynonymySupported()) {
            return false;
        }

        return rank >= def.getSynonymizedLevel();
    }

    /**
     * Updates the fullname field of any nodes effected by changes to <code>node</code> that are about
     * to be saved to the DB.
     * 
     * @param node
     * @param session
     * @param nameChanged
     * @param parentChanged
     * @param rankChanged
     */
    @SuppressWarnings("unchecked")
    protected void updateFullNamesIfNecessary(T node, DataProviderSessionIFace session) {
        if (node.getTreeId() == null) {
            // this is a new node
            // it shouldn't need updating since we set the fullname at creation time
            return;
        }

        boolean updateNodeFullName = false;
        boolean updateDescFullNames = false;

        // we need a way to determine if the name changed
        // load a fresh copy from the DB and get the values needed for comparison
        DataProviderSessionIFace tmpSession = DataProviderFactory.getInstance().createSession();
        T fromDB = (T) tmpSession.get(node.getClass(), node.getTreeId());
        tmpSession.close();

        if (fromDB == null) {
            // this node is new and hasn't yet been flushed to the DB, so we don't need to worry about updating fullnames
            //return;
            fromDB = node;
        }

        T origParent = fromDB.getParent();
        boolean parentChanged = false;
        T currentParent = node.getParent();
        if ((currentParent == null && origParent != null) || (currentParent != null && origParent == null)) {
            // I can't imagine how this would ever happen, but just in case
            parentChanged = true;
        }
        if (currentParent != null && origParent != null
                && !currentParent.getTreeId().equals(origParent.getTreeId())) {
            // the parent ID changed
            parentChanged = true;
        }

        boolean higherLevelsIncluded = false;
        if (parentChanged) {
            higherLevelsIncluded = higherLevelsIncludedInFullname(node);
            higherLevelsIncluded |= higherLevelsIncludedInFullname(fromDB);
        }

        if (parentChanged && higherLevelsIncluded) {
            updateNodeFullName = true;
            updateDescFullNames = true;
        }

        boolean nameChanged = !(fromDB.getName().equals(node.getName()));
        boolean rankChanged = !(fromDB.getRankId().equals(node.getRankId()));
        if (rankChanged || nameChanged) {
            updateNodeFullName = true;
            if (booleanValue(fromDB.getDefinitionItem().getIsInFullName(), false) == true) {
                updateDescFullNames = true;
            }
            if (booleanValue(node.getDefinitionItem().getIsInFullName(), false) == true) {
                updateDescFullNames = true;
            }
        } else if (fromDB == node) {
            updateNodeFullName = true;
        }

        if (updateNodeFullName) {
            if (updateDescFullNames) {
                // this could take a long time
                TreeHelper.fixFullnameForNodeAndDescendants(node);
            } else {
                // this should be really fast
                String fullname = TreeHelper.generateFullname(node);
                node.setFullName(fullname);
            }
        }
    }

    protected boolean higherLevelsIncludedInFullname(T node) {
        boolean higherLevelsIncluded = false;
        // this doesn't necessarily mean the fullname has to be changed
        // if no higher levels are included in the fullname, then nothing needs updating
        // so, let's see if higher levels factor into the fullname
        T l = node.getParent();
        while (l != null) {
            if ((l.getDefinitionItem().getIsInFullName() != null)
                    && (l.getDefinitionItem().getIsInFullName().booleanValue() == true)) {
                higherLevelsIncluded = true;
                break;
            }
            l = l.getParent();
        }

        return higherLevelsIncluded;
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.specify.datamodel.busrules.BaseBusRules#beforeSave(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
     */
    @SuppressWarnings("unchecked")
    @Override
    public void beforeSave(Object dataObj, DataProviderSessionIFace session) {
        super.beforeSave(dataObj, session);

        if (dataObj instanceof Treeable) {
            // NOTE: the instanceof check can't check against 'T' since T isn't a class
            //       this has a SMALL amount of risk to it
            T node = (T) dataObj;

            if (!node.getDefinition().getNodeNumbersAreUpToDate() && !node.getDefinition().isUploadInProgress()) {
                //Scary. If nodes are not up to date, tree rules may not work (actually this one is OK. (for now)). 
                //The application should prevent edits to items/trees whose tree numbers are not up to date except while uploading
                //workbenches.
                throw new RuntimeException(node.getDefinition().getName() + " has out of date node numbers.");
            }

            // set it's fullname
            String fullname = TreeHelper.generateFullname(node);
            node.setFullName(fullname);
        }
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.specify.datamodel.busrules.BaseBusRules#afterSaveCommit(java.lang.Object)
     */
    @SuppressWarnings("unchecked")
    @Override
    public boolean beforeSaveCommit(final Object dataObj, final DataProviderSessionIFace session) throws Exception {

        // PLEASE NOTE!
        // If any changes are made to this check to make sure no one (Like GeologicTimePeriod) is overriding this method
        // and make the appropriate changes there also.
        if (!super.beforeSaveCommit(dataObj, session)) {
            return false;
        }

        boolean success = true;

        // compare the dataObj values to the nodeBeforeSave values to determine if a node was moved or added
        if (dataObj instanceof Treeable) {
            // NOTE: the instanceof check can't check against 'T' since T isn't a class
            //       this has a SMALL amount of risk to it
            T node = (T) dataObj;

            if (!node.getDefinition().getNodeNumbersAreUpToDate() && !node.getDefinition().isUploadInProgress()) {
                //Scary. If nodes are not up to date, tree rules may not work.
                //The application should prevent edits to items/trees whose tree numbers are not up to date except while uploading
                //workbenches.
                throw new RuntimeException(node.getDefinition().getName() + " has out of date node numbers.");
            }

            // if the node doesn't have any assigned node number, it must be new
            boolean added = (node.getNodeNumber() == null);

            if (node.getDefinition().getDoNodeNumberUpdates() && node.getDefinition().getNodeNumbersAreUpToDate()) {
                log.info("Saved tree node was added.  Updating node numbers appropriately.");
                TreeDataService<T, D, I> dataServ = TreeDataServiceFactory.createService();
                if (added) {
                    success = dataServ.updateNodeNumbersAfterNodeAddition(node, session);
                } else {
                    success = dataServ.updateNodeNumbersAfterNodeEdit(node, session);
                }
            } else {
                node.getDefinition().setNodeNumbersAreUpToDate(false);
            }
        }

        return success;
    }

    /* (non-Javadoc)
    * @see edu.ku.brc.af.ui.forms.BaseBusRules#beforeDeleteCommit(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
    */
    /*
     * NOTE: If this method is overridden, freeLocks() MUST be called when result is false
     * !!
     * 
     */
    @Override
    public boolean beforeDeleteCommit(Object dataObj, DataProviderSessionIFace session) throws Exception {
        if (!super.beforeDeleteCommit(dataObj, session)) {
            return false;
        }
        if (dataObj != null
                && (formViewObj == null || !StringUtils.contains(formViewObj.getView().getName(), "TreeDef"))
                && BaseTreeBusRules.ALLOW_CONCURRENT_FORM_ACCESS && viewable != null) {
            return getRequiredLocks(dataObj);
        } else {
            return true;
        }
    }

    /* (non-Javadoc)
      * @see edu.ku.brc.ui.forms.BaseBusRules#afterDeleteCommit(java.lang.Object)
      */
    @SuppressWarnings("unchecked")
    @Override
    public void afterDeleteCommit(Object dataObj) {
        try {
            if (dataObj instanceof Treeable) {
                // NOTE: the instanceof check can't check against 'T' since T
                // isn't a class
                // this has a SMALL amount of risk to it
                T node = (T) dataObj;

                if (!node.getDefinition().getNodeNumbersAreUpToDate()
                        && !node.getDefinition().isUploadInProgress()) {
                    // Scary. If nodes are not up to date, tree rules may not
                    // work.
                    // The application should prevent edits to items/trees whose
                    // tree numbers are not up to date except while uploading
                    // workbenches.
                    throw new RuntimeException(node.getDefinition().getName() + " has out of date node numbers.");
                }

                if (node.getDefinition().getDoNodeNumberUpdates()
                        && node.getDefinition().getNodeNumbersAreUpToDate()) {
                    log.info("A tree node was deleted.  Updating node numbers appropriately.");
                    TreeDataService<T, D, I> dataServ = TreeDataServiceFactory.createService();
                    // apparently a refresh() is necessary. node can hold
                    // obsolete values otherwise.
                    // Possibly needs to be done for all business rules??
                    DataProviderSessionIFace session = null;
                    try {
                        session = DataProviderFactory.getInstance().createSession();
                        // rods - 07/28/08 commented out because the node is
                        // already deleted
                        // session.refresh(node);
                        dataServ.updateNodeNumbersAfterNodeDeletion(node, session);

                    } catch (Exception ex) {
                        edu.ku.brc.exceptions.ExceptionTracker.getInstance().capture(BaseTreeBusRules.class, ex);
                        ex.printStackTrace();

                    } finally {
                        if (session != null) {
                            session.close();
                        }
                    }

                } else {
                    node.getDefinition().setNodeNumbersAreUpToDate(false);
                }
            }
        } finally {
            if (BaseTreeBusRules.ALLOW_CONCURRENT_FORM_ACCESS && viewable != null) {
                this.freeLocks();
            }
        }
    }

    /**
     * Handles the {@link #beforeSave(Object)} method if the passed in {@link Object}
     * is an instance of {@link TreeDefItemIface}.  The real work of this method is to
     * update the 'fullname' field of all {@link Treeable} objects effected by the changes
     * to the passed in {@link TreeDefItemIface}.
     *
     * @param defItem the {@link TreeDefItemIface} being saved
     */
    @SuppressWarnings("unchecked")
    protected void beforeSaveTreeDefItem(I defItem) {
        // we need a way to determine if the 'isInFullname' value changed
        // load a fresh copy from the DB and get the values needed for comparison
        DataProviderSessionIFace tmpSession = DataProviderFactory.getInstance().createSession();
        I fromDB = (I) tmpSession.load(defItem.getClass(), defItem.getTreeDefItemId());
        tmpSession.close();

        DataProviderSessionIFace session = DataProviderFactory.getInstance().createSession();
        session.attach(defItem);

        boolean changeThisLevel = false;
        boolean changeAllDescendants = false;

        boolean fromDBIsInFullname = makeNotNull(fromDB.getIsInFullName());
        boolean currentIsInFullname = makeNotNull(defItem.getIsInFullName());
        if (fromDBIsInFullname != currentIsInFullname) {
            changeAllDescendants = true;
        }

        // look for changes in the 'textBefore', 'textAfter' or 'fullNameSeparator' fields
        String fromDbBeforeText = makeNotNull(fromDB.getTextBefore());
        String fromDbAfterText = makeNotNull(fromDB.getTextAfter());
        String fromDbSeparator = makeNotNull(fromDB.getFullNameSeparator());

        String before = makeNotNull(defItem.getTextBefore());
        String after = makeNotNull(defItem.getTextAfter());
        String separator = makeNotNull(defItem.getFullNameSeparator());

        boolean textFieldChanged = false;
        boolean beforeChanged = !before.equals(fromDbBeforeText);
        boolean afterChanged = !after.equals(fromDbAfterText);
        boolean sepChanged = !separator.equals(fromDbSeparator);
        if (beforeChanged || afterChanged || sepChanged) {
            textFieldChanged = true;
        }

        if (textFieldChanged) {
            if (currentIsInFullname) {
                changeAllDescendants = true;
            }
            changeThisLevel = true;
        }

        if (changeThisLevel && !changeAllDescendants) {
            Set<T> levelNodes = defItem.getTreeEntries();
            for (T node : levelNodes) {
                String generated = TreeHelper.generateFullname(node);
                node.setFullName(generated);
            }
        } else if (changeThisLevel && changeAllDescendants) {
            Set<T> levelNodes = defItem.getTreeEntries();
            for (T node : levelNodes) {
                TreeHelper.fixFullnameForNodeAndDescendants(node);
            }
        } else if (!changeThisLevel && changeAllDescendants) {
            Set<T> levelNodes = defItem.getTreeEntries();
            for (T node : levelNodes) {
                // grab all child nodes and go from there
                for (T child : node.getChildren()) {
                    TreeHelper.fixFullnameForNodeAndDescendants(child);
                }
            }
        }
        // else don't change anything

        session.close();
    }

    protected boolean booleanValue(Boolean bool, boolean defaultIfNull) {
        if (bool != null) {
            return bool.booleanValue();
        }
        return defaultIfNull;
    }

    /**
     * Converts a null string into an empty string.  If the provided String is not
     * null, it is returned unchanged.
     * 
     * @param s a string
     * @return the string or " ", if null
     */
    private String makeNotNull(String s) {
        return (s == null) ? "" : s;
    }

    /**
     * Returns the provided {@link Boolean}, or <code>false</code> if null
     * 
     * @param b the {@link Boolean} to convert to non-null
     * @return the provided {@link Boolean}, or <code>false</code> if null
     */
    private boolean makeNotNull(Boolean b) {
        return (b == null) ? false : b.booleanValue();
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.ui.forms.BaseBusRules#beforeDelete(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
     */
    @Override
    public Object beforeDelete(Object dataObj, DataProviderSessionIFace session) {
        super.beforeDelete(dataObj, session);
        if (dataObj instanceof Treeable<?, ?, ?>) {
            Treeable<?, ?, ?> node = (Treeable<?, ?, ?>) dataObj;
            if (node.getAcceptedParent() != null) {
                node.getAcceptedParent().getAcceptedChildren().remove(node);
                node.setAcceptedParent(null);
            }
        }
        return dataObj;
    }

    /**
     * @param parentDataObj
     * @param dataObj
     * @return
     */
    @SuppressWarnings("unchecked")
    protected boolean parentHasChildWithSameName(final Object parentDataObj, final Object dataObj) {
        if (dataObj instanceof Treeable<?, ?, ?>) {
            Treeable<T, D, I> node = (Treeable<T, D, I>) dataObj;
            Treeable<T, D, I> parent = parentDataObj == null ? node.getParent() : (Treeable<T, D, I>) parentDataObj;
            if (parent != null) {
                //XXX the sql below will only work if all Treeable tables use fields named 'isAccepted' and 'name' to store
                //the name and isAccepted properties.
                String tblName = DBTableIdMgr.getInstance().getInfoById(node.getTableId()).getName();
                String sql = "SELECT count(*) FROM " + tblName + " where isAccepted " + "and name = "
                        + BasicSQLUtils.getEscapedSQLStrExpr(node.getName());
                if (parent.getTreeId() != null) {
                    sql += " and parentid = " + parent.getTreeId();
                }
                if (node.getTreeId() != null) {
                    sql += " and " + tblName + "id != " + node.getTreeId();
                }
                return BasicSQLUtils.getNumRecords(sql) > 0;
            }
        }
        return false;
    }

    /**
     * @param parentDataObj
     * @param dataObj
     * @param isExistingObject
     * @return
     */
    @SuppressWarnings("unchecked")
    public STATUS checkForSiblingWithSameName(final Object parentDataObj, final Object dataObj,
            final boolean isExistingObject) {
        STATUS result = STATUS.OK;
        if (parentHasChildWithSameName(parentDataObj, dataObj)) {
            String parentName;
            if (parentDataObj == null) {
                parentName = ((Treeable<T, D, I>) dataObj).getParent().getFullName();
            } else {
                parentName = ((Treeable<T, D, I>) parentDataObj).getFullName();
            }
            boolean saveIt = UIRegistry.displayConfirm(
                    UIRegistry.getResourceString("BaseTreeBusRules.IDENTICALLY_NAMED_SIBLING_TITLE"),
                    String.format(UIRegistry.getResourceString("BaseTreeBusRules.IDENTICALLY_NAMED_SIBLING_MSG"),
                            parentName, ((Treeable<T, D, I>) dataObj).getName()),
                    UIRegistry.getResourceString("SAVE"), UIRegistry.getResourceString("CANCEL"),
                    JOptionPane.QUESTION_MESSAGE);
            if (!saveIt) {
                //Adding to reasonList prevents blank "Issue of Concern" popup -
                //but causes annoying second "duplicate child" nag.
                reasonList.add(UIRegistry.getResourceString("BaseTreeBusRules.IDENTICALLY_NAMED_SIBLING")); // XXX
                // i18n
                result = STATUS.Error;
            }
        }
        return result;
    }

    /**
     * @param dataObj
     * @return OK if required data is present.
     * 
     * Checks for requirements that can't be defined in the database schema.
     */
    protected STATUS checkForRequiredFields(Object dataObj) {
        if (dataObj instanceof Treeable<?, ?, ?>) {
            STATUS result = STATUS.OK;
            Treeable<?, ?, ?> obj = (Treeable<?, ?, ?>) dataObj;
            if (obj.getParent() == null) {
                if (obj.getDefinitionItem() != null && obj.getDefinitionItem().getParent() == null) {
                    //it's the root, null parent is OK.
                    return result;
                }
                result = STATUS.Error;
                DBTableInfo info = DBTableIdMgr.getInstance().getInfoById(obj.getTableId());
                DBFieldInfo fld = info.getFieldByColumnName("Parent");
                String fldTitle = fld != null ? fld.getTitle() : UIRegistry.getResourceString("PARENT");
                reasonList.add(String.format(UIRegistry.getResourceString("GENERIC_FIELD_MISSING"), fldTitle));
            }
            //check that non-accepted node has an 'AcceptedParent'
            if (obj.getIsAccepted() == null || !obj.getIsAccepted() && obj.getAcceptedParent() == null) {
                result = STATUS.Error;
                DBTableInfo info = DBTableIdMgr.getInstance().getInfoById(obj.getTableId());
                DBFieldInfo fld = info.getFieldByColumnName("AcceptedParent");
                String fldTitle = fld != null ? fld.getTitle() : UIRegistry.getResourceString("ACCEPTED");
                reasonList.add(String.format(UIRegistry.getResourceString("GENERIC_FIELD_MISSING"), fldTitle));
            }
            return result;
        }
        return STATUS.None; //???
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.ui.forms.BaseBusRules#processBusinessRules(java.lang.Object, java.lang.Object, boolean)
     */
    @Override
    public STATUS processBusinessRules(Object parentDataObj, Object dataObj, boolean isExistingObject) {
        reasonList.clear();
        STATUS result = STATUS.OK;
        if (!processedRules && dataObj instanceof Treeable<?, ?, ?>) {
            result = checkForSiblingWithSameName(parentDataObj, dataObj, isExistingObject);
            if (result == STATUS.OK) {
                result = checkForRequiredFields(dataObj);
            }
            if (result == STATUS.OK) {
                processedRules = true;
            }
        }
        return result;
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.ui.forms.BaseBusRules#isOkToSave(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
     */
    /*
     * NOTE: If this method is overridden, freeLocks() MUST be called when result is false
     * !!
     * 
     */
    @Override
    public boolean isOkToSave(Object dataObj, DataProviderSessionIFace session) {
        boolean result = super.isOkToSave(dataObj, session);
        if (result && dataObj != null && !StringUtils.contains(formViewObj.getView().getName(), "TreeDef")
                && BaseTreeBusRules.ALLOW_CONCURRENT_FORM_ACCESS) {
            if (!getRequiredLocks(dataObj)) {
                result = false;
                reasonList.add(getUnableToLockMsg());
            }
        }
        return result;
    }

    /**
     * @return true if locks were aquired.
     * 
     * Locks necessary tables prior to a save.
     * Only used when ALLOW_CONCURRENT_FORM_ACCESS is true.
     */
    protected boolean getRequiredLocks(Object dataObj) {
        TreeDefIface<?, ?, ?> treeDef = ((Treeable<?, ?, ?>) dataObj).getDefinition();
        boolean result = !TreeDefStatusMgr.isRenumberingNodes(treeDef)
                && TreeDefStatusMgr.isNodeNumbersAreUpToDate(treeDef);
        if (!result) {
            try {
                Thread.sleep(1500);
                result = !TreeDefStatusMgr.isRenumberingNodes(treeDef)
                        && TreeDefStatusMgr.isNodeNumbersAreUpToDate(treeDef);
            } catch (Exception e) {
                result = false;
            }
        }
        if (result) {
            TaskSemaphoreMgr.USER_ACTION r = TaskSemaphoreMgr.lock(getFormSaveLockTitle(), getFormSaveLockName(),
                    "save", TaskSemaphoreMgr.SCOPE.Discipline, false, new TaskSemaphoreMgrCallerIFace() {

                        /* (non-Javadoc)
                         * @see edu.ku.brc.specify.dbsupport.TaskSemaphoreMgrCallerIFace#resolveConflict(edu.ku.brc.specify.datamodel.SpTaskSemaphore, boolean, java.lang.String)
                         */
                        @Override
                        public USER_ACTION resolveConflict(SpTaskSemaphore semaphore, boolean previouslyLocked,
                                String prevLockBy) {
                            if (System.currentTimeMillis()
                                    - semaphore.getLockedTime().getTime() > FORM_SAVE_LOCK_MAX_DURATION_IN_MILLIS) {
                                //something is clearly wrong with the lock. Ignore it and re-use it. It will be cleared when save succeeds.
                                log.warn("automatically overriding expired " + getFormSaveLockTitle()
                                        + " lock set by " + prevLockBy + " at "
                                        + DateFormat.getDateTimeInstance().format(semaphore.getLockedTime()));
                                return USER_ACTION.OK;
                            } else {
                                return USER_ACTION.Error;
                            }
                        }

                    }, false);
            result = r == TaskSemaphoreMgr.USER_ACTION.OK;
        }
        return result;
    }

    /**
     * @return the class for the generic parameter <T>
     */
    protected abstract Class<?> getNodeClass();

    /**
     * @return the title for the form save lock.
     */
    protected String getFormSaveLockTitle() {
        return String.format(UIRegistry.getResourceString("BaseTreeBusRules.SaveLockTitle"),
                getNodeClass().getSimpleName());
    }

    /**
     * @return the name for the form save lock.
     */
    protected String getFormSaveLockName() {
        return getNodeClass().getSimpleName() + "Save";
    }

    /**
     * @return localized message to display in case of failure to lock for saving.
     */
    protected String getUnableToLockMsg() {
        return UIRegistry.getResourceString("BaseTreeBusRules.UnableToLockForSave");
    }

    /**
     * Free locks acquired for saving.
     */
    protected void freeLocks() {
        TaskSemaphoreMgr.unlock(getFormSaveLockTitle(), getFormSaveLockName(), TaskSemaphoreMgr.SCOPE.Discipline);
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.ui.forms.BaseBusRules#afterSaveCommit(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
     */
    @Override
    public boolean afterSaveCommit(Object dataObj, DataProviderSessionIFace session) {
        boolean result = false;
        if (!super.afterSaveCommit(dataObj, session)) {
            result = false;
        }
        if (BaseTreeBusRules.ALLOW_CONCURRENT_FORM_ACCESS && viewable != null) {
            freeLocks();
        }
        return result;
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.ui.forms.BaseBusRules#afterSaveFailure(java.lang.Object, edu.ku.brc.dbsupport.DataProviderSessionIFace)
     */
    @Override
    public void afterSaveFailure(Object dataObj, DataProviderSessionIFace session) {
        super.afterSaveFailure(dataObj, session);
        if (BaseTreeBusRules.ALLOW_CONCURRENT_FORM_ACCESS && viewable != null) {
            freeLocks();
        }
    }

    /* (non-Javadoc)
     * @see edu.ku.brc.af.ui.forms.BaseBusRules#processBusinessRules(java.lang.Object)
     */
    @Override
    public STATUS processBusinessRules(Object dataObj) {
        STATUS result = STATUS.OK;
        if (!processedRules) {
            result = super.processBusinessRules(dataObj);
            if (result == STATUS.OK) {
                result = checkForSiblingWithSameName(null, dataObj, false);
            }
            if (result == STATUS.OK) {
                result = checkForRequiredFields(dataObj);
            }
        } else {
            processedRules = false;
        }
        return result;
    }

}