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

org.sakaiproject.contentreview.service.BaseContentReviewService Maven / Gradle / Ivy

There is a newer version: 23.3
Show newest version
/**
 * Copyright (c) 2003-2019 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.service;

import java.time.Instant;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
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.content.api.ContentResource;
import org.sakaiproject.contentreview.exception.SubmissionException;
import org.sakaiproject.contentreview.exception.TransientSubmissionException;
import org.sakaiproject.contentreview.service.ContentReviewQueueService;
import org.sakaiproject.component.api.ServerConfigurationService;
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.entity.api.EntityManager;
import org.sakaiproject.entity.api.EntityPropertyNotDefinedException;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.user.api.PreferencesEdit;
import org.sakaiproject.user.api.PreferencesService;
import org.sakaiproject.util.ResourceLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

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

@Slf4j
public abstract class BaseContentReviewService implements ContentReviewService{

	@Setter
	protected AssignmentService assignmentService;
	@Setter
	protected ContentReviewQueueService crqs;
	@Setter
	protected EntityManager entityManager;
	@Setter
	protected PreferencesService preferencesService;
	@Setter
	protected ServerConfigurationService serverConfigurationService;
	
	private static final String PROP_KEY_EULA = "contentReviewEULA";
	private static final String PROP_KEY_EULA_TIMESTAMP = "contentReviewEULATimestamp";
	private static final String PROP_KEY_EULA_VERSION = "contentReviewEULAVersion";
	//relative path since it will be used within Sakai
	private static final String REDIRECT_URL_TEMPLATE =  "/content-review-tool/viewreport?contentId=%s&assignmentRef=%s&contextId=%s";
	//full path since it will be used externally
	private static final String WEBHOOK_URL_TEMPLATE = "%scontent-review-tool/webhooks?providerId=%s";

	private ResourceLoader rb;
	
	@Override
	public Instant getUserEULATimestamp(String userId) {
		Instant timestamp = null;
		try {
			ResourceProperties pref = preferencesService.getPreferences(userId).getProperties(PROP_KEY_EULA + getProviderId());
			if(pref != null) {
				timestamp = Instant.ofEpochMilli(pref.getLongProperty(PROP_KEY_EULA_TIMESTAMP));
			}
		}catch(EntityPropertyNotDefinedException e) {
			//nothing to do, prop is just not set
		}catch(Exception e) {
			log.error(e.getMessage(), e);
		}
		return timestamp;
	}
	
	@Override
	public String getUserEULAVersion(String userId) {
		String version = null;
		try {
			ResourceProperties pref = preferencesService.getPreferences(userId).getProperties(PROP_KEY_EULA + getProviderId());
			if(pref != null) {
				version = pref.getProperty(PROP_KEY_EULA_VERSION);
			}
		}catch(Exception e) {
			log.error(e.getMessage(), e);
		}
		return version;
	}
	
	@Override
	public void updateUserEULATimestamp(String userId) {
		try {
			PreferencesEdit pref = preferencesService.edit(userId);
			try {
				if(pref != null) {
					ResourcePropertiesEdit resourcePropEdit = pref.getPropertiesEdit(PROP_KEY_EULA + getProviderId());
					if(resourcePropEdit != null) {
						resourcePropEdit.addProperty(PROP_KEY_EULA_TIMESTAMP, "" + Instant.now().toEpochMilli());
						String EULAVersion = getEndUserLicenseAgreementVersion();
						if(StringUtils.isNotEmpty(EULAVersion)) {
							resourcePropEdit.addProperty(PROP_KEY_EULA_VERSION, EULAVersion);
						}
						preferencesService.commit(pref);
					}else {
						preferencesService.cancel(pref);
					}
				}
			}catch(Exception e) {
				log.error(e.getMessage(), e);
				preferencesService.cancel(pref);
			}
		}catch(Exception e) {
			log.error(e.getMessage(), e);
		}	
	}

	@Override
	public void createAssignment(final String contextId, final String taskId, final Map opts)
		throws SubmissionException, TransientSubmissionException {
		queueAllSubmissionsForAssignment(taskId);
	}

	protected void queueAllSubmissionsForAssignment(final String taskId) {
		try {
			Assignment assignment;
			try {
				// Assume it's from the Assignments tool, support can be added to other tools in the future if there is a need to integrate with content-review
				assignment = assignmentService.getAssignment(entityManager.newReference(taskId));
			}
			catch (IdUnusedException | PermissionException e) {
				return;
			}

			Set submissions = assignmentService.getSubmissions(assignment);
			if (submissions != null) {
				submissions.stream().filter(AssignmentSubmission::getSubmitted).forEach(sub -> {
					List attachments = assignmentService.getAllAcceptableAttachments(sub);
					if (!attachments.isEmpty()) {
						Optional submitter = assignmentService.getSubmissionSubmittee(sub);
						if (!submitter.isPresent() && allowSubmissionsOnBehalf()) {
							submitter = sub.getSubmitters().stream().findAny();
						}
						if (submitter.isPresent()) {
							try {
								queueContent(submitter.get().getSubmitter(), assignment.getContext(), taskId, attachments);
							}
							catch (QueueException e) {
								// Already queued most likely
							}
						}
					}
				});
			}
		}
		catch (Exception e)
		{
			log.error("Unknown excpetion queueing submissions for assessment " + taskId, e);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void deleteAssignment(String siteId, String taskId) {
		deleteAllSubmissionsForAssignment(siteId, taskId);
	}

	protected void deleteAllSubmissionsForAssignment(String siteId, String taskId) {
		Integer providerId = getProviderId();
		List items = crqs.getContentReviewItems(providerId, siteId, taskId);
		// Can't do this because it opens a new transaction, causing an IAE - "Removing a detached instance":
		// items.forEach(item -> crqs.delete(item));
		// So delete them via removeFromQueue:
		items.forEach(item -> crqs.removeFromQueue(providerId, item.getContentId()));
	}
	
	@Override
	public String getReviewReport(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {
		return formatRedirectUrl(contentId, assignmentRef, userId);
	}
	
	@Override
	public String getReviewReportInstructor(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {
		return formatRedirectUrl(contentId, assignmentRef, userId);
	}
	
	@Override
	public String getReviewReportStudent(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {
		return formatRedirectUrl(contentId, assignmentRef, userId);
	}
	
	private String formatRedirectUrl(String contentId, String assignmentRef, String userId)
			throws QueueException, ReportException {
		ContentReviewItem item = checkContentItemStatus(contentId);
		try {
			return String.format(REDIRECT_URL_TEMPLATE, URLEncoder.encode(contentId, "UTF-8"), 
					URLEncoder.encode(assignmentRef, "UTF-8"), URLEncoder.encode(item.getSiteId(), "UTF-8"));
		} catch(UnsupportedEncodingException e) {
			log.error(e.getMessage(), e);
			throw new ReportException("Error encoding contentId: " + contentId + ", assignmentRef: " + assignmentRef + ", siteId: " + item.getSiteId());
		}
	}
	
	private ContentReviewItem checkContentItemStatus(String contentId) throws ReportException {
		ContentReviewItem item = getContentReviewItemByContentId(contentId);
		if(item == null
				|| !ContentReviewConstants.CONTENT_REVIEW_SUBMITTED_REPORT_AVAILABLE_CODE.equals(item.getStatus())) {
			throw new ReportException("Report status: " + (item != null ? item.getStatus() : ""));
		}
		return item;
	}
	
	@Override
	public String getReviewReportRedirectUrl(String contentId, String assignmentRef, String userId, String contextId, boolean isInstructor) {
		return null;
	}

	public String getWebhookUrl(Optional customParam) {
		StringBuilder sb = new StringBuilder();
		sb.append(serverConfigurationService.getServerUrl());
		if(!StringUtils.endsWith(sb.toString(), "/")) {
			sb.append("/");
		}
		return String.format(WEBHOOK_URL_TEMPLATE, sb.toString(), getProviderId()) + (customParam.isPresent() ? "&custom=" + customParam.get() : "");
	}

	// ================== Internationalized Error Messages ==================

	// For methods below, see I18nXmlUtility's class level javadoc comments

	/**
	 * Creates XML that represents calls to ResourceLoader.getFormattedMessage().
	 * @return an XML element representing the i18n message key and arguments - unless document is null, in which case the formatted message is returned as a String using the current session locale
	 */
	protected Object createFormattedMessageXML(Document document, String key, Object... args) {
		try {
			return I18nXmlUtility.createFormattedMessageXML(document, key, args);
		} catch (Exception e) {
			log.warn("Could not create last error xml, returning the raw message localized to the current session", e);
			return getResourceLoader().getFormattedMessage(key, args);
		}
	}

	/**
	 * Functional interface for updateLastError
	 */
	public static interface LastErrorUpdater {
		public Object createLastErrorXML(Document doc);
	}

	/**
	 * Sets the ContentReviewItem's lastError property to the result of createLastError(leu).
	 * Changes are not committed - the caller is responsible to do so.
	 * Afterward, the message can be localized to end users via getLocalizedLastError(item).
	 */
	protected void setLastError(ContentReviewItem item, LastErrorUpdater leu) {
		item.setLastError(createLastError(leu));
	}

	/**
	 * Convenience method:
	 * If the Exception is an instance of ContentReviewProviderException, the item's lastError is set to the exception's I18nXml;
	 * otherwise it falls back to getLocalizedMessage()
	 */
	protected void setLastError(ContentReviewItem item, Exception e) {
		setLastError(item, e, null);
	}

	/**
	 * If the Exception is an instance of ContentReviewProviderException, the item's lastError is set to the exception's I18nXml;
	 * otherwise it falls back to leu;
	 * if leu is null, it falls back to getLocalizedMessage()
	 */
	protected void setLastError(ContentReviewItem item, Exception e, LastErrorUpdater leu) {
		if (e instanceof ContentReviewProviderException) {
			item.setLastError(((ContentReviewProviderException)e).getI18nXml());
		} else {
			String message = leu == null ? e.getLocalizedMessage() : createLastError(leu);
			item.setLastError(message);
		}
	}

	/**
	 * Create's an XML model that represents calls to ResourceLoader.getFormattedMessage(...).
	 *
	 * Example - represent an i18n compliant message for "An error has occurred with the service. Error code: 42; cause: A Sakaiger ate the paper"
	 * given:
	 *     service.error=An error has occurred with the service. Error code: {0}; cause: {1}
	 *     service.sakaiger.ate.paper=A Sakaiger ate the paper
	 * You may invoke:
	 * createLastError(doc -> createFormattedMessageXML(doc, "service.error", 42, createFormattedMessageXML(doc, "service.sakaiger.ate.paper")));
	 */
	protected String createLastError(LastErrorUpdater leu) {
		Document document = null;
		try {
			document = I18nXmlUtility.createXmlDocument();
		} catch (ContentReviewProviderException e) {
			// Shouldn't happen, but if it does, just use a null document. It'll generate the formatted string using the current session locale
			log.warn("Failed to create an XML document", e);
		}

		Object lastErrorXML = leu.createLastErrorXML(document);

		String lastError = null;
		if (lastErrorXML instanceof Element) {
			lastError = I18nXmlUtility.addElementAndGetDocumentAsString(document, (Element)lastErrorXML);
		}
		else if (lastErrorXML instanceof String) {
			lastError = (String)lastErrorXML;
		}
		else {
			throw new ContentReviewProviderException("Unexpected behavior while generating the last_error value");
		}

		return lastError;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getLocalizedLastError(ContentReviewItem item) {
		String xmlString = item.getLastError();
		if (StringUtils.isBlank(xmlString)) {
			return "";
		}
		try {
			Document doc = I18nXmlUtility.getDocumentBuilder().parse(new InputSource(new StringReader(xmlString)));

			Node root = doc.getFirstChild();

			return I18nXmlUtility.getLocalizedMessage(getResourceLoader(), root);
		} catch (Exception e) {
			log.warn("Could not internationalize last_error value:\n{}\nReturning the raw data", xmlString);
			log.debug("Cause:", e);
			// The content is most likely raw (e.g. data from before SAK-41883)
			return xmlString;
		}
	}

	/**
	 * The Resource Loader specific to the content-review service implementation
	 */
	protected ResourceLoader getResourceLoader() {
		if (rb == null) {
			rb = new ResourceLoader(getResourceLoaderName());
		}
		return rb;
	}

	/**
	 * Gets the name of the resource loader's file. Default implementation is getServiceName() to lower case, and this should be overriden as necessary
	 */
	protected String getResourceLoaderName() {
		return getServiceName().toLowerCase();
	}
	
	@Override
	public boolean allowSubmissionsOnBehalf() {
		return false;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy