org.olat.ims.qti.container.SectionContext.java Source code

Java tutorial

Introduction

Here is the source code for org.olat.ims.qti.container.SectionContext.java

Source

/**
 * 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.container;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import org.dom4j.Element;
import org.dom4j.Node;
import org.olat.ims.qti.container.qtielements.Objectives;
import org.olat.ims.qti.container.qtielements.SectionFeedback;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.QTIHelper;
import org.olat.ims.qti.process.elements.ExpressionBuilder;
import org.olat.ims.qti.process.elements.ScoreBooleanEvaluable;

/**
 * @author Potable Shop
 */
public class SectionContext implements Serializable {
    private String ident;
    // private String title;
    private AssessmentInstance assessInstance;

    // readonly ref!: the ref to the el_section; transient since it we don't want
    // to serialize it (too long) and can reattach it later
    private Element el_section;
    private Objectives objectives;
    private List itemContexts;

    private float totalScore; // only floats and integers supported at the moment
    private int cutvalue;
    private int currentItemContextPos;
    private long timeOfStart;
    // -1 = not started yet; server time at the time of the start of the section
    private long durationLimit; //
    private long timesAnswered;
    private long latestAnswerTime;
    private Output output;

    // private boolean outcomesProcessing;
    private boolean feedbacktesting; // has the section feedback calculation
    private boolean feedbackswitchedon; // is feedback allowed
    private boolean feedbackavailable; // is feedback currently available?
    private String scoremodel;

    /**
     * default constructor needed for persistence
     */
    public SectionContext() {
        //
    }

    /**
     * 
     */
    public void init() {
        totalScore = 0.0f;
        currentItemContextPos = -1;
        timeOfStart = -1; // not started yet
        timesAnswered = 0; // not answered yet (this flag has no direct meaning in
        // qti)
        latestAnswerTime = -1; // outcomesProcessing = false;
        feedbacktesting = false;
        cutvalue = -1;
        feedbackavailable = false;
    }

    /**
     * Start duration counters.
     */
    public void start() {
        if (timeOfStart == -1) { // if not started already
            timeOfStart = System.currentTimeMillis();
        }
    }

    /**
     *
     */
    public void sectionWasSubmitted() {
        timesAnswered++;
        latestAnswerTime = System.currentTimeMillis();
    }

    /**
     * Method eval.
     */
    public void eval() {
        if (assessInstance.isSurvey()) {
            return;
        }
        final int itccnt = getItemContextCount();
        for (int i = 0; i < itccnt; i++) {
            final ItemContext ict = getItemContext(i);
            ict.eval();
        }
        calcScore(); // calc feedback
        if (feedbacktesting) {
            calcFeedBack();
        }
    }

    /**
     * @param assessInstance
     * @param el_section
     * @param sw
     */
    public void setUp(final AssessmentInstance assessInstance, final Element el_section, Switches sw) {
        this.assessInstance = assessInstance;
        this.el_section = el_section;
        this.ident = el_section.attributeValue("ident");
        init();

        final Element dur = (Element) el_section.selectSingleNode("duration");
        if (dur == null) {
            durationLimit = -1; // no limit
        } else {
            final String sdur = dur.getText();
            durationLimit = QTIHelper.parseISODuration(sdur);
            if (durationLimit == 0) {
                durationLimit = -1; // Assesst Designer fix
            }
        }

        // get objectives
        final Element el_objectives = (Element) el_section.selectSingleNode("objectives");
        if (el_objectives != null) {
            objectives = new Objectives(el_objectives);
        }

        // feedback switches
        // ---------------------------------------------------------
        if (sw == null) { // no switches from the assessment context dominate
            // retrieve section switches
            final Element el_control = (Element) el_section.selectSingleNode("sectioncontrol");
            if (el_control != null) {
                final String feedbackswitch = el_control.attributeValue("feedbackswitch");
                final String hintswitch = el_control.attributeValue("hintswitch");
                final String solutionswitch = el_control.attributeValue("solutionswitch");
                feedbackswitchedon = (feedbackswitch == null) ? true : feedbackswitch.equals("Yes");
                final boolean hints = (hintswitch == null) ? true : hintswitch.equals("Yes");
                final boolean solutions = (solutionswitch == null) ? true : solutionswitch.equals("Yes");
                sw = new Switches(feedbackswitchedon, hints, solutions);
            }
        }

        // ----------------------- selection
        List el_items = new ArrayList();
        // determine which items (sections not implemented) will be chosen/selected
        // for this section
        // --- 1. take all items and resolved itemrefs which are in the section
        final List items = el_section.selectNodes("item|itemref");
        for (final Iterator iter = items.iterator(); iter.hasNext();) {
            Element el_item = (Element) iter.next();
            // <!ELEMENT itemref (#PCDATA)> <!ATTLIST itemref %I_LinkRefId; > <!ENTITY
            // % I_LinkRefId " linkrefid CDATA #REQUIRED">
            if (el_item.getName().equals("itemref")) {
                // resolve the entity first
                final String linkRefId = el_item.attributeValue("linkrefid");
                el_item = (Element) el_section.selectSingleNode("//item[@ident='" + linkRefId + "']");
                // if item == null -> TODO Error
            }
            el_items.add(el_item);
        }
        // --- 2. select all items from the objectbank which fulfill the selection
        // criteria

        final Element el_selordering = (Element) el_section.selectSingleNode("selection_ordering");
        if (el_selordering != null) {
            // do some selection and ordering
            // here comes the selection....
            // xpath =
            // "//item[itemmetadata/qtimetadata/qtimetadatafield[fieldlabel[text()='qmd_dificulty']
            // and fieldentry[text()='4']] or
            // itemmetadata/qtimetadata/qtimetadatafield[fieldlabel[text()='qmd_author']
            // and fieldentry[text()='felix']]]"
            // <!ELEMENT selection_ordering (qticomment? , sequence_parameter* ,
            // selection* , order?)>
            // <!ATTLIST selection_ordering sequence_type CDATA #IMPLIED >
            // <!ELEMENT selection (sourcebank_ref? , selection_number? ,
            // selection_metadata? ,
            // (and_selection | or_selection | not_selection | selection_extension)?)>
            // <!ELEMENT sourcebank_ref (#PCDATA)>
            // not <!ELEMENT order (order_extension?)>
            // <!ATTLIST order order_type CDATA #REQUIRED >
            // <!ELEMENT selection_number (#PCDATA)>
            // not <!ELEMENT sequence_parameter (#PCDATA)>
            // not <!ATTLIST sequence_parameter %I_Pname; >
            final List el_selections = el_selordering.selectNodes("selection");

            // iterate over all selection elements : after each we have some items to
            // add to the run-time-section
            for (final Iterator it_selection = el_selections.iterator(); it_selection.hasNext();) {
                List selectedItems;
                final Element el_selection = (Element) it_selection.next();
                final Element el_sourcebankref = (Element) el_selection.selectSingleNode("sourcebank_ref");
                if (el_sourcebankref == null) {
                    // no reference to sourcebank, -> take internal one, but dtd disallows
                    // it!?? TODO
                    /*
                     * 2:27 PM] <felix.jost> aus ims qti sao: [2:27 PM] <felix.jost> 3.2.1 <sourcebank_ref> Description: Identifies the objectbank to which the selection
                     * and ordering rules are to be applied. This objectbank may or may not be contained in the same <questestinterop> package. [2:27 PM] <felix.jost>
                     * aber dtd: [2:28 PM] <felix.jost> <!ELEMENT questestinterop (qticomment? , (objectbank | assessment | (section | item)+))>
                     */
                    selectedItems = new ArrayList();
                } else {
                    final String sourceBankRef = el_sourcebankref.getText();
                    final Element objectBank = assessInstance.getResolver().getObjectBank(sourceBankRef);

                    // traverse 1.: process "and" or "or" or "not" selection to get the
                    // items, if existing, otherwise take all items
                    // 2.: do the selection_number
                    final Element andornot_selection = (Element) el_selection
                            .selectSingleNode("and_selection|or_selection|not_selection|selection_metadata");
                    final StringBuilder select_expr = new StringBuilder("//item");
                    if (andornot_selection != null) {
                        // some criteria, extend above xpath to select only the appropriate
                        // elements
                        select_expr.append("[");
                        final String elName = andornot_selection.getName();
                        final ExpressionBuilder eb = QTIHelper.getExpressionBuilder(elName);
                        eb.buildXPathExpression(andornot_selection, select_expr, false, true);
                        select_expr.append("]");
                    }
                    selectedItems = objectBank.selectNodes(select_expr.toString());
                    el_items.addAll(selectedItems);
                }
                final Element el_selection_number = (Element) el_selection.selectSingleNode("selection_number");
                // --- 3. if selection_number exists, pick out some items
                if (el_selection_number != null) {
                    final String sNum = el_selection_number.getText();
                    int num = new Integer(sNum).intValue();
                    // now choose some x out of the items if selection_number exists
                    final List newList = new ArrayList();
                    final Random r = new Random();
                    int size = el_items.size();
                    // if num > size ??e.g. 5 elements should be picked, but there are
                    // only four
                    if (num > size) {
                        num = size;
                    }
                    for (int i = 0; i < num; i++) {
                        final int n = r.nextInt(size--);
                        final Object o = el_items.remove(n);
                        newList.add(o);
                    }
                    el_items = newList;
                    /*
                     * pick out items -> remove unused items from section
                     */
                    items.removeAll(el_items);
                    for (final Iterator iter = items.iterator(); iter.hasNext();) {
                        el_section.remove((Node) iter.next());
                    }

                }
                // append found items to existing ones
            }
        } // end of el_ordering != null

        // if there is order = random -> shuffle
        // <order order_type="Random"/>
        if (el_selordering != null) {
            final Element el_order = (Element) el_selordering.selectSingleNode("order");
            if (el_order != null) {
                final String order_type = el_order.attributeValue("order_type");
                if (order_type.equals("Random")) {
                    Collections.shuffle(el_items);
                }
            }
        }

        // now wrap all item contexts
        itemContexts = new ArrayList(10);
        for (final Iterator iter = el_items.iterator(); iter.hasNext();) {
            final Element item = (Element) iter.next();
            item.detach();
            final ItemContext itc = new ItemContext();
            itc.setUp(assessInstance, item, sw);
            if (durationLimit != -1 && assessInstance.isSectionPage()) {
                itc.clearDurationLimit();
            }
            itemContexts.add(itc);
        }

        // outcomesProcessing
        // <!ELEMENT section (qticomment? , duration? , qtimetadata* ,
        // objectives* , sectioncontrol* , sectionprecondition* ,
        // sectionpostcondition* ,
        // rubric* , presentation_material? ,
        // outcomes_processing* , sectionproc_extension? ,
        // sectionfeedback* , selection_ordering? ,
        // reference? , (itemref | item | sectionref | section)*)>

        // <!ELEMENT outcomes_processing (qticomment? , outcomes ,
        // objects_condition* , processing_parameter* , map_output* ,
        // outcomes_feedback_test*)>
        // <!ELEMENT outcomes (qticomment? , (decvar , interpretvar*)+)>

        // <!ELEMENT decvar (#PCDATA)>
        // <!ATTLIST decvar %I_VarName; .......cutvalue CDATA #IMPLIED >
        final Element el_outpro = (Element) el_section.selectSingleNode("outcomes_processing");
        if (el_outpro != null) {
            // get the scoring model: we need it later for calculating the score
            // <!ENTITY % I_ScoreModel " scoremodel CDATA #IMPLIED">
            scoremodel = el_outpro.attributeValue("scoremodel");
            // may be null -> then assume SumOfScores

            // set the cutvalue if given (only variable score)
            cutvalue = QTIHelper.getIntAttribute(el_outpro, "outcomes/decvar[@varname='SCORE']", "cutvalue");
            final List el_oft = el_outpro.selectNodes("outcomes_feedback_test");
            if (el_oft.size() != 0) {
                feedbacktesting = true;
            }
        }
    }

    /**
     * Method calcFeedBack.
     */
    private void calcFeedBack() {
        final List el_ofts = el_section.selectNodes("outcomes_processing/outcomes_feedback_test");
        feedbackavailable = false;
        for (final Iterator it_oft = el_ofts.iterator(); it_oft.hasNext();) {
            final Element el_oft = (Element) it_oft.next();
            // <!ELEMENT outcomes_feedback_test (test_variable , displayfeedback+)>
            final Element el_testvar = (Element) el_oft.selectSingleNode("test_variable");
            // must exist: dtd
            // <!ELEMENT test_variable (variable_test | and_test | or_test |
            // not_test)>
            final Element el_varandornot = (Element) el_testvar
                    .selectSingleNode("variable_test|and_test|or_test|not_test");
            final String elname = el_varandornot.getName();
            final ScoreBooleanEvaluable sbe = QTIHelper.getSectionBooleanEvaluableInstance(elname);
            final float totalscore = getScore();
            final boolean fulfilled = sbe.eval(el_varandornot, totalscore);
            if (fulfilled) {
                // get feedback
                final Element el_displayfeedback = (Element) el_oft.selectSingleNode("displayfeedback");
                final String linkRefId = el_displayfeedback.attributeValue("linkrefid");
                // must exist (dtd)
                // ignore feedbacktype, since we section or assess feedback only accepts
                // material, no hints or solutions
                final Element el_resolved = (Element) el_section
                        .selectSingleNode(".//sectionfeedback[@ident='" + linkRefId + "']");
                getOutput().setEl_response(new SectionFeedback(el_resolved));
                // give the whole sectionfeedback to render
                feedbackavailable = true;
            }
        }
    }

    /**
     * @return List of ItemContext instances
     */
    public List getItemContextsToRender() {
        return itemContexts;
    }

    /**
     * Method calcScore.
     */
    private void calcScore() {
        totalScore = 0;
        if (scoremodel == null || scoremodel.equalsIgnoreCase("SumOfScores")) { // sumofScores
            for (final Iterator iter = itemContexts.iterator(); iter.hasNext();) {
                final ItemContext ict = (ItemContext) iter.next();
                totalScore += ict.getScore();
            }
        } else if (scoremodel.equalsIgnoreCase("NumberCorrect")) {
            totalScore = 0;
            int tmpscore = 0;
            // correct number of items: an item is correct if it reaches the cutvalue
            for (final Iterator iter = itemContexts.iterator(); iter.hasNext();) {
                final ItemContext ict = (ItemContext) iter.next();

                final Variable var = ict.getVariables().getSCOREVariable();
                if (var == null) {
                    // we demand that a SCORE variable must always exist
                    throw new RuntimeException("no SCORE def for " + ict.getIdent());
                } else {
                    final float itemscore = var.getTruncatedValue();
                    final float itemcutval = var.getCutValue();
                    if (itemscore >= itemcutval) {
                        tmpscore++; // count items correct
                    }
                }
            }
            if (tmpscore >= cutvalue) {
                totalScore = 1.0f; // cutvalue of the section
            }
        } else {
            throw new RuntimeException("scoring algorithm " + scoremodel + " not supported");
        }
    }

    /**
     * @return
     */
    public float getScore() {
        calcScore();
        return totalScore;
    }

    /**
     * @return
     */
    public String getIdent() {
        return ident;
    }

    /**
     * @return
     */
    public String getTitle() {
        return el_section.attributeValue("title");
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "<br />section:" + getIdent() + " score:" + getScore() + ",items" + itemContexts.toString() + "="
                + super.toString();
    }

    /**
     * Returns the sectionItems.
     * 
     * @return List
     */
    public List getSectionItemContexts() {
        return itemContexts;
    }

    /**
     * Method getItemContext.
     * 
     * @param sIdent
     * @return ItemContext
     */
    public ItemContext getItemContext(final String sIdent) {
        for (final Iterator it_icts = getSectionItemContexts().iterator(); it_icts.hasNext();) {
            final ItemContext itc = (ItemContext) it_icts.next();
            if (itc.getIdent().equals(sIdent)) {
                return itc;
            }
        }

        // not found: for a user answer, no corresponding item could be found ->
        // error (in form?,logic...)
        // the other way round: no answer of user = the ItemInput of an ItemContext
        // is simply not set
        throw new RuntimeException(
                "could not find an itemcontext with ident " + ident + " , but got an itemInput with this ident!");
    }

    /**
     * @return
     */
    public int getItemsAttemptedCount() {
        int total = 0;
        final int itcnt = getItemContextCount();
        for (int i = 0; i < itcnt; i++) {
            if (getItemContext(i).getTimesAnswered() > 0) {
                total++;
            }
        }
        return total;
    }

    /**
     * @return
     */
    public int getItemsPresentedCount() {
        int total = 0;
        final int itcnt = getItemContextCount();
        for (int i = 0; i < itcnt; i++) {
            if (getItemContext(i).isStarted()) {
                total++;
            }
        }
        return total;
    }

    /**
     * @return
     */
    public int getItemsAnsweredCount() {
        int total = 0;
        final int itcnt = getItemContextCount();
        for (int i = 0; i < itcnt; i++) {
            if (getItemContext(i).getTimesAnswered() > 0) {
                total++;
            }
        }
        return total;
    }

    /**
     * @return
     */
    public int getItemsOpenCount() {
        int total = 0;
        final int itcnt = getItemContextCount();
        for (int i = 0; i < itcnt; i++) {
            if (getItemContext(i).isOpen()) {
                total++;
            }
        }
        return total;
    }

    /**
     * @return
     */
    public int getItemContextCount() {
        return itemContexts.size();
    }

    /**
     * Returns the currentItemContextPos.
     * 
     * @return int
     */
    public int getCurrentItemContextPos() {
        return currentItemContextPos;
    }

    /**
     * Sets the currentItemContextPos.
     * 
     * @param currentItemContextPos The currentItemContextPos to set
     */
    public void setCurrentItemContextPos(final int currentItemContextPos) {
        this.currentItemContextPos = currentItemContextPos;
    }

    /**
     * Method getCurrentItemContext.
     * 
     * @return Element
     */
    public ItemContext getCurrentItemContext() {
        return (ItemContext) itemContexts.get(currentItemContextPos);
    }

    /**
     * checks whether the user may still submit answers
     * 
     * @return
     */
    public boolean isOpen() {
        // not started yet or no timelimit or within timelimit
        return onTime();
    }

    /**
     * @return
     */
    public boolean onTime() {
        // ok if not started yet or no time limit or within limit
        return (timeOfStart == -1) || (durationLimit == -1)
                || (System.currentTimeMillis() < (timeOfStart + durationLimit));
    }

    /**
     * @return
     */
    public boolean isStarted() {
        return (timeOfStart != -1);
    }

    /**
     * @param pos
     * @return
     */
    public ItemContext getItemContext(final int pos) {
        return (ItemContext) itemContexts.get(pos);
    }

    /**
     * Returns the timeOfStart.
     * 
     * @return long
     */
    public long getTimeOfStart() {
        return timeOfStart;
    }

    /**
     * Returns the timeOfStop.
     * 
     * @return long
     */
    public long getLatestAnswerTime() {
        return latestAnswerTime;
    }

    /**
     * Get the time limit set on this section
     * 
     * @return
     */
    public long getDurationLimit() {
        return durationLimit;
    }

    /**
     * Get the time to completion for this section
     * 
     * @return
     */
    public long getDuration() {
        if (timesAnswered == 0) {
            return -1;
        }
        return latestAnswerTime - timeOfStart;
    }

    /**
     * @return
     */
    public float getMaxScore() {
        float score = 0.0f;
        for (int i = 0; i < getItemContextCount(); i++) {
            final ItemContext itctx = getItemContext(i);
            final float maxScore = itctx.getMaxScore();
            if (maxScore == -1) {
                return -1;
            } else {
                score += maxScore;
            }
        }
        return score;
    }

    /**
     * @return Output
     */
    public Output getOutput() {
        if (output == null) {
            output = new Output();
        }
        return output;
    }

    /**
     * @return boolean
     */
    public boolean isFeedbackavailable() {
        return feedbackavailable;
    }

    public Objectives getObjectives() {
        return objectives;
    }

    public int getCutValue() {
        return cutvalue;
    }
}