Java tutorial
/** * 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(final Element item) { final ChoiceQuestion instance = new ChoiceQuestion(); try { final 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; } final Element presentationXML = item.element("presentation"); Element material = presentationXML.element("material"); final 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"); } final 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"); } final 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 final Element flow_label = (Element) response_lid.selectSingleNode(".//flow_label"); if (flow_label != null) { instance.setFlowLabelClass(flow_label.attributeValue("class")); } final List response_lables = response_lid.selectNodes(".//response_label"); final List choices = QTIEditHelper.fetchChoices(response_lables); instance.setResponses(choices); final Element resprocessingXML = item.element("resprocessing"); if (resprocessingXML != null) { final List respconditions = resprocessingXML.elements("respcondition"); final HashMap points = QTIEditHelper.fetchPoints(respconditions, instance.getType()); // postprocessing choices for (final Iterator i = choices.iterator(); i.hasNext();) { final ChoiceResponse choice = (ChoiceResponse) i.next(); final 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); final 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); final 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 { final Node score = resprocessingXML .selectSingleNode(".//decvar[@varname='SCORE']/@maxvalue"); if (score != null) { maxValue = Float.parseFloat(score.getText()); } } catch (final NumberFormatException e) { // set maxValue 0 } for (int i = 0; i < choices.size(); i++) { final 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 (final 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(final Element root) { final Element presentationXML = root.addElement("presentation"); presentationXML.addAttribute("label", getLable()); // Question getQuestion().addToElement(presentationXML); final 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"); final 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 (final Iterator i = getResponses().iterator(); i.hasNext();) { final ChoiceResponse tmpChoice = (ChoiceResponse) i.next(); final 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()); final 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); } // ------------------ final Element resprocessingXML = root.addElement("resprocessing"); final Element outcomes = resprocessingXML.addElement("outcomes"); final 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 final Element respcondition_incorrect = resprocessingXML.addElement("respcondition"); respcondition_incorrect.addAttribute("title", "Fail"); respcondition_incorrect.addAttribute("continue", "Yes"); respcondition_incorrect.addElement("conditionvar").addElement("other"); final 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(final Element resprocessingXML) { final Element respcondition_correct = resprocessingXML.addElement("respcondition"); respcondition_correct.addAttribute("title", "Mastery"); respcondition_correct.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_correct.addElement("conditionvar"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { // fetch correct answer (there should be a single instance) final ChoiceResponse tmpChoice = (ChoiceResponse) i.next(); if (!tmpChoice.isCorrect()) { continue; } // found correct answer final 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; } final 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(final Element resprocessingXML) { final Element respcondition_correct = resprocessingXML.addElement("respcondition"); respcondition_correct.addAttribute("title", "Mastery"); respcondition_correct.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_correct.addElement("conditionvar"); final Element and = conditionvar.addElement("and"); final Element not = conditionvar.addElement("not"); final Element or = not.addElement("or"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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 final 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(final Element resprocessingXML) { for (final Iterator i = getResponses().iterator(); i.hasNext();) { final ChoiceResponse tmpChoice = (ChoiceResponse) i.next(); final float points = tmpChoice.getPoints(); if (points == 0) { continue; } final 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"); } final Element varequal = respcondition_correct.addElement("conditionvar").addElement("varequal"); varequal.addAttribute("respident", getIdent()); varequal.addAttribute("case", "Yes"); varequal.addText(tmpChoice.getIdent()); final 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 final Element respcondition_correct = resprocessingXML.addElement("respcondition"); respcondition_correct.addAttribute("title", "Mastery"); respcondition_correct.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_correct.addElement("conditionvar"); final Element and = conditionvar.addElement("and"); final Element not = conditionvar.addElement("not"); final Element or = not.addElement("or"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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(final Element resprocessingXML) { for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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: final Element respcondition_correct = resprocessingXML.addElement("respcondition"); respcondition_correct.addAttribute("title", "Mastery"); respcondition_correct.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_correct.addElement("conditionvar"); final Element and = conditionvar.addElement("and"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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(final Element resprocessingXML) { final Element respcondition_fail = resprocessingXML.addElement("respcondition"); respcondition_fail.addAttribute("title", "Fail"); respcondition_fail.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_fail.addElement("conditionvar"); final Element not = conditionvar.addElement("not"); final Element and = not.addElement("and"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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(final Element resprocessingXML, final String ident, final boolean mastery, final String points) { final 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"); } final Element varequal = condition.addElement("varequal"); varequal.addAttribute("respident", getIdent()); varequal.addAttribute("case", "Yes"); varequal.addText(ident); final 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(final Element resprocessingXML, final boolean isSingleCorrect) { // build final Element respcondition_fail = resprocessingXML.addElement("respcondition"); respcondition_fail.addAttribute("title", "Fail"); respcondition_fail.addAttribute("continue", "Yes"); final Element conditionvar = respcondition_fail.addElement("conditionvar"); final Element or = conditionvar.addElement("or"); for (final Iterator i = getResponses().iterator(); i.hasNext();) { final 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) { final 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(final Element resprocessingXML) { for (final Iterator i = getResponses().iterator(); i.hasNext();) { final Element respcondition = resprocessingXML.addElement("respcondition"); respcondition.addAttribute("title", "_olat_resp_feedback"); respcondition.addAttribute("continue", "Yes"); final Element conditionvar = respcondition.addElement("conditionvar"); final ChoiceResponse tmpChoice = (ChoiceResponse) i.next(); final 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(final 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; } } }