/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <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 the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <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> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.ims.qti21.questionimport; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; import org.olat.ims.qti21.model.xml.AssessmentItemFactory; import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.EntryType; import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry; import org.olat.ims.qti21.model.xml.interactions.KPrimAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation; import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuilder; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P; import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleAssociableChoice; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice; import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; import uk.ac.ed.ph.jqtiplus.types.Identifier; /** * * Initial date: 24.09.2014<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class CSVToAssessmentItemConverter { private static final OLog log = Tracing.createLoggerFor(CSVToAssessmentItemConverter.class); private int kprimPosition = 0; private ImportOptions options; private final QtiSerializer qtiSerializer; private AssessmentItemAndMetadata currentItem; private final List<AssessmentItemAndMetadata> items = new ArrayList<>(); public CSVToAssessmentItemConverter(ImportOptions options, QtiSerializer qtiSerializer) { this.options = options; this.qtiSerializer = qtiSerializer; } public List<AssessmentItemAndMetadata> getItems() { return items; } public void parse(String input) { String[] lines = input.split("\r?\n"); for (int i = 0; i<lines.length; i++) { String line = lines[i]; if (line.equals("")) { continue; } String delimiter = "\t"; // use comma as fallback delimiter, e.g. for better testing if (line.indexOf(delimiter) == -1) { delimiter = ","; } String[] parts = line.split(delimiter); if(parts.length > 1) { processLine(parts); } } if(currentItem != null) { build(); items.add(currentItem); currentItem = null; } } private void processLine(String[] parts) { String marker = parts[0].toLowerCase(); switch(marker) { case "typ": case "type": processType(parts); break; case "titel": case "title": processTitle(parts); break; case "beschreibung": case "description": processDescription(parts); break; case "frage": case "question": processQuestion(parts); break; case "punkte": case "points": processPoints(parts); break; case "fachbereich": case "subject": processTaxonomyPath(parts); break; case "feedback correct answer": processFeedbackCorrectAnswer(parts); break; case "feedback wrong answer": processFeedbackWrongAnswer(parts); break; case "schlagworte": case "keywords": processKeywords(parts); break; case "abdeckung": case "coverage": processCoverage(parts); break; case "level": processLevel(parts); break; case "sprache": case "language": processLanguage(parts); break; case "durchschnittliche bearbeitungszeit": case "typical learning time": processTypicalLearningTime(parts); break; case "itemschwierigkeit": case "difficulty index": processDifficultyIndex(parts); break; case "standardabweichung itemschwierigkeit": case "standard deviation": processStandardDeviation(parts); break; case "trennsch\u00E4rfe": case "discrimination index": processDiscriminationIndex(parts); break; case "anzahl distraktoren": case "distractors": processDistractors(parts); break; case "editor": processEditor(parts); break; case "editor version": processEditorVersion(parts); break; case "lizenz": case "license": processLicense(parts); break; default: processChoice(parts); } } private void processDescription(String[] parts) { if(currentItem == null || parts.length < 2) return; String description = parts[1]; if(StringHelper.containsNonWhitespace(description)) { currentItem.setDescription(description); } } private void processLevel(String[] parts) { if(currentItem == null || parts.length < 2) return; String level = parts[1]; if(StringHelper.containsNonWhitespace(level)) { currentItem.setLevel(level.trim()); } } private void processTypicalLearningTime(String[] parts) { if(currentItem == null || parts.length < 2) return; String time = parts[1]; if(StringHelper.containsNonWhitespace(time)) { currentItem.setTypicalLearningTime(time.trim()); } } private void processLicense(String[] parts) { if(currentItem == null || parts.length < 2) return; String license = parts[1]; if(StringHelper.containsNonWhitespace(license)) { currentItem.setLicense(license.trim()); } } private void processEditor(String[] parts) { if(currentItem == null || parts.length < 2) return; String editor = parts[1]; if(StringHelper.containsNonWhitespace(editor)) { currentItem.setEditor(editor.trim()); } } private void processEditorVersion(String[] parts) { if(currentItem == null || parts.length < 2) return; String editorVersion = parts[1]; if(StringHelper.containsNonWhitespace(editorVersion)) { currentItem.setEditorVersion(editorVersion.trim()); } } private void processFeedbackCorrectAnswer(String[] parts) { if(currentItem == null || parts.length < 2) return; String feedback = parts[1]; if(StringHelper.containsNonWhitespace(feedback)) { AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); itemBuilder.createCorrectFeedback().setText(feedback); } } private void processFeedbackWrongAnswer(String[] parts) { if(currentItem == null || parts.length < 2) return; String feedback = parts[1]; if(StringHelper.containsNonWhitespace(feedback)) { AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); itemBuilder.createIncorrectFeedback().setText(feedback); } } private void processDistractors(String[] parts) { if(currentItem == null || parts.length < 2) return; String distractors = parts[1]; if(StringHelper.containsNonWhitespace(distractors)) { try { currentItem.setNumOfAnswerAlternatives(Integer.parseInt(distractors.trim())); } catch (NumberFormatException e) { log.warn("", e); } } } private void processDiscriminationIndex(String[] parts) { if(currentItem == null || parts.length < 2) return; String discriminationIndex = parts[1]; if(StringHelper.containsNonWhitespace(discriminationIndex)) { try { currentItem.setDifferentiation(new BigDecimal(discriminationIndex.trim())); } catch (Exception e) { log.warn("", e); } } } private void processDifficultyIndex(String[] parts) { if(currentItem == null || parts.length < 2) return; String difficulty = parts[1]; if(StringHelper.containsNonWhitespace(difficulty)) { try { BigDecimal dif = new BigDecimal(difficulty.trim()); if(dif.doubleValue() >= 0.0d && dif.doubleValue() <= 1.0d) { currentItem.setDifficulty(dif); } else { currentItem.setHasError(true); } } catch (Exception e) { log.warn("", e); } } } private void processStandardDeviation(String[] parts) { if(currentItem == null || parts.length < 2) return; String stddev = parts[1]; if(StringHelper.containsNonWhitespace(stddev)) { try { BigDecimal dev = new BigDecimal(stddev.trim()); if(dev.doubleValue() >= 0.0d && dev.doubleValue() <= 1.0d) { currentItem.setStdevDifficulty(dev); } else { currentItem.setHasError(true); } } catch (Exception e) { log.warn("", e); } } } private void processType(String[] parts) { if(currentItem != null) { build(); items.add(currentItem); currentItem = null; } if(parts.length > 1) { String type = parts[1].toLowerCase(); AssessmentItemBuilder itemBuilder; switch(type) { case "fib": { FIBAssessmentItemBuilder fibItemBuilder = new FIBAssessmentItemBuilder("Gap text", EntryType.text, qtiSerializer); fibItemBuilder.setQuestion(""); fibItemBuilder.clearTextEntries(); fibItemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); itemBuilder = fibItemBuilder; break; } case "mc": { MultipleChoiceAssessmentItemBuilder mcItemBuilder = new MultipleChoiceAssessmentItemBuilder("Multiple choice", "New answer", qtiSerializer); mcItemBuilder.clearSimpleChoices(); mcItemBuilder.clearMapping(); mcItemBuilder.setShuffle(options.isShuffle()); mcItemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); itemBuilder = mcItemBuilder; break; } case "sc": { SingleChoiceAssessmentItemBuilder scItemBuilder = new SingleChoiceAssessmentItemBuilder("Single choice", "New answer", qtiSerializer); scItemBuilder.clearSimpleChoices(); scItemBuilder.clearMapping(); scItemBuilder.setShuffle(options.isShuffle()); scItemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); itemBuilder = scItemBuilder; break; } case "kprim": { kprimPosition = 0; KPrimAssessmentItemBuilder kprimItemBuilder = new KPrimAssessmentItemBuilder("Kprim", "New answer", qtiSerializer); itemBuilder = kprimItemBuilder; break; } default: { itemBuilder = null; } } if(itemBuilder != null) { currentItem = new AssessmentItemAndMetadata(itemBuilder); } else { log.warn("Question type not supported: " + type); currentItem = null; } } } private void build() { if(currentItem != null) { try { String question = currentItem.getItemBuilder().getQuestion(); if(!StringHelper.isHtml(question)) { question = "<p>" + question + "</p>"; } currentItem.getItemBuilder().setQuestion(question); currentItem.getItemBuilder().build(); } catch (Exception e) { log.error("", e); currentItem.setHasError(true); } } } private void processCoverage(String[] parts) { if(currentItem == null || parts.length < 2) return; String coverage = parts[1]; if(StringHelper.containsNonWhitespace(coverage)) { currentItem.setCoverage(coverage); } } private void processKeywords(String[] parts) { if(currentItem == null || parts.length < 2) return; String keywords = parts[1]; if(StringHelper.containsNonWhitespace(keywords)) { currentItem.setKeywords(keywords); } } private void processTaxonomyPath(String[] parts) { if(currentItem == null || parts.length < 2) return; String taxonomyPath = parts[1]; if(StringHelper.containsNonWhitespace(taxonomyPath)) { currentItem.setTaxonomyPath(taxonomyPath); } } private void processLanguage(String[] parts) { if(currentItem == null || parts.length < 2) return; String language = parts[1]; if(StringHelper.containsNonWhitespace(language)) { currentItem.setLanguage(language); } } private void processTitle(String[] parts) { if(currentItem == null || parts.length < 2) return; String title = parts[1]; if(StringHelper.containsNonWhitespace(title)) { currentItem.setTitle(title); } } private void processQuestion(String[] parts) { if(currentItem == null) return; String content = parts[1]; if(StringHelper.containsNonWhitespace(content)) { AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); itemBuilder.setQuestion("<p>" + content + "</p>"); } } private void processPoints(String[] parts) { if(currentItem == null) return; double points = parseFloat(parts[1], 1.0f); AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); if (itemBuilder instanceof SimpleChoiceAssessmentItemBuilder) { itemBuilder.setMinScore(0.0d); itemBuilder.setMaxScore(points); } else if(itemBuilder instanceof FIBAssessmentItemBuilder) { itemBuilder.setMinScore(0.0d); itemBuilder.setMaxScore(points); } else if(itemBuilder instanceof KPrimAssessmentItemBuilder) { itemBuilder.setMinScore(0.0d); itemBuilder.setMaxScore(points); } } private void processChoice(String[] parts) { if(currentItem == null || parts.length < 2) { return; } try { AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); if (itemBuilder instanceof SimpleChoiceAssessmentItemBuilder) { processChoice_smc(parts, (SimpleChoiceAssessmentItemBuilder)itemBuilder); } else if(itemBuilder instanceof FIBAssessmentItemBuilder) { processChoice_fib(parts, (FIBAssessmentItemBuilder)itemBuilder); } else if(itemBuilder instanceof KPrimAssessmentItemBuilder) { processChoice_kprim(parts, (KPrimAssessmentItemBuilder)itemBuilder); } } catch (NumberFormatException e) { log.warn("Cannot parse point for: " + parts[0] + " / " + parts[1], e); } } private void processChoice_smc(String[] parts, SimpleChoiceAssessmentItemBuilder choiceBuilder) { double point = parseFloat(parts[0], 1.0f); String content = parts[1]; ChoiceInteraction interaction = choiceBuilder.getChoiceInteraction(); SimpleChoice newChoice = AssessmentItemFactory .createSimpleChoice(interaction, content, choiceBuilder.getQuestionType().getPrefix()); choiceBuilder.addSimpleChoice(newChoice); choiceBuilder.setMapping(newChoice.getIdentifier(), point); if(point > 0.0) { if (choiceBuilder instanceof MultipleChoiceAssessmentItemBuilder) { ((MultipleChoiceAssessmentItemBuilder)choiceBuilder).addCorrectAnswer(newChoice.getIdentifier()); } else { ((SingleChoiceAssessmentItemBuilder)choiceBuilder).setCorrectAnswer(newChoice.getIdentifier()); } } } private void processChoice_fib(String[] parts, FIBAssessmentItemBuilder fibBuilder) { String firstPart = parts[0].toLowerCase(); if("text".equals(firstPart) || "texte".equals(firstPart)) { String text = parts[1]; if(StringHelper.containsNonWhitespace(fibBuilder.getQuestion())) { fibBuilder.setQuestion(fibBuilder.getQuestion() + " " + text); } else { fibBuilder.setQuestion(text); } } else { double score = parseFloat(parts[0], 1.0f); String correctBlank = parts[1]; String responseId = fibBuilder.generateResponseIdentifier(); TextEntry textEntry = fibBuilder.createTextEntry(responseId); parseAlternatives(correctBlank, score, textEntry); if(parts.length > 2) { String sizes = parts[2]; String[] sizeArr = sizes.split(","); if(sizeArr.length >= 2) { int size = Integer.parseInt(sizeArr[0]); //int maxLength = Integer.parseInt(sizeArr[1]); textEntry.setExpectedLength(size); } } String entry = " <textEntryInteraction responseIdentifier=\"" + responseId + "\"/>"; fibBuilder.setQuestion(fibBuilder.getQuestion() + " " + entry); } } private void processChoice_kprim(String[] parts, KPrimAssessmentItemBuilder kprimBuilder) { String firstPart = parts[0].toLowerCase(); String answer = parts[1]; Identifier correctIncorrectIdentifier; if("+".equals(firstPart)) { correctIncorrectIdentifier = QTI21Constants.CORRECT_IDENTIFIER; } else { correctIncorrectIdentifier = QTI21Constants.WRONG_IDENTIFIER; } List<SimpleAssociableChoice> choices = kprimBuilder.getKprimChoices(); SimpleAssociableChoice choice = choices.get(kprimPosition); P choiceText = AssessmentItemFactory.getParagraph(choice, answer); choice.getFlowStatics().clear(); choice.getFlowStatics().add(choiceText); kprimBuilder.setAssociation(choice.getIdentifier(), correctIncorrectIdentifier); kprimPosition++; } private void parseAlternatives(String value, double score, TextEntry textEntry) { String[] values = value.split(";"); if(values.length > 0) { textEntry.setSolution(values[0]); textEntry.setScore(score); } if(values.length > 1) { for(int i=1; i<values.length; i++) { textEntry.addAlternative(values[i], score); } } } private float parseFloat(String value, float defaultValue) { float floatValue = defaultValue; if(value != null) { if(value.indexOf(",") >= 0) { value = value.replace(",", "."); } floatValue = Float.parseFloat(value); } return floatValue; } }