diff --git a/src/main/java/seng302/models/parsers/StreamParser.java b/src/main/java/seng302/models/parsers/StreamParser.java index 265e5c82..10cae8ce 100644 --- a/src/main/java/seng302/models/parsers/StreamParser.java +++ b/src/main/java/seng302/models/parsers/StreamParser.java @@ -181,7 +181,6 @@ public class StreamParser extends Thread{ boatStatus += "\nEstTimeAtNextMark: " + extractTimeStamp(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20)), 6); boatStatus += "\nEstTimeAtFinish: " + extractTimeStamp(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20)), 6); boatStatuses.add(boatStatus); -// System.out.println("boatStatus = " + boatStatus); } } @@ -225,19 +224,19 @@ public class StreamParser extends Thread{ //Converts XML message to string to be parsed int currentChar; while (payloadStream.available() > 0 && (currentChar = payloadStream.read()) != 0) { - xmlMessage += (char)currentChar; + xmlMessage += (char)currentChar; } //Create XML document Object DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = null; + Document doc = null; try { db = dbf.newDocumentBuilder(); Document doc = db.parse(new InputSource(new StringReader(xmlMessage))); - // TODO: 25/04/17 ajm412: Check that the object matches expected structure and return Document object. } catch (ParserConfigurationException | IOException | SAXException e) { e.printStackTrace(); } - + // TODO: 30/04/2017 (ajm412) Figure out how this will tie into the backend of the visualiser now that the parsing is done. } /** diff --git a/src/main/java/seng302/models/parsers/StreamReceiver.java b/src/main/java/seng302/models/parsers/StreamReceiver.java index 5df8a1ab..9ceee0b0 100644 --- a/src/main/java/seng302/models/parsers/StreamReceiver.java +++ b/src/main/java/seng302/models/parsers/StreamReceiver.java @@ -50,6 +50,12 @@ public class StreamReceiver extends Thread { } + public StreamReceiver(Socket host, PriorityBlockingQueue packetBuffer){ + this.host=host; + this.packetBuffer = packetBuffer; + } + + public void connect(){ try { stream = host.getInputStream(); @@ -124,8 +130,8 @@ public class StreamReceiver extends Thread { } /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes + * takes an array of up to 7 bytes in little endian format and + * returns a positive long constructed from the input bytes * * @return a positive long if there is less than 7 bytes -1 otherwise */ @@ -143,8 +149,6 @@ public class StreamReceiver extends Thread { } - - public static void main(String[] args) { StreamReceiver sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"TestThread1"); diff --git a/src/main/java/seng302/models/parsers/XMLParser.java b/src/main/java/seng302/models/parsers/XMLParser.java new file mode 100644 index 00000000..ef65ea38 --- /dev/null +++ b/src/main/java/seng302/models/parsers/XMLParser.java @@ -0,0 +1,461 @@ +package seng302.models.parsers; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; + +/** + * Class to create an XML object from the XML Packet Messages. + */ +class XMLParser { + + /** + * Creates a Regatta XML Object from the data in a Regatta XML Message + * @param doc XML Document Object + * @return A new RegattaXMLObject from the input Document. + */ + RegattaXMLObject createRegattaXML(Document doc) { + return new RegattaXMLObject(doc); + } + + /** + * Creates a Race XML Object from the data in a Regatta XML Message + * @param doc XML Document Object + * @return A new RaceXMLObject from the input Document. + */ + RaceXMLObject createRaceXML(Document doc) { + return new RaceXMLObject(doc); + } + + /** + * Creates a Boat XML Object from the data in a Regatta XML Message + * @param doc XML Document Object + * @return A new BoatXMLObject from the input Document. + */ + BoatXMLObject createBoatXML(Document doc) { + return new BoatXMLObject(doc); + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as an Integer. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Integer getElementInt(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Integer.parseInt(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as an String. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static String getElementString(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return tagList.item(0).getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as a Double. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Double getElementDouble(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Double.parseDouble(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a String. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The String representation of the text content of an attribute in the given node, else returns null. + */ + private static String getNodeAttributeString(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return attrItem.getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as an Integer. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Integer representation of the text content of an attribute in the given node, else returns null. + */ + private static Integer getNodeAttributeInt(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Integer.parseInt(attrItem.getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a Double. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Double representation of the text content of an attribute in the given node, else returns null. + */ + private static Double getNodeAttributeDouble(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Double.parseDouble(attrItem.getTextContent()); + } else { + return null; + } + } + + class RegattaXMLObject { + //Regatta Info + private Integer regattaID; + private String regattaName; + private String courseName; + private Double centralLat; + private Double centralLng; + private Integer utcOffset; + + /** + * Constructor for a RegattaXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + RegattaXMLObject(Document doc) { + Element docEle = doc.getDocumentElement(); + + this.regattaID = getElementInt(docEle, "RegattaID"); + this.regattaName = getElementString(docEle, "RegattaName"); + this.courseName = getElementString(docEle, "CourseName"); + this.centralLat = getElementDouble(docEle, "CentralLatitude"); + this.centralLng = getElementDouble(docEle, "CentralLongitude"); + this.utcOffset = getElementInt(docEle, "UtcOffset"); + } + + public Integer getRegattaID() { return regattaID; } + public String getRegattaName() { return regattaName; } + public String getCourseName() { return courseName; } + public Double getCentralLat() { return centralLat; } + public Double getCentralLng() { return centralLng; } + public Integer getUtcOffset() { return utcOffset; } + + } + + class RaceXMLObject { + + // Race Info + private Integer raceID; + private String raceType; + private String creationTimeDate; // XML Creation Time + + //Race Start Details + private String raceStartTime; + private Boolean postponeStatus; + + //Non atomic race attributes + private ArrayList participants; + private ArrayList course; + private ArrayList compoundMarkSequence; + private ArrayList courseLimit; + + /** + * Constructor for a RaceXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + RaceXMLObject(Document doc) { + Element docEle = doc.getDocumentElement(); + + //Atomic and Semi-Atomic Elements + this.raceID = getElementInt(docEle, "RaceID"); + this.raceType = getElementString(docEle, "RaceType"); + this.creationTimeDate = getElementString(docEle, "CreationTimeDate"); + + Node raceStart = docEle.getElementsByTagName("RaceStartTime").item(0); + this.raceStartTime = getNodeAttributeString(raceStart, "Start") ; + this.postponeStatus = Boolean.parseBoolean(getNodeAttributeString(raceStart, "Postpone")); + + //Participants + participants = new ArrayList<>(); + + NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes(); + for (int i = 0; i < pList.getLength(); i++) { + Node pNode = pList.item(i); + String entry; + if (pNode.getNodeName().equals("Yacht")) { + Integer sourceID = getNodeAttributeInt(pNode, "SourceID"); + + if (pNode.getAttributes().getLength() == 2) { + entry = getNodeAttributeString(pNode, "Entry"); + } else { + entry = null; + } + + Participant pa = new Participant(sourceID, entry); + participants.add(pa); + } + } + + //Course + course = new ArrayList<>(); + + NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes(); + for (int i = 0; i < cMarkList.getLength(); i++) { + Node cMarkNode = cMarkList.item(i); + if (cMarkNode.getNodeName().equals("CompoundMark")) { + CompoundMark cMark = new CompoundMark(cMarkNode); + course.add(cMark); + } + } + + //Course Mark Sequence + compoundMarkSequence = new ArrayList<>(); + + NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0).getChildNodes(); + for (int i = 0; i < cornerList.getLength(); i++) { + Node cornerNode = cornerList.item(i); + if (cornerNode.getNodeName().equals("Corner")) { + Corner corner = new Corner(cornerNode); + compoundMarkSequence.add(corner); + } + } + + //Course Limits + courseLimit = new ArrayList<>(); + + NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes(); + for (int i = 0; i < limitList.getLength(); i++) { + Node limitNode = limitList.item(i); + if (limitNode.getNodeName().equals("Limit")) { + Limit limit = new Limit(limitNode); + courseLimit.add(limit); + } + } + } + + public Integer getRaceID() { return raceID; } + public String getRaceType() { return raceType; } + public String getCreationTimeDate() { return creationTimeDate; } + public String getRaceStartTime() { return raceStartTime; } + public Boolean getPostponeStatus() { return postponeStatus; } + + public ArrayList getParticipants() { return participants; } + public ArrayList getCompoundMarks() { return course; } + public ArrayList getCompoundMarkSequence() { return compoundMarkSequence; } + public ArrayList getCourseLimit() { return courseLimit; } + + class Participant { + Integer sourceID; + String entry; + + Participant(Integer sourceID, String entry) { + this.sourceID = sourceID; + this.entry = entry; + } + + public Integer getsourceID() { return sourceID; } + public String getEntry() { return entry; } + } + + class CompoundMark { + private Integer markID; + private String cMarkName; + private ArrayList marks; + + CompoundMark(Node compoundMark) { + marks = new ArrayList<>(); + this.markID = getNodeAttributeInt(compoundMark, "CompoundMarkID"); + this.cMarkName = getNodeAttributeString(compoundMark, "Name"); + NodeList childMarks = compoundMark.getChildNodes(); + for (int i = 0; i < childMarks.getLength(); i++) { + Node markNode = childMarks.item(i); + if (markNode.getNodeName().equals("Mark")) { + Mark mark = new Mark(markNode); + marks.add(mark); + } + } + } + + public Integer getMarkID() { return markID; } + public String getcMarkName() { return cMarkName; } + public ArrayList getMarks() { return marks; } + + class Mark { + private Integer seqID; + private Integer sourceID; + private String markName; + private Double targetLat; + private Double targetLng; + + Mark(Node markNode) { + + this.seqID = getNodeAttributeInt(markNode, "SeqID"); + this.sourceID = getNodeAttributeInt(markNode, "SourceID"); + this.markName = getNodeAttributeString(markNode, "Name"); + this.targetLat = getNodeAttributeDouble(markNode, "TargetLat"); + this.targetLng = getNodeAttributeDouble(markNode, "TargetLng"); + + } + + public Integer getSeqID() { return seqID; } + public Integer getSourceID() { return sourceID; } + public String getMarkName() { return markName; } + public Double getTargetLat() { return targetLat; } + public Double getTargetLng() { return targetLng; } + } + } + + class Corner { + private Integer seqID; + private Integer compoundMarkID; + private String rounding; + private Integer zoneSize; + + Corner(Node cornerNode) { + this.seqID = getNodeAttributeInt(cornerNode, "SeqID"); + this.compoundMarkID = getNodeAttributeInt(cornerNode, "CompoundMarkID"); + this.rounding = getNodeAttributeString(cornerNode, "Rounding"); + this.zoneSize = getNodeAttributeInt(cornerNode, "ZoneSize"); + } + + public Integer getSeqID() { return seqID; } + public Integer getCompoundMarkID() { return compoundMarkID; } + public String getRounding() { return rounding; } + public Integer getZoneSize() { return zoneSize; } + } + + class Limit { + private Integer seqID; + private Double lat; + private Double lng; + + Limit(Node limitNode) { + this.seqID = getNodeAttributeInt(limitNode, "SeqID"); + this.lat = getNodeAttributeDouble(limitNode, "Lat"); + this.lng = getNodeAttributeDouble(limitNode, "Lon"); + } + + public Integer getSeqID() { return seqID; } + public Double getLat() { return lat; } + public Double getLng() { return lng; } + } + + } + + class BoatXMLObject { + + private String lastModified; + private Integer version; + + //Settings for the boat type in the race. This may end up having to be reworked if multiple boat types compete. + private String boatType; + private Double boatLength; + private Double hullLength; + private Double markZoneSize; + private Double courseZoneSize; + private ArrayList zoneLimits;// will only contain 5 elements. Limits 1-5 + + //Boats + ArrayList boats; + + /** + * Constructor for a BoatXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + BoatXMLObject(Document doc) { + + Element docEle = doc.getDocumentElement(); + + this.lastModified = getElementString(docEle, "Modified"); + this.version = getElementInt(docEle, "Version"); + + NodeList settingsList = docEle.getElementsByTagName("Settings").item(0).getChildNodes(); + this.boatType = getNodeAttributeString(settingsList.item(1), "Type"); + this.boatLength = getNodeAttributeDouble(settingsList.item(3), "BoatLength"); + this.hullLength = getNodeAttributeDouble(settingsList.item(3), "HullLength"); + this.markZoneSize = getNodeAttributeDouble(settingsList.item(5), "MarkZoneSize"); + this.courseZoneSize = getNodeAttributeDouble(settingsList.item(5), "CourseZoneSize"); + + Node zoneLimitsList = settingsList.item(7); + this.zoneLimits = new ArrayList<>(); + for (int i = 0; i < zoneLimitsList.getAttributes().getLength(); i++) { + String tag = String.format("Limit%d", i+1); + this.zoneLimits.add(getNodeAttributeDouble(zoneLimitsList, tag)); + } + + this.boats = new ArrayList<>(); + NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes(); + for (int i = 0; i < boatsList.getLength(); i++) { + Node currentBoat = boatsList.item(i); + if (currentBoat.getNodeName().equals("Boat")) { + Boat boat = new Boat(currentBoat); + this.boats.add(boat); + } + //System.out.println(this.getBoats()); + } + + } + + public String getLastModified() { return lastModified; } + public Integer getVersion() { return version; } + public String getBoatType() { return boatType; } + public Double getBoatLength() { return boatLength; } + public Double getHullLength() { return hullLength; } + public Double getMarkZoneSize() { return markZoneSize; } + public Double getCourseZoneSize() { return courseZoneSize; } + public ArrayList getZoneLimits() { return zoneLimits; } + public ArrayList getBoats() { return boats; } + + class Boat { + + private String boatType; + private Integer sourceID; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + + Boat(Node boatNode) { + this.boatType = getNodeAttributeString(boatNode, "Type"); + this.sourceID = getNodeAttributeInt(boatNode, "SourceID"); + this.hullID = getNodeAttributeString(boatNode, "HullNum"); + this.shortName = getNodeAttributeString(boatNode, "ShortName"); + this.boatName = getNodeAttributeString(boatNode, "BoatName"); + this.country = getNodeAttributeString(boatNode, "Country"); + } + + public String getBoatType() { return boatType; } + public Integer getSourceID() { return sourceID; } + public String getHullID() { return hullID; } + public String getShortName() { return shortName; } + public String getBoatName() { return boatName; } + public String getCountry() { return country; } + + } + + } + +} \ No newline at end of file diff --git a/src/test/java/seng302/models/parsers/StreamReceiverTest.java b/src/test/java/seng302/models/parsers/StreamReceiverTest.java new file mode 100644 index 00000000..c7951e3b --- /dev/null +++ b/src/test/java/seng302/models/parsers/StreamReceiverTest.java @@ -0,0 +1,105 @@ +package seng302.models.parsers; + +import org.junit.Before; +import org.junit.Test; + +import java.io.*; +import java.lang.reflect.Method; +import java.net.Socket; +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by ptg19 on 26/04/17. + */ +public class StreamReceiverTest { + + private PriorityBlockingQueue pq; + private byte[] brokenPacket = {0x47, (byte) 0x83, 37, // sync1 sync2 and message type + 0b00000000, 0b01000000, 0b00100010, 0b00100100, 0b00011000, 0b00000000, //timestamp + 0b00000000, 0b00010000, 0b01000000, 0b00000000, //source id + 0b00100010, 0b00101000, // message length + 0b00010010, 0b00010010, 0b00010010}; //random start of payload + + private byte[] workingPacket = {0x47, (byte) 0x83, 37, // sync1 sync2 and message type + 0b00000000, 0b01000000, 0b00100010, 0b00100100, 0b00011000, 0b00000000, //timestamp + 0b00000000, 0b00010000, 0b01000000, 0b00000000, //source id + 0b00000010, 0b00000000, // message length + 0b00010010, 0b00010010, // payload + 0b00100110, (byte)0b10000111, 0b00110101, 0b01111000}; //crc + + private byte[] crcMismatchPacket = {0x47, (byte) 0x83, 37, // sync1 sync2 and message type + 0b00000000, 0b01000000, 0b00100010, 0b00100100, 0b00011000, 0b00000000, //timestamp + 0b00000000, 0b00000000, 0b01000000, 0b00000000, //source id + 0b00000010, 0b00000000, // message length + 0b00010010, 0b00010010, // payload + 0b00100110, (byte)0b10000111, 0b00110101, 0b01111000}; //crc + + + @Before + public void setup(){ + pq = new PriorityBlockingQueue<>(256, new Comparator() { + @Override + public int compare(StreamPacket s1, StreamPacket s2) { + return (int) (s1.getTimeStamp() - s2.getTimeStamp()); + } + }); + } + + @Test + public void connectExitsOnUnexpectedStreamEnd() throws Exception { + Socket host=mock(Socket.class); + InputStream stream = new ByteArrayInputStream(brokenPacket); + when(host.getInputStream()).thenReturn(stream); + StreamReceiver streamReceiver = new StreamReceiver(host, pq); + + streamReceiver.connect(); + assert pq.size() == 0; + } + + @Test + public void connectReadsAPacket() throws Exception { + Socket host=mock(Socket.class); + InputStream stream = new ByteArrayInputStream(workingPacket); + when(host.getInputStream()).thenReturn(stream); + StreamReceiver streamReceiver = new StreamReceiver(host, pq); + + streamReceiver.connect(); + assert pq.size() == 1; + } + + @Test + public void connectDropsAMismatchedCrc() throws Exception { + Socket host=mock(Socket.class); + InputStream stream = new ByteArrayInputStream(crcMismatchPacket); + when(host.getInputStream()).thenReturn(stream); + StreamReceiver streamReceiver = new StreamReceiver(host, pq); + + streamReceiver.connect(); + assert pq.size() == 0; + } + + @Test + public void bytestoLongTest() { + Socket host=mock(Socket.class); + StreamReceiver streamReceiver = new StreamReceiver(host, pq); + try { + Class[] args = new Class[1]; + args[0] = byte[].class; + Method bytesToLong = streamReceiver.getClass().getDeclaredMethod("bytesToLong", args); + bytesToLong.setAccessible(true); + byte[] sevenBtyeNumber = {0b01100100, 0b00110100, 0b00010100, 0b00000000, 0b00000000, 0b00000000, (byte)0b10000000}; + assert bytesToLong.invoke(streamReceiver, sevenBtyeNumber).equals(36028797020288100L); + byte[] eightByteNumber = {0b01100100, 0b00110100, 0b00010100, 0b00000000, 0b00000000, 0b00000000, (byte)0b10000000, 0b00100101}; + assert bytesToLong.invoke(streamReceiver, eightByteNumber).equals(-1L); + byte[] emptyArray = {}; + assert bytesToLong.invoke(streamReceiver, emptyArray).equals(0L); + } catch (Exception e){ + System.out.println(""); + } + } + +} \ No newline at end of file