package repository; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JOptionPane; 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 org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import events.EntityDrawboxChangedEvent; import events.EntityDrawboxChangedListener; import events.EntityHitboxChangedEvent; import events.EntityHitboxChangedListener; import exception.DuplicateEntryException; import launch.Launcher; import model.Entity; /** * Класс данных, который оперирует их сохранением, загрузкой, и хранением в памяти. * ВАЖНО: данный класс хранит так же актуальную копию XML-представления файла * */ public class Project implements Iterable, EntityDrawboxChangedListener, EntityHitboxChangedListener { /** * Путь к XML по-умолчанию. * Просто заглушка, обычно заменяется актуальным путём в ходе изменения программы.
* Cм. {@link #setXMLPath(String)} и {@link #getXMLPath()} * */ public static final String DEFAULT_XML_PATH = "res/"; //TODO: make an actual path to example objecttypes in the root of the project public static final String DEFAULT_XML_FILENAME = "objecttypes.xml"; /** * Переменная которая отслеживает, были ли произведены изменения XML-Dom-дерева. * */ public static boolean changeOfXmlDOM = false; static Project thisProject; private List listEntity = new ArrayList(); private String path = DEFAULT_XML_PATH; private String fileName = DEFAULT_XML_FILENAME; private DocumentBuilderFactory factory; private DocumentBuilder builder; private Document document; private static Logger logger = Logger.getLogger("repository.Project"); private Project(){}; public static Project getInstance() { if(thisProject == null) { thisProject = new Project(); } return thisProject; }; /** * Устанавливает значение пути к XML. Все операции загрузки и сохранения будут работать с этой директорией.
* По-умолчанию - {@link #DEFAULT_XML_PATH} * @param newPath - абсолютный или относительный адрес папки в виде строки. *
например: "C:/User/map/" или "/home/username/map/" или "res/map" (относительный путь рассчитывается от корня проекта) * */ public void setXMLPath(String newPath) { if(newPath != null && !newPath.isEmpty()) { path = newPath.trim(); } else System.err.println("Trying to submit empty path! Project.setXMLPath()"); } /** * Возвращает актуальный путь к директории, в которой лежит XML-файл с типами объектов, а так же папки с ресурсами.
* Считается, что ресурсы находятся в той же папке что XML-файл. Должен включать / в конце * */ public String getXMLPath() { return path; } /** * Устанавливает имя файла, из которого загружаются сущности. Ещё нужен путь (см. {@link #getXMLPath()}) * */ public void setXMLFileName(String name) { this.fileName = name; } /** * Возвращает имя файла, из которого загружаются ресурсы. Нужен для контроля доступа к нему, а так же избавления от жёстких зависимостей.
* Например, если fileName в какой-то момент со String сменится на URL достаточно будет адаптировать геттер а не переписывать * все места где происходят к нему обращения. * */ public String getXMLFileName() { return fileName; } /** * Использует {@link #setXMLFileName(String)} и {@link #setXMLPath(String)} для того, чтобы сохранить новый путь к XML-файлу и его имя. * Повторно вызывать их вручную не обязательно. * * @param directory - папка где хранится XML-файл и ресурсы (см. {@link #getXMLPath()}) * @param name - имя XML-файла с определениями типов сущностей * @see #load() * */ public void load(String directory, String name) { setXMLPath(directory); setXMLFileName(name); try { load(); } catch (SAXException | IOException | ParserConfigurationException e) { System.err.println("Failed to load project! Cause: "+e.getMessage()); } } /** * Загружает типы сущностей из файла с заданным именем и расположением.
0 * */ public void load() throws SAXException, IOException, ParserConfigurationException { listEntity.clear(); factory = DocumentBuilderFactory.newInstance(); builder = factory.newDocumentBuilder(); document = builder.parse(new File(path+fileName)); // TODO: remove this from Project, it should not know SHIT about MainGUI if(Launcher.getMainGUI() != null) // at the first program launch, main gui creates list gui before static link to main gui is set Launcher.getMainGUI().setTitle("Hitbox/Drawbox Editor: " + path + fileName); // Получение списка всех элементов objecttype внутри корневого элемента (getDocumentElement возвращает ROOT элемент XML файла). NodeList objecttypeElements = document.getDocumentElement().getElementsByTagName("objecttype"); for(int i = 0; i < objecttypeElements.getLength(); i++) { Node objecttype = objecttypeElements.item(i); NamedNodeMap attributesObject = objecttype.getAttributes(); String entityName = new String(attributesObject.getNamedItem("name").getNodeValue()); parsingElementXMLtoElementList(entityName,objecttype); } } private void parsingElementXMLtoElementList(String entityName, Node objecttype) { String newDrawbox = null; String newHitbox = null; String type = null; BufferedImage sprite = null; Element element = (Element)objecttype; NodeList propertyElements = element.getElementsByTagName("property"); if(propertyElements != null) { // do not lazy load images here - Hitbox parser needs real image sizes sprite = loadImageByName(entityName); for(int i = 0; i < propertyElements.getLength(); i++) { Element property = (Element)propertyElements.item(i); String propertyName = property.getAttribute("name"); String defaultProperty = property.getAttribute("default"); switch(propertyName) { case "drawbox": newDrawbox = defaultProperty; break; case "hitbox": newHitbox = defaultProperty; break; case "class": type = defaultProperty; break; } } Entity e = new Entity(entityName, newDrawbox, newHitbox, sprite); e.setType(type); listEntity.add(e); } } /** * Загружает с диска изображение с заданным именем. Подразумевается, что изображение находится {@link #path там же} где * XML-файл. Возвращает null если изображение не найдено. * */ public BufferedImage loadImageByName(String name) { String extension = "png"; // TODO: изображения следует подгружать в отдельном потоке! try { File imageFile = new File(path + name + '.' + extension); if (!imageFile.exists()) throw new FileNotFoundException(); BufferedImage image = ImageIO.read(imageFile); return image; } catch (FileNotFoundException fe) { logger.warning("Image file \""+path+name+'.'+extension+"\" is not found!"); } catch (IOException e) { logger.warning("Cannot read file \""+path+name+'.'+extension+"\"!"); } return null; } /** * Adds a new Entity to in-editor list of all entities and it's in-memory XML-representiation.
* Does not write anything to actual XML-file, use {@link #writeXML()} for it * @throws DuplicateEntryException - thrown if an entity with such a name already exists * @params e - entity to add */ public void addEntity(Entity e) throws DuplicateEntryException { if(getEntityByName(e.getName()) != null) throw new DuplicateEntryException("The entity with the name '" + e.getName() + "' already exists!"); //TODO: move image loading to AddListElementEntityListener and make it lazy e.setImage(loadImageByName(e.getName())); listEntity.add(e); Element objecttypeElement = document.createElement("objecttype"); document.getElementsByTagName("objecttypes")// get document root element named "objecttypes" .item(0) .appendChild(objecttypeElement); // add new "objecttype" element as a child objecttypeElement.setAttribute("name", e.getName()); objecttypeElement.setAttribute("color", "000000");//color of entity, needed by Tiled editor // format: Element classProperty = document.createElement("property"); classProperty.setAttribute("name", "class"); classProperty.setAttribute("type", "string"); classProperty.setAttribute("default", e.getType()); objecttypeElement.appendChild(classProperty); // format: Element drawboxProperty = document.createElement("property"); drawboxProperty.setAttribute("name", "drawbox"); drawboxProperty.setAttribute("type", "string"); drawboxProperty.setAttribute("default", "0 0 0 0 0 0 0 0"); // empty, because on creation there is no drawbox yet objecttypeElement.appendChild(drawboxProperty); // format: Element hitboxProperty = document.createElement("property"); hitboxProperty.setAttribute("name", "hitbox"); hitboxProperty.setAttribute("type", "string"); hitboxProperty.setAttribute("default", "Rectangle 0 0 0 0"); // empty, no hitbox yet objecttypeElement.appendChild(hitboxProperty); changeOfXmlDOM = true; //printXMlToConsole(); //DEBUG! } /** * Удаляется обьект из списка всех сущностей, и из xml-дерева */ public void removeEntity(Entity e) { listEntity.remove(e); NodeList nl = document.getElementsByTagName("objecttype"); for(int i = 0; i < nl.getLength(); i++) { Node objecttype = nl.item(i); if(objecttype.getAttributes().getNamedItem("name").getNodeValue().equals(e.getName())) document.getElementsByTagName("objecttypes").item(0).removeChild(objecttype); } } public Entity getEntity(int id) { return listEntity.get(id); } /** * Возвращает объект сущности с заданным именем, или вбрасывает исключение, если такой сущности не существует. * */ public Entity getEntityByName(String name) { //debug print //System.out.println("----- session started ------"); for(Entity e: listEntity) { //System.out.println("we need "+ name +" we got "+e.getName()); if(e.getName().equals(name)) return e; } return null; } public void writeXML() { stripEmptyElements(document); try { TransformerFactory transformerFactory = TransformerFactory.newInstance(); transformerFactory.setAttribute("indent-number", 4); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(document); StreamResult result = new StreamResult(new FileOutputStream(getXMLPath() + getXMLFileName())); transformer.transform(source, result); JOptionPane.showMessageDialog(null, "Бугагашеньки, сохранилось!", "Success", JOptionPane.INFORMATION_MESSAGE); } catch (TransformerException | FileNotFoundException e) { JOptionPane.showMessageDialog(null, "Saving project is unsuccsessfull! Erorr is: "+e, "Project save unsuccsesfull", JOptionPane.ERROR_MESSAGE); } changeOfXmlDOM = false; } public void PrintEntitys() { for(Entity ent:listEntity) { ent.PrintEntity(); } } public void printXMlToConsole() { stripEmptyElements(document); try { TransformerFactory transformerFactory = TransformerFactory.newInstance(); transformerFactory.setAttribute("indent-number", 4); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(document); StreamResult result = new StreamResult(System.out); transformer.transform(source, result); } catch (TransformerException e) { System.err.println("Cannot print XML to console. "+e); } } @Override public Iterator iterator() { return listEntity.iterator(); } /** * Лучше бы пользоваться этой функцией поменьше - почти всю работу вполне можно сделать через интерфейс Project
* Например, если нужно перебрать все сущности, стоит использовать цикл foreach с использованием инстанса Project, например:
* {@code for(Entity e: Project.getInstance()){ *тут клиентский код* }} * */ @Deprecated public List getListEntity() { return listEntity; } /** * Получение события при отрисовке нового Drawbox, для изменения XML-дерева. * Объект event хранит в себе ссылку на новый объект drawbox и объект entity, для которой он был создан. * */ @Override public void drawboxChanged(EntityDrawboxChangedEvent event) { String entityName = event.owner.getName(); Node entityToUpdate = getEntityXMLNodeByName(entityName); NodeList properties = entityToUpdate.getChildNodes(); for(int i = 0; i < properties.getLength(); i++) { if(properties.item(i) instanceof Element) { // ignoring #text nodes //we have hitbox, drawbox and class, gotta set the right one Element propertyElement = ((Element)properties.item(i)); if (propertyElement.getAttribute("name").equals("drawbox")) { propertyElement.setAttribute("default", event.owner.getDrawbox().listPointsToString()); } } } changeOfXmlDOM = true; } /** * Получение события при отрисовке нового Hitbox, для изменения XML-дерева. * Объект event хранит в себе ссылку на новый объект Hitbox и объект entity, для которой он был создан. * */ @Override public void hitboxChanged(EntityHitboxChangedEvent event) { String entityName = event.owner.getName(); Node entityToUpdate = getEntityXMLNodeByName(entityName); NodeList properties = entityToUpdate.getChildNodes(); for(int i = 0; i < properties.getLength(); i++) { if(properties.item(i) instanceof Element) { // ignoring #text nodes //we have hitbox, drawbox and class, gotta set the right one Element propertyElement = ((Element)properties.item(i)); if (propertyElement.getAttribute("name").equals("hitbox")) { propertyElement.setAttribute("default", event.owner.getHitbox().listPointsToString()); } } } changeOfXmlDOM = true; } private Node getEntityXMLNodeByName(String name) { //returns entitie's objecttype node NodeList nl = document.getElementsByTagName("objecttype"); for(int i = 0; i < nl.getLength(); i++) { Node objecttype = nl.item(i); if(objecttype.getAttributes().getNamedItem("name").getNodeValue().equals(name)) { return objecttype; } } return null; } // https://stackoverflow.com/a/64659614/6929164 // new empty lines will appear on every XML save without this function private static void stripEmptyElements(Node node) { NodeList children = node.getChildNodes(); for(int i = 0; i < children.getLength(); ++i) { Node child = children.item(i); if(child.getNodeType() == Node.TEXT_NODE) { if (child.getTextContent().trim().length() == 0) { child.getParentNode().removeChild(child); i--; } } stripEmptyElements(child); } } } //в момент окончания рисования, в зависимости в какой мы рисуем вкладке хитбокса, //в зависимости от того в какой панельке(jpanel)и подклассе интерфейса Editable //мы дорисовали фигуру, такой форматер и создается. //хотя не стоит забывать про сейв(не знаю зачем, но стоит подумать, когда сяду снова делать)