au.org.ala.delta.intkey.directives.invocation.UseDirectiveInvocation.java Source code

Java tutorial

Introduction

Here is the source code for au.org.ala.delta.intkey.directives.invocation.UseDirectiveInvocation.java

Source

/*******************************************************************************
 * Copyright (C) 2011 Atlas of Living Australia
 * All Rights Reserved.
 * 
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 ******************************************************************************/
package au.org.ala.delta.intkey.directives.invocation;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JOptionPane;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.FloatRange;

import au.org.ala.delta.intkey.directives.UseDirective;
import au.org.ala.delta.intkey.model.IntkeyCharacterOrder;
import au.org.ala.delta.intkey.model.IntkeyContext;
import au.org.ala.delta.intkey.model.IntkeyDataset;
import au.org.ala.delta.intkey.ui.CharacterSelectionDialog;
import au.org.ala.delta.intkey.ui.DirectivePopulator;
import au.org.ala.delta.intkey.ui.UIUtils;
import au.org.ala.delta.model.Attribute;
import au.org.ala.delta.model.AttributeFactory;
import au.org.ala.delta.model.Character;
import au.org.ala.delta.model.CharacterDependency;
import au.org.ala.delta.model.IntegerAttribute;
import au.org.ala.delta.model.IntegerCharacter;
import au.org.ala.delta.model.Item;
import au.org.ala.delta.model.MultiStateAttribute;
import au.org.ala.delta.model.MultiStateCharacter;
import au.org.ala.delta.model.RealAttribute;
import au.org.ala.delta.model.RealCharacter;
import au.org.ala.delta.model.Specimen;
import au.org.ala.delta.model.TextAttribute;
import au.org.ala.delta.model.TextCharacter;
import au.org.ala.delta.model.format.CharacterFormatter;
import au.org.ala.delta.model.format.Formatter.AngleBracketHandlingMode;
import au.org.ala.delta.model.format.Formatter.CommentStrippingMode;
import au.org.ala.delta.model.format.ItemFormatter;
import au.org.ala.delta.model.impl.SimpleAttributeData;
import au.org.ala.delta.util.Utils;

public class UseDirectiveInvocation extends BasicIntkeyDirectiveInvocation {

    private static final String USE_DIRECTIVE_NAME = "USE";
    private static final String CHANGE_DIRECTIVE_NAME = "CHANGE";

    private Map<au.org.ala.delta.model.Character, Attribute> _characterAttributes;
    private boolean _change;
    private boolean _suppressAlreadySetWarning;
    private CharacterFormatter _charFormatter;
    private ItemFormatter _taxonFormatter;

    private StringBuilder _stringRepresentationBuilder;
    private List<String> _stringRepresentationParts;

    // True if any updates were made to the specimen.
    private boolean _specimenUpdated = false;

    public UseDirectiveInvocation(boolean change, boolean suppressAlreadySetWarning,
            List<String> stringRepresentationParts) {
        _change = change;
        _suppressAlreadySetWarning = suppressAlreadySetWarning;

        // Use LinkedHashMap so that keys can be iterated over in the order
        // that they
        // were inserted.
        _characterAttributes = new LinkedHashMap<au.org.ala.delta.model.Character, Attribute>();

        _charFormatter = new CharacterFormatter(true, CommentStrippingMode.RETAIN, AngleBracketHandlingMode.REPLACE,
                true, false);
        _taxonFormatter = new ItemFormatter(false, CommentStrippingMode.STRIP_ALL, AngleBracketHandlingMode.REMOVE,
                true, false, false);

        _stringRepresentationParts = stringRepresentationParts;
    }

    @Override
    public boolean execute(IntkeyContext context) {
        _stringRepresentationBuilder = new StringBuilder();

        String directiveName = _change ? CHANGE_DIRECTIVE_NAME : USE_DIRECTIVE_NAME;

        _stringRepresentationBuilder.append(directiveName);
        _stringRepresentationBuilder.append(" ");

        if (_suppressAlreadySetWarning) {
            _stringRepresentationBuilder.append(UseDirective.SUPPRESS_ALREADY_SET_WARNING_FLAG + " ");
        }

        // Split up characters that have had their values specified and
        // those that
        // haven't. They need to be processed differently.
        List<au.org.ala.delta.model.Character> charsWithValuesSpecified = new ArrayList<au.org.ala.delta.model.Character>();
        List<au.org.ala.delta.model.Character> charsNoValuesSpecified = new ArrayList<au.org.ala.delta.model.Character>();

        for (au.org.ala.delta.model.Character ch : _characterAttributes.keySet()) {
            if (_characterAttributes.get(ch) == null) {
                charsNoValuesSpecified.add(ch);
            } else {
                charsWithValuesSpecified.add(ch);
            }
        }

        // Process characters with values specified first
        for (au.org.ala.delta.model.Character ch : charsWithValuesSpecified) {
            if (checkCharacterUsable(ch, context, !_suppressAlreadySetWarning && !_change)) {
                // Not interested in what controlling characters were
                // set/manually set here. Only need this when prompting for
                // character values (as the user can hit cancel, or hit OK with
                // no values input)
                if (!processControllingCharacters(ch, context, new ArrayList<Character>(),
                        new ArrayList<Character>())) {
                    // halt execution if values not sucessfully set for all
                    // controlling characters
                    if (_specimenUpdated) {
                        context.specimenUpdateComplete();
                    }
                    return false;
                }

                // second call to checkCharacterUsable() to ensure that
                // character has not been
                // made inapplicable by the value given to one or more of
                // its controlling
                // characters
                if (checkCharacterUsable(ch, context, false)) {
                    Attribute attr = _characterAttributes.get(ch);
                    setValueForCharacter(ch, attr, context);
                } else {
                    if (_specimenUpdated) {
                        context.specimenUpdateComplete();
                    }
                    return false;
                }
            } else {
                if (_specimenUpdated) {
                    context.specimenUpdateComplete();
                }
                return false;
            }
        }

        if (charsNoValuesSpecified.size() == 1) {
            au.org.ala.delta.model.Character ch = charsNoValuesSpecified.get(0);
            if (checkCharacterUsable(ch, context, !_suppressAlreadySetWarning && !_change)) {

                List<Character> setControllingCharacters = new ArrayList<Character>();
                List<Character> manuallySetControllingCharacters = new ArrayList<Character>();
                if (!processControllingCharacters(ch, context, setControllingCharacters,
                        manuallySetControllingCharacters)) {
                    // halt execution if values not sucessfully set for all
                    // controlling characters
                    if (_specimenUpdated) {
                        context.specimenUpdateComplete();
                    }
                    return false;
                }

                // second call to checkCharacterUsable() to ensure that
                // character has not been
                // made inapplicable by the value given to one or more of
                // its controlling
                // characters
                if (checkCharacterUsable(ch, context, false)) {
                    Attribute attr = promptForCharacterValue(ch, null, null, context.getDirectivePopulator(),
                            context.getSpecimen(), context);
                    if (attr != null) {
                        // store this value so that the prompt does not need
                        // to
                        // be done for subsequent invocations
                        _characterAttributes.put(ch, attr);
                        setValueForCharacter(ch, attr, context);
                    } else {
                        // User hit cancel or did not enter a value when
                        // prompted.
                        // Abort execution when this happens
                        cleanupSpecimenAfterCancel(setControllingCharacters, manuallySetControllingCharacters,
                                context.getSpecimen());
                        if (_specimenUpdated) {
                            context.specimenUpdateComplete();
                        }
                        return false;
                    }
                } else {
                    cleanupSpecimenAfterCancel(setControllingCharacters, manuallySetControllingCharacters,
                            context.getSpecimen());
                    if (_specimenUpdated) {
                        context.specimenUpdateComplete();
                    }
                    return false;
                }
            } else {
                if (_specimenUpdated) {
                    context.specimenUpdateComplete();
                }
                return false;
            }
        } else {
            List<Character> charsNoValuesCopy = new ArrayList<Character>(charsNoValuesSpecified);
            Collections.sort(charsNoValuesCopy);
            while (!charsNoValuesCopy.isEmpty()) {
                CharacterSelectionDialog selectDlg = new CharacterSelectionDialog(UIUtils.getMainFrame(),
                        charsNoValuesCopy, directiveName, context.getImageSettings(), context.displayNumbering(),
                        context);
                selectDlg.setVisible(true);

                List<au.org.ala.delta.model.Character> selectedCharacters = selectDlg.getSelectedCharacters();

                if (selectedCharacters.isEmpty()) {
                    // User hit cancel or did not select any characters.
                    // Abort
                    // execution when this happens.
                    // Directive should not be stored in
                    // Execution history
                    if (_specimenUpdated) {
                        context.specimenUpdateComplete();
                    }
                    return false;
                }

                for (au.org.ala.delta.model.Character ch : selectedCharacters) {

                    Attribute attr = null;

                    if (checkCharacterUsable(ch, context, !_suppressAlreadySetWarning && !_change)) {

                        List<Character> setControllingCharacters = new ArrayList<Character>();
                        List<Character> manuallySetControllingCharacters = new ArrayList<Character>();
                        if (!processControllingCharacters(ch, context, setControllingCharacters,
                                manuallySetControllingCharacters)) {
                            // halt execution if values not sucessfully set for
                            // all
                            // controlling characters
                            if (_specimenUpdated) {
                                context.specimenUpdateComplete();
                            }
                            return false;
                        }

                        // As the character's controlling characters have
                        // been successfully
                        // set, remove all of them from the list of
                        // characters needing to have
                        // their values set.
                        IntkeyDataset dataset = context.getDataset();
                        Map<MultiStateCharacter, Set<Integer>> charDeps = getAllControllingCharacterDependencies(ch,
                                dataset);
                        for (MultiStateCharacter controllingChar : charDeps.keySet()) {
                            charsNoValuesCopy.remove(controllingChar);
                        }

                        // second call to checkCharacterUsable() to ensure
                        // that character has not been
                        // made inapplicable by the value given to one or
                        // more of its controlling
                        // characters
                        if (checkCharacterUsable(ch, context, false)) {
                            attr = promptForCharacterValue(ch, null, null, context.getDirectivePopulator(),
                                    context.getSpecimen(), context);
                        } else {
                            cleanupSpecimenAfterCancel(setControllingCharacters, manuallySetControllingCharacters,
                                    context.getSpecimen());
                            if (_specimenUpdated) {
                                context.specimenUpdateComplete();
                            }
                            return false;
                        }

                        if (attr != null) {
                            // store this value so that the prompt does not
                            // need
                            // to
                            // be done for subsequent invocations
                            _characterAttributes.put(ch, attr);
                            setValueForCharacter(ch, attr, context);

                            charsNoValuesCopy.remove(ch);
                        } else {
                            cleanupSpecimenAfterCancel(setControllingCharacters, manuallySetControllingCharacters,
                                    context.getSpecimen());
                            if (_specimenUpdated) {
                                context.specimenUpdateComplete();
                            }
                        }
                    } else {
                        if (_specimenUpdated) {
                            context.specimenUpdateComplete();
                        }
                        return false;
                    }
                }
            }
        }

        // Finish building string representation of directive call
        for (Character ch : charsNoValuesSpecified) {
            // Generate a string representation of the value set for the
            // character

            Attribute attr = _characterAttributes.get(ch);
            int strRepIndex = _stringRepresentationParts.indexOf(Integer.toString(ch.getCharacterId()));
            _stringRepresentationParts.remove(strRepIndex);
            String strRepresentation = Integer.toString(ch.getCharacterId()) + ","
                    + formatAttributeValueForLog(attr);
            _stringRepresentationParts.add(strRepIndex, strRepresentation);
        }

        _stringRepresentationBuilder.append(StringUtils.join(_stringRepresentationParts, " "));

        context.appendToLog("*" + _stringRepresentationBuilder.toString());
        if (_specimenUpdated) {
            context.specimenUpdateComplete();
        }
        return true;
    }

    private String formatAttributeValueForLog(Attribute attr) {
        if (attr instanceof TextAttribute) {
            return ((TextAttribute) attr).getText();
        } else if (attr instanceof MultiStateAttribute) {
            return Utils.formatIntegersAsListOfRanges(((MultiStateAttribute) attr).getPresentStatesAsList(), "/",
                    "-");
        } else if (attr instanceof IntegerAttribute) {
            Set<Integer> presentValues = ((IntegerAttribute) attr).getPresentValues();
            List<Integer> presentValuesList = new ArrayList<Integer>(presentValues);
            Collections.sort(presentValuesList);
            return Utils.formatIntegersAsListOfRanges(presentValuesList, "/", "-");
        } else if (attr instanceof RealAttribute) {
            FloatRange range = ((RealAttribute) attr).getPresentRange();
            StringBuilder builder = new StringBuilder();
            float min = range.getMinimumFloat();
            float max = range.getMaximumFloat();

            if (min == max) {
                builder.append(min);
            } else {
                builder.append(min);
                builder.append("-");
                builder.append(max);
            }
            return builder.toString();
        } else {
            throw new IllegalArgumentException("Unrecognised attribute type!");
        }
    }

    public void addCharacterValue(au.org.ala.delta.model.Character ch, Attribute attr) {
        _characterAttributes.put(ch, attr);
    }

    private void setValueForCharacter(au.org.ala.delta.model.Character ch, Attribute attr, IntkeyContext context) {
        context.setSpecimenAttributeForCharacter(ch, attr);

        // If using the SEPARATE character order, give the user an opportunity
        // to change the value if it results in
        // the elimination of the taxon being separated.
        if (context.getCharacterOrder() == IntkeyCharacterOrder.SEPARATE) {
            Item taxonToSeparate = context.getDataset().getItem(context.getTaxonToSeparate());
            if (!context.getAvailableTaxa().contains(taxonToSeparate)) {
                boolean changeValue = context.getDirectivePopulator()
                        .promptForYesNoOption(UIUtils.getResourceString("UseDirective.TaxonToSeparateEliminatedMsg",
                                _charFormatter.formatCharacterDescription(ch),
                                _taxonFormatter.formatItemDescription(taxonToSeparate)));

                if (changeValue) {
                    Attribute newAttr = promptForCharacterValue(ch, null, null, context.getDirectivePopulator(),
                            context.getSpecimen(), context);
                    if (newAttr != null) {
                        _characterAttributes.put(ch, newAttr);
                        setValueForCharacter(ch, newAttr, context);
                    }
                }

            }
        }

        _specimenUpdated = true;
    }

    private boolean checkCharacterUsable(au.org.ala.delta.model.Character ch, IntkeyContext context,
            boolean warnAlreadySetOrMaybeInapplicable) {
        CharacterFormatter formatter = new CharacterFormatter(true, CommentStrippingMode.RETAIN,
                AngleBracketHandlingMode.REPLACE, true, false);

        // is character fixed?
        if (context.charactersFixed()) {
            if (context.getFixedCharactersList().contains(ch.getCharacterId())) {
                if (!context.isProcessingDirectivesFile()) {
                    String msg = UIUtils.getResourceString("UseDirective.CharacterFixed",
                            formatter.formatCharacterDescription(ch));
                    String title = UIUtils.getResourceString("Intkey.informationDlgTitle");
                    JOptionPane.showMessageDialog(UIUtils.getMainFrame(), msg, title, JOptionPane.ERROR_MESSAGE);
                }
                return false;
            }
        }

        // is character already used?
        if (warnAlreadySetOrMaybeInapplicable) {
            if (context.getSpecimen().hasValueFor(ch)) {
                if (context.isProcessingDirectivesFile()) {
                    return true;
                } else {
                    String msg = UIUtils.getResourceString("UseDirective.CharacterAlreadyUsed",
                            formatter.formatCharacterDescription(ch));
                    String title = UIUtils.getResourceString("Intkey.informationDlgTitle");
                    int choice = JOptionPane.showConfirmDialog(UIUtils.getMainFrame(), msg, title,
                            JOptionPane.YES_NO_OPTION);
                    if (choice == JOptionPane.YES_OPTION) {
                        return true;
                    } else {
                        return false;
                    }
                }
            }
        }

        // is character maybe inapplicable?
        if (warnAlreadySetOrMaybeInapplicable) {
            if (context.getSpecimen().isCharacterMaybeInapplicable(ch)) {
                if (context.isProcessingDirectivesFile()) {
                    return true;
                } else {
                    String msg = UIUtils.getResourceString("UseDirective.CharacterMaybeInapplicable",
                            formatter.formatCharacterDescription(ch));
                    String title = UIUtils.getResourceString("Intkey.informationDlgTitle");
                    int choice = JOptionPane.showConfirmDialog(UIUtils.getMainFrame(), msg, title,
                            JOptionPane.YES_NO_OPTION);
                    if (choice == JOptionPane.YES_OPTION) {
                        return true;
                    } else {
                        return false;
                    }
                }
            }
        }

        // is character unavailable?
        if (context.getSpecimen().isCharacterInapplicable(ch)) {
            if (!context.isProcessingDirectivesFile()) {
                String msg = UIUtils.getResourceString("UseDirective.CharacterUnavailable",
                        formatter.formatCharacterDescription(ch));
                String title = UIUtils.getResourceString("Intkey.informationDlgTitle",
                        formatter.formatCharacterDescription(ch));
                JOptionPane.showMessageDialog(UIUtils.getMainFrame(), msg, title, JOptionPane.ERROR_MESSAGE);
            }
            return false;
        }

        // is character excluded?
        if (!context.getIncludedCharacters().contains(ch)) {
            if (!context.isProcessingDirectivesFile()) {
                String msg = UIUtils.getResourceString("UseDirective.CharacterExcluded",
                        formatter.formatCharacterDescription(ch));
                String title = UIUtils.getResourceString("Intkey.informationDlgTitle",
                        formatter.formatCharacterDescription(ch));
                JOptionPane.showMessageDialog(UIUtils.getMainFrame(), msg, title, JOptionPane.ERROR_MESSAGE);
            }
            return false;
        }

        return true;
    }

    /**
     * Set values for all controlling characters for the specified character,
     * prompting if necessary
     * 
     * @param ch
     * @param context
     * @param controllingCharAttributes
     *            - should be an empty list. Will be populated with the
     *            attribute values set in the specimen for the controlling
     *            characters.
     * @return true if values were set successfully for all controlling
     *         characters
     */
    private boolean processControllingCharacters(au.org.ala.delta.model.Character ch, IntkeyContext context,
            List<Character> setControllingCharacters, List<Character> manuallySetControllingCharacters) {

        // Map of controlling characters to states of these controlling
        // characters that will cause this character to be inapplicable
        Map<MultiStateCharacter, Set<Integer>> controllingCharInapplicableStates = getAllControllingCharacterDependencies(
                ch, context.getDataset());

        // Sort the controlling characters such that any character in the list
        // is after all characters in the list that control it.
        List<MultiStateCharacter> controllingCharsList = new ArrayList<MultiStateCharacter>(
                controllingCharInapplicableStates.keySet());
        Collections.sort(controllingCharsList, new ControllingCharacterComparator());

        // For each controlling character, set its value or prompt the user
        // for its value as appropriate
        for (MultiStateCharacter cc : controllingCharsList) {
            // If the controlling character already has a value set for it in
            // the specimen, do not modify it.
            if (context.getSpecimen().hasValueFor(cc)) {
                continue;
            }

            // Need to check that the character is still applicable, as the
            // value set by the user for one of
            // the controlling characters may have made it inapplicable
            if (!checkCharacterUsable(ch, context, false)) {
                return false;
            }

            Set<Integer> inapplicableStates = controllingCharInapplicableStates.get(cc);

            // states for the controlling character that will make dependent
            // characters applicable. At least one of these states needs to
            // be
            // set on the controlling character.
            Set<Integer> applicableStates = new HashSet<Integer>();

            for (int i = 1; i < cc.getStates().length + 1; i++) {
                if (!inapplicableStates.contains(i)) {
                    applicableStates.add(i);
                }
            }

            if (applicableStates.size() == cc.getStates().length) {
                throw new RuntimeException(MessageFormat.format(
                        "There are no states for character %s that will make character {0} applicable",
                        cc.getCharacterId(), ch.getCharacterId()));
            }

            // If not processing an input file, prompt the user to set the
            // value of the controlling character if it has been supplied as
            // an argument to the
            // NON AUTOMATIC CONTROLLING CHARACTERS confor directive, if the
            // dependent character has been supplied as an argument
            // to the USE CONTROLLING CHARACTERS FIRST confor directive, or
            // if there are multiple states that the controlling character
            // can be set to for which the dependent character will be
            // applicable.
            if (!context.isProcessingDirectivesFile() && (cc.getNonAutoCc() || ch.getUseCc()
                    || !cc.getNonAutoCc() && !cc.getUseCc() && applicableStates.size() > 1)) {
                Attribute attr = promptForCharacterValue(cc, ch, applicableStates, context.getDirectivePopulator(),
                        context.getSpecimen(), context);
                if (attr != null) {
                    context.setSpecimenAttributeForCharacter(cc, attr);
                    _specimenUpdated = true;
                    setControllingCharacters.add(cc);
                    manuallySetControllingCharacters.add(cc);
                } else {
                    // No values selected or cancel pressed. Return as
                    // values have not been set for all
                    // controlling characters
                    return false;
                }
            } else {
                // let intkey automatically use the character
                SimpleAttributeData impl = new SimpleAttributeData(false, false);
                impl.setPresentStateOrIntegerValues(applicableStates);
                Attribute attr = AttributeFactory.newAttribute(cc, impl);
                attr.setSpecimenAttribute(true);
                context.setSpecimenAttributeForCharacter(cc, attr);
                _specimenUpdated = true;
                setControllingCharacters.add(cc);
            }

            // output USEd controlling characters directly to the log
            // window

            context.appendToLog(MessageFormat.format("    *USE {0},{1}", cc.getCharacterId(),
                    StringUtils.join(applicableStates, "/")));
        }

        return true;
    }

    /**
     * For the given character, build a map of all its controlling characters,
     * and the states of these controlling characters that will make the
     * supplied character inapplicable.
     * 
     * @param ch
     * @param ds
     * @return
     */
    private Map<MultiStateCharacter, Set<Integer>> getAllControllingCharacterDependencies(
            au.org.ala.delta.model.Character ch, IntkeyDataset ds) {
        HashMap<MultiStateCharacter, Set<Integer>> retMap = new HashMap<MultiStateCharacter, Set<Integer>>();

        List<CharacterDependency> controllingDependencies = ch.getControllingCharacters();

        for (CharacterDependency cd : controllingDependencies) {

            // A controlling character must be a multistate character
            MultiStateCharacter cc = (MultiStateCharacter) ds.getCharacter(cd.getControllingCharacterId());

            // states for the controlling character that will make dependent
            // characters inapplicable
            Set<Integer> inapplicableStates = cd.getStates();

            if (retMap.containsKey(cc)) {
                retMap.get(cc).addAll(inapplicableStates);
            } else {
                retMap.put(cc, new HashSet<Integer>(inapplicableStates));
            }

            retMap.putAll(getAllControllingCharacterDependencies(cc, ds));
        }

        return retMap;
    }

    private Attribute promptForCharacterValue(au.org.ala.delta.model.Character ch,
            au.org.ala.delta.model.Character dependentChar, Set<Integer> applicableStates,
            DirectivePopulator populator, Specimen specimen, IntkeyContext context) {
        SimpleAttributeData impl = new SimpleAttributeData(false, false);

        if (ch instanceof MultiStateCharacter) {
            Set<Integer> stateValues;
            if (dependentChar != null) {
                // We are setting a value for a controlling character. Ensure
                // that the states that make the dependent character applicable
                // are pre-selected.
                stateValues = populator.promptForMultiStateValue((MultiStateCharacter) ch, applicableStates,
                        dependentChar);
            } else {
                // Get the current value of the character as set in the
                // specimen. If no
                // value is set for the character, an attribute with
                // unknown = true is returned.
                MultiStateAttribute currentAttribute = (MultiStateAttribute) specimen.getAttributeForCharacter(ch);
                stateValues = populator.promptForMultiStateValue((MultiStateCharacter) ch,
                        currentAttribute.isUnknown() ? null : currentAttribute.getPresentStates(), null);
            }
            if (stateValues != null && stateValues.isEmpty() && dependentChar == null) {
                // User hit OK but did not select any states. Delete any value
                // set
                // for the character.
                if (specimen.hasValueFor(ch)) {
                    deleteCharacter(ch, context);
                }
                return null;
            } else if (stateValues != null && stateValues.size() > 0) {
                impl.setPresentStateOrIntegerValues(stateValues);
            } else {
                return null;
            }
        } else if (ch instanceof IntegerCharacter) {
            // Get the current value of the character as set in the specimen. If
            // no
            // value is set for the character, an attribute with
            // unknown = true is returned.
            IntegerAttribute currentAttribute = (IntegerAttribute) specimen.getAttributeForCharacter(ch);

            Set<Integer> intValue = populator.promptForIntegerValue((IntegerCharacter) ch,
                    currentAttribute.isUnknown() ? null : currentAttribute.getPresentValues());

            if (intValue != null && intValue.isEmpty()) {
                // User hit OK but did not input any values. Delete any value
                // set
                // for the character
                if (specimen.hasValueFor(ch)) {
                    deleteCharacter(ch, context);
                }
                return null;
            } else if (intValue != null && intValue.size() > 0) {
                impl.setPresentStateOrIntegerValues(intValue);
            } else {
                return null;
            }
        } else if (ch instanceof RealCharacter) {
            // Get the current value of the character as set in the specimen. If
            // no
            // value is set for the character, an attribute with
            // unknown = true is returned.
            RealAttribute currentAttribute = (RealAttribute) specimen.getAttributeForCharacter(ch);

            FloatRange floatRange = populator.promptForRealValue((RealCharacter) ch,
                    currentAttribute.isUnknown() ? null : currentAttribute.getPresentRange());

            if (floatRange != null) {
                // Float range with minimum and maximum value as negative
                // infinity indicates that the user hit OK without inputting
                // values.
                if (floatRange.getMaximumFloat() == Float.NEGATIVE_INFINITY
                        && floatRange.getMinimumFloat() == Float.NEGATIVE_INFINITY) {
                    // User hit OK but did not input any values. Delete any
                    // value set
                    // for the character
                    if (specimen.hasValueFor(ch)) {
                        deleteCharacter(ch, context);
                    }
                    return null;
                }
                impl.setRealRange(floatRange);
            } else {
                return null;
            }
        } else if (ch instanceof TextCharacter) {
            // Get the current value of the character as set in the specimen. If
            // no
            // value is set for the character, an attribute with
            // unknown = true is returned.
            TextAttribute currentAttribute = (TextAttribute) specimen.getAttributeForCharacter(ch);

            List<String> stringList = populator.promptForTextValue((TextCharacter) ch,
                    currentAttribute.isUnknown() ? null
                            : Arrays.asList(currentAttribute.getValueAsString().split("/")));

            if (stringList != null && stringList.isEmpty()) {
                // User hit OK but did not input any values. Delete any
                // value set
                // for the character
                if (specimen.hasValueFor(ch)) {
                    deleteCharacter(ch, context);
                }
                return null;
            } else if (stringList != null && stringList.size() > 0) {
                impl.setValueFromString(StringUtils.join(stringList, '/'));
            } else {
                return null;
            }
        } else {
            throw new IllegalArgumentException("Unrecognized character type");
        }

        Attribute attr = AttributeFactory.newAttribute(ch, impl);
        attr.setSpecimenAttribute(true);

        return attr;
    }

    private void deleteCharacter(Character ch, IntkeyContext context) {
        List<Character> charInList = new ArrayList<Character>();
        charInList.add(ch);
        DeleteDirectiveInvocation deleteInvoc = new DeleteDirectiveInvocation();
        deleteInvoc.setCharacters(charInList);
        deleteInvoc.setStringRepresentation("DELETE " + ch.getCharacterId());

        context.executeDirective(deleteInvoc);
    }

    /**
     * Cleans up the specimen after the prompt for a character value is
     * cancelled, or returns successfully but with no values specified. The
     * values for any automatically-set controlling characters are removed from
     * the specimen, unless they are higher in the dependency hierarchy than a
     * manually set controlling character.
     * 
     * @param setControllingCharacters
     *            A list of all controlling characters that were set, in the
     *            order that they were set.
     * @param manuallySetControllingCharacters
     *            A list of all manually set controlling characters, in the
     *            order that they were set
     * @param specimen
     *            the specimen.
     */
    private void cleanupSpecimenAfterCancel(List<Character> setControllingCharacters,
            List<Character> manuallySetControllingCharacters, Specimen specimen) {
        List<Character> charsToRemoveFromSpecimen = new ArrayList<Character>();

        if (manuallySetControllingCharacters.isEmpty()) {
            charsToRemoveFromSpecimen.addAll(setControllingCharacters);
        } else {
            Character lastManuallySetCharacter = manuallySetControllingCharacters
                    .get(manuallySetControllingCharacters.size() - 1);
            charsToRemoveFromSpecimen.addAll(
                    setControllingCharacters.subList(setControllingCharacters.indexOf(lastManuallySetCharacter) + 1,
                            setControllingCharacters.size()));
        }

        for (Character ch : charsToRemoveFromSpecimen) {
            specimen.removeValueForCharacter(ch);
        }

        // If all values set for controlling characters have been removed, the
        // net change to the specimen is zero. Hence it does not need to be
        // considered updated.
        if (charsToRemoveFromSpecimen.equals(setControllingCharacters)) {
            _specimenUpdated = false;
        }
    }

    @Override
    public String toString() {
        return _stringRepresentationBuilder.toString();
    }

    /**
     * Comparator used to sort a list of characters such that all of a
     * character's controlling characters will appear before it in the sorted
     * order.
     * 
     * @author ChrisF
     * 
     */
    private static class ControllingCharacterComparator implements Comparator<au.org.ala.delta.model.Character> {
        @Override
        public int compare(au.org.ala.delta.model.Character c1, au.org.ala.delta.model.Character c2) {
            if (isControlledBy(c1, c2)) {
                return 1;
            } else if (isControlledBy(c2, c2)) {
                return -1;
            } else {
                return 0;
            }
        }

        private boolean isControlledBy(au.org.ala.delta.model.Character c1, au.org.ala.delta.model.Character c2) {
            boolean result = false;

            List<CharacterDependency> controllingDependencies = c1.getControllingCharacters();

            for (CharacterDependency cd : controllingDependencies) {
                if (cd.getControllingCharacterId() == c2.getCharacterId()) {
                    result = true;
                    break;
                }
            }

            return result;
        }
    }
}