/** * <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.qpool; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.io.IOUtils; import org.cyberneko.html.parsers.SAXParser; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentFactory; import org.dom4j.Element; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; import org.olat.core.util.PathUtils.CopyVisitor; import org.olat.core.util.PathUtils.YesMatcher; import org.olat.core.util.StringHelper; import org.olat.core.util.ZipUtil; import org.olat.core.util.vfs.LocalImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.xml.XMLParser; import org.olat.ims.qti.QTIConstants; import org.olat.ims.qti.editor.beecom.objects.QTIDocument; import org.olat.ims.qti.editor.beecom.objects.Question; import org.olat.ims.qti.editor.beecom.parser.ItemParser; import org.olat.ims.qti.questionimport.ItemAndMetadata; import org.olat.ims.resources.IMSEntityResolver; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionType; import org.olat.modules.qpool.TaxonomyLevel; import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; import org.olat.modules.qpool.manager.QLicenseDAO; import org.olat.modules.qpool.manager.QPoolFileStorage; import org.olat.modules.qpool.manager.QuestionItemDAO; import org.olat.modules.qpool.manager.TaxonomyLevelDAO; import org.olat.modules.qpool.model.QEducationalContext; import org.olat.modules.qpool.model.QItemType; import org.olat.modules.qpool.model.QLicense; import org.olat.modules.qpool.model.QuestionItemImpl; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * This class is NOT thread-safe * * Initial date: 07.03.2013<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ class QTIImportProcessor { private static final OLog log = Tracing.createLoggerFor(QTIImportProcessor.class); private final Identity owner; private final Locale defaultLocale; private final String importedFilename; private final File importedFile; private final DB dbInstance; private final QLicenseDAO qLicenseDao; private final QItemTypeDAO qItemTypeDao; private final QPoolFileStorage qpoolFileStorage; private final QuestionItemDAO questionItemDao; private final TaxonomyLevelDAO taxonomyLevelDao; private final QEducationalContextDAO qEduContextDao; public QTIImportProcessor(Identity owner, Locale defaultLocale, QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, TaxonomyLevelDAO taxonomyLevelDao, QLicenseDAO qLicenseDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) { this(owner, defaultLocale, null, null, questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance); } public QTIImportProcessor(Identity owner, Locale defaultLocale, String importedFilename, File importedFile, QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, TaxonomyLevelDAO taxonomyLevelDao, QLicenseDAO qLicenseDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) { this.owner = owner; this.dbInstance = dbInstance; this.defaultLocale = defaultLocale; this.importedFilename = importedFilename; this.importedFile = importedFile; this.qLicenseDao = qLicenseDao; this.qItemTypeDao = qItemTypeDao; this.questionItemDao = questionItemDao; this.qEduContextDao = qEduContextDao; this.qpoolFileStorage = qpoolFileStorage; this.taxonomyLevelDao = taxonomyLevelDao; } public List<QuestionItem> process() { List<QuestionItem> qItems = new ArrayList<QuestionItem>(); try { List<DocInfos> docInfoList = getDocInfos(); if(docInfoList != null) { for(DocInfos docInfos:docInfoList) { List<QuestionItem> processdItems = process(docInfos); qItems.addAll(processdItems); dbInstance.commit(); } } } catch (IOException e) { log.error("", e); } return qItems; } private List<QuestionItem> process(DocInfos docInfos) { List<QuestionItem> qItems = new ArrayList<>(); if(docInfos.doc != null) { List<ItemInfos> itemInfos = getItemList(docInfos); for(ItemInfos itemInfo:itemInfos) { QuestionItemImpl qItem = processItem(docInfos, itemInfo, null); if(qItem != null) { processFiles(qItem, itemInfo, docInfos); qItem = questionItemDao.merge(qItem); qItems.add(qItem); } } } return qItems; } protected List<ItemInfos> getItemList(DocInfos doc) { Document document = doc.getDocument(); List<ItemInfos> itemElements = new ArrayList<ItemInfos>(); Element item = (Element)document.selectSingleNode("/questestinterop/item"); Element assessment = (Element)document.selectSingleNode("/questestinterop/assessment"); if(item != null) { ItemInfos itemInfos = new ItemInfos(item, true); Element comment = (Element)document.selectSingleNode("/questestinterop/qticomment"); String qtiComment = getText(comment); itemInfos.setComment(qtiComment); itemElements.add(itemInfos); } else if(assessment != null) { @SuppressWarnings("unchecked") List<Element> items = assessment.selectNodes("//item"); for(Element it:items) { itemElements.add(new ItemInfos(it, false)); } } return itemElements; } protected QuestionItemImpl processItem(DocInfos docInfos, ItemInfos itemInfos, ItemAndMetadata metadata) { Element itemEl = itemInfos.getItemEl(); String comment = itemInfos.getComment(); String originalFilename = null; if(itemInfos.isOriginalItem()) { originalFilename = docInfos.filename; } return processItem(itemEl, comment, originalFilename, null, null, docInfos, metadata); } protected QuestionItemImpl processItem(Element itemEl, String comment, String originalItemFilename, String editor, String editorVersion, DocInfos docInfos, ItemAndMetadata metadata) { //filename String filename; String ident = getAttributeValue(itemEl, "ident"); if(originalItemFilename != null) { filename = originalItemFilename; } else if(StringHelper.containsNonWhitespace(ident)) { filename = StringHelper.transformDisplayNameToFileSystemName(ident) + ".xml"; } else { filename = "item.xml"; } String dir = qpoolFileStorage.generateDir(); //title String title = getAttributeValue(itemEl, "title"); if(!StringHelper.containsNonWhitespace(title)) { title = ident; } if(!StringHelper.containsNonWhitespace(title)) { title = importedFilename; } QuestionItemImpl poolItem = questionItemDao.create(title, QTIConstants.QTI_12_FORMAT, dir, filename); //description poolItem.setDescription(comment); //language from default poolItem.setLanguage(defaultLocale.getLanguage()); //question type first boolean ooFormat = processItemQuestionType(poolItem, ident, itemEl); if(StringHelper.containsNonWhitespace(editor)) { poolItem.setEditor(editor); poolItem.setEditorVersion(editorVersion); } else if(ooFormat) { poolItem.setEditor("OpenOLAT"); } //if question type not found, can be overridden by the metadatas processItemMetadata(poolItem, itemEl); if(poolItem.getType() == null) { QItemType defType = qItemTypeDao.loadByType(QuestionType.UNKOWN.name()); poolItem.setType(defType); } if(docInfos != null) { processSidecarMetadata(poolItem, docInfos); } if(metadata != null) { processItemMetadata(poolItem, metadata); } questionItemDao.persist(owner, poolItem); return poolItem; } private void processItemMetadata(QuestionItemImpl poolItem, ItemAndMetadata metadata) { //non heuristic set of question type int questionType = metadata.getQuestionType(); if(questionType >= 0) { String typeStr; switch(questionType) { case Question.TYPE_MC: typeStr = QuestionType.MC.name(); break; case Question.TYPE_SC: typeStr = QuestionType.SC.name(); break; case Question.TYPE_FIB: typeStr = QuestionType.FIB.name(); break; case Question.TYPE_ESSAY: typeStr = QuestionType.ESSAY.name(); break; default: typeStr = null; } if(typeStr != null) { QItemType type = qItemTypeDao.loadByType(typeStr); if(type != null) { poolItem.setType(type); } } } String coverage = metadata.getCoverage(); if(StringHelper.containsNonWhitespace(coverage)) { poolItem.setCoverage(coverage); } String language = metadata.getLanguage(); if(StringHelper.containsNonWhitespace(language)) { poolItem.setLanguage(language); } String keywords = metadata.getKeywords(); if(StringHelper.containsNonWhitespace(keywords)) { poolItem.setKeywords(keywords); } String taxonomyPath = metadata.getTaxonomyPath(); if(StringHelper.containsNonWhitespace(taxonomyPath)) { QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); TaxonomyLevel taxonomyLevel = converter.toTaxonomy(taxonomyPath); poolItem.setTaxonomyLevel(taxonomyLevel); } String level = metadata.getLevel(); if(StringHelper.containsNonWhitespace(level)) { QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); QEducationalContext educationalContext = converter.toEducationalContext(level); poolItem.setEducationalContext(educationalContext); } String time = metadata.getTypicalLearningTime(); if(StringHelper.containsNonWhitespace(time)) { poolItem.setEducationalLearningTime(time); } String editor = metadata.getEditor(); if(StringHelper.containsNonWhitespace(editor)) { poolItem.setEditor(editor); } String editorVersion = metadata.getEditorVersion(); if(StringHelper.containsNonWhitespace(editorVersion)) { poolItem.setEditorVersion(editorVersion); } int numOfAnswerAlternatives = metadata.getNumOfAnswerAlternatives(); if(numOfAnswerAlternatives > 0) { poolItem.setNumOfAnswerAlternatives(numOfAnswerAlternatives); } poolItem.setDifficulty(metadata.getDifficulty()); poolItem.setDifferentiation(metadata.getDifferentiation()); poolItem.setStdevDifficulty(metadata.getStdevDifficulty()); String license = metadata.getLicense(); if(StringHelper.containsNonWhitespace(license)) { QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); QLicense qLicense = converter.toLicense(license); poolItem.setLicense(qLicense); } } private void processItemMetadata(QuestionItemImpl poolItem, Element itemEl) { @SuppressWarnings("unchecked") List<Element> qtiMetadataFieldList = itemEl.selectNodes("./itemmetadata/qtimetadata/qtimetadatafield"); for(Element qtiMetadataField:qtiMetadataFieldList) { Element labelEl = (Element)qtiMetadataField.selectSingleNode("./fieldlabel"); Element entryEl = (Element)qtiMetadataField.selectSingleNode("./fieldentry"); if(labelEl != null && entryEl != null) { processMetadataField(poolItem, labelEl, entryEl); } } } /** * <ul> * <li>qmd_computerscored</li> * <li>qmd_feedbackpermitted</li> * <li>qmd_hintspermitted</li> * <li>qmd_itemtype -> (check is made on the content of the item)</li> * <li>qmd_levelofdifficulty -> educational context</li> * <li>qmd_maximumscore</li> * <li>qmd_renderingtype</li> * <li>qmd_responsetype</li> * <li>qmd_scoringpermitted</li> * <li>qmd_solutionspermitted</li> * <li>qmd_status</li> * <li>qmd_timedependence</li> * <li>qmd_timelimit</li> * <li>qmd_toolvendor -> editor</li> * <li>qmd_topic</li> * <li>qmd_material</li> * <li>qmd_typeofsolution</li> * <li>qmd_weighting</li> * </ul> * @param poolItem * @param labelEl * @param entryEl */ private void processMetadataField(QuestionItemImpl poolItem, Element labelEl, Element entryEl) { String label = labelEl.getText(); String entry = entryEl.getText(); if(QTIConstants.META_LEVEL_OF_DIFFICULTY.equals(label)) { if(StringHelper.containsNonWhitespace(entry)) { QEducationalContext context = qEduContextDao.loadByLevel(entry); if(context == null) { context = qEduContextDao.create(entry, true); } poolItem.setEducationalContext(context); } } else if(QTIConstants.META_ITEM_TYPE.equals(label)) { if(poolItem.getType() == null && StringHelper.containsNonWhitespace(entry)) { //some heuristic String typeStr = entry; if(typeStr.equalsIgnoreCase("MCQ") || typeStr.equalsIgnoreCase("Multiple choice")) { typeStr = QuestionType.MC.name(); } else if(typeStr.equalsIgnoreCase("SCQ") || typeStr.equalsIgnoreCase("Single choice")) { typeStr = QuestionType.SC.name(); } else if(typeStr.equalsIgnoreCase("fill-in") || typeStr.equals("Fill-in-the-Blank") || typeStr.equalsIgnoreCase("Fill-in-Blank") || typeStr.equalsIgnoreCase("Fill In the Blank")) { typeStr = QuestionType.FIB.name(); } else if(typeStr.equalsIgnoreCase("Essay")) { typeStr = QuestionType.ESSAY.name(); } QItemType type = qItemTypeDao.loadByType(entry); if(type == null) { type = qItemTypeDao.create(entry, true); } poolItem.setType(type); } } else if(QTIConstants.META_TOOLVENDOR.equals(label)) { poolItem.setEditor(entry); } } /** * Save the item element in a <questestinterop> cartridge if needed * @param item * @param itemEl */ protected void processFiles(QuestionItemImpl item, ItemInfos itemInfos, DocInfos docInfos) { if(itemInfos.originalItem) { processItemFiles(item, docInfos); } else { //an assessment package processAssessmentFiles(item, itemInfos); } } protected void processAssessmentFiles(QuestionItemImpl item, ItemInfos itemInfos) { //a package with an item String dir = item.getDirectory(); String rootFilename = item.getRootFilename(); VFSContainer container = qpoolFileStorage.getContainer(dir); VFSLeaf endFile = container.createChildLeaf(rootFilename); //embed in <questestinterop> DocumentFactory df = DocumentFactory.getInstance(); Document itemDoc = df.createDocument(); Element questestinteropEl = df.createElement(QTIDocument.DOCUMENT_ROOT); itemDoc.setRootElement(questestinteropEl); Element deepClone = (Element)itemInfos.getItemEl().clone(); questestinteropEl.add(deepClone); //write try { OutputStream os = endFile.getOutputStream(false);; XMLWriter xw = new XMLWriter(os, new OutputFormat(" ", true)); xw.write(itemDoc.getRootElement()); xw.close(); os.close(); } catch (IOException e) { log.error("", e); } //there perhaps some other materials if(importedFilename.toLowerCase().endsWith(".zip")) { processAssessmentMaterials(deepClone, container); } } private void processAssessmentMaterials(Element itemEl, VFSContainer container) { List<String> materials = getMaterials(itemEl); try { InputStream in = new FileInputStream(importedFile); ZipInputStream zis = new ZipInputStream(in); ZipEntry entry; try { while ((entry = zis.getNextEntry()) != null) { String name = entry.getName(); if(materials.contains(name)) { VFSLeaf leaf = container.createChildLeaf(name); OutputStream out = leaf.getOutputStream(false); BufferedOutputStream bos = new BufferedOutputStream (out); FileUtils.cpio(new BufferedInputStream(zis), bos, "unzip:"+entry.getName()); bos.flush(); bos.close(); out.close(); } } } catch(Exception e) { log.error("", e); } finally { IOUtils.closeQuietly(zis); IOUtils.closeQuietly(in); } } catch (IOException e) { log.error("", e); } } @SuppressWarnings("unchecked") protected List<String> getMaterials(Element el) { List<String> materialPath = new ArrayList<String>(); //mattext List<Element> mattextList = el.selectNodes(".//mattext"); for(Element mat:mattextList) { Attribute texttypeAttr = mat.attribute("texttype"); if(texttypeAttr != null) { String texttype = texttypeAttr.getValue(); if("text/html".equals(texttype)) { String content = mat.getStringValue(); findMaterialInMatText(content, materialPath); } } } //matimage uri List<Element> matList = new ArrayList<Element>(); matList.addAll(el.selectNodes(".//matimage")); matList.addAll(el.selectNodes(".//mataudio")); matList.addAll(el.selectNodes(".//matvideo")); for(Element mat:matList) { Attribute uriAttr = mat.attribute("uri"); String uri = uriAttr.getValue(); materialPath.add(uri); } return materialPath; } /** * Parse the content and collect the images source * @param content * @param materialPath */ protected void findMaterialInMatText(String content, List<String> materialPath) { try { SAXParser parser = new SAXParser(); QTI12HtmlHandler contentHandler = new QTI12HtmlHandler(materialPath); parser.setContentHandler(contentHandler); parser.parse(new InputSource(new StringReader(content))); } catch (SAXException e) { log.error("", e); } catch (IOException e) { log.error("", e); } catch (Exception e) { log.error("", e); } } /** * Process the file of an item's package * @param item * @param itemInfos */ protected void processItemFiles(QuestionItemImpl item, DocInfos docInfos) { //a package with an item String dir = item.getDirectory(); String rootFilename = item.getRootFilename(); VFSContainer container = qpoolFileStorage.getContainer(dir); if(docInfos != null && docInfos.getRoot() != null) { try { Path destDir = ((LocalImpl)container).getBasefile().toPath(); //unzip to container Path path = docInfos.getRoot(); Files.walkFileTree(path, new CopyVisitor(path, destDir, new YesMatcher())); } catch (IOException e) { log.error("", e); } } else if(importedFilename.toLowerCase().endsWith(".zip")) { ZipUtil.unzipStrict(importedFile, container); } else { VFSLeaf endFile = container.createChildLeaf(rootFilename); OutputStream out = null; FileInputStream in = null; try { out = endFile.getOutputStream(false); in = new FileInputStream(importedFile); IOUtils.copy(in, out); } catch (IOException e) { log.error("", e); } finally { IOUtils.closeQuietly(out); IOUtils.closeQuietly(in); } } } private boolean processSidecarMetadata(QuestionItemImpl item, DocInfos docInfos) { InputStream metadataIn = null; try { Path path = docInfos.root; if(path != null && path.getFileName() != null) { Path metadata = path.resolve(path.getFileName().toString() + "_metadata.xml"); metadataIn = Files.newInputStream(metadata); SAXReader reader = new SAXReader(); Document document = reader.read(metadataIn); Element rootElement = document.getRootElement(); QTIMetadataConverter enricher = new QTIMetadataConverter(rootElement, qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); enricher.toQuestion(item); } return true; } catch(NoSuchFileException e) { //nothing to do return true; } catch (Exception e) { log.error("", e); return false; } finally { IOUtils.closeQuietly(metadataIn); } } private boolean processItemQuestionType(QuestionItemImpl poolItem, String ident, Element itemEl) { boolean openolatFormat = false; //question type: mc, sc... QuestionType type = null; //test with openolat ident if (ident != null && ident.startsWith(ItemParser.ITEM_PREFIX_SCQ)) { type = QuestionType.SC; openolatFormat = true; } else if(ident != null && ident.startsWith(ItemParser.ITEM_PREFIX_MCQ)) { type = QuestionType.MC; openolatFormat = true; } else if(ident != null && ident.startsWith(ItemParser.ITEM_PREFIX_FIB)) { type = QuestionType.FIB; openolatFormat = true; } else if(ident != null && ident.startsWith(ItemParser.ITEM_PREFIX_ESSAY)) { type = QuestionType.ESSAY; openolatFormat = true; } else if(ident != null && ident.startsWith(ItemParser.ITEM_PREFIX_KPRIM)) { type = QuestionType.KPRIM; openolatFormat = true; } else if(itemEl.selectNodes("//render_choice").size() == 1) { Element lidEl = (Element)itemEl.selectSingleNode("//response_lid"); String rcardinality = getAttributeValue(lidEl, "rcardinality"); if("Single".equals(rcardinality)) { type = QuestionType.SC; } else if("Multiple".equals(rcardinality)) { type = QuestionType.MC; } } else if(itemEl.selectNodes("//render_fib").size() == 1) { type = QuestionType.FIB; } if(type != null) { QItemType itemType = qItemTypeDao.loadByType(type.name()); poolItem.setType(itemType); } return openolatFormat; } private String getAttributeValue(Element el, String attrName) { if(el == null) return null; Attribute attr = el.attribute(attrName); return (attr == null) ? null : attr.getStringValue(); } private String getText(Element el) { if(el == null) return null; return el.getText(); } protected List<DocInfos> getDocInfos() throws IOException { List<DocInfos> doc; if(importedFilename.toLowerCase().endsWith(".zip")) { //doc = traverseZip(importedFile); doc = traverseZip_nio(importedFile); } else { doc = Collections.singletonList(traverseFile(importedFile)); } return doc; } private DocInfos traverseFile(File file) throws IOException { InputStream in = new FileInputStream(file); try { Document doc = readXml(in); if(doc != null) { DocInfos d = new DocInfos(); d.doc = doc; d.filename = file.getName(); return d; } return null; } catch(Exception e) { log.error("", e); return null; } finally { IOUtils.closeQuietly(in); } } /* private List<DocInfos> traverseZip(File file) throws IOException { InputStream in = new FileInputStream(file); ZipInputStream zis = new ZipInputStream(in); List<DocInfos> docInfos = new ArrayList<>(); ZipEntry entry; try { while ((entry = zis.getNextEntry()) != null) { String name = entry.getName(); if(name != null && name.toLowerCase().endsWith(".xml")) { Document doc = readXml(new ShieldInputStream(zis)); if(doc != null) { DocInfos d = new DocInfos(); d.doc = doc; d.filename = name; docInfos.add(d); } } } } catch(Exception e) { log.error("", e); } finally { IOUtils.closeQuietly(zis); IOUtils.closeQuietly(in); } return docInfos; } */ private List<DocInfos> traverseZip_nio(File file) throws IOException { List<DocInfos> docInfos = new ArrayList<>(); Path fPath = FileSystems.newFileSystem(file.toPath(), null).getPath("/"); if(fPath != null) { DocInfosVisitor visitor = new DocInfosVisitor(); Files.walkFileTree(fPath, visitor); List<Path> xmlFiles = visitor.getXmlFiles(); for(Path xmlFile:xmlFiles) { InputStream in = Files.newInputStream(xmlFile); Document doc = readXml(in); if(doc != null) { DocInfos d = new DocInfos(); d.setDocument(doc); d.setRoot(xmlFile.getParent()); d.setFilename(xmlFile.getFileName().toString()); docInfos.add(d); } } } return docInfos; } public static class DocInfosVisitor extends SimpleFileVisitor<Path> { private final List<Path> xmlFiles = new ArrayList<>(); public List<Path> getXmlFiles() { return xmlFiles; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String name = file.getFileName().toString(); if(name != null && name.toLowerCase().endsWith(".xml")) { xmlFiles.add(file); } return FileVisitResult.CONTINUE; } } private Document readXml(InputStream in) { Document doc = null; try { XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); doc = xmlParser.parse(in, false); return doc; } catch (Exception e) { return null; } } public static class ItemInfos { private String comment; private final Element itemEl; private final boolean originalItem; public ItemInfos(Element itemEl, boolean originalItem) { this.itemEl = itemEl; this.originalItem = originalItem; } public Element getItemEl() { return itemEl; } public boolean isOriginalItem() { return originalItem; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } public static class DocInfos { private Document doc; private String filename; private Path root; private Path metadata; private String qtiComment; public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } public Document getDocument() { return doc; } public void setDocument(Document doc) { this.doc = doc; } public Path getMetadata() { return metadata; } public void setMetadata(Path metadata) { this.metadata = metadata; } public Path getRoot() { return root; } public void setRoot(Path root) { this.root = root; } public String getQtiComment() { return qtiComment; } public void setQtiComment(String qtiComment) { this.qtiComment = qtiComment; } } }