package mekhq; import java.io.ByteArrayInputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Iterator; import java.util.Vector; import javax.xml.transform.Result; import javax.xml.transform.Source; 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 megamek.common.Aero; import megamek.common.BombType; import megamek.common.CommonConstants; import megamek.common.Entity; import megamek.common.EntityListFile; import megamek.common.FighterSquadron; import megamek.common.IPlayer; import megamek.common.Jumpship; import megamek.common.MULParser; import megamek.common.Tank; import megamek.common.util.StringUtil; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; public class MekHqXmlUtil { public static void writeSimpleXmlTag(PrintWriter pw1, int indent, String name, String val) { for (int x=0; x<indent; x++) pw1.print("\t"); pw1.print("<"+name+">"); pw1.print(escape(val)); pw1.println("</"+name+">"); } public static void writeSimpleXmlTag(PrintWriter pw1, int indent, String name, int val) { for (int x=0; x<indent; x++) pw1.print("\t"); pw1.print("<"+name+">"); pw1.print(val); pw1.println("</"+name+">"); } public static void writeSimpleXmlTag(PrintWriter pw1, int indent, String name, boolean val) { for (int x=0; x<indent; x++) pw1.print("\t"); pw1.print("<"+name+">"); pw1.print(val); pw1.println("</"+name+">"); } public static void writeSimpleXmlTag(PrintWriter pw1, int indent, String name, long val) { for (int x=0; x<indent; x++) pw1.print("\t"); pw1.print("<"+name+">"); pw1.print(val); pw1.println("</"+name+">"); } public static void writeSimpleXmlTag(PrintWriter pw1, int indent, String name, double val) { for (int x=0; x<indent; x++) pw1.print("\t"); pw1.print("<"+name+">"); pw1.print(val); pw1.println("</"+name+">"); } public static String indentStr(int level) { String retVal = ""; for (int x=0; x<level; x++) retVal += "\t"; return retVal; } public static String xmlToString(Node node) throws TransformerException { Source source = new DOMSource(node); StringWriter stringWriter = new StringWriter(); Result result = new StreamResult(stringWriter); TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = factory.newTransformer(); transformer.transform(source, result); return stringWriter.getBuffer().toString(); } /** * Contents copied from megamek.common.EntityListFile.saveTo(...) Modified * to support saving to/from XML for our purposes in MekHQ TODO: Some of * this may want to be back-ported into entity itself in MM and then * re-factored out of EntityListFile. * * @param tgtEnt * The entity to serialize to XML. * @return A string containing the XML representation of the entity. */ public static String writeEntityToXmlString(Entity tgtEnt, int indentLvl, ArrayList<Entity> list) { // Holdover from EntityListFile in MM. // I guess they simply ignored all squadrons for writing out entities? if (tgtEnt instanceof FighterSquadron) { return ""; } String retVal = ""; // Start writing this entity to the file. retVal += MekHqXmlUtil.indentStr(indentLvl) + "<entity chassis=\"" + escape(tgtEnt.getChassis()) + "\" model=\"" + escape(tgtEnt.getModel()) + "\" type=\"" + escape(tgtEnt.getMovementModeAsString()) + "\" commander=\"" + String.valueOf(tgtEnt.isCommander()); retVal += "\" externalId=\""; retVal += tgtEnt.getExternalIdAsString(); if (tgtEnt.countQuirks() > 0) { retVal += "\" quirks=\""; retVal += String.valueOf(escape(tgtEnt.getQuirkList("::"))); } if (tgtEnt.getC3Master() != null) { retVal += "\" c3MasterIs=\""; retVal += tgtEnt.getGame() .getEntity(tgtEnt.getC3Master().getId()) .getC3UUIDAsString(); } if (tgtEnt.hasC3() || tgtEnt.hasC3i()) { retVal += "\" c3UUID=\""; retVal += tgtEnt.getC3UUIDAsString(); } if (null != tgtEnt.getCamoCategory() && tgtEnt.getCamoCategory() != IPlayer.NO_CAMO && !tgtEnt.getCamoCategory().isEmpty()) { retVal += "\" camoCategory=\""; retVal += String.valueOf(escape(tgtEnt.getCamoCategory())); } if (null != tgtEnt.getCamoFileName() && tgtEnt.getCamoFileName() != IPlayer.NO_CAMO && !tgtEnt.getCamoFileName().isEmpty()) { retVal += "\" camoFileName=\""; retVal += String.valueOf(escape(tgtEnt.getCamoFileName())); } retVal += "\">\n"; // If it's a tank, add a movement tag. // Since tank movement can be affected by damage other than equipment // damage... // And thus can't necessarily be calculated. if (tgtEnt instanceof Tank) { Tank tentity = (Tank) tgtEnt; retVal += getMovementString(tentity, indentLvl+1); if (tentity.isTurretLocked(Tank.LOC_TURRET)) { retVal += getTurretLockedString(tentity, indentLvl+1); } // Crits retVal += getTankCritString(tentity, indentLvl+1); } // add a bunch of stuff for aeros if (tgtEnt instanceof Aero) { Aero a = (Aero) tgtEnt; // SI retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<structural integrity=\"" + String.valueOf(a.getSI()) + "\"/>\n"; // Heat sinks retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<heat sinks=\"" + String.valueOf(a.getHeatSinks()) + "\"/>\n"; // Fuel retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<fuel left=\"" + String.valueOf(a.getFuel()) + "\"/>\n"; int[] bombChoices = a.getBombChoices(); if (bombChoices.length > 0) { retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<bombs>\n"; for (int type = 0; type < BombType.B_NUM; type++) { if (bombChoices[type] > 0) { String typeName = BombType.getBombInternalName(type); retVal += MekHqXmlUtil.indentStr(indentLvl+2) + "<bomb type=\""; retVal += typeName; retVal += "\" load=\""; retVal += String.valueOf(bombChoices[type]); retVal += "\"/>\n"; } } retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "</bombs>\n"; } // TODO: dropship docking collars, bays // Large craft stuff if (a instanceof Jumpship) { Jumpship j = (Jumpship) a; // KF integrity retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<KF integrity=\"" + String.valueOf(j.getKFIntegrity()) + "\"/>\n"; // KF sail integrity retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<sail integrity=\"" + String.valueOf(j.getSailIntegrity()) + "\"/>\n"; } // Crits retVal += getAeroCritString(a, indentLvl+1); } // Add the locations of this entity (if any are needed). String loc = EntityListFile.getLocString(tgtEnt, indentLvl+1); if (null != loc) { retVal += loc; } // Write the C3i Data if needed if (tgtEnt.hasC3i()) { retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "<c3iset>"; retVal += CommonConstants.NL; Iterator<Entity> c3iList = list.iterator(); while (c3iList.hasNext()) { final Entity C3iEntity = c3iList.next(); if (C3iEntity.onSameC3NetworkAs(tgtEnt, true)) { retVal += MekHqXmlUtil.indentStr(indentLvl+2) + "<c3i_link link=\""; retVal += C3iEntity.getC3UUIDAsString(); retVal += "\"/>"; retVal += CommonConstants.NL; } } retVal += MekHqXmlUtil.indentStr(indentLvl+1) + "</c3iset>"; retVal += CommonConstants.NL; } // Finish writing this entity to the file. retVal += MekHqXmlUtil.indentStr(indentLvl) + "</entity>"; // Okay, return whatever we've got! return retVal; } /** * Contents copied from megamek.common.EntityListFile.getAeroCritString(...) * Modified to support saving to/from XML for our purposes in MekHQ * * @param a * The Aero unit to generate a crit string for. * @return The generated crit string. */ private static String getAeroCritString(Aero a, int indentLvl) { String retVal = MekHqXmlUtil.indentStr(indentLvl) + "<acriticals"; String critVal = ""; // crits if (a.getAvionicsHits() > 0) { critVal = critVal.concat(" avionics=\""); critVal = critVal.concat(Integer.toString(a.getAvionicsHits())); critVal = critVal.concat("\""); } if (a.getSensorHits() > 0) { critVal = critVal.concat(" sensors=\""); critVal = critVal.concat(Integer.toString(a.getSensorHits())); critVal = critVal.concat("\""); } if (a.getEngineHits() > 0) { critVal = critVal.concat(" engine=\""); critVal = critVal.concat(Integer.toString(a.getEngineHits())); critVal = critVal.concat("\""); } if (a.getFCSHits() > 0) { critVal = critVal.concat(" fcs=\""); critVal = critVal.concat(Integer.toString(a.getFCSHits())); critVal = critVal.concat("\""); } if (a.getCICHits() > 0) { critVal = critVal.concat(" cic=\""); critVal = critVal.concat(Integer.toString(a.getCICHits())); critVal = critVal.concat("\""); } if (a.getLeftThrustHits() > 0) { critVal = critVal.concat(" leftThrust=\""); critVal = critVal.concat(Integer.toString(a.getLeftThrustHits())); critVal = critVal.concat("\""); } if (a.getRightThrustHits() > 0) { critVal = critVal.concat(" rightThrust=\""); critVal = critVal.concat(Integer.toString(a.getRightThrustHits())); critVal = critVal.concat("\""); } if (!a.hasLifeSupport()) { critVal = critVal.concat(" lifeSupport=\"none\""); } if (a.isGearHit()) { critVal = critVal.concat(" gear=\"none\""); } if (!critVal.equals("")) { // then add beginning and end retVal = retVal.concat(critVal); retVal = retVal.concat("/>\n"); } else { return critVal; } return retVal; } /** * Contents copied from * megamek.common.EntityListFile.getTurretLockedString(...) Modified to * support saving to/from XML for our purposes in MekHQ * * @param e * The tank to generate a turret-locked string for. * @return The generated string. */ private static String getTurretLockedString(Tank e, int indentLvl) { String retval = MekHqXmlUtil.indentStr(indentLvl) + "<turretlock direction=\""; retval = retval.concat(Integer.toString(e.getSecondaryFacing())); retval = retval.concat("\"/>\n"); return retval; } /** * Contents copied from megamek.common.EntityListFile.getMovementString(...) * Modified to support saving to/from XML for our purposes in MekHQ * * @param e * The tank to generate a movement string for. * @return The generated string. */ private static String getMovementString(Tank e, int indentLvl) { String retVal = MekHqXmlUtil.indentStr(indentLvl) + "<movement speed=\""; boolean im = false; // This can throw an NPE for no obvious reason. // Okay, fine. If the tank doesn't even *have* an object related to this... // Lets assume it's fully mobile, as any other fact hasn't been recorded. try { im = e.isImmobile(); } catch (NullPointerException ex) { // Ignore - just don't completely fail out. } if (im) { retVal = retVal.concat("immobile"); } else { retVal = retVal.concat(Integer.toString(e.getOriginalWalkMP())); } retVal = retVal.concat("\"/>\n"); // save any motive hits retVal = retVal.concat(MekHqXmlUtil.indentStr(indentLvl) + "<motive damage=\""); retVal = retVal.concat(Integer.toString(e.getMotiveDamage())); retVal = retVal.concat("\" penalty=\""); retVal = retVal.concat(Integer.toString(e.getMotivePenalty())); retVal = retVal.concat("\"/>\n"); return retVal; } /** * Contents copied from megamek.common.EntityListFile.getTankCritString(...) * Modified to support saving to/from XML for our purposes in MekHQ * * @param e * The tank to generate a movement string for. * @return The generated string. */ private static String getTankCritString(Tank e, int indentLvl) { String retVal = MekHqXmlUtil.indentStr(indentLvl) + "<tcriticals"; String critVal = ""; // crits if (e.getSensorHits() > 0) { critVal = critVal.concat(" sensors=\""); critVal = critVal.concat(Integer.toString(e.getSensorHits())); critVal = critVal.concat("\""); } if (e.isEngineHit()) { critVal = critVal.concat(" engine=\""); critVal = critVal.concat("hit"); critVal = critVal.concat("\""); } /* crew are handled as a Person object in MekHq... if (e.isDriverHit()) { critVal = critVal.concat(" driver=\""); critVal = critVal.concat("hit"); critVal = critVal.concat("\""); } if (e.isCommanderHit()) { critVal = critVal.concat(" commander=\""); critVal = critVal.concat("hit"); critVal = critVal.concat("\""); } */ if (!critVal.equals("")) { // then add beginning and end retVal = retVal.concat(critVal); retVal = retVal.concat("/>\n"); } else { return critVal; } return retVal; } public static Entity getEntityFromXmlString(Node xml) throws UnsupportedEncodingException, TransformerException { MekHQ.logMessage("Executing getEntityFromXmlString(Node)...", 4); return getEntityFromXmlString(MekHqXmlUtil.xmlToString(xml)); } public static Entity getEntityFromXmlString(String xml) throws UnsupportedEncodingException { MekHQ.logMessage("Executing getEntityFromXmlString(String)...", 4); Entity retVal = null; MULParser prs = new MULParser(new ByteArrayInputStream(xml.getBytes("UTF-8"))); Vector<Entity> ents = prs.getEntities(); if (ents.size() > 1) throw new IllegalArgumentException( "More than one entity contained in XML string! Expecting a single entity."); else if (ents.size() != 0) retVal = ents.get(0); MekHQ.logMessage("Returning "+retVal+" from getEntityFromXmlString(String)...", 4); return retVal; } /** Escaping code for XML borrowed from org.json.XML * Full license and code available https://github.com/douglascrockford/JSON-java/blob/master/XML.java * @param string The string to be encoded * @return An encoded copy of the string **/ public static String escape(String string) { if (StringUtil.isNullOrEmpty(string)) { return string; } StringBuffer sb = new StringBuffer(); for (int i = 0, length = string.length(); i < length; i++) { char c = string.charAt(i); switch (c) { case '&': sb.append("&"); break; case '<': sb.append("<"); break; case '>': sb.append(">"); break; case '"': sb.append("""); break; case '\'': sb.append("'"); break; default: sb.append(c); } } return sb.toString(); } /** * Unescape...well, it reverses escaping... **/ public static String unEscape(String string) { return string.replaceAll( "&", "&" ).replaceAll( "<", "<" ).replaceAll( ">", ">" ).replaceAll( """, "\"" ).replaceAll( "&apos", "\'" ); } public static String getEntityNameFromXmlString(Node node) { NamedNodeMap attrs = node.getAttributes(); String chassis = attrs.getNamedItem("chassis").getTextContent(); String model = attrs.getNamedItem("model").getTextContent(); return chassis + " " + model; } }