package org.erikaredmark.monkeyshines.editor.persist;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.erikaredmark.monkeyshines.Conveyer.Rotation;
import org.erikaredmark.monkeyshines.editor.exception.BadEditorPersistantFormatException;
import org.erikaredmark.monkeyshines.editor.model.Template;
import org.erikaredmark.monkeyshines.editor.model.Template.TemplateTile;
import org.erikaredmark.monkeyshines.tiles.CollapsibleTile;
import org.erikaredmark.monkeyshines.tiles.CommonTile;
import org.erikaredmark.monkeyshines.tiles.ConveyerTile;
import org.erikaredmark.monkeyshines.tiles.HazardTile;
import org.erikaredmark.monkeyshines.tiles.TileType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
/**
*
* Writes out the templates in memory to an xml stream. The xml file will be created if one does not exist.
*
* @author Erika Redmark
*
*/
public final class TemplateXmlWriter {
private TemplateXmlWriter() { }
/**
*
* Writes out all templates for the given world name to the given output file, creating it if it does not
* already exist.
* <p/>
* Writing out templates for a world will REPLACE the existing templates defined for that world (so the template
* list that is extracted originally must be maintained). This will NOT replace templates from OTHER worlds, however.
*
* @param xmlFile
* the xmlFile to write to
*
* @param worldName
* name of the world that this templates will be saved as. If templates already exist for a given world, all templates
* for that given world will be overwritten
*
* @param templates
* a list of templates to write to the xml file under the given world name
*
*/
public static void writeOutTemplatesForWorld(Path xmlFile, String worldName, List<Template> templates) throws BadEditorPersistantFormatException {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
if (!(Files.exists(xmlFile) ) ) createEditorXmlFile(docFactory, xmlFile);
// Document is required outside of initial parse
Document doc = null;
try (InputStream in = Files.newInputStream(xmlFile) ) {
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
doc = docBuilder.parse(in);
// We will use XPath to query for all Template nodes that are nested under the given world name
XPath worldTemplateQuery = XPathFactory.newInstance().newXPath();
XPathExpression worldTemplates = worldTemplateQuery.compile("/msleveleditor/world[@name='" + worldName + "']/templates");
XPathExpression worldEntry = worldTemplateQuery.compile("/msleveleditor/world[@name='" + worldName + "']");
Node templatesNode = (Node) worldTemplates.evaluate(doc, XPathConstants.NODE);
// <world> .... </world> will always be an element
Element worldNode = (Element) worldEntry.evaluate(doc, XPathConstants.NODE);
// if worldNode doesn't exist, create it.
if (worldNode == null) {
Element root = doc.getDocumentElement();
worldNode = doc.createElement("world");
worldNode.setAttribute("name", worldName);
root.appendChild(worldNode);
}
// If the node list is not empty, remove and replace.
if (templatesNode != null ) {
worldNode.removeChild(templatesNode);
templatesNode = null;
}
// If a templates node existed, it's gone now. Create a new one.
Element newTemplatesNode = doc.createElement("templates");
addTemplatesToNode(doc, newTemplatesNode, templates);
worldNode.appendChild(newTemplatesNode);
} catch (IOException | ParserConfigurationException | SAXException | XPathExpressionException e) {
throw new BadEditorPersistantFormatException(e);
}
// The DOM has been properly modified. Rewrite out to the file.
assert (doc != null);
saveDocToFile(doc, xmlFile);
}
private static void saveDocToFile(Document doc, Path xmlFile) throws BadEditorPersistantFormatException {
try (OutputStream os = Files.newOutputStream(xmlFile) ) {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(os);
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.transform(source, result);
} catch (IOException | TransformerException e) {
throw new BadEditorPersistantFormatException(e);
}
}
private static void createEditorXmlFile(DocumentBuilderFactory docFactory, Path xmlFile) throws BadEditorPersistantFormatException {
try {
Document doc = docFactory.newDocumentBuilder().newDocument();
Element root = doc.createElement("msleveleditor");
doc.appendChild(root);
saveDocToFile(doc, xmlFile);
} catch (ParserConfigurationException e) {
throw new RuntimeException("Cannot create editor xml file due to " + e.getMessage(), e);
}
}
private static String tileTypeToXml(TileType tile) {
if (tile instanceof CommonTile) {
switch ( ((CommonTile)tile).getUnderlyingType() ) {
case NONE: return "empty";
case SCENE: return "scene";
case SOLID: return "solid";
case THRU: return "thru";
default: throw new RuntimeException("Unknown stateless tile type " + ((CommonTile)tile).getUnderlyingType() );
}
} else if (tile instanceof CollapsibleTile) {
return "collapsible";
} else if (tile instanceof ConveyerTile) {
if (((ConveyerTile)tile).getConveyer().getRotation() == Rotation.CLOCKWISE) {
return "conveyer_clockwise";
} else {
return "conveyer_anti_clockwise";
}
} else if (tile instanceof HazardTile) {
return "hazard";
}
throw new RuntimeException("Xml decoding for templates cannot handle tiles of type " + tile.getClass().getName() );
}
// Call on a node of name "templates"
private static void addTemplatesToNode(Document doc, Node node, List<Template> templates) {
assert "templates".equals(node.getNodeName() );
// Now add the template nodes.
for (Template t : templates) {
Element templateNode = doc.createElement("template");
for (TemplateTile tempTile : t.getTilesInTemplate() ) {
Element tileNode = doc.createElement("tile");
tileNode.setAttribute("id", String.valueOf(tempTile.tile.getId() ) );
tileNode.setAttribute("row", String.valueOf(tempTile.row) );
tileNode.setAttribute("col", String.valueOf(tempTile.col) );
tileNode.setAttribute("type", tileTypeToXml(tempTile.tile) );
templateNode.appendChild(tileNode);
}
node.appendChild(templateNode);
}
}
}