/** * 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.ims.qti.process; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.olat.core.util.cache.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 { private static class QTIDocument { private final Long date; private final byte[] content; public QTIDocument(Long date, byte[] content) { this.date = date; this.content = content; } } private static CacheWrapper<String,QTIDocument> ehCachLoadedQTIDocs = CoordinatorManager.getInstance() .getCoordinator().getCacher().getCache(QTIHelper.class.getSimpleName(), "QTI_xml_Documents"); /** * */ private static Map<String,BooleanEvaluable> booleanEvals; private static Map<String,ScoreBooleanEvaluable> scoreBooleanEvals; private static Map<String,ExpressionBuilder> 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<String,BooleanEvaluable>(); 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<String,ScoreBooleanEvaluable>(); 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<String,ExpressionBuilder>(); 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(String name) { BooleanEvaluable bev = booleanEvals.get(name); if (bev == null) throw new RuntimeException("no bev for '<" + name + ">'"); return bev; } /** * @param name * @return */ public static ScoreBooleanEvaluable getSectionBooleanEvaluableInstance(String name) { ScoreBooleanEvaluable sbev = scoreBooleanEvals.get(name); if (sbev == null) throw new RuntimeException("no section bev for " + name); return sbev; } public static ExpressionBuilder getExpressionBuilder(String name) { ExpressionBuilder eb = 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(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' 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 (ArrayIndexOutOfBoundsException e) { return -1; } return result; } private static int parseIntFromString(String str, char stopChar) { int stopCharPos = str.indexOf(stopChar); if (stopCharPos < 0) return -1; // stop char not found String val = str.substring(0, stopCharPos); try { return Integer.parseInt(val); } catch (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(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(Element el_outcomes) { String varName; Variables variables = new Variables(); if (el_outcomes == null) return variables; 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 (Iterator iter = decvars.iterator(); iter.hasNext();) { 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")) { String def = decvar.attributeValue("defaultval"); String min = decvar.attributeValue("minvalue"); String max = decvar.attributeValue("maxvalue"); 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(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(Element el_root, String xPath, String attName) { int res = -1; if (xPath == null) { String val = el_root.attributeValue(attName); res = Integer.parseInt(val); } else { Element el_el = (Element) el_root.selectSingleNode(xPath); if (el_el != null) { 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(Element el_root, String xPath, String attName) { float res = -1; if (xPath == null) { String val = el_root.attributeValue(attName); res = Float.parseFloat(val); } else { Element el_el = (Element) el_root.selectSingleNode(xPath); if (el_el != null) { 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(LocalFileImpl pathToXml) { if (pathToXml == null) { // xml file does not exist! return null; } byte[] doc = null; // get lastmodified to see if the file is newer than the cache entry and we thus need to reload it. Long lmf = Long.valueOf(pathToXml.getLastModified()); String key = ((LocalFolderImpl) pathToXml.getParentContainer()).getBasefile().getAbsolutePath(); QTIDocument tuple = ehCachLoadedQTIDocs.get(key); if (tuple != null && tuple.date.compareTo(lmf) == 0) { // in cache and not modified doc = tuple.content; } else { // load it: either not in cache anymore or modified in the meantime doc = getDocumentAsXML(pathToXml.getInputStream()); if(doc == null) { //the xml file could not be parsed return null; } if(tuple == null) { QTIDocument cachedTuple = ehCachLoadedQTIDocs.putIfAbsent(key, new QTIDocument(lmf, doc )); if(cachedTuple != null) { doc = cachedTuple.content; } } else { // 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.update(key, new QTIDocument(lmf, doc )); } } // we do not know if the receiver is destructive -> protect the cached entry // return a copy of the doc. return getDocument(doc); } public static Document getDocument(Path xmlPath) { try(InputStream in=Files.newInputStream(xmlPath)) { XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); return xmlParser.parse(in, false); } catch(IOException e) { return null; } } public static Document getDocument(byte[] xml) { try { XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); return xmlParser.parse(new ByteArrayInputStream(xml), false); } catch(Exception e) { return null; } } public static byte[] getDocumentAsXML(InputStream in) { try { XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); return xmlParser.parse(in, false).asXML().getBytes(); } catch(Exception e) { return null; } } }