Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CourseFactory.java 43.59 KiB
/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/

package org.olat.course;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.zip.ZipOutputStream;

import org.apache.logging.log4j.Logger;
import org.olat.admin.quota.QuotaConstants;
import org.olat.basesecurity.GroupRoles;
import org.olat.basesecurity.OrganisationRoles;
import org.olat.commons.calendar.CalendarManager;
import org.olat.commons.calendar.CalendarNotificationManager;
import org.olat.commons.calendar.manager.ImportToCalendarManager;
import org.olat.commons.calendar.ui.components.KalendarRenderWrapper;
import org.olat.commons.info.InfoMessageFrontendManager;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.services.notifications.NotificationsManager;
import org.olat.core.commons.services.notifications.Publisher;
import org.olat.core.commons.services.notifications.SubscriptionContext;
import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.htmlheader.jscss.CustomCSS;
import org.olat.core.gui.components.stack.TooledStackedPanel;
import org.olat.core.gui.components.tree.TreeNode;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.Roles;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.Tracing;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.ExportUtil;
import org.olat.core.util.FileUtils;
import org.olat.core.util.Formatter;
import org.olat.core.util.ObjectCloner;
import org.olat.core.util.StringHelper;
import org.olat.core.util.UserSession;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.cache.CacheWrapper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.SyncerExecutor;
import org.olat.core.util.event.MultiUserEvent;
import org.olat.core.util.nodes.INode;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.tree.TreeVisitor;
import org.olat.core.util.tree.Visitor;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.Quota;
import org.olat.core.util.vfs.QuotaManager;
import org.olat.core.util.vfs.VFSConstants;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.VFSStatus;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.course.archiver.ScoreAccountingHelper;
import org.olat.course.config.CourseConfig;
import org.olat.course.config.CourseConfigManager;
import org.olat.course.config.ui.courselayout.CourseLayoutHelper;
import org.olat.course.editor.EditorMainController;
import org.olat.course.editor.PublishProcess;
import org.olat.course.editor.PublishSetInformations;
import org.olat.course.editor.StatusDescription;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.groupsandrights.PersistingCourseGroupManager;
import org.olat.course.nodes.BCCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.STCourseNode;
import org.olat.course.nodes.TACourseNode;
import org.olat.course.nodes.gta.GTAManager;
import org.olat.course.properties.CoursePropertyManager;
import org.olat.course.properties.PersistingCoursePropertyManager;
import org.olat.course.run.RunMainController;
import org.olat.course.run.environment.CourseEnvironment;
import org.olat.course.statistic.AsyncExportManager;
import org.olat.course.tree.CourseEditorTreeModel;
import org.olat.course.tree.CourseEditorTreeNode;
import org.olat.course.tree.PublishTreeModel;
import org.olat.group.BusinessGroup;
import org.olat.instantMessaging.InstantMessagingService;
import org.olat.instantMessaging.manager.ChatLogHelper;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryEntryStatusEnum;
import org.olat.repository.RepositoryManager;
import org.olat.repository.RepositoryService;
import org.olat.repository.model.RepositoryEntrySecurity;
import org.olat.resource.OLATResource;
import org.olat.resource.OLATResourceManager;
import org.olat.resource.references.Reference;
import org.olat.resource.references.ReferenceManager;
import org.olat.user.UserManager;
import org.olat.util.logging.activity.LoggingResourceable;

/**
 * Description: <BR>
 * Use the course factory to create course run and edit controllers or to load a
 * course from disk
 *
 * Initial Date: Oct 12, 2004
 * @author Felix Jost
 * @author guido
 */
public class CourseFactory {

	private static CacheWrapper<Long,PersistingCourseImpl> loadedCourses;
	private static ConcurrentMap<Long, ModifyCourseEvent> modifyCourseEvents = new ConcurrentHashMap<>();

	public static final String COURSE_EDITOR_LOCK = "courseEditLock";
  //this is the lock that must be aquired at course editing, copy course, export course, configure course.
	private static Map<Long,PersistingCourseImpl> courseEditSessionMap = new ConcurrentHashMap<>();
	private static final Logger log = Tracing.createLoggerFor(CourseFactory.class);
	private static ReferenceManager referenceManager;


	/**
	 * [used by spring]
	 */
	private CourseFactory(CoordinatorManager coordinatorManager, ReferenceManager referenceManager) {
		loadedCourses = coordinatorManager.getCoordinator().getCacher().getCache(CourseFactory.class.getSimpleName(), "courses");
		CourseFactory.referenceManager = referenceManager;
	}

	/**
	 * Create an editor controller for the given course resourceable
	 *
	 * @param ureq
	 * @param wControl
	 * @param courseEntry
	 * @return editor controller for the given course resourceable; if the editor
	 *         is already locked, it returns a controller with a lock message
	 */
	public static EditorMainController createEditorController(UserRequest ureq, WindowControl wControl,
			TooledStackedPanel toolbar, RepositoryEntry courseEntry, CourseNode selectedNode) {
		ICourse course = loadCourse(courseEntry);
		EditorMainController emc = new EditorMainController(ureq, wControl, toolbar, course, selectedNode);
		if (emc.getLockEntry() == null) {
			Translator translator = Util.createPackageTranslator(RunMainController.class, ureq.getLocale());
			wControl.setWarning(translator.translate("error.editoralreadylocked", new String[] { "?" }));
			return null;
		} else if(!emc.getLockEntry().isSuccess()) {
			// get i18n from the course runmaincontroller to say that this editor is
			// already locked by another person

			Translator translator = Util.createPackageTranslator(RunMainController.class, ureq.getLocale());
			String lockerName = CoreSpringFactory.getImpl(UserManager.class).getUserDisplayName(emc.getLockEntry().getOwner());
			wControl.setWarning(translator.translate("error.editoralreadylocked", new String[] { lockerName }));
			return null;
		}
		//set the logger if editor is started
		//since 5.2 groups / areas can be created from the editor -> should be logged.
		emc.addLoggingResourceable(LoggingResourceable.wrap(course));
		return emc;
	}

	/**
	 * Creates an empty course with a single root node. The course is linked to
	 * the resourceable ores. The efficiency statment are enabled per default!
	 *
	 * @param ores
	 * @param shortTitle Short title of root node
	 * @param longTitle Long title of root node
	 * @param learningObjectives Learning objectives of root node
	 * @return An empty course with a single root node.
	 */
	public static ICourse createCourse(RepositoryEntry courseEntry,
			String shortTitle, String longTitle, String learningObjectives) {
		OLATResource courseResource = courseEntry.getOlatResource();
		PersistingCourseImpl newCourse = new PersistingCourseImpl(courseResource);
		// Put new course in course cache
		loadedCourses.put(newCourse.getResourceableId(), newCourse);

		Structure initialStructure = new Structure();
		CourseNode runRootNode = new STCourseNode();
		runRootNode.setShortTitle(shortTitle);
		runRootNode.setLongTitle(longTitle);
		runRootNode.setLearningObjectives(learningObjectives);
		initialStructure.setRootNode(runRootNode);
		newCourse.setRunStructure(initialStructure);
		newCourse.saveRunStructure();

		CourseEditorTreeModel editorTreeModel = new CourseEditorTreeModel();
		CourseEditorTreeNode editorRootNode = new CourseEditorTreeNode((CourseNode) ObjectCloner.deepCopy(runRootNode));
		editorTreeModel.setRootNode(editorRootNode);
		newCourse.setEditorTreeModel(editorTreeModel);
		newCourse.saveEditorTreeModel();

		//enable efficiency statement per default
		CourseConfig courseConfig = newCourse.getCourseConfig();
		courseConfig.setEfficencyStatementIsEnabled(true);
		newCourse.setCourseConfig(courseConfig);

		return newCourse;
	}



	/**
	 * Gets the course from cache if already there, or loads the course and puts it into cache.
	 * To be called for the "CourseRun" model.
	 * @param resourceableId
	 * @return the course with the given id (the type is always
	 *         CourseModule.class.toString())
	 */
	public static ICourse loadCourse(RepositoryEntry courseEntry) {
		if (courseEntry == null) {
			throw new AssertException("No resourceable ID found.");
		}
		Long resourceableId = courseEntry.getOlatResource().getResourceableId();
		PersistingCourseImpl course = loadedCourses.get(resourceableId);
		if (course == null) {
			// o_clusterOK by:ld - load and put in cache in doInSync block to ensure
			// that no invalidate cache event was missed
			PersistingCourseImpl theCourse = new PersistingCourseImpl(courseEntry);
			theCourse.load();

			PersistingCourseImpl cachedCourse = loadedCourses.putIfAbsent(resourceableId, theCourse);
			if(cachedCourse != null) {
				course = cachedCourse;
				course.updateCourseEntry(courseEntry);
			} else {
				course = theCourse;
			}
		} else {
			course.updateCourseEntry(courseEntry);
		}

		return course;
	}

	public static ICourse loadCourse(final Long resourceableId) {
		if (resourceableId == null) throw new AssertException("No resourceable ID found.");
		PersistingCourseImpl course = loadedCourses.get(resourceableId);
		if (course == null) {
			// o_clusterOK by:ld - load and put in cache in doInSync block to ensure
			// that no invalidate cache event was missed
			OLATResource resource = OLATResourceManager.getInstance().findResourceable(resourceableId, "CourseModule");
			PersistingCourseImpl theCourse = new PersistingCourseImpl(resource);
			theCourse.load();

			PersistingCourseImpl cachedCourse = loadedCourses.putIfAbsent(resourceableId, theCourse);
			if(cachedCourse != null) {
				course = cachedCourse;
			} else {
				course = theCourse;
			}
		}
		return course;
	}

	/**
	 * Load the course for the given course resourceable
	 *
	 * @param olatResource
	 * @return the course for the given course resourceable
	 */
	public static ICourse loadCourse(OLATResourceable olatResource) {
		Long resourceableId = olatResource.getResourceableId();
		return loadCourse(resourceableId);
	}

	/**
	 *
	 * @param resourceableId
	 */
	private static void removeFromCache(Long resourceableId) { //o_clusterOK by: ld
		loadedCourses.remove(resourceableId);
		log.debug("removeFromCache");
	}

	/**
	 * Puts the current course in the local cache and removes it from other caches (other cluster nodes).
	 * @param resourceableId
	 * @param course
	 */
	private static void updateCourseInCache(Long resourceableId, PersistingCourseImpl course) { //o_clusterOK by:ld
		loadedCourses.update(resourceableId, course);
		log.debug("updateCourseInCache");
	}

	/**
	 * Delete a course including its course folder and all references to resources
	 * this course holds.
	 *
	 * @param res
	 */
	public static void deleteCourse(RepositoryEntry entry, OLATResource res) {
		final long start = System.currentTimeMillis();
		log.info("deleteCourse: starting to delete course. res="+res);

		PersistingCourseImpl course = null;
		try {
			course = (PersistingCourseImpl)loadCourse(res);
		} catch (CorruptedCourseException e) {
			log.error("Try to delete a corrupted course, I make want I can.");
		}

		// call cleanupOnDelete for nodes
		if(course != null) {
			Visitor visitor = new NodeDeletionVisitor(course);
			TreeVisitor tv = new TreeVisitor(visitor, course.getRunStructure().getRootNode(), true);
			tv.visitAll();
		}
		// delete assessment notifications
		OLATResourceable assessmentOres = OresHelper.createOLATResourceableInstance(CourseModule.ORES_COURSE_ASSESSMENT, res.getResourceableId());
		NotificationsManager.getInstance().deletePublishersOf(assessmentOres);
		// delete all course notifications
		NotificationsManager.getInstance().deletePublishersOf(res);
		//delete calendar subscription
		clearCalenderSubscriptions(res, course);
		// delete course configuration (not really usefull, the config is in
		// the course folder which is deleted right after)
		if(course != null) {
			CoreSpringFactory.getImpl(CourseConfigManager.class).deleteConfigOf(course);
		}

		CoreSpringFactory.getImpl(TaskExecutorManager.class).delete(res);

		// delete course group- and rightmanagement
		CourseGroupManager courseGroupManager = PersistingCourseGroupManager.getInstance(res);
		courseGroupManager.deleteCourseGroupmanagement();
		// delete all remaining course properties
		CoursePropertyManager propertyManager = PersistingCoursePropertyManager.getInstance(res);
		propertyManager.deleteAllCourseProperties();
		// delete course calendar
		CoreSpringFactory.getImpl(ImportToCalendarManager.class).deleteCourseImportedCalendars(res);
		CoreSpringFactory.getImpl(CalendarManager.class).deleteCourseCalendar(res);
		
		// delete IM messages
		CoreSpringFactory.getImpl(InstantMessagingService.class).deleteMessages(res);
		//delete tasks
		CoreSpringFactory.getImpl(GTAManager.class).deleteAllTaskLists(entry);
		//delete the storage folder of info messages attachments
		CoreSpringFactory.getImpl(InfoMessageFrontendManager.class).deleteStorage(course);

		// cleanup cache
		removeFromCache(res.getResourceableId());

		// Everything is deleted, so we could get rid of course logging
		// with the change in user audit logging - which now all goes into a DB
		// we no longer do this though!

		// delete course directory
		VFSContainer fCourseBasePath = getCourseBaseContainer(res.getResourceableId());
		VFSStatus status = fCourseBasePath.deleteSilently();
		boolean deletionSuccessful = (status == VFSConstants.YES || status == VFSConstants.SUCCESS);
		log.info("deleteCourse: finished deletion. res="+res+", deletion successful: "+deletionSuccessful+", duration: "+(System.currentTimeMillis()-start)+" ms.");
	}

	/**
	 * Checks all learning group calendars and the course calendar for publishers (of subscriptions)
	 * and sets their state to "1" which indicates that the ressource is deleted.
	 */
	private static void clearCalenderSubscriptions(OLATResourceable res, ICourse course) {
		//set Publisher state to 1 (= ressource is deleted) for all calendars of the course
		CalendarManager calMan = CoreSpringFactory.getImpl(CalendarManager.class);
		CalendarNotificationManager notificationManager = CoreSpringFactory.getImpl(CalendarNotificationManager.class);
		NotificationsManager nfm = NotificationsManager.getInstance();

		if(course != null) {
			CourseGroupManager courseGroupManager = course.getCourseEnvironment().getCourseGroupManager();
			List<BusinessGroup> learningGroups = courseGroupManager.getAllBusinessGroups();
			//all learning and right group calendars
			for (BusinessGroup bg : learningGroups) {
				KalendarRenderWrapper calRenderWrapper = calMan.getGroupCalendar(bg);
				SubscriptionContext subsContext = notificationManager.getSubscriptionContext(calRenderWrapper);
				Publisher pub = nfm.getPublisher(subsContext);
				if (pub != null) {
					pub.setState(1); //int 0 is OK -> all other is not OK
				}
			}
		}
		//the course calendar
		try {
			KalendarRenderWrapper courseCalendar = calMan.getCalendarForDeletion(res);
			if(courseCalendar != null) {
				SubscriptionContext subContext = notificationManager.getSubscriptionContext(courseCalendar, res);
				OLATResourceable oresToDelete = OresHelper.createOLATResourceableInstance(subContext.getResName(), subContext.getResId());
				nfm.deletePublishersOf(oresToDelete);
			}
		} catch (AssertException e) {
			//if we have a broken course (e.g. canceled import or no repo entry somehow) skip calendar deletion...
		}
	}

	/**
	 * Copies a course. More specifically, the run and editor structures and the
	 * course folder will be copied to create a new course.
	 *
	 *
	 * @param sourceRes
	 * @param ureq
	 * @return copy of the course.
	 */
	public static OLATResourceable copyCourse(OLATResourceable sourceRes, OLATResource targetRes) {
		PersistingCourseImpl sourceCourse = (PersistingCourseImpl)loadCourse(sourceRes);
		PersistingCourseImpl targetCourse = new PersistingCourseImpl(targetRes);
		LocalFolderImpl fTargetCourseBaseContainer = targetCourse.getCourseBaseContainer();
		File fTargetCourseBasePath = fTargetCourseBaseContainer.getBasefile();

		//close connection before file copy
		DBFactory.getInstance().commitAndCloseSession();

		synchronized (sourceCourse) { // o_clusterNOK - cannot be solved with doInSync since could take too long (leads to error: "Lock wait timeout exceeded")
			// copy configuration
			CourseConfig courseConf = CoreSpringFactory.getImpl(CourseConfigManager.class).copyConfigOf(sourceCourse);
			targetCourse.setCourseConfig(courseConf);
			// save structures
			targetCourse.setRunStructure((Structure) XStreamHelper.xstreamClone(sourceCourse.getRunStructure()));
			targetCourse.saveRunStructure();
			targetCourse.setEditorTreeModel((CourseEditorTreeModel) XStreamHelper.xstreamClone(sourceCourse.getEditorTreeModel()));
			targetCourse.saveEditorTreeModel();

			// copy course folder
			VFSContainer sourceCourseContainer = sourceCourse.getIsolatedCourseBaseContainer();
			if (sourceCourseContainer.exists()) {
				targetCourse.getIsolatedCourseBaseContainer()
					.copyContentOf(sourceCourseContainer);
			}

			// copy folder nodes directories
			VFSContainer sourceFoldernodesContainer = VFSManager
					.olatRootContainer(BCCourseNode.getFoldernodesPathRelToFolderBase(sourceCourse.getCourseEnvironment()));
			if (sourceFoldernodesContainer.exists()) {
				VFSContainer targetFoldernodesContainer = VFSManager
						.olatRootContainer(BCCourseNode.getFoldernodesPathRelToFolderBase(targetCourse.getCourseEnvironment()));
				targetFoldernodesContainer.copyContentOf(sourceFoldernodesContainer);
			}

			// copy task folder directories
			File fSourceTaskfoldernodesFolder = new File(FolderConfig.getCanonicalRoot()
					+ TACourseNode.getTaskFoldersPathRelToFolderRoot(sourceCourse.getCourseEnvironment()));
			if (fSourceTaskfoldernodesFolder.exists()) FileUtils.copyDirToDir(fSourceTaskfoldernodesFolder, fTargetCourseBasePath, false, "copy task folder directories");

			// update references
			List<Reference> refs = referenceManager.getReferences(sourceCourse);
			int count = 0;
			for (Reference ref: refs) {
				referenceManager.addReference(targetCourse, ref.getTarget(), ref.getUserdata());
				if(count++ % 20 == 0) {
					DBFactory.getInstance().intermediateCommit();
				}
			}

			// set quotas
			Quota sourceQuota = VFSManager.isTopLevelQuotaContainer(sourceCourse.getCourseFolderContainer());
			Quota targetQuota = VFSManager.isTopLevelQuotaContainer(targetCourse.getCourseFolderContainer());
			if (sourceQuota != null && targetQuota != null) {
				QuotaManager qm = CoreSpringFactory.getImpl(QuotaManager.class);
				if (sourceQuota.getQuotaKB() != qm.getDefaultQuota(QuotaConstants.IDENTIFIER_DEFAULT_COURSE).getQuotaKB()) {
					targetQuota = qm.createQuota(targetQuota.getPath(), sourceQuota.getQuotaKB(), sourceQuota.getUlLimitKB());
					qm.setCustomQuotaKB(targetQuota);
				}
			}
		}
		return targetRes;
	}

	/**
	 * Exports an entire course to a zip file.
	 *
	 * @param sourceRes
	 * @param fTargetZIP
	 * @return true if successfully exported, false otherwise.
	 */
	public static void exportCourseToZIP(OLATResourceable sourceRes, File fTargetZIP, boolean runtimeDatas) {
		PersistingCourseImpl sourceCourse = (PersistingCourseImpl) loadCourse(sourceRes);

		// add files to ZIP
		File fExportDir = new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID());
		fExportDir.mkdirs();
		log.info("Export folder: " + fExportDir);
		synchronized (sourceCourse) { //o_clusterNOK - cannot be solved with doInSync since could take too long (leads to error: "Lock wait timeout exceeded")
			OLATResource courseResource = sourceCourse.getCourseEnvironment().getCourseGroupManager().getCourseResource();
			sourceCourse.exportToFilesystem(courseResource, fExportDir, runtimeDatas);
			Set<String> fileSet = new HashSet<>();
			String[] files = fExportDir.list();
			for (int i = 0; i < files.length; i++) {
				fileSet.add(files[i]);
			}
			ZipUtil.zip(fileSet, fExportDir, fTargetZIP, false);
			log.info("Delete export folder: " + fExportDir);
			FileUtils.deleteDirsAndFiles(fExportDir, true, true);
		}
	}

	/**
	 * Import a course from a ZIP file.
	 *
	 * @param ores
	 * @param zipFile
	 * @return New Course.
	 */
	public static ICourse importCourseFromZip(OLATResource ores, File zipFile) {
		// Generate course with filesystem
		PersistingCourseImpl newCourse = new PersistingCourseImpl(ores);
		
		CourseConfigManager courseConfigMgr = CoreSpringFactory.getImpl(CourseConfigManager.class);
		courseConfigMgr.deleteConfigOf(newCourse);

		// Unzip course structure in new course
		LocalFolderImpl courseBaseContainer = newCourse.getCourseBaseContainer();
		File fCanonicalCourseBasePath = courseBaseContainer.getBasefile();
		if (ZipUtil.unzip(zipFile, fCanonicalCourseBasePath)) {
			// Load course structure now
			try {
				newCourse.load();
				CourseConfig cc = courseConfigMgr.loadConfigFor(newCourse);
				//newCourse is not in cache yet, so we cannot call setCourseConfig()
				newCourse.setCourseConfig(cc);
				loadedCourses.put(newCourse.getResourceableId(), newCourse);
				
				//course folder
				File courseFolderZip = new File(fCanonicalCourseBasePath, "oocoursefolder.zip");
				if(courseFolderZip.exists()) {
					VFSContainer courseFolder = VFSManager.getOrCreateContainer(courseBaseContainer, PersistingCourseImpl.COURSEFOLDER);
					ZipUtil.unzipNonStrict(courseFolderZip, courseFolder, null, false);
					FileUtils.deleteFile(courseFolderZip);
				}
				return newCourse;
			} catch (AssertException ae) {
				// ok failed, cleanup below
				// better logging to search error
				log.error("rollback importCourseFromZip",ae);
			}
		}
		// cleanup if not successfull
		FileUtils.deleteDirsAndFiles(fCanonicalCourseBasePath, true, true);
		return null;
	}

	/**
	 * Publish the course with some standard options
	 * @param course
	 * @param locale
	 * @param identity
	 */
	public static void publishCourse(ICourse course, RepositoryEntryStatusEnum accessStatus, boolean allUsers, boolean guests,
			Identity identity, Locale locale) {
		 CourseEditorTreeModel cetm = course.getEditorTreeModel();
		 PublishProcess publishProcess = PublishProcess.getInstance(course, cetm, locale);
		 PublishTreeModel publishTreeModel = publishProcess.getPublishTreeModel();
		 publishProcess.changeGeneralAccess(identity, accessStatus, allUsers, guests);

		 if (publishTreeModel.hasPublishableChanges()) {
			 List<String>nodeToPublish = new ArrayList<>();
			 visitPublishModel(publishTreeModel.getRootNode(), publishTreeModel, nodeToPublish);

			 publishProcess.createPublishSetFor(nodeToPublish);
			 PublishSetInformations set = publishProcess.testPublishSet(locale);
			 StatusDescription[] status = set.getWarnings();
			 //publish not possible when there are errors
			 for(int i = 0; i < status.length; i++) {
				 if(status[i].isError()) {
					 log.error("Status error by publish: " + status[i].getLongDescription(locale));
					 return;
				 }
			 }

			 try {
				 course = CourseFactory.openCourseEditSession(course.getResourceableId());
				 publishProcess.applyPublishSet(identity, locale, false);
			 } catch(Exception e) {
				 log.error("",  e);
			 } finally {
				 closeCourseEditSession(course.getResourceableId(), true);
			 }
		 }
	}

	/**
	 * Create a user locale dependent help-course run controller
	 *
	 * @param ureq The user request
	 * @param wControl The current window controller
	 * @return The help-course run controller
	 */
	public static Controller createHelpCourseLaunchController(UserRequest ureq, WindowControl wControl) {
		// Find repository entry for this course
		String helpCourseSoftKey = CoreSpringFactory.getImpl(CourseModule.class).getHelpCourseSoftKey();
		RepositoryManager rm = RepositoryManager.getInstance();
		RepositoryService rs = CoreSpringFactory.getImpl(RepositoryService.class);
		RepositoryEntry entry = null;
		if (StringHelper.containsNonWhitespace(helpCourseSoftKey)) {
			entry = rm.lookupRepositoryEntryBySoftkey(helpCourseSoftKey, false);
		}
		if (entry == null) {
			Translator translator = Util.createPackageTranslator(CourseFactory.class, ureq.getLocale());
			wControl.setError(translator.translate("error.helpcourse.not.configured"));
			// create empty main controller
			return new LayoutMain3ColsController(ureq, wControl, null, null, null);
		} else {
			// Increment launch counter
			rs.incrementLaunchCounter(entry);
			ICourse course = loadCourse(entry);

			ContextEntry ce = BusinessControlFactory.getInstance().createContextEntry(entry);
			WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(ce, wControl);
			RepositoryEntrySecurity reSecurity = new RepositoryEntrySecurity(false, false, false, false, false, false, false, false, false, false, false, false, true, false);
			return new RunMainController(ureq, bwControl, null, course, entry, reSecurity, null);
		}
	}

	/**
	 * visit all nodes in the specified course and make them archiving any data
	 * into the identity's export directory.
	 *
	 * @param res
	 * @param charset
	 * @param locale
	 * @param identity
	 */
	public static void archiveCourse(OLATResourceable res, String charset, Locale locale, Identity identity, Roles roles) {
		RepositoryEntry courseRe = RepositoryManager.getInstance().lookupRepositoryEntry(res, false);
		PersistingCourseImpl course = (PersistingCourseImpl) loadCourse(res);
		File exportDirectory = CourseFactory.getOrCreateDataExportDirectory(identity, course.getCourseTitle());
		
		RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class);
		boolean isAdministrator = roles.isAdministrator()
				&& repositoryService.hasRoleExpanded(identity, courseRe, OrganisationRoles.administrator.name());
		boolean isOresOwner = repositoryService.hasRole(identity, courseRe, GroupRoles.owner.name());
		boolean isOresInstitutionalManager = roles.isLearnResourceManager()
				&& repositoryService.hasRoleExpanded(identity, courseRe, OrganisationRoles.learnresourcemanager.name());
		archiveCourse(identity, course, charset, locale, exportDirectory, isAdministrator, isOresOwner, isOresInstitutionalManager);
	}

	/**
	 * visit all nodes in the specified course and make them archiving any data
	 * into the identity's export directory.
	 *
	 * @param res
	 * @param charset
	 * @param locale
	 * @param identity
	 */
	public static void archiveCourse(Identity archiveOnBehalfOf, ICourse course, String charset, Locale locale, File exportDirectory, boolean isAdministrator, boolean... oresRights) {
		// archive course results overview
		List<Identity> users = ScoreAccountingHelper.loadUsers(course.getCourseEnvironment());
		List<CourseNode> nodes = ScoreAccountingHelper.loadAssessableNodes(course.getCourseEnvironment());
		
		String fileName = ExportUtil.createFileNameWithTimeStamp(course.getCourseTitle(), "zip");
		try(OutputStream out = new FileOutputStream(new File(exportDirectory, fileName));
				ZipOutputStream zout = new ZipOutputStream(out)) {
			ScoreAccountingHelper.createCourseResultsOverview(users, nodes, course, locale, zout);
		} catch(IOException e) {
			log.error("", e);
		}

		// archive all nodes content
		Visitor archiveV = new NodeArchiveVisitor(locale, course, exportDirectory, charset);
		TreeVisitor tv = new TreeVisitor(archiveV, course.getRunStructure().getRootNode(), true);
		tv.visitAll();
		// archive all course log files
		//OLATadmin gets all logfiles independent of the visibility configuration
		boolean isOresOwner = (oresRights.length > 0)?oresRights[0]:false;
		boolean isOresInstitutionalManager = (oresRights.length > 1)?oresRights[1]:false;

		boolean aLogV = isOresOwner || isOresInstitutionalManager || isAdministrator;
		boolean uLogV = isAdministrator;
		boolean sLogV = isOresOwner || isOresInstitutionalManager || isAdministrator;

		// make an intermediate commit here to make sure long running course log export doesn't
		// cause db connection timeout to be triggered
		// rework when backgroundjob infrastructure exists
		DBFactory.getInstance().intermediateCommit();
		CoreSpringFactory.getImpl(AsyncExportManager.class).asyncArchiveCourseLogFiles(archiveOnBehalfOf, () -> { /* nothing to do */ },
				course.getResourceableId(), exportDirectory.getPath(), null, null, aLogV, uLogV, sLogV, null, null);

		course.getCourseEnvironment().getCourseGroupManager().archiveCourseGroups(exportDirectory);

		CoreSpringFactory.getImpl(ChatLogHelper.class).archive(course, exportDirectory);

	}

	/**
	 * Returns the data export directory. If the directory does not yet exist the
	 * directory will be created
	 *
	 * @param ureq The user request
	 * @param courseName The course name or title. Will be used as directory name
	 * @return The file representing the dat export directory
	 */
	public static File getOrCreateDataExportDirectory(Identity identity, String courseName) {
		String courseFolder = StringHelper.transformDisplayNameToFileSystemName(courseName);
		// folder where exported user data should be put
		File exportFolder = new File(FolderConfig.getCanonicalRoot() + FolderConfig.getUserHomes() + "/" + identity.getName() + "/private/archive/"
						+ courseFolder);
		if (exportFolder.exists()) {
			if (!exportFolder.isDirectory()) { throw new OLATRuntimeException(ExportUtil.class, "File " + exportFolder.getAbsolutePath()
					+ " already exists but it is not a folder!", null); }
		} else {
			exportFolder.mkdirs();
		}
		return exportFolder;
	}


	/**
	 * Returns the data export directory.
	 *
	 * @param ureq The user request
	 * @param courseName The course name or title. Will be used as directory name
	 * @return The file representing the dat export directory
	 */
	public static File getDataExportDirectory(Identity identity, String courseName) {
		File exportFolder = new File( // folder where exported user data should be
				// put
				FolderConfig.getCanonicalRoot() + FolderConfig.getUserHomes() + "/" + identity.getName() + "/private/archive/"
						+ Formatter.makeStringFilesystemSave(courseName));
		return exportFolder;
	}

	/**
	 * Returns the personal folder of the given identity.
	 * <p>
	 * The idea of this method is to match the first part of what
	 * getOrCreateDataExportDirectory() returns.
	 * <p>
	 * @param identity
	 * @return
	 */
	public static File getPersonalDirectory(Identity identity) {
		if (identity==null) {
			return null;
		}
		return new File(FolderConfig.getCanonicalRoot() + FolderConfig.getUserHomes() + "/" + identity.getName());
	}

	/**
	 * Returns the data export directory. If the directory does not yet exist the
	 * directory will be created
	 *
	 * @param ureq The user request
	 * @param courseName The course name or title. Will be used as directory name
	 * @return The file representing the dat export directory
	 */
	public static File getOrCreateStatisticDirectory(Identity identity, String courseName) {
		File exportFolder = new File( // folder where exported user data should be
				// put
				FolderConfig.getCanonicalRoot() + FolderConfig.getUserHomes() + "/" + identity.getName() + "/private/statistics/"
						+ Formatter.makeStringFilesystemSave(courseName));
		if (exportFolder.exists()) {
			if (!exportFolder.isDirectory()) { throw new OLATRuntimeException(ExportUtil.class, "File " + exportFolder.getAbsolutePath()
					+ " already exists but it is not a folder!", null); }
		} else {
			exportFolder.mkdirs();
		}
		return exportFolder;
	}

	/**
	 * Stores the editor tree model AND the run structure (both xml files). Called at publish.
	 * @param resourceableId
	 */
	public static void saveCourse(final Long resourceableId) {
		if (resourceableId == null) throw new AssertException("No resourceable ID found.");

		PersistingCourseImpl theCourse = getCourseEditSession(resourceableId);
		if(theCourse!=null) {
			//o_clusterOK by: ld (although the course is locked for editing, we still have to insure that load course is synchronized)
			CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(theCourse, new SyncerExecutor(){
				@Override
				public void execute() {
					final PersistingCourseImpl course = getCourseEditSession(resourceableId);
					if(course!=null && course.isReadAndWrite()) {
						course.initHasAssessableNodes();
						course.saveRunStructure();
						course.saveEditorTreeModel();

						//clear modifyCourseEvents at publish, since the updateCourseInCache is called anyway
						modifyCourseEvents.remove(resourceableId);
						updateCourseInCache(resourceableId, course);
					} else if(!course.isReadAndWrite()) {
						throw new AssertException("Cannot saveCourse because theCourse is readOnly! You have to open an courseEditSession first!");
					}
				}
			});
		} else {
			throw new AssertException("Cannot saveCourse because theCourse is null! Have you opened a courseEditSession yet?");
		}
	}

	/**
	 * Stores ONLY the editor tree model (e.g. at course tree editing - add/remove/move course nodes).
	 * @param resourceableId
	 */
	public static void saveCourseEditorTreeModel(Long resourceableId) {
		if (resourceableId == null) throw new AssertException("No resourceable ID found.");

		PersistingCourseImpl course = getCourseEditSession(resourceableId);
		if(course!=null && course.isReadAndWrite()) {
			synchronized(loadedCourses) { //o_clusterOK by: ld (clusterOK since the course is locked for editing)
		    course.saveEditorTreeModel();

		    modifyCourseEvents.putIfAbsent(resourceableId, new ModifyCourseEvent(resourceableId));
			}
		} else if(course==null) {
			throw new AssertException("Cannot saveCourseEditorTreeModel because course is null! Have you opened a courseEditSession yet?");
		} else if(!course.isReadAndWrite()) {
			throw new AssertException("Cannot saveCourse because theCourse is readOnly! You have to open an courseEditSession first!");
		}
	}

	/**
	 * Updates the course cache forcing other cluster nodes to reload this course. <br/>
	 * This is triggered after the course editor is closed. <br/>
	 * It also removes the courseEditSession for this course.
	 *
	 * @param resourceableId
	 */
	public static void fireModifyCourseEvent(Long resourceableId) {
		ModifyCourseEvent modifyCourseEvent = modifyCourseEvents.get(resourceableId);
		if(modifyCourseEvent!=null){
			synchronized(modifyCourseEvents) { //o_clusterOK by: ld
				modifyCourseEvent = modifyCourseEvents.remove(resourceableId);
				if(modifyCourseEvent != null) {
					PersistingCourseImpl course = getCourseEditSession(resourceableId);
			    if(course!=null) {
			    	updateCourseInCache(resourceableId, course);
			    }
				}
			}
		}
		//close courseEditSession if not already closed
		closeCourseEditSession(resourceableId, false);
	}

	/**
	 * Create a custom css object for the course layout. This can then be set on a
	 * MainLayoutController to activate the course layout
	 *
	 * @param usess The user session
	 * @param courseEnvironment the course environment
	 * @return The custom course css or NULL if no course css is available
	 */
	public static CustomCSS getCustomCourseCss(UserSession usess, CourseEnvironment courseEnvironment) {
		CustomCSS customCSS = null;
		CourseConfig courseConfig = courseEnvironment.getCourseConfig();
		if (courseConfig.hasCustomCourseCSS()) {
			// Notify the current tab that it should load a custom CSS
			return CourseLayoutHelper.getCustomCSS(usess, courseEnvironment);
		}
		return customCSS;
	}


	/**
	 * the provided resourceableID must belong to a ICourse.getResourceableId(), otherwise you
	 * risk to use a wrong course base container.
	 * @param resourceableId
	 * @return
	 */
	public static VFSContainer getCourseBaseContainer(Long resourceableId) {
		String relPath = "/course/" + resourceableId.longValue();
		LocalFolderImpl courseRootContainer = VFSManager.olatRootContainer(relPath, null);
		File fBasePath = courseRootContainer.getBasefile();
		if (!fBasePath.exists())
			throw new OLATRuntimeException(PersistingCourseImpl.class, "Could not resolve course base path:" + courseRootContainer, null);
		return courseRootContainer;
	}

	/**
	 * Save courseConfig and update cache.
	 * @param resourceableId
	 * @param cc
	 */
	public static void setCourseConfig(final Long resourceableId, final CourseConfig cc) {
		if (resourceableId == null) throw new AssertException("No resourceable ID found.");

		PersistingCourseImpl theCourse = getCourseEditSession(resourceableId);
		if(theCourse!=null) {
			//o_clusterOK by: ld (although the course is locked for editing, we still have to insure that load course is synchronized)
			CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(theCourse, new SyncerExecutor(){
				@Override
				public void execute() {
					PersistingCourseImpl course = getCourseEditSession(resourceableId);
					if(course!=null) {
						course.setCourseConfig(cc);
						updateCourseInCache(resourceableId, course);
					}
				}
			});
		} else {
			throw new AssertException("Cannot setCourseConfig because theCourse is null! Have you opened a courseEditSession yet?");
		}
	}

	/**
	 * Loads the course or gets it from cache, and adds it to the courseEditSessionMap. <br/>
	 * It guarantees that the returned value is never null. <br/>
	 * The courseEditSession object should live between acquire course lock and release course lock.
	 * 
	 * @param resourceableId The resource id
	 * @return
	 */
	public static PersistingCourseImpl openCourseEditSession(Long resourceableId) {
		PersistingCourseImpl course = courseEditSessionMap.get(resourceableId);
		if(course != null) {
			throw new AssertException("There is already an edit session open for this course: " + resourceableId);
		} else {
			course = (PersistingCourseImpl)loadCourse(resourceableId);
			course.setReadAndWrite(true);
			courseEditSessionMap.put(resourceableId, course);
			log.debug("getCourseEditSession - put course in courseEditSessionMap: {}", resourceableId);
		}
		return course;
	}

	public static boolean isCourseEditSessionOpen(Long resourceableId) {
		return courseEditSessionMap.containsKey(resourceableId);
	}

	/**
	 * Provides the currently edited course object with this id. <br/>
	 * It guarantees that the returned value is never null if the openCourseEditSession was called first. <br/>
	 * The CourseEditSession object should live between acquire course lock and release course lock.
	 *
	 * @param resourceableId
	 * @return
	 */
	public static PersistingCourseImpl getCourseEditSession(Long resourceableId) {
		PersistingCourseImpl course = courseEditSessionMap.get(resourceableId);
		if(course==null) {
			throw new AssertException("No edit session open for this course: " + resourceableId + " - Open a session first!");
		}
		return course;
	}

	public static void closeCourseEditSession(Long resourceableId, boolean checkIfAnyAvailable) {
		PersistingCourseImpl course = courseEditSessionMap.get(resourceableId);
		if(course==null && checkIfAnyAvailable) {
			throw new AssertException("No edit session open for this course: " + resourceableId + " - There is nothing to be closed!");
		}	else if (course!=null) {
		  course.setReadAndWrite(false);
		  courseEditSessionMap.remove(resourceableId);
		  log.debug("removeCourseEditSession for course: {}", resourceableId);
		}
	}

	private static void visitPublishModel(TreeNode node, PublishTreeModel publishTreeModel, Collection<String> nodeToPublish) {
		int numOfChildren = node.getChildCount();
		for (int i = 0; i < numOfChildren; i++) {
			INode child = node.getChildAt(i);
			if (child instanceof TreeNode && publishTreeModel.isVisible(child)) {
				nodeToPublish.add(child.getIdent());
				visitPublishModel((TreeNode)child, publishTreeModel, nodeToPublish);
			}
		}
	}

	private static class NodeArchiveVisitor implements Visitor {
		private File exportPath;
		private Locale locale;
		private ICourse course;
		private String charset;

		/**
		 * @param locale
		 * @param course
		 * @param exportPath
		 * @param charset
		 */
		public NodeArchiveVisitor(Locale locale, ICourse course, File exportPath, String charset) {
			this.locale = locale;
			this.exportPath = exportPath;
			//o_clusterOk by guido: save to hold reference to course inside editor
			this.course = course;
			this.charset = charset;
		}

		@Override
		public void visit(INode node) {
			CourseNode cn = (CourseNode) node;

			String archiveName = cn.getType() + "_"
					+ StringHelper.transformDisplayNameToFileSystemName(cn.getShortName())
					+ "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis()));

			File exportFile = new File(exportPath, archiveName);
			try(FileOutputStream fileStream = new FileOutputStream(exportFile);
					ZipOutputStream exportStream = new ZipOutputStream(fileStream);) {
				cn.archiveNodeData(locale, course, null, exportStream, "", charset);
			} catch (IOException e) {
				log.error("", e);
			}
		}
	}

	private static class NodeDeletionVisitor implements Visitor {

		private ICourse course;

		/**
		 * Constructor of the node deletion visitor
		 *
		 * @param course
		 */
		public NodeDeletionVisitor(ICourse course) {
			this.course = course;
		}

		/**
		 * Visitor pattern to delete the course nodes
		 *
		 * @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
		 */
		@Override
		public void visit(INode node) {
			CourseNode cNode = (CourseNode) node;
			cNode.cleanupOnDelete(course);
		}
	}
}

/**
 *
 * Description:<br>
 * Event triggered if a course was edited - namely the course tree model have changed
 * (e.g. nodes added, deleted)
 *
 * <P>
 * Initial Date:  22.07.2008 <br>
 * @author Lavinia Dumitrescu
 */
class ModifyCourseEvent extends MultiUserEvent {
	private static final long serialVersionUID = -2940724437608086461L;
	private final Long courseId;
	/**
	 * @param command
	 */
	public ModifyCourseEvent(Long resourceableId) {
		super("modify_course");
		courseId = resourceableId;
	}

	public Long getCourseId() {
		return courseId;
	}
}