Skip to content
Snippets Groups Projects
CourseFactory.java 53.7 KiB
Newer Older
Alan Moran's avatar
Alan Moran committed
/**
* 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.
srosse's avatar
srosse committed
* <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.
*/
Alan Moran's avatar
Alan Moran committed

package org.olat.course;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collection;
Alan Moran's avatar
Alan Moran committed
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipOutputStream;
Alan Moran's avatar
Alan Moran committed

import org.apache.poi.util.IOUtils;
Alan Moran's avatar
Alan Moran committed
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.BaseSecurityManager;
import org.olat.basesecurity.Constants;
import org.olat.basesecurity.SecurityGroup;
import org.olat.commons.calendar.CalendarManager;
import org.olat.commons.calendar.CalendarManagerFactory;
import org.olat.commons.calendar.notification.CalendarNotificationManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.commons.calendar.ui.components.KalendarRenderWrapper;
import org.olat.core.CoreSpringFactory;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.htmlheader.jscss.CustomCSS;
import org.olat.core.gui.components.stack.StackedController;
import org.olat.core.gui.components.tree.TreeNode;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.layout.MainLayoutController;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
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.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.manager.BasicManager;
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;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.UserSession;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.ZipUtil;
import org.olat.core.util.cache.CacheWrapper;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.SyncerCallback;
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.notifications.NotificationsManager;
import org.olat.core.util.notifications.Publisher;
import org.olat.core.util.notifications.SubscriptionContext;
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.VFSConstants;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSStatus;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.util.xml.XStreamHelper;
import org.olat.course.archiver.ScoreAccountingHelper;
import org.olat.course.assessment.manager.UserCourseInformationsManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.course.config.CourseConfig;
import org.olat.course.config.CourseConfigManagerImpl;
srosse's avatar
srosse committed
import org.olat.course.config.ui.courselayout.CourseLayoutHelper;
Alan Moran's avatar
Alan Moran committed
import org.olat.course.editor.EditorMainController;
import org.olat.course.editor.PublishProcess;
import org.olat.course.editor.StatusDescription;
Alan Moran's avatar
Alan Moran committed
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.groupsandrights.PersistingCourseGroupManager;
import org.olat.course.nodes.AssessableCourseNode;
Alan Moran's avatar
Alan Moran committed
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.properties.CoursePropertyManager;
import org.olat.course.properties.PersistingCoursePropertyManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.course.repository.ImportGlossaryReferencesController;
import org.olat.course.repository.ImportSharedfolderReferencesController;
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;
Alan Moran's avatar
Alan Moran committed
import org.olat.group.BusinessGroup;
import org.olat.instantMessaging.InstantMessagingService;
import org.olat.instantMessaging.manager.ChatLogHelper;
Alan Moran's avatar
Alan Moran committed
import org.olat.modules.glossary.GlossaryManager;
import org.olat.modules.sharedfolder.SharedFolderManager;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryEntryImportExport;
import org.olat.repository.RepositoryManager;
import org.olat.resource.OLATResource;
import org.olat.resource.OLATResourceManager;
import org.olat.resource.references.ReferenceImpl;
import org.olat.resource.references.ReferenceManager;
import org.olat.testutils.codepoints.server.Codepoint;
Alan Moran's avatar
Alan Moran committed
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 extends BasicManager {
		
	private static CacheWrapper<Long,PersistingCourseImpl> loadedCourses;
	private static ConcurrentHashMap<Long, ModifyCourseEvent> modifyCourseEvents = new ConcurrentHashMap<Long, ModifyCourseEvent>();
Alan Moran's avatar
Alan Moran committed

	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<Long,PersistingCourseImpl>();
Alan Moran's avatar
Alan Moran committed
	private static OLog log = Tracing.createLoggerFor(CourseFactory.class);
	private static RepositoryManager repositoryManager;
	private static OLATResourceManager olatResourceManager;
	private static BaseSecurity securityManager;
	private static ReferenceManager referenceManager;
	private static GlossaryManager glossaryManager;
	
	
	/**
	 * [used by spring]
	 */
	private CourseFactory(CoordinatorManager coordinatorManager, RepositoryManager repositoryManager, OLATResourceManager olatResourceManager, 
			BaseSecurity securityManager, ReferenceManager referenceManager, GlossaryManager glossaryManager) {
		loadedCourses = coordinatorManager.getCoordinator().getCacher().getCache(CourseFactory.class.getSimpleName(), "courses");
		CourseFactory.repositoryManager = repositoryManager;
		CourseFactory.olatResourceManager = olatResourceManager;
		CourseFactory.securityManager = securityManager;
		CourseFactory.referenceManager = referenceManager;
		CourseFactory.glossaryManager = glossaryManager;
Alan Moran's avatar
Alan Moran committed
	}
	
	/**
	 * Create a run controller for the given course resourceable
	 * 
	 * @param ureq
	 * @param wControl
Alan Moran's avatar
Alan Moran committed
	 * @param initialViewIdentifier if null the default view will be started,
	 *          otherwise a controllerfactory type dependant view will be
	 *          activated (subscription subtype)
	 * @return run controller for the given course resourceable
	 */
	public static MainLayoutController createLaunchController(UserRequest ureq, WindowControl wControl, final RepositoryEntry re) {
		ICourse course = loadCourse(re.getOlatResource());
Alan Moran's avatar
Alan Moran committed
		long startT = 0;
		if(isDebug){
			startT = System.currentTimeMillis();
		}
		MainLayoutController launchC = new RunMainController(ureq, wControl, course, re, true, true);
Alan Moran's avatar
Alan Moran committed
		if(isDebug){
			log.debug("Runview for [["+course.getCourseTitle()+"]] took [ms]"+(System.currentTimeMillis() - startT));
Alan Moran's avatar
Alan Moran committed
		}
		
		return launchC;
	}

	/**
	 * Create an editor controller for the given course resourceable
	 * 
	 * @param ureq
	 * @param wControl
	 * @param olatResource
	 * @return editor controller for the given course resourceable; if the editor
	 *         is already locked, it returns a controller with a lock message
	 */
	public static Controller createEditorController(UserRequest ureq, WindowControl wControl, StackedController stack,
			OLATResourceable olatResource, CourseNode selectedNode) {
Alan Moran's avatar
Alan Moran committed
		ICourse course = loadCourse(olatResource);
		EditorMainController emc = new EditorMainController(ureq, wControl, course, stack, selectedNode);
Alan Moran's avatar
Alan Moran committed
		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 }));
Alan Moran's avatar
Alan Moran committed
			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.
	 * 
	 * @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 createEmptyCourse(OLATResourceable ores, String shortTitle, String longTitle, String learningObjectives) {
		PersistingCourseImpl newCourse = new PersistingCourseImpl(ores.getResourceableId());
		// Put new course in course cache    
		putCourseInCache(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();

		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(final Long resourceableId) {
		if (resourceableId == null) throw new AssertException("No resourceable ID found.");
		PersistingCourseImpl course = getCourseFromCache(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
			if (log.isDebug()) log.debug("try to load course with resourceableId=" + resourceableId);
			OLATResourceable courseResourceable = OresHelper.createOLATResourceableInstance(PersistingCourseImpl.class, resourceableId);
			course = CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(courseResourceable, new SyncerCallback<PersistingCourseImpl>() {
				public PersistingCourseImpl execute() {
					PersistingCourseImpl theCourse = null;
					theCourse = getCourseFromCache(resourceableId);
					if (theCourse == null) {
						long startTime = 0;
						long endTime = 0;
						if (log.isDebug()) startTime = System.currentTimeMillis();
						theCourse = new PersistingCourseImpl(resourceableId);
						theCourse.load();
						if (log.isDebug()) endTime = System.currentTimeMillis();
						putCourseInCache(resourceableId, theCourse);
						long diff = 0;
						if (log.isDebug()) diff = Long.valueOf(endTime - startTime);
						if (log.isDebug()) 	log.debug("[[" + resourceableId + "[[" + diff + "[[" + theCourse.getCourseTitle());
					}
					return 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
	 * @return the PersistingCourseImpl instance for the input key.
	 */
	static PersistingCourseImpl getCourseFromCache(Long resourceableId) {	//o_clusterOK by:ld    
		return loadedCourses.get(resourceableId);
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * Puts silent.
	 * @param resourceableId
	 * @param course
	 */
	static void putCourseInCache(Long resourceableId, PersistingCourseImpl course) { //o_clusterOK by:ld    
		loadedCourses.put(resourceableId, course);
		log.debug("putCourseInCache ");
Alan Moran's avatar
Alan Moran committed
	}
			
	/**
	 * 
	 * @param resourceableId
	 */
	private static void removeFromCache(Long resourceableId) { //o_clusterOK by: ld
		loadedCourses.remove(resourceableId);	
		log.debug("removeFromCache");
Alan Moran's avatar
Alan Moran committed
	}
	
	/**
	 * 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");
Alan Moran's avatar
Alan Moran committed
	}

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

		// find all references to course
		List<ReferenceImpl> refs = referenceManager.getReferences(res);
		for (Iterator<ReferenceImpl> iter = refs.iterator(); iter.hasNext();) {
Alan Moran's avatar
Alan Moran committed
			ReferenceImpl ref = (ReferenceImpl) iter.next();
			referenceManager.delete(ref);
		}
		
		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();
		}

Alan Moran's avatar
Alan Moran committed
		// 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);
		// delete course configuration (not really usefull, the config is in
		// the course folder which is deleted right after)
		if(course != null) {
			CourseConfigManagerImpl.getInstance().deleteConfigOf(course);
		}
		//clean up tasks
		OLATResource resource = course.getCourseEnvironment().getCourseGroupManager().getCourseResource();
		CoreSpringFactory.getImpl(TaskExecutorManager.class).delete(resource);
		
Alan Moran's avatar
Alan Moran committed
		// delete course group- and rightmanagement
		CourseGroupManager courseGroupManager = PersistingCourseGroupManager.getInstance(res);
		courseGroupManager.deleteCourseGroupmanagement();
Alan Moran's avatar
Alan Moran committed
		// delete all remaining course properties
		CoursePropertyManager propertyManager = PersistingCoursePropertyManager.getInstance(res);
		propertyManager.deleteAllCourseProperties();
Alan Moran's avatar
Alan Moran committed
		// delete course calendar
		CalendarManager calManager = CalendarManagerFactory.getInstance().getCalendarManager();
		calManager.deleteCourseCalendar(res);
		// delete IM messages
		CoreSpringFactory.getImpl(InstantMessagingService.class).deleteMessages(res);
Alan Moran's avatar
Alan Moran committed
		// cleanup cache
		removeFromCache(res.getResourceableId());
		//TODO: ld: broadcast event: DeleteCourseEvent

		// 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.delete();
		boolean deletionSuccessful = (status == VFSConstants.YES || status == VFSConstants.SUCCESS);
Alan Moran's avatar
Alan Moran committed
		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) {
Alan Moran's avatar
Alan Moran committed
		//set Publisher state to 1 (= ressource is deleted) for all calendars of the course
		CalendarManager calMan = CalendarManagerFactory.getInstance().getCalendarManager();
		CalendarNotificationManager notificationManager = CoreSpringFactory.getImpl(CalendarNotificationManager.class);
Alan Moran's avatar
Alan Moran committed
		NotificationsManager nfm = NotificationsManager.getInstance();
		CourseGroupManager courseGroupManager = PersistingCourseGroupManager.getInstance(res);
		List<BusinessGroup> learningGroups = courseGroupManager.getAllBusinessGroups();
Alan Moran's avatar
Alan Moran committed
		//all learning and right group calendars
		for (BusinessGroup bg : learningGroups) {
			KalendarRenderWrapper calRenderWrapper = calMan.getGroupCalendar(bg);
			SubscriptionContext subsContext = notificationManager.getSubscriptionContext(calRenderWrapper);
Alan Moran's avatar
Alan Moran committed
			Publisher pub = nfm.getPublisher(subsContext);
			if (pub != null) {
				pub.setState(1); //int 0 is OK -> all other is not OK
			}
		}
		//the course calendar
		try {
			/**
			 * TODO:gs 2010-01-26
			 * OLAT-4947: if we do not have an repo entry we get an exception here. 
			 * This is normal in the case of courseimport and click canceling.
			 */
			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);
Alan Moran's avatar
Alan Moran committed
			}
		} 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, UserRequest ureq) {
		
		PersistingCourseImpl sourceCourse = (PersistingCourseImpl) loadCourse(sourceRes);

		OLATResourceable targetRes = OLATResourceManager.getInstance().createOLATResourceInstance(CourseModule.class);
		PersistingCourseImpl targetCourse = new PersistingCourseImpl(targetRes.getResourceableId());
		File fTargetCourseBasePath = targetCourse.getCourseBaseContainer().getBasefile();
		
		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 = CourseConfigManagerImpl.getInstance().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();
			
			Codepoint.codepoint(CourseFactory.class, "copyCourseAfterSaveTreeModel");

			// copy course folder
			File fSourceCourseFolder = sourceCourse.getIsolatedCourseFolder().getBasefile();
			if (fSourceCourseFolder.exists()) FileUtils.copyDirToDir(fSourceCourseFolder, fTargetCourseBasePath, false, "copy course folder");
Alan Moran's avatar
Alan Moran committed
			// copy folder nodes directories
			File fSourceFoldernodesFolder = new File(FolderConfig.getCanonicalRoot()
					+ BCCourseNode.getFoldernodesPathRelToFolderBase(sourceCourse.getCourseEnvironment()));
			if (fSourceFoldernodesFolder.exists()) FileUtils.copyDirToDir(fSourceFoldernodesFolder, fTargetCourseBasePath, false, "copy folder nodes directories");

			// 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");

			//make sure the DB connection is available after this point
			DBFactory.getInstance(false).commitAndCloseSession();
			
Alan Moran's avatar
Alan Moran committed
			// update references
			List<ReferenceImpl> refs = referenceManager.getReferences(sourceCourse);
			int count = 0;
			for (ReferenceImpl ref: refs) {
Alan Moran's avatar
Alan Moran committed
				referenceManager.addReference(targetCourse, ref.getTarget(), ref.getUserdata());
				if(count % 20 == 0) {
					DBFactory.getInstance(false).intermediateCommit();
				}
Alan Moran's avatar
Alan Moran committed
			}
		}
		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, boolean backwardsCompatible) {
Alan Moran's avatar
Alan Moran committed
		PersistingCourseImpl sourceCourse = (PersistingCourseImpl) loadCourse(sourceRes);

		// add files to ZIP
		File fExportDir = new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID());
Alan Moran's avatar
Alan Moran committed
		fExportDir.mkdirs();
		log.info("Export folder: " + fExportDir);
Alan Moran's avatar
Alan Moran committed
		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, backwardsCompatible);
Alan Moran's avatar
Alan Moran committed
			Set<String> fileSet = new HashSet<String>();
			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);
Alan Moran's avatar
Alan Moran committed
			FileUtils.deleteDirsAndFiles(fExportDir, true, true);
		}
	}

	/**
	 * Import a course from a ZIP file.
	 * 
	 * @param ores
	 * @param zipFile
	 * @return New Course.
	 */
	public static ICourse importCourseFromZip(OLATResourceable ores, File zipFile) {
		// Generate course with filesystem
		PersistingCourseImpl newCourse = new PersistingCourseImpl(ores.getResourceableId());
		CourseConfigManagerImpl.getInstance().deleteConfigOf(newCourse);
		
		// Unzip course strucure in new course
		File fCanonicalCourseBasePath = newCourse.getCourseBaseContainer().getBasefile();
		if (ZipUtil.unzip(zipFile, fCanonicalCourseBasePath)) {
			// Load course strucure now
			try {
				newCourse.load();
				CourseConfig cc = CourseConfigManagerImpl.getInstance().loadConfigFor(newCourse);								
				//newCourse is not in cache yet, so we cannot call setCourseConfig()
				newCourse.setCourseConfig(cc);
				putCourseInCache(newCourse.getResourceableId(), newCourse);						
				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;
	}

	/**
	 * Deploys a course from an exported course ZIP file. This process is unatended and
	 * therefore relies on some default assumptions on how to setup the entry and add
	 * any referenced resources to the repository.
	 * 
	 * @param exportedCourseZIPFile
	 */
	public static RepositoryEntry deployCourseFromZIP(File exportedCourseZIPFile, int access) {
		return deployCourseFromZIP(exportedCourseZIPFile, "administrator", null, access);
	}
	
	public static RepositoryEntry deployCourseFromZIP(File exportedCourseZIPFile, String initialAuthor, String softKey, int access) {
Alan Moran's avatar
Alan Moran committed
		// create the course instance
		OLATResource newCourseResource = olatResourceManager.createOLATResourceInstance(CourseModule.class);
		ICourse course = CourseFactory.importCourseFromZip(newCourseResource, exportedCourseZIPFile);
		// course is now also in course cache!
		if (course == null) {
			log.error("Error deploying course from ZIP: " + exportedCourseZIPFile.getAbsolutePath());
Alan Moran's avatar
Alan Moran committed
			return null;
		}
		File courseExportData = course.getCourseExportDataDir().getBasefile();
Alan Moran's avatar
Alan Moran committed
		// get the export data directory
		// create the repository entry
		RepositoryEntry re = repositoryManager.createRepositoryEntryInstance("administrator");
		RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(courseExportData);
		if(!StringHelper.containsNonWhitespace(softKey)) {
			softKey = importExport.getSoftkey();
		}
Alan Moran's avatar
Alan Moran committed
		RepositoryEntry existingEntry = repositoryManager.lookupRepositoryEntryBySoftkey(softKey, false);
		if (existingEntry != null) {
			log.info("RepositoryEntry with softkey " + softKey + " already exists. Course will not be deployed.");
			//seem to be a problem
			UserCourseInformationsManager userCourseInformationsManager = CoreSpringFactory.getImpl(UserCourseInformationsManager.class);
			userCourseInformationsManager.deleteUserCourseInformations(existingEntry);
Alan Moran's avatar
Alan Moran committed
			CourseFactory.deleteCourse(newCourseResource);
			return existingEntry;
		}
		// ok, continue import
		newCourseResource = olatResourceManager.findOrPersistResourceable(newCourseResource);
		re.setOlatResource(newCourseResource);
		re.setSoftkey(softKey);
		re.setInitialAuthor(importExport.getInitialAuthor());
		re.setDisplayname(importExport.getDisplayName());
		re.setResourcename(importExport.getResourceName());
		re.setDescription(importExport.getDescription());
		re.setCanLaunch(true);
		
		// set access configuration
		re.setAccess(access);

		// create security group
		SecurityGroup ownerGroup = securityManager.createAndPersistSecurityGroup();
		// member of this group may modify member's membership
		securityManager.createAndPersistPolicy(ownerGroup, Constants.PERMISSION_ACCESS, ownerGroup);
		// members of this group are always authors also
		securityManager.createAndPersistPolicy(ownerGroup, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_AUTHOR);
		securityManager.addIdentityToSecurityGroup(securityManager.findIdentityByName("administrator"), ownerGroup);
		re.setOwnerGroup(ownerGroup);
		// save the repository entry
		repositoryManager.saveRepositoryEntry(re);
		// Create course admin policy for owner group of repository entry
		// -> All owners of repository entries are course admins
		securityManager.createAndPersistPolicy(re.getOwnerGroup(), Constants.PERMISSION_ADMIN, re.getOlatResource());
		
		//fxdiff VCRP-1,2: access control of resources
		// create security group for tutors / coaches
		SecurityGroup tutorGroup = securityManager.createAndPersistSecurityGroup();
		// member of this group may modify member's membership
		securityManager.createAndPersistPolicy(tutorGroup, Constants.PERMISSION_ACCESS, re.getOlatResource());
		securityManager.createAndPersistPolicy(tutorGroup, Constants.PERMISSION_COACH, re.getOlatResource());
		// members of this group are always tutors also
		securityManager.createAndPersistPolicy(tutorGroup, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_TUTOR);
		re.setTutorGroup(tutorGroup);
	
		// create security group for participants
		SecurityGroup participantGroup = securityManager.createAndPersistSecurityGroup();
		// member of this group may modify member's membership
		securityManager.createAndPersistPolicy(participantGroup, Constants.PERMISSION_ACCESS, re.getOlatResource());
		securityManager.createAndPersistPolicy(participantGroup, Constants.PERMISSION_PARTI, re.getOlatResource());
		// members of this group are always participants also
		securityManager.createAndPersistPolicy(participantGroup, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_PARTICIPANT);
		re.setParticipantGroup(participantGroup);
		
		//import groups
		course = openCourseEditSession(course.getResourceableId());
		// create group management
		CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager();
		// import groups
		cgm.importCourseBusinessGroups(courseExportData);
Alan Moran's avatar
Alan Moran committed
		// deploy any referenced repository entries of the editor structure. This will also
		// include any references in the run structure, since any node in the runstructure is also
		// present in the editor structure.
		deployReferencedRepositoryEntries(courseExportData, course,
				(CourseEditorTreeNode)course.getEditorTreeModel().getRootNode());
		// register any references in the run structure. The referenced entries have been 
		// previousely deplyed (as part of the editor structure deployment process - see above method call)
		registerReferences(course, course.getRunStructure().getRootNode());
		// import shared folder references
		deployReferencedSharedFolders(courseExportData, course);
		// import glossary references
		deployReferencedGlossary(courseExportData, course);
		closeCourseEditSession(course.getResourceableId(), true);
		// cleanup export data
		FileUtils.deleteDirsAndFiles(courseExportData, true, true);
		log.info("Successfully deployed course " + re.getDisplayname() + " from ZIP: " + exportedCourseZIPFile.getAbsolutePath());
Alan Moran's avatar
Alan Moran committed
		return re;
	}
	
	
	/**
	 * Unattended deploy any referenced repository entries.
	 * 
	 * @param importDirectory
	 * @param course
	 * @param currentNode
	 */
	private static void deployReferencedRepositoryEntries(File importDirectory, ICourse course, CourseEditorTreeNode currentNode) {
		for (int i = 0; i < currentNode.getChildCount(); i++) {
			CourseEditorTreeNode childNode = (CourseEditorTreeNode)currentNode.getChildAt(i);
			childNode.getCourseNode().importNode(importDirectory, course, true, null, null);
			deployReferencedRepositoryEntries(importDirectory, course, childNode);
		}
	}
	
	/**
	 * Register any referenced repository entries.
	 * @param course
	 * @param currentNode
	 */
	private static void registerReferences(ICourse course, CourseNode currentNode) {
		for (int i = 0; i < currentNode.getChildCount(); i++) {
			CourseNode childNode = (CourseNode)currentNode.getChildAt(i);
			if (childNode.needsReferenceToARepositoryEntry()) {
				referenceManager.addReference(course,
					childNode.getReferencedRepositoryEntry().getOlatResource(), childNode.getIdent());
			}
			registerReferences(course, childNode);
		}
	}
	
	private static void deployReferencedSharedFolders(File importDirectory, ICourse course) {
		CourseConfig cc = course.getCourseEnvironment().getCourseConfig();
		if (!cc.hasCustomSharedFolder()) return;
		RepositoryEntryImportExport importExport = SharedFolderManager.getInstance()
			.getRepositoryImportExport(importDirectory);
		Identity owner = BaseSecurityManager.getInstance().findIdentityByName("administrator");
		ImportSharedfolderReferencesController.doImport(importExport, course, false, owner);
	}

	/**
	 * Deploy referenced glossaries using the administrator account as owner
	 * @param importDirectory
	 * @param course
	 */
	private static void deployReferencedGlossary(File importDirectory, ICourse course) {
		CourseConfig cc = course.getCourseEnvironment().getCourseConfig();
		if (!cc.hasGlossary()) return;
		RepositoryEntryImportExport importExport = glossaryManager.getRepositoryImportExport(importDirectory);
		Identity owner = securityManager.findIdentityByName("administrator");
		ImportGlossaryReferencesController.doImport(importExport, course, false, owner);
	}
	
	/**
	 * Publish the course with some standard options
	 * @param course
	 * @param locale
	 * @param identity
	 */
	public static void publishCourse(ICourse course, int access, boolean membersOnly, Identity identity, Locale locale) {
		 CourseEditorTreeModel cetm = course.getEditorTreeModel();
		 PublishProcess publishProcess = PublishProcess.getInstance(course, cetm, locale);
		 PublishTreeModel publishTreeModel = publishProcess.getPublishTreeModel();

		 int newAccess = (access < RepositoryEntry.ACC_OWNERS || access > RepositoryEntry.ACC_USERS_GUESTS)
				 ? RepositoryEntry.ACC_USERS : access;
		 //access rule -> all users can the see course
		 //RepositoryEntry.ACC_OWNERS
		 //only owners can the see course
		 //RepositoryEntry.ACC_OWNERS_AUTHORS //only owners and authors can the see course
		 //RepositoryEntry.ACC_USERS_GUESTS // users and guests can see the course
		 //fxdiff VCRP-1,2: access control of resources
		 publishProcess.changeGeneralAccess(null, newAccess, membersOnly);
		 
		 if (publishTreeModel.hasPublishableChanges()) {
			 List<String>nodeToPublish = new ArrayList<String>();
			 visitPublishModel(publishTreeModel.getRootNode(), publishTreeModel, nodeToPublish);

			 publishProcess.createPublishSetFor(nodeToPublish);
			 StatusDescription[] status = publishProcess.testPublishSet(locale);
			 //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);
			 } catch(Exception e) {
				 log.error("",  e);
			 } finally {
				 closeCourseEditSession(course.getResourceableId(), true);
			 }
Alan Moran's avatar
Alan Moran committed
	/**
	 * Get a details form for a given course resourceable
	 * 
	 * @param res
	 * @param ureq
	 * @return details component displaying details of the course.
	 */
	public static Controller getDetailsForm(UserRequest ureq, WindowControl wControl, OLATResourceable res) {
		// course does not provide a details component, this is somehow hardcoded in the
		// RepositoryDetailsController
		return null;
	}

	/**
	 * 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 = CourseModule.getHelpCourseSoftKey();
		RepositoryManager rm = RepositoryManager.getInstance();
		RepositoryEntry entry = null;
		if (StringHelper.containsNonWhitespace(helpCourseSoftKey)) {
Alan Moran's avatar
Alan Moran committed
			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
			LayoutMain3ColsController emptyCtr = new LayoutMain3ColsController(ureq, wControl, null, null, null, null);
			return emptyCtr;
		} else {
			// Increment launch counter
			rm.incrementLaunchCounter(entry);
			OLATResource ores = entry.getOlatResource();
			ICourse course = loadCourse(ores);
			
			ContextEntry ce = BusinessControlFactory.getInstance().createContextEntry(entry);
			WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(ce, wControl);	
			
			RunMainController launchC = new RunMainController(ureq, bwControl, course, entry, false, false);
Alan Moran's avatar
Alan Moran committed
			return launchC;			
		}		
	}

	/**
	 * 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) {
		RepositoryEntry courseRe = RepositoryManager.getInstance().lookupRepositoryEntry(res, false);
Alan Moran's avatar
Alan Moran committed
		PersistingCourseImpl course = (PersistingCourseImpl) loadCourse(res);
		File exportDirectory = CourseFactory.getOrCreateDataExportDirectory(identity, course.getCourseTitle());
		boolean isOLATAdmin = BaseSecurityManager.getInstance().isIdentityPermittedOnResourceable(identity, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_ADMIN);
		boolean isOresOwner = RepositoryManager.getInstance().isOwnerOfRepositoryEntry(identity, courseRe);
		boolean isOresInstitutionalManager = RepositoryManager.getInstance().isInstitutionalRessourceManagerFor(courseRe, identity);
Alan Moran's avatar
Alan Moran committed
		archiveCourse(identity, course, charset, locale, exportDirectory, isOLATAdmin, 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 isOLATAdmin, boolean... oresRights) {
		// archive course results overview
		List<Identity> users = ScoreAccountingHelper.loadUsers(course.getCourseEnvironment());
		List<AssessableCourseNode> nodes = ScoreAccountingHelper.loadAssessableNodes(course.getCourseEnvironment());
Alan Moran's avatar
Alan Moran committed
		
		String result = ScoreAccountingHelper.createCourseResultsOverviewTable(users, nodes, course, locale);
		String fileName = ExportUtil.createFileNameWithTimeStamp(course.getCourseTitle(), "xls");
		ExportUtil.writeContentToFile(fileName, result, exportDirectory, charset);
		
		// 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 || isOLATAdmin;
		boolean uLogV = isOLATAdmin;
		boolean sLogV = isOresOwner || isOresInstitutionalManager || isOLATAdmin;
		
		// make an intermediate commit here to make sure long running course log export doesn't 
		// cause db connection timeout to be triggered
		//@TODO transactions/backgroundjob:
		// rework when backgroundjob infrastructure exists
		DBFactory.getInstance(false).intermediateCommit();
		AsyncExportManager.getInstance().asyncArchiveCourseLogFiles(archiveOnBehalfOf, new Runnable() {
			public void run() {
				// that's fine, I dont need to do anything here
			};
		}, course.getResourceableId(), exportDirectory.getPath(), null, null, aLogV, uLogV, sLogV, charset, null, null);

		PersistingCourseGroupManager.getInstance(course).archiveCourseGroups(exportDirectory);
		
		CoreSpringFactory.getImpl(ChatLogHelper.class).archive(course, exportDirectory);
		
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * 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);
Alan Moran's avatar
Alan Moran committed
		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(){