All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sakaiproject.contentreview.turnitin.TurnitinReviewServiceImpl Maven / Gradle / Ivy

There is a newer version: 23.3
Show newest version
/**
 * 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.turnitin;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import org.sakaiproject.api.common.edu.person.SakaiPerson;
import org.sakaiproject.api.common.edu.person.SakaiPersonManager;
import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.contentreview.advisors.ContentReviewSiteAdvisor;
import org.sakaiproject.contentreview.dao.ContentReviewConstants;
import org.sakaiproject.contentreview.dao.ContentReviewItem;
import org.sakaiproject.contentreview.exception.ContentReviewProviderException;
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.BaseContentReviewService;
import org.sakaiproject.contentreview.turnitin.util.TurnitinAPIUtil;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entitybroker.EntityReference;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.grading.api.Assignment;
import org.sakaiproject.grading.api.GradingService;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.tool.api.ToolManager;
import org.sakaiproject.user.api.PreferencesService;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.util.ResourceLoader;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TurnitinReviewServiceImpl extends BaseContentReviewService {

	public static final String TURNITIN_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

	private static final String SERVICE_NAME = "Turnitin";

	// Site property to enable or disable use of Turnitin for the site
	private static final String TURNITIN_SITE_PROPERTY = "turnitin";

	final static long LOCK_PERIOD = 12000000;

	private boolean studentAccountNotified = true;

	private int sendSubmissionNotification = 0;

	private Long maxRetry = null;

	// note that the assignment id actually has to be unique globally so use
	// this as a prefix
	// eg. assignid = defaultAssignId + siteId
	private String defaultAssignId = null;

	private String defaultClassPassword = null;

	private List enabledSiteTypes;

	// Define Turnitin's acceptable file extensions and MIME types, order of these arrays DOES matter
	private final String[] DEFAULT_ACCEPTABLE_FILE_EXTENSIONS = new String[] {
		".doc",
		".docx",
		".xls",
		".xls",
		".xls",
		".xls",
		".xlsx",
		".ppt",
		".ppt",
		".ppt",
		".ppt",
		".pptx",
		".pps",
		".pps",
		".ppsx",
		".pdf",
		".ps",
		".eps",
		".txt",
		".html",
		".htm",
		".wpd",
		".wpd",
		".odt",
		".rtf",
		".rtf",
		".rtf",
		".rtf",
		".hwp"
	};
	private final String[] DEFAULT_ACCEPTABLE_MIME_TYPES = new String[] {
		"application/msword",
		"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
		"application/excel",
		"application/vnd.ms-excel",
		"application/x-excel",
		"application/x-msexcel",
		"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		"application/mspowerpoint",
		"application/powerpoint",
		"application/vnd.ms-powerpoint",
		"application/x-mspowerpoint",
		"application/vnd.openxmlformats-officedocument.presentationml.presentation",
		"application/mspowerpoint",
		"application/vnd.ms-powerpoint",
		"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
		"application/pdf",
		"application/postscript",
		"application/postscript",
		"text/plain",
		"text/html",
		"text/html",
		"application/wordperfect",
		"application/x-wpwin",
		"application/vnd.oasis.opendocument.text",
		"text/rtf",
		"application/rtf",
		"application/x-rtf",
		"text/richtext",
		"application/x-hwp"
	};

	// Sakai.properties overriding the arrays above
	private final String PROP_ACCEPT_ALL_FILES = "turnitin.accept.all.files";

	private final String PROP_ACCEPTABLE_FILE_EXTENSIONS = "turnitin.acceptable.file.extensions";
	private final String PROP_ACCEPTABLE_MIME_TYPES = "turnitin.acceptable.mime.types";

	// A list of the displayable file types (ie. "Microsoft Word", "WordPerfect document", "Postscript", etc.)
	private final String PROP_ACCEPTABLE_FILE_TYPES = "turnitin.acceptable.file.types";

	private final String KEY_FILE_TYPE_PREFIX = "file.type";

  // Sakai Roles
	private final String INST_ROLE = "section.role.instructor";
	private final String TA_ROLE = "section.role.ta";
	private final String STUDENT_ROLE = "section.role.student";

	/**
	 * If set to true in properties, will result in 3 random digits being
	 * appended to the email name. In other words, [email protected] will
	 * become something like [email protected]
	 */
	private boolean spoilEmailAddresses = false;

	/** Prefer system profile email addresses */
	private boolean preferSystemProfileEmail = true;

	/** Use guest account eids as email addresses */
	private boolean preferGuestEidEmail = true;

	/** Enroll a Sakai Teaching Assistant as a Turnitin Instructor */
	private boolean enrollAssistantsAsInstructors = false;

	@Setter
	private TurnitinAccountConnection turnitinConn;

	@Setter
	private ContentHostingService contentHostingService;

	@Setter
	private SakaiPersonManager sakaiPersonManager;

	@Setter
	private UserDirectoryService userDirectoryService;

	@Setter
	private SiteService siteService;

	@Setter
	private PreferencesService preferencesService;

	@Setter
	private TurnitinContentValidator turnitinContentValidator;

	@Setter
	private GradingService gradingService;

	@Setter
	private SecurityService securityService;

	@Setter
	private SessionManager sessionManager;

	@Setter
	private ContentReviewSiteAdvisor siteAdvisor;
	
	@Setter
	private ToolManager toolManager;

	public void init() {

		studentAccountNotified = turnitinConn.isStudentAccountNotified();
		sendSubmissionNotification = turnitinConn.getSendSubmissionNotification();
		maxRetry = turnitinConn.getMaxRetry();
		defaultAssignId = turnitinConn.getDefaultAssignId();
		defaultClassPassword = turnitinConn.getDefaultClassPassword();

		spoilEmailAddresses = serverConfigurationService.getBoolean("turnitin.spoilEmailAddresses", false);
		preferSystemProfileEmail = serverConfigurationService.getBoolean("turnitin.preferSystemProfileEmail", true);
		preferGuestEidEmail = serverConfigurationService.getBoolean("turnitin.preferGuestEidEmail", true);
		enrollAssistantsAsInstructors = serverConfigurationService.getBoolean("turnitin.enroll_assistants_as_instructors", false);

		enabledSiteTypes = Arrays
				.asList(ArrayUtils.nullToEmpty(serverConfigurationService.getStrings("turnitin.sitetypes")));

		log.info("init(): spoilEmailAddresses=" + spoilEmailAddresses + " preferSystemProfileEmail="
				+ preferSystemProfileEmail + " preferGuestEidEmail=" + preferGuestEidEmail);

		if (enabledSiteTypes != null && !enabledSiteTypes.isEmpty()) {
			log.info("Turnitin is enabled for site types: " + StringUtils.join(enabledSiteTypes, ","));
		}

		if (!turnitinConn.isUseSourceParameter()) {
			if (serverConfigurationService.getBoolean("turnitin.updateAssingments", false))
				doAssignments();
		}
	}

	@Override
	public boolean allowSubmissionsOnBehalf() {
		return true;
	}

	@Override
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/**
	 * Allow Turnitin for this site?
	 */
	@Override
	public boolean isSiteAcceptable(Site s) {

		if (s == null) {
			return false;
		}

		log.debug("isSiteAcceptable: " + s.getId() + " / " + s.getTitle());

		// Delegated to another bean
		if (siteAdvisor != null) {
			return siteAdvisor.siteCanUseReviewService(s);
		}

		// Check site property
		ResourceProperties properties = s.getProperties();

		String prop = (String) properties.get(TURNITIN_SITE_PROPERTY);
		if (prop != null) {
			log.debug("Using site property: " + prop);
			return Boolean.parseBoolean(prop);
		}

		// Check list of allowed site types, if defined
		if (enabledSiteTypes != null && !enabledSiteTypes.isEmpty()) {
			log.debug("Using site type: " + s.getType());
			return enabledSiteTypes.contains(s.getType());
		}

		// No property set, no restriction on site types, so allow
		return true;
	}

	public String getIconCssClassforScore(int score, String contentId) {
		if (score == 0) {
			return "contentReviewIconThreshold-5";
		} else if (score < 25) {
			return "contentReviewIconThreshold-4";
		} else if (score < 50) {
			return "contentReviewIconThreshold-3";
		} else if (score < 75) {
			return "contentReviewIconThreshold-2";
		} else {
			return "contentReviewIconThreshold-1";
		}
	}

	/**
	 * This uses the default Instructor information or current user.
	 *
	 * @see org.sakaiproject.contentreview.impl.BaseReviewServiceImpl#getReviewReportInstructor(java.lang.String)
	 */
	public String getReviewReportInstructor(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {

		Optional matchingItem = crqs.getQueuedItem(getProviderId(), contentId);

		if (!matchingItem.isPresent()) {
			log.debug("Content " + contentId + " has not been queued previously");
			throw new QueueException("Content " + contentId + " has not been queued previously");
		}

		// check that the report is available
		// TODO if the database record does not show report available check with
		// turnitin (maybe)

		ContentReviewItem item = matchingItem.get();

		if (item.getStatus().compareTo(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE) != 0) {
			log.debug("Report not available: " + item.getStatus());
			throw new ReportException("Report not available: " + item.getStatus());
		}

		// report is available - generate the URL to display

		String oid = item.getExternalId();
		String fid = "6";
		String fcmd = "1";
		String cid = item.getSiteId();
		String assignid = defaultAssignId + item.getSiteId();
		String utp = "2";

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", fid, "fcmd", fcmd, "assignid",
				assignid, "cid", cid, "oid", oid, "utp", utp);

		params.putAll(getInstructorInfo(item.getSiteId()));

		return turnitinConn.buildTurnitinURL(params);
	}

	public String getReviewReportStudent(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {

		Optional matchingItem = crqs.getQueuedItem(getProviderId(), contentId);

		if (!matchingItem.isPresent()) {
			log.debug("Content " + contentId + " has not been queued previously");
			throw new QueueException("Content " + contentId + " has not been queued previously");
		}

		// check that the report is available
		// TODO if the database record does not show report available check with
		// turnitin (maybe)

		ContentReviewItem item = matchingItem.get();
		if (item.getStatus().compareTo(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE) != 0) {
			log.debug("Report not available: " + item.getStatus());
			throw new ReportException("Report not available: " + item.getStatus());
		}

		// report is available - generate the URL to display

		String oid = item.getExternalId();
		String fid = "6";
		String fcmd = "1";
		String cid = item.getSiteId();
		String assignid = defaultAssignId + item.getSiteId();

		User user = userDirectoryService.getCurrentUser();

		// USe the method to get the correct email
		String uem = getEmail(user);
		String ufn = getUserFirstName(user);
		String uln = getUserLastName(user);
		String uid = item.getUserId();
		String utp = "1";

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", fid, "fcmd", fcmd, "assignid",
				assignid, "uid", uid, "cid", cid, "oid", oid, "uem", uem, "ufn", ufn, "uln", uln, "utp", utp);

		return turnitinConn.buildTurnitinURL(params);
	}

	public String getReviewReport(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {

		// first retrieve the record from the database to get the externalId of
		// the content
		log.warn("Deprecated Methog getReviewReport(String contentId) called");
		return this.getReviewReportInstructor(contentId, assignmentRef, userId);
	}

	private Optional getItemByContentId(String contentId) {
		return crqs.getQueuedItem(getProviderId(), contentId);
	}

	/**
	 * Get additional data from String if available
	 * 
	 * @return array containing site ID, Task ID, Task Title
	 */
	private String[] getAssignData(String data) {
		String[] assignData = null;
		try {
			if (data.contains("#")) {
				assignData = data.split("#");
			}
		} catch (Exception e) {
		}
		return assignData;
	}

	public String getInlineTextId(String assignmentReference, String userId, long submissionTime) {
		return "";
	}

	public boolean acceptInlineAndMultipleAttachments() {
		return false;
	}

	public int getReviewScore(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException, Exception {
		ContentReviewItem item = null;
		try {
			Optional matchingItem = getItemByContentId(contentId);
			if (!matchingItem.isPresent()) {
				log.debug("Content " + contentId + " has not been queued previously");
			}
			item = matchingItem.get();
			if (item.getStatus().compareTo(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE) != 0) {
				log.debug("Report not available: " + item.getStatus());
			}
		} catch (Exception e) {
			log.error("(getReviewScore)" + e);
		}

		String[] assignData = null;
		try {
			assignData = getAssignData(contentId);
		} catch (Exception e) {
			log.error("(assignData)" + e);
		}

		String siteId = "", taskId = "", taskTitle = "";
		Map data = new HashMap();
		if (assignData != null) {
			siteId = assignData[0];
			taskId = assignData[1];
			taskTitle = assignData[2];
		} else {
			siteId = item.getSiteId();
			taskId = item.getTaskId();
			taskTitle = getAssignmentTitle(taskId);
			data.put("assignment1", "assignment1");
		}
		// Sync Grades
		if (turnitinConn.getUseGradeMark()) {
			try {
				data.put("siteId", siteId);
				data.put("taskId", taskId);
				data.put("taskTitle", taskTitle);
				syncGrades(data);
			} catch (Exception e) {
				log.error("Error syncing grades. " + e);
			}
		}

		return item.getReviewScore().intValue();
	}

	/**
	 * Check if grade sync has been run already for the specified site
	 * 
	 * @param sess
	 *            Current Session
	 * @param taskId
	 * @return
	 */
	public boolean gradesChecked(Session sess, String taskId) {
		String sessSync = "";
		try {
			sessSync = sess.getAttribute("sync").toString();
			if (sessSync.equals(taskId)) {
				return true;
			}
		} catch (Exception e) {
			// log.error("(gradesChecked)"+e);
		}
		return false;
	}

	/**
	 * Check if the specified user has the student role on the specified site.
	 * 
	 * @param siteId
	 *            Site ID
	 * @param userId
	 *            User ID
	 * @return true if user has student role on the site.
	 */
	public boolean isUserStudent(String siteId, String userId) {
		boolean isStudent = false;
		try {
			Set studentIds = getActiveUsersForRole(STUDENT_ROLE, siteId);
			List activeUsers = userDirectoryService.getUsers(studentIds);
			for (int i = 0; i < activeUsers.size(); i++) {
				User user = activeUsers.get(i);
				if (userId.equals(user.getId())) {
					return true;
				}
			}
		} catch (Exception e) {
			log.info("(isStudentUser)" + e);
		}
		return isStudent;
	}

	/**
	 * Return the Gradebook item associated with an assignment.
	 * 
	 * @param data
	 *            Map containing Site/Assignment IDs
	 * @return Associated gradebook item or null if not found
	 */
	public Assignment getAssociatedGbItem(Map data) {
		String taskId = data.get("taskId").toString();
		String siteId = data.get("siteId").toString();
		String taskTitle = data.get("taskTitle").toString();
		Assignment assignment = null;

		SecurityAdvisor advisor = pushAdvisor();
		try {
			List allGbItems = gradingService.getAssignments(siteId);
			for (Assignment assign : allGbItems) {
				// Match based on External ID / Assignment title
				if (taskId.equals(assign.getExternalId()) || assign.getName().equals(taskTitle)) {
					assignment = assign;
				}
			}
		} finally {
			popAdvisor(advisor);
		}
		return assignment;
	}

	/**
	 * Check Turnitin for grades and write them to the associated gradebook
	 * 
	 * @param data
	 *            Map containing relevant IDs (site ID, Assignment ID, Title)
	 */
	public void syncGrades(Map data) {
		// Get session and check if gardes have already been synced
		Session sess = sessionManager.getCurrentSession();
		boolean runOnce = gradesChecked(sess, data.get("taskId").toString());
		boolean isStudent = isUserStudent(data.get("siteId").toString(), sess.getUserId());
		String siteId = data.get("siteId").toString();

		if (!runOnce && !isStudent && turnitinConn.getUseGradeMark()) {
			log.info("Syncing Grades with Turnitin");

			String taskId = data.get("taskId").toString();

			HashMap reportTable = new HashMap();
			HashMap additionalData = new HashMap();
			String tiiUserId = "";

			String assign = taskId;
			if (data.containsKey("assignment1")) {
				// Assignments 1 uses the actual title whereas Assignments 2
				// uses the ID
				assign = getAssignmentTitle(taskId);
			}

			// Run once
			sess.setAttribute("sync", taskId);

			// Get students enrolled on class in Turnitin
			Map enrollmentInfo = getAllEnrollmentInfo(siteId);

			try {
				// Get Associated GB item
				Assignment assignment = getAssociatedGbItem(data);
				if (assignment == null) {
					log.warn("Failed to find assignment when syncing site: "+ siteId);
					return;
				}

				// List submissions call
				Map params = new HashMap();
				params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", "10", "fcmd", "2", "tem",
						getTEM(siteId), "assign", assign, "assignid", taskId, "cid", siteId, "ctl", siteId, "utp", "2");
				params.putAll(getInstructorInfo(siteId));

				Document document = null;
				document = turnitinConn.callTurnitinReturnDocument(params);
				Element root = document.getDocumentElement();
				if (((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim()
						.compareTo("72") == 0) {
					NodeList objects = root.getElementsByTagName("object");
					String grade = "";
					log.debug(objects.getLength() + " objects in the returned list");

					for (int i = 0; i < objects.getLength(); i++) {
						tiiUserId = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("userid").item(0)
								.getFirstChild())).getData().trim();
						additionalData.put("tiiUserId", tiiUserId);
						// Get GradeMark Grade
						try {
							grade = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("score").item(0)
									.getFirstChild())).getData().trim();
							reportTable.put("grade" + tiiUserId, Integer.valueOf(grade));
						} catch (Exception e) {
							// No score returned
							grade = "";
						}

						if (!grade.equals("")) {
							// Update Grade ----------------
							writeGrade(assignment, data, reportTable, additionalData, enrollmentInfo);
						}
					}
				} else {
					log.debug("Report list request not successful");
					log.debug(document.getTextContent());
				}
			} catch (TransientSubmissionException e) {
				log.error(e.getMessage());
			} catch (SubmissionException e) {
				log.warn("SubmissionException error. " + e.getMessage());
			}
		}
	}

	/**
	 * Check if a grade returned from Turnitin is greater than the max points
	 * for an assignment. If so then set to max points. (Grade is unchanged in
	 * Turnitin)
	 * 
	 * @param grade
	 *            Grade returned from Turnitin
	 * @param assignment
	 * @return
	 */
	public String processGrade(String grade, Assignment assignment) {
		String processedGrade = "";
		try {
			int gradeVal = Integer.parseInt(grade);
			if (gradeVal > assignment.getPoints()) {
				processedGrade = Double.toString(assignment.getPoints());
				log.info("Grade exceeds maximum point value for this assignment(" + assignment.getName()
						+ ") Setting to Max Points value");
			} else {
				processedGrade = grade;
			}
		} catch (NumberFormatException e) {
			log.warn("Error parsing grade");
		} catch (Exception e) {
			log.warn("Error processing grade");
		}
		return processedGrade;
	}

	/**
	 * Write a grade to the gradebook for the current specified user
	 * 
	 * @param assignment
	 * @param data
	 * @param reportTable
	 * @param additionalData
	 * @param enrollmentInfo
	 * @return
	 */
	public boolean writeGrade(Assignment assignment, Map data, HashMap reportTable,
			HashMap additionalData, Map enrollmentInfo) {
		boolean success = false;
		String grade = null;
		String siteId = data.get("siteId").toString();
		String currentStudentUserId = additionalData.get("tiiUserId").toString();
		String tiiExternalId = "";

		if (!enrollmentInfo.isEmpty()) {
			if (enrollmentInfo.containsKey(currentStudentUserId)) {
				tiiExternalId = enrollmentInfo.get(currentStudentUserId).toString();
				log.info("tiiExternalId: " + tiiExternalId);
			}
		} else {
			return false;
		}

		// Check if the returned grade is greater than the maximum possible
		// grade
		// If so then set to the maximum grade
		grade = processGrade(reportTable.get("grade" + currentStudentUserId).toString(), assignment);

		SecurityAdvisor advisor = pushAdvisor();
		try {
			if (grade != null) {
				try {
					if (data.containsKey("assignment1")) {
						gradingService.updateExternalAssessmentScore(siteId,
								assignment.getExternalId(), tiiExternalId, grade);
					} else {
						gradingService.setAssignmentScoreString(siteId, data.get("taskTitle").toString(),
								tiiExternalId, grade, "SYNC");
					}
					log.info("UPDATED GRADE (" + grade + ") FOR USER (" + tiiExternalId + ") IN ASSIGNMENT ("
							+ assignment.getName() + ")");
					success = true;
				} catch (Exception e) {
					log.error("Error update grade " + e.toString());
				}
			}
		} catch (Exception e) {
			log.error("Error setting grade " + e.toString());
		} finally {
			popAdvisor(advisor);
		}
		return success;
	}

	/**
	 * Get a list of students enrolled on a class in Turnitin
	 * 
	 * @param siteId
	 *            Site ID
	 * @return Map containing Students turnitin / Sakai ID
	 */
	public Map getAllEnrollmentInfo(String siteId) {
		Map params = new HashMap();
		Map enrollmentInfo = new HashMap();
		String tiiExternalId = "";// the ID sakai stores
		String tiiInternalId = "";// Turnitin internal ID
		User user = null;
		Map instructorInfo = getInstructorInfo(siteId, true);
		try {
			user = userDirectoryService.getUser(instructorInfo.get("uid").toString());
		} catch (UserNotDefinedException e) {
			log.error("(getAllEnrollmentInfo)User not defined. " + e);
		}
		params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", "19", "fcmd", "5", "tem",
				getTEM(siteId), "ctl", siteId, "cid", siteId, "utp", "2", "uid", user.getId(), "uem", getEmail(user),
				"ufn", user.getFirstName(), "uln", user.getLastName());
		Document document = null;
		try {
			document = turnitinConn.callTurnitinReturnDocument(params);
		} catch (Exception e) {
			log.warn("Failed to get enrollment data using user: " + user.getDisplayName(), e);
		}

		Element root = document.getDocumentElement();
		if (((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim()
				.compareTo("93") == 0) {
			NodeList objects = root.getElementsByTagName("student");
			for (int i = 0; i < objects.getLength(); i++) {
				tiiExternalId = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("uid").item(0)
						.getFirstChild())).getData().trim();
				tiiInternalId = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("userid").item(0)
						.getFirstChild())).getData().trim();
				enrollmentInfo.put(tiiInternalId, tiiExternalId);
			}
		}
		return enrollmentInfo;
	}

	private SecurityAdvisor pushAdvisor() {
		SecurityAdvisor advisor = (userId, function, reference) -> SecurityAdvisor.SecurityAdvice.ALLOWED;
		securityService.pushAdvisor(advisor);
		return advisor;
	}

	private void popAdvisor(SecurityAdvisor advisor) {
		securityService.popAdvisor(advisor);
	}

	/**
	 * private methods
	 */
	private String encodeParam(String name, String value, String boundary) {
		return "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n";
	}

	/**
	 * This method was originally private, but is being made public for the
	 * moment so we can run integration tests. TODO Revisit this decision.
	 *
	 * @param siteId
	 * @throws SubmissionException
	 * @throws TransientSubmissionException
	 */
	@SuppressWarnings("unchecked")
	public void createClass(String siteId) throws SubmissionException, TransientSubmissionException {
		log.debug("Creating class for site: " + siteId);

		String cpw = defaultClassPassword;
		String ctl = siteId;
		String fcmd = "2";
		String fid = "2";
		String utp = "2"; // user type 2 = instructor
		String cid = siteId;

		Document document = null;

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "cid", cid, "cpw", cpw, "ctl", ctl,
				"fcmd", fcmd, "fid", fid, "utp", utp);

		params.putAll(getInstructorInfo(siteId));

		document = turnitinConn.callTurnitinReturnDocument(params);

		Element root = document.getDocumentElement();
		String rcode = ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim();

		if (((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim()
				.compareTo("20") == 0
				|| ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim()
						.compareTo("21") == 0) {
			log.debug("Create Class successful");
		} else {
			String rmessage = ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim();
			throw new ContentReviewProviderException(getFormattedMessage("enrolment.class.creation.error.with.code", rmessage, rcode),
				createLastError(doc->createFormattedMessageXML(doc, "enrolment.class.creation.error.with.code", rmessage, rcode)));
		}
	}

	/**
	 * This returns the String that will be used as the Assignment Title in Turn
	 * It In.
	 *
	 * The current implementation here has a few interesting caveats so that it
	 * will work with both, the existing Assignments 1 integration, and the new
	 * Assignments 2 integration under development.
	 *
	 * We will check and see if the taskId starts with /assignment/. If it does
	 * we will look up the Assignment Entity on the legacy Entity bus. (not the
	 * entitybroker). This needs some general work to be made generally modular
	 * ( and useful for more than just Assignments 1 and 2 ). We will need to
	 * look at some more concrete use cases and then factor it accordingly in
	 * the future when the next scenerio is required.
	 *
	 * Another oddity is that to get rid of our hard dependency on Assignments 1
	 * we are invoking the getTitle method by hand. We probably need a mechanism
	 * to register a title handler or something as part of the setup process for
	 * new services that want to be reviewable.
	 *
	 * @param taskId
	 * @return
	 */
	private String getAssignmentTitle(String taskId) {
		String togo = taskId;
		if (taskId.startsWith("/assignment/")) {
			try {
				Reference ref = entityManager.newReference(taskId);
				log.debug("got ref " + ref + " of type: " + ref.getType());
				EntityProducer ep = ref.getEntityProducer();

				Entity ent = ep.getEntity(ref);
				log.debug("got entity " + ent);
				if(ent != null){
					String title = scrubSpecialCharacters(ent.getClass().getMethod("getTitle").invoke(ent).toString());
					log.debug("Got reflected assignment title from entity " + title);
					togo = URLDecoder.decode(title, "UTF-8");
				}
			} catch (Exception e) {
				log.error(e.getMessage(), e);
			}
		}

		// Turnitin requires Assignment titles to be at least two characters
		// long
		if (togo.length() == 1) {
			togo = togo + "_";
		}

		return togo;

	}

	private String scrubSpecialCharacters(String title) {

		try {
			if (title.contains("&")) {
				title = title.replace('&', 'n');
			}
			if (title.contains("%")) {
				title = title.replace("%", "percent");
			}
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}

		return title;
	}

	/**
	 * @param siteId
	 * @param taskId
	 * @throws SubmissionException
	 * @throws TransientSubmissionException
	 */
	public void createAssignment(String siteId, String taskId)
			throws SubmissionException, TransientSubmissionException {
		createAssignment(siteId, taskId, null);
	}

	/**
	 * Works by fetching the Instructor User info based on defaults or current
	 * user.
	 *
	 * @param siteId
	 * @param taskId
	 * @return
	 * @throws SubmissionException
	 * @throws TransientSubmissionException
	 */
	@SuppressWarnings("unchecked")
	public Map getAssignment(String siteId, String taskId) throws SubmissionException, TransientSubmissionException {
		String taskTitle = getAssignmentTitle(taskId);

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "assign", taskTitle, "assignid", taskId,
				"cid", siteId, "ctl", siteId, "fcmd", "7", "fid", "4", "utp", "2");

		params.putAll(getInstructorInfo(siteId));

		return turnitinConn.callTurnitinReturnMap(params);
	}

	public void addTurnitinInstructor(Map userparams) throws SubmissionException, TransientSubmissionException {
		Map params = new HashMap();
		params.putAll(userparams);
		params.putAll(turnitinConn.getBaseTIIOptions());
		params.put("fid", "1");
		params.put("fcmd", "2");
		params.put("utp", "2");
		turnitinConn.callTurnitinReturnMap(params);
	}

	/**
	 * Creates or Updates an Assignment
	 *
	 * This method will look at the current user or default instructor for it's
	 * user information.
	 *
	 *
	 * @param siteId
	 * @param taskId
	 * @param extraAsnnOpts
	 * @throws SubmissionException
	 * @throws TransientSubmissionException
	 */
	@SuppressWarnings("unchecked")
	public void createAssignment(String siteId, String taskId, Map extraAsnnOpts)
			throws SubmissionException, TransientSubmissionException {

		// get the assignment reference
		String rawTitle = "";
		if (extraAsnnOpts.containsKey("title")) {
			rawTitle = extraAsnnOpts.get("title").toString();
		} else {
			rawTitle = getAssignmentTitle(taskId);
		}
		String taskTitle = rawTitle.replaceAll("[^\\w\\s]", "");
		log.debug("Creating assignment for site: " + siteId + ", task: " + taskId + ", rawTitle: " + rawTitle + ", taskTitle: " + taskTitle);

		SimpleDateFormat dform = ((SimpleDateFormat) DateFormat.getDateInstance());
		dform.applyPattern(TURNITIN_DATETIME_FORMAT);
		Calendar cal = Calendar.getInstance();
		// set this to yesterday so we avoid timezone problems etc
		// TII-143 seems this now causes problems may need a finner tweak than 1
		// day like midnight +1 min or something
		cal.set(Calendar.HOUR, 0);
		cal.set(Calendar.MINUTE, 1);
		// cal.add(Calendar.DAY_OF_MONTH, -1);
		String dtstart = dform.format(cal.getTime());
		String today = dtstart;

		// set the due dates for the assignments to be in 5 month's time
		// turnitin automatically sets each class end date to 6 months after it
		// is created
		// the assignment end date must be on or before the class end date

		String fcmd = "2"; // new assignment
		boolean asnnExists = false;
		// If this assignment already exists, we should use fcmd 3 to update it.
		Map tiiresult = this.getAssignment(siteId, taskId);
		if (tiiresult.get("rcode") != null && tiiresult.get("rcode").equals("85")) {
			fcmd = "3";
			asnnExists = true;
		}

		/*
		 * Some notes about start and due dates. This information is accurate as
		 * of Nov 12, 2009 and was determined by testing and experimentation
		 * with some Sash scripts.
		 *
		 * A turnitin due date, must be after the start date. This makes sense
		 * and follows the logic in both Assignments 1 and 2.
		 *
		 * When *creating* a new Turnitin Assignment, the start date must be
		 * todays date or later. The format for dates only includes the day, and
		 * not any specific times. I believe that, in order to make up for time
		 * zone differences between your location and the turnitin cloud, it can
		 * be basically the current day anywhere currently, with some slack. For
		 * instance I can create an assignment for yesterday, but not for 2 days
		 * ago. Doing so causes an error.
		 *
		 * However! For an existing turnitin assignment, you appear to have the
		 * liberty of changing the start date to sometime in the past. You can
		 * also change an assignment to have a due date in the past as long as
		 * it is still after the start date.
		 *
		 * So, to avoid errors when syncing information, or adding turnitin
		 * support to new or existing assignments we will:
		 *
		 * 1. If the assignment already exists we'll just save it.
		 *
		 * 2. If the assignment does not exist, we will save it once using
		 * todays date for the start and due date, and then save it again with
		 * the proper dates to ensure we're all tidied up and in line.
		 *
		 * Also, with our current class creation, due dates can be 5 years out,
		 * but not further. This seems a bit lower priortity, but we still
		 * should figure out an appropriate way to deal with it if it does
		 * happen.
		 *
		 */

		// TODO use the 'secret' function to change this to longer
		cal.add(Calendar.MONTH, 5);
		String dtdue = dform.format(cal.getTime());
		log.debug("Set date due to: " + dtdue);
		if (extraAsnnOpts != null && extraAsnnOpts.containsKey("dtdue")) {
			dtdue = extraAsnnOpts.get("dtdue").toString();
			log.debug("Settign date due from external to: " + dtdue);
			extraAsnnOpts.remove("dtdue");
		}

		String fid = "4"; // function id
		String utp = "2"; // user type 2 = instructor
		String s_view_report = "1";
		if (extraAsnnOpts != null && extraAsnnOpts.containsKey("s_view_report")) {
			s_view_report = extraAsnnOpts.get("s_view_report").toString();
			extraAsnnOpts.remove("s_view_report");
		}

		// erater
		String erater = (serverConfigurationService.getBoolean("turnitin.option.erater.default", false)) ? "1" : "0";
		String ets_handbook = "1";
		String ets_dictionary = "en";
		String ets_spelling = "1";
		String ets_style = "1";
		String ets_grammar = "1";
		String ets_mechanics = "1";
		String ets_usage = "1";

		try {
			if (extraAsnnOpts != null && extraAsnnOpts.containsKey("erater")) {
				erater = extraAsnnOpts.get("erater").toString();
				extraAsnnOpts.remove("erater");

				ets_handbook = extraAsnnOpts.get("ets_handbook").toString();
				extraAsnnOpts.remove("ets_handbook");

				ets_dictionary = extraAsnnOpts.get("ets_dictionary").toString();
				extraAsnnOpts.remove("ets_dictionary");

				ets_spelling = extraAsnnOpts.get("ets_spelling").toString();
				extraAsnnOpts.remove("ets_spelling");

				ets_style = extraAsnnOpts.get("ets_style").toString();
				extraAsnnOpts.remove("ets_style");

				ets_grammar = extraAsnnOpts.get("ets_grammar").toString();
				extraAsnnOpts.remove("ets_grammar");

				ets_mechanics = extraAsnnOpts.get("ets_mechanics").toString();
				extraAsnnOpts.remove("ets_mechanics");

				ets_usage = extraAsnnOpts.get("ets_usage").toString();
				extraAsnnOpts.remove("ets_usage");
			}
		} catch (Exception e) {
			log.info("(createAssignment)erater extraAsnnOpts. " + e);
		}

		String cid = siteId;
		String assignid = taskId;
		String ctl = siteId;

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "assign", taskTitle, "assignid",
				assignid, "cid", cid, "ctl", ctl, "dtdue", dtdue, "dtstart", dtstart, "fcmd", "3", "fid", fid,
				"s_view_report", s_view_report, "utp", utp, "erater", erater, "ets_handbook", ets_handbook,
				"ets_dictionary", ets_dictionary, "ets_spelling", ets_spelling, "ets_style", ets_style, "ets_grammar",
				ets_grammar, "ets_mechanics", ets_mechanics, "ets_usage", ets_usage);

		// Save instructorInfo up here to reuse for calls in this
		// method, since theoretically getInstructorInfo could return
		// different instructors for different invocations and we need
		// the same one since we're using a session id.
		Map instructorInfo = getInstructorInfo(siteId);
		params.putAll(instructorInfo);

		if (extraAsnnOpts != null) {
			for (Object key : extraAsnnOpts.keySet()) {
				if (extraAsnnOpts.get(key) == null) {
					continue;
				}
				params = TurnitinAPIUtil.packMap(params, key.toString(), extraAsnnOpts.get(key).toString());
			}
		}

		// We only need to use a session id if we are creating this
		// assignment for the first time.
		String sessionid = null;
		Map sessionParams = null;

		if (!asnnExists) {
			// Try adding the user in case they don't exist TII-XXX
			addTurnitinInstructor(instructorInfo);

			sessionParams = turnitinConn.getBaseTIIOptions();
			sessionParams.putAll(instructorInfo);
			sessionParams.put("utp", utp);
			sessionid = TurnitinSessionFuncs.getTurnitinSession(turnitinConn, sessionParams);

			Map firstparams = new HashMap();
			firstparams.putAll(params);
			firstparams.put("session-id", sessionid);
			firstparams.put("dtstart", today);

			// Make the due date in the future
			Calendar caldue = Calendar.getInstance();
			caldue.add(Calendar.MONTH, 5);
			String dtdue_first = dform.format(caldue.getTime());
			firstparams.put("dtdue", dtdue_first);

			log.debug("date due is: " + dtdue);
			log.debug("Start date: " + today);
			firstparams.put("fcmd", "2");
			Document firstSaveDocument = turnitinConn.callTurnitinReturnDocument(firstparams);
			Element root = firstSaveDocument.getDocumentElement();
			int rcode = new Integer(
					((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim())
							.intValue();
			String rmessage = ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim();
			if ((rcode > 0 && rcode < 100) || rcode == 419) {
				log.debug("Create FirstDate Assignment successful");
				log.debug("tii returned {}. Code: {}", rmessage, rcode);
			} else {
				log.debug("FirstDate Assignment creation failed with message: {}. Code: {}", rmessage, rcode);
				String errorMessage = getFormattedMessage("assignment.creation.error.with.code", rmessage, rcode);
				TransientSubmissionException tse = new TransientSubmissionException(errorMessage, Integer.valueOf(rcode));
				throw new ContentReviewProviderException(errorMessage, createLastError(doc->createFormattedMessageXML(doc, "assignment.creation.error.with.code", rmessage, rcode)), tse);
			}
		}
		log.debug("going to attempt second update");
		if (sessionid != null) {
			params.put("session-id", sessionid);
		}
		Document document = turnitinConn.callTurnitinReturnDocument(params);

		Element root = document.getDocumentElement();
		String rmessage = ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim();
		int rcode = new Integer(
				((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim())
						.intValue();
		if ((rcode > 0 && rcode < 100) || rcode == 419) {
			log.debug("Create Assignment successful");
			log.debug("tii returned {}. Code: {}", rmessage, rcode);
		} else {
			log.debug("Assignment creation failed with message: {}. Code: {}", rmessage, rcode);
			// log.debug(root);
			String errorMessage = getFormattedMessage("assignment.creation.error.with.code", rmessage, rcode);
			TransientSubmissionException tse = new TransientSubmissionException(errorMessage, Integer.valueOf(rcode));
			throw new ContentReviewProviderException(errorMessage, createLastError(doc->createFormattedMessageXML(doc, "assignment.creation.error.with.code", rmessage, rcode)), tse);
		}

		if (sessionid != null) {
			TurnitinSessionFuncs.logoutTurnitinSession(turnitinConn, sessionid, sessionParams);
		}
	}

	/**
	 * Currently public for integration tests. TODO Revisit visibility of
	 * method.
	 *
	 * @param userId
	 * @param uem
	 * @param siteId
	 * @throws SubmissionException
	 */
	public void enrollInClass(String userId, String uem, String siteId)
			throws SubmissionException, TransientSubmissionException {

		String uid = userId;
		String cid = siteId;

		String ctl = siteId;
		String fid = "3";
		String fcmd = "2";
		String tem = getTEM(cid);

		User user;
		try {
			user = userDirectoryService.getUser(userId);
		} catch (Exception t) {
			throw new ContentReviewProviderException(t.getLocalizedMessage(), createLastError(doc->createFormattedMessageXML(doc, "enrolment.user.idunusedexception")));
		}

		log.debug("Enrolling user " + user.getEid() + "(" + userId + ")  in class " + siteId);

		String ufn = getUserFirstName(user);
		if (ufn == null) {
			throw new ContentReviewProviderException("First name required for user: " + userId, createLastError(doc->createFormattedMessageXML(doc, "enrolment.first.name.required")));
		}

		String uln = getUserLastName(user);
		if (uln == null) {
			throw new ContentReviewProviderException("Last name required for user: " + userId, createLastError(doc->createFormattedMessageXML(doc, "enrolment.last.name.required")));
		}

		String utp = "1";

		Map params = new HashMap();
		params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", fid, "fcmd", fcmd, "cid", cid, "tem",
				tem, "ctl", ctl, "dis", studentAccountNotified ? "0" : "1", "uem", uem, "ufn", ufn, "uln", uln, "utp",
				utp, "uid", uid);

		Document document = turnitinConn.callTurnitinReturnDocument(params);

		Element root = document.getDocumentElement();

		String rMessage = ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData();
		String rCode = ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData();
		if ("31".equals(rCode)) {
			log.debug("Results from enrollInClass with user + " + userId + " and class title: " + ctl + ".\n"
					+ "rCode: " + rCode + " rMessage: " + rMessage);
		} else {
			// certain return codes need to be logged
			log.warn("Results from enrollInClass with user + " + userId + " and class title: " + ctl + ". " + "rCode: "
					+ rCode + ", rMessage: " + rMessage);
			// TODO for certain types we should probably throw an exception here
			// and stop the proccess
		}

	}

	/*
	 * Get the next item that needs to be submitted
	 *
	 */
	private Optional getNextItemInSubmissionQueue() {
		return crqs.getNextItemInQueueToSubmit(getProviderId());
	}

	public void processQueue() {

		log.info("Processing submission queue");
		int errors = 0;
		int success = 0;

		Optional nextItem = null;
		while ((nextItem = getNextItemInSubmissionQueue()).isPresent()) {
			ContentReviewItem item = nextItem.get();
			
			log.debug("Attempting to submit content: " + item.getContentId() + " for user: "
					+ item.getUserId() + " and site: " + item.getSiteId());

			if (item.getRetryCount() == null) {
				item.setRetryCount(Long.valueOf(0));
				item.setNextRetryTime(this.getNextRetryTime(0));
				crqs.update(item);
			} else if (item.getRetryCount().intValue() > maxRetry) {
				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(this.getNextRetryTime(Long.valueOf(l)));
				crqs.update(item);
			}

			User user;

			try {
				user = userDirectoryService.getUser(item.getUserId());
			} catch (UserNotDefinedException e1) {
				log.error("Submission attempt unsuccessful - User not found.", e1);
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
				crqs.update(item);
				errors++;
				continue;
			}

			String uem = getEmail(user);
			if (uem == null) {
				log.error("User: " + user.getEid() + " has no valid email");
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_USER_DETAILS_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "enrolment.email.required"));
				crqs.update(item);
				errors++;
				continue;
			}

			String ufn = getUserFirstName(user);
			if (ufn == null || ufn.equals("")) {
				log.error("Submission attempt unsuccessful - User has no first name");
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_USER_DETAILS_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "enrolment.first.name.required"));
				crqs.update(item);
				errors++;
				continue;
			}

			String uln = getUserLastName(user);
			if (uln == null || uln.equals("")) {
				log.error("Submission attempt unsuccessful - User has no last name");
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_USER_DETAILS_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "enrolment.last.name.required"));
				crqs.update(item);
				errors++;
				continue;
			}

			if (!turnitinConn.isUseSourceParameter()) {
				try {
					createClass(item.getSiteId());
				} catch (Exception e) {
					log.error("Submission attempt unsuccessful: Could not create class", e);
					setLastError(item, e, doc->createFormattedMessageXML(doc, "enrolment.class.creation.error", e.getLocalizedMessage()));
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
					crqs.update(item);
					errors++;
					continue;
				}
			}

			try {
				enrollInClass(item.getUserId(), uem, item.getSiteId());
			} catch (Exception t) {
				log.error("Submission attempt unsuccessful: Could not enroll user in class", t);
				setLastError(item, t, doc->createFormattedMessageXML(doc, "enrolment.generic.error", t.getLocalizedMessage()));
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
				crqs.update(item);
				errors++;
				continue;
			}

			if (!turnitinConn.isUseSourceParameter()) {
				try {
					Map tiiresult = this.getAssignment(item.getSiteId(), item.getTaskId());
					if (tiiresult.get("rcode") != null && !tiiresult.get("rcode").equals("85")) {
						createAssignment(item.getSiteId(), item.getTaskId());
					}
				} catch (Exception e) {
					setLastError(item, e);
					Throwable cause = e.getCause() == null ? e : e.getCause();
					Long status = cause instanceof SubmissionException ? ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE : ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE;
					item.setStatus(status);
					Integer errorCode = null;
					if (cause instanceof TransientSubmissionException) {
						errorCode = ((TransientSubmissionException) cause).getErrorCode();
					} else if (cause instanceof SubmissionException) {
						errorCode = ((SubmissionException) cause).getErrorCode();
					}
					if (errorCode != null) {
						item.setErrorCode(errorCode);
					}
					crqs.update(item);
					errors++;
					continue;
				}
			}

			// get all the info for the api call
			// we do this before connecting so that if there is a problem we can
			// jump out - saves time
			// these errors should probably be caught when a student is enrolled
			// in a class
			// but we check again here to be sure

			String fcmd = "2";
			String fid = "5";

			// to get the name of the initial submited file we need the title
			ContentResource resource = null;
			ResourceProperties resourceProperties = null;
			String fileName = null;
			try {
				try {
					resource = contentHostingService.getResource(item.getContentId());

				} catch (IdUnusedException e4) {
					// ToDo we should probably remove these from the Queue
					log.warn("IdUnusedException: no resource with id " + item.getContentId());
					crqs.delete(item);
					errors++;
					continue;
				}
				resourceProperties = resource.getProperties();
				fileName = resourceProperties.getProperty(resourceProperties.getNamePropDisplayName());
				fileName = escapeFileName(fileName, resource.getId());
			} catch (PermissionException e2) {
				log.error("Submission failed due to permission error.", e2);
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "submission.permission.exception"));
				crqs.update(item);
				errors++;
				continue;
			} catch (TypeException e) {
				log.error("Submission failed due to content Type error.", e);
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "submission.type.exception", e.getLocalizedMessage()));
				crqs.update(item);
				errors++;
				continue;
			}

			// TII-97 filenames can't be longer than 200 chars
			if (fileName != null && fileName.length() >= 200) {
				fileName = truncateFileName(fileName, 198);
			}

			String userEid = item.getUserId();
			try {
				userEid = userDirectoryService.getUserEid(item.getUserId());
			} catch (UserNotDefinedException unde) {
				// nothing realy to do?
			}

			String ptl = userEid + ":" + fileName;
			String ptype = "2";

			String uid = item.getUserId();
			String cid = item.getSiteId();
			String assignid = item.getTaskId();

			// TODO ONC-1292 How to get this, and is it still required with
			// src=9?
			String tem = getTEM(cid);

			String utp = "1";

			log.debug("Using Emails: tem: " + tem + " uem: " + uem);

			String assign = getAssignmentTitle(item.getTaskId());
			String ctl = item.getSiteId();

			Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "assignid", assignid, "uid", uid,
					"cid", cid, "assign", assign, "ctl", ctl, "dis",
					Integer.valueOf(sendSubmissionNotification).toString(), "fcmd", fcmd, "fid", fid, "ptype", ptype,
					"ptl", ptl, "tem", tem, "uem", uem, "ufn", ufn, "uln", uln, "utp", utp, "resource_obj", resource);

			Document document = null;
			try {
				document = turnitinConn.callTurnitinReturnDocument(params, true);
			} catch (Exception e) {
				item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
				setLastError(item, doc->createFormattedMessageXML(doc, "submission.transient.submission.exception", e.getLocalizedMessage()));
				crqs.update(item);
				errors++;
				continue;
			}

			Element root = document.getDocumentElement();

			String rMessage = ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild()))
					.getData();
			String rCode = ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData();

			if (rCode == null)
				rCode = "";
			else
				rCode = rCode.trim();

			if (rMessage == null)
				rMessage = rCode;
			else
				rMessage = rMessage.trim();

			if (rCode.compareTo("51") == 0) {
				String externalId = ((CharacterData) (root.getElementsByTagName("objectID").item(0).getFirstChild()))
						.getData().trim();
				if (externalId != null && externalId.length() > 0) {
					log.debug("Submission successful");
					item.setExternalId(externalId);
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_AWAITING_REPORT_CODE);
					item.setRetryCount(Long.valueOf(0));
					item.setLastError(null);
					item.setErrorCode(null);
					item.setDateSubmitted(new Date());
					success++;
					crqs.update(item);
				} else {
					log.warn("invalid external id");
					setLastError(item, doc->createFormattedMessageXML(doc, "submission.no.external.id"));
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
					errors++;
					crqs.update(item);
				}
			} else {
				log.debug("Submission not successful: "
						+ ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData()
								.trim());

				if (rMessage.equals("User password does not match user email") || "1001".equals(rCode)
						|| "".equals(rMessage) || "413".equals(rCode) || "1025".equals(rCode) || "250".equals(rCode)) {
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
					log.warn("Submission not successful. It will be retried.");
					errors++;
				} else if (rCode.equals("423")) {
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_USER_DETAILS_CODE);
					errors++;

				} else if (rCode.equals("301")) {
					// this took a long time
					log.warn("Submission not successful due to timeout. It will be retried.");
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_CODE);
					Calendar cal = Calendar.getInstance();
					cal.set(Calendar.HOUR_OF_DAY, 22);
					item.setNextRetryTime(cal.getTime());
					errors++;

				} else {
					log.error("Submission not successful. It will NOT be retried.");
					item.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_NO_RETRY_CODE);
					errors++;
				}
				final String arg1 = rMessage;
				final String arg2 = rCode;
				setLastError(item, doc->createFormattedMessageXML(doc, "submission.error.with.code", arg1, arg2));
				item.setErrorCode(Integer.valueOf(rCode));
				crqs.update(item);

			}
			// release the lock so the reports job can handle it
			getNextItemInSubmissionQueue();
		}

		log.info("Submission queue run completed: " + success + " items submitted, " + errors + " errors.");
	}

	public String escapeFileName(String fileName, String contentId) {
		log.debug("origional filename is: " + fileName);
		if (fileName == null) {
			// use the id
			fileName = contentId;
		} else if (fileName.length() > 199) {
			fileName = fileName.substring(0, 199);
		}
		log.debug("fileName is :" + fileName);
		try {
			fileName = URLDecoder.decode(fileName, "UTF-8");
			// in rare cases it seems filenames can be double encoded
			while (fileName.indexOf("%20") > 0 || fileName.contains("%2520")) {
				try {
					fileName = URLDecoder.decode(fileName, "UTF-8");
				} catch (IllegalArgumentException eae) {
					log.warn("Unable to decode fileName: " + fileName + ", using contentId: " + contentId);
					// as the result is likely to cause a MD5 exception use the
					// ID
					return contentId;
					/*
					 * currentItem.setStatus(ContentReviewItem.
					 * SUBMISSION_ERROR_NO_RETRY_CODE);
					 * currentItem.setLastError("FileName decode exception: " +
					 * fileName); dao.update(currentItem);
					 * releaseLock(currentItem); errors++; throw new
					 * SubmissionException("Can't decode fileName!");
					 */
				}

			}
		} catch (IllegalArgumentException eae) {
			log.warn("Unable to decode fileName: " + fileName + ", using contentId: " + contentId);
			return contentId;
		} catch (UnsupportedEncodingException e) {
			log.warn(e.getMessage(), e);
		}

		fileName = fileName.replace(' ', '_');
		// its possible we have double _ as a result of this lets do some
		// cleanup
		fileName = StringUtils.replace(fileName, "__", "_");

		log.debug("fileName is :" + fileName);
		return fileName;
	}

	private String truncateFileName(String fileName, int i) {
		// get the extension for later re-use
		String extension = "";
		if (fileName.contains(".")) {
			extension = fileName.substring(fileName.lastIndexOf("."));
		}

		fileName = fileName.substring(0, i - extension.length());
		fileName = fileName + extension;

		return fileName;
	}

	public void checkForReports() {
		checkForReportsBulk();
	}

	public void syncRosters() {
		processSyncQueue();
	}

	/*
	 * Fetch reports on a class by class basis
	 */
	@SuppressWarnings({ "deprecation", "unchecked" })
	public void checkForReportsBulk() {

		SimpleDateFormat dform = ((SimpleDateFormat) DateFormat.getDateInstance());
		dform.applyPattern(TURNITIN_DATETIME_FORMAT);

		log.info("Fetching reports from Turnitin");

		// get the list of all items that are waiting for reports
		List awaitingReport = crqs.getAwaitingReports(getProviderId());

		Iterator listIterator = awaitingReport.iterator();
		HashMap reportTable = new HashMap();

		log.debug("There are " + awaitingReport.size() + " submissions awaiting reports");

		ContentReviewItem currentItem;
		while (listIterator.hasNext()) {
			currentItem = (ContentReviewItem) listIterator.next();

			// has the item reached its next retry time?
			if (currentItem.getNextRetryTime() == null)
				currentItem.setNextRetryTime(new Date());

			if (currentItem.getNextRetryTime().after(new Date())) {
				// we haven't reached the next retry time
				log.info("next retry time not yet reached for item: " + currentItem.getId());
				crqs.update(currentItem);
				continue;
			}

			if (currentItem.getRetryCount() == null) {
				currentItem.setRetryCount(Long.valueOf(0));
				currentItem.setNextRetryTime(this.getNextRetryTime(0));
			} else if (currentItem.getRetryCount().intValue() > maxRetry) {
				currentItem.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMISSION_ERROR_RETRY_EXCEEDED_CODE);
				crqs.update(currentItem);
				continue;
			} else {
				log.debug("Still have retries left, continuing. ItemID: " + currentItem.getId());
				// Moving down to check for report generate speed.
				// long l = currentItem.getRetryCount().longValue();
				// l++;
				// currentItem.setRetryCount(Long.valueOf(l));
				// currentItem.setNextRetryTime(this.getNextRetryTime(Long.valueOf(l)));
				// dao.update(currentItem);
			}

			if (currentItem.getExternalId() == null || currentItem.getExternalId().equals("")) {
				currentItem.setStatus(Long.valueOf(4));
				crqs.update(currentItem);
				continue;
			}

			if (!reportTable.containsKey(currentItem.getExternalId())) {
				// get the list from turnitin and see if the review is available

				log.debug("Attempting to update hashtable with reports for site " + currentItem.getSiteId());

				String fcmd = "2";
				String fid = "10";

				try {
					User user = userDirectoryService.getUser(currentItem.getUserId());
				} catch (Exception e) {
					log.error("Unable to look up user: " + currentItem.getUserId() + " for contentItem: "
							+ currentItem.getId(), e);
				}

				String cid = currentItem.getSiteId();
				String tem = getTEM(cid);

				String utp = "2";

				String assignid = currentItem.getTaskId();

				String assign = currentItem.getTaskId();
				String ctl = currentItem.getSiteId();

				// TODO FIXME Current sgithens
				// Move the update setRetryAttempts to here, and first call and
				// check the assignment from TII to see if the generate until
				// due is enabled. In that case we don't want to waste retry
				// attempts and should just continue.
				try {
					// TODO FIXME This is broken at the moment because we need
					// to have a userid, but this is assuming it's coming from
					// the thread, but we're in a quartz job.
					// Map curasnn = getAssignment(currentItem.getSiteId(),
					// currentItem.getTaskId());
					// TODO FIXME Parameterize getAssignment method to take user
					// information
					Map getAsnnParams = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "assign",
							getAssignmentTitle(currentItem.getTaskId()), "assignid", currentItem.getTaskId(), "cid",
							currentItem.getSiteId(), "ctl", currentItem.getSiteId(), "fcmd", "7", "fid", "4", "utp",
							"2");

					getAsnnParams.putAll(getInstructorInfo(currentItem.getSiteId()));

					Map curasnn = turnitinConn.callTurnitinReturnMap(getAsnnParams);

					if (curasnn.containsKey("object")) {
						Map curasnnobj = (Map) curasnn.get("object");
						String reportGenSpeed = (String) curasnnobj.get("generate");
						String duedate = (String) curasnnobj.get("dtdue");
						SimpleDateFormat retform = ((SimpleDateFormat) DateFormat.getDateInstance());
						retform.applyPattern(TURNITIN_DATETIME_FORMAT);
						Date duedateObj = null;
						try {
							if (duedate != null) {
								duedateObj = retform.parse(duedate);
							}
						} catch (ParseException pe) {
							log.warn("Unable to parse turnitin dtdue: " + duedate, pe);
						}
						if (reportGenSpeed != null && duedateObj != null && reportGenSpeed.equals("2")
								&& duedateObj.after(new Date())) {
							log.info("Report generate speed is 2, skipping for now. ItemID: " + currentItem.getId());
							// If there was previously a transient error for
							// this item, reset the status
							if (ContentReviewConstants.CONTENT_REVIEW_REPORT_ERROR_RETRY_CODE.equals(currentItem.getStatus())) {
								currentItem.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_AWAITING_REPORT_CODE);
								currentItem.setLastError(null);
								currentItem.setErrorCode(null);
								crqs.update(currentItem);
							}
							continue;
						} else {
							log.debug("Incrementing retry count for currentItem: " + currentItem.getId());
							long l = currentItem.getRetryCount().longValue();
							l++;
							currentItem.setRetryCount(Long.valueOf(l));
							currentItem.setNextRetryTime(this.getNextRetryTime(Long.valueOf(l)));
							crqs.update(currentItem);
						}
					}
				} catch (SubmissionException e) {
					log.error("Unable to check the report gen speed of the asnn for item: " + currentItem.getId(), e);
				} catch (TransientSubmissionException e) {
					log.error("Unable to check the report gen speed of the asnn for item: " + currentItem.getId(), e);
				}

				Map params = new HashMap();
				// try {
				params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "fid", fid, "fcmd", fcmd, "tem", tem,
						"assign", assign, "assignid", assignid, "cid", cid, "ctl", ctl, "utp", utp);
				params.putAll(getInstructorInfo(currentItem.getSiteId()));

				Document document = null;

				try {
					document = turnitinConn.callTurnitinReturnDocument(params);
				} catch (Exception e) {
					log.warn("Update failed:", e);
					currentItem.setStatus(ContentReviewConstants.CONTENT_REVIEW_REPORT_ERROR_RETRY_CODE);
					setLastError(currentItem, doc->createFormattedMessageXML(doc, "report.generic.error", e.getLocalizedMessage()));
					crqs.update(currentItem);
					break;
				}

				Element root = document.getDocumentElement();
				if (((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim()
						.compareTo("72") == 0) {
					log.debug("Report list returned successfully");

					NodeList objects = root.getElementsByTagName("object");
					String objectId;
					String similarityScore;
					String overlap = "";
					log.debug(objects.getLength() + " objects in the returned list");
					for (int i = 0; i < objects.getLength(); i++) {
						similarityScore = ((CharacterData) (((Element) (objects.item(i)))
								.getElementsByTagName("similarityScore").item(0).getFirstChild())).getData().trim();
						objectId = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("objectID")
								.item(0).getFirstChild())).getData().trim();
						if (similarityScore.compareTo("-1") != 0) {
							overlap = ((CharacterData) (((Element) (objects.item(i))).getElementsByTagName("overlap")
									.item(0).getFirstChild())).getData().trim();
							reportTable.put(objectId, Integer.valueOf(overlap));
						} else {
							reportTable.put(objectId, Integer.valueOf(-1));
						}

						log.debug("objectId: " + objectId + " similarity: " + similarityScore + " overlap: " + overlap);
					}
				} else {
					log.debug("Report list request not successful");
					log.debug(document.getTextContent());

				}
			}

			int reportVal;
			// check if the report value is now there (there may have been a
			// failure to get the list above)
			if (reportTable.containsKey(currentItem.getExternalId())) {
				reportVal = ((Integer) (reportTable.get(currentItem.getExternalId()))).intValue();
				log.debug("reportVal for " + currentItem.getExternalId() + ": " + reportVal);
				if (reportVal != -1) {
					currentItem.setReviewScore(reportVal);
					currentItem.setStatus(ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE);
					currentItem.setDateReportReceived(new Date());
					crqs.update(currentItem);
					log.debug("new report received: " + currentItem.getExternalId() + " -> "
							+ currentItem.getReviewScore());
				}
			}
		}

		log.info("Finished fetching reports from Turnitin");
	}

	// returns null if no valid email exists
	public String getEmail(User user) {

		String uem = null;

		// Check account email address
		String account_email = null;

		if (isValidEmail(user.getEmail())) {
			account_email = user.getEmail().trim();
		}

		// Lookup system profile email address if necessary
		String profile_email = null;
		if (account_email == null || preferSystemProfileEmail) {
			SakaiPerson sp = sakaiPersonManager.getSakaiPerson(user.getId(), sakaiPersonManager.getSystemMutableType());
			if (sp != null && isValidEmail(sp.getMail())) {
				profile_email = sp.getMail().trim();
			}
		}

		// Check guest accounts and use eid as the email if preferred
		if (this.preferGuestEidEmail && isValidEmail(user.getEid())) {
			uem = user.getEid();
		}

		if (uem == null && preferSystemProfileEmail && profile_email != null) {
			uem = profile_email;
		}

		if (uem == null && account_email != null) {
			uem = account_email;
		}

		// Randomize the email address if preferred
		if (spoilEmailAddresses && uem != null) {
			// Scramble it
			String[] parts = uem.split("@");

			String emailName = parts[0];

			Random random = new Random();
			int int1 = random.nextInt();
			int int2 = random.nextInt();
			int int3 = random.nextInt();

			emailName += (int1 + int2 + int3);

			uem = emailName + "@" + parts[1];

			if (log.isDebugEnabled())
				log.debug("SCRAMBLED EMAIL:" + uem);
		}

		log.debug("Using email " + uem + " for user eid " + user.getEid() + " id " + user.getId());
		return uem;
	}

	/**
	 * Is this a valid email the service will recognize
	 * 
	 * @param email
	 * @return
	 */
	private boolean isValidEmail(String email) {

		// TODO: Use a generic Sakai utility class (when a suitable one exists)

		if (email == null || email.equals(""))
			return false;

		email = email.trim();
		// must contain @
		if (email.indexOf("@") == -1)
			return false;

		// an email can't contain spaces
		if (email.indexOf(" ") > 0)
			return false;

		// use commons-validator
		EmailValidator validator = EmailValidator.getInstance();
		if (validator.isValid(email))
			return true;

		return false;
	}

	// Methods for updating all assignments that exist
	public void doAssignments() {
		log.info("About to update all turnitin assignments");
		
		List items = crqs.getAllContentReviewItemsGroupedBySiteAndTask(getProviderId());
		
		for (ContentReviewItem item : items) {
			try {
				updateAssignment(item.getSiteId(), item.getTaskId());
			} catch (SubmissionException e) {
				log.error(e.getMessage(), e);
			}
		}
	}

	/**
	 * Update Assignment. This method is not currently called by Assignments 1.
	 */
	public void updateAssignment(String siteId, String taskId) throws SubmissionException {
		log.info("updateAssignment(" + siteId + " , " + taskId + ")");
		// get the assignment reference
		String taskTitle = getAssignmentTitle(taskId);
		log.debug("Creating assignment for site: " + siteId + ", task: " + taskId + " tasktitle: " + taskTitle);

		SimpleDateFormat dform = ((SimpleDateFormat) DateFormat.getDateInstance());
		dform.applyPattern(TURNITIN_DATETIME_FORMAT);
		Calendar cal = Calendar.getInstance();
		// set this to yesterday so we avoid timezpne problems etc
		cal.add(Calendar.DAY_OF_MONTH, -1);
		String dtstart = dform.format(cal.getTime());

		// set the due dates for the assignments to be in 5 month's time
		// turnitin automatically sets each class end date to 6 months after it
		// is created
		// the assignment end date must be on or before the class end date

		// TODO use the 'secret' function to change this to longer
		cal.add(Calendar.MONTH, 5);
		String dtdue = dform.format(cal.getTime());

		String fcmd = "3"; // new assignment
		String fid = "4"; // function id
		String utp = "2"; // user type 2 = instructor
		String s_view_report = "1";

		// erater
		String erater = "0";
		String ets_handbook = "1";
		String ets_dictionary = "en";
		String ets_spelling = "1";
		String ets_style = "1";
		String ets_grammar = "1";
		String ets_mechanics = "1";
		String ets_usage = "1";

		String cid = siteId;
		String assignid = taskId;
		String assign = taskTitle;
		String ctl = siteId;

		String assignEnc = assign;
		try {
			if (assign.contains("&")) {
				// log.debug("replacing & in assingment title");
				assign = assign.replace('&', 'n');

			}
			assignEnc = assign;
			log.debug("Assign title is " + assignEnc);

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(), "assign", assignEnc, "assignid",
				assignid, "cid", cid, "ctl", ctl, "dtdue", dtdue, "dtstart", dtstart, "fcmd", fcmd, "fid", fid,
				"s_view_report", s_view_report, "utp", utp, "erater", erater, "ets_handbook", ets_handbook,
				"ets_dictionary", ets_dictionary, "ets_spelling", ets_spelling, "ets_style", ets_style, "ets_grammar",
				ets_grammar, "ets_mechanics", ets_mechanics, "ets_usage", ets_usage);

		params.putAll(getInstructorInfo(siteId));

		Document document = null;

		try {
			document = turnitinConn.callTurnitinReturnDocument(params);
		} catch (TransientSubmissionException tse) {
			log.error("Error on API call in updateAssignment siteid: " + siteId + " taskid: " + taskId, tse);
			return;
		} catch (SubmissionException se) {
			log.error("Error on API call in updateAssignment siteid: " + siteId + " taskid: " + taskId, se);
			return;
		}

		Element root = document.getDocumentElement();
		int rcode = new Integer(
				((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim())
						.intValue();
		if ((rcode > 0 && rcode < 100) || rcode == 419) {
			log.debug("Create Assignment successful");
		} else {
			log.debug("Assignment creation failed with message: "
					+ ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim()
					+ ". Code: " + rcode);
			throw new SubmissionException(
				getFormattedMessage("assignment.creation.error.with.code",
					((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim(),
					rcode));
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sakaiproject.contentreview.service.ContentReviewService#
	 * isAcceptableContent(org.sakaiproject.content.api.ContentResource)
	 */
	public boolean isAcceptableContent(ContentResource resource) {
		return turnitinContentValidator.isAcceptableContent(resource, getAcceptableFileExtensions(), getAcceptableMimeTypes());
	}

	public String[] getAcceptableFileExtensions() {
		String[] extensions = serverConfigurationService.getStrings(PROP_ACCEPTABLE_FILE_EXTENSIONS);
		if (extensions != null && extensions.length > 0) {
			return extensions;
		}
		return DEFAULT_ACCEPTABLE_FILE_EXTENSIONS;
	}

	// TII-157	--bbailla2
	public String[] getAcceptableMimeTypes() {
		String[] mimeTypes = serverConfigurationService.getStrings(PROP_ACCEPTABLE_MIME_TYPES);
		if (mimeTypes != null && mimeTypes.length > 0) {
			return mimeTypes;
		}
		return DEFAULT_ACCEPTABLE_MIME_TYPES;
	}

	// TII-157	--bbailla2
	public String [] getAcceptableFileTypes() {
		return serverConfigurationService.getStrings(PROP_ACCEPTABLE_FILE_TYPES);
	}

	// TII-157	--bbailla2
	/**
	 * Inserts (key, value) into a Map> such that value is inserted into the value Set associated with key.
	 * The value set is implemented as a TreeSet, so the Strings will be in alphabetical order
	 * Eg. if we insert (a, b) and (a, c) into map, then map.get(a) will return {b, c}
	 */
	private void appendToMap(Map> map, String key, String value) {
		SortedSet valueList = map.get(key);
		if (valueList == null) {
			valueList = new TreeSet<>();
			map.put(key, valueList);
		}
		valueList.add(value);
	}
	/**
	 * find the next time this item should be tried
	 * 
	 * @param retryCount
	 * @return
	 */
	private Date getNextRetryTime(long retryCount) {
		int offset = 5;

		if (retryCount > 9 && retryCount < 20) {

			offset = 10;

		} else if (retryCount > 19 && retryCount < 30) {
			offset = 20;
		} else if (retryCount > 29 && retryCount < 40) {
			offset = 40;
		} else if (retryCount > 39 && retryCount < 50) {
			offset = 80;
		} else if (retryCount > 49 && retryCount < 60) {
			offset = 160;
		} else if (retryCount > 59) {
			offset = 220;
		}

		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.MINUTE, offset);
		return cal.getTime();
	}

	/**
	 * Gets a first name for a user or generates an initial from the eid
	 * 
	 * @param user
	 *            a sakai user
	 * @return the first name or at least an initial if possible, "X" if no fn
	 *         can be made
	 */
	private String getUserFirstName(User user) {
		String ufn = user.getFirstName().trim();
		if (ufn == null || ufn.equals("")) {
			boolean genFN = (boolean) serverConfigurationService.getBoolean("turnitin.generate.first.name", true);
			if (genFN) {
				String eid = user.getEid();
				if (eid != null && eid.length() > 0) {
					ufn = eid.substring(0, 1);
				} else {
					ufn = "X";
				}
			}
		}
		return ufn;
	}

	/**
	 * Get user last Name. If turnitin.generate.last.name is set to true last
	 * name is anonamised
	 * 
	 * @param user
	 * @return
	 */
	private String getUserLastName(User user) {
		String uln = user.getLastName().trim();
		if (uln == null || uln.equals("")) {
			boolean genLN = serverConfigurationService.getBoolean("turnitin.generate.last.name", false);
			if (genLN) {
				String eid = user.getEid();
				if (eid != null && eid.length() > 0) {
					uln = eid.substring(0, 1);
				} else {
					uln = "X";
				}
			}
		}
		return uln;
	}

	private ResourceLoader getResourceLoader(String userRef) {
		String userId = EntityReference.getIdFromRef(userRef);
		return new ResourceLoader(userId, "turnitin");
	}

	public String getFormattedMessage(String key, Object... args) {
		return getResourceLoader().getFormattedMessage(key, args);
	}

	public String getLocalizedStatusMessage(String messageCode, String userRef) {
		return getResourceLoader(userRef).getString(messageCode);
	}

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

	public String getLocalizedStatusMessage(String messageCode) {
		return getLocalizedStatusMessage(messageCode, userDirectoryService.getCurrentUser().getReference());
	}

	public String getLocalizedStatusMessage(String messageCode, Locale locale) {
		// TODO not sure how to do this with the sakai resource loader
		return null;
	}

	public String getLocalizedReviewErrorMessage(String contentId) {
		log.debug("Returning review error for content: " + contentId);

		Optional item = crqs.getQueuedItem(getProviderId(), contentId);

		if (item.isPresent()) {
			// its possible the error code column is not populated
			Integer errorCode = item.get().getErrorCode();
			if (errorCode != null) {
				return getLocalizedStatusMessage(errorCode.toString());
			}
			return item.get().getLastError();
		}

		log.debug("Content " + contentId + " has not been queued previously");
		return null;
	}

	private String getTEM(String cid) {
		if (turnitinConn.isUseSourceParameter()) {
			return getInstructorInfo(cid).get("uem").toString();
		} else {
			return turnitinConn.getDefaultInstructorEmail();
		}
	}

	/**
	 * This will return a map of the information for the instructor such as uem,
	 * username, ufn, etc. If the system is configured to use src9 provisioning,
	 * this will draw information from the current thread based user. Otherwise
	 * it will use the default Instructor information that has been configured
	 * for the system.
	 *
	 * @return
	 */
	public Map getInstructorInfo(String siteId) {
		return getInstructorInfo(siteId, false);
	}

	public Map getInstructorInfo(String siteId, boolean ignoreUseSource) {

		log.debug("Getting instructor info for site " + siteId);

		Map togo = new HashMap<>();
		if (!turnitinConn.isUseSourceParameter() && !ignoreUseSource) {
			togo.put("uem", turnitinConn.getDefaultInstructorEmail());
			togo.put("ufn", turnitinConn.getDefaultInstructorFName());
			togo.put("uln", turnitinConn.getDefaultInstructorLName());
			togo.put("uid", turnitinConn.getDefaultInstructorId());
		} else {
			User inst = null;
			try {
				Site site = siteService.getSite(siteId);
				User user = userDirectoryService.getCurrentUser();

				log.debug("Current user: " + user.getId());

				if (site.isAllowed(user.getId(), INST_ROLE)) {
					inst = user;
				} else {
					Set instIds = getActiveUsersForRole(INST_ROLE, site);
					if (instIds.size() > 0) {
						inst = userDirectoryService.getUser((String) instIds.toArray()[0]);
					}
				}
			} catch (IdUnusedException e) {
				log.error("Unable to fetch site in getAbsoluteInstructorInfo: " + siteId, e);
			} catch (UserNotDefinedException e) {
				log.error("Unable to fetch user in getAbsoluteInstructorInfo", e);
			}

			if (inst == null) {
				log.error("Instructor is null in getAbsoluteInstructorInfo");
			} else {
				togo.put("uem", StringUtils.trimToEmpty(getEmail(inst)));
				togo.put("ufn", StringUtils.trimToEmpty(inst.getFirstName()));
				togo.put("uln", StringUtils.trimToEmpty(inst.getLastName()));
				togo.put("uid", StringUtils.trimToEmpty(inst.getId()));
				togo.put("username", StringUtils.trimToEmpty(inst.getDisplayName()));
			}
		}

		return togo;
	}

	private Set getActiveUsersForRole(final String rolePermission, final String siteId) {
		try {
			Site site = siteService.getSite(siteId);
			return getActiveUsersForRole(rolePermission, site);
		} catch (IdUnusedException e) {
			log.info("Ignoring site " + siteId + " which no longer exists.");
		}

		return new HashSet();
	}

	private Set getActiveUsersForRole(final String rolePermission, final Site site) {
		log.debug("Getting active IDs for permission {} in site={}", INST_ROLE, site.getId());

		Set userIds = site.getUsersIsAllowed(rolePermission);

		// the site could contain references to deleted users
		List activeUsers = userDirectoryService.getUsers(userIds);
		Set ret = new HashSet();
		for (int i = 0; i < activeUsers.size(); i++) {
			User user = activeUsers.get(i);
			// Ignore users who do not have a first and/or last name set or do
			// not have
			// a valid email address, as this will cause a TII API call to fail
			if (user.getFirstName() != null && !user.getFirstName().trim().isEmpty() && user.getLastName() != null
					&& !user.getLastName().trim().isEmpty() && getEmail(user) != null) {
				ret.add(user.getId());
			}
		}

		return ret;
	}

	@Override
	public boolean allowAllContent() {
		// Turntin reports errors when content is submitted that it can't check originality against. So we will block unsupported content.
		return serverConfigurationService.getBoolean(PROP_ACCEPT_ALL_FILES, false);
	}

	@Override
	public Map> getAcceptableExtensionsToMimeTypes() {
		Map> acceptableExtensionsToMimeTypes = new HashMap<>();
		String[] acceptableFileExtensions = getAcceptableFileExtensions();
		String[] acceptableMimeTypes = getAcceptableMimeTypes();
		int min = Math.min(acceptableFileExtensions.length, acceptableMimeTypes.length);
		for (int i = 0; i < min; i++) {
			appendToMap(acceptableExtensionsToMimeTypes, acceptableFileExtensions[i], acceptableMimeTypes[i]);
		}

		return acceptableExtensionsToMimeTypes;
	}

	@Override
	public Map> getAcceptableFileTypesToExtensions() {
		Map> acceptableFileTypesToExtensions = new LinkedHashMap<>();
		String[] acceptableFileTypes = getAcceptableFileTypes();
		String[] acceptableFileExtensions = getAcceptableFileExtensions();
		if (acceptableFileTypes != null && acceptableFileTypes.length > 0) {
			// The acceptable file types are listed in sakai.properties. Sakai.properties takes precedence.
			int min = Math.min(acceptableFileTypes.length, acceptableFileExtensions.length);
			for (int i = 0; i < min; i++) {
				appendToMap(acceptableFileTypesToExtensions, acceptableFileTypes[i], acceptableFileExtensions[i]);
			}
		}
		else {
			/*
			 * acceptableFileTypes not specified in sakai.properties (this is normal).
			 * Use ResourceLoader to resolve the file types.
			 * If the resource loader doesn't find the file extenions, log a warning and return the [missing key...] messages
			 */
			ResourceLoader resourceLoader = getResourceLoader();
			for( String fileExtension : acceptableFileExtensions ) {
				String key = KEY_FILE_TYPE_PREFIX + fileExtension;
				if (!resourceLoader.getIsValid(key)) {
					log.warn("While resolving acceptable file types for Turnitin, the sakai.property " + PROP_ACCEPTABLE_FILE_TYPES + " is not set, and the message bundle " + key + " could not be resolved. Displaying [missing key ...] to the user");
				}
				String fileType = resourceLoader.getString(key);
				appendToMap( acceptableFileTypesToExtensions, fileType, fileExtension );
			}
		}

		return acceptableFileTypesToExtensions;
	}

	@Override
	public void queueContent(String userId, String siteId, String taskId, List content)
			throws QueueException {

		log.debug("Method called queueContent()");

		if (content == null || content.isEmpty()) {
			return;
		}

		if (userId == null) {
			log.debug("Using current user");
			userId = userDirectoryService.getCurrentUser().getId();
		}

		if (siteId == null) {
			log.debug("Using current site");
			siteId = toolManager.getCurrentPlacement().getContext();
		}

		if (taskId == null) {
			log.debug("Generating default taskId");
			taskId = siteId + " " + "defaultAssignment";
		}

		log.debug("Adding content from site " + siteId + " and user: " + userId + " for task: " + taskId
				+ " to submission queue");
		crqs.queueContent(getProviderId(), userId, siteId, taskId, content);
	}

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

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

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

	@Override
	public List getReportList(String siteId, String taskId)
			throws QueueException, SubmissionException, ReportException {
		return crqs.getContentReviewItems(getProviderId(), siteId, taskId);
	}

	@Override
	public List getReportList(String siteId)
			throws QueueException, SubmissionException, ReportException {
		return getReportList(siteId, null);
	}

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

	@Override
	public void resetUserDetailsLockedItems(String userId) {
		crqs.resetUserDetailsLockedItems(getProviderId(), userId);
	}

	@Override
	public boolean allowResubmission() {
		return true;
	}

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

	@Override
	public ContentReviewItem getContentReviewItemByContentId(String contentId){
		Optional cri = getItemByContentId(contentId);
		if(cri.isPresent()){
			ContentReviewItem item = cri.get();
			
			//TII specific work
		
			// Sync Grades
			if (turnitinConn.getUseGradeMark()) {
				try {				
					String[] assignData = getAssignData(contentId);
					String siteId = "", taskId = "", taskTitle = "";
					Map data = new HashMap();
					if (assignData != null) {
						siteId = assignData[0];
						taskId = assignData[1];
						taskTitle = assignData[2];
					} else {
						siteId = item.getSiteId();
						taskId = item.getTaskId();
						taskTitle = getAssignmentTitle(taskId);
						data.put("assignment1", "assignment1");
					}
					data.put("siteId", siteId);
					data.put("taskId", taskId);
					data.put("taskTitle", taskTitle);
					syncGrades(data);
				} catch (Exception e) {
					log.error("Error syncing grades. " + e);
				}
			}

			return item;
		} else {
			log.debug("Content " + contentId + " has not been queued previously");
		}
		return null;
	}

	@Override
	public String getEndUserLicenseAgreementLink(String userId) {
		return null;
	}

	@Override
	public Instant getEndUserLicenseAgreementTimestamp() {
		return null;
	}

	@Override
	public String getEndUserLicenseAgreementVersion() {
		return null;
	}
	
	@Override
	public void webhookEvent(HttpServletRequest request, int providerId, Optional customParam) {
		//Auto-generated method stub
	}

	/**
	 * This method takes a Sakai Site ID and returns the xml document from
	 * Turnitin that lists all the instructors and students for that Turnitin
	 * course.
	 *
	 * @param sakaiSiteID
	 * @return
	 */
	private Document getEnrollmentDocument(String sakaiSiteID) {
		Map instinfo = getInstructorInfo(sakaiSiteID);

		Map params = TurnitinAPIUtil.packMap(null,
				"fid","19",
				"fcmd","5",
				"utp","2",
				"ctl",sakaiSiteID,
				"cid",sakaiSiteID,
				"src","9",
				"encrypt","0"
				);
		params.putAll(instinfo);

		Document togo = null;
		try {
			togo = turnitinConn.callTurnitinWDefaultsReturnDocument(params);
		} catch (SubmissionException e) {
			log.error("Error getting enrollment document for sakai site: "
					+ sakaiSiteID, e);
		} catch (TransientSubmissionException e) {
			log.error("Error getting enrollment document for sakai site: "
					+ sakaiSiteID, e);
		}
		return togo;
	}

	/**
	 * This will make an API call to Turnitin to fetch the list of instructors
	 * and students for the site.  Remember that in Turnitin, a user can be
	 * both a student and an instructor.
	 *
	 * @param sakaiSiteID
	 * @return An Map. The first element is a List of instructor ids,
	 * the second element is a List of student ids.
	 */
	private Map> getInstructorsStudentsForSite(String sakaiSiteID) {

		List instructorIds = new ArrayList();
		List studentIds = new ArrayList();
		Document doc = getEnrollmentDocument(sakaiSiteID);

		if (doc == null) {
			return null;
		}

		NodeList instructors = doc.getElementsByTagName("instructor");

		for (int i = 0; i < instructors.getLength(); i++) {
			Element nextInst = (Element) instructors.item(i);
			String instUID = nextInst.getElementsByTagName("uid").item(0).getTextContent();
			instructorIds.add(instUID);
		}

		NodeList students = doc.getElementsByTagName("student");

		for (int i = 0 ; i < students.getLength(); i++) {
			Element nextStud = (Element) students.item(i);
			String studUID = nextStud.getElementsByTagName("uid").item(0).getTextContent();
			studentIds.add(studUID);
		}

		Map> togo = new HashMap<>();
		togo.put("instructor", instructorIds);
		togo.put("student", studentIds);

		return togo;
	}

	/**
	 * This method swap a users role in a Turnitin site. The currentRole should
	 * be accurate for the users current information otherwise the method may
	 * fail (this all depends on calls to Turnitin's Webservice API's). So if
	 * you pass in a site, a user, and the value 1 (student) that user should be
	 * switched to an instructor in that site.
	 *
	 * @param siteId
	 * @param user
	 * @param currentRole The current role using Turnitin codes. In Turnitin a
	 * value of 1 always represents a student and a value of 2 represents an
	 * instructor.
	 * @return
	 */
	private boolean swapTurnitinRoles(String siteId, User user, int currentRole ) {

		if (user != null) {
			String uem = getEmail(user);
			Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(),
					"fid","19","fcmd", "3", "uem", uem, "uid", user.getId(),
					"ufn", user.getFirstName(), "uln", user.getLastName(),
					"username", user.getDisplayName(), "ctl", siteId, "cid", siteId,
					"utp", currentRole+"",
					"tem", getInstructorInfo(siteId).get("uem"));

			Map ret = new HashMap();
			try {
				ret = turnitinConn.callTurnitinWDefaultsReturnMap(params);
			} catch (SubmissionException e) {
				log.error("Error syncing Turnitin site: " + siteId + " user: " + user.getEid(), e);
			} catch (TransientSubmissionException e) {
				log.error("Error syncing Turnitin site: " + siteId + " user: " + user.getEid(), e);
			}

			// A Successful return should look like:
			// {rmessage=Successful!, rcode=93}
			if (ret.containsKey("rcode") && ret.get("rcode").equals("93")) {
				log.info("Successfully swapped user roles for site: " + siteId + " user: " + user.getEid() + " oldRole: " + currentRole);
				return true;
			}

			// Special case: ignore return code 450 (unable to swap role because the user is the only instructor)
			// This is typically because there is more than one Sakai user in the site with the same email address,
			// and Turnitin is not distinguishing between them despite the uid being supplied.
			if (ret.containsKey("rcode") && ret.get("rcode").equals("450")) {
				log.info("Response code 450 (user is only instructor in the site); not changing user role for site: " + siteId + " user: " + user.getEid());
				return true;
			}
		}
		else {
			// This was successful because the user doesn't exist in our Sakai
			// installation, and so we don't need to sync them at all.
			return true;
		}

		log.warn("Unable to swap user roles for site: " + siteId + " user: " + user.getEid() + " oldRole: " + currentRole);
		return false;
	}

	/**
	 * Add an instructor to a class in Turnitin, allowing them to properly
	 * access assignments created by other instructors. (Only applicable to SRC 9)
	 * @param siteId Sakai site ID
	 * @param userId Sakai User ID
	 * @throws SubmissionException
	 * @throws TransientSubmissionException
	 */
	@SuppressWarnings("unchecked")
	private void addInstructor(String siteId, String userId) throws SubmissionException, TransientSubmissionException {
		log.info("Adding Instructor ("+userId+") to site: " + siteId);
		User user;
		try {
			user = userDirectoryService.getUser(userId);
		} catch (Exception t) {
			throw new SubmissionException ("(addInstructor)Cannot get user information.", t);
		}
		String cpw = turnitinConn.getDefaultClassPassword();
		String ctl = siteId;
		String fcmd = "2";
		String fid = "2";
		String utp = "2";
		String cid = siteId;
		String uem = getEmail(user);
		if (uem == null || uem.trim().isEmpty()) {
			log.debug("User " + userId + " has no email address");
			throw new SubmissionException ("User has no email address");
		}
		String uid = user.getId();
		String ufn = user.getFirstName();
		if (ufn == null || ufn.trim().isEmpty()) {
			log.debug("User " + userId + " has no first name");
			throw new SubmissionException ("User has no first name");
		}
		String uln = user.getLastName();
		if (uln == null || uln.trim().isEmpty()) {
			log.debug("User " + userId + " has no last name");
			throw new SubmissionException ("User has no last name");
		}
		String dis = turnitinConn.isInstructorAccountNotified() ? "0" : "1"; // dis=1 means disable sending email to the user

		Document document = null;

		Map params = TurnitinAPIUtil.packMap(turnitinConn.getBaseTIIOptions(),
				"uid", uid,
				"cid", cid,
				"cpw", cpw,
				"ctl", ctl,
				"fcmd", fcmd,
				"fid", fid,
				"uem", uem,
				"ufn", ufn,
				"uln", uln,
				"utp", utp,
				"dis", dis
				);
		document = turnitinConn.callTurnitinReturnDocument(params);

		Element root = document.getDocumentElement();
		String rcode = ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim();

		if (((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim().compareTo("20") == 0 ||
				((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim().compareTo("21") == 0 ) {
			log.debug("Add instructor successful");
		} else {
			if ("218".equals(rcode) || "9999".equals(rcode)) {
				throw new TransientSubmissionException("Create Class not successful. Message: " + ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim() + ". Code: " + ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim());
			} else {
				throw new SubmissionException("Create Class not successful. Message: " + ((CharacterData) (root.getElementsByTagName("rmessage").item(0).getFirstChild())).getData().trim() + ". Code: " + ((CharacterData) (root.getElementsByTagName("rcode").item(0).getFirstChild())).getData().trim());
			}
		}
	}
	/**
	 * Looks up a Sakai {@link org.sakaiproject.user.api.User} by userid,
	 * returns null if they do not exist.
	 *
	 * @param userid
	 * @return the User object or null if the user does not exist in the Sakai
	 * installation.
	 */
	private User getUser(String userid) {
		User user = null;
		try {
			user = userDirectoryService.getUser(userid);
		} catch (UserNotDefinedException e) {
			log.warn("Attemping to lookup user for Turnitn Sync that does not exist: " + userid, e);
		}
		return user;
	}

	/**
	 * The primary method of this class. Syncs the enrollment between a Sakai
	 * Site and it's corresponding
	 *
	 * @param sakaiSiteID
	 */
	private boolean syncSiteWithTurnitin(final String sakaiSiteID) {
		boolean success = true;

		Site site = null;
		try {
			site = siteService.getSite(sakaiSiteID);
		} catch (IdUnusedException e) {
			log.info("Ignoring site " + sakaiSiteID + " which no longer exists.");
			return true;
		}

		Map> enrollment = getInstructorsStudentsForSite(sakaiSiteID);

		if (enrollment == null) {
			return false;
		}

		// Only run if using SRC 9
		if (turnitinConn.isUseSourceParameter()) {
			// Enroll all instructors
			log.debug("Enrolling all instructors in site={}", sakaiSiteID);
			Set instIds = getActiveUsersForRole(INST_ROLE, site);

			if (enrollAssistantsAsInstructors) {
				instIds.addAll(getActiveUsersForRole(TA_ROLE, site));
			}

			for (String key : instIds) {
				try {
					addInstructor(sakaiSiteID, key);
				} catch (SubmissionException e) {
					log.warn("SubmissionException from syncSiteWithTurnitin", e);
				} catch(TransientSubmissionException e){
					log.warn("TransientSubmissionException from syncSiteWithTurnitin", e);
				} catch(Exception e){
					log.error("Unknown error", e);
				}
			}
		} else {
			log.debug("Checking users with Turnitin student role for site={}", sakaiSiteID);
			for (String uid: enrollment.get("student")) {
				if (site.isAllowed(uid, INST_ROLE)) {
					// User has an instructor role in Sakai - change Turnitin role from student to instructor
					boolean status = swapTurnitinRoles(sakaiSiteID, getUser(uid), 1);
					if (status == false) {
						success = false;
					}
				}
			}
		}

		log.debug("Checking users with Turnitin instructor role for site={}", sakaiSiteID);
		for (String uid: enrollment.get("instructor")) {
			if (!site.isAllowed(uid, INST_ROLE)) {
				// User does not have instructor role in Sakai - change Turnitin role from instructor to student
				boolean status = swapTurnitinRoles(sakaiSiteID, getUser(uid), 2);
				if (status == false) {
					success = false;
				}
			}
		}

		return success;
	}

	/**
	 * This is the main processing method that's meant to be periodically run
	 * by a quartz job or other script. It will sync all the Sakai Sites that
	 * have been put in the queue due to site updates or something.
	 */
	public void processSyncQueue() {

		log.info("Running Turnitin Roster Sync");

		List items = crqs.getContentReviewItemsGroupedBySite(getProviderId());
		for (String siteId : items) {
			log.debug("Turnitin roster sync site: {}", siteId);
			try {
				syncSiteWithTurnitin(siteId);
			} catch (Exception e) {
				log.error("Unable to complete Turnitin Roster Sync for site", e);
			}
		}

		log.info("Completed Turnitin Roster Sync");
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy