org.sakaiproject.contentreview.vericite.ContentReviewServiceVeriCite.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.contentreview.vericite.ContentReviewServiceVeriCite.java

Source

/**
 * Copyright (c) 2003 The Apereo 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://opensource.org/licenses/ecl2
 *
 * 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.contentreview.vericite;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.SortedSet;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.contentreview.dao.ContentReviewConstants;
import org.sakaiproject.contentreview.dao.ContentReviewItem;
import org.sakaiproject.contentreview.exception.QueueException;
import org.sakaiproject.contentreview.exception.ReportException;
import org.sakaiproject.contentreview.exception.SubmissionException;
import org.sakaiproject.contentreview.exception.TransientSubmissionException;
import org.sakaiproject.contentreview.service.ContentReviewQueueService;
import org.sakaiproject.contentreview.service.ContentReviewService;
import org.sakaiproject.contentreview.vericite.client.ApiClient;
import org.sakaiproject.contentreview.vericite.client.ApiException;
import org.sakaiproject.contentreview.vericite.client.api.DefaultApi;
import org.sakaiproject.contentreview.vericite.client.model.AssignmentData;
import org.sakaiproject.contentreview.vericite.client.model.ExternalContentData;
import org.sakaiproject.contentreview.vericite.client.model.ExternalContentUploadInfo;
import org.sakaiproject.contentreview.vericite.client.model.ReportMetaData;
import org.sakaiproject.contentreview.vericite.client.model.ReportScoreReponse;
import org.sakaiproject.contentreview.vericite.client.model.ReportURLLinkReponse;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.memory.api.SimpleConfiguration;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserNotDefinedException;

import lombok.Setter;

@Slf4j
public class ContentReviewServiceVeriCite implements ContentReviewService {

    @Setter
    private ServerConfigurationService serverConfigurationService;

    @Setter
    private UserDirectoryService userDirectoryService;

    @Setter
    private EntityManager entityManager;

    @Setter
    private SecurityService securityService;

    @Setter
    private SiteService siteService;

    @Setter
    private ContentReviewQueueService crqs;

    @Setter
    private ContentHostingService contentHostingService;

    private static final String PARAM_USER_ROLE_INSTRUCTOR = "Instructor";
    private static final String PARAM_USER_ROLE_LEARNER = "Learner";
    private static final String SERVICE_NAME = "VeriCite";
    private static final String VERICITE_API_VERSION = "v1";
    private static final int VERICITY_RETRY_TIME_MINS = 30;
    private static final int VERICITE_MAX_RETRY = 30;
    private static final int VERICITE_SERVICE_CALL_THROTTLE_MINS = 2;
    private static final String VERICITE_CACHE_PLACEHOLDER = "VERICITE_LAST_CHECKED";

    private String serviceUrl;
    private String consumer;
    private String consumerSecret;

    private MemoryService memoryService;
    //Caches requests for instructors so that we don't have to send a request for every student
    Cache userUrlCache, contentScoreCache, assignmentTitleCache;
    private static final int CACHE_EXPIRE_URLS_MINS = 20;

    private static final int CONTENT_SCORE_CACHE_MINS = 5;

    public void init() {
        serviceUrl = serverConfigurationService.getString("vericite.serviceUrl", "");
        consumer = serverConfigurationService.getString("vericite.consumer", "");
        consumerSecret = serverConfigurationService.getString("vericite.consumerSecret", "");
        userUrlCache = memoryService.createCache(
                "org.sakaiproject.contentreview.vericite.ContentReviewServiceVeriCite.userUrlCache",
                new SimpleConfiguration<>(10000, CACHE_EXPIRE_URLS_MINS * 60, -1));
        contentScoreCache = memoryService.createCache(
                "org.sakaiproject.contentreview.vericite.ContentReviewServiceVeriCite.contentScoreCache",
                new SimpleConfiguration<>(10000, CONTENT_SCORE_CACHE_MINS * 60, -1));
        assignmentTitleCache = memoryService.getCache(
                "org.sakaiproject.contentreview.vericite.ContentReviewServiceVeriCite.assignmentTitleCache");
    }

    public boolean allowResubmission() {
        return true;
    }

    public void checkForReports() {

    }

    public void createAssignment(final String contextId, final String assignmentRef, final Map opts)
            throws SubmissionException, TransientSubmissionException {
        new Thread() {
            public void run() {
                boolean isA2 = isA2(null, assignmentRef);
                String assignmentId = getAssignmentId(assignmentRef, isA2);
                Map<String, ContentResource> attachmentsMap = new HashMap<String, ContentResource>();
                if (assignmentId != null) {
                    AssignmentData assignmentData = new AssignmentData();
                    if (opts != null) {
                        if (opts.containsKey("title")) {
                            assignmentData.setAssignmentTitle(opts.get("title").toString());
                        } else if (!isA2) {
                            //we can find the title from the assignment ref for A1
                            String assignmentTitle = getAssignmentTitle(assignmentRef);
                            if (assignmentTitle != null) {
                                assignmentData.setAssignmentTitle(assignmentTitle);
                            }
                        }
                        if (opts.containsKey("instructions")) {
                            assignmentData.setAssignmentInstructions(opts.get("instructions").toString());
                        }
                        if (opts.containsKey("exclude_quoted")) {
                            assignmentData
                                    .setAssignmentExcludeQuotes("1".equals(opts.get("exclude_quoted").toString()));
                        }
                        if (opts.containsKey("exclude_self_plag")) {
                            assignmentData.setAssignmentExcludeSelfPlag(
                                    "1".equals(opts.get("exclude_self_plag").toString()));
                        }
                        if (opts.containsKey("store_inst_index")) {
                            assignmentData
                                    .setAssignmentStoreInIndex("1".equals(opts.get("store_inst_index").toString()));
                        }
                        if (opts.containsKey("dtdue")) {
                            SimpleDateFormat dform = ((SimpleDateFormat) DateFormat.getDateInstance());
                            dform.applyPattern("yyyy-MM-dd HH:mm:ss");
                            try {
                                Date dueDate = dform.parse(opts.get("dtdue").toString());
                                if (dueDate != null) {
                                    assignmentData.setAssignmentDueDate(dueDate.getTime());
                                }
                            } catch (ParseException e) {
                                log.error(e.getMessage(), e);
                            }
                        }
                        //Pass in 0 to delete a grade, otherwise, set the grade.
                        assignmentData.setAssignmentGrade(0);
                        if (opts.containsKey("points")) {
                            //points are stored as integers and multiplied by 100 (i.e. 5.5 = 550; 1 = 100, etc)
                            try {
                                Integer points = Integer.parseInt(opts.get("points").toString()) / 100;
                                assignmentData.setAssignmentGrade(points);
                            } catch (Exception e) {
                                log.error(e.getMessage(), e);
                            }
                        }
                        if (opts.containsKey("attachments") && opts.get("attachments") instanceof List) {
                            SecurityAdvisor yesMan = new SecurityAdvisor() {
                                public SecurityAdvice isAllowed(String arg0, String arg1, String arg2) {
                                    return SecurityAdvice.ALLOWED;
                                }
                            };
                            securityService.pushAdvisor(yesMan);
                            try {
                                List<ExternalContentData> attachments = new ArrayList<ExternalContentData>();
                                for (String refStr : (List<String>) opts.get("attachments")) {
                                    try {
                                        Reference ref = entityManager.newReference(refStr);
                                        ContentResource res = (ContentResource) ref.getEntity();
                                        if (res != null) {
                                            ExternalContentData attachment = new ExternalContentData();
                                            String fileName = res.getProperties()
                                                    .getProperty(ResourceProperties.PROP_DISPLAY_NAME);
                                            attachment.setFileName(FilenameUtils.getBaseName(fileName));
                                            attachment.setExternalContentID(getAssignmentAttachmentId(consumer,
                                                    contextId, assignmentId, res.getId()));
                                            attachment.setUploadContentLength((int) res.getContentLength());
                                            attachment.setUploadContentType(FilenameUtils.getExtension(fileName));
                                            attachments.add(attachment);
                                            attachmentsMap.put(attachment.getExternalContentID(), res);
                                        }
                                    } catch (Exception e) {
                                        log.error(e.getMessage(), e);
                                    }
                                }
                                if (attachments.size() > 0) {
                                    assignmentData.setAssignmentAttachmentExternalContent(attachments);
                                }
                            } catch (Exception e) {
                                log.error(e.getMessage(), e);
                            } finally {
                                securityService.popAdvisor(yesMan);
                            }
                        }
                    }
                    DefaultApi vericiteApi = getVeriCiteAPI();
                    try {
                        List<ExternalContentUploadInfo> uploadInfo = vericiteApi
                                .assignmentsContextIDAssignmentIDPost(contextId, assignmentId, consumer,
                                        consumerSecret, assignmentData);
                        //see if there are any attachment presigned urls to upload to
                        if (uploadInfo != null) {
                            //see if this attachment needs uploaded:
                            for (ExternalContentUploadInfo info : uploadInfo) {
                                if (attachmentsMap.containsKey(info.getExternalContentId())) {
                                    //upload this attachment
                                    ContentResource res = attachmentsMap.get(info.getExternalContentId());
                                    try {
                                        uploadExternalContent(info.getUrlPost(), res.getContent());
                                    } catch (ServerOverloadException e) {
                                        log.error(e.getMessage(), e);
                                    }
                                }
                            }
                        }
                    } catch (ApiException e) {
                        log.error(e.getMessage(), e);
                    }
                }
            }
        }.start();
    }

    public List<ContentReviewItem> getAllContentReviewItems(String siteId, String taskId)
            throws QueueException, SubmissionException, ReportException {
        return crqs.getContentReviewItems(getProviderId(), siteId, taskId);
    }

    public Map getAssignment(String arg0, String arg1) throws SubmissionException, TransientSubmissionException {
        return null;
    }

    public Date getDateQueued(String contextId) throws QueueException {
        return crqs.getDateQueued(getProviderId(), contextId);
    }

    public Date getDateSubmitted(String contextId) throws QueueException, SubmissionException {
        return crqs.getDateSubmitted(getProviderId(), contextId);
    }

    public String getIconUrlforScore(Long score) {
        String urlBase = "/library/content-review/";
        String suffix = ".png";

        if (score.compareTo(Long.valueOf(0)) < 0) {
            return urlBase + "greyflag" + suffix;
        } else if (score.equals(Long.valueOf(0))) {
            return urlBase + "blueflag" + suffix;
        } else if (score.compareTo(Long.valueOf(25)) < 0) {
            return urlBase + "greenflag" + suffix;
        } else if (score.compareTo(Long.valueOf(50)) < 0) {
            return urlBase + "yellowflag" + suffix;
        } else if (score.compareTo(Long.valueOf(75)) < 0) {
            return urlBase + "orangeflag" + suffix;
        } else {
            return urlBase + "redflag" + suffix;
        }
    }

    public String getLocalizedStatusMessage(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    public String getLocalizedStatusMessage(String arg0, String arg1) {
        // TODO Auto-generated method stub
        return null;
    }

    public String getLocalizedStatusMessage(String arg0, Locale arg1) {
        // TODO Auto-generated method stub
        return null;
    }

    public List<ContentReviewItem> getReportList(String siteId)
            throws QueueException, SubmissionException, ReportException {
        // TODO Auto-generated method stub
        return null;
    }

    public List<ContentReviewItem> getReportList(String siteId, String taskId)
            throws QueueException, SubmissionException, ReportException {
        return null;
    }

    public String getReviewReport(String contentId, String assignmentRef, String userId)
            throws QueueException, ReportException {
        return getAccessUrl(contentId, assignmentRef, userId, false);
    }

    public String getReviewReportInstructor(String contentId, String assignmentRef, String userId)
            throws QueueException, ReportException {
        /**
         * contentId: /attachment/04bad844-493c-45a1-95b4-af70129d54d1/Assignments/b9872422-fb24-4f85-abf5-2fe0e069b251/plag.docx
         */
        return getAccessUrl(contentId, assignmentRef, userId, true);
    }

    public String getReviewReportStudent(String contentId, String assignmentRef, String userId)
            throws QueueException, ReportException {
        return getAccessUrl(contentId, assignmentRef, userId, false);
    }

    private String getAccessUrl(String contentId, String assignmentRef, String userId, boolean instructor)
            throws QueueException, ReportException {
        //assignmentRef: /assignment/a/f7d8c921-7d5a-4116-8781-9b61a7c92c43/cbb993da-ea12-4e74-bab1-20d16185a655
        String context = getSiteIdFromConentId(contentId);
        if (context != null) {
            String externalContentId = getAttachmentId(contentId);
            String returnUrl = null;
            String assignmentId = getAssignmentId(assignmentRef, isA2(contentId, assignmentRef));
            String cacheKey = context + ":" + assignmentId + ":" + userId;
            //first check if cache already has the URL for this contentId and user
            if (userUrlCache.containsKey(cacheKey)) {
                Object cacheObj = userUrlCache.get(cacheKey);
                if (cacheObj != null && cacheObj instanceof Map) {
                    Map<String, Object[]> userUrlCacheObj = (Map<String, Object[]>) cacheObj;
                    if (userUrlCacheObj.containsKey(externalContentId)) {
                        //check if cache has expired:
                        Object[] cacheItem = userUrlCacheObj.get(externalContentId);
                        Calendar cal = Calendar.getInstance();
                        cal.setTime(new Date());
                        //subtract the exipre time (currently set to 20 while the plag token is set to 30, leaving 10 mins in worse case for instructor to use token)
                        cal.add(Calendar.MINUTE, CACHE_EXPIRE_URLS_MINS * -1);
                        if (((Date) cacheItem[1]).after(cal.getTime())) {
                            //token hasn't expired, use it
                            returnUrl = (String) cacheItem[0];
                        } else {
                            //token is expired, remove it
                            userUrlCacheObj.remove(externalContentId);
                            userUrlCache.put(cacheKey, userUrlCacheObj);
                        }
                    }
                }
            }

            if (StringUtils.isEmpty(returnUrl)) {
                //instructors get all URLs at once, so only check VC every 2 minutes to avoid multiple calls in the same thread:
                boolean skip = false;
                if (instructor && userUrlCache.containsKey(cacheKey)) {
                    Object cacheObj = userUrlCache.get(cacheKey);
                    if (cacheObj != null && cacheObj instanceof Map) {
                        Map<String, Object[]> userUrlCacheObj = (Map<String, Object[]>) cacheObj;
                        if (userUrlCacheObj.containsKey(VERICITE_CACHE_PLACEHOLDER)) {
                            Object[] cacheItem = userUrlCacheObj.get(VERICITE_CACHE_PLACEHOLDER);
                            Calendar cal = Calendar.getInstance();
                            cal.setTime(new Date());
                            //only check vericite every 2 mins to prevent subsequent calls from the same thread
                            cal.add(Calendar.MINUTE, VERICITE_SERVICE_CALL_THROTTLE_MINS * -1);
                            if (((Date) cacheItem[1]).after(cal.getTime())) {
                                //we just checked VC, skip asking again
                                skip = true;
                            }
                        }
                    }
                }
                if (!skip) {
                    //we couldn't find the URL in the cache, so look it up (if instructor, look up all URLs so reduce the number of calls to the API)
                    DefaultApi vericiteApi = getVeriCiteAPI();
                    String tokenUserRole = PARAM_USER_ROLE_LEARNER;
                    String externalContentIDFilter = null;
                    if (instructor) {
                        tokenUserRole = PARAM_USER_ROLE_INSTRUCTOR;
                        //keep track of last call to make sure we don't call VC too much
                        Object cacheObject = userUrlCache.get(cacheKey);
                        if (cacheObject == null) {
                            cacheObject = new HashMap<String, Object[]>();
                        }
                        ((Map<String, Object[]>) cacheObject).put(VERICITE_CACHE_PLACEHOLDER,
                                new Object[] { VERICITE_CACHE_PLACEHOLDER, new Date() });
                        userUrlCache.put(cacheKey, cacheObject);
                    } else {
                        //since students will only be able to see their own content, make sure to filter it:
                        externalContentIDFilter = externalContentId;
                    }
                    List<ReportURLLinkReponse> urls = null;
                    try {
                        urls = vericiteApi.reportsUrlsContextIDGet(context, assignmentId, consumer, consumerSecret,
                                userId, tokenUserRole, null, externalContentIDFilter);
                    } catch (ApiException e) {
                        log.error(e.getMessage(), e);
                    }
                    if (urls != null) {
                        for (ReportURLLinkReponse url : urls) {
                            if (externalContentId.equals(url.getExternalContentID())) {
                                //this is the current url requested
                                returnUrl = url.getUrl();
                            }
                            //store in cache for later
                            Object cacheObject = userUrlCache.get(cacheKey);
                            if (cacheObject == null) {
                                cacheObject = new HashMap<String, Object[]>();
                            }
                            ((Map<String, Object[]>) cacheObject).put(url.getExternalContentID(),
                                    new Object[] { url.getUrl(), new Date() });
                            userUrlCache.put(cacheKey, cacheObject);
                        }
                    }
                }
            }
            if (StringUtils.isNotEmpty(returnUrl)) {
                //we either found the url in the cache or from the API, return it
                return returnUrl;
            }
        }
        //shouldn't get here is all went well:
        throw new ReportException("Url was null or contentId wasn't correct: " + contentId);
    }

    public int getReviewScore(String contentId, String assignmentRef, String userId)
            throws QueueException, ReportException, Exception {
        /**
         * contentId: /attachment/04bad844-493c-45a1-95b4-af70129d54d1/Assignments/b9872422-fb24-4f85-abf5-2fe0e069b251/plag.docx
         * assignmentRef: /assignment/a/f7d8c921-7d5a-4116-8781-9b61a7c92c43/cbb993da-ea12-4e74-bab1-20d16185a655
         */
        String externalContentId = getAttachmentId(contentId);
        //first check if contentId already exists in cache:
        boolean isA2 = isA2(contentId, null);
        String context = getSiteIdFromConentId(contentId);
        Integer score = null;
        String assignment = getAssignmentId(assignmentRef, isA2);
        if (StringUtils.isNotEmpty(assignment)) {
            if (contentScoreCache.containsKey(assignment)) {
                Object cacheObj = contentScoreCache.get(assignment);
                if (cacheObj != null && cacheObj instanceof Map) {
                    Map<String, Map<String, Object[]>> contentScoreCacheObject = (Map<String, Map<String, Object[]>>) cacheObj;
                    if (contentScoreCacheObject.containsKey(userId)
                            && contentScoreCacheObject.get(userId).containsKey(externalContentId)) {
                        Object[] cacheItem = contentScoreCacheObject.get(userId).get(externalContentId);
                        Calendar cal = Calendar.getInstance();
                        cal.setTime(new Date());
                        //subtract the exipre time
                        cal.add(Calendar.MINUTE, CONTENT_SCORE_CACHE_MINS * -1);
                        if (((Date) cacheItem[1]).after(cal.getTime())) {
                            //token hasn't expired, use it
                            score = (Integer) cacheItem[0];
                        } else {
                            //token is expired, remove it
                            contentScoreCacheObject.remove(userId);
                            contentScoreCache.put(assignment, contentScoreCacheObject);
                        }
                    }
                }
            }
        }
        if (score == null) {
            //wasn't in cache
            //make sure we didn't just check VC:
            boolean skip = false;
            if (StringUtils.isNotEmpty(assignment) && contentScoreCache.containsKey(assignment)) {
                Object cacheObj = contentScoreCache.get(assignment);
                if (cacheObj != null && cacheObj instanceof Map) {
                    Map<String, Map<String, Object[]>> contentScoreCacheObject = (Map<String, Map<String, Object[]>>) cacheObj;
                    if (contentScoreCacheObject.containsKey(VERICITE_CACHE_PLACEHOLDER) && contentScoreCacheObject
                            .get(VERICITE_CACHE_PLACEHOLDER).containsKey(VERICITE_CACHE_PLACEHOLDER)) {
                        Object[] cacheItem = contentScoreCacheObject.get(VERICITE_CACHE_PLACEHOLDER)
                                .get(VERICITE_CACHE_PLACEHOLDER);
                        Calendar cal = Calendar.getInstance();
                        cal.setTime(new Date());
                        //only check vericite every 2 mins to prevent subsequent calls from the same thread
                        cal.add(Calendar.MINUTE, VERICITE_SERVICE_CALL_THROTTLE_MINS * -1);
                        if (((Date) cacheItem[1]).after(cal.getTime())) {
                            //we just checked VC, skip asking again
                            skip = true;
                        }
                    }
                }
            }
            //look up score in VC 
            if (context != null && !skip) {
                DefaultApi vericiteApi = getVeriCiteAPI();
                String externalContentID = null;
                if (assignmentRef == null) {
                    externalContentID = externalContentId;
                }
                List<ReportScoreReponse> scores = vericiteApi.reportsScoresContextIDGet(context, consumer,
                        consumerSecret, assignment, null, externalContentID);
                if (scores != null) {
                    for (ReportScoreReponse scoreResponse : scores) {
                        if (externalContentId.equals(scoreResponse.getExternalContentId())) {
                            score = scoreResponse.getScore();
                        }
                        //only cache the score if it is > 0
                        if (scoreResponse.getScore() != null && scoreResponse.getScore().intValue() >= 0) {
                            Object userCacheMap = contentScoreCache.get(scoreResponse.getAssignment());
                            if (userCacheMap == null) {
                                userCacheMap = new HashMap<String, Map<String, Object[]>>();
                            }
                            Map<String, Object[]> cacheMap = ((Map<String, Map<String, Object[]>>) userCacheMap)
                                    .get(scoreResponse.getUser());
                            if (cacheMap == null) {
                                cacheMap = new HashMap<String, Object[]>();
                            }
                            cacheMap.put(scoreResponse.getExternalContentId(),
                                    new Object[] { scoreResponse.getScore(), new Date() });
                            ((Map<String, Map<String, Object[]>>) userCacheMap).put(scoreResponse.getUser(),
                                    cacheMap);
                            contentScoreCache.put(scoreResponse.getAssignment(), userCacheMap);
                        }
                    }
                }
                //keep track of last call to make sure we don't call VC too much
                if (StringUtils.isNotEmpty(assignment)) {
                    Object userCacheMap = contentScoreCache.get(assignment);
                    if (userCacheMap == null) {
                        userCacheMap = new HashMap<String, Map<String, Object[]>>();
                    }
                    Map<String, Object[]> cacheMap = ((Map<String, Map<String, Object[]>>) userCacheMap)
                            .get(VERICITE_CACHE_PLACEHOLDER);
                    if (cacheMap == null) {
                        cacheMap = new HashMap<String, Object[]>();
                    }
                    cacheMap.put(VERICITE_CACHE_PLACEHOLDER, new Object[] { 0, new Date() });
                    ((Map<String, Map<String, Object[]>>) userCacheMap).put(VERICITE_CACHE_PLACEHOLDER, cacheMap);
                    contentScoreCache.put(assignment, userCacheMap);
                }
            }
        }

        Optional<ContentReviewItem> reviewItem = crqs.getQueuedItem(getProviderId(), contentId);
        if (score == null) {
            //nothing was found, throw exception for this contentId
            //remove from queue so that we can start again.
            if (needsRequeue(reviewItem)) {
                if (reviewItem.isPresent()) {
                    //in order to requeue, we need to delete the old queue:
                    crqs.removeFromQueue(getProviderId(), contentId);
                }
                throw new QueueException("No report was found for contentId: " + contentId);
            } else {
                return -1;
            }
        } else {
            //update the score in the db so admins can run reports:
            if (reviewItem.isPresent()) {
                if (reviewItem.get().getReviewScore() == null
                        || reviewItem.get().getReviewScore().intValue() != score.intValue()) {
                    //score has changed, update it and save it in the db
                    reviewItem.get().setReviewScore(score);
                    crqs.update(reviewItem.get());
                }
            }
            //score wasn't null and there should have only been one score, so just return that value
            return score;
        }
    }

    private boolean needsRequeue(Optional<ContentReviewItem> reviewItem) {
        boolean requeue = false;
        if (reviewItem.isPresent()) {
            //see whether the queue is old or reties failed:
            Date submitted = reviewItem.get().getDateSubmitted();
            if (submitted == null) {
                //check if there is another date
                submitted = reviewItem.get().getDateQueued();
            }
            if (submitted != null) {
                //see how long it has been since queue status
                Calendar cal = Calendar.getInstance();
                cal.add(Calendar.HOUR, -2);
                //if it has been over 3 hours, try again
                if (cal.getTime().after(submitted)) {
                    requeue = true;
                }
            } else {
                //something is weird here since dates are missing, just requeue:
                requeue = true;
            }
        } else {
            //there is no queue item, so create one!
            requeue = true;
        }
        return requeue;
    }

    public Long getReviewStatus(String contentId) throws QueueException {
        return crqs.getReviewStatus(getProviderId(), contentId);
    }

    public String getServiceName() {
        return SERVICE_NAME;
    }

    public boolean isAcceptableContent(ContentResource arg0) {
        return true;
    }

    public boolean isSiteAcceptable(Site arg0) {
        return true;
    }

    public void processQueue() {
        log.info("Processing VeriCite submission queue");
        int errors = 0;
        int success = 0;
        DefaultApi veriCiteApi = getVeriCiteAPI();
        Optional<ContentReviewItem> nextItem = null;
        while ((nextItem = crqs.getNextItemInQueueToSubmit(getProviderId())).isPresent()) {
            ContentReviewItem item = nextItem.get();
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.MINUTE, VERICITY_RETRY_TIME_MINS);
            if (item.getRetryCount() == null) {
                item.setRetryCount(Long.valueOf(0));
                item.setNextRetryTime(cal.getTime());
                crqs.update(item);
            } else if (item.getRetryCount().intValue() > VERICITE_MAX_RETRY) {
                item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_EXCEEDED_CODE);
                crqs.update(item);
                errors++;
                continue;
            } else {
                long l = item.getRetryCount().longValue();
                l++;
                item.setRetryCount(Long.valueOf(l));
                item.setNextRetryTime(cal.getTime());
                crqs.update(item);
            }
            String userFirstNameParam = null;
            String userLastNameParam = null;
            String userEmailParam = null;
            try {
                User user = userDirectoryService.getUser(item.getUserId());
                userFirstNameParam = user.getFirstName();
                userLastNameParam = user.getLastName();
                userEmailParam = user.getEmail();
            } catch (UserNotDefinedException e1) {
                //VC doesn't really require a user, so as long as the user ID exists then it will succeed
                log.error("User not found for item: " + item.getId() + ", user: " + item.getUserId(), e1);
            }
            //it doesn't matter, all users are learners in the Sakai Integration
            final String userRoleParam = PARAM_USER_ROLE_LEARNER;
            ReportMetaData reportMetaData = new ReportMetaData();

            //get assignment title
            String assignmentTitle = getAssignmentTitle(item.getTaskId());
            if (assignmentTitle != null) {
                reportMetaData.setAssignmentTitle(assignmentTitle);
            }
            //get site title
            try {
                Site site = siteService.getSite(item.getSiteId());
                if (site != null) {
                    reportMetaData.setContextTitle(site.getTitle());
                }
            } catch (Exception e) {
                //no worries, just log it
                log.error("Site not found for item: " + item.getId() + ", site: " + item.getSiteId(), e);
            }
            reportMetaData.setUserEmail(userEmailParam);
            reportMetaData.setUserFirstName(userFirstNameParam);
            reportMetaData.setUserLastName(userLastNameParam);
            reportMetaData.setUserRole(userRoleParam);
            ContentResource resource = null;
            try {
                resource = contentHostingService.getResource(item.getContentId());
            } catch (IdUnusedException e4) {
                log.error("IdUnusedException: no resource with id " + item.getContentId());
                item.setLastError("IdUnusedException: no resource with id " + item.getContentId());
                item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
                crqs.update(item);
                errors++;
                continue;
            } catch (PermissionException e) {
                log.error("PermissionException: no resource with id " + item.getContentId());
                item.setLastError("PermissionException: no resource with id " + item.getContentId());
                item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
                crqs.update(item);
                errors++;
                continue;
            } catch (TypeException e) {
                log.error("TypeException: no resource with id " + item.getContentId());
                item.setLastError("TypeException: no resource with id " + item.getContentId());
                item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
                crqs.update(item);
                errors++;
                continue;
            }
            List<ExternalContentData> externalContentDataList = new ArrayList<ExternalContentData>();
            String externalContentId = getAttachmentId(resource.getId());
            ExternalContentData externalContentData = new ExternalContentData();
            externalContentData.setExternalContentID(externalContentId);
            String fileName = resource.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME);
            externalContentData.setFileName(FilenameUtils.getBaseName(fileName));
            externalContentData.setUploadContentType(FilenameUtils.getExtension(fileName));
            externalContentData.setUploadContentLength((int) resource.getContentLength());
            externalContentDataList.add(externalContentData);
            reportMetaData.setExternalContentData(externalContentDataList);

            List<ExternalContentUploadInfo> uploadInfo = null;
            try {
                String assignmentParam = getAssignmentId(item.getTaskId(), isA2(item.getContentId(), null));
                uploadInfo = veriCiteApi.reportsSubmitRequestContextIDAssignmentIDUserIDPost(item.getSiteId(),
                        assignmentParam, item.getUserId(), consumer, consumerSecret, reportMetaData);
            } catch (ApiException e) {
                log.error(e.getMessage() + ", id: " + item.getContentId(), e);
                item.setLastError(e.getMessage());
                item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
                crqs.update(item);
                errors++;
                continue;
            }
            //see if there are any attachment presigned urls to upload to (if VC already has the attachment, this will be empty)
            if (uploadInfo != null) {
                //see if this attachment needs uploaded:
                for (ExternalContentUploadInfo info : uploadInfo) {
                    if (externalContentId.equals(info.getExternalContentId())) {
                        try {
                            uploadExternalContent(info.getUrlPost(), resource.getContent());
                        } catch (Exception e) {
                            log.warn("ServerOverloadException: " + item.getContentId(), e);
                            item.setLastError(e.getMessage());
                            item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
                            crqs.update(item);
                            errors++;
                            continue;
                        }
                    }
                }
            }
            //Success
            log.debug("Submission successful");
            item.setExternalId(externalContentId);
            item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE);
            item.setRetryCount(Long.valueOf(0));
            item.setLastError(null);
            item.setErrorCode(null);
            item.setDateSubmitted(new Date());
            item.setDateReportReceived(new Date());
            success++;
            crqs.update(item);
        }
        log.info(
                "Submission VeriCite queue run completed: " + success + " items submitted, " + errors + " errors.");
    }

    public void queueContent(String userId, String siteId, String assignmentReference,
            List<ContentResource> content) throws QueueException {
        crqs.queueContent(getProviderId(), userId, siteId, assignmentReference, content);
    }

    public void removeFromQueue(String contentId) {
        crqs.removeFromQueue(getProviderId(), contentId);
    }

    public void resetUserDetailsLockedItems(String arg0) {
        // TODO Auto-generated method stub

    }

    public String getReviewError(String contentId) {
        return null;
    }

    public static String getContent(HttpResponse response) throws IOException {
        BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
        String body = "";
        String content = "";

        while ((body = rd.readLine()) != null) {
            content += body + "\n";
        }
        return content.trim();
    }

    private String getAssignmentTitle(String assignmentRef) {
        if (assignmentTitleCache.containsKey(assignmentRef) && assignmentTitleCache.get(assignmentRef) != null) {
            return (String) assignmentTitleCache.get(assignmentRef);
        } else {
            String assignmentTitle = null;
            if (assignmentRef.startsWith("/assignment/")) {
                try {
                    Reference ref = entityManager.newReference(assignmentRef);
                    EntityProducer ep = ref.getEntityProducer();
                    Entity ent = ep.getEntity(ref);
                    if (ent != null) {
                        assignmentTitle = URLDecoder
                                .decode(ent.getClass().getMethod("getTitle").invoke(ent).toString(), "UTF-8");
                        assignmentTitleCache.put(assignmentRef, assignmentTitle);
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
            return assignmentTitle;
        }
    }

    private class FileSubmission {
        public String contentId;
        public byte[] data;
        public String fileName;
        public long contentLength = 0;

        public FileSubmission(String contentId, String fileName, byte[] data, long contentLength) {
            this.contentId = contentId;
            this.fileName = fileName;
            this.data = data;
            this.contentLength = contentLength;
        }
    }

    private boolean isA2(String contentId, String assignmentRef) {
        if (contentId != null && contentId.contains("/Assignment2/")) {
            return true;
        }
        if (assignmentRef != null && assignmentRef.startsWith("/asnn2contentreview/")) {
            return true;
        }
        return false;
    }

    private String getSiteIdFromConentId(String contentId) {
        //contentId: /attachment/04bad844-493c-45a1-95b4-af70129d54d1/Assignments/b9872422-fb24-4f85-abf5-2fe0e069b251/plag.docx
        if (contentId != null) {
            String[] split = contentId.split("/");
            if (split.length > 2) {
                return split[2];
            }
        }
        return null;
    }

    private String getAssignmentId(String assignmentRef, boolean isA2) {
        if (assignmentRef != null) {
            String[] split = assignmentRef.split("/");
            if (isA2) {
                if (split.length > 2) {
                    return split[2];
                }
            } else {
                if (split.length > 4) {
                    return split[4];
                }
            }
        }
        return null;
    }

    public boolean allowAllContent() {
        return true;
    }

    public Map<String, SortedSet<String>> getAcceptableExtensionsToMimeTypes() {
        return new HashMap<String, SortedSet<String>>();
    }

    public Map<String, SortedSet<String>> getAcceptableFileTypesToExtensions() {
        return new HashMap<String, SortedSet<String>>();
    }

    private DefaultApi getVeriCiteAPI() {
        ApiClient apiClient = new ApiClient();
        String apiUrl = serviceUrl;
        if (StringUtils.isEmpty(apiUrl) || !apiUrl.endsWith("/")) {
            apiUrl += "/";
        }
        apiUrl += VERICITE_API_VERSION;
        apiClient.setBasePath(apiUrl);
        apiClient.setConnectTimeout(30000); //30 sec timeout
        return new DefaultApi(apiClient);
    }

    private String getAssignmentAttachmentId(String consumer, String contextId, String assignmentId,
            String attachmentId) {
        return "/" + consumer + "/" + contextId + "/" + assignmentId + "/" + attachmentId;
    }

    private String getAttachmentId(String resourceid) {
        return "/" + consumer + resourceid;
    }

    private void uploadExternalContent(String urlString, byte[] data) {
        URL url = null;
        HttpURLConnection connection = null;
        DataOutputStream out = null;
        try {
            url = new URL(urlString);

            connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setRequestMethod("PUT");
            out = new DataOutputStream(connection.getOutputStream());
            out.write(data);
            out.close();
            int responseCode = connection.getResponseCode();
            if (responseCode < 200 || responseCode >= 300) {
                log.error("VeriCite upload content failed with code: " + responseCode);
            }
        } catch (MalformedURLException e) {
            log.error(e.getMessage(), e);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
        }

    }

    public MemoryService getMemoryService() {
        return memoryService;
    }

    public void setMemoryService(MemoryService memoryService) {
        this.memoryService = memoryService;
    }
}