package de.uni_passau.fim.infosun.prophet.util.qTree.handlers; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; import java.util.Objects; import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import com.thoughtworks.xstream.XStream; import de.uni_passau.fim.infosun.prophet.util.qTree.Attribute; import de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode; import nu.xom.Builder; import nu.xom.Document; import nu.xom.Element; import nu.xom.Elements; import nu.xom.ParsingException; import org.xml.sax.SAXException; /** * Handles XML operations for the <code>QTree</code>. This includes: * <ul> * <li>Checking a file against the XSD Schema for new or legacy XML files</li> * <li>Reading files and extracting the root node (and all the children/attributes)</li> * <li>Saving the root node and all children/attributes to a file</li> * </ul> */ public final class QTreeXMLHandler extends QTreeFormatHandler { private static final String xmlProlog = String.format("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>%n"); private static final XStream saveLoadStream; private static final XStream answerStream; private static Validator legacyValidator; private static Validator validator; static { saveLoadStream = new XStream(); // QTreeNode XML adjustments String simpleName = QTreeNode.class.getSimpleName(); saveLoadStream.alias(simpleName, QTreeNode.class); saveLoadStream.useAttributeFor(QTreeNode.class, "type"); saveLoadStream.useAttributeFor(QTreeNode.class, "name"); saveLoadStream.addImplicitMap(QTreeNode.class, "attributes", "attribute", Attribute.class, "key"); saveLoadStream.addImplicitCollection(QTreeNode.class, "children", simpleName, QTreeNode.class); saveLoadStream.omitField(QTreeNode.class, "answers"); saveLoadStream.omitField(QTreeNode.class, "answerTime"); // Attribute XML adjustments saveLoadStream.alias("Attribute", Attribute.class); saveLoadStream.useAttributeFor(Attribute.class, "key"); saveLoadStream.useAttributeFor(Attribute.class, "value"); saveLoadStream.addImplicitMap(Attribute.class, "subAttributes", "attribute", Attribute.class, "key"); answerStream = new XStream(); answerStream.alias(simpleName, QTreeNode.class); answerStream.useAttributeFor(QTreeNode.class, "type"); answerStream.useAttributeFor(QTreeNode.class, "name"); answerStream.omitField(QTreeNode.class, "html"); answerStream.omitField(QTreeNode.class, "attributes"); answerStream.omitField(QTreeNode.class, "parent"); answerStream.addImplicitCollection(QTreeNode.class, "children", simpleName, QTreeNode.class); answerStream.useAttributeFor(QTreeNode.class, "answerTime"); SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); try { Schema legacySchema = factory.newSchema(QTreeXMLHandler.class.getResource("LegacyExperiment.xsd")); legacyValidator = legacySchema.newValidator(); } catch (SAXException e) { System.err.println("Could not create a Validator for the legacy XML format."); } try { Schema schema = factory.newSchema(QTreeXMLHandler.class.getResource("Experiment.xsd")); validator = schema.newValidator(); } catch (SAXException e) { System.err.println("Could not create a Validator for the XML format."); } } /** * Utility class. */ private QTreeXMLHandler() {} /** * Saves the given <code>QTreeNode</code> (and thereby the whole tree under it) to the given file using * the answers.xml format. This will overwrite <code>saveFile</code>. * Neither <code>root</code> nor <code>saveFile</code> may be <code>null</code>. * * @param root * the root of the tree to save * @param saveFile * the file to save the tree to * * @throws IOException * if the file can not be written to */ public static void saveAnswerXML(QTreeNode root, File saveFile) throws IOException { Objects.requireNonNull(root, "root must not be null!"); Objects.requireNonNull(saveFile, "saveFile must not be null!"); CharsetEncoder utf8encoder = StandardCharsets.UTF_8.newEncoder(); checkParent(saveFile); try (Writer writer = new OutputStreamWriter(new FileOutputStream(saveFile), utf8encoder)) { writer.write(xmlProlog); answerStream.toXML(root, writer); } } /** * Loads the <code>QTreeNode</code> from an XML file. * If there is an error de-serialising the <code>xmlFile</code> <code>null</code> will be returned. * * @param xmlFile * the XML file containing a <code>QTreeNode</code> * * @return the <code>QTreeNode</code> contained in the file or <code>null</code> */ public static QTreeNode loadExperimentXML(File xmlFile) { Objects.requireNonNull(xmlFile, "xmlFile must not be null!"); CharsetDecoder utf8decoder = StandardCharsets.UTF_8.newDecoder(); QTreeNode node; if (isValidXML(validator, "CurrentValidator" , xmlFile)) { try (Reader reader = new InputStreamReader(new FileInputStream(xmlFile), utf8decoder)) { node = (QTreeNode) saveLoadStream.fromXML(reader); } catch (IOException e) { System.err.println("There was an error deserializing " + xmlFile.getName()); System.err.println(e.getMessage()); node = null; } } else if (isValidXML(legacyValidator, "LegacyValidator" , xmlFile)) { node = loadOldExperimentXML(xmlFile); } else { node = null; } return node; } /** * Checks whether the given XML file conforms to the <code>Validator</code>s schema. * This method will return <code>false</code> if <code>validator</code> or * <code>xmlFile</code> is <code>null</code>. The <code>validatorID</code> will be used to identify the * <code>Validator</code> when the cause of the validation failure is printed to <code>System.err</code>. * * @param validator * the <code>Validator</code> to use for validation * @param validatorID * a <code>String</code> identifying the <code>Validator</code> * @param xmlFile * the XML file to validate * * @return true iff the <code>Validator</code> accepts the <code>xmlFile</code> */ private static boolean isValidXML(Validator validator, String validatorID, File xmlFile) { CharsetDecoder utf8decoder = StandardCharsets.UTF_8.newDecoder(); if (validator == null || xmlFile == null) { System.err.println("Validator or XML file is null. Considering the file invalid."); return false; } try (Reader reader = new InputStreamReader(new FileInputStream(xmlFile), utf8decoder)) { validator.validate(new StreamSource(reader)); } catch (SAXException | IOException e) { System.err.println("The file " + xmlFile.getName() + " is invalid according to " + validatorID); System.err.println("Cause: " + e.getMessage()); return false; } return true; } /** * Saves the given <code>QTreeNode</code> (and thereby the whole tree under it) to the given file. * This will overwrite <code>saveFile</code>. * Neither <code>root</code> nor <code>saveFile</code> may be <code>null</code>. * * @param root * the root of the tree to save * @param saveFile * the file to save the tree to * * @throws IOException * if the file can not be written to */ public static void saveExperimentXML(QTreeNode root, File saveFile) throws IOException { Objects.requireNonNull(root, "root must not be null!"); Objects.requireNonNull(saveFile, "saveFile must not be null!"); CharsetEncoder utf8encoder = StandardCharsets.UTF_8.newEncoder(); checkParent(saveFile); try (Writer writer = new OutputStreamWriter(new FileOutputStream(saveFile), utf8encoder)) { writer.write(xmlProlog); saveLoadStream.toXML(root, writer); } } // Code to handle the old-style XML format below. private static final String TYPE_EXPERIMENT = "experiment"; private static final String TYPE_CATEGORY = "category"; private static final String TYPE_QUESTION = "question"; private static final String TYPE_ATTRIBUTE = "attribute"; private static final String TYPE_ATTRIBUTES = "attributes"; private static final String TYPE_CHILDREN = "children"; private static final String ATTRIBUTE_NAME = "name"; private static final String ATTRIBUTE_VALUE = "value"; /** * Loads the <code>QTreeNode</code> from an old-style XML file. * If there is an error de-serialising the <code>xmlFile</code> <code>null</code> will be returned. * * @param xmlFile * the old-style XML file containing a question tree * * @return the <code>QTreeNode</code> resulting from converting the file or <code>null</code> */ private static QTreeNode loadOldExperimentXML(File xmlFile) { Objects.requireNonNull(xmlFile, "xmlFile must not be null!"); CharsetDecoder utf8decoder = StandardCharsets.UTF_8.newDecoder(); Builder parser = new Builder(); Document document; try (Reader reader = new InputStreamReader(new FileInputStream(xmlFile), utf8decoder)) { document = parser.build(reader); } catch (ParsingException | IOException e) { System.err.println("Could not parse " + xmlFile.getName()); System.err.println(e.getMessage()); return null; } return loadOldTreeNode(document.getRootElement(), null); } /** * Loads a <code>QTreeNode</code> from the given <code>Element</code>. * * @param element * the <code>Element</code> containing information to be de-serialised to a <code>QTreeNode</code> * @param parent * the parent of the resulting <code>QTreeNode</code> * * @return the resulting <code>QTreeNode</code> */ private static QTreeNode loadOldTreeNode(Element element, QTreeNode parent) { QTreeNode node; QTreeNode.Type type; String name; String html; Element attributes; Element children; Elements attributeNodes; switch (element.getLocalName()) { case TYPE_EXPERIMENT: type = QTreeNode.Type.EXPERIMENT; break; case TYPE_CATEGORY: type = QTreeNode.Type.CATEGORY; break; case TYPE_QUESTION: type = QTreeNode.Type.QUESTION; break; default: System.err.println("Unknown node type: " + element.getLocalName()); type = null; } name = element.getAttributeValue(ATTRIBUTE_NAME); html = element.getAttributeValue(ATTRIBUTE_VALUE); node = new QTreeNode(parent, type, name); node.setHtml(html); attributes = element.getFirstChildElement(TYPE_ATTRIBUTES); children = element.getFirstChildElement(TYPE_CHILDREN); if (attributes != null) { attributeNodes = attributes.getChildElements(TYPE_ATTRIBUTE); for (int i = 0; i < attributeNodes.size(); i++) { node.addAttribute(loadOldAttributeNode(attributeNodes.get(i))); } } if (children != null) { Elements childElements = null; switch (node.getType()) { case EXPERIMENT: childElements = children.getChildElements(TYPE_CATEGORY); break; case CATEGORY: childElements = children.getChildElements(TYPE_QUESTION); break; default: System.err.println("No rule to get the children of a node with type " + node.getType()); } if (childElements != null) { for (int i = 0; i < childElements.size(); i++) { node.addChild(loadOldTreeNode(childElements.get(i), node)); } } } return node; } /** * Loads an <code>Attribute</code> from the given <code>Element</code>. * * @param element * the <code>Element</code> containing information to be de-serialised to an <code>Attribute</code> * * @return the resulting <code>Attribute</code> */ private static Attribute loadOldAttributeNode(Element element) { Attribute attribute; Element attributes; Elements attributeNodes; String key = element.getAttributeValue(ATTRIBUTE_NAME); String value = element.getAttributeValue(ATTRIBUTE_VALUE); attribute = new Attribute(key, value); attributes = element.getFirstChildElement(TYPE_ATTRIBUTES); if (attributes != null) { attributeNodes = attributes.getChildElements(TYPE_ATTRIBUTE); for (int i = 0; i < attributeNodes.size(); i++) { attribute.addSubAttribute(loadOldAttributeNode(attributeNodes.get(i))); } } return attribute; } }