tds.student.services.ItemScoringService.java Source code

Java tutorial

Introduction

Here is the source code for tds.student.services.ItemScoringService.java

Source

/*******************************************************************************
 * Educational Online Test Delivery System Copyright (c) 2014 American
 * Institutes for Research
 * 
 * Distributed under the AIR Open Source License, Version 1.0 See accompanying
 * file AIR-License-1_0.txt or at http://www.smarterapp.org/documents/
 * American_Institutes_for_Research_Open_Source_Software_License.pdf
 ******************************************************************************/
package tds.student.services;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import javax.xml.bind.JAXBException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import AIR.Common.TDSLogger.ITDSLogger;
import AIR.Common.Web.EncryptionHelper;
import AIR.Common.Web.WebValueCollection;
import AIR.Common.Web.WebValueCollectionCorrect;
import TDS.Shared.Data.ReturnStatus;
import TDS.Shared.Exceptions.ReturnStatusException;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;

import tds.itemrenderer.data.AccLookup;
import tds.itemrenderer.data.IITSDocument;
import tds.itemrenderer.data.ITSMachineRubric;
import tds.itemrenderer.data.ITSMachineRubric.ITSMachineRubricType;
import tds.itemrenderer.processing.ITSDocumentHelper;
import tds.itemscoringengine.ItemScoreInfo;
import tds.itemscoringengine.IItemScorerManager;
import tds.itemscoringengine.ItemScore;
import tds.itemscoringengine.ResponseInfo;
import tds.itemscoringengine.RubricContentSource;
import tds.itemscoringengine.RubricContentType;
import tds.itemscoringengine.ScoreRationale;
import tds.itemscoringengine.ScorerInfo;
import tds.itemscoringengine.ScoringStatus;
import tds.itemscoringengine.WebProxyItemScorerCallback;
import tds.student.configuration.ItemScoringSettings;
import tds.student.services.abstractions.IContentService;
import tds.student.services.abstractions.IItemScoringService;
import tds.student.sql.abstractions.IConfigRepository;
import tds.student.sql.abstractions.IResponseRepository;
import tds.student.sql.abstractions.IScoringRepository;
import tds.student.sql.data.IItemResponseScorable;
import tds.student.sql.data.IItemResponseUpdate;
import tds.student.sql.data.ItemResponseUpdate;
import tds.student.sql.data.ItemResponseUpdateStatus;
import tds.student.sql.data.ItemScoringConfig;
import tds.student.sql.data.OpportunityInstance;
import tds.student.sql.singletons.ClientSingleton;
import tds.student.tdslogger.TDSLogger;

@Service("legacyItemScoringService")
@Scope("prototype")
public class ItemScoringService implements IItemScoringService {

    private static final Logger _logger = LoggerFactory.getLogger(ItemScoringService.class);

    @Autowired
    private IResponseRepository _responseRepository;
    @Autowired
    private IConfigRepository _configRepository;
    @Autowired
    private IScoringRepository _scoringRepository;
    @Autowired
    private IContentService _contentService;
    @Autowired
    private ItemScoringSettings _itemScoringSettings;
    @Autowired
    private IItemScorerManager _itemScorer;
    @Autowired
    private ITDSLogger _tdsLogger;

    private String getItemID(IItemResponseScorable responseScorable) {
        return String.format("I-%s-%s", responseScorable.getBankKey(), responseScorable.getItemKey());
    }

    /**
     * Get the item scoring config.
     */
    private ItemScoringConfig getItemScoringConfig(String format, String testID) throws ReturnStatusException {
        Iterable<ItemScoringConfig> itemScoringConfigs = _configRepository.getItemScoringConfigs();
        Iterator<ItemScoringConfig> configsIterator = itemScoringConfigs.iterator();
        ItemScoringConfig selected = null;

        while (configsIterator.hasNext()) {
            ItemScoringConfig c = configsIterator.next();
            // match format and context
            if ((c.getItemType().equalsIgnoreCase(format) || c.getItemType().equals("*"))
                    && (c.getContext().equalsIgnoreCase(testID) || c.getContext().equals("*"))) {
                // now find the highest priority config
                if (selected == null || c.getPriority() > selected.getPriority())
                    selected = c;
            }
        }
        return selected;
    }

    // function for logging item scorer errors
    private ItemScore createEmptyScore(ScoringStatus scoreStatus, final String message) {
        return new ItemScore(-1, -1, scoreStatus, null, new ScoreRationale() {
            {
                setMsg(message);
            }
        }, null, null);
    }

    /**
     * Check a response to see if it is scoreable. If it is not scoreable then you
     * will get back a Score object with the reason why. Otherwise if you get back
     * NULL then there are no problems and you can proceed with scoring the
     * response.
     */
    @Override
    public ItemScore checkScoreability(IItemResponseScorable responseScorable, IITSDocument itsDoc)
            throws ReturnStatusException {
        // check if item scoring is disabled
        if (!_itemScoringSettings.getEnabled()) {
            return createEmptyScore(ScoringStatus.NotScored, "Item scoring setting is disabled.");
        }

        if (responseScorable == null) {
            return createEmptyScore(ScoringStatus.NotScored, "Could not find scoreable response.");
        }

        if (responseScorable.getValue() == null) {
            return createEmptyScore(ScoringStatus.NotScored, "There was nothing to score.");
        }

        // check if doc exists
        if (itsDoc == null) {
            return createEmptyScore(ScoringStatus.ScoringError, "ITS document was not found.");
        }

        String itemFormat = itsDoc.getFormat();

        // get the scorer information for this item type
        ScorerInfo scorerInfo = _itemScorer.GetScorerInfo(itemFormat);

        // if there is no scorer info then this item type is not registered to a
        // scorer and cannot be scored
        if (scorerInfo == null) {
            return createEmptyScore(ScoringStatus.NotScored, "TDS does not score " + itemFormat);
        }

        // check if we should parse the xml for the rubric
        if (scorerInfo.getRubricContentSource() != RubricContentSource.None) {
            ITSMachineRubric machineRubric = _contentService.parseMachineRubric(itsDoc,
                    responseScorable.getLanguage(), scorerInfo.getRubricContentSource());

            // make sure this item has a machine rubric
            if (machineRubric == null) {
                return createEmptyScore(ScoringStatus.ScoringError, "Machine rubric was not found.");
            }

            // check if rubric has any data
            if (!machineRubric.getIsValid()) {
                return createEmptyScore(ScoringStatus.ScoringError, "Machine rubric was empty.");
            }
        }

        // check if item scoring config is enabled for this format
        ItemScoringConfig itemScoringConfig = getItemScoringConfig(itemFormat, responseScorable.getTestID());

        // check if item scoring config exists (there should always be a default
        // configured)
        if (itemScoringConfig == null) {
            return createEmptyScore(ScoringStatus.ScoringError, "Item scoring config was not found.");
        }

        // check if the item format is disabled
        if (!itemScoringConfig.isEnabled()) {
            return createEmptyScore(ScoringStatus.NotScored,
                    "Item scoring config for this item format is disabled.");
        }

        return null;
    }

    private String getDimensionsXmlForSP(ItemScore score) throws ReturnStatusException {
        // TODO Shiva: Why are the rationales being set to null ?
        ItemScoreInfo scoreInfo = score.getScoreInfo();
        scoreInfo.setRationale(null);

        if (scoreInfo.getSubScores() != null) {
            for (ItemScoreInfo subScore : scoreInfo.getSubScores()) {
                subScore.setRationale(null);
            }
        }

        String xml;
        try {
            xml = scoreInfo.toXmlString();
            // TODO Shiva: hack!!! I am not sure at this late stage how the AA will
            // react to the xml string. so i am going to take it out.
            xml = StringUtils.replace(xml, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "");
        } catch (JAXBException e) {
            _logger.error(e.getMessage());
            throw new ReturnStatusException("Could not parse scoreinfo xml");
        }
        return xml;
    }

    @Override
    public boolean updateItemScore(UUID oppKey, IItemResponseScorable response, ItemScore score)
            throws ReturnStatusException {
        ScoreRationale scoreRationale = score.getScoreInfo().getRationale(); // get
                                                                             // this
                                                                             // here
                                                                             // before it is
                                                                             // removed
        String scoreDimensions = getDimensionsXmlForSP(score);

        ReturnStatus updateStatus = _scoringRepository.updateItemScore(oppKey, response,
                score.getScoreInfo().getPoints(), score.getScoreInfo().getStatus().toString(),
                scoreRationale.getMsg(), scoreDimensions);

        return (updateStatus.getStatus().equals("updated"));

    }

    /**
     * Update a response for an instance of a test opportunity.
     * 
     * @throws ReturnStatusException
     */
    protected ItemResponseUpdateStatus updateResponse(OpportunityInstance oppInstance,
            IItemResponseUpdate responseUpdated, ItemScore score, Float itemDuration) throws ReturnStatusException {
        StopWatch dbTimer = new StopWatch();
        dbTimer.start();

        ItemScoreInfo scoreInfoObj = score.getScoreInfo();
        ScoreRationale scoreRationaleObj = score.getScoreInfo().getRationale();
        ReturnStatus updateStatus = _responseRepository.updateScoredResponse(oppInstance, responseUpdated,
                scoreInfoObj.getPoints(), scoreInfoObj.getStatus().toString(), scoreRationaleObj.getMsg(),
                score.getScoreLatency(), itemDuration);

        dbTimer.stop();

        if (!updateStatus.getStatus().equals("updated") && !updateStatus.getStatus().equals("warning")) {
            throw new ReturnStatusException(updateStatus);
        }

        return (new ItemResponseUpdateStatus(responseUpdated.getPosition(), updateStatus.getStatus(),
                updateStatus.getReason(), dbTimer.getTime()));
    }

    @Override
    public List<ItemResponseUpdateStatus> updateResponses(OpportunityInstance oppInstance,
            List<ItemResponseUpdate> responsesUpdated, Float pageDuration) throws ReturnStatusException {
        List<ItemResponseUpdateStatus> responseResults = new ArrayList<ItemResponseUpdateStatus>();

        for (ItemResponseUpdate responseUpdate : responsesUpdated) {
            ItemResponseUpdateStatus updateStatus;

            // get its doc
            IITSDocument itsDoc = _contentService.getContent(responseUpdate.getFilePath(), AccLookup.getNone());

            // check if loaded document
            if (itsDoc == null) {
                throw new ReturnStatusException(
                        String.format("When updating item id '%s' could not load the file '%s'.",
                                responseUpdate.getItemID(), responseUpdate.getFilePath()));
            }

            // check for any score errors
            ItemScore score = checkScoreability(responseUpdate, itsDoc);

            // if there was a score returned from score errors check then there was a
            // problem
            if (score != null) {
                // save response with error
                updateStatus = updateResponse(oppInstance, responseUpdate, score, pageDuration);
            } else {
                // for asynchronous we need to save the score first indicating it
                // is machine scorable and then submit to the scoring web site
                if (isScoringAsynchronous(itsDoc)) {
                    score = new ItemScore(-1, -1, ScoringStatus.WaitingForMachineScore, null, new ScoreRationale() {
                        {
                            setMsg("Waiting for machine score.");
                        }
                    }, new ArrayList<ItemScoreInfo>(), null);
                    updateStatus = updateResponse(oppInstance, responseUpdate, score, pageDuration);

                    // TODO: if score returned here ends up being
                    // ScoringStatus.ScoringError should we save this?
                    score = scoreResponse(oppInstance.getKey(), responseUpdate, itsDoc);
                }
                // for synchronous we need to score first and then save
                else {
                    score = scoreResponse(oppInstance.getKey(), responseUpdate, itsDoc);
                    updateStatus = updateResponse(oppInstance, responseUpdate, score, pageDuration);
                }
            }

            responseResults.add(updateStatus);
        }

        return responseResults;
    }

    /**
     * Score a response for an instance of a test opportunity.
     * 
     * @throws ReturnStatusException
     */
    private ItemScore scoreResponse(UUID oppKey, IItemResponseScorable responseScorable, IITSDocument itsDoc)
            throws ReturnStatusException {
        StopWatch stopwatch = new StopWatch();
        stopwatch.start();

        ItemScore score = scoreItem(oppKey, responseScorable, itsDoc);

        // if there is no score then create a not scored
        if (score == null) {
            score = new ItemScore(-1, -1, ScoringStatus.NotScored, null, null, null, null);
        }

        stopwatch.stop();
        score.setScoreLatency(stopwatch.getTime());

        return score;
    }

    /**
     * Get the server url for proxy server.
     * 
     * @throws ReturnStatusException
     */
    private URL getServerUri(String format, String testID) throws ReturnStatusException {
        String serverUrl = null;

        // TODO shiva: this does not make sense? should we not look for in the
        // itemscoring config table first for
        // a more specific url before looking in a generic url? but that is how .NEt
        // has been coded up.
        // i am instead going to code it up differently.
        // if there is no url from settings then try and get from configs
        // get server url from item scoring config SP
        ItemScoringConfig itemScoringConfig = getItemScoringConfig(format, testID);

        if (itemScoringConfig != null && itemScoringConfig.getServerUrl() != null) {
            // use config url
            serverUrl = itemScoringConfig.getServerUrl();
        }

        // check if multiple server urls was provided
        if (!StringUtils.isEmpty(serverUrl) && serverUrl.indexOf("|") > 0) {
            String[] servers = serverUrl.split("|");
            int serverIndex = (new Random()).nextInt(servers.length);
            serverUrl = servers[serverIndex];
        }

        // use the generic server.
        if (serverUrl == null)
            serverUrl = _itemScoringSettings.getServerUrl();

        URL uri;
        try {
            uri = new URL(serverUrl);
        } catch (MalformedURLException e) {
            _logger.error(e.getMessage());
            throw new ReturnStatusException("Malformed scoring server url.");
        }
        return uri;
    }

    private URL getCallbackUri() throws ReturnStatusException {
        String callbackUrl = _itemScoringSettings.getCallbackUrl();
        URL uri;
        try {
            uri = new URL(callbackUrl);
        } catch (MalformedURLException e) {
            _logger.error(e.getMessage());
            throw new ReturnStatusException("Malformed callback url.");
        }
        return uri;
    }

    // function for logging item scorer errors and use TDSLogger
    private void log(IItemResponseScorable responseScorable, String message, String methodName, Exception ex) {
        String error = String.format("ITEM SCORER (%s): %s", responseScorable.getItemID(), message);
        _tdsLogger.applicationError(error, methodName, null, ex);

    };

    @Override
    public ItemScore scoreItem(UUID oppKey, IItemResponseScorable responseScorable, IITSDocument itsDoc)
            throws ReturnStatusException {
        final String itemID = getItemID(responseScorable);
        String itemFormat = itsDoc.getFormat();

        // get the scorer information for this item type
        ScorerInfo scorerInfo = _itemScorer.GetScorerInfo(itemFormat);

        ITSMachineRubric machineRubric = new ITSMachineRubric(ITSMachineRubricType.Text, null);
        RubricContentType rubricContentType = RubricContentType.ContentString;

        if (scorerInfo.getRubricContentSource() != RubricContentSource.None) {
            // get items rubric
            machineRubric = _contentService.parseMachineRubric(itsDoc, responseScorable.getLanguage(),
                    scorerInfo.getRubricContentSource());
            if (machineRubric == null)
                return null;

            // create response info to pass to item scorer
            if (machineRubric.getType().equals(ITSMachineRubricType.Uri))
                rubricContentType = RubricContentType.Uri;
            else
                rubricContentType = RubricContentType.ContentString;

            // if this is true then load the rubric manually for the scoring engine
            if (rubricContentType == RubricContentType.Uri && _itemScoringSettings.getAlwaysLoadRubric()) {
                rubricContentType = RubricContentType.ContentString;

                // read text from stream
                try {
                    InputStream stream = ITSDocumentHelper.getStream(machineRubric.getData());
                    BufferedReader streamReader = new BufferedReader(new InputStreamReader(stream));
                    try {
                        StringBuilder sb = new StringBuilder();
                        String line;

                        line = streamReader.readLine();

                        while (line != null) {
                            sb.append(line);
                            sb.append(System.lineSeparator());
                            line = streamReader.readLine();
                        }
                        machineRubric.setData(sb.toString());
                    } finally {
                        try {
                            streamReader.close();
                        } catch (Exception e) {
                        }
                    }
                } catch (IOException e) {
                    _logger.error(e.getMessage(), e);
                    throw new ReturnStatusException("Failed to read rubric.");
                }
            }
        }

        // create rubric object
        Object rubricContent;
        if (rubricContentType == RubricContentType.Uri) {
            rubricContent = machineRubric.createUri();
        } else {
            rubricContent = machineRubric.getData(); // xml
        }

        ResponseInfo responseInfo = new ResponseInfo(itemFormat, itemID, responseScorable.getValue(), rubricContent,
                rubricContentType, null, true);

        // perform sync item scoring
        if (!isScoringAsynchronous(itsDoc)) {
            try {
                return _itemScorer.ScoreItem(responseInfo, null);
            } catch (final Exception ex) {
                return new ItemScore(-1, -1, ScoringStatus.ScoringError, null, new ScoreRationale() {
                    {
                        setMsg("Exception scoring item " + itemID + ": " + ex);
                    }
                }, null, null);
            }
        }

        // create callback token if there is a callback url
        // TODO: Add warning at app startup if ItemScoringCallbackUrl is missing
        if (!StringUtils.isEmpty(_itemScoringSettings.getCallbackUrl())) {
            // Create the token
            WebValueCollectionCorrect tokenData = new WebValueCollectionCorrect();
            tokenData.put("oppKey", oppKey);
            tokenData.put("testKey", responseScorable.getTestKey());
            tokenData.put("testID", responseScorable.getTestID());
            tokenData.put("language", responseScorable.getLanguage());
            tokenData.put("position", responseScorable.getPosition());
            tokenData.put("itsBank", responseScorable.getBankKey());
            tokenData.put("itsItem", responseScorable.getItemKey());
            tokenData.put("segmentID", responseScorable.getSegmentID());
            tokenData.put("sequence", responseScorable.getSequence());
            tokenData.put("scoremark", responseScorable.getScoreMark());

            // encrypt token (do not url encode)
            String encryptedToken = EncryptionHelper.EncryptToBase64(tokenData.toString(false));

            // save token
            responseInfo.setContextToken(encryptedToken);
        }

        ItemScore scoreItem = null;

        // perform scoring
        String message = null;
        try {
            WebProxyItemScorerCallback webProxyCallback = null;

            // check if there is a URL which will make this call asynchronous
            URL serverUri = getServerUri(itemFormat, responseScorable.getTestID());
            URL callbackUri = getCallbackUri();

            if (serverUri != null && callbackUri != null) {
                webProxyCallback = new WebProxyItemScorerCallback(serverUri.toString(), callbackUri.toString());
            }

            // call web proxy scorer
            scoreItem = _itemScorer.ScoreItem(responseInfo, webProxyCallback);

            // validate results
            if (scoreItem == null) {
                log(responseScorable, "Web proxy returned NULL score.", "scoreItem", null);
            } else if (scoreItem.getScoreInfo().getStatus() == ScoringStatus.ScoringError) {
                message = String.format("Web proxy returned a scoring error status: '%s'.",
                        (scoreItem.getScoreInfo().getRationale() != null ? scoreItem.getScoreInfo().getRationale()
                                : ""));
                log(responseScorable, message, "scoreItem", null);
            } else if (webProxyCallback != null
                    && scoreItem.getScoreInfo().getStatus() != ScoringStatus.WaitingForMachineScore) {
                message = String.format(
                        "Web proxy is in asynchronous mode and returned a score status of %s. It should return %s.",
                        scoreItem.getScoreInfo().getStatus().toString(),
                        ScoringStatus.WaitingForMachineScore.toString());
                log(responseScorable, message, "scoreItem", null);
            } else if (webProxyCallback == null
                    && scoreItem.getScoreInfo().getStatus() == ScoringStatus.WaitingForMachineScore) {
                message = String.format("Web proxy is in synchronous mode but returned incorrect status of %s.",
                        scoreItem.getScoreInfo().getStatus().toString());
                log(responseScorable, message, "scoreItem", null);
            }
        } catch (Exception ex) {
            message = String.format("EXCEPTION = '%s'.", ex.getMessage());
            log(responseScorable, message, "scoreItem", ex);
        }

        return scoreItem;
    }

    /**
     * Is the scoring for this response asynchronous?
     */
    private boolean isScoringAsynchronous(IITSDocument itsDoc) {
        if (itsDoc != null && !StringUtils.isEmpty(itsDoc.getFormat())) {
            ScorerInfo scorerInfo = _itemScorer.GetScorerInfo(itsDoc.getFormat());

            // check if scorer exists - not all item types are scored in TDS
            if (scorerInfo != null) {
                return scorerInfo.isSupportsAsyncMode();
            }
        }

        return false;
    }

}