/**
* 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.editor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org._3pq.jgrapht.DirectedGraph;
import org._3pq.jgrapht.Edge;
import org._3pq.jgrapht.alg.CycleDetector;
import org._3pq.jgrapht.edge.EdgeFactories;
import org._3pq.jgrapht.edge.EdgeFactories.DirectedEdgeFactory;
import org._3pq.jgrapht.graph.DefaultDirectedGraph;
import org.olat.core.util.Util;
import org.olat.core.util.nodes.INode;
import org.olat.core.util.tree.TreeVisitor;
import org.olat.core.util.tree.Visitor;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.assessment.AssessmentHelper;
import org.olat.course.condition.interpreter.ConditionErrorMessage;
import org.olat.course.condition.interpreter.ConditionExpression;
import org.olat.course.condition.interpreter.ConditionInterpreter;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.nodes.BCCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.ENCourseNode;
import org.olat.course.nodes.bc.BCCourseNodeEditController;
import org.olat.course.tree.CourseEditorTreeModel;
import org.olat.course.tree.CourseEditorTreeNode;
import org.olat.group.area.BGArea;
/**
* Description:<br>
* TODO: guido Class Description for CourseEditorEnvImpl
*/
public class CourseEditorEnvImpl implements CourseEditorEnv {
/**
* the course editor tree model used in this editing session, exist only once
* per open course editor
*/
private CourseEditorTreeModel cetm;
String currentCourseNodeId = null;
/**
* the course group manager is used for answering the existXXX questions
* concering, groups and areas
*/
private CourseGroupManager cgm;
/**
* the editor locale, it is used in the condition interpreter to provide
* localized error messages.
*/
private Locale editorLocale;
/**
* book keeping of (coursNodeId,
* {conditionexpression,conditionexpression,...}) TODO: do we really need the
* information splitted up by category and condition expression?
*/
private Map<String,List<ConditionExpression>> softRefs = new HashMap<String,List<ConditionExpression>>();
/**
* book keeping of (courseNodeId, StatusDescription)
*/
private Map<String,List<StatusDescription>> statusDescs = new HashMap<String,List<StatusDescription>>();
/**
* current active condition expression, it is activated by a call to
* <code>validateConditionExpression(..)</code> the condition interpreter is
* then asked for validating the expression. This validation parses the
* expression into the atomic functions etc, which in turn access the
* <code>CourseEditorEnvImpl</code> to <code>pushError()</code> and
* <code>addSoftReference()</code>.
*/
private ConditionExpression currentConditionExpression = null;
/**
* the condition interpreter for evaluating the condtion expressions.
*/
private ConditionInterpreter ci = null;
public CourseEditorEnvImpl(CourseEditorTreeModel cetm, CourseGroupManager cgm, Locale editorLocale) {
this.cetm = cetm;
this.cgm = cgm;
this.editorLocale = editorLocale;
}
/**
* @param ci
*/
public void setConditionInterpreter(ConditionInterpreter ci) {
this.ci = ci;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#isEnrollmentNode(java.lang.String)
*/
public boolean isEnrollmentNode(String nodeId) {
CourseEditorTreeNode cen = cetm.getCourseEditorNodeById(nodeId);
if (cen == null) return false;
if (cen.isDeleted()) return false;
// node exists and is not marked as deleted, check the associated course
// node correct type
return (cen.getCourseNode() instanceof ENCourseNode);
}
/**
* @see org.olat.course.editor.CourseEditorEnv#isAssessable(java.lang.String)
*/
public boolean isAssessable(String nodeId) {
CourseEditorTreeNode cen = cetm.getCourseEditorNodeById(nodeId);
if (cen == null) return false;
if (cen.isDeleted()) return false;
// node exists and is not marked as deleted, check the associated course
// node for assessability.
return AssessmentHelper.checkIfNodeIsAssessable(cen.getCourseNode());
}
/**
* @see org.olat.course.editor.CourseEditorEnv#existsNode(java.lang.String)
*/
public boolean existsNode(String nodeId) {
CourseEditorTreeNode cen = cetm.getCourseEditorNodeById(nodeId);
boolean retVal = cen != null && !cen.isDeleted();
return retVal;
}
// <OLATCE-91>
/**
* @see org.olat.course.editor.CourseEditorEnv#getNode(java.lang.String)
*/
@Override
public CourseNode getNode(String nodeId) {
CourseNode cen = cetm.getCourseNode(nodeId);
return cen;
}
// </OLATCE-91>
/**
* @see org.olat.course.editor.CourseEditorEnv#existsGroup(java.lang.String)
*/
public boolean existsGroup(String groupNameOrKey) {
return cgm.existGroup(groupNameOrKey);
}
/**
* @see org.olat.course.editor.CourseEditorEnv#existsArea(java.lang.String)
*/
public boolean existsArea(String areaNameOrKey) {
return cgm.existArea(areaNameOrKey);
}
@Override
public List<String> validateAreas(List<String> areanames) {
List<BGArea> cnt = cgm.getAllAreas();
List<String> invalidNames = new ArrayList<String>();
a_a:
for(String areaname:areanames) {
for (BGArea element : cnt) {
if (element.getName().equals(areaname)) {
continue a_a;
}
}
invalidNames.add(areaname);
}
return invalidNames;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#getCurrentCourseNodeId()
*/
public String getCurrentCourseNodeId() {
return currentCourseNodeId;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#setCurrentCourseNodeId(java.lang.String)
*/
public void setCurrentCourseNodeId(String courseNodeId) {
this.currentCourseNodeId = courseNodeId;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#getEditorEnvLocale()
*/
public Locale getEditorEnvLocale() {
return editorLocale;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#validateConditionExpression(org.olat.course.condition.interpreter.ConditionExpression)
*/
public ConditionErrorMessage[] validateConditionExpression(ConditionExpression condExpr) {
// first set the active condition expression, which will be accessed from
// the conditions functions inserting soft references
currentConditionExpression = condExpr;
if(condExpr.getExptressionString()==null) {
return null;
}
// evaluate expression
ConditionErrorMessage[] cems = ci.syntaxTestExpression(condExpr);
if (softRefs.containsKey(this.currentCourseNodeId)) {
List<ConditionExpression> condExprs = softRefs.get(this.currentCourseNodeId);
for (Iterator<ConditionExpression> iter = condExprs.iterator(); iter.hasNext();) {
ConditionExpression element = iter.next();
if (element.getId().equals(currentConditionExpression.getId())) {
condExprs.remove(element);
break;
}
}
condExprs.add(currentConditionExpression);
} else {
List<ConditionExpression> condExprs = new ArrayList<ConditionExpression>();
condExprs.add(currentConditionExpression);
softRefs.put(currentCourseNodeId, condExprs);
}
return cems;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#addSoftReference(java.lang.String,
* java.lang.String)
*/
@Override
public void addSoftReference(String category, String softReference, boolean cycleDetector) {
currentConditionExpression.addSoftReference(category, softReference, cycleDetector);
}
/**
* @see org.olat.course.editor.CourseEditorEnv#pushError(java.lang.Exception)
*/
@Override
public void pushError(Exception e) {
currentConditionExpression.pushError(e);
}
/**
* @see org.olat.course.editor.CourseEditorEnv#validateCourse()
*/
@Override
public void validateCourse() {
/*
* collect all condition error messages and soft references collect all
* configuration errors.
*/
String currentNodeWas = currentCourseNodeId;
// reset all
softRefs = new HashMap<String,List<ConditionExpression>>();
Visitor v = new CollectConditionExpressionsVisitor();
(new TreeVisitor(v, cetm.getRootNode(), true)).visitAll();
// refresh,create status descriptions of the course
statusDescs = new HashMap<String,List<StatusDescription>>();
v = new CollectStatusDescriptionVisitor(this);
(new TreeVisitor(v, cetm.getRootNode(), true)).visitAll();
//
currentCourseNodeId = currentNodeWas;
}
/**
* @see org.olat.course.editor.CourseEditorEnv#getCourseStatus()
*/
public StatusDescription[] getCourseStatus() {
String[] a = statusDescs.keySet().toArray(new String[statusDescs.keySet().size()]);
Arrays.sort(a);
List<StatusDescription> all2gether = new ArrayList<StatusDescription>();
for (int i = a.length - 1; i >= 0; i--) {
all2gether.addAll(statusDescs.get(a[i]));
}
ICourse course = CourseFactory.loadCourse(cgm.getCourseEntry());
if(course!= null){
if(course.getCourseConfig().getSharedFolderSoftkey().equals("sf.notconfigured")){
INode rootNode = course.getEditorTreeModel().getRootNode();
List<StatusDescription> descriptions = new ArrayList<StatusDescription>();
descriptions = checkFolderNodes(rootNode, course, descriptions);
if(!descriptions.isEmpty()){
all2gether.addAll(descriptions);
}
}
}
StatusDescription[] retVal = new StatusDescription[all2gether.size()];
retVal = all2gether.toArray(retVal);
return retVal;
}
private List<StatusDescription> checkFolderNodes(INode rootNode, ICourse course, List<StatusDescription> descriptions){
List<StatusDescription> descriptionsI = descriptions;
Visitor visitor = new Visitor() {
@Override
public void visit(INode node) {
CourseEditorTreeNode courseNode = (CourseEditorTreeNode) course.getEditorTreeModel().getNodeById(node.getIdent());
if(!courseNode.isDeleted() && courseNode.getCourseNode() instanceof BCCourseNode){
BCCourseNode bcNode = (BCCourseNode) courseNode.getCourseNode();
if (bcNode.isSharedFolder()) {
String translPackage = Util.getPackageName(BCCourseNodeEditController.class);
StatusDescription status = new StatusDescription(StatusDescription.ERROR, "warning.no.sharedfolder", "warning.no.sharedfolder", null, translPackage);
status.setDescriptionForUnit(bcNode.getIdent());
// set which pane is affected by error
status.setActivateableViewIdentifier(BCCourseNodeEditController.PANE_TAB_FOLDER);
descriptionsI.add(status);
}
}
}
};
TreeVisitor v = new TreeVisitor(visitor, rootNode, false);
v.visitAll();
return descriptionsI;
}
@Override
public String toString() {
String retVal = "";
Set<String> keys = softRefs.keySet();
for (Iterator<String> iter = keys.iterator(); iter.hasNext();) {
String nodId = iter.next();
retVal += "nodeId:" + nodId + "\n";
List<ConditionExpression> conditionExprs = softRefs.get(nodId);
for (Iterator<ConditionExpression> iterator = conditionExprs.iterator(); iterator.hasNext();) {
ConditionExpression ce = iterator.next();
retVal += "\t" + ce.toString() + "\n";
}
retVal += "\n";
}
return retVal;
}
class CollectStatusDescriptionVisitor implements Visitor {
private CourseEditorEnv cev;
public CollectStatusDescriptionVisitor(CourseEditorEnv cev) {
this.cev = cev;
}
/**
* @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
*/
public void visit(INode node) {
/**
* collect only status descriptions of not deleted nodes
*/
CourseEditorTreeNode tmp = (CourseEditorTreeNode) node;
if (!tmp.isDeleted()) {
CourseNode cn = tmp.getCourseNode();
String key = cn.getIdent();
StatusDescription[] allSds = cn.isConfigValid(cev);
if (allSds.length > 0) {
for (int i = 0; i < allSds.length; i++) {
StatusDescription sd = allSds[i];
if (sd != StatusDescription.NOERROR) {
if (!statusDescs.containsKey(key)) {
statusDescs.put(key, new ArrayList<StatusDescription>());
}
List<StatusDescription> sds = statusDescs.get(key);
sds.add(sd);
}
}
}
}
}
}
class CollectConditionExpressionsVisitor implements Visitor {
/**
* @see org.olat.core.util.tree.Visitor#visit(org.olat.core.util.nodes.INode)
*/
public void visit(INode node) {
/**
* collect condition expressions only for not deleted nodes
*/
CourseEditorTreeNode tmp = (CourseEditorTreeNode) node;
CourseNode cn = tmp.getCourseNode();
String key = cn.getIdent();
List<ConditionExpression> condExprs = cn.getConditionExpressions();
if (condExprs.size() > 0 && !tmp.isDeleted()) {
// evaluate each expression
for (Iterator<ConditionExpression> iter = condExprs.iterator(); iter.hasNext();) {
ConditionExpression ce = iter.next();
currentCourseNodeId = key;
currentConditionExpression = ce;
ci.syntaxTestExpression(ce);
}
// add it to the cache.
softRefs.put(key, condExprs);
}
}
}
private static class Convert2DGVisitor implements Visitor{
private DirectedEdgeFactory def;
private DirectedGraph dg;
public Convert2DGVisitor(DirectedGraph dg) {
this.dg = dg;
def = new EdgeFactories.DirectedEdgeFactory();
}
@Override
public void visit(INode node) {
CourseEditorTreeNode tmp = (CourseEditorTreeNode) node;
CourseNode cn = tmp.getCourseNode();
String key = cn.getIdent();
dg.addVertex(key);
/*
* add edge from parent to child. This directed edge represents the visibility accessability inheritance direction.
*/
INode parent = tmp.getParent();
if(parent!=null) {
dg.addVertex(parent.getIdent());
Edge toParent = def.createEdge( parent.getIdent(),key);
dg.addEdge(toParent);
}
}
}
/**
*
* @see org.olat.course.editor.CourseEditorEnv#listCycles()
*/
@Override
public Set<String> listCycles() {
/*
* convert nodeRefs datastructure to a directed graph
*/
DirectedGraph dg = new DefaultDirectedGraph();
DirectedEdgeFactory def = new EdgeFactories.DirectedEdgeFactory();
/*
* add the course structure as directed graph, where
*/
Visitor v = new Convert2DGVisitor(dg);
(new TreeVisitor(v, cetm.getRootNode(), true)).visitAll();
/*
* iterate over nodeRefs, add each not existing node id as vertex, for each
* key - child relation add an edge to the directed graph.
*/
Map<String,Set<String>> nodeSoftRefs = new HashMap<>();
for (Iterator<String> iter = softRefs.keySet().iterator(); iter.hasNext();) {
String nodeId = iter.next();
List<ConditionExpression> conditionExprs = softRefs.get(nodeId);
for (int i = 0; i < conditionExprs.size(); i++) {
ConditionExpression ce = conditionExprs.get(i);
Set<String> refs = ce.getSoftReferencesForCycleDetectorOf("courseNodeId");
if (refs != null && refs.size() > 0) {
if(nodeSoftRefs.containsKey(nodeId)) {
nodeSoftRefs.get(nodeId).addAll(refs);
} else {
nodeSoftRefs.put(nodeId, refs);
}
}
}
}
for(Iterator<String> keys = nodeSoftRefs.keySet().iterator(); keys.hasNext(); ) {
//a node
String key = keys.next();
if(!dg.containsVertex(key)) {
dg.addVertex(key);
}
//and its children
Set<String> children = nodeSoftRefs.get(key);
for(Iterator<String> childrenIt = children.iterator(); childrenIt.hasNext(); ){
String child = childrenIt.next();
if(!dg.containsVertex(child)) {
dg.addVertex(child);
}
//add edge, precondition: vertex key - child are already added to the graph
Edge de = def.createEdge(key, child);
dg.addEdge(de);
}
}
/*
* find the id's participating in the cycle, and return the intersection
* with set of id's which actually produce references.
*/
CycleDetector cd = new CycleDetector(dg);
Set<String> cycleIds = cd.findCycles();
cycleIds.retainAll(nodeSoftRefs.keySet());
return cycleIds;
}
/**
*
* @return CourseGroupManager for this course environment
*/
public CourseGroupManager getCourseGroupManager() {
return cgm;
}
public String getRootNodeId() {
return cetm.getRootNode().getIdent();
}
}