package org.erikaredmark.monkeyshines.editor.persist; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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.World; import org.erikaredmark.monkeyshines.editor.exception.BadEditorPersistantFormatException; import org.erikaredmark.monkeyshines.editor.model.Template; import org.erikaredmark.monkeyshines.tiles.CommonTile; import org.erikaredmark.monkeyshines.tiles.TileType; import org.erikaredmark.monkeyshines.tiles.TileTypes; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import com.google.common.base.Function; /** * * Interprets an xml stream as the save state for an editor, and allows for the parsing of a list of templates based on * the stream and the name of the world we are looking for templates for. * * @author Erika Redmark * */ public final class TemplateXmlReader { /** * * Reads the list of templates from the given stream for the given world name. The stream is NOT closed by the method. * <p/> * Because one bad template should not affect the others, issues are designated via callbacks. The most likely issue is * in regards to a template defining a tile type whose id is out of range of the current graphics type. In this case, the * specific template in error will be ignored and not returned. * * @param s * stream to read * * @param world * the world to load the templates for. This is used for searching as well as generating templates with * tile types found in the world * * @param badTemplateCallback * if a template refers to tiles out-of-range of the current world, this is called with a reference to the * XML node of the {@code <template> } object that failed and a reason * * @return * list of templates for that world, an empty list if no templates are defined. Never {@code null} * * @throws BadEditorPersistantFormatException * if the given stream is not a valid editor persistant format * */ public static List<Template> read(InputStream s, World world, Function<TemplateIssue, Void> badTemplateCallback) throws BadEditorPersistantFormatException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); Document doc = docBuilder.parse(s); // We will use XPath to query for all Template nodes that are nested under the given world name XPath worldTemplateQuery = XPathFactory.newInstance().newXPath(); XPathExpression expression = worldTemplateQuery.compile("/msleveleditor/world[@name='" + world.getWorldName() + "']/templates/template"); NodeList nodes = (NodeList) expression.evaluate(doc, XPathConstants.NODESET); List<Template> returnList = new ArrayList<>(); for (int i = 0; i < nodes.getLength(); ++i) { Node n = nodes.item(i); // This node should be a single <template> entry in the <templates> container. We can build a template from // its type boolean skipTemplate = false; if ("template".equals(n.getNodeName() ) ) { Template.Builder templateBuilder = new Template.Builder(); NodeList tiles = n.getChildNodes(); for (int j = 0; j < tiles.getLength(); ++j) { Node tile = tiles.item(j); // Some nodes are just #text nodes. Skip them if they have not attributes NamedNodeMap attribs = tile.getAttributes(); if (attribs == null) continue; Node rowNode = attribs.getNamedItem("row"); Node colNode = attribs.getNamedItem("col"); Node idNode = attribs.getNamedItem("id"); Node typeNode = attribs.getNamedItem("type"); if (rowNode == null || colNode == null || idNode == null || typeNode == null) { badTemplateCallback.apply(new TemplateIssue(tile, IssueType.TILE_MISSING_REQUIRED_ATTRIBUTES) ); skipTemplate = true; break; } int row = Integer.parseInt(rowNode.getNodeValue() ); int col = Integer.parseInt(colNode.getNodeValue() ); int id = Integer.parseInt(idNode.getNodeValue() ); TileType tileType = null; switch (typeNode.getNodeValue() ) { case SOLIDS: tileType = TileTypes.solidFromId(id); break; case THRUS: tileType = TileTypes.thruFromId(id); break; case SCENES: tileType = TileTypes.sceneFromId(id); break; case HAZARDS: if (!(TileTypes.canHazardFromId(id, world) ) ) { badTemplateCallback.apply(new TemplateIssue(tile, IssueType.TILE_ID_NOT_AVAILABLE) ); skipTemplate = true; } else { tileType = TileTypes.hazardFromId(id, world); } break; case CONVEYER_CLOCKWISE: if (!(TileTypes.canConveyerFromId(id, world) ) ) { badTemplateCallback.apply(new TemplateIssue(tile, IssueType.TILE_ID_NOT_AVAILABLE) ); skipTemplate = true; } else { tileType = TileTypes.clockwiseConveyerFromId(id, world); } break; case CONVEYER_ANTI_CLOCKWISE: if (!(TileTypes.canConveyerFromId(id, world) ) ) { badTemplateCallback.apply(new TemplateIssue(tile, IssueType.TILE_ID_NOT_AVAILABLE) ); skipTemplate = true; } else { tileType = TileTypes.anticlockwiseConveyerFromId(id, world); } break; case COLLAPSIBLE: tileType = TileTypes.collapsibleFromId(id); break; case EMPTY: tileType = CommonTile.NONE; break; default: badTemplateCallback.apply(new TemplateIssue(tile, IssueType.TILE_TYPE_UNKNOWN) ); skipTemplate = true; break; } // Breaks in switch statement are normal... check for template skipping to break from // for loop if (skipTemplate) break; assert tile != null; templateBuilder.addTile(row, col, tileType); } // Skips this template due to an issue with a specific tile. Does not skip all templates. if (skipTemplate) continue; // We didn't skip the template? Good, we have a valid template. Create and add to the list. returnList.add(templateBuilder.build() ); } } return returnList; } catch (IOException | ParserConfigurationException | SAXException | XPathExpressionException e) { throw new BadEditorPersistantFormatException(e); } } /** * * Represents an issue when parsing the xml. Wraps together both the node that failed along with the reason the node failed. * * @author Erika Redmark * */ public static class TemplateIssue { public IssueType issue; public Node issueNode; private TemplateIssue(final Node issueNode, final IssueType issue) { this.issue = issue; this.issueNode = issueNode; } } /** * * Indicates a type of issue with the xml when parsing. Contained in {@code TemplateIssue} * * @author Erika Redmark * */ public enum IssueType { TILE_MISSING_REQUIRED_ATTRIBUTES("The <tile> element is missing required attributes. Must contain 'row', 'col', 'id', and 'type' "), TILE_TYPE_UNKNOWN("The <tile> element refers to a type that is not available"), TILE_ID_NOT_AVAILABLE("The <tile> element refers to an id of a tile that the given world does not have a graphics resource for"); private IssueType(final String msg) { this.msg = msg; } public String getMessage() { return msg; } private final String msg; } // Tiletypes are they are named in the XML form private static final String SOLIDS = "solid"; private static final String THRUS = "thru"; private static final String SCENES = "scene"; private static final String HAZARDS = "hazard"; private static final String CONVEYER_CLOCKWISE = "conveyer_clockwise"; private static final String CONVEYER_ANTI_CLOCKWISE = "conveyer_anti_clockwise"; private static final String COLLAPSIBLE = "collapsible"; private static final String EMPTY = "empty"; }