/**
* 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.nodes;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipOutputStream;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.stack.BreadcrumbPanel;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.messages.MessageUIFactory;
import org.olat.core.gui.control.generic.tabbable.TabbableController;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.nodes.GenericNode;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.course.ICourse;
import org.olat.course.condition.Condition;
import org.olat.course.condition.KeyAndNameConverter;
import org.olat.course.condition.additionalconditions.AdditionalCondition;
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.editor.CourseEditorEnv;
import org.olat.course.editor.NodeConfigFormController;
import org.olat.course.editor.PublishEvents;
import org.olat.course.editor.StatusDescription;
import org.olat.course.export.CourseEnvironmentMapper;
import org.olat.course.run.navigation.NodeRunConstructionResult;
import org.olat.course.run.userview.NodeEvaluation;
import org.olat.course.run.userview.TreeEvaluation;
import org.olat.course.run.userview.TreeFilter;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.statistic.StatisticResourceOption;
import org.olat.course.statistic.StatisticResourceResult;
import org.olat.ims.qti.statistics.QTIType;
import org.olat.modules.ModuleConfiguration;
/**
* Description:<br>
* @author Felix Jost
* @author BPS (<a href="http://www.bps-system.de/">BPS Bildungsportal Sachsen GmbH</a>)
*/
public abstract class GenericCourseNode extends GenericNode implements CourseNode {
private static final long serialVersionUID = -1093400247219150363L;
private String type, shortTitle, longTitle, learningObjectives, displayOption;
private ModuleConfiguration moduleConfiguration;
private String noAccessExplanation;
private Condition preConditionVisibility;
private Condition preConditionAccess;
protected transient StatusDescription[] oneClickStatusCache = null;
protected List<AdditionalCondition> additionalConditions = new ArrayList<AdditionalCondition>();
/**
* Generic course node constructor
*
* @param type The course node type
*
* ATTENTION:
* all course nodes must call updateModuleConfigDefaults(true) here
*/
public GenericCourseNode(String type) {
super();
this.type = type;
moduleConfiguration = new ModuleConfiguration();
}
/**
* @see org.olat.course.nodes.CourseNode#createEditController(org.olat.core.gui.UserRequest,
* org.olat.core.gui.control.WindowControl, org.olat.course.ICourse)
*
* ATTENTION:
* all course nodes must call updateModuleConfigDefaults(false) here
*/
@Override
public abstract TabbableController createEditController(UserRequest ureq, WindowControl wControl, BreadcrumbPanel stackPanel, ICourse course,
UserCourseEnvironment euce);
/**
* @see org.olat.course.nodes.CourseNode#createNodeRunConstructionResult(UserRequest,
* WindowControl, UserCourseEnvironment, NodeEvaluation, String)
*
* ATTENTION: all course nodes must call
* updateModuleConfigDefaults(false) here
*/
@Override
public abstract NodeRunConstructionResult createNodeRunConstructionResult(
UserRequest ureq, WindowControl wControl,
UserCourseEnvironment userCourseEnv, NodeEvaluation ne,
String nodecmd);
protected String getDefaultTitleOption() {
return CourseNode.DISPLAY_OPTS_TITLE_DESCRIPTION_CONTENT;
}
/**
* Default implementation of the peekview controller that returns NULL: no
* node specific peekview information should be shown<br>
* Override this method with a specific implementation if you have
* something interesting to show in the peekview
*
* @see org.olat.course.nodes.CourseNode#createPeekViewRunController(UserRequest, WindowControl, UserCourseEnvironment, NodeEvaluation)
*/
@Override
public Controller createPeekViewRunController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv,
NodeEvaluation ne) {
return null;
}
/**
* default implementation of the previewController
*
* @see org.olat.course.nodes.CourseNode#createPreviewController(org.olat.core.gui.UserRequest,
* @see org.olat.core.gui.control.WindowControl,
* @see org.olat.course.run.userview.UserCourseEnvironment,
* @see org.olat.course.run.userview.NodeEvaluation)
*/
//no userCourseEnv or NodeEvaluation needed here
@Override
public Controller createPreviewController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv, NodeEvaluation ne) {
Translator translator = Util.createPackageTranslator(GenericCourseNode.class, ureq.getLocale());
String text = translator.translate("preview.notavailable");
return MessageUIFactory.createInfoMessage(ureq, wControl, null, text);
}
@Override
public StatisticResourceResult createStatisticNodeResult(UserRequest ureq, WindowControl wControl,
UserCourseEnvironment userCourseEnv, StatisticResourceOption options, QTIType... types) {
return null;
}
@Override
public boolean isStatisticNodeResultAvailable(UserCourseEnvironment userCourseEnv, QTIType... types) {
return false;
}
/**
* @return String
*/
@Override
public String getLearningObjectives() {
return learningObjectives;
}
/**
* @return String
*/
@Override
public String getLongTitle() {
return longTitle;
}
/**
* @return String
*/
@Override
public String getShortTitle() {
return shortTitle;
}
/**
* allows to specify if default value should be returned in case where there is no value.
* @param returnDefault if false: null may be returned if no value found!
* @return String
*/
public String getDisplayOption(boolean returnDefault) {
if(!StringHelper.containsNonWhitespace(displayOption) && returnDefault) {
return getDefaultTitleOption();
}
return displayOption;
}
/**
* @return String with the old behavior (default value if none existing)
*/
@Override
public String getDisplayOption() {
return getDisplayOption(true);
}
/**
* @return String
*/
@Override
public String getType() {
return type;
}
/**
* Sets the learningObjectives.
*
* @param learningObjectives The learningObjectives to set
*/
@Override
public void setLearningObjectives(String learningObjectives) {
this.learningObjectives = learningObjectives;
}
/**
* Sets the longTitle.
*
* @param longTitle The longTitle to set
*/
@Override
public void setLongTitle(String longTitle) {
this.longTitle = longTitle;
}
/**
* Sets the shortTitle.
*
* @param shortTitle The shortTitle to set
*/
@Override
public void setShortTitle(String shortTitle) {
this.shortTitle = shortTitle;
}
/**
* Sets the display option
* @param displayOption
*/
@Override
public void setDisplayOption(String displayOption) {
this.displayOption = displayOption;
}
/**
* Sets the type.
*
* @param type The type to set
*/
public void setType(String type) {
this.type = type;
}
/**
* @return ModuleConfiguration
*/
@Override
public ModuleConfiguration getModuleConfiguration() {
return moduleConfiguration;
}
/**
* Sets the moduleConfiguration.
*
* @param moduleConfiguration The moduleConfiguration to set
*/
public void setModuleConfiguration(ModuleConfiguration moduleConfiguration) {
this.moduleConfiguration = moduleConfiguration;
}
@Override
public NodeEvaluation eval(ConditionInterpreter ci, TreeEvaluation treeEval, TreeFilter filter) {
// each CourseNodeImplementation has the full control over all children eval.
// default behaviour is to eval all visible children
NodeEvaluation nodeEval = new NodeEvaluation(this);
calcAccessAndVisibility(ci, nodeEval);
if(filter != null && !filter.isVisible(this)) {
nodeEval.setVisible(false);
}
nodeEval.build();
treeEval.cacheCourseToTreeNode(this, nodeEval.getTreeNode());
// only add children (coursenodes/nodeeval) when I am visible and
// atleastOneAccessible myself
if (nodeEval.isVisible() && nodeEval.isAtLeastOneAccessible()) {
int childcnt = getChildCount();
for (int i = 0; i < childcnt; i++) {
CourseNode cn = (CourseNode)getChildAt(i);
NodeEvaluation chdEval = cn.eval(ci, treeEval, filter);
if (chdEval.isVisible()) { // child is visible
nodeEval.addNodeEvaluationChild(chdEval);
}
}
}
return nodeEval;
}
/**
* @param ci the ConditionInterpreter as the calculating machine
* @param nodeEval the object to write the results into
*/
protected abstract void calcAccessAndVisibility(ConditionInterpreter ci, NodeEvaluation nodeEval);
/**
* @return String
*/
@Override
public String getNoAccessExplanation() {
return noAccessExplanation;
}
/**
* Sets the noAccessExplanation.
*
* @param noAccessExplanation The noAccessExplanation to set
*/
@Override
public void setNoAccessExplanation(String noAccessExplanation) {
this.noAccessExplanation = noAccessExplanation;
}
/**
* @return Condition
*/
@Override
public Condition getPreConditionVisibility() {
if (preConditionVisibility == null) {
preConditionVisibility = new Condition();
}
preConditionVisibility.setConditionId("visibility");
return preConditionVisibility;
}
/**
* Sets the preConditionVisibility.
*
* @param preConditionVisibility The preConditionVisibility to set
*/
@Override
public void setPreConditionVisibility(Condition preConditionVisibility) {
if (preConditionVisibility == null) {
preConditionVisibility = getPreConditionVisibility();
}
this.preConditionVisibility = preConditionVisibility;
this.preConditionVisibility.setConditionId("visibility");
}
/**
* @return Condition
*/
@Override
public Condition getPreConditionAccess() {
if (preConditionAccess == null) {
preConditionAccess = new Condition();
}
preConditionAccess.setConditionId("accessability");
return preConditionAccess;
}
@Override
public void updateOnPublish(Locale locale, ICourse course, Identity publisher, PublishEvents publishEvents) {
//default do nothing
}
/**
* Generic interface implementation. May be overriden by specific node's
* implementation.
*
* @see org.olat.course.nodes.CourseNode#informOnDelete(org.olat.core.gui.UserRequest,
* org.olat.course.ICourse)
*/
@Override
public String informOnDelete(Locale locale, ICourse course) {
return null;
}
/**
* Generic interface implementation. May be overriden by specific node's
* implementation.
*
* @see org.olat.course.nodes.CourseNode#cleanupOnDelete(org.olat.course.ICourse)
*/
@Override
public void cleanupOnDelete(ICourse course) {
// do nothing in default implementation
}
/**
* Generic interface implementation. May be overriden by specific node's
* implementation.
*
* @see org.olat.course.nodes.CourseNode#archiveNodeData(java.util.Locale,
* org.olat.course.ICourse, java.util.zip.ZipOutputStream, String charset)
*/
@Override
public boolean archiveNodeData(Locale locale, ICourse course, ArchiveOptions options, ZipOutputStream exportStream, String charset) {
// nothing to do in default implementation
return true;
}
/**
* @see org.olat.course.nodes.CourseNode#exportNode(java.io.File,
* org.olat.course.ICourse)
*/
@Override
public void exportNode(File exportDirectory, ICourse course) {
// nothing to do in default implementation
}
/**
* Implemented by specialized node
* @see org.olat.course.nodes.CourseNode#importNode(java.io.File,
* org.olat.course.ICourse, org.olat.core.gui.UserRequest,
* org.olat.core.gui.control.WindowControl, boolean)
*/
@Override
public void importNode(File importDirectory, ICourse course, Identity owner, Locale locale, boolean withReferences) {
// nothing to do in default implementation
}
@Override
public void postCopy(CourseEnvironmentMapper envMapper, Processing processType, ICourse course, ICourse sourceCrourse) {
postImportCopyConditions(envMapper);
}
@Override
public void postImport(File importDirectory, ICourse course, CourseEnvironmentMapper envMapper, Processing processType) {
postImportCopyConditions(envMapper);
}
/**
* Post process the conditions
* @param envMapper
*/
protected void postImportCopyConditions(CourseEnvironmentMapper envMapper) {
postImportCondition(preConditionAccess, envMapper);
postImportCondition(preConditionVisibility, envMapper);
}
protected void postImportCondition(Condition condition, CourseEnvironmentMapper envMapper) {
if(condition == null) return;
if(condition.isExpertMode()) {
String expression = condition.getConditionExpression();
if(StringHelper.containsNonWhitespace(expression)) {
String processExpression = KeyAndNameConverter.convertExpressionNameToKey(expression, envMapper);
processExpression = KeyAndNameConverter.convertExpressionKeyToKey(processExpression, envMapper);
if(!expression.equals(processExpression)) {
condition.setConditionExpression(processExpression);
}
}
} else if(StringHelper.containsNonWhitespace(condition.getConditionFromEasyModeConfiguration())) {
List<Long> groupKeys = condition.getEasyModeGroupAccessIdList();
if(groupKeys == null || groupKeys.isEmpty()) {
//this is an old course -> get group keys from original names
groupKeys = envMapper.toGroupKeyFromOriginalNames(condition.getEasyModeGroupAccess());
} else {
//map the original exported group key to the newly created one
groupKeys = envMapper.toGroupKeyFromOriginalKeys(groupKeys);
}
condition.setEasyModeGroupAccessIdList(groupKeys);//update keys
condition.setEasyModeGroupAccess(envMapper.toGroupNames(groupKeys));//update names with the current values
List<Long> areaKeys = condition.getEasyModeGroupAreaAccessIdList();
if(areaKeys == null || areaKeys.isEmpty()) {
areaKeys = envMapper.toAreaKeyFromOriginalNames(condition.getEasyModeGroupAreaAccess());
} else {
areaKeys = envMapper.toAreaKeyFromOriginalKeys(areaKeys);
}
condition.setEasyModeGroupAreaAccessIdList(areaKeys);
condition.setEasyModeGroupAreaAccess(envMapper.toAreaNames(areaKeys));
String condString = condition.getConditionFromEasyModeConfiguration();
condition.setConditionExpression(condString);
}
}
@Override
public void postExport(CourseEnvironmentMapper envMapper, boolean backwardsCompatible) {
postExportCondition(preConditionAccess, envMapper, backwardsCompatible);
postExportCondition(preConditionVisibility, envMapper, backwardsCompatible);
}
protected void postExportCondition(Condition condition, CourseEnvironmentMapper envMapper, boolean backwardsCompatible) {
if(condition == null) return;
boolean easy = StringHelper.containsNonWhitespace(condition.getConditionFromEasyModeConfiguration());
if(easy) {
//already processed?
if(condition.getEasyModeGroupAccessIdList() != null
|| condition.getEasyModeGroupAreaAccessIdList() != null) {
String groupNames = envMapper.toGroupNames(condition.getEasyModeGroupAccessIdList());
condition.setEasyModeGroupAccess(groupNames);
String areaNames = envMapper.toAreaNames(condition.getEasyModeGroupAreaAccessIdList());
condition.setEasyModeGroupAreaAccess(areaNames);
String condString = condition.getConditionFromEasyModeConfiguration();
if(backwardsCompatible) {
condString = KeyAndNameConverter.convertExpressionKeyToName(condString, envMapper);
}
condition.setConditionExpression(condString);
}
} else if(condition.isExpertMode() && backwardsCompatible) {
String expression = condition.getConditionExpression();
if(StringHelper.containsNonWhitespace(expression)) {
String processExpression = KeyAndNameConverter.convertExpressionKeyToName(expression, envMapper);
if(!expression.equals(processExpression)) {
condition.setConditionExpression(processExpression);
}
}
}
if(backwardsCompatible) {
condition.setEasyModeGroupAreaAccessIds(null);
condition.setEasyModeGroupAccessIds(null);
//condition.setConditionUpgraded(null);
}
}
/**
* @see org.olat.core.gui.ShortName#getShortName()
*/
@Override
public String getShortName() {
return getShortTitle();
}
@Override
public CourseNode createInstanceForCopy(boolean isNewTitle, ICourse course, Identity author) {
CourseNode copyInstance = (CourseNode) XStreamHelper.xstreamClone(this);
copyInstance.setIdent(String.valueOf(CodeHelper.getForeverUniqueID()));
copyInstance.setPreConditionVisibility(null);
if (isNewTitle) {
String newTitle = "Copy of " + getShortTitle();
if (newTitle.length() > NodeConfigFormController.SHORT_TITLE_MAX_LENGTH) {
newTitle = newTitle.substring(0, NodeConfigFormController.SHORT_TITLE_MAX_LENGTH - 1);
}
copyInstance.setShortTitle(newTitle);
}
return copyInstance;
}
@Override
public void copyConfigurationTo(CourseNode courseNode, ICourse course) {
if(courseNode instanceof GenericCourseNode) {
GenericCourseNode newNode = (GenericCourseNode)courseNode;
newNode.setDisplayOption(getDisplayOption());
newNode.setLearningObjectives(getLearningObjectives());
newNode.setLongTitle(getLongTitle());
newNode.setNoAccessExplanation(getNoAccessExplanation());
newNode.setShortTitle(getShortTitle());
if(preConditionVisibility != null) {
newNode.setPreConditionVisibility(preConditionVisibility.clone());
}
}
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append("courseNode[id=").append(getIdent()).append(":title=").append(getShortTitle()).append("]");
return sb.toString();
}
/**
* @see org.olat.course.nodes.CourseNode#getConditionExpressions()
*/
public List<ConditionExpression> getConditionExpressions() {
ArrayList<ConditionExpression> retVal = new ArrayList<ConditionExpression>();
String coS = getPreConditionVisibility().getConditionExpression();
if (coS != null && !coS.equals("")) {
// an active condition is defined
ConditionExpression ce = new ConditionExpression(getPreConditionVisibility().getConditionId());
ce.setExpressionString(getPreConditionVisibility().getConditionExpression());
retVal.add(ce);
}
//
return retVal;
}
/**
* must be implemented in the concrete subclasses as a translator is needed
* for the errormessages which comes with evaluating condition expressions
*
* @see org.olat.course.nodes.CourseNode#isConfigValid(org.olat.course.run.userview.UserCourseEnvironment)
*/
public abstract StatusDescription[] isConfigValid(CourseEditorEnv cev);
/**
* @param userCourseEnv
* @param translatorStr
* @return
*/
//for StatusDescription.WARNING
protected List<StatusDescription> isConfigValidWithTranslator(CourseEditorEnv cev, String translatorStr, List<ConditionExpression> condExprs) {
List<StatusDescription> condExprsStatusDescs = new ArrayList<StatusDescription>();
// check valid configuration without course environment
StatusDescription first = isConfigValid();
// check valid configuration within the course environment
if (cev == null) {
// course environment not configured!??
condExprsStatusDescs.add(first);
return condExprsStatusDescs;
}
/*
* there is course editor environment, we can check further. Iterate over
* all conditions of this course node, validate the condition expression and
* transform the condition error message into a status description
*/
for (int i = 0; i < condExprs.size(); i++) {
ConditionExpression ce = condExprs.get(i);
ConditionErrorMessage[] cems = cev.validateConditionExpression(ce);
if (cems != null && cems.length > 0) {
for (int j = 0; j < cems.length; j++) {
StatusDescription sd = new StatusDescription(StatusDescription.WARNING, cems[j].errorKey, cems[j].solutionMsgKey,
cems[j].errorKeyParams, translatorStr);
sd.setDescriptionForUnit(getIdent());
condExprsStatusDescs.add(sd);
}
}
}
condExprsStatusDescs.add(first);
return condExprsStatusDescs;
}
/**
* @see org.olat.course.nodes.CourseNode#explainThisDuringPublish(org.olat.core.gui.control.StatusDescription)
*/
public StatusDescription explainThisDuringPublish(StatusDescription description) {
if (description == null) return null;
StatusDescription retVal = null;
if (description.getShortDescriptionKey().equals("error.notfound.coursenodeid")) {
retVal = description.transformTo("error.notfound.coursenodeid.publish", "error.notfound.coursenodeid.publish", null);
} else if (description.getShortDescriptionKey().equals("error.notfound.name")) {
retVal = description.transformTo("error.notfound.name.publish", "error.notfound.name.publish", null);
} else if (description.getShortDescriptionKey().equals("error.notassessable.coursenodid")) {
retVal = description.transformTo("error.notassessable.coursenodid.publish", "error.notassessable.coursenodid.publish", null);
} else {
// throw new OLATRuntimeException("node does not know how to translate <b
// style='color:red'>" + description.getShortDescriptionKey()
// + "</b> in publish env", new IllegalArgumentException());
return description;
}
return retVal;
}
@Override
public List<StatusDescription> publishUpdatesExplanations(CourseEditorEnv cev) {
return Collections.<StatusDescription>emptyList();
}
/**
* Update the module configuration to have all mandatory configuration flags
* set to usefull default values
*
* @param isNewNode true: an initial configuration is set; false: upgrading
* from previous node configuration version, set default to maintain
* previous behaviour
*
* This is the workflow:
* On every click on a entry of the navigation tree, this method will be called
* to ensure a valid configration of the depending module. This is only done in
* RAM. If the user clicks on that node in course editor and publishes the course
* after that, then the updated config will be persisted to disk. Otherwise
* everything what is done here has to be done once at every course start.
*/
//implemented by specialized node
public void updateModuleConfigDefaults(boolean isNewNode) {
/**
* Do NO updating here, since this method can be overwritten by all classes
* implementing this. This is only implemented here to avoid changing all
* couseNode classes which do not implement this method.
*/
}
}