org.sakaiproject.contentreview.turnitin.oc.ContentReviewServiceTurnitinOC Maven / Gradle / Ivy
/**
* 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.oc;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.sakaiproject.assignment.api.AssignmentConstants;
import org.sakaiproject.assignment.api.AssignmentService;
import org.sakaiproject.assignment.api.model.Assignment;
import org.sakaiproject.assignment.api.model.AssignmentSubmission;
import org.sakaiproject.assignment.api.model.AssignmentSubmissionSubmitter;
import org.sakaiproject.authz.api.AuthzGroupService;
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.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.service.ContentReviewQueueService;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.memory.api.SimpleConfiguration;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.util.ResourceLoader;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
@Slf4j
public class ContentReviewServiceTurnitinOC extends BaseContentReviewService {
@Setter
private UserDirectoryService userDirectoryService;
@Setter
private EntityManager entityManager;
@Setter
private SecurityService securityService;
@Setter
private AssignmentService assignmentService;
@Setter
private SiteService siteService;
@Setter
private ContentReviewQueueService crqs;
@Setter
private ContentHostingService contentHostingService;
@Setter
private SessionManager sessionManager;
@Setter
private MemoryService memoryService;
@Setter
private AuthzGroupService authzGroupService;
private static final String SERVICE_NAME = "Turnitin";
private static final String TURNITIN_OC_API_VERSION = "v1";
private static final String INTEGRATION_FAMILY = "sakai";
private static final String CONTENT_TYPE_JSON = "application/json";
private static final String CONTENT_TYPE_BINARY = "application/octet-stream";
private static final String HEADER_NAME = "X-Turnitin-Integration-Name";
private static final String HEADER_VERSION = "X-Turnitin-Integration-Version";
private static final String HEADER_AUTH = "Authorization";
private static final String HEADER_CONTENT = "Content-Type";
private static final String HEADER_DISP = "Content-Disposition";
private static final String HTML_EXTENSION = ".html";
private static final String STATUS_CREATED = "CREATED";
private static final String STATUS_COMPLETE = "COMPLETE";
private static final String STATUS_PROCESSING = "PROCESSING";
private static final String RESPONSE_CODE = "responseCode";
private static final String RESPONSE_MESSAGE = "responseMessage";
private static final String RESPONSE_BODY = "responseBody";
private static final String GIVEN_NAME = "given_name";
private static final String FAMILY_NAME = "family_name";
private static final String VIEWER_USER_ID = "viewer_user_id";
private static final String AUTHOR_METADATA_OVERRIDE = "author_metadata_override";
private static final String MATCH_OVERVIEW = "match_overview";
private static final String ALL_SOURCES = "all_sources";
private static final String MODES = "modes";
private static final String SIMILARITY = "similarity";
private static final String SAVE_CHANGES = "save_changes";
private static final String VIEW_SETTINGS = "view_settings";
private static final String VIEWER_PERMISSIONS = "viewer_permissions";
private static final String VIEWER_PERMISSION_MAY_VIEW_SUBMISSIONS_FULL_SOURCE = "may_view_submission_full_source";
private static final String VIEWER_PERMISSION_MAY_VIEW_MATCH_SUBMISSION_INFO = "may_view_match_submission_info";
private static final String VIEWER_DEFAULT_PERMISSIONS = "viewer_default_permission_set";
private enum ROLES{
INSTRUCTOR(Arrays.asList("Faculty", "Instructor", "Mentor", "Staff", "maintain", "Teaching Assistant"), Boolean.TRUE),
LEARNER(Arrays.asList("Learner", "Student", "access"), Boolean.FALSE),
EDITOR(Arrays.asList(), Boolean.FALSE),
USER(Arrays.asList("Alumni", "guest", "Member", "Observer", "Other"), Boolean.FALSE),
APPLICANT(Arrays.asList("ProspectiveStudent"), Boolean.FALSE),
ADMINISTRATOR(Arrays.asList("Administrator", "Admin"), Boolean.TRUE),
UNDEFINED(Arrays.asList(), Boolean.FALSE);
List mappedRoles;
Boolean maySaveReportChanges;
private ROLES(List mappedRoles, Boolean maySaveReportChanges) {
this.mappedRoles = mappedRoles;
this.maySaveReportChanges = maySaveReportChanges;
}
private void setMappedRoles(List mappedRoles) {
this.mappedRoles = mappedRoles;
}
private List getMappedRoles() {
return mappedRoles;
}
public Boolean getMaySaveReportChanges() {
return maySaveReportChanges;
}
public void setMaySaveReportChanges(Boolean maySaveReportChanges) {
this.maySaveReportChanges = maySaveReportChanges;
}
};
private static final String GENERATE_REPORTS_IMMEDIATELY_AND_ON_DUE_DATE= "1";
private static final String GENERATE_REPORTS_ON_DUE_DATE = "2";
private static final String PLACEHOLDER_STRING_FLAG = "_placeholder";
private static final Integer PLACEHOLDER_ITEM_REVIEW_SCORE = -10;
private static final String COMPLETE_STATUS = "COMPLETE";
private static final String CREATED_STATUS = "CREATED";
private static final String PROCESSING_STATUS = "PROCESSING";
private static final String SUBMISSION_COMPLETE_EVENT_TYPE = "SUBMISSION_COMPLETE";
private static final String SIMILARITY_COMPLETE_EVENT_TYPE = "SIMILARITY_COMPLETE";
private static final String SIMILARITY_UPDATED_EVENT_TYPE = "SIMILARITY_UPDATED";
private String serviceUrl;
private String apiKey;
private String sakaiVersion;
private int maxRetryMinutes;
private int maxRetry;
private boolean skipDelays;
private HashMap BASE_HEADERS = new HashMap();
private HashMap SUBMISSION_REQUEST_HEADERS = new HashMap();
private HashMap SIMILARITY_REPORT_HEADERS = new HashMap();
private HashMap CONTENT_UPLOAD_HEADERS = new HashMap();
private HashMap WEBHOOK_SETUP_HEADERS = new HashMap();
private enum AUTO_EXCLUDE_SELF_MATCHING_SCOPE{
ALL,
NONE,
GROUP,
GROUP_CONTEXT
}
//Caches requests for instructors so that we don't have to send a request for every student
private Cache EULA_CACHE;
private static final String EULA_LATEST_KEY = "latest";
private static final String EULA_DEFAULT_LOCALE = "en-US";
// Define Turnitin's acceptable file extensions and MIME types, order of these arrays DOES matter
private final String[] DEFAULT_ACCEPTABLE_FILE_EXTENSIONS = new String[] {
".pdf",
".doc",
".ppt",
".pps",
".xls",
".doc",
".docx",
".ppt",
".pptx",
".ppsx",
".pps",
".pptx",
".ppsx",
".xlsx",
".xls",
".ps",
".rtf",
".doc",
".rtf",
".doc",
".htm",
".html",
".wpd",
".odt",
".txt"
};
private final String[] DEFAULT_ACCEPTABLE_MIME_TYPES = new String[] {
"application/pdf",
"application/msword",
"application/vnd.ms-powerpoint",
"application/vnd.ms-powerpoint",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/postscript",
"text/rtf",
"text/rtf",
"application/rtf",
"application/rtf",
"text/html",
"text/html",
"application/wordperfect",
"application/vnd.oasis.opendocument.text",
"text/plain"
};
// Sakai.properties overriding the arrays above
private final String PROP_ACCEPT_ALL_FILES = "turnitin.oc.accept.all.files";
private final String PROP_ACCEPTABLE_FILE_EXTENSIONS = "turnitin.oc.acceptable.file.extensions";
private final String PROP_ACCEPTABLE_MIME_TYPES = "turnitin.oc.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.oc.acceptable.file.types";
private final String KEY_FILE_TYPE_PREFIX = "file.type";
private String autoExcludeSelfMatchingScope;
private Boolean mayViewSubmissionFullSourceOverrideStudent = null;
private Boolean mayViewMatchSubmissionInfoOverrideStudent = null;
private Boolean mayViewSubmissionFullSourceOverrideInstructor = null;
private Boolean mayViewMatchSubmissionInfoOverrideInstructor = null;
public void init() {
EULA_CACHE = memoryService.createCache("org.sakaiproject.contentreview.turnitin.oc.ContentReviewServiceTurnitinOC.LATEST_EULA_CACHE", new SimpleConfiguration<>(10000, 24 * 60 * 60, -1));
// Retrieve Service URL and API key
serviceUrl = serverConfigurationService.getString("turnitin.oc.serviceUrl", "");
apiKey = serverConfigurationService.getString("turnitin.oc.apiKey", "");
// Retrieve Sakai Version if null set default
sakaiVersion = serverConfigurationService.getString("version.sakai", "UNKNOWN");
// Maximum delay between retries after recoverable errors
maxRetryMinutes = serverConfigurationService.getInt("turnitin.oc.max.retry.minutes", 240); // 4 hours
// Maximum number of retries for recoverable errors
maxRetry = serverConfigurationService.getInt("turnitin.oc.max.retry", 16);
// For local development only; do not set this in production:
skipDelays = serverConfigurationService.getBoolean("turnitin.oc.skip.delays", false);
autoExcludeSelfMatchingScope =Arrays.stream(AUTO_EXCLUDE_SELF_MATCHING_SCOPE.values())
.filter(e -> e.name().equalsIgnoreCase(serverConfigurationService.getString("turnitin.oc.auto_exclude_self_matching_scope")))
.findAny().orElse(AUTO_EXCLUDE_SELF_MATCHING_SCOPE.GROUP_CONTEXT).name();
log.info("Exclude Scope: " + autoExcludeSelfMatchingScope);
// Find any permission overrides, if not set, set value to null to skip overrides
mayViewSubmissionFullSourceOverrideStudent = StringUtils.isNotEmpty(serverConfigurationService.getString("turnitin.oc.may_view_submission_full_source.student"))
? serverConfigurationService.getBoolean("turnitin.oc.may_view_submission_full_source.student", false)
: null;
mayViewMatchSubmissionInfoOverrideStudent = StringUtils.isNotEmpty(serverConfigurationService.getString("turnitin.oc.may_view_match_submission_info.student"))
? serverConfigurationService.getBoolean("turnitin.oc.may_view_match_submission_info.student", false)
: null;
mayViewSubmissionFullSourceOverrideInstructor = StringUtils.isNotEmpty(serverConfigurationService.getString("turnitin.oc.may_view_submission_full_source.instructor"))
? serverConfigurationService.getBoolean("turnitin.oc.may_view_submission_full_source.instructor", false)
: null;
mayViewMatchSubmissionInfoOverrideInstructor = StringUtils.isNotEmpty(serverConfigurationService.getString("turnitin.oc.may_view_match_submission_info.instructor"))
? serverConfigurationService.getBoolean("turnitin.oc.may_view_match_submission_info.instructor", false)
: null;
// Override default Sakai->Turnitin roles mapping
for(ROLES role : ROLES.values()) {
//map Sakai roles to Turnitin roles
role.setMappedRoles(serverConfigurationService.getStringList("turnitin.oc.roles." + role.name().toLowerCase() + ".mapping", role.getMappedRoles()));
//set maySaveReportChanges permission for each role
role.setMaySaveReportChanges(serverConfigurationService.getBoolean("turnitin.oc.roles." + role.name().toLowerCase() + ".may_save_report_changes", role.getMaySaveReportChanges()));
}
// Populate base headers that are needed for all calls to TCA
BASE_HEADERS.put(HEADER_NAME, INTEGRATION_FAMILY);
BASE_HEADERS.put(HEADER_VERSION, sakaiVersion);
BASE_HEADERS.put(HEADER_AUTH, "Bearer " + apiKey);
// Populate submission request headers used in getSubmissionId
SUBMISSION_REQUEST_HEADERS.putAll(BASE_HEADERS);
SUBMISSION_REQUEST_HEADERS.put(HEADER_CONTENT, CONTENT_TYPE_JSON);
// Populate similarity report headers used in generateSimilarityReport
SIMILARITY_REPORT_HEADERS.putAll(BASE_HEADERS);
SIMILARITY_REPORT_HEADERS.put(HEADER_CONTENT, CONTENT_TYPE_JSON);
// Populate webhook generation headers used in setupWebhook
WEBHOOK_SETUP_HEADERS.putAll(BASE_HEADERS);
WEBHOOK_SETUP_HEADERS.put(HEADER_CONTENT, CONTENT_TYPE_JSON);
// Populate content upload headers used in uploadExternalContent
CONTENT_UPLOAD_HEADERS.putAll(BASE_HEADERS);
CONTENT_UPLOAD_HEADERS.put(HEADER_CONTENT, CONTENT_TYPE_BINARY);
if(StringUtils.isNotEmpty(apiKey) && StringUtils.isNotEmpty(serviceUrl)) {
try {
// Get the webhook url
String webhookUrl = getWebhookUrl(Optional.empty());
boolean webhooksSetup = false;
// Check to see if any webhooks have already been set up for this url
for (Webhook webhook : getWebhooks()) {
log.info("Found webhook: " + webhook.getUrl());
if (StringUtils.isNotEmpty(webhook.getUrl()) && webhook.getUrl().equals(webhookUrl)) {
webhooksSetup = true;
break;
}
}
if (!webhooksSetup) {
// No webhook set up for this url, set one up
log.info("No matching webhook for " + webhookUrl);
String id = setupWebhook(webhookUrl);
if(StringUtils.isNotEmpty(id)) {
log.info("successfully created webhook: " + id);
}
}
} catch (Exception e) {
log.error(e.getLocalizedMessage(), e);
}
}
}
public String setupWebhook(String webhookUrl) throws Exception {
String id = null;
Map data = new HashMap();
List types = new ArrayList<>();
types.add(SIMILARITY_UPDATED_EVENT_TYPE);
types.add(SIMILARITY_COMPLETE_EVENT_TYPE);
types.add(SUBMISSION_COMPLETE_EVENT_TYPE);
data.put("signing_secret", base64Encode(apiKey));
data.put("url", webhookUrl);
data.put("description", "Sakai " + sakaiVersion);
data.put("allow_insecure", false);
data.put("event_types", types);
HashMap response = makeHttpCall("POST",
getNormalizedServiceUrl() + "webhooks",
WEBHOOK_SETUP_HEADERS,
data,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
String error = null;
if ((responseCode >= 200) && (responseCode < 300) && (responseBody != null)) {
// create JSONObject from responseBody
JSONObject responseJSON = JSONObject.fromObject(responseBody);
if (responseJSON.has("id")) {
id = responseJSON.getString("id");
} else {
error = "returned with no ID: " + responseJSON;
}
}else {
error = responseMessage;
}
if(StringUtils.isEmpty(id)) {
log.info("Error setting up webhook: " + error);
}
return id;
}
public ArrayList getWebhooks() throws Exception {
ArrayList webhooks = new ArrayList<>();
HashMap response = makeHttpCall("GET",
getNormalizedServiceUrl() + "webhooks",
BASE_HEADERS,
null,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
if(StringUtils.isNotEmpty(responseBody)
&& responseCode >= 200
&& responseCode < 300
&& !"[]".equals(responseBody)) {
// Loop through response via JSON, convert objects to Webhooks
JSONArray webhookList = JSONArray.fromObject(responseBody);
for (int i=0; i < webhookList.size(); i++) {
JSONObject webhookJSON = webhookList.getJSONObject(i);
if (webhookJSON.has("id") && webhookJSON.has("url")) {
webhooks.add(new Webhook(webhookJSON.getString("id"), webhookJSON.getString("url")));
}
}
}else {
log.info("getWebhooks: " + responseMessage);
}
return webhooks;
}
public boolean allowResubmission() {
return true;
}
@Override
public void checkForReports() {
// Auto-generated method stub
}
@Override
public void syncRosters() {
// Auto-generated method stub
}
@Override
public void createAssignment(final String contextId, final String assignmentRef, final Map opts)
throws SubmissionException, TransientSubmissionException {
// Auto-generated method stub
}
public List getAllContentReviewItems(String siteId, String taskId)
throws QueueException, SubmissionException, ReportException {
return crqs.getContentReviewItems(getProviderId(), siteId, taskId);
}
public Map getAssignment(String arg0, String arg1) throws SubmissionException, TransientSubmissionException {
return null;
}
public Date getDateQueued(String contextId) throws QueueException {
return crqs.getDateQueued(getProviderId(), contextId);
}
public Date getDateSubmitted(String contextId) throws QueueException, SubmissionException {
return crqs.getDateSubmitted(getProviderId(), contextId);
}
public String getIconCssClassforScore(int score, String contentId) {
String cssClass;
if (score < 0) {
cssClass = "contentReviewIconThreshold-6";
} else if (score == 0) {
cssClass = "contentReviewIconThreshold-5";
} else if (score < 25) {
cssClass = "contentReviewIconThreshold-4";
} else if (score < 50) {
cssClass = "contentReviewIconThreshold-3";
} else if (score < 75) {
cssClass = "contentReviewIconThreshold-2";
} else {
cssClass = "contentReviewIconThreshold-1";
}
return cssClass;
}
public String getLocalizedStatusMessage(String arg0) {
return null;
}
public String getLocalizedStatusMessage(String arg0, String arg1) {
return null;
}
public String getLocalizedStatusMessage(String arg0, Locale arg1) {
return null;
}
public List getReportList(String siteId)
throws QueueException, SubmissionException, ReportException {
return null;
}
public List getReportList(String siteId, String taskId)
throws QueueException, SubmissionException, ReportException {
return null;
}
@Override
public String getReviewReportRedirectUrl(String contentId, String assignmentRef, String userId, String contextId, boolean isInstructor) {
// Set variables
String viewerUrl = null;
Optional optionalItem = crqs.getQueuedItem(getProviderId(), contentId);
ContentReviewItem item = optionalItem.isPresent() ? optionalItem.get() : null;
if(item != null && ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE.equals(item.getStatus())) {
try {
//Get report owner user information
String givenName = "";
String familyName = "";
try{
User user = userDirectoryService.getUser(item.getUserId());
givenName = user.getFirstName();
familyName = user.getLastName();
}catch (Exception e) {
log.error(e.getMessage(), e);
}
Map data = new HashMap();
// Set user name
Map authorMetaDataOverride = new HashMap();
authorMetaDataOverride.put(GIVEN_NAME, givenName);
authorMetaDataOverride.put(FAMILY_NAME, familyName);
data.put(AUTHOR_METADATA_OVERRIDE, authorMetaDataOverride);
data.put(VIEWER_USER_ID, userId);
Map similarity = new HashMap();
Map modes = new HashMap();
modes.put(MATCH_OVERVIEW, Boolean.TRUE);
modes.put(ALL_SOURCES, Boolean.TRUE);
similarity.put(MODES, modes);
Map viewSettings = new HashMap<>();
similarity.put(VIEW_SETTINGS, viewSettings);
data.put(SIMILARITY, similarity);
ROLES userRole = mapUserRole(userId, contextId, isInstructor);
data.put(VIEWER_DEFAULT_PERMISSIONS, userRole.name());
viewSettings.put(SAVE_CHANGES, userRole.getMaySaveReportChanges());
//Check if there are any sakai.properties overrides for the default permissions
Map viewerPermissionsOverride = new HashMap();
if(!isInstructor && mayViewSubmissionFullSourceOverrideStudent != null) {
viewerPermissionsOverride.put(VIEWER_PERMISSION_MAY_VIEW_SUBMISSIONS_FULL_SOURCE, mayViewSubmissionFullSourceOverrideStudent);
}
if(!isInstructor && mayViewMatchSubmissionInfoOverrideStudent != null) {
viewerPermissionsOverride.put(VIEWER_PERMISSION_MAY_VIEW_MATCH_SUBMISSION_INFO, mayViewMatchSubmissionInfoOverrideStudent);
}
if(isInstructor && mayViewSubmissionFullSourceOverrideInstructor != null) {
viewerPermissionsOverride.put(VIEWER_PERMISSION_MAY_VIEW_SUBMISSIONS_FULL_SOURCE, mayViewSubmissionFullSourceOverrideInstructor);
}
if(isInstructor && mayViewMatchSubmissionInfoOverrideInstructor != null) {
viewerPermissionsOverride.put(VIEWER_PERMISSION_MAY_VIEW_MATCH_SUBMISSION_INFO, mayViewMatchSubmissionInfoOverrideInstructor);
}
if(viewerPermissionsOverride.size() > 0) {
data.put(VIEWER_PERMISSIONS, viewerPermissionsOverride);
}
// Set locale, getLanguage removes locale region
data.put("locale", preferencesService.getLocale(userId).getLanguage());
HashMap response = makeHttpCall("GET",
getNormalizedServiceUrl() + "submissions/" + item.getExternalId() + "/viewer-url",
SUBMISSION_REQUEST_HEADERS,
data,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
if ((responseCode >= 200) && (responseCode < 300) && (responseBody != null)) {
// create JSONObject from responseBody
JSONObject responseJSON = JSONObject.fromObject(responseBody);
if (responseJSON.containsKey("viewer_url")) {
viewerUrl = responseJSON.getString("viewer_url");
log.debug("Successfully retrieved viewer url: " + viewerUrl);
} else {
log.error("Viewer URL not found. Response: " + responseMessage);
}
} else {
log.error(responseMessage);
}
} catch (Exception e) {
log.error(e.getLocalizedMessage(), e);
}
}else {
// Only generate viewerUrl if report is available
log.info("Content review item is not ready for the report: " + contentId + ", " + (item != null ? item.getStatus() : ""));
}
return viewerUrl;
}
public int getReviewScore(String contentId, String assignmentRef, String userId)
throws QueueException, ReportException, Exception {
Optional optionalItem = crqs.getQueuedItem(getProviderId(), contentId);
if(optionalItem.isPresent()) {
return optionalItem.get().getReviewScore();
}else {
throw new ReportException("Could not find content item: " + contentId);
}
}
public Long getReviewStatus(String contentId) throws QueueException {
return crqs.getReviewStatus(getProviderId(), contentId);
}
public String getServiceName() {
return SERVICE_NAME;
}
@Override
public Integer getProviderId() {
//Since there is an already existing Turnitin integration, we can't use the same "namespace" for the provider ID
return Math.abs("TurnitinOC".hashCode());
}
public boolean isAcceptableContent(ContentResource resource) {
if (serverConfigurationService.getBoolean(PROP_ACCEPT_ALL_FILES, false)) {
return true;
}
String mime = resource.getContentType();
// Check the mime type
Map> acceptableExtensionsToMimeTypes = getAcceptableExtensionsToMimeTypes();
if (acceptableExtensionsToMimeTypes.values().stream().anyMatch(set -> set.contains(mime))) {
return true;
}
// Check the file extension
ResourceProperties resourceProperties = resource.getProperties();
String fileName = resourceProperties.getProperty(resourceProperties.getNamePropDisplayName());
if (fileName.indexOf(".") > 0) {
String extension = fileName.substring(fileName.lastIndexOf("."));
if (acceptableExtensionsToMimeTypes.containsKey(extension)) {
return true;
}
}
return false;
}
public boolean isSiteAcceptable(Site arg0) {
return true;
}
public SecurityAdvisor pushAdvisor() {
SecurityAdvisor advisor = new SecurityAdvisor() {
public SecurityAdvisor.SecurityAdvice isAllowed(String userId, String function, String reference) {
return SecurityAdvisor.SecurityAdvice.ALLOWED;
}
};
securityService.pushAdvisor(advisor);
return advisor;
}
public void popAdvisor(SecurityAdvisor advisor) {
securityService.popAdvisor(advisor);
}
private HashMap makeHttpCall(String method, String urlStr, Map headers, Map data, byte[] dataBytes)
throws Exception {
// Set variables
HttpURLConnection connection = null;
URL url = null;
// Construct URL
url = new URL(urlStr);
// Open connection and set HTTP method
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(method);
// Set headers
if (headers == null) {
throw new Exception("No headers present for call: " + method + ":" + urlStr);
}
for (Entry entry : headers.entrySet()) {
connection.setRequestProperty(entry.getKey(), entry.getValue());
}
if (data != null || dataBytes != null) {
connection.setDoOutput(true);
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
// Set Post body:
if (data != null) {
// Convert data to string:
try (BufferedWriter br = new BufferedWriter(new OutputStreamWriter(wr, StandardCharsets.UTF_8))) {
ObjectMapper objectMapper = new ObjectMapper();
String dataStr = objectMapper.writeValueAsString(data);
br.write(dataStr);
br.flush();
}
} else if (dataBytes != null) {
wr.write(dataBytes);
}
}
}
// Send request:
int responseCode = connection.getResponseCode();
String responseMessage = connection.getResponseMessage();
String responseBody;
if (responseCode < 200 || responseCode >= 300)
{
InputStream inputStream = connection.getErrorStream() != null ? connection.getErrorStream() : connection.getInputStream();
// getInputStream() throws an exception in this case, but getErrorStream() has the information necessary for troubleshooting
responseBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
log.warn("Turnitin response code: " + responseCode + "; message: " + responseMessage + "; body:\n" + responseBody);
}
else
{
responseBody = IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8);
log.debug("Turnitin response code: " + responseCode + "; message: " + responseMessage + "; body:\n" + responseBody);
}
HashMap response = new HashMap();
response.put(RESPONSE_CODE, responseCode);
response.put(RESPONSE_MESSAGE, responseMessage);
response.put(RESPONSE_BODY, responseBody);
return response;
}
private void indexSubmission(String reportId) throws Exception {
HashMap response = makeHttpCall("PUT",
getNormalizedServiceUrl() + "submissions/" + reportId + "/index",
SIMILARITY_REPORT_HEADERS,
null,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
if ((responseCode >= 200) && (responseCode < 300)) {
log.info("Successfully indexed submission: " + reportId);
} else if ((responseCode == 400)) {
log.info("File must be uploaded to submission before indexing for submission: " + reportId);
} else {
throw new ContentReviewProviderException("Submission failed to be indexed: " + responseCode + ", " + responseMessage + ", " + responseBody,
createLastError(doc -> createFormattedMessageXML(doc, "report.error.unsuccessful", responseMessage, responseCode)));
}
}
private void generateSimilarityReport(String reportId, String assignmentRef) throws Exception {
Assignment assignment = assignmentService.getAssignment(entityManager.newReference(assignmentRef));
Map assignmentSettings = assignment.getProperties();
// Pass the full list of repositories, TCA will filter out which repositories the tenant isn't allowed to use
List repositories = Arrays.asList("INTERNET", "SUBMITTED_WORK", "PUBLICATION", "CROSSREF", "CROSSREF_POSTED_CONTENT");
// Build header maps
Map reportData = new HashMap();
Map generationSearchSettings = new HashMap();
generationSearchSettings.put("search_repositories", repositories);
generationSearchSettings.put("auto_exclude_self_matching_scope", autoExcludeSelfMatchingScope);
reportData.put("generation_settings", generationSearchSettings);
Map viewSettings = new HashMap();
viewSettings.put("exclude_quotes", "true".equals(assignmentSettings.get("exclude_quoted")));
viewSettings.put("exclude_bibliography", "true".equals(assignmentSettings.get("exclude_biblio")));
reportData.put("view_settings", viewSettings);
Map indexingSettings = new HashMap();
indexingSettings.put("add_to_index", "true".equals(assignmentSettings.get("store_inst_index")));
reportData.put("indexing_settings", indexingSettings);
HashMap response = makeHttpCall("PUT",
getNormalizedServiceUrl() + "submissions/" + reportId + "/similarity",
SIMILARITY_REPORT_HEADERS,
reportData,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
if ((responseCode >= 200) && (responseCode < 300)) {
log.debug("Successfully initiated Similarity Report generation.");
} else if ((responseCode == 409)) {
log.debug("A Similarity Report is already generating for this submission");
} else {
throw new ContentReviewProviderException("Submission failed to initiate: " + responseCode + ", " + responseMessage + ", " + responseBody,
createLastError(doc -> createFormattedMessageXML(doc, "report.error.unsuccessful", responseMessage, responseCode)));
}
}
private JSONObject getSubmissionJSON(String reportId) throws Exception {
HashMap response = makeHttpCall("GET",
getNormalizedServiceUrl() + "submissions/" + reportId,
BASE_HEADERS,
null,
null);
// Get response data:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
// Create JSONObject from response
JSONObject responseJSON = JSONObject.fromObject(responseBody);
if ((responseCode < 200) || (responseCode >= 300)) {
throw new ContentReviewProviderException("getSubmissionJSON invalid request: " + responseCode + ", " + responseMessage + ", " + responseBody,
createLastError(doc -> createFormattedMessageXML(doc, "submission.error.unexpected.response", responseMessage, responseCode)));
}
return responseJSON;
}
private int getSimilarityReportStatus(String reportId) throws Exception {
HashMap response = makeHttpCall("GET",
getNormalizedServiceUrl() + "submissions/" + reportId + "/similarity",
BASE_HEADERS,
null,
null);
// Get response:
int responseCode = !response.containsKey(RESPONSE_CODE) ? 0 : (int) response.get(RESPONSE_CODE);
String responseMessage = !response.containsKey(RESPONSE_MESSAGE) ? "" : (String) response.get(RESPONSE_MESSAGE);
String responseBody = !response.containsKey(RESPONSE_BODY) ? "" : (String) response.get(RESPONSE_BODY);
// create JSONObject from response
JSONObject responseJSON = JSONObject.fromObject(responseBody);
if ((responseCode >= 200) && (responseCode < 300)) {
// See if report is complete or pending. If pending, ignore, if complete, get
// score and viewer URL
if (responseJSON.containsKey("status") && responseJSON.getString("status").equals(STATUS_COMPLETE)) {
log.debug("Submission successful");
if (responseJSON.containsKey("overall_match_percentage")) {
return responseJSON.getInt("overall_match_percentage");
} else {
log.error("Report came back as complete, but without a score");
return -2;
}
} else if (responseJSON.containsKey("status")
&& responseJSON.getString("status").equals(STATUS_PROCESSING)) {
log.debug("report is processing...");
return -1;
} else {
log.error("Something went wrong in the similarity report process: reportId " + reportId);
return -2;
}
} else {
log.error("Submission status call failed: " + responseMessage);
return -2;
}
}
private String getSubmissionId(ContentReviewItem item, String fileName, Site site, Assignment assignment) {
String userID = item.getUserId();
List submissionOwners = new ArrayList<>();
String submitterID = null;
try {
AssignmentSubmission currentSubmission = assignmentService.getSubmission(assignment.getId(), userID);
Set ownerIds = currentSubmission.getSubmitters().stream()
.map(AssignmentSubmissionSubmitter::getSubmitter)
.collect(Collectors.toSet());
//find submitter by filtering submittee=true, if not found, then use the assignment property SUBMITTER_USER_ID
submitterID = currentSubmission.getSubmitters().stream().filter(AssignmentSubmissionSubmitter::getSubmittee).findAny()
.map(AssignmentSubmissionSubmitter::getSubmitter)
.orElseGet(() -> currentSubmission.getProperties().get(AssignmentConstants.SUBMITTER_USER_ID));
if (userID.equals(submitterID) || StringUtils.isBlank(submitterID)) {
//no need to keep track of the submitterID if it is the same as the ownerID
submitterID = null;
} else {
ownerIds.add(submitterID);
}
submissionOwners.addAll(userDirectoryService.getUsers(ownerIds));
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
String submissionId = null;
try {
// Build header maps
Map data = new HashMap();
data.put("owner", userID);
if(site != null) {
data.put("owner_default_permission_set", mapUserRole(userID, site.getId(), false).name());
}
if (StringUtils.isNotBlank(submitterID)) {
data.put("submitter", submitterID);
if(site != null) {
data.put("submitter_default_permission_set", mapUserRole(submitterID, site.getId(), false).name());
}
}
data.put("title", fileName);
String eulaUserId = StringUtils.isNotEmpty(submitterID) ? submitterID : userID;
Instant eulaTimestamp = getUserEULATimestamp(eulaUserId);
String eulaVersion = getUserEULAVersion(eulaUserId);
if(eulaTimestamp == null || StringUtils.isEmpty(eulaVersion)) {
//best effort to make sure the user has a EULA acceptance timestamp, but if not
//add a warning in the logs and continue so that the report will still generate
eulaTimestamp = Instant.now();
eulaVersion = getEndUserLicenseAgreementVersion();
log.warn("EULA not found for user: " + eulaUserId + ", contentId: " + item.getId());
}
Map eula = new HashMap<>();
eula.put("accepted_timestamp", eulaTimestamp.toString());
eula.put("language", getUserEulaLocale(eulaUserId));
eula.put("version", eulaVersion);
data.put("eula", eula);
Map metadata = new HashMap<>();
if(assignment != null) {
Map group = new HashMap<>();
group.put("id", assignment.getId());
group.put("name", assignment.getTitle());
group.put("type", "ASSIGNMENT");
metadata.put("group", group);
if(site != null) {
Map groupContext = new HashMap<>();
groupContext.put("id", site.getId());
groupContext.put("name", site.getTitle());
metadata.put("group_context", groupContext);
}
}
//set submission owner metadata
if (submissionOwners.size() > 0) {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy