org.olat.ims.qti.process.QTIHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.olat.ims.qti.process.QTIHelper.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.process;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.olat.core.logging.Tracing;
import org.olat.core.util.ObjectCloner;
import org.olat.core.util.cache.n.CacheWrapper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.vfs.LocalFileImpl;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.xml.XMLParser;
import org.olat.ims.qti.container.DecimalVariable;
import org.olat.ims.qti.container.Variable;
import org.olat.ims.qti.container.Variables;
import org.olat.ims.qti.process.elements.BooleanEvaluable;
import org.olat.ims.qti.process.elements.ExpressionBuilder;
import org.olat.ims.qti.process.elements.QTI_and;
import org.olat.ims.qti.process.elements.QTI_item;
import org.olat.ims.qti.process.elements.QTI_not;
import org.olat.ims.qti.process.elements.QTI_or;
import org.olat.ims.qti.process.elements.QTI_other;
import org.olat.ims.qti.process.elements.QTI_respcondition;
import org.olat.ims.qti.process.elements.QTI_resprocessing;
import org.olat.ims.qti.process.elements.QTI_varequal;
import org.olat.ims.qti.process.elements.QTI_vargt;
import org.olat.ims.qti.process.elements.QTI_vargte;
import org.olat.ims.qti.process.elements.QTI_varinside;
import org.olat.ims.qti.process.elements.QTI_varlt;
import org.olat.ims.qti.process.elements.QTI_varlte;
import org.olat.ims.qti.process.elements.ScoreBooleanEvaluable;
import org.olat.ims.qti.process.elements.section.QTI_and_selection;
import org.olat.ims.qti.process.elements.section.QTI_and_test;
import org.olat.ims.qti.process.elements.section.QTI_not_selection;
import org.olat.ims.qti.process.elements.section.QTI_not_test;
import org.olat.ims.qti.process.elements.section.QTI_or_selection;
import org.olat.ims.qti.process.elements.section.QTI_or_test;
import org.olat.ims.qti.process.elements.section.QTI_selection_metadata;
import org.olat.ims.qti.process.elements.section.QTI_variable_test;
import org.olat.ims.resources.IMSEntityResolver;

/**
 */
public class QTIHelper {
    // logging
    private static final Logger log = Logger.getLogger(QTIHelper.class);

    private static CacheWrapper ehCachLoadedQTIDocs = CoordinatorManager.getInstance().getCoordinator().getCacher()
            .getOrCreateCache(QTIHelper.class, "QTI_xml_Documents");
    /**
     * 
     */
    private static Map booleanEvals;
    private static Map scoreBooleanEvals;
    private static Map expressionBuilders;

    private final static long sec = 1000;
    private final static long minute = 60 * sec;
    private final static long hour = 60 * minute;
    private final static long day = 24 * hour;
    private final static long year = 365 * day;
    private final static long month = 30 * day;

    static {
        booleanEvals = new HashMap();
        booleanEvals.put("and", new QTI_and());
        booleanEvals.put("or", new QTI_or());
        booleanEvals.put("not", new QTI_not());
        booleanEvals.put("varequal", new QTI_varequal());
        booleanEvals.put("vargte", new QTI_vargte());
        booleanEvals.put("vargt", new QTI_vargt());
        booleanEvals.put("varlte", new QTI_varlte());
        booleanEvals.put("varlt", new QTI_varlt());
        booleanEvals.put("varinside", new QTI_varinside());
        booleanEvals.put("other", new QTI_other());

        // section boolean evaluables
        scoreBooleanEvals = new HashMap();
        scoreBooleanEvals.put("and_test", new QTI_and_test());
        scoreBooleanEvals.put("or_test", new QTI_or_test());
        scoreBooleanEvals.put("not_test", new QTI_not_test());
        scoreBooleanEvals.put("variable_test", new QTI_variable_test());

        // ims qti sao
        expressionBuilders = new HashMap();
        expressionBuilders.put("and_selection", new QTI_and_selection());
        expressionBuilders.put("or_selection", new QTI_or_selection());
        expressionBuilders.put("not_selection", new QTI_not_selection());
        expressionBuilders.put("selection_metadata", new QTI_selection_metadata());
    }

    private static QTI_respcondition respcondition = new QTI_respcondition();
    private static QTI_resprocessing resprocessing = new QTI_resprocessing();
    // private static QTI_or_selection or_selection = new QTI_or_selection();
    private static QTI_item QtiItem = new QTI_item();

    /**
     * @return
     */
    public static QTI_resprocessing getQTI_resprocessing() {
        return resprocessing;
    }

    /**
     * 
     */
    public static QTI_respcondition getQTI_respcondition() {
        return respcondition;
    }

    /**
     * 
     */
    public static QTI_and getQTI_and() {
        return (QTI_and) booleanEvals.get("and");
    }

    /**
     * @param name
     * @return
     */
    public static BooleanEvaluable getBooleanEvaluableInstance(final String name) {
        final BooleanEvaluable bev = (BooleanEvaluable) booleanEvals.get(name);
        if (bev == null) {
            throw new RuntimeException("no bev for '<" + name + ">'");
        }
        return bev;
    }

    /**
     * @param name
     * @return
     */
    public static ScoreBooleanEvaluable getSectionBooleanEvaluableInstance(final String name) {
        final ScoreBooleanEvaluable sbev = (ScoreBooleanEvaluable) scoreBooleanEvals.get(name);
        if (sbev == null) {
            throw new RuntimeException("no section bev for " + name);
        }
        return sbev;
    }

    public static ExpressionBuilder getExpressionBuilder(final String name) {
        final ExpressionBuilder eb = (ExpressionBuilder) expressionBuilders.get(name);
        if (eb == null) {
            throw new RuntimeException("no expression builder for " + name);
        }
        return eb;
    }

    /**
     * @return QTI_item
     */
    public static QTI_item getQtiItem() {
        return QtiItem;
    }

    /**
     * Parse ISO8601 duration and return millis equivalent. Durations are preceeded by a 'P' character. Followed by year(Y), month(M), day(D), hour(H), minutes(M) and
     * second(S). Time components (HMS) are preceeded by a 'T' character. (e.g. P0Y0M1DT3H15M2S -> 1 day, 3 hours, 15 minutes, 2 seconds PT15M30S -> 15 minutes, 30
     * seconds.
     * 
     * @return millis representing ISO duration
     */
    public static long parseISODuration(final String iso) {
        String trunc = iso;
        long result = 0;

        if (trunc.charAt(0) != 'P') {
            return -1; // must begin with 'P'
        }
        try {
            // parseIntFromString returns -1 if stop char is not found.
            // return -1 in that case (catch statement).
            trunc = trunc.substring(1, trunc.length()); // truncate 'P'
            final int timeComp = trunc.indexOf('T');
            if (timeComp != 0) { // we have a YMD component
                int i = parseIntFromString(trunc, 'Y'); // parse year component
                if (i >= 0) {
                    result += i * year;
                    trunc = trunc.substring(trunc.indexOf('Y') + 1, trunc.length());
                }

                i = parseIntFromString(trunc, 'M'); // parse month component
                if (i >= 0 && i < timeComp) { // Month component if 'M' before 'T'
                    result += i * month;
                    trunc = trunc.substring(trunc.indexOf('M') + 1, trunc.length());
                }

                i = parseIntFromString(trunc, 'D');
                if (i >= 0) {
                    result += i * day;
                    trunc = trunc.substring(trunc.indexOf('D') + 1, trunc.length());
                }
            }

            if (timeComp != -1) {
                // we have a time component
                trunc = trunc.substring(1, trunc.length()); // truncate 'T'
                int i = parseIntFromString(trunc, 'H'); // parse hour component
                if (i >= 0) {
                    result += i * hour;
                    trunc = trunc.substring(trunc.indexOf('H') + 1, trunc.length());
                }

                i = parseIntFromString(trunc, 'M'); // parse minute component
                if (i >= 0) {
                    result += i * minute;
                    trunc = trunc.substring(trunc.indexOf('M') + 1, trunc.length());
                }

                i = parseIntFromString(trunc, 'S'); // parse sec component
                if (i >= 0) {
                    result += i * sec;
                }
            }
        } catch (final ArrayIndexOutOfBoundsException e) {
            return -1;
        }
        return result;
    }

    private static int parseIntFromString(final String str, final char stopChar) {
        final int stopCharPos = str.indexOf(stopChar);
        if (stopCharPos < 0) {
            return -1; // stop char not found
        }
        final String val = str.substring(0, stopCharPos);
        try {
            return Integer.parseInt(val);
        } catch (final Exception e) {
            return -1;
        }
    }

    /**
     * Return assessment duration in ISO8601 unspecified duration format (e.g. P0Y0M1DT3H15M2S -> 1 day, 3 hours, 15 minutes, 2 seconds)
     * 
     * @return The string representation in ISO8601 format.
     */
    public static String getISODuration(final long duration) {
        String result = "P";
        long rest = duration;

        // years
        long tmp = rest / year;
        result += tmp + "Y";
        rest -= tmp * year;
        // months
        tmp = rest / month;
        result += tmp + "M";
        rest -= tmp * month;
        // days
        tmp = rest / day;
        result += tmp + "DT";
        rest -= tmp * day;
        // hours
        tmp = (int) rest / hour;
        result += tmp + "H";
        rest -= tmp * hour;
        // minutes
        tmp = (int) rest / minute;
        result += tmp + "M";
        rest -= tmp * minute;
        // secs
        tmp = rest / sec;
        result += tmp + "S";
        return result;
    }

    /**
     * 
     */
    public static Variables declareVariables(final Element el_outcomes) {
        String varName;
        final Variables variables = new Variables();

        if (el_outcomes == null) {
            return variables;
        }
        final List decvars = el_outcomes.selectNodes("decvar");
        /*
         * <decvar defaultval = "0" varname = "Var_SumofScores" vartype = "Integer" minvalue = "-10" maxvalue = "10" cutvalue = "0"/> <decvar minvalue = "0" maxvalue =
         * "1" defaultval = "0"/>
         */
        for (final Iterator iter = decvars.iterator(); iter.hasNext();) {
            final Element decvar = (Element) iter.next();
            varName = decvar.attributeValue("varname"); // dtd CDATA 'SCORE'
            if (varName == null) {
                varName = "SCORE";
            }
            String varType = decvar.attributeValue("vartype");
            if (varType == null) {
                varType = "Integer"; // default
            }
            Variable v = null;
            if (varType.equals("Integer") || varType.equals("Decimal")) {
                final String def = decvar.attributeValue("defaultval");
                final String min = decvar.attributeValue("minvalue");
                final String max = decvar.attributeValue("maxvalue");
                final String cut = decvar.attributeValue("cutvalue");
                v = new DecimalVariable(varName, max, min, cut, def);
                variables.setVariable(v);
            } else {
                throw new RuntimeException("vartype " + varType + " not supported (declaration)");
            }

        }
        return variables;
    }

    public static float attributeToFloat(final Attribute att) {
        float val = -1;
        if (att != null) {
            String sval = att.getValue();
            sval = sval.trim();
            // assume int value, even so dtd cannot enforce it
            val = Integer.parseInt(sval);
        }
        return val;
    }

    /**
     * Method getIntAttribute.
     * 
     * @param el_outpro
     * @param string
     * @param string1
     * @return int
     */
    public static int getIntAttribute(final Element el_root, final String xPath, final String attName) {
        int res = -1;
        if (xPath == null) {
            final String val = el_root.attributeValue(attName);
            res = Integer.parseInt(val);
        } else {
            final Element el_el = (Element) el_root.selectSingleNode(xPath);
            if (el_el != null) {
                final String val = el_el.attributeValue(attName);
                res = Integer.parseInt(val);
            }
        }
        return res;
    }

    /**
     * Method getFloatAttribute.
     * 
     * @param el_outpro
     * @param string
     * @param string1
     * @return float
     */
    public static float getFloatAttribute(final Element el_root, final String xPath, final String attName) {
        float res = -1;
        if (xPath == null) {
            final String val = el_root.attributeValue(attName);
            res = Float.parseFloat(val);
        } else {
            final Element el_el = (Element) el_root.selectSingleNode(xPath);
            if (el_el != null) {
                final String val = el_el.attributeValue(attName);
                res = Float.parseFloat(val);
            }
        }
        return res;
    }

    /**
     * give the hint if the document should be cached or not.
     * 
     * @see QTIHelper#getDocument(LocalFileImpl)
     * @param pathToXml
     * @param useCache
     * @return
     */
    public static Document getDocument(final LocalFileImpl pathToXml) {

        final boolean isDebugEnabled = log.isDebugEnabled();
        long debugEnabledTime = 0;

        Document doc = null;
        if (pathToXml == null) {
            // xml file does not exist!
            return null;
        }
        // get lastmodified to see if the file is newer than the cache entry and we thus need to reload it.
        final Long lmf = Long.valueOf(pathToXml.getLastModified());
        final String key = ((LocalFolderImpl) pathToXml.getParentContainer()).getBasefile().getAbsolutePath();

        // debug info
        if (isDebugEnabled) {
            // identify debug info statements which belong together.
            debugEnabledTime = System.currentTimeMillis();
            log.debug("[" + debugEnabledTime + "] getDocument(..) for [[" + key + "]]");
            log.debug("[" + debugEnabledTime + "] file size is " + pathToXml.getSize());
        }
        // Object[] tuple = (Object[]) (EHCacheManager.getInstance().get(ehCachLoadedQTIDocs, key));
        final Object[] tuple = (Object[]) (ehCachLoadedQTIDocs.get(key));

        if (tuple != null && ((Long) tuple[0]).compareTo(lmf) == 0) {
            // in cache and not modified
            doc = (Document) tuple[1];
            Tracing.logAudit("Document Cache Hit for [[" + key + "]]", QTIHelper.class);
            if (isDebugEnabled) {
                log.debug("[" + debugEnabledTime + "] Document comes from EHCache!");
                log.debug(
                        "[" + debugEnabledTime + "] Document approx Mem usage " + ObjectCloner.getObjectSize(doc));
            }

        } else {
            // load it: either not in cache anymore or modified in the meantime
            doc = getDocument(pathToXml.getInputStream());
            if (doc == null) {
                // the xml file could not be parsed
                log.debug("[" + debugEnabledTime + "] Document could not be parsed, return null!");
                return null;
            }
            // add or replace the document in the cache
            // EHCacheManager.getInstance().putInCache(ehCachLoadedQTIDocs, key, new Object[] { lmf, doc });

            // we use a putSilent here (no invalidation notifications to other cluster nodes), since
            // we did not generate new data, but simply asked to reload it.
            ehCachLoadedQTIDocs.put(key, new Object[] { lmf, doc });
            Tracing.logAudit("load, parse and cache Document for [[" + key + "]]", QTIHelper.class);

            if (isDebugEnabled) {
                log.debug("[" + debugEnabledTime + "] Document loaded, parsed and put into cache!");
                log.debug(
                        "[" + debugEnabledTime + "] Document approx Mem usage " + ObjectCloner.getObjectSize(doc));
            }

        }
        // we do not know if the receiver is destructive -> protect the cached entry
        // return the uncached doc if it is not chached.
        return (Document) ObjectCloner.deepCopy(doc);
    }

    /**
     * Get a document from InputStream. This method will close the input stream.
     * 
     * @param is
     * @return
     */
    private static Document getDocument(final InputStream is) {
        final XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
        Document doc = null;
        try {
            doc = xmlParser.parse(is, false);
            is.close();
        } catch (final Exception e) {
            // nothing we can do - could be IOException or org.dom4j.DocumentException
            doc = null;
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (final IOException e2) {
                // we did our best to close the inputstream
            }
        }
        return doc;
    }

}