diff --git a/src/main/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksHandler.java b/src/main/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..3c9ddc55237979ae0ba7f87f91a55340fcb6b25e --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksHandler.java @@ -0,0 +1,197 @@ +/** + * <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.model.xml; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.ext.DefaultHandler2; + +/** + * It converts Onyx final to qtiWorks. It fix:<br> + * <ul> + * <li>imsmanifest: type from "imsqti_assessment_xmlv2p1" to "imsqti_test_xmlv2p1"</li> + * <li>assessmentTest: surround rubricBlock's text only content with <p></li> + * <li>assesementItem: surround itemBody's text only with <p></li> + * <li>assesementItem: strip html code from <prompt> + * </ul> + * + * + * Initial date: 25.06.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class OnyxToQtiWorksHandler extends DefaultHandler2 { + + private final XMLStreamWriter xtw; + + private boolean itemBody = false; + private StringBuilder itemCharacterBuffer; + + private boolean rubricBlock = false; + private StringBuilder rubricCharacterBuffer; + + private boolean prompt = false; + + + public OnyxToQtiWorksHandler(XMLStreamWriter xtw) { + this.xtw = xtw; + } + + @Override + public void startDocument() throws SAXException { + try{ + xtw.writeStartDocument("utf-8", "1.0"); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void comment(char[] ch, int start, int length) + throws SAXException { + try{ + String comment = new String(ch, start, length); + xtw.writeComment(comment); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + try { + if(prompt) { + return; + } + + if(itemBody) { + String characters = itemCharacterBuffer.toString().trim(); + if(characters.length() > 0) { + xtw.writeStartElement("p"); + xtw.writeCharacters(characters); + xtw.writeEndElement(); + } + itemBody = false; + itemCharacterBuffer = null; + } else if(rubricBlock) { + String characters = rubricCharacterBuffer.toString().trim(); + if(characters.length() > 0) { + xtw.writeStartElement("p"); + xtw.writeCharacters(characters); + xtw.writeEndElement(); + } + + rubricBlock = false; + rubricCharacterBuffer = null; + } + + xtw.writeStartElement(qName); + if("imscp:resource".equals(qName)) { + int numOfAttributes = attributes.getLength(); + for(int i=0;i<numOfAttributes; i++) { + String attrQName = attributes.getQName(i); + String attrValue = attributes.getValue(i); + if("type".equals(attrQName) && "imsqti_assessment_xmlv2p1".equals(attrValue)) { + xtw.writeAttribute(attrQName, "imsqti_test_xmlv2p1"); + } else { + xtw.writeAttribute(attrQName, attrValue); + } + } + } else { + int numOfAttributes = attributes.getLength(); + for(int i=0;i<numOfAttributes; i++) { + String attrQName = attributes.getQName(i); + String attrValue = attributes.getValue(i); + xtw.writeAttribute(attrQName, attrValue); + } + } + + if("itemBody".equals(qName)) { + itemBody = true; + itemCharacterBuffer = new StringBuilder(); + } else if("rubricBlock".equals(qName)) { + rubricBlock = true; + rubricCharacterBuffer = new StringBuilder(); + } else if("prompt".equals(qName)) { + prompt = true; + } + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if(itemBody) { + itemCharacterBuffer.append(ch, start, length); + } else if(rubricBlock) { + rubricCharacterBuffer.append(ch, start, length); + } else { + try { + xtw.writeCharacters(ch, start, length); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + try { + if(rubricBlock) { + String characters = rubricCharacterBuffer.toString().trim(); + if(characters.length() > 0) { + xtw.writeStartElement("p"); + xtw.writeCharacters(characters); + xtw.writeEndElement(); + } + + rubricBlock = false; + rubricCharacterBuffer = null; + } else if(prompt) { + if(!"prompt".equals(qName)) { + return;//only print characters + } else { + prompt = false; + } + } + xtw.writeEndElement(); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void endDocument() throws SAXException { + try { + xtw.writeEndDocument(); + xtw.flush(); + xtw.close(); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java b/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java index 2f0aefba518f2ef25eb58a8d4030f12c4f6f18f4..c62f139cbfb52e892277d5bdb3fea635a07f97aa 100644 --- a/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java +++ b/src/main/java/org/olat/ims/qti21/repository/handlers/QTI21AssessmentTestHandler.java @@ -20,12 +20,24 @@ 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.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.Locale; -import java.util.UUID; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.persistence.DBFactory; @@ -40,6 +52,7 @@ import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; +import org.olat.core.util.PathUtils.YesMatcher; import org.olat.core.util.coordinate.LockResult; import org.olat.course.assessment.AssessmentMode; import org.olat.fileresource.FileResourceManager; @@ -47,16 +60,11 @@ import org.olat.fileresource.types.FileResource; import org.olat.fileresource.types.ImsQTI21Resource; import org.olat.fileresource.types.ResourceEvaluation; import org.olat.ims.qti21.model.xml.ManifestPackage; +import org.olat.ims.qti21.model.xml.OnyxToQtiWorksHandler; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.InMemoryOutcomesListener; import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController; -import org.olat.imscp.xml.manifest.FileType; -import org.olat.imscp.xml.manifest.ManifestMetadataType; import org.olat.imscp.xml.manifest.ManifestType; -import org.olat.imscp.xml.manifest.ObjectFactory; -import org.olat.imscp.xml.manifest.OrganizationsType; -import org.olat.imscp.xml.manifest.ResourceType; -import org.olat.imscp.xml.manifest.ResourcesType; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryService; import org.olat.repository.handlers.EditionSupport; @@ -145,13 +153,89 @@ public class QTI21AssessmentTestHandler extends FileHandler { OLATResource resource = OLATResourceManager.getInstance().createAndPersistOLATResourceInstance(ores); File fResourceFileroot = FileResourceManager.getInstance().getFileResourceRootImpl(resource).getBasefile(); File zipDir = new File(fResourceFileroot, FileResourceManager.ZIPDIR); - FileResource.copyResource(file, filename, zipDir); + copyResource(file, filename, zipDir); RepositoryEntry re = CoreSpringFactory.getImpl(RepositoryService.class) .create(initialAuthor, null, "", displayname, description, resource, RepositoryEntry.ACC_OWNERS); DBFactory.getInstance().commit(); return re; } + + private boolean copyResource(File file, String filename, File targetDirectory) { + try { + Path path = FileResource.getResource(file, filename); + if(path == null) { + return false; + } + + Path destDir = targetDirectory.toPath(); + Files.walkFileTree(path, new CopyAndConvertVisitor(path, destDir, new YesMatcher())); + return true; + } catch (IOException e) { + log.error("", e); + return false; + } + } + + private static class CopyAndConvertVisitor extends SimpleFileVisitor<Path> { + + private final Path source; + private final Path destDir; + private final PathMatcher filter; + + public CopyAndConvertVisitor(Path source, Path destDir, PathMatcher filter) { + this.source = source; + this.destDir = destDir; + this.filter = filter; + } + + @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 != null && filename.endsWith("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 + * @param inputFile + * @param outputFile + */ + private void convertXmlFile(Path inputFile, Path outputFile) { + try(InputStream in = Files.newInputStream(inputFile); + Writer out = Files.newBufferedWriter(outputFile, Charset.forName("UTF-8"))) { + XMLOutputFactory xof = XMLOutputFactory.newInstance(); + XMLStreamWriter xtw = xof.createXMLStreamWriter(out); + + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + OnyxToQtiWorksHandler myHandler = new OnyxToQtiWorksHandler(xtw); + saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", myHandler); + saxParser.parse(in, myHandler); + } catch(Exception e) { + log.error("", e); + } + } + } @Override public MediaResource getAsMediaResource(OLATResourceable res, boolean backwardsCompatible) { diff --git a/src/main/java/org/olat/ims/qti21/restapi/MathWebService.java b/src/main/java/org/olat/ims/qti21/restapi/MathWebService.java index 667ec82039349f24989a03dcf005251a20405135..a1432cb8725c998dab18a8778fa9fda904ef4086 100644 --- a/src/main/java/org/olat/ims/qti21/restapi/MathWebService.java +++ b/src/main/java/org/olat/ims/qti21/restapi/MathWebService.java @@ -1,3 +1,22 @@ +/** + * <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.restapi; import java.util.Date; @@ -16,6 +35,14 @@ import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.XsltStylesheetCache; import uk.ac.ed.ph.qtiworks.mathassess.XsltStylesheetCacheAdapter; import uk.ac.ed.ph.qtiworks.mathassess.glue.AsciiMathHelper; +/** + * + * + * + * Initial date: 25.06.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ @Path("math") public class MathWebService { @@ -28,7 +55,6 @@ public class MathWebService { @Path("verifyAsciiMath") @Produces({MediaType.APPLICATION_JSON}) public Response verifyAsciiMath(@FormParam("input") String asciiMathInput) { - XsltStylesheetCache stylesheetCache = new SimpleXsltStylesheetCache(); AsciiMathHelper asciiMathHelper = new AsciiMathHelper(new XsltStylesheetCacheAdapter(stylesheetCache)); Map<String, String> upConvertedAsciiMathInput = asciiMathHelper.upConvertAsciiMathInput(asciiMathInput); diff --git a/src/test/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksTest.java b/src/test/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d24ae1513e9b7518934d971bc4c116fde32a69d1 --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/OnyxToQtiWorksTest.java @@ -0,0 +1,111 @@ +/** + * <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.model.xml; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; + +import org.junit.Test; +import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.provision.BadResourceException; +import uk.ac.ed.ph.jqtiplus.reading.AssessmentObjectXmlLoader; +import uk.ac.ed.ph.jqtiplus.reading.QtiModelBuildingError; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlInterpretationException; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader; +import uk.ac.ed.ph.jqtiplus.validation.ItemValidationResult; +import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; + +/** + * Only to play with + * + * + * Initial date: 25.06.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class OnyxToQtiWorksTest { + + @Test + public void fixItem() { + File itemFile = new File("/Users/srosse/Desktop/QTI/Beispielaufgaben_Onyx_3_6/Auswahlaufgabe_1509468352.xml"); + File rubricFile = new File("/Users/srosse/Desktop/QTI/fxtest_Onyx_3_1_1/Fxtest_684549665.xml"); + File manifestFile = new File("/Users/srosse/Desktop/QTI/Beispielaufgaben_Onyx_3_6/imsmanifest.xml"); + File promptFile = new File("/Users/srosse/Desktop/QTI/fxtest_Onyx_3_1_1/Task_1597435347.xml"); + + File xmlFile = promptFile; + + SAXParserFactory factory = SAXParserFactory.newInstance(); + File outFile = new File("/Users/srosse/Desktop/QTI/text.xml"); + if(outFile.exists()) { + outFile.delete(); + outFile = new File("/Users/srosse/Desktop/QTI/text.xml"); + } + + try (OutputStream byteOut = new FileOutputStream(outFile); + OutputStreamWriter out = new OutputStreamWriter(byteOut, "UTF8")) { + // Parse the input + XMLOutputFactory xof = XMLOutputFactory.newInstance(); + XMLStreamWriter xtw = xof.createXMLStreamWriter(out); + + SAXParser saxParser = factory.newSAXParser(); + OnyxToQtiWorksHandler myHandler = new OnyxToQtiWorksHandler(xtw); + + saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", myHandler); + saxParser.parse(xmlFile, myHandler); + + out.flush(); + byteOut.flush(); + + + QtiXmlReader qtiXmlReader = new QtiXmlReader(new JqtiExtensionManager()); + ResourceLocator fileResourceLocator = new PathResourceLocator(outFile.toPath()); + AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator); + + ItemValidationResult itemResult = assessmentObjectXmlLoader.loadResolveAndValidateItem(outFile.toURI()); + System.out.println("Has errors: " + (itemResult.getModelValidationErrors().size() > 0)); + + BadResourceException e = itemResult.getResolvedAssessmentItem().getItemLookup().getBadResourceException(); + if(e instanceof QtiXmlInterpretationException) { + QtiXmlInterpretationException qe = (QtiXmlInterpretationException)e; + for(QtiModelBuildingError error :qe.getQtiModelBuildingErrors()) { + String localName = error.getElementLocalName(); + String msg = error.getException().getMessage(); + int lineNumber = error.getElementLocation().getLineNumber(); + System.out.println(lineNumber + " :: " + localName + " :: " + msg); + } + } + //System.out.println("Actual validation result: " + ObjectDumper.dumpObject(itemResult, DumpMode.DEEP)); + + + } catch (Throwable t) { + t.printStackTrace(); + } + + } +} diff --git a/src/test/java/org/olat/ims/qti21/model/xml/ValidationTest.java b/src/test/java/org/olat/ims/qti21/model/xml/ValidationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..aca25ed91681ebb55ac09d6544d8f55907dd9b2a --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/ValidationTest.java @@ -0,0 +1,71 @@ +/** + * <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.model.xml; + +import java.io.File; + +import org.junit.Test; +import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.provision.BadResourceException; +import uk.ac.ed.ph.jqtiplus.reading.AssessmentObjectXmlLoader; +import uk.ac.ed.ph.jqtiplus.reading.QtiModelBuildingError; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlInterpretationException; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader; +import uk.ac.ed.ph.jqtiplus.validation.ItemValidationResult; +import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; + +/** + * Only to play with + * + * Initial date: 25.06.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ValidationTest { + + @Test + public void validateItem() { + + File itemFile = new File("/Users/srosse/Desktop/QTI/fxtest_Onyx_3_1_1/Task_1597435347.xml"); + + QtiXmlReader qtiXmlReader = new QtiXmlReader(new JqtiExtensionManager()); + ResourceLocator fileResourceLocator = new PathResourceLocator(itemFile.toPath()); + AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator); + + ItemValidationResult itemResult = assessmentObjectXmlLoader.loadResolveAndValidateItem(itemFile.toURI()); + System.out.println("Has errors: " + (itemResult.getModelValidationErrors().size() > 0)); + + BadResourceException e = itemResult.getResolvedAssessmentItem().getItemLookup().getBadResourceException(); + if(e instanceof QtiXmlInterpretationException) { + QtiXmlInterpretationException qe = (QtiXmlInterpretationException)e; + for(QtiModelBuildingError error :qe.getQtiModelBuildingErrors()) { + String localName = error.getElementLocalName(); + String msg = error.getException().getMessage(); + int lineNumber = error.getElementLocation().getLineNumber(); + System.out.println(lineNumber + " :: " + localName + " :: " + msg); + } + } + //System.out.println("Actual validation result: " + ObjectDumper.dumpObject(itemResult, DumpMode.DEEP)); + + } + +}