/** * <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.qti.questionimport; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import org.olat.core.gui.translator.Translator; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.filter.Filter; import org.olat.core.util.filter.FilterFactory; import org.olat.ims.qti.editor.QTIEditHelper; import org.olat.ims.qti.editor.beecom.objects.ChoiceQuestion; import org.olat.ims.qti.editor.beecom.objects.ChoiceResponse; import org.olat.ims.qti.editor.beecom.objects.Control; import org.olat.ims.qti.editor.beecom.objects.FIBQuestion; import org.olat.ims.qti.editor.beecom.objects.FIBResponse; import org.olat.ims.qti.editor.beecom.objects.Item; import org.olat.ims.qti.editor.beecom.objects.Material; import org.olat.ims.qti.editor.beecom.objects.Mattext; import org.olat.ims.qti.editor.beecom.objects.QTIObject; import org.olat.ims.qti.editor.beecom.objects.Question; import org.olat.ims.qti.editor.beecom.objects.Response; /** * * Initial date: 24.09.2014<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class CSVToQuestionConverter { private static final OLog log = Tracing.createLoggerFor(CSVToQuestionConverter.class); private ItemAndMetadata currentItem; private final List<ItemAndMetadata> items = new ArrayList<>(); private Translator translator; private ImportOptions options; public CSVToQuestionConverter(Translator translator, ImportOptions options) { this.translator = translator; this.options = options; } public List<ItemAndMetadata> 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) { 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 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)) { Item item = currentItem.getItem(); Control control = QTIEditHelper.getControl(item); if(control.getFeedback() != 1) { control.setFeedback(1); } QTIEditHelper.setFeedbackMastery(item, feedback); } } private void processFeedbackWrongAnswer(String[] parts) { if(currentItem == null || parts.length < 2) return; String feedback = parts[1]; if(StringHelper.containsNonWhitespace(feedback)) { Item item = currentItem.getItem(); Control control = QTIEditHelper.getControl(item); if(control.getFeedback() != 1) { control.setFeedback(1); } QTIEditHelper.setFeedbackFail(item, 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) { items.add(currentItem); } if(parts.length > 1) { String type = parts[1].toLowerCase(); switch(type) { case "fib": { currentItem = new ItemAndMetadata(QTIEditHelper.createFIBItem(translator)); ((FIBQuestion)currentItem.getItem().getQuestion()).getResponses().clear(); break; } case "mc": { currentItem = new ItemAndMetadata(QTIEditHelper.createMCItem(translator)); ((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear(); ((ChoiceQuestion)currentItem.getItem().getQuestion()).setShuffle(options.isShuffle()); break; } case "sc": { currentItem = new ItemAndMetadata(QTIEditHelper.createSCItem(translator)); ((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear(); ((ChoiceQuestion)currentItem.getItem().getQuestion()).setShuffle(options.isShuffle()); break; } default: { log.warn("Question type not supported: " + type); currentItem = null; } } } } 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 processDescription(String[] parts) { if(currentItem == null || parts.length < 2) return; String description = parts[1]; if(StringHelper.containsNonWhitespace(description)) { currentItem.setDescription(description); } } private void processQuestion(String[] parts) { if(currentItem == null) return; Question question = currentItem.getItem().getQuestion(); Material mat = question.getQuestion(); String content = parts[1]; Mattext matText = new Mattext(content); List<QTIObject> elements = new ArrayList<QTIObject>(1); elements.add(matText); mat.setElements(elements); } private void processPoints(String[] parts) { if(currentItem == null) return; float points = parseFloat(parts[1], 1.0f); Question question = currentItem.getItem().getQuestion(); int type = question.getType(); if (type == Question.TYPE_MC) { question.setMinValue(0.0f); question.setMaxValue(points); question.setSingleCorrect(false); question.setSingleCorrectScore(0.0f); } else if (type == Question.TYPE_SC) { question.setSingleCorrect(true); question.setSingleCorrectScore(points); } else if(type == Question.TYPE_FIB) { question.setMinValue(0.0f); question.setMaxValue(points); question.setSingleCorrect(false); question.setSingleCorrectScore(0.0f); } } private void processChoice(String[] parts) { if(currentItem == null || parts.length < 2) { return; } try { Question question = currentItem.getItem().getQuestion(); int type = question.getType(); if (type == Question.TYPE_MC || type == Question.TYPE_SC) { float point = parseFloat(parts[0], 1.0f); String content = parts[1]; ChoiceQuestion choice = (ChoiceQuestion)question; List<Response> choices = choice.getResponses(); ChoiceResponse newChoice = new ChoiceResponse(); newChoice.getContent().add(createMattext(content)); newChoice.setCorrect(point > 0.0f); newChoice.setPoints(point); choices.add(newChoice); } else if(type == Question.TYPE_FIB) { String firstPart = parts[0].toLowerCase(); FIBQuestion fib = (FIBQuestion)question; if("text".equals(firstPart) || "texte".equals(firstPart)) { String text = parts[1]; FIBResponse response = new FIBResponse(); response.setType(FIBResponse.TYPE_CONTENT); Material mat = createMaterialWithText(text); response.setContent(mat); fib.getResponses().add(response); } else { float point = parseFloat(parts[0], 1.0f); String correctBlank = parts[1]; FIBResponse response = new FIBResponse(); response.setType(FIBResponse.TYPE_BLANK); response.setCorrectBlank(correctBlank); response.setPoints(point); 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]); response.setSize(size); response.setMaxLength(maxLength); } } fib.getResponses().add(response); } } } catch (NumberFormatException e) { log.warn("Cannot parse point for: " + parts[0] + " / " + parts[1], e); } } 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; } private Material createMaterialWithText(String text) { Material material = new Material(); material.add(createMattext(text)); return material; } private Mattext createMattext(String text) { //text is already in a CDATA text = text.replace("// <![CDATA[", "").replace("// ]]>", ""); // Strip unnecessary BR tags at the beginning and the end which are added // automaticall by mysterious tiny code and cause problems in FIB questions. (OLAT-4363) // Use explicit return which create a P tag if you want a line break. if (text.startsWith("<br />") && text.length() > 6) text = text.substring(6); if (text.endsWith("<br />") && text.length() > 6) text = text.substring(0, text.length()-6); // Remove any conditional comments due to strange behavior in test (OLAT-4518) Filter conditionalCommentFilter = FilterFactory.getConditionalHtmlCommentsFilter(); text = conditionalCommentFilter.filter(text); Mattext mattext = new Mattext(text); return mattext; } }