/** * <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.repository.handlers; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.UUID; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.WebappHelper; import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; import org.olat.ims.qti21.model.xml.BadRessourceHelper; import org.olat.ims.qti21.model.xml.Onyx38ToQtiWorksHandler; import org.olat.ims.qti21.model.xml.OnyxToQtiWorksHandler; import org.olat.ims.qti21.model.xml.QTI21ExplorerHandler; import org.olat.ims.qti21.model.xml.QTI21Infos; import org.olat.ims.qti21.model.xml.QTI21Infos.InputType; import org.xml.sax.ext.DefaultHandler2; import org.xml.sax.helpers.DefaultHandler; import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; import uk.ac.ed.ph.jqtiplus.node.RootNode; import uk.ac.ed.ph.jqtiplus.provision.BadResourceException; import uk.ac.ed.ph.jqtiplus.reading.AssessmentObjectXmlLoader; import uk.ac.ed.ph.jqtiplus.reading.QtiXmlInterpretationException; import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest; import uk.ac.ed.ph.jqtiplus.validation.ItemValidationResult; import uk.ac.ed.ph.jqtiplus.validation.TestValidationResult; import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; /** * Copy and eventually convert the XML files. Converter available: * <ul> * <li>Onyx 3.x to QtiWorks fix text directly under itemBody, remove font tags</li> * <li>Onyx Web to QtiWorks fix the rubric issue (difference between the QTI model and the XSD)</li> * </ul> * It does a validation (as indication) but check if it can load the assessmentItem/assessmentTest * and open it. * * Initial date: 1 févr. 2017<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ class CopyAndConvertVisitor extends SimpleFileVisitor<Path> { private static final OLog log = Tracing.createLoggerFor(CopyAndConvertVisitor.class); private final Path source; private final Path destDir; private final PathMatcher filter; private QTI21Infos infos; public CopyAndConvertVisitor(Path source, Path destDir, QTI21Infos infos, PathMatcher filter) { this.source = source; this.destDir = destDir; this.filter = filter; this.infos = infos; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path relativeFile = source.relativize(file); final Path destFile = Paths.get(destDir.toString(), relativeFile.toString()); if(filter.matches(file)) { String filename = file.getFileName().toString(); if(filename.startsWith(".")) { //ignore } else if(filename != null && filename.endsWith("xml") && !filename.equals("imsmanifest.xml")) { convertXmlFile(file, destFile); } else { Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING); } } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path relativeDir = source.relativize(dir); final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString()); if(Files.notExists(dirToCreate)){ Files.createDirectory(dirToCreate); } return FileVisitResult.CONTINUE; } /** * Convert the XML files, assessmentItem or assessmentTest * * @param inputFile * @param outputFile */ public boolean convertXmlFile(Path inputFile, Path outputFile) { try { boolean validated = true; QTI21Infos fileInfos = scanFile(inputFile); if(onyx38Family(fileInfos)) { validated = convertXmlFile(inputFile, outputFile, fileInfos.getType(), new HandlerProvider() { @Override public DefaultHandler2 create(XMLStreamWriter xtw) { return new Onyx38ToQtiWorksHandler(xtw); } }); } else if(onyxWebFamily(fileInfos)) { validated = convertXmlFile(inputFile, outputFile, infos.getType(), new HandlerProvider() { @Override public DefaultHandler2 create(XMLStreamWriter xtw) { return new OnyxToQtiWorksHandler(xtw, infos); } }); } else { Files.copy(inputFile, outputFile, StandardCopyOption.REPLACE_EXISTING); } return validated; } catch (IOException | FactoryConfigurationError e) { log.error("", e); return false; } } private boolean onyx38Family(QTI21Infos fileInfos) { if(fileInfos == null || fileInfos.getEditor() == null) return false; String version = infos.getVersion(); return "Onyx Editor".equals(infos.getEditor()) && version != null && (version.startsWith("2.") || version.startsWith("3.")); } private boolean onyxWebFamily(QTI21Infos fileInfos) { if(fileInfos == null || fileInfos.getEditor() == null) return false; return "ONYX Editor".equals(infos.getEditor()); } private QTI21Infos scanFile(Path inputFile) { QTI21ExplorerHandler infosHandler = new QTI21ExplorerHandler(); try(InputStream in = Files.newInputStream(inputFile)) { SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", infosHandler); saxParser.parse(in, infosHandler); } catch(Exception e1) { log.error("", e1); } return infosHandler.getInfos(); } private boolean convertXmlFile(Path inputFile, Path outputFile, InputType type, HandlerProvider provider) { File tmpFile = new File(WebappHelper.getTmpDir(), UUID.randomUUID() + ".xml"); try(InputStream in = Files.newInputStream(inputFile); Writer out = Files.newBufferedWriter(tmpFile.toPath(), Charset.forName("UTF-8"))) { XMLOutputFactory xof = XMLOutputFactory.newInstance(); XMLStreamWriter xtw = xof.createXMLStreamWriter(out); SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); DefaultHandler myHandler = provider.create(xtw); saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", myHandler); saxParser.parse(in, myHandler); boolean valid = validate(tmpFile.toPath(), type, true); if(valid) { Files.copy(tmpFile.toPath(), outputFile, StandardCopyOption.REPLACE_EXISTING); } return valid; } catch(Exception e1) { log.error("", e1); return false; } finally { if(tmpFile.exists()) { tmpFile.delete(); } } } private boolean validate(Path inputFile, InputType type, boolean verbose) { try { QtiXmlReader qtiXmlReader = new QtiXmlReader(new JqtiExtensionManager()); ResourceLocator fileResourceLocator = new PathResourceLocator(inputFile.getParent()); AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator); RootNode rootNode = null; BadResourceException e = null; URI uri = new URI("zip", inputFile.getFileName().toString(), null); if(type == InputType.assessmentItem) { ItemValidationResult itemResult = assessmentObjectXmlLoader.loadResolveAndValidateItem(uri); e = itemResult.getResolvedAssessmentItem().getItemLookup().getBadResourceException(); ResolvedAssessmentItem resolvedAssessmentItem = assessmentObjectXmlLoader.loadAndResolveAssessmentItem(uri); rootNode = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); } else if(type == InputType.assessmentTest) { TestValidationResult testResult = assessmentObjectXmlLoader.loadResolveAndValidateTest(uri); e = testResult.getResolvedAssessmentTest().getTestLookup().getBadResourceException(); ResolvedAssessmentTest resolvedAssessmentTest = assessmentObjectXmlLoader.loadAndResolveAssessmentTest(uri); rootNode = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful(); } if(e != null && verbose) { StringBuilder err = new StringBuilder(); BadRessourceHelper.extractMessage(e, err); log.warn(err.toString()); } return (rootNode != null) && (e == null || (e instanceof QtiXmlInterpretationException && ((QtiXmlInterpretationException)e).getXmlParseResult().getFatalErrors().isEmpty())); } catch (URISyntaxException e) { log.error("", e); return false; } } public interface HandlerProvider { public DefaultHandler2 create(XMLStreamWriter xtw); } }