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

org.sakaiproject.coursemanagement.impl.CourseSiteRemovalServiceImpl Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2015 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.coursemanagement.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.FunctionManager;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.coursemanagement.api.AcademicSession;
import org.sakaiproject.coursemanagement.api.CourseManagementService;
import org.sakaiproject.coursemanagement.api.CourseOffering;
import org.sakaiproject.coursemanagement.api.Section;
import org.sakaiproject.coursemanagement.api.CourseSiteRemovalService;
import org.sakaiproject.coursemanagement.util.CourseManagementConstants;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.SiteService.SelectionType;
import org.sakaiproject.site.api.SiteService.SortType;
import org.springframework.orm.hibernate5.support.HibernateDaoSupport;

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


/**
 * This class is an implementation of the auto site removal service interface.
 */
@Slf4j
public class CourseSiteRemovalServiceImpl extends HibernateDaoSupport implements CourseSiteRemovalService {

   // class members
   private static final long ONE_DAY_IN_MS = 1000L * 60L * 60L * 24L;    // one day in ms = 1000ms/s · 60s/m · 60m/h · 24h/day

   // batch sizes to silently unpublish sites
   private static final long SILENT_UNPUBLISH_BATCH_SIZE = 1000;

   private static final String SAK_PROP_HANDLE_CROSSLISTING = "course_site_removal_service.handle.crosslisting";
   private static final String SAK_PROP_SILENT_UNPUBLISH = "course_site_removal_service.silently.unpublish";
   private static final boolean SAK_PROP_HANDLE_CROSSLISTING_DEFAULT = false;
   private static final boolean SAK_PROP_SILENT_UNPUBLISH_DEFAULT = false;

   private final int NUM_SITE_IDS_TO_LOG = 1000;

   // Used to append "Additional " after the first time we log (so we initialize to "" and set its value afterwards)
   private String additional = "";

   // sakai services
   @Getter @Setter
   private AuthzGroupService authzGroupService;
   @Getter @Setter
   private CourseManagementService courseManagementService;
   @Getter @Setter
   private FunctionManager functionManager;
   @Getter @Setter
   private SecurityService securityService;
   @Getter @Setter
   private ServerConfigurationService serverConfigurationService;
   @Getter @Setter
   private SiteService siteService;

   /**
    * called by the spring framework.
    */
   public void destroy() {
      log.debug("destroy()");

       // no code necessary
   }

   /**
    * called by the spring framework after this class has been instantiated, this method registers the permissions necessary to invoke the course site removal service.
    */
   public void init() {
      log.debug("init()");

      // register permissions with sakai
      functionManager.registerFunction(PERMISSION_COURSE_SITE_REMOVAL);
   }

   /**
    * removes\\unpublishes course sites whose terms have ended and a specified number of days have passed.
    * Once a term has ended, the course sites for that term remain available for a specified number of days, whose duration is specified in sakai.properties
    * via the course_site_removal_service.num_days_after_term_ends property.  After the specified period has elapsed, this invoking this service will either
    * remove or unpublish the course site, depending on the value of the course_site_removal_service.action sakai property.
    * 

* @param action whether to delete the course site or to simply unpublish it. * @param numDaysAfterTermEnds number of days after a term ends when course sites expire. *

* @return the number of course sites that were removed\\unpublished. */ public int removeCourseSites(CourseSiteRemovalService.Action action, int numDaysAfterTermEnds) { log.info("removeCourseSites({} course sites, {} days after the term ends)", action, numDaysAfterTermEnds); Date today = new Date(); Date expirationDate = new Date(today.getTime() - numDaysAfterTermEnds * ONE_DAY_IN_MS); boolean handleCrosslistedTerms = serverConfigurationService.getBoolean(SAK_PROP_HANDLE_CROSSLISTING, SAK_PROP_HANDLE_CROSSLISTING_DEFAULT); if (handleCrosslistedTerms) { return removeCourseSitesWithCriteria(action, null, expirationDate); } // get the list of the academic term(s) List academicSessions = courseManagementService.getAcademicSessions(); int numSitesRemoved = 0; for (AcademicSession academicSession : academicSessions) { // see if the academic session ended more than the specified number of days ago if (academicSession.getEndDate().getTime() < expirationDate.getTime()) { // get a list of all published course sites in ascending creation date order which are associated with the specified academic session Map propertyCriteria = new HashMap<>(); propertyCriteria.put("term_eid", academicSession.getEid()); numSitesRemoved += removeCourseSitesWithCriteria(action, propertyCriteria, expirationDate); } } return numSitesRemoved; } /** * Removes all sites matching the specified propertyCriteria except sites that: * 1) have been touched by this job in the past, or that have academic sessions that are not yet expired * 2) are attached to rosters that have academic sessions that are not yet expired (applies only if isHandleCrosslistedTerms()) * @param action action to perform: remove / unpublish (default: unpublish) * @param propertyCriteria map of propertyCriteria for SiteService.getSites(...) * @param expirationDate sessions are considered expired if they fall before this date * @return the number of sites that were successfully removed */ private int removeCourseSitesWithCriteria(CourseSiteRemovalService.Action action, Map propertyCriteria, Date expirationDate) { int numSitesRemoved = 0; // select published / non-deleted sites only: // SelectionType: id=any, ignoreSpecial=false, ignoreUser=false, ignoreUnpublished=true, ignoreSoftlyDeleted=true SelectionType publishedNonDeletedOnly = new SelectionType("any", false, false, true ,true); // Also, in the case that an instructors has manaully re-published a site processed by this job, we shouldn't keep removing it on them, // so use the COURSE_SITE_REMOVAL property to skip sites stamped by this job in the past Map propertyRestrictions = Collections.singletonMap(SITE_PROPERTY_COURSE_SITE_REMOVAL, "set"); List siteIds = siteService.getSiteIds(publishedNonDeletedOnly, "course", null, propertyCriteria, propertyRestrictions, SortType.CREATED_ON_ASC, null, null); // A heartbeat log; log something for every 1000 sites determined to be unpublished List siteIdsToLog = new ArrayList<>(Math.min(NUM_SITE_IDS_TO_LOG, siteIds.size())); /* * Two ways to unpublish a site: * 1) SiteService.save(Site site) * -triggers SiteAdvisors * -deletes all pages, tools, properties, etc associated with the site, then inserts them all back with any modifications * -handles authz group changes * -notifies all ContextObservers to do their own work related to the site modification * -triggers EventTrackingService * 2) SiteService.silentlyUnpublish(List siteIds) * -sets PUBLISHED flag to 0 on all SAKAI_SITE matches * -triggers EventTrackingService */ boolean silentlyUnpublish = action == CourseSiteRemovalService.Action.unpublish && serverConfigurationService.getBoolean(SAK_PROP_SILENT_UNPUBLISH, SAK_PROP_SILENT_UNPUBLISH_DEFAULT); // toUnpublish will collect siteIds to unpublish in bulk // It is only used if we are silently unpublishing sites, otherwise we unpublish / remove sites one at a time // Size will grow towards siteIds.size(), but it will not necessarily reach that size List toUnpublish = silentlyUnpublish ? new ArrayList<>(siteIds.size()) : Collections.EMPTY_LIST; for (String siteId : siteIds) { try { if (isHandleCrosslistedTerms()) { if (isSiteCrosslistedWithEndDateAfterExpirationDate(siteId, expirationDate)) { // This site is attached to an academic session that has not yet expired; don't unpublish it continue; } } // Check if site is unset or set to auto Site site = siteService.getSite(siteId); ResourcePropertiesEdit siteProperties = site.getPropertiesEdit(); String publishTypeProperty = siteProperties.getProperty(CourseManagementConstants.SITE_PUBLISH_TYPE); if (StringUtils.equalsAny(publishTypeProperty, CourseManagementConstants.SITE_PUBLISH_TYPE_MANUAL, CourseManagementConstants.SITE_PUBLISH_TYPE_SCHEDULED)) { // Skip all sites set to scheduled or manual publish continue; } // check permissions if (!checkPermission(PERMISSION_COURSE_SITE_REMOVAL, siteId)) { log.error("You do not have permission to {} the site with id {}", action, siteId); } else if (action == CourseSiteRemovalService.Action.remove) { // remove the course site log.debug("{} removing course site {} ({}).", action, site.getTitle(), site.getId()); siteService.removeSite(site); numSitesRemoved++; } else { // unpublish the course site (default) log.debug("unpublishing course site {}", siteId); if (silentlyUnpublish) { toUnpublish.add(siteId); } else { // Unpublish the site and commit site property addition site.setPublished(false); siteService.save(site); numSitesRemoved++; } } siteIdsToLog.add(siteId); if (siteIdsToLog.size() == NUM_SITE_IDS_TO_LOG) { logProgress(siteIdsToLog, silentlyUnpublish); siteIdsToLog.clear(); } } catch (PermissionException | IdUnusedException ex) { logger.error(ex.getMessage(), ex); } } if (!siteIdsToLog.isEmpty()) { logProgress(siteIdsToLog, silentlyUnpublish); } if (silentlyUnpublish) { log.info("Unpublishing {} sites.", toUnpublish.size()); // bulk unpublish siteService.silentlyUnpublish(toUnpublish); // Add site property on sites siteService.saveSitePropertyOnSites(SITE_PROPERTY_COURSE_SITE_REMOVAL, "set", toUnpublish.toArray(new String[toUnpublish.size()])); numSitesRemoved += toUnpublish.size(); log.info("{} sites unpublished.", toUnpublish.size()); } return numSitesRemoved; } /** * For crosslisted sites, the sections may belong to multiple academic sessions which have differing end dates. * If we find a date that isn't before the grace period, this site is not supposed to be removed / unpublished. * @return true if this site has an academic session with an end date after the expiration date */ private boolean isSiteCrosslistedWithEndDateAfterExpirationDate(String siteId, Date expirationDate) { String siteReference = siteService.siteReference(siteId); Set providerIds = authzGroupService.getProviderIds(siteReference); for (String providerId : providerIds) { Section section = courseManagementService.getSection(providerId); if (section != null) { CourseOffering offering = courseManagementService.getCourseOffering(section.getCourseOfferingEid()); if (offering != null) { AcademicSession session = offering.getAcademicSession(); if (session != null) { Date endDate = session.getEndDate(); if (endDate != null && endDate.getTime() >= expirationDate.getTime()) { return true; } } } } } return false; } private boolean checkPermission(String lock, String reference) { return securityService.unlock(lock, reference); } private boolean isHandleCrosslistedTerms() { return serverConfigurationService.getBoolean(SAK_PROP_HANDLE_CROSSLISTING, SAK_PROP_HANDLE_CROSSLISTING_DEFAULT); } private void logProgress(List sitesToLog, boolean silentlyUnpublish) { // when silentlyUnpublished is true, we find sites first, and the unpublishing is the final step // Otherwise, we unpublish sites as we discover them String logString = silentlyUnpublish ? " sites will be unpublished: " : " sites have been removed / unpublished: "; log.info("{}{}{}{}", additional, sitesToLog.size(), logString, sitesToLog); additional = "Additional "; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy