org.sakaiproject.tool.assessment.services.GradingService.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.tool.assessment.services.GradingService.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (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.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.tool.assessment.services;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.math.complex.Complex;
import org.apache.commons.math.complex.ComplexFormat;
import org.apache.commons.math.util.MathUtils;
import org.sakaiproject.event.cover.EventTrackingService;
import org.sakaiproject.service.gradebook.shared.GradebookExternalAssessmentService;
import org.sakaiproject.spring.SpringBeanLocator;
import org.sakaiproject.tool.assessment.data.dao.grading.AssessmentGradingData;
import org.sakaiproject.tool.assessment.data.dao.grading.ItemGradingAttachment;
import org.sakaiproject.tool.assessment.data.dao.grading.ItemGradingData;
import org.sakaiproject.tool.assessment.data.dao.grading.MediaData;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AnswerIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AssessmentIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.EvaluationModelIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemMetaDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemTextIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.PublishedAssessmentIfc;
import org.sakaiproject.tool.assessment.data.ifc.grading.StudentGradingSummaryIfc;
import org.sakaiproject.tool.assessment.data.ifc.shared.TypeIfc;
import org.sakaiproject.tool.assessment.facade.AgentFacade;
import org.sakaiproject.tool.assessment.facade.GradebookFacade;
import org.sakaiproject.tool.assessment.facade.TypeFacade;
import org.sakaiproject.tool.assessment.facade.TypeFacadeQueriesAPI;
import org.sakaiproject.tool.assessment.integration.context.IntegrationContextFactory;
import org.sakaiproject.tool.assessment.integration.helper.ifc.GradebookServiceHelper;
import org.sakaiproject.tool.assessment.services.assessment.PublishedAssessmentService;
import org.sakaiproject.tool.assessment.util.SamigoExpressionError;
import org.sakaiproject.tool.assessment.util.SamigoExpressionParser;

/**
 * The GradingService calls the back end to get/store grading information. 
 * It also calculates scores for autograded types.
 */
public class GradingService {
    /**
     *   Key for a complext numeric answer e.g. 9+9i 
     */
    public static final String ANSWER_TYPE_COMPLEX = "COMPLEX";

    /**
     * key for a real number representation e.g 1 or 10E5 
     */
    public static final String ANSWER_TYPE_REAL = "REAL";

    // CALCULATED_QUESTION
    final String OPEN_BRACKET = "\\{";
    final String CLOSE_BRACKET = "\\}";
    final String CALCULATION_OPEN = "[["; // not regex safe
    final String CALCULATION_CLOSE = "]]"; // not regex safe
    /**
     * regular expression for matching the contents of a variable or formula name 
     * in Calculated Questions
     * NOTE: Old regex: ([\\w\\s\\.\\-\\^\\$\\!\\&\\@\\?\\*\\%\\(\\)\\+=#`~&:;|,/<>\\[\\]\\\\\\'\"]+?)
     * was way too complicated.
     */
    final String CALCQ_VAR_FORM_NAME = "[a-zA-Z][^\\{\\}]*?"; // non-greedy (must start wtih alpha)
    final String CALCQ_VAR_FORM_NAME_EXPRESSION = "(" + CALCQ_VAR_FORM_NAME + ")";

    // variable match - (?<!\{)\{([^\{\}]+?)\}(?!\}) - means any sequence inside braces without a braces before or after
    final Pattern CALCQ_ANSWER_PATTERN = Pattern
            .compile("(?<!\\{)" + OPEN_BRACKET + CALCQ_VAR_FORM_NAME_EXPRESSION + CLOSE_BRACKET + "(?!\\})");
    final Pattern CALCQ_FORMULA_PATTERN = Pattern
            .compile(OPEN_BRACKET + OPEN_BRACKET + CALCQ_VAR_FORM_NAME_EXPRESSION + CLOSE_BRACKET + CLOSE_BRACKET);
    final Pattern CALCQ_FORMULA_SPLIT_PATTERN = Pattern
            .compile("(" + OPEN_BRACKET + OPEN_BRACKET + CALCQ_VAR_FORM_NAME + CLOSE_BRACKET + CLOSE_BRACKET + ")");
    final Pattern CALCQ_CALCULATION_PATTERN = Pattern.compile("\\[\\[([^\\[\\]]+?)\\]\\]?"); // non-greedy

    private Log log = LogFactory.getLog(GradingService.class);

    /**
     * Get all scores for a published assessment from the back end.
     */
    public ArrayList getTotalScores(String publishedId, String which) {
        ArrayList results = null;
        try {
            results = new ArrayList(PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getTotalScores(publishedId, which));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public ArrayList getTotalScores(String publishedId, String which, boolean getSubmittedOnly) {
        ArrayList results = null;
        try {
            results = new ArrayList(PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getTotalScores(publishedId, which, getSubmittedOnly));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    /**
     * Get all submissions for a published assessment from the back end.
     */
    public List getAllSubmissions(String publishedId) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getAllSubmissions(publishedId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List getAllAssessmentGradingData(Long publishedId) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getAllAssessmentGradingData(publishedId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public ArrayList getHighestAssessmentGradingList(Long publishedId) {
        ArrayList results = null;
        try {
            results = new ArrayList(PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getHighestAssessmentGradingList(publishedId));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List getHighestSubmittedOrGradedAssessmentGradingList(Long publishedId) {
        ArrayList results = null;
        try {
            results = new ArrayList(PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getHighestSubmittedOrGradedAssessmentGradingList(publishedId));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public ArrayList getLastAssessmentGradingList(Long publishedId) {
        ArrayList results = null;
        try {
            results = new ArrayList(PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastAssessmentGradingList(publishedId));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List getLastSubmittedAssessmentGradingList(Long publishedId) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastSubmittedAssessmentGradingList(publishedId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List getLastSubmittedOrGradedAssessmentGradingList(Long publishedId) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastSubmittedOrGradedAssessmentGradingList(publishedId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public void saveTotalScores(ArrayList gdataList, PublishedAssessmentIfc pub) {
        //log.debug("**** GradingService: saveTotalScores");
        try {
            AssessmentGradingData gdata = null;
            if (gdataList.size() > 0)
                gdata = (AssessmentGradingData) gdataList.get(0);
            else
                return;

            Integer scoringType = getScoringType(pub);
            ArrayList oldList = getAssessmentGradingsByScoringType(scoringType, gdata.getPublishedAssessmentId());
            for (int i = 0; i < gdataList.size(); i++) {
                AssessmentGradingData ag = (AssessmentGradingData) gdataList.get(i);
                saveOrUpdateAssessmentGrading(ag);
                EventTrackingService.post(EventTrackingService.newEvent("sam.total.score.update",
                        "siteId=" + AgentFacade.getCurrentSiteId() + ", gradedBy=" + AgentFacade.getAgentString()
                                + ", assessmentGradingId=" + ag.getAssessmentGradingId() + ", totalAutoScore="
                                + ag.getTotalAutoScore() + ", totalOverrideScore=" + ag.getTotalOverrideScore()
                                + ", FinalScore=" + ag.getFinalScore() + ", comments=" + ag.getComments(),
                        true));
            }

            // no need to notify gradebook if this submission is not for grade
            // we only want to notify GB when there are changes
            ArrayList newList = getAssessmentGradingsByScoringType(scoringType, gdata.getPublishedAssessmentId());
            ArrayList l = getListForGradebookNotification(newList, oldList);

            notifyGradebook(l, pub);
        } catch (GradebookServiceException ge) {
            log.error("GradebookServiceException" + ge);
            throw ge;
        }
    }

    private ArrayList getListForGradebookNotification(ArrayList newList, ArrayList oldList) {
        ArrayList l = new ArrayList();
        HashMap h = new HashMap();
        for (int i = 0; i < oldList.size(); i++) {
            AssessmentGradingData ag = (AssessmentGradingData) oldList.get(i);
            h.put(ag.getAssessmentGradingId(), ag);
        }

        for (int i = 0; i < newList.size(); i++) {
            AssessmentGradingData a = (AssessmentGradingData) newList.get(i);
            Object o = h.get(a.getAssessmentGradingId());
            if (o == null) { // this does not exist in old list, so include it for update
                l.add(a);
            } else { // if new is different from old, include it for update
                AssessmentGradingData b = (AssessmentGradingData) o;
                if ((a.getFinalScore() != null && b.getFinalScore() != null)
                        && !a.getFinalScore().equals(b.getFinalScore())) {
                    l.add(a);
                }
                // if scores are not modified but comments are added, include it for update
                else if (a.getComments() != null) {
                    if (b.getComments() != null) {
                        if (!a.getComments().equals(b.getComments())) {
                            l.add(a);
                        }
                    } else {
                        l.add(a);
                    }
                }
            }
        }
        return l;
    }

    public ArrayList getAssessmentGradingsByScoringType(Integer scoringType, Long publishedAssessmentId) {
        List l = null;
        // get the list of highest score
        if ((scoringType).equals(EvaluationModelIfc.HIGHEST_SCORE)) {
            l = getHighestSubmittedOrGradedAssessmentGradingList(publishedAssessmentId);
        }
        // get the list of last score
        else if ((scoringType).equals(EvaluationModelIfc.LAST_SCORE)) {
            l = getLastSubmittedOrGradedAssessmentGradingList(publishedAssessmentId);
        } else {
            l = getTotalScores(publishedAssessmentId.toString(), "3", false);
        }
        return new ArrayList(l);
    }

    public Integer getScoringType(PublishedAssessmentIfc pub) {
        Integer scoringType = null;
        EvaluationModelIfc e = pub.getEvaluationModel();
        if (e != null) {
            scoringType = e.getScoringType();
        }
        return scoringType;
    }

    private boolean updateGradebook(AssessmentGradingData data, PublishedAssessmentIfc pub) {
        // no need to notify gradebook if this submission is not for grade
        boolean forGrade = (Boolean.TRUE).equals(data.getForGrade());

        boolean toGradebook = false;
        EvaluationModelIfc e = pub.getEvaluationModel();
        if (e != null) {
            String toGradebookString = e.getToGradeBook();
            toGradebook = toGradebookString.equals(EvaluationModelIfc.TO_DEFAULT_GRADEBOOK.toString());
        }
        return (forGrade && toGradebook);
    }

    private void notifyGradebook(ArrayList l, PublishedAssessmentIfc pub) {
        for (int i = 0; i < l.size(); i++) {
            notifyGradebook((AssessmentGradingData) l.get(i), pub);
        }
    }

    /**
     * Get the score information for each item from the assessment score.
     */
    public HashMap getItemScores(Long publishedId, Long itemId, String which) {
        try {
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemScores(publishedId, itemId, which);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    public HashMap getItemScores(Long publishedId, Long itemId, String which, boolean loadItemGradingAttachment) {
        try {
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemScores(publishedId, itemId, which, loadItemGradingAttachment);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    public HashMap getItemScores(Long itemId, List scores, boolean loadItemGradingAttachment) {
        try {
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemScores(itemId, scores, loadItemGradingAttachment);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    /**
     * Get the last set of itemgradingdata for a student per assessment
     */
    public HashMap getLastItemGradingData(String publishedId, String agentId) {
        try {
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastItemGradingData(Long.valueOf(publishedId), agentId);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    /**
     * Get the grading data for a given submission
     */
    public HashMap getStudentGradingData(String assessmentGradingId) {
        try {
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getStudentGradingData(assessmentGradingId);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    /**
     * Get the last submission for a student per assessment
     */
    public HashMap getSubmitData(String publishedId, String agentId, Integer scoringoption,
            String assessmentGradingId) {
        try {
            Long gradingId = null;
            if (assessmentGradingId != null)
                gradingId = Long.valueOf(assessmentGradingId);
            return (HashMap) PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSubmitData(Long.valueOf(publishedId), agentId, scoringoption, gradingId);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    public String getTextForId(Long typeId) {
        TypeFacadeQueriesAPI typeFacadeQueries = PersistenceService.getInstance().getTypeFacadeQueries();
        TypeFacade type = typeFacadeQueries.getTypeFacadeById(typeId);
        return (type.getKeyword());
    }

    public int getSubmissionSizeOfPublishedAssessment(String publishedAssessmentId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSubmissionSizeOfPublishedAssessment(Long.valueOf(publishedAssessmentId));
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    public Long saveMedia(byte[] media, String mimeType) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveMedia(media, mimeType);
    }

    public Long saveMedia(MediaData mediaData) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveMedia(mediaData);
    }

    public MediaData getMedia(String mediaId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getMedia(Long.valueOf(mediaId));
    }

    public ArrayList getMediaArray(String itemGradingId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getMediaArray(Long.valueOf(itemGradingId));
    }

    public ArrayList getMediaArray2(String itemGradingId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getMediaArray2(Long.valueOf(itemGradingId));
    }

    public ArrayList getMediaArray(ItemGradingData i) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getMediaArray(i);
    }

    public HashMap getMediaItemGradingHash(Long assessmentGradingId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getMediaItemGradingHash(assessmentGradingId);
    }

    public List<MediaData> getMediaArray(String publishedId, String publishItemId, String which) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getMediaArray(Long.valueOf(publishedId), Long.valueOf(publishItemId), which);
    }

    public ItemGradingData getLastItemGradingDataByAgent(String publishedItemId, String agentId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastItemGradingDataByAgent(Long.valueOf(publishedItemId), agentId);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public ItemGradingData getItemGradingData(String assessmentGradingId, String publishedItemId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemGradingData(Long.valueOf(assessmentGradingId), Long.valueOf(publishedItemId));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public AssessmentGradingData load(String assessmentGradingId) {
        return load(assessmentGradingId, true);
    }

    public AssessmentGradingData load(String assessmentGradingId, boolean loadGradingAttachment) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .load(Long.valueOf(assessmentGradingId), loadGradingAttachment);
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public ItemGradingData getItemGrading(String itemGradingId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemGrading(Long.valueOf(itemGradingId));
        } catch (Exception e) {
            log.error(e);
            throw new Error(e);
        }
    }

    public AssessmentGradingData getLastAssessmentGradingByAgentId(String publishedAssessmentId,
            String agentIdString) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString);
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public AssessmentGradingData getLastSavedAssessmentGradingByAgentId(String publishedAssessmentId,
            String agentIdString) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastSavedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId), agentIdString);
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public AssessmentGradingData getLastSubmittedAssessmentGradingByAgentId(String publishedAssessmentId,
            String agentIdString, String assessmentGradingId) {

        AssessmentGradingData assessmentGranding = null;
        try {
            if (assessmentGradingId != null) {
                assessmentGranding = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                        .getLastSubmittedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId),
                                agentIdString, Long.valueOf(assessmentGradingId));
            } else {
                assessmentGranding = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                        .getLastSubmittedAssessmentGradingByAgentId(Long.valueOf(publishedAssessmentId),
                                agentIdString, null);
            }
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }

        return assessmentGranding;
    }

    public void saveItemGrading(ItemGradingData item) {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveItemGrading(item);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void saveOrUpdateAssessmentGrading(AssessmentGradingData assessment) {
        try {
            /*   
            // Comment out the whole IF section because the only thing we do here is to 
            // update the itemGradingSet. However, this update is redundant as it will
            // be updated in saveOrUpdateAssessmentGrading(assessment).
            if (assessment.getAssessmentGradingId()!=null 
                && assessment.getAssessmentGradingId().longValue()>0){
             //1. if assessmentGrading contain itemGrading, we want to insert/update itemGrading first
              Set itemGradingSet = assessment.getItemGradingSet(); 
              Iterator iter = itemGradingSet.iterator();
              while (iter.hasNext()) {
                 ItemGradingData itemGradingData = (ItemGradingData) iter.next();
                 log.debug("date = " + itemGradingData.getSubmittedDate());
              }
              // The following line seems redundant. I cannot see a reason why we need to save the itmeGradingSet
              // here and then again in following saveOrUpdateAssessmentGrading(assessment). Comment it out.    
             //saveOrUpdateAll(itemGradingSet);
            }
            */
            // this will update itemGradingSet and assessmentGrading. May as well, otherwise I would have
            // to reload assessment again
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .saveOrUpdateAssessmentGrading(assessment);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    // This API only touch SAM_ASSESSMENTGRADING_T. No data gets inserted/updated in SAM_ITEMGRADING_T
    public void saveOrUpdateAssessmentGradingOnly(AssessmentGradingData assessment) {
        Set origItemGradingSet = assessment.getItemGradingSet();
        HashSet h = new HashSet(origItemGradingSet);

        // Clear the itemGradingSet so no data gets inserted/updated in SAM_ITEMGRADING_T;
        origItemGradingSet.clear();
        int size = assessment.getItemGradingSet().size();
        log.debug("before persist to db: size = " + size);
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .saveOrUpdateAssessmentGrading(assessment);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // Restore the original itemGradingSet back
            assessment.setItemGradingSet(h);
            size = assessment.getItemGradingSet().size();
            log.debug("after persist to db: size = " + size);
        }
    }

    public List getAssessmentGradingIds(String publishedItemId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getAssessmentGradingIds(Long.valueOf(publishedItemId));
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public AssessmentGradingData getHighestAssessmentGrading(String publishedAssessmentId, String agentId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getHighestAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId);
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public AssessmentGradingData getHighestSubmittedAssessmentGrading(String publishedAssessmentId, String agentId,
            String assessmentGradingId) {
        AssessmentGradingData assessmentGrading = null;
        try {
            if (assessmentGradingId != null) {
                assessmentGrading = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                        .getHighestSubmittedAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId,
                                Long.valueOf(assessmentGradingId));
            } else {
                assessmentGrading = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                        .getHighestSubmittedAssessmentGrading(Long.valueOf(publishedAssessmentId), agentId, null);
            }
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }

        return assessmentGrading;
    }

    public AssessmentGradingData getHighestSubmittedAssessmentGrading(String publishedAssessmentId,
            String agentId) {
        return getHighestSubmittedAssessmentGrading(publishedAssessmentId, agentId, null);
    }

    public Set getItemGradingSet(String assessmentGradingId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemGradingSet(Long.valueOf(assessmentGradingId));
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public HashMap getAssessmentGradingByItemGradingId(String publishedAssessmentId) {
        try {
            return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getAssessmentGradingByItemGradingId(Long.valueOf(publishedAssessmentId));
        } catch (Exception e) {
            log.error(e);
            throw new RuntimeException(e);
        }
    }

    public void updateItemScore(ItemGradingData gdata, double scoreDifference, PublishedAssessmentIfc pub) {
        try {
            AssessmentGradingData adata = load(gdata.getAssessmentGradingId().toString());
            adata.setItemGradingSet(getItemGradingSet(adata.getAssessmentGradingId().toString()));

            Set itemGradingSet = adata.getItemGradingSet();
            Iterator iter = itemGradingSet.iterator();
            double totalAutoScore = 0;
            double totalOverrideScore = adata.getTotalOverrideScore().doubleValue();
            while (iter.hasNext()) {
                ItemGradingData i = (ItemGradingData) iter.next();
                if (i.getItemGradingId().equals(gdata.getItemGradingId())) {
                    i.setAutoScore(gdata.getAutoScore());
                    i.setComments(gdata.getComments());
                    i.setGradedBy(AgentFacade.getAgentString());
                    i.setGradedDate(new Date());
                }
                if (i.getAutoScore() != null)
                    totalAutoScore += i.getAutoScore().doubleValue();
            }

            adata.setTotalAutoScore(Double.valueOf(totalAutoScore));
            if (Double.compare((totalAutoScore + totalOverrideScore), Double.valueOf("0").doubleValue()) < 0) {
                adata.setFinalScore(Double.valueOf("0"));
            } else {
                adata.setFinalScore(Double.valueOf(totalAutoScore + totalOverrideScore));
            }
            saveOrUpdateAssessmentGrading(adata);
            if (scoreDifference != 0) {
                notifyGradebookByScoringType(adata, pub);
            }
        } catch (GradebookServiceException ge) {
            ge.printStackTrace();
            throw ge;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * Assume this is a new item.
     */
    public void storeGrades(AssessmentGradingData data, PublishedAssessmentIfc pub, HashMap publishedItemHash,
            HashMap publishedItemTextHash, HashMap publishedAnswerHash, HashMap invalidFINMap,
            ArrayList invalidSALengthList) throws GradebookServiceException, FinFormatException {
        log.debug("storeGrades: data.getSubmittedDate()" + data.getSubmittedDate());
        storeGrades(data, false, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, true,
                invalidFINMap, invalidSALengthList);
    }

    /**
     * Assume this is a new item.
     */
    public void storeGrades(AssessmentGradingData data, PublishedAssessmentIfc pub, HashMap publishedItemHash,
            HashMap publishedItemTextHash, HashMap publishedAnswerHash, boolean persistToDB, HashMap invalidFINMap,
            ArrayList invalidSALengthList) throws GradebookServiceException, FinFormatException {
        log.debug("storeGrades (not persistToDB) : data.getSubmittedDate()" + data.getSubmittedDate());
        storeGrades(data, false, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, persistToDB,
                invalidFINMap, invalidSALengthList);
    }

    public void storeGrades(AssessmentGradingData data, boolean regrade, PublishedAssessmentIfc pub,
            HashMap publishedItemHash, HashMap publishedItemTextHash, HashMap publishedAnswerHash,
            boolean persistToDB) throws GradebookServiceException, FinFormatException {
        log.debug("storeGrades (not persistToDB) : data.getSubmittedDate()" + data.getSubmittedDate());
        storeGrades(data, regrade, pub, publishedItemHash, publishedItemTextHash, publishedAnswerHash, persistToDB,
                null, null);
    }

    /**
     * This is the big, complicated mess where we take all the items in
     * an assessment, store the grading data, auto-grade it, and update
     * everything.
     *
     * If regrade is true, we just recalculate the graded score.  If it's
     * false, we do everything from scratch.
     */
    public void storeGrades(AssessmentGradingData data, boolean regrade, PublishedAssessmentIfc pub,
            HashMap publishedItemHash, HashMap publishedItemTextHash, HashMap publishedAnswerHash,
            boolean persistToDB, HashMap invalidFINMap, ArrayList invalidSALengthList)
            throws GradebookServiceException, FinFormatException {
        log.debug("****x1. regrade =" + regrade + " " + (new Date()).getTime());
        try {
            boolean imageMapAllOk = true;
            boolean NeededAllOk = false;

            String agent = data.getAgentId();

            // note that this itemGradingSet is a partial set of answer submitted. it contains only 
            // newly submitted answers, updated answers and MCMR/FIB/FIN answers ('cos we need the old ones to
            // calculate scores for new ones)
            Set<ItemGradingData> itemGradingSet = data.getItemGradingSet();
            if (itemGradingSet == null)
                itemGradingSet = new HashSet<ItemGradingData>();
            log.debug("****itemGrading size=" + itemGradingSet.size());

            List<ItemGradingData> tempItemGradinglist = new ArrayList<ItemGradingData>(itemGradingSet);

            // CALCULATED_QUESTION - if this is a calc question. Carefully sort the list of answers
            if (isCalcQuestion(tempItemGradinglist, publishedItemHash)) {
                Collections.sort(tempItemGradinglist, new Comparator<ItemGradingData>() {
                    public int compare(ItemGradingData o1, ItemGradingData o2) {
                        ItemGradingData gradeData1 = o1;
                        ItemGradingData gradeData2 = o2;

                        // protect against blank ones in samigo initial setup.
                        if (gradeData1 == null)
                            return -1;
                        if (gradeData2 == null)
                            return 1;
                        if (gradeData1.getPublishedAnswerId() == null)
                            return -1;
                        if (gradeData2.getPublishedAnswerId() == null)
                            return 1;
                        return gradeData1.getPublishedAnswerId().compareTo(gradeData2.getPublishedAnswerId());
                    }
                });
            }

            Iterator<ItemGradingData> iter = tempItemGradinglist.iterator();

            // fibEmiAnswersMap contains a map of HashSet of answers for a FIB or EMI item,
            // key =itemid, value= HashSet of answers for each item.  
            // For FIB: This is used to keep track of answers we have already used for
            // mutually exclusive multiple answer type of FIB, such as 
            // The flag of the US is {red|white|blue},{red|white|blue}, and {red|white|blue}.
            // so if the first blank has an answer 'red', the 'red' answer should 
            // not be included in the answers for the other mutually exclusive blanks.
            // For EMI: This keeps track of how many answers were given so we don't give
            // extra marks for to many answers.
            Map fibEmiAnswersMap = new HashMap();
            Map<Long, Map<Long, Set<EMIScore>>> emiScoresMap = new HashMap<Long, Map<Long, Set<EMIScore>>>();

            //change algorithm based on each question (SAK-1930 & IM271559) -cwen
            HashMap totalItems = new HashMap();
            log.debug("****x2. " + (new Date()).getTime());
            double autoScore = (double) 0;
            Long itemId = (long) 0;
            int calcQuestionAnswerSequence = 1; // sequence of answers for CALCULATED_QUESTION
            while (iter.hasNext()) {
                ItemGradingData itemGrading = iter.next();

                // CALCULATED_QUESTION - We increment this so we that calculated 
                // questions can know where we are in the sequence of answers.
                if (itemGrading.getPublishedItemId().equals(itemId)) {
                    calcQuestionAnswerSequence++;
                } else {
                    calcQuestionAnswerSequence = 1;
                }

                itemId = itemGrading.getPublishedItemId();
                ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
                if (item == null) {
                    //this probably shouldn't happen
                    log.error("unable to retrive itemDataIfc for: " + publishedItemHash.get(itemId));
                    continue;
                }
                Iterator i = item.getItemMetaDataSet().iterator();
                while (i.hasNext()) {
                    ItemMetaDataIfc meta = (ItemMetaDataIfc) i.next();
                    if (meta.getLabel().equals(ItemMetaDataIfc.REQUIRE_ALL_OK)) {
                        if (meta.getEntry().equals("true")) {
                            NeededAllOk = true;
                            break;
                        }
                        if (meta.getEntry().equals("false")) {
                            NeededAllOk = false;
                            break;
                        }
                    }
                }
                Long itemType = item.getTypeId();
                autoScore = (double) 0;

                itemGrading.setAssessmentGradingId(data.getAssessmentGradingId());
                //itemGrading.setSubmittedDate(new Date());
                itemGrading.setAgentId(agent);
                itemGrading.setOverrideScore(Double.valueOf(0));

                if (itemType == 5 && itemGrading.getAnswerText() != null) {
                    String processedAnswerText = itemGrading.getAnswerText().replaceAll("\r", "").replaceAll("\n",
                            "");
                    if (processedAnswerText.length() > 32000) {
                        if (invalidSALengthList != null) {
                            invalidSALengthList.add(item.getItemId());
                        }
                    }
                }

                // note that totalItems & fibAnswersMap would be modified by the following method
                try {
                    autoScore = getScoreByQuestionType(itemGrading, item, itemType, publishedItemTextHash,
                            totalItems, fibEmiAnswersMap, emiScoresMap, publishedAnswerHash, regrade,
                            calcQuestionAnswerSequence);
                } catch (FinFormatException e) {
                    autoScore = 0d;
                    if (invalidFINMap != null) {
                        if (invalidFINMap.containsKey(itemId)) {
                            ArrayList list = (ArrayList) invalidFINMap.get(itemId);
                            list.add(itemGrading.getItemGradingId());
                        } else {
                            ArrayList list = new ArrayList();
                            list.add(itemGrading.getItemGradingId());
                            invalidFINMap.put(itemId, list);
                        }
                    }
                }
                if ((TypeIfc.IMAGEMAP_QUESTION.equals(itemType)) && (NeededAllOk)
                        && ((autoScore == -123456789) || !imageMapAllOk)) {
                    autoScore = 0;
                    imageMapAllOk = false;
                }

                log.debug("**!regrade, autoScore=" + autoScore);
                if (!(TypeIfc.MULTIPLE_CORRECT).equals(itemType)
                        && !(TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType))
                    totalItems.put(itemId, Double.valueOf(autoScore));

                if (regrade && TypeIfc.AUDIO_RECORDING.equals(itemType))
                    itemGrading.setAttemptsRemaining(item.getTriesAllowed());

                itemGrading.setAutoScore(Double.valueOf(autoScore));
            }

            if ((invalidFINMap != null && invalidFINMap.size() > 0)
                    || (invalidSALengthList != null && invalidSALengthList.size() > 0)) {
                return;
            }
            // Added persistToDB because if we don't save data to DB later, we shouldn't update the assessment
            // submittedDate either. The date should be sync in delivery bean and DB
            // This is for DeliveryBean.checkDataIntegrity()
            if (!regrade && persistToDB) {
                data.setSubmittedDate(new Date());
                setIsLate(data, pub);
            }

            log.debug("****x3. " + (new Date()).getTime());

            List<ItemGradingData> emiItemGradings = new ArrayList<ItemGradingData>();
            // the following procedure ensure total score awarded per question is no less than 0
            // this probably only applies to MCMR question type - daisyf
            iter = itemGradingSet.iterator();
            //since the itr goes through each answer (multiple answers for a signle mc question), keep track
            //of its total score by itemId -> autoScore[]{user's score, total possible}
            Map<Long, Double[]> mcmcAllOrNothingCheck = new HashMap<Long, Double[]>();
            Map<Long, Integer> countMcmcAllItemGradings = new HashMap<Long, Integer>();
            //get item information to check if it's MCMS and Not Partial Credit
            Long itemType2 = -1l;
            String mcmsPartialCredit = "";
            double itemScore = -1;
            while (iter.hasNext()) {
                ItemGradingData itemGrading = iter.next();
                itemId = itemGrading.getPublishedItemId();
                ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
                //SAM-1724 it's possible the item is not in the hash -DH
                if (item == null) {
                    log.error("unable to retrive itemDataIfc for: " + publishedItemHash.get(itemId));
                    continue;
                }
                itemType2 = item.getTypeId();
                //get item information to check if it's MCMS and Not Partial Credit
                mcmsPartialCredit = item.getItemMetaDataByLabel(ItemMetaDataIfc.MCMS_PARTIAL_CREDIT);
                itemScore = item.getScore();
                //double autoScore = (double) 0;

                // this does not apply to EMI
                // just create a short-list and handle differently below
                if ((TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType2)) {
                    emiItemGradings.add(itemGrading);
                    continue;
                }

                double eachItemScore = ((Double) totalItems.get(itemId)).doubleValue();
                if ((eachItemScore < 0)
                        && !((TypeIfc.MULTIPLE_CHOICE).equals(itemType2) || (TypeIfc.TRUE_FALSE).equals(itemType2)
                                || (TypeIfc.MULTIPLE_CORRECT_SINGLE_SELECTION).equals(itemType2))) {
                    itemGrading.setAutoScore(Double.valueOf(0));
                }
                //keep track of MCMC answer's total score in order to check for all or nothing
                if (TypeIfc.MULTIPLE_CORRECT.equals(itemType2) && "false".equals(mcmsPartialCredit)) {
                    Double accumulatedScore = itemGrading.getAutoScore();
                    if (mcmcAllOrNothingCheck.containsKey(itemId)) {
                        Double[] accumulatedScoreArr = mcmcAllOrNothingCheck.get(itemId);
                        accumulatedScore += accumulatedScoreArr[0];
                    }
                    mcmcAllOrNothingCheck.put(itemId, new Double[] { accumulatedScore, item.getScore() });
                    int count = 0;
                    if (countMcmcAllItemGradings.containsKey(itemId))
                        count = ((Integer) countMcmcAllItemGradings.get(itemId)).intValue();
                    countMcmcAllItemGradings.put(itemId, new Integer(++count));
                }
            }

            log.debug("****x3.1 " + (new Date()).getTime());

            // Loop 1: this procedure ensure total score awarded per EMI item
            // is correct
            // For emi's there are multiple gradings per item per question,
            // for the grading we only know scores after grading so we need
            // to reset the grading score here to the correct scores
            // this currently only applies to EMI question type
            if (emiItemGradings != null && !emiItemGradings.isEmpty()) {
                Map<Long, Map<Long, Map<Long, EMIScore>>> emiOrderedScoresMap = reorderEMIScoreMap(emiScoresMap);
                iter = emiItemGradings.iterator();
                while (iter.hasNext()) {
                    ItemGradingData itemGrading = iter.next();

                    //SAM-2016 check for Nullity
                    if (itemGrading == null) {
                        log.warn("Map contains null itemgrading!");
                        continue;
                    }

                    Map<Long, Map<Long, EMIScore>> innerMap = emiOrderedScoresMap
                            .get(itemGrading.getPublishedItemId());

                    if (innerMap == null) {
                        log.warn("Inner map is empty!");
                        continue;
                    }

                    Map<Long, EMIScore> scoreMap = innerMap.get(itemGrading.getPublishedItemTextId());

                    if (scoreMap == null) {
                        log.warn("Score map is empty!");
                        continue;
                    }

                    EMIScore score = scoreMap.get(itemGrading.getPublishedAnswerId());
                    if (score == null) {
                        //its possible! SAM-2016 
                        log.warn("we can't find a score for answer: " + itemGrading.getPublishedAnswerId());
                        continue;
                    }
                    itemGrading.setAutoScore(emiOrderedScoresMap.get(itemGrading.getPublishedItemId())
                            .get(itemGrading.getPublishedItemTextId())
                            .get(itemGrading.getPublishedAnswerId()).effectiveScore);
                }
            }

            // if it's MCMS and Not Partial Credit and the score isn't 100% (totalAutoScoreCheck != itemScore),
            // that means the user didn't answer all of the correct answers only.  
            // We need to set their score to 0 for all ItemGrading items
            for (Entry<Long, Double[]> entry : mcmcAllOrNothingCheck.entrySet()) {
                if (Double.compare(entry.getValue()[0], entry.getValue()[1]) != 0) {
                    //reset all scores to 0 since the user didn't get all correct answers
                    iter = itemGradingSet.iterator();
                    while (iter.hasNext()) {
                        ItemGradingData itemGrading = iter.next();
                        Long itemId2 = entry.getKey();
                        if (itemGrading.getPublishedItemId().equals(itemId2)) {
                            AnswerIfc answer = (AnswerIfc) publishedAnswerHash
                                    .get(itemGrading.getPublishedAnswerId());
                            if (answer == null) {
                                itemGrading.setAutoScore(Double.valueOf(0));
                                log.error("unable to retrieve answerIfc for: " + itemId2);
                                continue;
                            }
                            if (!countMcmcAllItemGradings.containsKey(itemId2)) {
                                itemGrading.setAutoScore(Double.valueOf(0));
                                log.error("unable to retrieve itemGrading's counter for: " + itemId2);
                                continue;
                            }
                            double discount = (Math.abs(answer.getDiscount().doubleValue()) * ((double) -1));
                            int count = ((Integer) countMcmcAllItemGradings.get(itemId2)).intValue();
                            double itemGrDisc = discount / count;
                            itemGrading.setAutoScore(Double.valueOf(itemGrDisc));
                        }
                    }
                }
            }

            log.debug("****x4. " + (new Date()).getTime());

            // save#1: this itemGrading Set is a partial set of answers submitted. it contains new answers and
            // updated old answers and FIB answers ('cos we need the old answer to calculate the score for new
            // ones). we need to be cheap, we don't want to update record that hasn't been
            // changed. Yes, assessmentGrading's total score will be out of sync at this point, I am afraid. It
            // would be in sync again once the whole method is completed sucessfully. 
            if (persistToDB) {
                saveOrUpdateAll(itemGradingSet);
            }
            log.debug("****x5. " + (new Date()).getTime());

            // save#2: now, we need to get the full set so we can calculate the total score accumulate for the
            // whole assessment.
            Set fullItemGradingSet = getItemGradingSet(data.getAssessmentGradingId().toString());
            double totalAutoScore = getTotalAutoScore(fullItemGradingSet);
            data.setTotalAutoScore(Double.valueOf(totalAutoScore));
            //log.debug("**#1 total AutoScore"+totalAutoScore);
            if (Double.compare((totalAutoScore + data.getTotalOverrideScore().doubleValue()),
                    new Double("0").doubleValue()) < 0) {
                data.setFinalScore(Double.valueOf("0"));
            } else {
                data.setFinalScore(Double.valueOf(totalAutoScore + data.getTotalOverrideScore().doubleValue()));
            }
            log.debug("****x6. " + (new Date()).getTime());
        } catch (GradebookServiceException ge) {
            ge.printStackTrace();
            throw ge;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        // save#3: itemGradingSet has been saved above so just need to update assessmentGrading
        // therefore setItemGradingSet as empty first - daisyf
        // however, if we do not persit to DB, we want to keep itemGradingSet with data for later use
        // Because if itemGradingSet is not saved to DB, we cannot go to DB to get it. We have to 
        // get it through data.
        if (persistToDB) {
            data.setItemGradingSet(new HashSet());
            saveOrUpdateAssessmentGrading(data);
            log.debug("****x7. " + (new Date()).getTime());
            if (!regrade) {
                notifyGradebookByScoringType(data, pub);
            }
        }
        log.debug("****x8. " + (new Date()).getTime());

        // I am not quite sure what the following code is doing... I modified this based on my assumption:
        // If this happens dring regrade, we don't want to clean these data up
        // We only want to clean them out in delivery
        if (!regrade && Boolean.TRUE.equals(data.getForGrade())) {
            // remove the assessmentGradingData created during gradiing (by updatding total score page)
            removeUnsubmittedAssessmentGradingData(data);
        }
    }

    private double getTotalAutoScore(Set itemGradingSet) {
        //log.debug("*** no. of itemGrading="+itemGradingSet.size());
        double totalAutoScore = 0;
        Iterator iter = itemGradingSet.iterator();
        while (iter.hasNext()) {
            ItemGradingData i = (ItemGradingData) iter.next();
            //log.debug(i.getItemGradingId()+"->"+i.getAutoScore());
            if (i.getAutoScore() != null)
                totalAutoScore += i.getAutoScore().doubleValue();
        }
        return totalAutoScore;
    }

    private void notifyGradebookByScoringType(AssessmentGradingData data, PublishedAssessmentIfc pub) {
        Integer scoringType = pub.getEvaluationModel().getScoringType();
        if (updateGradebook(data, pub)) {
            AssessmentGradingData d = data; // data is the last submission
            // need to decide what to tell gradebook
            if ((scoringType).equals(EvaluationModelIfc.HIGHEST_SCORE))
                d = getHighestSubmittedAssessmentGrading(pub.getPublishedAssessmentId().toString(),
                        data.getAgentId());
            notifyGradebook(d, pub);
        }
    }

    private double getScoreByQuestionType(ItemGradingData itemGrading, ItemDataIfc item, Long itemType,
            Map publishedItemTextHash, Map totalItems, Map fibAnswersMap,
            Map<Long, Map<Long, Set<EMIScore>>> emiScoresMap, HashMap publishedAnswerHash, boolean regrade,
            int calcQuestionAnswerSequence) throws FinFormatException {
        //double score = (double) 0;
        double initScore = (double) 0;
        double autoScore = (double) 0;
        double accumelateScore = (double) 0;
        Long itemId = item.getItemId();
        int type = itemType.intValue();
        switch (type) {
        case 1: // MC Single Correct
            if (item.getPartialCreditFlag())
                autoScore = getAnswerScoreMCQ(itemGrading, publishedAnswerHash);
            else {
                autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
            }
            //overridescore
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();
            totalItems.put(itemId, new Double(autoScore));
            break;// MC Single Correct
        case 12: // MC Multiple Correct Single Selection         
        case 3: // MC Survey
        case 4: // True/False          
            autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
            //overridescore
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();
            totalItems.put(itemId, Double.valueOf(autoScore));
            break;
        case 2: // MC Multiple Correct
            ItemTextIfc itemText = (ItemTextIfc) publishedItemTextHash.get(itemGrading.getPublishedItemTextId());
            List answerArray = itemText.getAnswerArray();
            int correctAnswers = 0;
            if (answerArray != null) {
                for (int i = 0; i < answerArray.size(); i++) {
                    AnswerIfc a = (AnswerIfc) answerArray.get(i);
                    if (a.getIsCorrect().booleanValue())
                        correctAnswers++;
                }
            }
            initScore = getAnswerScore(itemGrading, publishedAnswerHash);
            if (initScore > 0)
                autoScore = initScore / correctAnswers;
            else
                autoScore = (getTotalCorrectScore(itemGrading, publishedAnswerHash) / correctAnswers)
                        * ((double) -1);

            //overridescore?
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();
            if (!totalItems.containsKey(itemId)) {
                totalItems.put(itemId, Double.valueOf(autoScore));
                //log.debug("****0. first answer score = "+autoScore);
            } else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                //log.debug("****1. before adding new score = "+accumelateScore);
                //log.debug("****2. this answer score = "+autoScore);
                accumelateScore += autoScore;
                //log.debug("****3. add 1+2 score = "+accumelateScore);
                totalItems.put(itemId, Double.valueOf(accumelateScore));
                //log.debug("****4. what did we put in = "+((Double)totalItems.get(itemId)).doubleValue());
            }
            break;

        case 9: // Matching     
            initScore = getAnswerScore(itemGrading, publishedAnswerHash);
            if (initScore > 0) {
                int nonDistractors = 0;
                Iterator<ItemTextIfc> itemIter = item.getItemTextArraySorted().iterator();
                while (itemIter.hasNext()) {
                    ItemTextIfc curItem = itemIter.next();
                    if (!isDistractor(curItem)) {
                        nonDistractors++;
                    }
                }
                autoScore = initScore / nonDistractors;
            }
            //overridescore?
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();

            if (!totalItems.containsKey(itemId))
                totalItems.put(itemId, Double.valueOf(autoScore));
            else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                accumelateScore += autoScore;
                totalItems.put(itemId, Double.valueOf(accumelateScore));
            }
            break;

        case 8: // FIB
            autoScore = getFIBScore(itemGrading, fibAnswersMap, item, publishedAnswerHash)
                    / (double) ((ItemTextIfc) item.getItemTextSet().toArray()[0]).getAnswerSet().size();
            //overridescore - cwen
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();

            if (!totalItems.containsKey(itemId))
                totalItems.put(itemId, Double.valueOf(autoScore));
            else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                accumelateScore += autoScore;
                totalItems.put(itemId, Double.valueOf(accumelateScore));
            }
            break;
        case 15: // CALCULATED_QUESTION
        case 11: // FIN
            try {
                if (type == 15) { // CALCULATED_QUESTION
                    Map<Integer, String> calculatedAnswersMap = getCalculatedAnswersMap(itemGrading, item);
                    int numAnswers = calculatedAnswersMap.size();
                    autoScore = getCalcQScore(itemGrading, item, calculatedAnswersMap, calcQuestionAnswerSequence)
                            / (double) numAnswers;
                } else {
                    autoScore = getFINScore(itemGrading, item, publishedAnswerHash)
                            / (double) ((ItemTextIfc) item.getItemTextSet().toArray()[0]).getAnswerSet().size();
                }
            } catch (FinFormatException e) {
                throw e;
            }
            //overridescore - cwen
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();

            if (!totalItems.containsKey(itemId))
                totalItems.put(itemId, Double.valueOf(autoScore));
            else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                accumelateScore += autoScore;
                totalItems.put(itemId, Double.valueOf(accumelateScore));
            }
            break;

        case 14: // EMI
            autoScore = getEMIScore(itemGrading, itemId, totalItems, emiScoresMap, publishedItemTextHash,
                    publishedAnswerHash);
            break;
        case 5: // SAQ
        case 6: // file upload
        case 7: // audio recording
                //overridescore - cwen
            if (regrade && itemGrading.getAutoScore() != null) {
                autoScore = itemGrading.getAutoScore();
            }
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();
            if (!totalItems.containsKey(itemId))
                totalItems.put(itemId, Double.valueOf(autoScore));
            else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                accumelateScore += autoScore;
                totalItems.put(itemId, Double.valueOf(accumelateScore));
            }
            break;
        case 16:
            initScore = getImageMapScore(itemGrading, item, (HashMap) publishedItemTextHash, publishedAnswerHash);
            //if one answer is 0 or negative, and need all OK to be scored, then autoScore=-123456789
            //and we break the case...

            boolean NeededAllOk = false;
            Iterator i = item.getItemMetaDataSet().iterator();
            while (i.hasNext()) {
                ItemMetaDataIfc meta = (ItemMetaDataIfc) i.next();
                if (meta.getLabel().equals(ItemMetaDataIfc.REQUIRE_ALL_OK)) {
                    if (meta.getEntry().equals("true")) {
                        NeededAllOk = true;
                        break;
                    }
                }
            }
            if (NeededAllOk && initScore <= 0) {
                autoScore = -123456789;
                break;
            }
            //if (initScore > 0) {
            autoScore += initScore;
            //}

            //overridescore?
            if (itemGrading.getOverrideScore() != null)
                autoScore += itemGrading.getOverrideScore().doubleValue();

            if (!totalItems.containsKey(itemId)) {
                totalItems.put(itemId, Double.valueOf(autoScore));
            } else {
                accumelateScore = ((Double) totalItems.get(itemId)).doubleValue();
                accumelateScore += autoScore;
                totalItems.put(itemId, Double.valueOf(accumelateScore));
            }

            break;
        }
        return autoScore;
    }

    /**
       * This grades multiple choice and true false questions.  Since
       * multiple choice/multiple select has a separate ItemGradingData for
       * each choice, they're graded the same way the single choice are.
       * Choices should be given negative score values if one wants them
       * to lose points for the wrong choice.
       */
    public double getAnswerScore(ItemGradingData data, Map publishedAnswerHash) {
        AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answer == null || answer.getScore() == null) {
            return (double) 0;
        }
        ItemDataIfc item = (ItemDataIfc) answer.getItem();
        Long itemType = item.getTypeId();
        if (answer.getIsCorrect() == null || !answer.getIsCorrect().booleanValue()) {
            // return (double) 0;
            // Para que descuente (For discount)
            if ((TypeIfc.EXTENDED_MATCHING_ITEMS).equals(itemType) || (TypeIfc.MULTIPLE_CHOICE).equals(itemType)
                    || (TypeIfc.TRUE_FALSE).equals(itemType)
                    || (TypeIfc.MULTIPLE_CORRECT_SINGLE_SELECTION).equals(itemType)) {
                return (Math.abs(answer.getDiscount().doubleValue()) * ((double) -1));
            } else {
                return (double) 0;
            }
        }
        return answer.getScore().doubleValue();
    }

    public void notifyGradebook(AssessmentGradingData data, PublishedAssessmentIfc pub)
            throws GradebookServiceException {
        // If the assessment is published to the gradebook, make sure to update the scores in the gradebook
        String toGradebook = pub.getEvaluationModel().getToGradeBook();

        GradebookExternalAssessmentService g = null;
        boolean integrated = IntegrationContextFactory.getInstance().isIntegrated();
        if (integrated) {
            g = (GradebookExternalAssessmentService) SpringBeanLocator.getInstance()
                    .getBean("org.sakaiproject.service.gradebook.GradebookExternalAssessmentService");
        }

        GradebookServiceHelper gbsHelper = IntegrationContextFactory.getInstance().getGradebookServiceHelper();

        PublishedAssessmentService publishedAssessmentService = new PublishedAssessmentService();
        String currentSiteId = publishedAssessmentService
                .getPublishedAssessmentSiteId(pub.getPublishedAssessmentId().toString());
        if (gbsHelper.gradebookExists(GradebookFacade.getGradebookUId(currentSiteId), g)
                && toGradebook.equals(EvaluationModelIfc.TO_DEFAULT_GRADEBOOK.toString())) {
            if (log.isDebugEnabled())
                log.debug("Attempting to update a score in the gradebook");

            // add retry logic to resolve deadlock problem while sending grades to gradebook

            Double originalFinalScore = data.getFinalScore();
            int retryCount = PersistenceService.getInstance().getPersistenceHelper().getRetryCount().intValue();
            while (retryCount > 0) {
                try {
                    // Send the average score if average was selected for multiple submissions
                    Integer scoringType = pub.getEvaluationModel().getScoringType();
                    if (scoringType.equals(EvaluationModelIfc.AVERAGE_SCORE)) {
                        // status = 5: there is no submission but grader update something in the score page
                        if (data.getStatus() == 5) {
                            data.setFinalScore(data.getFinalScore());
                        } else {
                            Double averageScore = PersistenceService.getInstance()
                                    .getAssessmentGradingFacadeQueries().getAverageSubmittedAssessmentGrading(
                                            Long.valueOf(pub.getPublishedAssessmentId()), data.getAgentId());
                            data.setFinalScore(averageScore);
                        }
                    }
                    gbsHelper.updateExternalAssessmentScore(data, g);
                    retryCount = 0;
                } catch (org.sakaiproject.service.gradebook.shared.AssessmentNotFoundException ante) {
                    log.warn("problem sending grades to gradebook: " + ante.getMessage());
                    if (AssessmentIfc.RETRACT_FOR_EDIT_STATUS.equals(pub.getStatus())) {
                        retryCount = retry(retryCount, ante, pub, true);
                    } else {
                        // Otherwise, do the same exeption handling as others
                        retryCount = retry(retryCount, ante, pub, false);
                    }
                } catch (Exception e) {
                    retryCount = retry(retryCount, e, pub, false);
                }
            }

            // change the final score back to the original score since it may set to average score.
            // data.getFinalScore() != originalFinalScore
            if (!(MathUtils.equalsIncludingNaN(data.getFinalScore(), originalFinalScore, 0.0001))) {
                data.setFinalScore(originalFinalScore);
            }

            try {
                Long publishedAssessmentId = data.getPublishedAssessmentId();
                String agent = data.getAgentId();
                String comment = data.getComments();
                gbsHelper.updateExternalAssessmentComment(publishedAssessmentId, agent, comment, g);
            } catch (Exception ex) {
                log.warn("Error sending comments to gradebook: " + ex.getMessage());
            }
        } else {
            if (log.isDebugEnabled())
                log.debug("Not updating the gradebook.  toGradebook = " + toGradebook);
        }
    }

    private int retry(int retryCount, Exception e, PublishedAssessmentIfc pub, boolean retractForEditStatus) {
        log.warn("retrying...sending grades to gradebook. ");
        log.warn("retry....");
        retryCount--;
        try {
            int deadlockInterval = PersistenceService.getInstance().getPersistenceHelper().getDeadlockInterval()
                    .intValue();
            Thread.sleep(deadlockInterval);
        } catch (InterruptedException ex) {
            log.warn(ex.getMessage());
        }
        if (retryCount == 0) {
            if (retractForEditStatus) {
                // This happens in following scenario:
                // 1. The assessment is active and has "None" for GB setting
                // 2. Instructor retracts it for edit and update the to "Send to GB"
                // 3. Instructor updates something on the total Score page
                // Because the GB will not be created until the assessment gets republished,
                // "AssessmentNotFoundException" will be thrown here. Since, this is the expected
                // exception, we simply log a debug message without retrying or notifying the user.
                // Of course, you can argue about what if the assessment gets deleted by other cause.
                // But I would say the major cause would be this "retract" scenario. Also, without knowing 
                // the change history of the assessment, I think this is the best handling. 
                log.info(
                        "We quietly sallow the AssessmentNotFoundException excption here. Published Assessment Name: "
                                + pub.getTitle());
            } else {
                // after retries, still failed updating gradebook
                log.warn("After all retries, still failed ...  Now throw error to UI");
                throw new GradebookServiceException(e);
            }
        }
        return retryCount;
    }

    /**
      * This grades Fill In Blank questions.  (see SAK-1685) 
        
      * There will be two valid cases for scoring when there are multiple fill 
      * in blanks in a question:
        
      * Case 1- There are different sets of answers (a set can contain one or more 
      * item) for each blank (e.g. The {dog|coyote|wolf} howls and the {lion|cougar} 
      * roars.) In this case each blank is tested for correctness independently. 
        
      * Case 2-There is the same set of answers for each blank: e.g.  The flag of the US 
      * is {red|white|blue},{red|white|blue}, and {red|white|blue}. 
        
      * These are the only two valid types of questions. When authoring, it is an 
      * ERROR to include: 
        
      * (1) a mixture of independent answer and common answer blanks 
      * (e.g. The {dog|coyote|wolf} howls at the {red|white|blue}, {red|white|blue}, 
      * and {red|white|blue} flag.)
        
      * (2) more than one set of blanks with a common answer ((e.g. The US flag 
      * is {red|white|blue}, {red|white|blue}, and {red|white|blue} and the Italian 
      * flag is {red|white|greem}, {red|white|greem}, and {red|white|greem}.)
        
      * These two invalid questions specifications should be authored as two 
      * separate questions.
        
    Here are the definition and 12 cases I came up with (lydia, 01/2006):
        
    single answers : roses are {red} and vilets are {blue}
    multiple answers : {dogs|cats} have 4 legs 
    multiple answers , mutually exclusive, all answers must be identical, can be in diff. orders : US flag has {red|blue|white} and {red |white|blue} and {blue|red|white} colors
    multiple answers , mutually non-exclusive : {dogs|cats} have 4 legs and {dogs|cats} can be pets. 
    wildcard uses *  to mean one of more characters 
        
        
    -. wildcard single answer, case sensitive
    -. wildcard single answer, case insensitive
    -. single answer, no wildcard , case sensitive
    -. single answer, no wildcard , case insensitive
    -. multiple answer, mutually non-exclusive, no wildcard , case sensitive
    -. multiple answer, mutually non-exclusive, no wildcard , case in sensitive
    -. multiple answer, mutually non-exclusive, wildcard , case sensitive
    -. multiple answer, mutually non-exclusive, wildcard , case insensitive
    -. multiple answer, mutually exclusive, no wildcard , case sensitive
    -. multiple answer, mutually exclusive, no wildcard , case in sensitive
    -. multiple answer, mutually exclusive, wildcard , case sensitive
    -. multiple answer, mutually exclusive, wildcard , case insensitive
        
     */
    public double getFIBScore(ItemGradingData data, Map fibmap, ItemDataIfc itemdata, Map publishedAnswerHash) {
        String studentanswer = "";
        boolean matchresult = false;
        double totalScore = (double) 0;
        data.setIsCorrect(Boolean.FALSE);

        if (data.getPublishedAnswerId() == null) {
            return totalScore;
        }
        AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answerIfc == null) {
            return totalScore;
        }
        String answertext = answerIfc.getText();
        Long itemId = itemdata.getItemId();

        String casesensitive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB);
        String mutuallyexclusive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB);
        //Set answerSet = new HashSet();

        if (answertext != null) {
            StringTokenizer st = new StringTokenizer(answertext, "|");
            while (st.hasMoreTokens()) {
                String answer = st.nextToken().trim();
                if ("true".equalsIgnoreCase(casesensitive)) {
                    if (data.getAnswerText() != null) {
                        studentanswer = data.getAnswerText().trim();
                        matchresult = fibmatch(answer, studentanswer, true);

                    }
                } // if case sensitive 
                else {
                    // case insensitive , if casesensitive is false, or null, or "".
                    if (data.getAnswerText() != null) {
                        studentanswer = data.getAnswerText().trim();
                        matchresult = fibmatch(answer, studentanswer, false);
                    }
                } // else , case insensitive

                if (matchresult) {

                    boolean alreadyused = false;
                    // add check for mutual exclusive
                    if ("true".equalsIgnoreCase(mutuallyexclusive)) {
                        // check if answers are already used.
                        Set answer_used_sofar = (HashSet) fibmap.get(itemId);
                        if ((answer_used_sofar != null)
                                && (answer_used_sofar.contains(studentanswer.toLowerCase()))) {
                            // already used, so it's a wrong answer for mutually exclusive questions
                            alreadyused = true;
                        } else {
                            // not used, it's a good answer, now add this to the already_used list.
                            // we only store lowercase strings in the fibmap.
                            if (answer_used_sofar == null) {
                                answer_used_sofar = new HashSet();
                            }

                            answer_used_sofar.add(studentanswer.toLowerCase());
                            fibmap.put(itemId, answer_used_sofar);
                        }
                    }

                    if (!alreadyused) {
                        totalScore += ((AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId())).getScore()
                                .doubleValue();
                        data.setIsCorrect(Boolean.TRUE);
                    }

                    // SAK-3005: quit if answer is correct, e.g. if you answered A for {a|A}, you already scored
                    break;
                }
            }
        }
        return totalScore;
    }

    public boolean getFIBResult(ItemGradingData data, HashMap fibmap, ItemDataIfc itemdata,
            HashMap publishedAnswerHash) {
        // this method is similiar to getFIBScore(), except it returns true/false for the answer, not scores.  
        // may be able to refactor code out to be reused, but totalscores for mutually exclusive case is a bit tricky. 
        String studentanswer = "";
        boolean matchresult = false;

        if (data.getPublishedAnswerId() == null) {
            return matchresult;
        }

        AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answerIfc == null) {
            return matchresult;
        }
        String answertext = answerIfc.getText();
        Long itemId = itemdata.getItemId();

        String casesensitive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB);
        String mutuallyexclusive = itemdata.getItemMetaDataByLabel(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB);
        //Set answerSet = new HashSet();

        if (answertext != null) {
            StringTokenizer st = new StringTokenizer(answertext, "|");
            while (st.hasMoreTokens()) {
                String answer = st.nextToken().trim();
                if ("true".equalsIgnoreCase(casesensitive)) {
                    if (data.getAnswerText() != null) {
                        studentanswer = data.getAnswerText().trim();
                        matchresult = fibmatch(answer, studentanswer, true);
                    }
                } // if case sensitive 
                else {
                    // case insensitive , if casesensitive is false, or null, or "".
                    if (data.getAnswerText() != null) {
                        studentanswer = data.getAnswerText().trim();
                        matchresult = fibmatch(answer, studentanswer, false);
                    }
                } // else , case insensitive

                if (matchresult) {

                    boolean alreadyused = false;
                    // add check for mutual exclusive
                    if ("true".equalsIgnoreCase(mutuallyexclusive)) {
                        // check if answers are already used.
                        Set answer_used_sofar = (HashSet) fibmap.get(itemId);
                        if ((answer_used_sofar != null)
                                && (answer_used_sofar.contains(studentanswer.toLowerCase()))) {
                            // already used, so it's a wrong answer for mutually exclusive questions
                            alreadyused = true;
                        } else {
                            // not used, it's a good answer, now add this to the already_used list.
                            // we only store lowercase strings in the fibmap.
                            if (answer_used_sofar == null) {
                                answer_used_sofar = new HashSet();
                            }

                            answer_used_sofar.add(studentanswer.toLowerCase());
                            fibmap.put(itemId, answer_used_sofar);
                        }
                    }

                    if (alreadyused) {
                        matchresult = false;
                    }

                    break;
                }

            }
        }
        return matchresult;
    }

    public double getFINScore(ItemGradingData data, ItemDataIfc itemdata, Map publishedAnswerHash)
            throws FinFormatException {
        data.setIsCorrect(Boolean.FALSE);
        double totalScore = (double) 0;
        boolean matchresult = getFINResult(data, itemdata, publishedAnswerHash);
        if (matchresult) {
            totalScore += ((AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId())).getScore()
                    .doubleValue();
            data.setIsCorrect(Boolean.TRUE);
        }
        return totalScore;

    }

    public boolean getFINResult(ItemGradingData data, ItemDataIfc itemdata, Map publishedAnswerHash)
            throws FinFormatException {
        String studentanswer = "";
        boolean range;
        boolean matchresult = false;
        ComplexFormat complexFormat = new ComplexFormat();
        Complex answerComplex = null;
        Complex studentAnswerComplex = null;
        BigDecimal answerNum = null, answer1Num = null, answer2Num = null, studentAnswerNum = null;

        if (data.getPublishedAnswerId() == null) {
            return false;
        }

        AnswerIfc answerIfc = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answerIfc == null) {
            return matchresult;
        }
        String answertext = answerIfc.getText();
        if (answertext != null) {
            StringTokenizer st = new StringTokenizer(answertext, "|");
            range = false;
            if (st.countTokens() > 1) {
                range = true;
            }

            String studentAnswerText = null;
            if (data.getAnswerText() != null) {
                studentAnswerText = data.getAnswerText().replaceAll("\\s+", "").replace(',', '.'); // in Spain, comma is used as a decimal point
            }

            if (range) {

                String answer1 = st.nextToken().trim();
                String answer2 = st.nextToken().trim();

                if (answer1 != null) {
                    answer1 = answer1.trim().replace(',', '.'); // in Spain, comma is used as a decimal point     
                }
                if (answer2 != null) {
                    answer2 = answer2.trim().replace(',', '.'); // in Spain, comma is used as a decimal point     
                }

                try {
                    answer1Num = new BigDecimal(answer1);
                    answer2Num = new BigDecimal(answer2);
                } catch (Exception e) {
                    log.debug("Number is not BigDecimal: " + answer1 + " or " + answer2);
                }

                Map map = validate(studentAnswerText);
                studentAnswerNum = (BigDecimal) map.get(ANSWER_TYPE_REAL);

                matchresult = (answer1Num != null && answer2Num != null && studentAnswerNum != null
                        && (answer1Num.compareTo(studentAnswerNum) <= 0)
                        && (answer2Num.compareTo(studentAnswerNum) >= 0));
            } else { // not range
                String answer = st.nextToken().trim();

                if (answer != null) {
                    answer = answer.replaceAll("\\s+", "").replace(',', '.'); // in Spain, comma is used as a decimal point     
                }

                try {
                    answerNum = new BigDecimal(answer);
                } catch (NumberFormatException ex) {
                    log.debug("Number is not BigDecimal: " + answer);
                }

                try {
                    answerComplex = complexFormat.parse(answer);
                } catch (ParseException ex) {
                    log.debug("Number is not Complex: " + answer);
                }

                if (data.getAnswerText() != null) {
                    Map map = validate(studentAnswerText);

                    if (answerNum != null) {
                        studentAnswerNum = (BigDecimal) map.get(ANSWER_TYPE_REAL);
                        matchresult = (studentAnswerNum != null && answerNum.compareTo(studentAnswerNum) == 0);
                    } else if (answerComplex != null) {
                        studentAnswerComplex = (Complex) map.get(ANSWER_TYPE_COMPLEX);
                        matchresult = (studentAnswerComplex != null && answerComplex.equals(studentAnswerComplex));
                    }
                }
            }
        }
        return matchresult;
    }

    public double getImageMapScore(ItemGradingData data, ItemDataIfc itemdata, HashMap publishedItemTextHash,
            HashMap publishedAnswerHash) {
        // Final score must be... 
        // IF NOT PARTIALCREDIT THEN 0 or total
        // IF PARTIALCREDIT EACH PART ADDED. 

        data.setIsCorrect(Boolean.FALSE);
        double totalScore;

        Iterator iter = publishedAnswerHash.keySet().iterator();
        int answerNumber = 0;
        while (iter.hasNext()) {
            Long answerId = Long.valueOf(iter.next().toString());
            AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(answerId);

            if (answer.getItem().getItemId().equals(data.getPublishedItemId()))
                answerNumber = answerNumber + 1;

        }

        double answerScore = itemdata.getScore();

        if (answerNumber != 0) {
            answerScore = answerScore / answerNumber;
        }

        ItemTextIfc itemTextIfc = (ItemTextIfc) publishedItemTextHash.get(data.getPublishedItemTextId());

        ArrayList answerArray = (ArrayList) itemTextIfc.getAnswerArray();
        AnswerIfc answerIfc = (AnswerIfc) answerArray.get(0);

        try {
            String area = answerIfc.getText();
            Integer areax1 = Integer.valueOf(
                    area.substring(area.indexOf("\"x1\":") + 5, area.indexOf(",", area.indexOf("\"x1\":"))));
            Integer areay1 = Integer.valueOf(
                    area.substring(area.indexOf("\"y1\":") + 5, area.indexOf(",", area.indexOf("\"y1\":"))));
            Integer areax2 = Integer.valueOf(
                    area.substring(area.indexOf("\"x2\":") + 5, area.indexOf(",", area.indexOf("\"x2\":"))));
            Integer areay2 = Integer.valueOf(
                    area.substring(area.indexOf("\"y2\":") + 5, area.indexOf("}", area.indexOf("\"y2\":"))));

            String point = data.getAnswerText();
            Integer pointx = Integer.valueOf(
                    point.substring(point.indexOf("\"x\":") + 4, point.indexOf(",", point.indexOf("\"x\":"))));
            Integer pointy = Integer.valueOf(
                    point.substring(point.indexOf("\"y\":") + 4, point.indexOf("}", point.indexOf("\"y\":"))));

            if (((pointx >= areax1) && (pointx <= areax2)) && ((pointy >= areay1) && (pointy <= areay2))) {
                totalScore = answerScore;
                data.setIsCorrect(Boolean.TRUE);
            } else {
                totalScore = 0;
            }
        } catch (Exception ex) {
            totalScore = 0;
        }

        return totalScore;
    }

    /**
     * Validate a students numeric answer 
     * @param The answer to validate
     * @return a Map containing either Real or Complex answer keyed by {@link #ANSWER_TYPE_REAL} or {@link #ANSWER_TYPE_COMPLEX} 
     */
    public Map validate(String value) {
        HashMap map = new HashMap();
        if (value == null || value.trim().equals("")) {
            return map;
        }
        String trimmedValue = value.trim();
        boolean isComplex = true;
        boolean isRealNumber = true;

        BigDecimal studentAnswerReal = null;
        try {
            studentAnswerReal = new BigDecimal(trimmedValue);
        } catch (Exception e) {
            isRealNumber = false;
        }

        // Test for complex number only if it is not a BigDecimal
        Complex studentAnswerComplex = null;
        if (!isRealNumber) {
            try {
                DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
                df.setGroupingUsed(false);

                // Numerical format ###.## (decimal symbol is the point)
                ComplexFormat complexFormat = new ComplexFormat(df);
                studentAnswerComplex = complexFormat.parse(trimmedValue);

                // This is because there is a bug parsing complex number. 9i is parsed as 9
                if (studentAnswerComplex.getImaginary() == 0 && trimmedValue.contains("i")) {
                    isComplex = false;
                }
            } catch (Exception e) {
                isComplex = false;
            }
        }

        Boolean isValid = isComplex || isRealNumber;
        if (!isValid) {
            throw new FinFormatException("Not a valid FIN Input. studentanswer=" + trimmedValue);
        }

        if (isRealNumber) {
            map.put(ANSWER_TYPE_REAL, studentAnswerReal);
        } else if (isComplex) {
            map.put(ANSWER_TYPE_COMPLEX, studentAnswerComplex);
        }

        return map;
    }

    /**
     * EMI score processing
     * 
     */
    private double getEMIScore(ItemGradingData itemGrading, Long itemId, Map totalItems,
            Map<Long, Map<Long, Set<EMIScore>>> emiScoresMap, Map publishedItemTextHash, Map publishedAnswerHash) {

        log.debug("getEMIScore( " + itemGrading + ", " + itemId);
        double autoScore = 0.0;
        if (!totalItems.containsKey(itemId)) {
            totalItems.put(itemId, new HashMap());
            emiScoresMap.put(itemId, new HashMap<Long, Set<EMIScore>>());
        }

        autoScore = getAnswerScore(itemGrading, publishedAnswerHash);
        AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(itemGrading.getPublishedAnswerId());
        if (answer == null) {
            //its possible we have an orphaned object ...
            log.warn("could not find answer: " + itemGrading.getPublishedAnswerId() + ", for item "
                    + itemGrading.getItemGradingId());
            return 0.0;
        }
        Long itemTextId = itemGrading.getPublishedItemTextId();

        // update the fibEmiAnswersMap so we can keep track
        // of how many answers were given
        Map<Long, Set<EMIScore>> emiItemScoresMap = emiScoresMap.get(itemId);
        // place the answer scores in a sorted set.
        // so now we can mark the correct ones and discount the extra incorrect
        // ones.
        Set<EMIScore> scores = null;
        if (emiItemScoresMap.containsKey(itemTextId)) {
            scores = emiItemScoresMap.get(itemTextId);
        } else {
            scores = new TreeSet<EMIScore>();
            emiItemScoresMap.put(itemTextId, scores);
        }
        scores.add(new EMIScore(itemId, itemTextId, itemGrading.getPublishedAnswerId(), answer.getIsCorrect(),
                autoScore));

        ItemTextIfc itemText = (ItemTextIfc) publishedItemTextHash.get(itemTextId);
        int numberCorrectAnswers = itemText.getEmiCorrectOptionLabels().length();
        Integer requiredCount = itemText.getRequiredOptionsCount();
        // re-calculate the scores over for the whole item
        autoScore = 0.0;
        int c = 0;
        for (EMIScore s : scores) {
            c++;
            s.effectiveScore = 0.0;
            if (c <= numberCorrectAnswers && c <= requiredCount) {
                // if correct and in count then add score
                s.effectiveScore = s.correct ? s.score : 0.0;
            } else if (c > numberCorrectAnswers) {
                // if incorrect and over count add discount
                s.effectiveScore = !s.correct ? s.score : 0.0;
            }
            if (autoScore + s.effectiveScore < 0.0) {
                // the current item tipped it to negative,
                // we cannot do this, so add zero
                s.effectiveScore = 0.0;
            }
            autoScore += s.effectiveScore;
        }

        // override score
        if (itemGrading.getOverrideScore() != null)
            autoScore += itemGrading.getOverrideScore().doubleValue();

        HashMap totalItemTextScores = (HashMap) totalItems.get(itemId);
        totalItemTextScores.put(itemTextId, Double.valueOf(autoScore));
        return autoScore;
    }

    /**
     * CALCULATED_QUESTION
     * Returns a double score value for the ItemGrading element being scored for a Calculated Question
     * 
     * @param calcQuestionAnswerSequence the order of answers in the list
     * @return score for the item.
     */
    public double getCalcQScore(ItemGradingData data, ItemDataIfc itemdata,
            Map<Integer, String> calculatedAnswersMap, int calcQuestionAnswerSequence) {
        double totalScore = (double) 0;

        if (data.getAnswerText() == null)
            return totalScore; // zero for blank

        if (!calculatedAnswersMap.containsKey(calcQuestionAnswerSequence)) {
            return totalScore;
        }
        // this variable should look something like this "42.1|2,2"
        String allAnswerText = calculatedAnswersMap.get(calcQuestionAnswerSequence).toString();

        // NOTE: this correctAnswer will already have been trimmed to the appropriate number of decimals
        BigDecimal correctAnswer = new BigDecimal(getAnswerExpression(allAnswerText));

        // Determine if the acceptable variance is a constant or a % of the answer
        String varianceString = allAnswerText.substring(allAnswerText.indexOf("|") + 1, allAnswerText.indexOf(","));
        BigDecimal acceptableVariance = BigDecimal.ZERO;
        if (varianceString.contains("%")) {
            double percentage = Double.valueOf(varianceString.substring(0, varianceString.indexOf("%")));
            acceptableVariance = correctAnswer.multiply(new BigDecimal(percentage / 100));
        } else {
            acceptableVariance = new BigDecimal(varianceString);
        }

        String userAnswerString = data.getAnswerText().replaceAll(",", "").trim();
        BigDecimal userAnswer;
        try {
            userAnswer = new BigDecimal(userAnswerString);
        } catch (NumberFormatException nfe) {
            return totalScore; // zero because it's not even a number!
        }
        //double userAnswer = Double.valueOf(userAnswerString);

        // this compares the correctAnswer against the userAnsewr
        BigDecimal answerDiff = (correctAnswer.subtract(userAnswer));
        boolean closeEnough = (answerDiff.abs().compareTo(acceptableVariance.abs()) <= 0);
        if (closeEnough) {
            totalScore += itemdata.getScore();
        }
        return totalScore;

    }

    public boolean getCalcQResult(ItemGradingData data, ItemDataIfc itemdata,
            Map<Integer, String> calculatedAnswersMap, int calcQuestionAnswerSequence) {
        boolean result = false;

        if (data.getAnswerText() == null)
            return result;

        if (!calculatedAnswersMap.containsKey(calcQuestionAnswerSequence)) {
            return result;
        }
        // this variable should look something like this "42.1|2,2"
        String allAnswerText = calculatedAnswersMap.get(calcQuestionAnswerSequence).toString();

        // NOTE: this correctAnswer will already have been trimmed to the appropriate number of decimals
        BigDecimal correctAnswer = new BigDecimal(getAnswerExpression(allAnswerText));

        // Determine if the acceptable variance is a constant or a % of the answer
        String varianceString = allAnswerText.substring(allAnswerText.indexOf("|") + 1, allAnswerText.indexOf(","));
        BigDecimal acceptableVariance = BigDecimal.ZERO;
        if (varianceString.contains("%")) {
            double percentage = Double.valueOf(varianceString.substring(0, varianceString.indexOf("%")));
            acceptableVariance = correctAnswer.multiply(new BigDecimal(percentage / 100));
        } else {
            acceptableVariance = new BigDecimal(varianceString);
        }

        String userAnswerString = data.getAnswerText().replaceAll(",", "").trim();
        BigDecimal userAnswer;
        try {
            userAnswer = new BigDecimal(userAnswerString);
        } catch (NumberFormatException nfe) {
            return result;
        }

        // this compares the correctAnswer against the userAnsewr
        BigDecimal answerDiff = (correctAnswer.subtract(userAnswer));
        boolean closeEnough = (answerDiff.abs().compareTo(acceptableVariance.abs()) <= 0);
        if (closeEnough) {
            result = true;
        }
        return result;

    }

    public double getTotalCorrectScore(ItemGradingData data, Map publishedAnswerHash) {
        AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answer == null || answer.getScore() == null)
            return (double) 0;
        return answer.getScore().doubleValue();
    }

    private void setIsLate(AssessmentGradingData data, PublishedAssessmentIfc pub) {
        // If submit from timeout popup, we don't record LATE
        if (data.getSubmitFromTimeoutPopup()) {
            data.setIsLate(Boolean.valueOf(false));
        } else {
            if (pub.getAssessmentAccessControl() != null && pub.getAssessmentAccessControl().getDueDate() != null
                    && pub.getAssessmentAccessControl().getDueDate().before(new Date()))
                data.setIsLate(Boolean.TRUE);
            else
                data.setIsLate(Boolean.valueOf(false));
        }

        if (data.getForGrade().booleanValue())
            data.setStatus(Integer.valueOf(1));

        data.setTotalOverrideScore(Double.valueOf(0));
    }

    public void deleteAll(Collection c) {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries().deleteAll(c);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /* Note:
     * assessmentGrading contains set of itemGrading that are not saved in the DB yet
     */
    public void updateAssessmentGradingScore(AssessmentGradingData adata, PublishedAssessmentIfc pub) {
        try {
            Set itemGradingSet = adata.getItemGradingSet();
            Iterator iter = itemGradingSet.iterator();
            double totalAutoScore = 0;
            double totalOverrideScore = adata.getTotalOverrideScore().doubleValue();
            while (iter.hasNext()) {
                ItemGradingData i = (ItemGradingData) iter.next();
                if (i.getAutoScore() != null)
                    totalAutoScore += i.getAutoScore().doubleValue();
            }
            double oldAutoScore = adata.getTotalAutoScore().doubleValue();
            double scoreDifference = totalAutoScore - oldAutoScore;
            adata.setTotalAutoScore(Double.valueOf(totalAutoScore));
            if (Double.compare((totalAutoScore + totalOverrideScore), Double.valueOf("0").doubleValue()) < 0) {
                adata.setFinalScore(Double.valueOf("0"));
            } else {
                adata.setFinalScore(Double.valueOf(totalAutoScore + totalOverrideScore));
            }
            saveOrUpdateAssessmentGrading(adata);
            if (scoreDifference != 0) {
                notifyGradebookByScoringType(adata, pub);
            }
        } catch (GradebookServiceException ge) {
            ge.printStackTrace();
            throw ge;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public void saveOrUpdateAll(Collection<ItemGradingData> c) {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveOrUpdateAll(c);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public PublishedAssessmentIfc getPublishedAssessmentByAssessmentGradingId(String id) {
        PublishedAssessmentIfc pub = null;
        try {
            pub = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getPublishedAssessmentByAssessmentGradingId(Long.valueOf(id));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return pub;
    }

    public PublishedAssessmentIfc getPublishedAssessmentByPublishedItemId(String publishedItemId) {
        PublishedAssessmentIfc pub = null;
        try {
            pub = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getPublishedAssessmentByPublishedItemId(Long.valueOf(publishedItemId));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return pub;
    }

    public ArrayList getLastItemGradingDataPosition(Long assessmentGradingId, String agentId) {
        ArrayList results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLastItemGradingDataPosition(assessmentGradingId, agentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List getPublishedItemIds(Long assessmentGradingId) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getPublishedItemIds(assessmentGradingId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public HashSet getItemSet(Long publishedAssessmentId, Long sectionId) {
        HashSet results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getItemSet(publishedAssessmentId, sectionId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public Long getTypeId(Long itemGradingId) {
        Long typeId = null;
        try {
            typeId = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getTypeId(itemGradingId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return typeId;
    }

    public boolean fibmatch(String answer, String input, boolean casesensitive) {

        try {
            StringBuilder regex_quotebuf = new StringBuilder();

            String REGEX = answer.replaceAll("\\*", "|*|");
            String[] oneblank = REGEX.split("\\|");
            for (int j = 0; j < oneblank.length; j++) {
                if ("*".equals(oneblank[j])) {
                    regex_quotebuf.append(".+");
                } else {
                    regex_quotebuf.append(Pattern.quote(oneblank[j]));
                }
            }

            String regex_quote = regex_quotebuf.toString();
            Pattern p;
            if (casesensitive) {
                p = Pattern.compile(regex_quote);
            } else {
                p = Pattern.compile(regex_quote, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
            }
            Matcher m = p.matcher(input);
            boolean result = m.matches();
            return result;

        } catch (Exception e) {
            return false;
        }
    }

    public List getAllAssessmentGradingByAgentId(Long publishedAssessmentId, String agentIdString) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getAllAssessmentGradingByAgentId(publishedAssessmentId, agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public List<ItemGradingData> getAllItemGradingDataForItemInGrading(Long assesmentGradingId,
            Long publihsedItemId) {
        List<ItemGradingData> results = new ArrayList<ItemGradingData>();
        results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getAllItemGradingDataForItemInGrading(assesmentGradingId, publihsedItemId);
        return results;
    }

    public HashMap getSiteSubmissionCountHash(String siteId) {
        HashMap results = new HashMap();
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSiteSubmissionCountHash(siteId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public HashMap getSiteInProgressCountHash(final String siteId) {
        HashMap results = new HashMap();
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSiteInProgressCountHash(siteId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public int getActualNumberRetake(Long publishedAssessmentId, String agentIdString) {
        int actualNumberReatke = 0;
        try {
            actualNumberReatke = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getActualNumberRetake(publishedAssessmentId, agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return actualNumberReatke;
    }

    public HashMap getActualNumberRetakeHash(String agentIdString) {
        HashMap actualNumberReatkeHash = new HashMap();
        try {
            actualNumberReatkeHash = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getActualNumberRetakeHash(agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return actualNumberReatkeHash;
    }

    public HashMap getSiteActualNumberRetakeHash(String siteIdString) {
        HashMap numberRetakeHash = new HashMap();
        try {
            numberRetakeHash = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSiteActualNumberRetakeHash(siteIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return numberRetakeHash;
    }

    public List getStudentGradingSummaryData(Long publishedAssessmentId, String agentIdString) {
        List results = null;
        try {
            results = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getStudentGradingSummaryData(publishedAssessmentId, agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return results;
    }

    public int getNumberRetake(Long publishedAssessmentId, String agentIdString) {
        int numberRetake = 0;
        try {
            numberRetake = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getNumberRetake(publishedAssessmentId, agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return numberRetake;
    }

    public HashMap getNumberRetakeHash(String agentIdString) {
        HashMap numberRetakeHash = new HashMap();
        try {
            numberRetakeHash = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getNumberRetakeHash(agentIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return numberRetakeHash;
    }

    public HashMap getSiteNumberRetakeHash(String siteIdString) {
        HashMap siteActualNumberRetakeList = new HashMap();
        try {
            siteActualNumberRetakeList = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSiteNumberRetakeHash(siteIdString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return siteActualNumberRetakeList;
    }

    public void saveStudentGradingSummaryData(StudentGradingSummaryIfc studentGradingSummaryData) {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .saveStudentGradingSummaryData(studentGradingSummaryData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public int getLateSubmissionsNumberByAgentId(Long publishedAssessmentId, String agentIdString, Date dueDate) {
        int numberRetake = 0;
        try {
            numberRetake = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getLateSubmissionsNumberByAgentId(publishedAssessmentId, agentIdString, dueDate);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return numberRetake;
    }

    /**
     * 
     * @param publishedAssessmentId
     * @param anonymous
     * @param audioMessage
     * @param fileUploadMessage
     * @param noSubmissionMessage
     * @param showPartAndTotalScoreSpreadsheetColumns
     * @param poolString
     * @param partString
     * @param questionString
     * @param textString
     * @param rationaleString
     * @param itemGradingCommentsString
     * @param useridMap
     * @param responseCommentString
     * @return a list of responses or null if there are none
     */
    public List getExportResponsesData(String publishedAssessmentId, boolean anonymous, String audioMessage,
            String fileUploadMessage, String noSubmissionMessage, boolean showPartAndTotalScoreSpreadsheetColumns,
            String poolString, String partString, String questionString, String textString, String rationaleString,
            String itemGradingCommentsString, Map useridMap, String responseCommentString) {
        List list = null;
        try {
            list = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getExportResponsesData(
                    publishedAssessmentId, anonymous, audioMessage, fileUploadMessage, noSubmissionMessage,
                    showPartAndTotalScoreSpreadsheetColumns, poolString, partString, questionString, textString,
                    rationaleString, itemGradingCommentsString, useridMap, responseCommentString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    private void removeUnsubmittedAssessmentGradingData(AssessmentGradingData data) {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .removeUnsubmittedAssessmentGradingData(data);
        } catch (Exception e) {
            //e.printStackTrace();
            log.error("Exception thrown from removeUnsubmittedAssessmentGradingData" + e.getMessage());
        }
    }

    public boolean getHasGradingData(Long publishedAssessmentId) {
        boolean hasGradingData = false;
        try {
            hasGradingData = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getHasGradingData(publishedAssessmentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return hasGradingData;
    }

    /**
     * CALCULATED_QUESTION
     * @param itemGrading
     * @param item
     * @return map of calc answers
     */
    private Map<Integer, String> getCalculatedAnswersMap(ItemGradingData itemGrading, ItemDataIfc item) {
        HashMap<Integer, String> calculatedAnswersMap = new HashMap<Integer, String>();
        // return value from extractCalcQAnswersArray is not used, calculatedAnswersMap is populated by this call
        extractCalcQAnswersArray(calculatedAnswersMap, item, itemGrading.getAssessmentGradingId(),
                itemGrading.getAgentId());
        return calculatedAnswersMap;
    }

    /**
     * extractCalculations() is a utility function for Calculated Questions.  It takes
     * one parameter, which is a block of text, and looks for any calculations
     * that are encoded in the text.  A calculations is enclosed in [[ ]].
     * <p>For example, if the passed parameter is <code>{a} + {b} = {{c}}, [[{a}+{b}]]</code>, 
     * the resulting list would contain one entry: a string of "{a}+{b}"
     * <p>Formulas must contain at least one variable OR parens OR calculation symbol (*-+/)
     * @param text contents to be searched
     * @return a list of matching calculations.  If no calculations are found, the 
     * list will be empty.
     */
    public List<String> extractCalculations(String text) {
        List<String> calculations = extractCalculatedQuestionKeyFromItemText(text, CALCQ_CALCULATION_PATTERN);
        for (Iterator<String> iterator = calculations.iterator(); iterator.hasNext();) {
            String calc = iterator.next();
            if (!StringUtils.containsAny(calc, "{}()+-*/")) {
                iterator.remove();
            }
        }
        return calculations;
    }

    /**
     * extractFormulas() is a utility function for Calculated Questions.  It takes
     * one parameter, which is a block of text, and looks for any formula names
     * that are encoded in the text.  A formula name is enclosed in {{ }}.  The 
     * formula itself is encoded elsewhere.
     * <p>For example, if the passed parameter is <code>{a} + {b} = {{c}}</code>, 
     * the resulting list would contain one entry: a string of "c"
     * <p>Formulas must begin with an alpha, but subsequent character can be 
     * alpha-numeric
     * @param text contents to be searched
     * @return a list of matching formula names.  If no formulas are found, the 
     * list will be empty.
     */
    public List<String> extractFormulas(String text) {
        return extractCalculatedQuestionKeyFromItemText(text, CALCQ_FORMULA_PATTERN);
    }

    /**
     * extractVariables() is a utility function for Calculated Questions.  It 
     * takes one parameter, which is a block of text, and looks for any variable 
     * names that are encoded in the text.  A variable name is enclosed in { }.  
     * The values of the variable are encoded elsewhere.
     * <p>For example, if the passed parameter is <code>{a} + {b} = {{c}}</code>, 
     * the resulting list would contain two entries: strings of "a" and "b"
     * <p>Variables must begin with an alpha, but subsequent character can be 
     * alpha-numeric.
     * <p>Note - a formula, encoded as {{ }}, will not be mistaken for a variable.
     * @param text content to be searched
     * @return a list of matching variable names.  If no variables are found, the 
     * list will be empty
     */
    public List<String> extractVariables(String text) {
        return extractCalculatedQuestionKeyFromItemText(text, CALCQ_ANSWER_PATTERN);
    }

    /**
     * extractCalculatedQuestionKeyFromItemText() is a utility function for Calculated Questions.  It 
     * takes a block of item text, and uses a pattern to looks for keys 
     * that are encoded in the text.
     * @param itemText content to be searched
     * @param identifierPattern pattern to use to do the search
     * @return a list of matching key values OR empty if none are found
     */
    private List<String> extractCalculatedQuestionKeyFromItemText(String itemText, Pattern identifierPattern) {
        LinkedHashSet<String> keys = new LinkedHashSet<String>();
        if (itemText != null && itemText.trim().length() > 0) {
            Matcher keyMatcher = identifierPattern.matcher(itemText);
            while (keyMatcher.find()) {
                String match = keyMatcher.group(1);
                keys.add(match);
                /*
                // first character before matching group
                int start = keyMatcher.start(1) - 2;
                // first character after matching group
                int end = keyMatcher.end(1) + 1; // first character after the matching group
                // if matching group is wrapped by {}, it's not what we are looking for (like another key or just some text)
                if (start < 0 || end >= itemText.length() || itemText.charAt(start) != '{' || itemText.charAt(end) != '}') {
                    keys.add(match);
                }*/
            }
        }
        return new ArrayList<String>(keys);
    }

    /**
     * CALCULATED_QUESTION
     * @param item the item which contains the formula
     * @param formulaName the name of the formula
     * @return the actual formula that matches this formula name OR "" (empty string) if it is not found
     */
    private String replaceFormulaNameWithFormula(ItemDataIfc item, String formulaName) {
        String result = "";
        @SuppressWarnings("unchecked")
        List<ItemTextIfc> items = item.getItemTextArray();
        for (ItemTextIfc itemText : items) {
            if (itemText.getText().equals(formulaName)) {
                @SuppressWarnings("unchecked")
                List<AnswerIfc> answers = itemText.getAnswerArray();
                for (AnswerIfc answer : answers) {
                    if (itemText.getSequence().equals(answer.getSequence())) {
                        result = answer.getText();
                        break;
                    }
                }
            }
        }
        return result;
    }

    /**
     * CALCULATED_QUESTION
     * Takes the instructions and breaks it into segments, based on the location 
     * of formula names.  One formula would give two segments, two formulas gives
     * three segments, etc.
     * <p>Note - in this context, it would probably be easier if any variable value
     * substitutions have occurred before the breakup is done; otherwise,
     * each segment will need to have substitutions done.
     * @param instructions string to be broken up
     * @return the original string, broken up based on the formula name delimiters
     */
    protected List<String> extractInstructionSegments(String instructions) {
        List<String> segments = new ArrayList<String>();
        if (instructions != null && instructions.length() > 0) {
            String[] results = CALCQ_FORMULA_SPLIT_PATTERN.split(instructions); // only works because all variables and calculations are already replaced
            for (String part : results) {
                segments.add(part);
            }
            if (segments.size() == 1) {
                // add in the trailing segment
                segments.add("");
            }
            /*
            final String FUNCTION_BEGIN = "{{";
            final String FUNCTION_END = "}}";
            while (instructions.indexOf(FUNCTION_BEGIN) > -1 && instructions.indexOf(FUNCTION_END) > -1) {
                String segment = instructions.substring(0, instructions.indexOf(FUNCTION_BEGIN));
                instructions = instructions.substring(instructions.indexOf(FUNCTION_END) + FUNCTION_END.length());
                segments.add(segment);
            }
            segments.add(instructions);
             */
        }
        return segments;
    }

    /**
     * CALCULATED_QUESTION
     * applyPrecisionToNumberString() takes a string representation of a number and returns
     * a string representation of that number, rounded to the specified number of
     * decimal places, including trimming decimal places if needed.
     * Will also throw away the extra trailing zeros as well as removing a trailing decimal point.
     * @param numberStr
     * @param decimalPlaces
     * @return processed number string (will never be null or empty string)
     */
    public String applyPrecisionToNumberString(String numberStr, int decimalPlaces) {
        // Trim off excess decimal points based on decimalPlaces value
        BigDecimal bd = new BigDecimal(numberStr);
        bd = bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_EVEN);

        String decimal = ".";
        // TODO handle localized decimal separator?
        //DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale);
        //char dec = dfs.getDecimalFormatSymbols().getDecimalSeparator();

        String displayAnswer = bd.toString();
        if (displayAnswer.length() > 2 && displayAnswer.contains(decimal)) {
            if (decimalPlaces == 0) { // Remove ".0" if decimalPlaces == 0
                displayAnswer = displayAnswer.replace(decimal + "0", "");
            } else {
                // trim away all the extra 0s from the end of the number
                if (displayAnswer.endsWith("0")) {
                    displayAnswer = StringUtils.stripEnd(displayAnswer, "0");
                }
                if (displayAnswer.endsWith(decimal)) {
                    displayAnswer = displayAnswer.substring(0, displayAnswer.length() - 1);
                }
            }
        }
        return displayAnswer;
    }

    /**
     * CALCULATED_QUESTION
     * calculateFormulaValues() evaluates all formulas referenced in the 
     * instructions.  For each formula name it finds, it retrieves the formula
     * for the name, substitutes the randomized value for the variables,
     * evaluates the formula to a real answer, and put the answer, along with
     * the needed precision and decimal places in the returning Map. 
     * @param variables a Map<String, String> of variables,  The key is the 
     * variable name, the value is the text representation, after randomization, 
     * of a number in the variable's defined range.
     * @param item The question itself, which is needed to provide additional 
     * information for called functions
     * @return a Map<Integer, String>.  the Integer is simply the sequence.  
     * Answers are returned in the order that the formulas are found.
     * The String is the result of the formula, encoded as (value)|(tolerance),(decimal places)
     * @throws Exception if either the formula expression fails to pass the 
     * Samigo expression parser, which should never happen as this is validated
     * when the question is saved, or if a divide by zero error occurs.
     */
    private Map<Integer, String> calculateFormulaValues(Map<String, String> variables, ItemDataIfc item)
            throws Exception {
        Map<Integer, String> values = new HashMap<Integer, String>();
        String instructions = item.getInstruction();
        List<String> formulaNames = this.extractFormulas(instructions);
        for (int i = 0; i < formulaNames.size(); i++) {
            String formulaName = formulaNames.get(i);
            String longFormula = replaceFormulaNameWithFormula(item, formulaName); // {a}+{b}|0.1,1
            longFormula = defaultVarianceAndDecimal(longFormula); // sets defaults, in case tolerance or precision isn't set

            String formula = getAnswerExpression(longFormula); // returns just the formula
            String answerData = getAnswerData(longFormula); // returns just tolerance and precision
            int decimalPlaces = getAnswerDecimalPlaces(answerData);

            String substitutedFormula = replaceMappedVariablesWithNumbers(formula, variables);
            String formulaValue = processFormulaIntoValue(substitutedFormula, decimalPlaces);
            values.put(i + 1, formulaValue + answerData); // later answerData will be used for scoring
        }
        return values;
    }

    /**
     * CALCULATED_QUESTION
     * This is a busy method. It does three things:
     * <br>1. It removes the answer expressions ie. {{x+y}} from the question text. This value is
     *    returned in the ArrayList texts. This format is necessary so that input boxes can be
     *    placed in the text where the {{..}}'s appear.
     * <br>2. It will call methods to swap out the defined variables with randomly generated values
     *    within the ranges defined by the user.
     * <br>3. It updates the HashMap answerList with the calculated answers in sequence. It will 
     *    parse and calculate what each answer needs to be.
     * <p>Note: If a divide by zero occurs. We change the random values and try again. It gets limited chances to
     *    get valid values and then will return "infinity" as the answer.
     * @param answerList will enter the method empty and be filled with sequential answers to the question
     * @return ArrayList of the pieces of text to display surrounding input boxes
     */
    public List<String> extractCalcQAnswersArray(Map<Integer, String> answerList, ItemDataIfc item, Long gradingId,
            String agentId) {
        final int MAX_ERROR_TRIES = 100;
        boolean hasErrors = true;
        Map<String, String> variableRangeMap = buildVariableRangeMap(item);
        List<String> instructionSegments = new ArrayList<String>(0);

        int attemptCount = 1;
        while (hasErrors && attemptCount <= MAX_ERROR_TRIES) {
            instructionSegments.clear();
            Map<String, String> variablesWithValues = determineRandomValuesForRanges(variableRangeMap,
                    item.getItemId(), gradingId, agentId, attemptCount);
            try {
                Map<Integer, String> evaluatedFormulas = calculateFormulaValues(variablesWithValues, item);
                answerList.putAll(evaluatedFormulas);
                // replace the variables in the text with values
                String instructions = item.getInstruction();
                instructions = replaceMappedVariablesWithNumbers(instructions, variablesWithValues);
                // then replace the calculations with values (must happen AFTER the variable replacement)
                try {
                    instructions = replaceCalculationsWithValues(instructions, 5); // what decimal precision should we use here?
                    // if could not process the calculation into a result then throws IllegalStateException which will be caught below and cause the numbers to regenerate
                } catch (SamigoExpressionError e1) {
                    log.warn("Samigo calculated item (" + item.getItemId() + ") calculation invalid: " + e1.get());
                }
                // only pull out the segments if the formulas worked
                instructionSegments = extractInstructionSegments(instructions);
                hasErrors = false;
            } catch (Exception e) {
                attemptCount++;
            }
        }
        return instructionSegments;
    }

    /**
     * CALCULATED_QUESTION
     * This returns the decimal places value in the stored answer data.
     * @param allAnswerText
     * @return
     */
    private int getAnswerDecimalPlaces(String allAnswerText) {
        String answerData = getAnswerData(allAnswerText);
        int decimalPlaces = Integer.valueOf(answerData.substring(answerData.indexOf(",") + 1, answerData.length()));
        return decimalPlaces;
    }

    /**
     * CALCULATED_QUESTION
     * This returns the "|2,2" (variance and decimal display) from the stored answer data.
     * @param allAnswerText
     * @return
     */
    private String getAnswerData(String allAnswerText) {
        String answerData = allAnswerText.substring(allAnswerText.indexOf("|"), allAnswerText.length());
        return answerData;
    }

    /**
     * CALCULATED_QUESTION
     * This is just "(x+y)/z" or if values have been added to the expression it's the
     * calculated value as stored in the answer data.
     * @param allAnswerText
     * @return
     */
    private String getAnswerExpression(String allAnswerText) {
        String answerExpression = allAnswerText.substring(0, allAnswerText.indexOf("|"));
        return answerExpression;
    }

    /**
     * CALCULATED_QUESTION
     * Default acceptable variance and decimalPlaces. An answer is defined by an expression
     * such as {x+y|1,2} if the variance and decimal places are left off. We have to default
     * them to something.
     */
    private String defaultVarianceAndDecimal(String allAnswerText) {
        String defaultVariance = "0.001";
        String defaultDecimal = "3";

        if (!allAnswerText.contains("|")) {
            if (!allAnswerText.contains(","))
                allAnswerText = allAnswerText.concat("|" + defaultVariance + "," + defaultDecimal);
            else
                allAnswerText = allAnswerText.replace(",", "|" + defaultVariance + ",");
        }
        if (!allAnswerText.contains(","))
            allAnswerText = allAnswerText.concat("," + defaultDecimal);

        return allAnswerText;
    }

    /**
     * CALCULATED_QUESTION
     * Takes an answer string and checks for the value returned
     * is NaN or Infinity, indicating a Samigo formula parse error
     * Returns false if divide by zero is detected.
     */
    public boolean isAnswerValid(String answer) {
        String INFINITY = "Infinity";
        String NaN = "NaN";
        if (answer.length() == 0)
            return false;
        if (answer.equals(INFINITY))
            return false;
        if (answer.equals(NaN))
            return false;
        return true;
    }

    /**
     * CALCULATED_QUESTION
     * replaceMappedVariablesWithNumbers() takes a string and substitutes any variable
     * names found with the value of the variable.  Variables look like {a}, the name of 
     * that variable is "a", and the value of that variable is in variablesWithValues
     * <p>Note - this function comprehends syntax like "5{x}".  If "x" is 37, the
     * result would be "5*37"
     * @param expression - the string being substituted into
     * @param variables - Map key is the variable name, value is what will be 
     * substituted into the expression.
     * @return a string with values substituted.  If answerExpression is null, 
     * returns a blank string (i.e "").  If variablesWithValues is null, returns
     * the original answerExpression 
     */
    public String replaceMappedVariablesWithNumbers(String expression, Map<String, String> variables) {
        if (expression == null) {
            expression = "";
        }

        if (variables == null) {
            variables = new HashMap<String, String>();
        }

        for (Map.Entry<String, String> entry : variables.entrySet()) {
            String name = "{" + entry.getKey() + "}";
            String value = entry.getValue();

            // not doing string replace or replaceAll because the value being
            // substituted can change for each occurrence of the variable.
            int index = expression.indexOf(name);
            while (index > -1) {
                String prefix = expression.substring(0, index);
                String suffix = expression.substring(index + name.length());

                String replacementValue = value;
                // if last character of prefix is a number or the edge of parenthesis, multiply by the variable
                // if x = 37, 5{x} -> 5*37
                // if x = 37 (5+2){x} -> (5+2)*37 (prefix is (5+2)
                if (prefix.length() > 0 && (Character.isDigit(prefix.charAt(prefix.length() - 1))
                        || prefix.charAt(prefix.length() - 1) == ')')) {
                    replacementValue = "*" + replacementValue;
                }

                // if first character of suffix is a number or the edge of parenthesis, multiply by the variable
                // if x = 37, {x}5 -> 37*5
                // if x = 37, {x}(5+2) -> 37*(5+2) (suffix is (5+2)
                if (suffix.length() > 0 && (Character.isDigit(suffix.charAt(0)) || suffix.charAt(0) == '(')) {
                    replacementValue = replacementValue + "*";
                }

                // perform substitution, then look for the next instance of current variable
                expression = prefix + replacementValue + suffix;
                index = expression.indexOf(name);
            }
        }
        return expression;
    }

    /**
     * CALCULATED_QUESTION
     * replaceMappedVariablesWithNumbers() takes a string and substitutes any variable
     * names found with the value of the variable.  Variables look like {a}, the name of 
     * that variable is "a", and the value of that variable is in variablesWithValues
     * <p>Note - this function comprehends syntax like "5{x}".  If "x" is 37, the
     * result would be "5*37"
     * @param expression - the string which will be scanned for calculations
     * @return the input string with calculations replaced with number values. If answerExpression is null, 
     * returns a blank string (i.e "") and if no calculations are found then original string is returned.
     * @throws IllegalStateException if the formula value cannot be calculated
     * @throws SamigoExpressionError if the formula cannot be parsed
     */
    public String replaceCalculationsWithValues(String expression, int decimalPlaces) throws SamigoExpressionError {
        if (StringUtils.isEmpty(expression)) {
            expression = "";
        } else {
            Matcher keyMatcher = CALCQ_CALCULATION_PATTERN.matcher(expression);
            ArrayList<String> toReplace = new ArrayList<String>();
            while (keyMatcher.find()) {
                String match = keyMatcher.group(1);
                toReplace.add(match); // should be the formula
            }
            if (toReplace.size() > 0) {
                for (String formula : toReplace) {
                    String replace = CALCULATION_OPEN + formula + CALCULATION_CLOSE;
                    String formulaValue = processFormulaIntoValue(formula, decimalPlaces);
                    expression = StringUtils.replace(expression, replace, formulaValue);
                }
            }
        }
        return expression;
    }

    /**
     * CALCULATED_QUESTION
     * Process a single formula into a final string representing the calculated value of the formula
     * 
     * @param formula the formula to process (e.g. 1 * 2 + 3 - 4), All variable replacement must have already happened
     * @param decimalPlaces number of decimals to include in the final output
     * @return the value of the formula OR empty string if there is nothing to process
     * @throws IllegalStateException if the formula value cannot be calculated (typically caused by 0 divisors and the like)
     * @throws SamigoExpressionError if the formula cannot be parsed
     */
    public String processFormulaIntoValue(String formula, int decimalPlaces) throws SamigoExpressionError {
        String value = "";
        if (StringUtils.isEmpty(formula)) {
            value = "";
        } else {
            if (decimalPlaces < 0) {
                decimalPlaces = 0;
            }
            formula = cleanFormula(formula);
            SamigoExpressionParser parser = new SamigoExpressionParser(); // this will turn the expression into a number in string form
            String numericString = parser.parse(formula, decimalPlaces + 1);
            if (this.isAnswerValid(numericString)) {
                numericString = applyPrecisionToNumberString(numericString, decimalPlaces);
                value = numericString;
            } else {
                throw new IllegalStateException("Invalid calculation formula (" + formula + ") result ("
                        + numericString + "), result could not be calculated");
            }
        }
        return value;
    }

    /**
     * Cleans up formula text so that whitespaces are normalized or removed
     * @param formula formula with variables or without
     * @return the cleaned formula
     */
    public static String cleanFormula(String formula) {
        if (StringUtils.isEmpty(formula)) {
            formula = "";
        } else {
            formula = StringUtils.trimToEmpty(formula).replaceAll("\\s+", " ");
        }
        return formula;
    }

    /**
     * isNegativeSqrt() looks at the incoming expression and looks specifically
     * to see if it executes the SQRT function.  If it does, it evaluates it.  If
     * it has an error, it assumes that the SQRT function tried to evaluate a 
     * negative number and evaluated to NaN.
     * <p>Note - the incoming expression should have no variables.  They should 
     * have been replaced before this function was called
     * @param expression a mathematical formula, with all variables replaced by
     * real values, to be evaluated
     * @return true if the function uses the SQRT function, and the SQRT function
     * evaluates as an error; else false
     * @throws SamigoExpressionError if the evaluation of the SQRT function throws
     * some other parse error
     */
    public boolean isNegativeSqrt(String expression) throws SamigoExpressionError {
        Pattern sqrt = Pattern.compile("sqrt\\s*\\(");
        boolean isNegative = false;
        if (expression == null) {
            expression = "";
        }
        expression = expression.toLowerCase();
        Matcher matcher = sqrt.matcher(expression);
        while (matcher.find()) {
            int x = matcher.end();
            int p = 1; // Parentheses left to match
            int len = expression.length();
            while (p > 0 && x < len) {
                if (expression.charAt(x) == ')') {
                    --p;
                } else if (expression.charAt(x) == '(') {
                    ++p;
                }
                ++x;
            }
            if (p == 0) {
                String sqrtExpression = expression.substring(matcher.start(), x);
                SamigoExpressionParser parser = new SamigoExpressionParser();
                String numericAnswerString = parser.parse(sqrtExpression);
                if (!isAnswerValid(numericAnswerString)) {
                    isNegative = true;
                    break; // finding 1 invalid one is enough
                }
            }
        }
        return isNegative;
    }

    /**
     * CALCULATED_QUESTION
     * Takes a map of ranges and randomly chooses values for those ranges and stores them in a new map.
     */
    public Map<String, String> determineRandomValuesForRanges(Map<String, String> variableRangeMap, long itemId,
            long gradingId, String agentId, int validAnswersAttemptCount) {
        Map<String, String> variableValueMap = new HashMap<String, String>();

        // seed random number generator
        long seed = getCalcuatedQuestionSeed(itemId, gradingId, agentId, validAnswersAttemptCount);
        Random generator = new Random(seed);

        Iterator<Map.Entry<String, String>> i = variableRangeMap.entrySet().iterator();
        while (i.hasNext()) {
            Map.Entry<String, String> entry = i.next();

            String delimRange = entry.getValue().toString(); // ie. "-100|100,2"

            double minVal = Double.valueOf(delimRange.substring(0, delimRange.indexOf('|')));
            double maxVal = Double
                    .valueOf(delimRange.substring(delimRange.indexOf('|') + 1, delimRange.indexOf(',')));
            int decimalPlaces = Integer
                    .valueOf(delimRange.substring(delimRange.indexOf(',') + 1, delimRange.length()));

            // This line does the magic of creating the random variable value within the range.
            Double randomValue = minVal + (maxVal - minVal) * generator.nextDouble();

            // Trim off excess decimal points based on decimalPlaces value
            BigDecimal bd = new BigDecimal(randomValue);
            bd = bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP);
            randomValue = bd.doubleValue();

            String displayNumber = randomValue.toString();
            // Remove ".0" if decimalPlaces ==0
            if (decimalPlaces == 0) {
                displayNumber = displayNumber.replace(".0", "");
            }

            variableValueMap.put(entry.getKey(), displayNumber);
        }

        return variableValueMap;
    }

    /**
     * CALCULATED_QUESTION
     * Accepts an ItemDataIfc and returns a HashMap with the pairs of 
     * variable names and variable ranges.
     */
    private Map<String, String> buildVariableRangeMap(ItemDataIfc item) {
        HashMap<String, String> variableRangeMap = new HashMap<String, String>();

        String instructions = item.getInstruction();
        List<String> variables = this.extractVariables(instructions);

        // Loop through each VarName
        @SuppressWarnings("unchecked")
        List<ItemTextIfc> itemTextList = item.getItemTextArraySorted();
        for (ItemTextIfc varName : itemTextList) {
            // only look at variables for substitution, ignore formulas
            if (variables.contains(varName.getText())) {
                @SuppressWarnings("unchecked")
                List<AnswerIfc> answerList = varName.getAnswerArray();
                for (AnswerIfc range : answerList) {
                    if (!(range.getLabel() == null)) { // answer records and variable records are in the same set
                        if (range.getSequence().equals(varName.getSequence()) && range.getText().contains("|")) {
                            variableRangeMap.put(varName.getText(), range.getText());
                        }
                    }
                }
            }
        }

        return variableRangeMap;
    }

    /**
     * CALCULATED_QUESTION
     * Make seed by combining user id, item (question) id, grading (submission) id, and attempt count (due to div by 0)
     */
    private long getCalcuatedQuestionSeed(long itemId, long gradingId, String agentId,
            int validAnswersAttemptCount) {
        long userSeed = (long) agentId.hashCode();
        return userSeed * itemId * gradingId * validAnswersAttemptCount;
    }

    /**
     * CALCULATED_QUESTION
     * Simple to check to see if this is a calculated question. It's used in storeGrades() to see if the sort is necessary.
     */
    private boolean isCalcQuestion(List tempItemGradinglist, HashMap publishedItemHash) {
        if (tempItemGradinglist == null)
            return false;
        if (tempItemGradinglist.size() == 0)
            return false;

        Iterator iter = tempItemGradinglist.iterator();
        while (iter.hasNext()) {
            ItemGradingData itemCheck = (ItemGradingData) iter.next();
            Long itemId = itemCheck.getPublishedItemId();
            ItemDataIfc item = (ItemDataIfc) publishedItemHash.get(itemId);
            if (item.getTypeId().equals(TypeIfc.CALCULATED_QUESTION)) {
                return true;
            }
        }
        return false;
    }

    public ArrayList getHasGradingDataAndHasSubmission(Long publishedAssessmentId) {
        ArrayList al = new ArrayList();
        try {
            al = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getHasGradingDataAndHasSubmission(publishedAssessmentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return al;
    }

    public String getFileName(Long itemGradingId, String agentId, String filename) {
        String name = "";
        try {
            name = PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getFilename(itemGradingId,
                    agentId, filename);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return name;
    }

    public List getUpdatedAssessmentList(String agentId, String siteId) {
        List list = null;
        try {
            list = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getUpdatedAssessmentList(agentId, siteId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    public List getSiteNeedResubmitList(String siteId) {
        List list = null;
        try {
            list = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .getSiteNeedResubmitList(siteId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    public void autoSubmitAssessments() {
        try {
            PersistenceService.getInstance().getAssessmentGradingFacadeQueries().autoSubmitAssessments();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ItemGradingAttachment createItemGradingAttachment(ItemGradingData itemGrading, String resourceId,
            String filename, String protocol) {
        ItemGradingAttachment attachment = null;
        try {
            attachment = PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                    .createItemGradingtAttachment(itemGrading, resourceId, filename, protocol);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return attachment;
    }

    public void removeItemGradingAttachment(String attachmentId) {
        PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .removeItemGradingAttachment(Long.valueOf(attachmentId));
    }

    public void saveOrUpdateAttachments(List list) {
        PersistenceService.getInstance().getAssessmentGradingFacadeQueries().saveOrUpdateAttachments(list);
    }

    public HashMap getInProgressCounts(String siteId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getInProgressCounts(siteId);
    }

    public HashMap getSubmittedCounts(String siteId) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries().getSubmittedCounts(siteId);
    }

    public void completeItemGradingData(AssessmentGradingData assessmentGradingData) {
        PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .completeItemGradingData(assessmentGradingData);
    }

    /**
     * This grades multiple choice and true false questions.  Since
     * multiple choice/multiple select has a separate ItemGradingData for
     * each choice, they're graded the same way the single choice are.
     * BUT since we have Partial Credit stuff around we have to have a separate method here  --mustansar
     * Choices should be given negative score values if one wants them
     * to lose points for the wrong choice.
     */
    public double getAnswerScoreMCQ(ItemGradingData data, Map publishedAnswerHash) {
        AnswerIfc answer = (AnswerIfc) publishedAnswerHash.get(data.getPublishedAnswerId());
        if (answer == null || answer.getScore() == null) {
            return 0d;
        } else if (answer.getIsCorrect().booleanValue()) { // instead of using answer score Item score needs to be used here 
            return (answer.getItem().getScore().doubleValue()); //--mustansar 
        }
        return (answer.getItem().getScore().doubleValue() * answer.getPartialCredit().doubleValue()) / 100d;
    }

    /**
     *  Reoder a map of EMI scores
     * @param emiScoresMap
     * @return
     */
    private Map<Long, Map<Long, Map<Long, EMIScore>>> reorderEMIScoreMap(
            Map<Long, Map<Long, Set<EMIScore>>> emiScoresMap) {
        Map<Long, Map<Long, Map<Long, EMIScore>>> scoresMap = new HashMap<Long, Map<Long, Map<Long, EMIScore>>>();
        for (Map<Long, Set<EMIScore>> emiItemScoresMap : emiScoresMap.values()) {
            for (Set<EMIScore> scoreSet : emiItemScoresMap.values()) {
                for (EMIScore s : scoreSet) {
                    Map<Long, Map<Long, EMIScore>> scoresItem = scoresMap.get(s.itemId);
                    if (scoresItem == null) {
                        scoresItem = new HashMap<Long, Map<Long, EMIScore>>();
                        scoresMap.put(s.itemId, scoresItem);
                    }
                    Map<Long, EMIScore> scoresItemText = scoresItem.get(s.itemTextId);
                    if (scoresItemText == null) {
                        scoresItemText = new HashMap<Long, EMIScore>();
                        scoresItem.put(s.itemTextId, scoresItemText);
                    }
                    scoresItemText.put(s.answerId, s);
                }
            }
        }
        return scoresMap;
    }

    /**
     * hasDistractors looks at an itemData object for a Matching question and determines
     * if all of the choices have correct matches or not.
     * @param item
     * @return true if any of the choices do not have a correct answer (a distractor choice), or false
     * if all choices have at least one correct answer
     */
    public boolean hasDistractors(ItemDataIfc item) {
        boolean hasDistractor = false;
        Iterator<ItemTextIfc> itemIter = item.getItemTextArraySorted().iterator();
        while (itemIter.hasNext()) {
            ItemTextIfc curItem = itemIter.next();
            if (isDistractor(curItem)) {
                hasDistractor = true;
                break;
            }
        }
        return hasDistractor;
    }

    /**
     * determines if the passed parameter is a distractor
     * <p>For ItemTextIfc objects that hold data for matching type questions, a distractor 
     * is a choice that has no valid matches (i.e. no correct answers).  This function returns
     * if this ItemTextIfc object has any correct answers
     * @param itemText
     * @return true if itemtext has no correct answers (a distrator) or false if itemtext has at least
     * one correct answer
     */
    public boolean isDistractor(ItemTextIfc itemText) {
        // look for items that do not have any correct answers
        boolean hasCorrectAnswer = false;
        List<AnswerIfc> answers = itemText.getAnswerArray();
        Iterator<AnswerIfc> answerIter = answers.iterator();
        while (answerIter.hasNext()) {
            AnswerIfc answer = answerIter.next();
            if (answer.getIsCorrect() != null && answer.getIsCorrect().booleanValue()) {
                hasCorrectAnswer = true;
                break;
            }
        }
        return !hasCorrectAnswer;
    }

    public List getUnSubmittedAssessmentGradingDataList(Long publishedAssessmentId, String agentIdString) {
        return PersistenceService.getInstance().getAssessmentGradingFacadeQueries()
                .getUnSubmittedAssessmentGradingDataList(publishedAssessmentId, agentIdString);
    }
}

/**
 * A EMI score
 * @author jsmith
 *
 */
class EMIScore implements Comparable<EMIScore> {
    long itemId = 0L;
    long itemTextId = 0L;
    long answerId = 0L;
    boolean correct = false;
    double score = 0.0;
    double effectiveScore = 0.0;

    /**
     * Create an EMI Score object
     * @param itemId
     * @param itemTextId
     * @param answerId
     * @param correct
     * @param score
     */
    public EMIScore(Long itemId, Long itemTextId, Long answerId, boolean correct, Double score) {
        this.itemId = itemId == null ? 0L : itemId.longValue();
        this.itemTextId = itemTextId == null ? 0L : itemTextId.longValue();
        this.answerId = answerId == null ? 0L : answerId.longValue();
        this.correct = correct;
        this.score = score == null ? 0L : score.doubleValue();
    }

    public int compareTo(EMIScore o) {
        //we want the correct higher scores first
        if (correct == o.correct) {
            int c = Double.compare(o.score, score);
            if (c == 0) {
                if (itemId != o.itemId) {
                    return (int) (itemId - o.itemId);
                }
                if (itemTextId != o.itemTextId) {
                    return (int) (itemTextId - o.itemTextId);
                }
                if (answerId != o.answerId) {
                    return (int) (answerId - o.answerId);
                }
                return hashCode() - o.hashCode();
            } else {
                return c;
            }
        } else {
            return correct ? -1 : 1;
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) itemId;
        result = prime * result + (int) itemTextId;
        result = prime * result + (int) answerId;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (this == obj)
            return true;
        if (getClass() != obj.getClass()) {
            return false;
        }
        EMIScore other = (EMIScore) obj;
        return (itemId == other.itemId && itemTextId == other.itemTextId && answerId == other.answerId);
    }

    @Override
    public String toString() {
        return itemId + ":" + itemTextId + ":" + answerId + "(" + correct + ":" + score + ":" + effectiveScore
                + ")";
    }
}