Newer
Older
/**
* 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.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
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 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.ui.components.KalendarRenderWrapper;
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.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.htmlheader.jscss.CustomCSS;
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.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;
import org.olat.core.util.UserSession;
import org.olat.core.util.Util;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.cache.n.CacheWrapper;
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;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.course.archiver.ScoreAccountingHelper;
import org.olat.course.assessment.manager.UserCourseInformationsManager;
import org.olat.course.config.CourseConfig;
import org.olat.course.config.CourseConfigManagerImpl;
import org.olat.course.config.ui.courselayout.CourseLayoutHelper;
import org.olat.course.editor.PublishProcess;
import org.olat.course.editor.StatusDescription;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.groupsandrights.PersistingCourseGroupManager;
import org.olat.course.nodes.AssessableCourseNode;
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;
import org.olat.course.repository.ImportCourseController;
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;
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import org.olat.group.BusinessGroup;
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;
import org.olat.util.logging.activity.LoggingResourceable;
import de.bps.olat.util.notifications.SubscriptionProvider;
import de.bps.olat.util.notifications.SubscriptionProviderImpl;
/**
* 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 loadedCourses;
private static ConcurrentHashMap <Long, ModifyCourseEvent> modifyCourseEvents = new ConcurrentHashMap <Long, ModifyCourseEvent>();
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 HashMap<Long,PersistingCourseImpl>();
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().getOrCreateCache(CourseFactory.class, "courses");
CourseFactory.repositoryManager = repositoryManager;
CourseFactory.olatResourceManager = olatResourceManager;
CourseFactory.securityManager = securityManager;
CourseFactory.referenceManager = referenceManager;
CourseFactory.glossaryManager = glossaryManager;
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
}
/**
* Create a run controller for the given course resourceable
*
* @param ureq
* @param wControl
* @param olatResource
* @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 OLATResourceable olatResource,
String initialViewIdentifier) {
ICourse course = loadCourse(olatResource);
boolean isDebug = Tracing.isDebugEnabled(CourseFactory.class);
long startT = 0;
if(isDebug){
startT = System.currentTimeMillis();
}
MainLayoutController launchC = new RunMainController(ureq, wControl, course, initialViewIdentifier, true, true);
if(isDebug){
Tracing.logDebug("Runview for [["+course.getCourseTitle()+"]] took [ms]"+(System.currentTimeMillis() - startT), CourseFactory.class);
}
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, OLATResourceable olatResource) {
ICourse course = loadCourse(olatResource);
EditorMainController emc = new EditorMainController(ureq, wControl, course);
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());
wControl.setWarning(translator.translate("error.editoralreadylocked", new String[] { emc.getLockEntry().getOwner().getName() }));
return null;
//return new MonologController(ureq.getLocale(), translator.translate("error.editoralreadylocked", new String[] { emc.getLockEntry()
// .getOwner().getName() }), null, true);
}
//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 (PersistingCourseImpl)loadedCourses.get(String.valueOf(resourceableId.longValue()));
}
/**
* Puts silent.
* @param resourceableId
* @param course
*/
static void putCourseInCache(Long resourceableId, PersistingCourseImpl course) { //o_clusterOK by:ld
loadedCourses.put(String.valueOf(resourceableId.longValue()), course);
Tracing.logDebug("putCourseInCache ", CourseFactory.class);
}
/**
*
* @param resourceableId
*/
private static void removeFromCache(Long resourceableId) { //o_clusterOK by: ld
loadedCourses.remove(String.valueOf(resourceableId.longValue()));
Tracing.logDebug("removeFromCache ", CourseFactory.class);
}
/**
* 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(String.valueOf(resourceableId.longValue()), course);
Tracing.logDebug("updateCourseInCache ", CourseFactory.class);
}
/**
* 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();) {
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();
}
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);
}
CourseGroupManager courseGroupManager = PersistingCourseGroupManager.getInstance(res);
courseGroupManager.deleteCourseGroupmanagement();
CoursePropertyManager propertyManager = PersistingCoursePropertyManager.getInstance(res);
propertyManager.deleteAllCourseProperties();
// delete course calendar
CalendarManager calManager = CalendarManagerFactory.getInstance().getCalendarManager();
calManager.deleteCourseCalendar(res);
// 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);
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) {
//set Publisher state to 1 (= ressource is deleted) for all calendars of the course
CalendarManager calMan = CalendarManagerFactory.getInstance().getCalendarManager();
NotificationsManager nfm = NotificationsManager.getInstance();
CourseGroupManager courseGroupManager = PersistingCourseGroupManager.getInstance(res);
List<BusinessGroup> learningGroups = courseGroupManager.getAllLearningGroupsFromAllContexts();
List<BusinessGroup> rightGroups = courseGroupManager.getAllRightGroupsFromAllContexts();
learningGroups.addAll(rightGroups);
//all learning and right group calendars
for (BusinessGroup bg : learningGroups) {
KalendarRenderWrapper calRenderWrapper = calMan.getGroupCalendar(bg);
SubscriptionProvider subProvider = new SubscriptionProviderImpl(calRenderWrapper);
SubscriptionContext subsContext = subProvider.getSubscriptionContext();
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) {
SubscriptionProvider subProvider = new SubscriptionProviderImpl(courseCalendar, res);
SubscriptionContext subContext = subProvider.getSubscriptionContext();
OLATResourceable oresToDelete = OresHelper.createOLATResourceableInstance(subContext.getResName(), subContext.getResId());
nfm.deletePublishersOf(oresToDelete);
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
}
} 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");
// 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();
List<ReferenceImpl> refs = referenceManager.getReferences(sourceCourse);
int count = 0;
for (ReferenceImpl ref: refs) {
referenceManager.addReference(targetCourse, ref.getTarget(), ref.getUserdata());
if(count % 20 == 0) {
DBFactory.getInstance(false).intermediateCommit();
}
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
}
CourseGroupManager sourceCgm = sourceCourse.getCourseEnvironment().getCourseGroupManager();
CourseGroupManager targetCgm = targetCourse.getCourseEnvironment().getCourseGroupManager();
targetCgm.createCourseGroupmanagementAsCopy(sourceCgm, sourceCourse.getCourseTitle());
}
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) {
PersistingCourseImpl sourceCourse = (PersistingCourseImpl) loadCourse(sourceRes);
// add files to ZIP
File fExportDir = new File(System.getProperty("java.io.tmpdir")+File.separator+CodeHelper.getRAMUniqueID());
fExportDir.mkdirs();
synchronized (sourceCourse) { //o_clusterNOK - cannot be solved with doInSync since could take too long (leads to error: "Lock wait timeout exceeded")
sourceCourse.exportToFilesystem(fExportDir);
Codepoint.codepoint(CourseFactory.class, "longExportCourseToZIP");
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);
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) {
// 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) {
Tracing.logError("Error deploying course from ZIP: " + exportedCourseZIPFile.getAbsolutePath(), CourseFactory.class);
return null;
}
File courseExportData = ImportCourseController.getExportDataDir(course);
// get the export data directory
// create the repository entry
RepositoryEntry re = repositoryManager.createRepositoryEntryInstance("administrator");
RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(courseExportData);
String softKey = importExport.getSoftkey();
RepositoryEntry existingEntry = repositoryManager.lookupRepositoryEntryBySoftkey(softKey, false);
if (existingEntry != null) {
Tracing.logInfo("RepositoryEntry with softkey " + softKey + " already exists. Course will not be deployed.", CourseFactory.class);
//seem to be a problem
UserCourseInformationsManager userCourseInformationsManager = CoreSpringFactory.getImpl(UserCourseInformationsManager.class);
userCourseInformationsManager.deleteUserCourseInformations(existingEntry);
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
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);
course = openCourseEditSession(course.getResourceableId());
// create group management
CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager();
cgm.createCourseGroupmanagement(course.getResourceableId().toString());
// import groups
cgm.importCourseLearningGroups(courseExportData);
cgm.importCourseRightGroups(courseExportData);
// 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);
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
// 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);
Tracing.logInfo("Successfully deployed course " + re.getDisplayname() + " from ZIP: " + exportedCourseZIPFile.getAbsolutePath(), CourseFactory.class);
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);
}
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
/**
* Publish the course with some standard options
* @param course
* @param locale
* @param identity
*/
public static void publishCourse(ICourse course, Identity identity, Locale locale) {
CourseEditorTreeModel cetm = course.getEditorTreeModel();
PublishProcess publishProcess = PublishProcess.getInstance(course, cetm, locale);
PublishTreeModel publishTreeModel = publishProcess.getPublishTreeModel();
int newAccess = RepositoryEntry.ACC_USERS;
//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, false);
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()) return;
}
}
course = CourseFactory.openCourseEditSession(course.getResourceableId());
publishProcess.applyPublishSet(identity, locale);
closeCourseEditSession(course.getResourceableId(), true);
}
/**
* 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)) {
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
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, null, false, false);
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) {
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, RepositoryManager.getInstance().lookupRepositoryEntry(res, false));
boolean isOresInstitutionalManager = RepositoryManager.getInstance().isInstitutionalRessourceManagerFor(RepositoryManager.getInstance().lookupRepositoryEntry(res, false), identity);
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());
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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);
}
/**
* 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) {
File exportFolder = new File( // folder where exported user data should be
// put
FolderConfig.getCanonicalRoot() + FolderConfig.getUserHomes() + "/" + identity.getName() + "/private/archive/"
+ 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;
}
/**
* 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(){
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.");