/**
* 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.
* <p>
*/
package org.olat.ims.qti.editor.beecom.objects;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Element;
import org.dom4j.Node;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.ims.qti.editor.QTIEditHelper;
import org.olat.ims.qti.editor.beecom.parser.ItemParser;
import org.olat.ims.qti.editor.beecom.parser.ParserManager;
/**
* @author rkulow Handles both single choice and multiple choice.
* @author sandraroth kprim added
*/
public class ChoiceQuestion extends Question implements QTIObject {
private static final OLog log = Tracing.createLoggerFor(ChoiceQuestion.class);
private static ParserManager parserManager = new ParserManager();
/*
* If singleCorrect=true, all answers marked as correct have to be selected as
* answer by the user in order to get points. Points are given according to
* singleCorrectScore in this case. If singleCorrect=false, any of the answers
* marked as correct give points. Points are given according to
* Choice.getPoints() in this case. singleCorrect can be extracted by looking
* at the way respconditions are built. singleCorrect=true builds a single
* respcondition with <setvar action="Set"> whereas singleCorrect=false builds
* a respcondition for every correct answer with <setvar action="Add">.
*/
public static final String BLOCK = "Block";
public static final String LIST = "List";
private String flowLabelClass = LIST; // default value
public ChoiceQuestion() {
super();
}
/**
* Called by ItemParser to fetch question/answers.
*
* @param item
* @return
*/
public static ChoiceQuestion getInstance(Element item) {
ChoiceQuestion instance = new ChoiceQuestion();
try {
String item_ident = item.attributeValue("ident");
if (item_ident.startsWith(ItemParser.ITEM_PREFIX_SCQ)) instance.setType(TYPE_SC);
else if (item_ident.startsWith(ItemParser.ITEM_PREFIX_MCQ)) instance.setType(TYPE_MC);
else if (item_ident.startsWith(ItemParser.ITEM_PREFIX_KPRIM)) instance.setType(TYPE_KPRIM);
else return null;
Element presentationXML = item.element("presentation");
Element material = presentationXML.element("material");
Element flow = presentationXML.element("flow");
if (material == null && flow != null) {
/*
* This is a bugfix (see OLAT-4194). According to the qti specification,
* the presentation element can either have the elements material and
* response_lid as children or they may be children of the flow element
* which itself is a child of presentation.
*/
material = flow.element("material");
}
Material matQuestion = (Material) parserManager.parse(material);
if (matQuestion != null) instance.setQuestion(matQuestion);
Element response_lid = presentationXML.element("response_lid");
if (response_lid == null && flow != null) {
response_lid = flow.element("response_lid");
}
String identQuestion = response_lid.attribute("ident").getText();
instance.setIdent(identQuestion);
String shuffle = response_lid.element("render_choice").attributeValue("shuffle");
if (shuffle == null) shuffle = "Yes";
instance.setShuffle(shuffle.equals("Yes"));
// Set first flow_label class that is found for entire question. This
// editor uses the same flow_label on every response
Element flow_label = (Element) response_lid.selectSingleNode(".//flow_label");
if (flow_label != null) instance.setFlowLabelClass(flow_label.attributeValue("class"));
List response_lables = response_lid.selectNodes(".//response_label");
List choices = QTIEditHelper.fetchChoices(response_lables);
instance.setResponses(choices);
Element resprocessingXML = item.element("resprocessing");
if (resprocessingXML != null) {
List respconditions = resprocessingXML.elements("respcondition");
HashMap points = QTIEditHelper.fetchPoints(respconditions, instance.getType());
// postprocessing choices
for (Iterator i = choices.iterator(); i.hasNext();) {
ChoiceResponse choice = (ChoiceResponse) i.next();
Float fPoints = (Float) points.get(choice.getIdent());
if (fPoints != null) {
choice.setPoints(fPoints.floatValue());
choice.setCorrect(true);
}
}
// get type of multiple choice
if (instance.getType() == TYPE_MC) {
// if does not contain any ANDs, assume only one combination
// of answers is possible (which sets points by a setvar action="Set")
if (resprocessingXML.selectNodes(".//setvar[@action='Add']").size() == 0) {
instance.setSingleCorrect(true);
Collection values = points.values();
if (values.size() > 0) instance.setSingleCorrectScore(((Float) (values.iterator().next())).floatValue());
} else {
instance.setSingleCorrect(false);
}
} else if (instance.getType() == TYPE_SC) {
instance.setSingleCorrect(true);
Collection values = points.values();
if (values.size() > 0) {
instance.setSingleCorrectScore(((Float) (values.iterator().next())).floatValue());
} else {
instance.setSingleCorrect(false);
}
} else if (instance.getType() == TYPE_KPRIM) {
instance.setSingleCorrect(false);
float maxValue = 0;
try {
Node score = resprocessingXML.selectSingleNode(".//decvar[@varname='SCORE']/@maxvalue");
if (score != null) {
maxValue = Float.parseFloat(score.getText());
}
} catch (NumberFormatException e) {
// set maxValue 0
}
for (int i = 0; i < choices.size(); i++) {
ChoiceResponse choice = (ChoiceResponse) choices.get(i);
if (resprocessingXML.selectNodes(
"./respcondition[@title='Mastery']/conditionvar/varequal[text()='" + choice.getIdent() + ":correct']").size() > 0) {
choice.setCorrect(true);
choice.setPoints(maxValue / 4);
} else {
choice.setCorrect(false);
choice.setPoints(maxValue / 4);
}
}
}
// set min/max score
QTIEditHelper.configureMinMaxScore(instance, (Element) resprocessingXML.selectSingleNode(".//decvar"));
}
} catch (NullPointerException e) {
/*
* A null pointer exeption may occur (and has occured) due to incomplete
* implementation of the qti specification within OLAT. Since the QTI xml
* validation at this point already passed, it's hard to still output user
* information. At this point, definitely log error.
*/
log.error("Reading choice question failed. Might be due to incomplete qti implementation.", e);
}
return instance;
}
/**
* Build XML tree (presentation & resprocessing)
*/
public void addToElement(Element root) {
Element presentationXML = root.addElement("presentation");
presentationXML.addAttribute("label", getLable());
// Question
getQuestion().addToElement(presentationXML);
Element response_lid = presentationXML.addElement("response_lid");
response_lid.addAttribute("ident", getIdent());
response_lid.addAttribute("rcardinality", (getType() == TYPE_SC) ? "Single" : "Multiple");
response_lid.addAttribute("rtiming", "No");
Element render_choice = response_lid.addElement("render_choice");
render_choice.addAttribute("shuffle", isShuffle() ? "Yes" : "No");
render_choice.addAttribute("minnumber", getType() == TYPE_SC ? "1" : "0");
render_choice.addAttribute("maxnumber", getType() == TYPE_SC ? "1" : String.valueOf(getResponses().size()));
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
Element flow_label = render_choice.addElement("flow_label");
// Add horizontal or vertical alignment. All flow_labels get the
// same value
flow_label.addAttribute("class", getFlowLabelClass());
Element response_lable = flow_label.addElement("response_label");
response_lable.addAttribute("ident", tmpChoice.getIdent());
response_lable.addAttribute("rshuffle", "Yes"); // QTI default value
tmpChoice.getContent().addToElement(response_lable);
}
// ------------------
Element resprocessingXML = root.addElement("resprocessing");
Element outcomes = resprocessingXML.addElement("outcomes");
Element decvar = outcomes.addElement("decvar");
decvar.addAttribute("varname", "SCORE");
decvar.addAttribute("vartype", "Decimal");
decvar.addAttribute("defaultval", "0");
decvar.addAttribute("minvalue", "" + getMinValue());
float maxScore = QTIEditHelper.calculateMaxScore(this);
maxScore = maxScore > getMaxValue() ? getMaxValue() : maxScore;
decvar.addAttribute("maxvalue", "" + maxScore);
decvar.addAttribute("cutvalue", "" + maxScore);
// process respcondition_correct and fail
if (getType() == TYPE_SC) {
buildRespconditionSC_mastery(resprocessingXML);
buildRespcondition_fail(resprocessingXML, true);
} else if (getType() == TYPE_MC) {
if (isSingleCorrect()) {
buildRespconditionMCSingle_mastery(resprocessingXML);
buildRespcondition_fail(resprocessingXML, true);
} else {
buildRespconditionMCMulti_mastery(resprocessingXML);
buildRespcondition_fail(resprocessingXML, false);
}
} else if (getType() == TYPE_KPRIM) {
buildRespconditionKprim(resprocessingXML);
buildRespconditionKprim_fail(resprocessingXML);
}
// hint
if (getHintText() != null) QTIEditHelper.addHintElement(root, getHintText());
// solution
if (getSolutionText() != null) QTIEditHelper.addSolutionElement(root, getSolutionText());
// Response feedback
if (getType() != TYPE_KPRIM) {
buildRespconditionOlatFeedback(resprocessingXML);
// Feedback for all other cases eg. none has been selected
Element respcondition_incorrect = resprocessingXML.addElement("respcondition");
respcondition_incorrect.addAttribute("title", "Fail");
respcondition_incorrect.addAttribute("continue", "Yes");
respcondition_incorrect.addElement("conditionvar").addElement("other");
Element setvar = respcondition_incorrect.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Set");
setvar.addText("0");
QTIEditHelper.addFeedbackFail(respcondition_incorrect);
QTIEditHelper.addFeedbackHint(respcondition_incorrect);
QTIEditHelper.addFeedbackSolution(respcondition_incorrect);
}
} // addToElement
/**
* Build resprocessing for single choice item. Set score to correct value and
* use mastery feedback
*
* @param resprocessingXML
*/
private void buildRespconditionSC_mastery(Element resprocessingXML) {
Element respcondition_correct = resprocessingXML.addElement("respcondition");
respcondition_correct.addAttribute("title", "Mastery");
respcondition_correct.addAttribute("continue", "Yes");
Element conditionvar = respcondition_correct.addElement("conditionvar");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
// fetch correct answer (there should be a single instance)
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
if (!tmpChoice.isCorrect()) continue;
// found correct answer
Element varequal = conditionvar.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
break;
} // for loop
// check if conditionvar has correct value
if (conditionvar.elements().size() == 0) {
resprocessingXML.remove(respcondition_correct);
return;
}
Element setvar = respcondition_correct.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Set");
setvar.addText("" + getSingleCorrectScore());
// Use mastery feedback
QTIEditHelper.addFeedbackMastery(respcondition_correct);
}
/**
* Build resprocessing for multiple choice item with a single correct answer.
* Set score to correct value and use mastery feedback.
*
* @param resprocessingXML
*/
private void buildRespconditionMCSingle_mastery(Element resprocessingXML) {
Element respcondition_correct = resprocessingXML.addElement("respcondition");
respcondition_correct.addAttribute("title", "Mastery");
respcondition_correct.addAttribute("continue", "Yes");
Element conditionvar = respcondition_correct.addElement("conditionvar");
Element and = conditionvar.addElement("and");
Element not = conditionvar.addElement("not");
Element or = not.addElement("or");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
Element varequal;
if (tmpChoice.isCorrect()) { // correct answers
varequal = and.addElement("varequal");
} else { // incorrect answers
varequal = or.addElement("varequal");
}
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
} // for loop
Element setvar = respcondition_correct.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Set");
setvar.addText("" + getSingleCorrectScore());
// Use mastery feedback
QTIEditHelper.addFeedbackMastery(respcondition_correct);
// remove whole respcondition if empty
if (or.element("varequal") == null && and.element("varequal") == null) {
resprocessingXML.remove(respcondition_correct);
} else {
// remove any unset <and> and <not> nodes
if (and.element("varequal") == null) conditionvar.remove(and);
if (or.element("varequal") == null) conditionvar.remove(not);
}
}
/**
* Build resprocessing for multiple choice question with multiple correct
* answers. Sets correct score for positive (mastery) and negative (fail)
* conditions and use mastery feedback when all mastery responses have been
* selected.
*
* @param resprocessingXML
*/
private void buildRespconditionMCMulti_mastery(Element resprocessingXML) {
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
float points = tmpChoice.getPoints();
if (points == 0) continue;
Element respcondition_correct = resprocessingXML.addElement("respcondition");
respcondition_correct.addAttribute("continue", "Yes");
if (points > 0) respcondition_correct.addAttribute("title", "Mastery");
else respcondition_correct.addAttribute("title", "Fail");
Element varequal = respcondition_correct.addElement("conditionvar").addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
Element setvar = respcondition_correct.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Add");
setvar.addText("" + points);
} // for loop
// Resp condition for feedback mastery:
// all response with points>0 must be selected
Element respcondition_correct = resprocessingXML.addElement("respcondition");
respcondition_correct.addAttribute("title", "Mastery");
respcondition_correct.addAttribute("continue", "Yes");
Element conditionvar = respcondition_correct.addElement("conditionvar");
Element and = conditionvar.addElement("and");
Element not = conditionvar.addElement("not");
Element or = not.addElement("or");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
Element varequal;
if (tmpChoice.getPoints() > 0) {
varequal = and.addElement("varequal");
} else { // incorrect answers
varequal = or.addElement("varequal");
}
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
} // for loop
// Use mastery feedback
QTIEditHelper.addFeedbackMastery(respcondition_correct);
// remove whole respcondition if empty
if (or.element("varequal") == null && and.element("varequal") == null) {
resprocessingXML.remove(respcondition_correct);
} else {
// remove any unset <and> and <not> nodes
if (and.element("varequal") == null) conditionvar.remove(and);
if (or.element("varequal") == null) conditionvar.remove(not);
}
}
/**
* Build resprocessing for Kprim question.
* @param resprocessingXML
*/
private void buildRespconditionKprim(Element resprocessingXML) {
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse choice = (ChoiceResponse) i.next();
if (choice.isCorrect()) {
addRespcondition(resprocessingXML, choice.getIdent() + ":correct", true, String.valueOf(choice.getPoints()));
addRespcondition(resprocessingXML, choice.getIdent() + ":correct", false, String.valueOf(-choice.getPoints()));
} else {
addRespcondition(resprocessingXML, choice.getIdent() + ":wrong", true, String.valueOf(choice.getPoints()));
addRespcondition(resprocessingXML, choice.getIdent() + ":wrong", false, String.valueOf(-choice.getPoints()));
}
}
// Resp condition for feedback mastery kprim:
Element respcondition_correct = resprocessingXML.addElement("respcondition");
respcondition_correct.addAttribute("title", "Mastery");
respcondition_correct.addAttribute("continue", "Yes");
Element conditionvar = respcondition_correct.addElement("conditionvar");
Element and = conditionvar.addElement("and");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse choice = (ChoiceResponse) i.next();
Element varequal;
varequal = and.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
if (choice.isCorrect()) {
varequal.addText(choice.getIdent() + ":correct");
} else {
varequal.addText(choice.getIdent() + ":wrong");
}
}
// Use mastery feedback
QTIEditHelper.addFeedbackMastery(respcondition_correct);
}
/**
* Feedback, solution and hints in case of failure
* @param resprocessingXML
*/
private void buildRespconditionKprim_fail(Element resprocessingXML) {
Element respcondition_fail = resprocessingXML.addElement("respcondition");
respcondition_fail.addAttribute("title", "Fail");
respcondition_fail.addAttribute("continue", "Yes");
Element conditionvar = respcondition_fail.addElement("conditionvar");
Element not = conditionvar.addElement("not");
Element and = not.addElement("and");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse choice = (ChoiceResponse) i.next();
Element varequal;
varequal = and.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
if (choice.isCorrect()) {
varequal.addText(choice.getIdent() + ":correct");
} else { // incorrect answers
varequal.addText(choice.getIdent() + ":wrong");
}
}
QTIEditHelper.addFeedbackFail(respcondition_fail);
QTIEditHelper.addFeedbackHint(respcondition_fail);
QTIEditHelper.addFeedbackSolution(respcondition_fail);
}
/**
* Adds condition to resprocessing with ident
*
* @param resprocessingXML
* @param ident
* @param mastery
* @param points
*/
private void addRespcondition(Element resprocessingXML, String ident, boolean mastery, String points) {
Element respcondition = resprocessingXML.addElement("respcondition");
respcondition.addAttribute("continue", "Yes");
if (mastery) respcondition.addAttribute("title", "Mastery");
else respcondition.addAttribute("title", "Fail");
Element condition = respcondition.addElement("conditionvar");
if (!mastery) condition = condition.addElement("not");
Element varequal = condition.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(ident);
Element setvar = respcondition.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Add");
setvar.addText(points);
}
/**
* Build fail resprocessing: Adjust score to 0 (if multiple correct mode) and
* set hints, solutions and fail feedback
*
* @param resprocessingXML
* @param isSingleCorrect
*/
private void buildRespcondition_fail(Element resprocessingXML, boolean isSingleCorrect) {
// build
Element respcondition_fail = resprocessingXML.addElement("respcondition");
respcondition_fail.addAttribute("title", "Fail");
respcondition_fail.addAttribute("continue", "Yes");
Element conditionvar = respcondition_fail.addElement("conditionvar");
Element or = conditionvar.addElement("or");
for (Iterator i = getResponses().iterator(); i.hasNext();) {
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
Element varequal;
// Add this response to the fail case
// if single correct type and not correct
// or multi correct and points negative or 0
if ((isSingleCorrect && !tmpChoice.isCorrect()) || (!isSingleCorrect && tmpChoice.getPoints() <= 0)) {
varequal = or.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
}
} // for loop
if (isSingleCorrect) {
Element setvar = respcondition_fail.addElement("setvar");
setvar.addAttribute("varname", "SCORE");
setvar.addAttribute("action", "Set");
setvar.addText("0");
}
// Use fail feedback, hints and solutions
QTIEditHelper.addFeedbackFail(respcondition_fail);
QTIEditHelper.addFeedbackHint(respcondition_fail);
QTIEditHelper.addFeedbackSolution(respcondition_fail);
// remove whole respcondition if empty
if (or.element("varequal") == null) resprocessingXML.remove(respcondition_fail);
}
/**
* Build resprocessing for olat response feedbacks (uses naming conventions:
* respcondition:title is set to _olat_resp_feedback to signal a feedback that
* it belongs directly to the response with the same response ident as the
* current feedback)
*
* @param resprocessingXML
*/
private void buildRespconditionOlatFeedback(Element resprocessingXML) {
for (Iterator i = getResponses().iterator(); i.hasNext();) {
Element respcondition = resprocessingXML.addElement("respcondition");
respcondition.addAttribute("title", "_olat_resp_feedback");
respcondition.addAttribute("continue", "Yes");
Element conditionvar = respcondition.addElement("conditionvar");
ChoiceResponse tmpChoice = (ChoiceResponse) i.next();
Element varequal = conditionvar.addElement("varequal");
varequal.addAttribute("respident", getIdent());
varequal.addAttribute("case", "Yes");
varequal.addText(tmpChoice.getIdent());
QTIEditHelper.addFeedbackOlatResp(respcondition, tmpChoice.getIdent());
}
}
/**
* ************************ GETTERS and SETTERS
* ********************************
*/
/**
* @return
*/
public String getFlowLabelClass() {
return flowLabelClass;
}
/**
* Set alignment of response items:<br>
* For horizontal alignment: Block <br>
* For vertical alignment: List
*
* @param string
*/
public void setFlowLabelClass(String string) {
// only allow Block or List as value, default is set to List
if (string != null && string.equals(BLOCK)) flowLabelClass = BLOCK;
else flowLabelClass = LIST;
}
}