+// Copyright (C) 2004, 2015 Christian Jaekl
+
+package net.jaekl.qd.xml;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.junit.Assert;
+
+import org.junit.Test;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+public class ParseResultTest {
+ // Some samples of XML that we're going to (try to) parse\
+ private static final String MINIMAL_XML =
+ "<Root/>";
+ private static final String MINIMAL_XML_WITH_PROLOGUE =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root/>";
+ private static final String XML_WITH_MINOR_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root><One/></Root>";
+ private static final String ROOT_INSIDE_SECONDARY_ELEMENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Secondary><Root><One/></Root></Secondary>";
+ private static final String PROLOGUE_AND_SECONDARY_ELEMENT =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Secondary/>";
+ private static final String SIMPLE_INTERNAL_TAGS =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root><One/><Two>content of two</Two><Three>3</Three></Root>";
+ private static final String ROUTE_SUMMARY_FOR_STOP =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n"
+ + "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"
+ + " <soap:Body>\n"
+ + " <GetRouteSummaryForStopResponse xmlns=\"http://octranspo.com\">\n"
+ + " <GetRouteSummaryForStopResult>\n"
+ + " <StopNo xmlns=\"http://tempuri.org/\">1234</StopNo>\n"
+ + " <StopDescription"
+ + " xmlns=\"http://tempuri.org/\">ONE-TWO-THREE-FOUR</StopDescription>\n"
+ + " <Error xmlns=\"http://tempuri.org/\"/>\n"
+ + " <Routes xmlns=\"http://tempuri.org/\">\n"
+ + " <Route>\n"
+ + " <RouteNo>123</RouteNo>\n"
+ + " <DirectionID>0</DirectionID>\n"
+ + " <Direction>NORTH</Direction>\n"
+ + " <RouteHeading>First Mall</RouteHeading>\n"
+ + " </Route>\n"
+ + " <Route>\n"
+ + " <RouteNo>123</RouteNo>\n"
+ + " <DirectionID>1</DirectionID>\n"
+ + " <Direction>SOUTH</Direction>\n"
+ + " <RouteHeading>Second Mall</RouteHeading>\n"
+ + " </Route>\n"
+ + " </Routes>\n"
+ + " </GetRouteSummaryForStopResult>\n"
+ + " </GetRouteSummaryForStopResponse>\n"
+ + " </soap:Body>\n"
+ + "</soap:Envelope>\n";
+ private static final String NEXT_TRIPS_FOR_STOP =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ + "<soap:Body><GetRouteSummaryForStopResponse xmlns=\"http://octranspo.com\">"
+ + "<GetRouteSummaryForStopResult>"
+ + "<StopNo xmlns=\"http://tempuri.org/\">2438</StopNo>"
+ + "<StopDescription xmlns=\"http://tempuri.org/\">BRONSON SUNNYSIDE</StopDescription>"
+ + "<Error xmlns=\"http://tempuri.org/\"/><Routes xmlns=\"http://tempuri.org/\">"
+ + "<Route><RouteNo>4</RouteNo><DirectionID>1</DirectionID><Direction>Northbound</Direction>"
+ + "<RouteHeading>Rideau C / Ctr Rideau</RouteHeading>"
+ + "<Trips>"
+ + "<Trip><TripDestination>Rideau Centre / Centre Rideau</TripDestination><TripStartTime>19:00</TripStartTime>"
+ + "<AdjustedScheduleTime>16</AdjustedScheduleTime><AdjustmentAge>0.45</AdjustmentAge><LastTripOfSchedule/>"
+ + "<BusType>4LB - IN</BusType><Latitude>45.408957</Latitude><Longitude>-75.664125</Longitude>"
+ + "<GPSSpeed>66.4</GPSSpeed></Trip>"
+ + "<Trip><TripDestination>Rideau Centre / Centre Rideau</TripDestination>"
+ + "<TripStartTime>19:30</TripStartTime><AdjustedScheduleTime>40</AdjustedScheduleTime><AdjustmentAge>-1</AdjustmentAge>"
+ + "<LastTripOfSchedule/><BusType>4LB - IN</BusType><Latitude/><Longitude/><GPSSpeed/></Trip>"
+ + "<Trip><TripDestination>Rideau Centre / Centre Rideau</TripDestination><TripStartTime>20:00</TripStartTime>"
+ + "<AdjustedScheduleTime>70</AdjustedScheduleTime><AdjustmentAge>-1</AdjustmentAge><LastTripOfSchedule/>"
+ + "<BusType>4LB - IN</BusType><Latitude/><Longitude/><GPSSpeed/></Trip>"
+ + "</Trips></Route></Routes></GetRouteSummaryForStopResult></GetRouteSummaryForStopResponse>"
+ + "</soap:Body></soap:Envelope>";
+
+ // Do the least possible parsing: check for the <Root/> element only.
+ public static class MinimalParse extends ParseResult {
+ private static final String[] INTERNAL = {};
+ private static final Object[][] EXTERNAL = {} ;
+
+ public MinimalParse() {
+ super("Root", INTERNAL, EXTERNAL);
+ }
+
+ @Override
+ public void endContents(String uri, String localName, String qName,
+ String chars, Attributes attr) throws XmlParseException
+ {
+ Assert.fail("Should not have any contents to end.");
+ }
+
+ @Override
+ public void endExternal(String uri, String localName, String qName)
+ throws XmlParseException
+ {
+ Assert.fail("Should not have any external tags to end.");
+ }
+ }
+
+ // Check that we can parse a minimal document without errors.
+ // Because there's no content being parsed (beyond the root element), there is
+ // no "correct" behaviour to assert. The test is to confirm that we
+ // don't do anything incorrect--no calls to endContent() nor endExternal(),
+ // and no exceptions thrown along the way.
+ @Test
+ public void test_withMinimalParse() throws IOException, SAXException {
+ MinimalParse mp = new MinimalParse();
+ ByteArrayInputStream bais = null;
+
+ String[] data = {
+ MINIMAL_XML,
+ MINIMAL_XML_WITH_PROLOGUE,
+ XML_WITH_MINOR_CONTENT,
+ ROOT_INSIDE_SECONDARY_ELEMENT
+ };
+
+ for (String datum : data) {
+ try {
+ bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
+ XMLReader reader = XMLReaderFactory.createXMLReader();
+ ParseHandler ph = new ParseHandler(mp);
+ reader.setContentHandler(ph);
+ reader.parse(new InputSource(bais));
+ }
+ finally {
+ if (null != bais) {
+ bais.close();
+ }
+ }
+ }
+ }
+
+ // If we parse something that doesn't have the expected root element, we should generate an exception
+ @Test
+ public void test_minimalParseWithMismatchedRootElement() throws IOException {
+ MinimalParse mp = new MinimalParse();
+ ByteArrayInputStream bais = null;
+
+ String[] data = { PROLOGUE_AND_SECONDARY_ELEMENT };
+
+ for (String datum : data) {
+ try {
+ bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
+ XMLReader reader = XMLReaderFactory.createXMLReader();
+ ParseHandler ph = new ParseHandler(mp);
+ reader.setContentHandler(ph);
+ reader.parse(new InputSource(bais));
+ Assert.fail("Should have thrown an exception.");
+ }
+ catch ( SAXException se ) {
+ Throwable cause = se.getCause();
+ Assert.assertNotNull(cause);
+ Assert.assertTrue(cause instanceof MissingInfoException);
+ MissingInfoException mie = (MissingInfoException) cause;
+ Assert.assertEquals("Root", mie.getTagName());
+ }
+ finally {
+ if (null != bais) {
+ bais.close();
+ }
+ }
+ }
+ }
+
+ // Do the some simple parsing: <Root/> and some subtags that are processed internally
+ public static class SimpleParse extends ParseResult {
+ private static final String ONE = "One";
+ private static final String TWO = "Two";
+ private static final String THREE = "Three";
+
+ private static final String[] INTERNAL = {ONE, TWO, THREE};
+ private static final Object[][] EXTERNAL = {} ;
+
+ String m_one;
+ String m_two;
+ String m_three;
+
+ public SimpleParse() {
+ super("Root", INTERNAL, EXTERNAL);
+
+ m_one = m_two = m_three = null;
+ }
+
+ public String getOne() { return m_one; }
+ public String getTwo() { return m_two; }
+ public String getThree() { return m_three; }
+
+ @Override
+ public void endContents(String uri, String localName, String qName,
+ String chars, Attributes attr) throws XmlParseException
+ {
+ if (localName.equals(ONE)) {
+ m_one = chars;
+ }
+ else if (localName.equals(TWO)) {
+ m_two = chars;
+ }
+ else if (localName.equals(THREE)) {
+ m_three = chars;
+ }
+ }
+
+ @Override
+ public void endExternal(String uri, String localName, String qName)
+ throws XmlParseException
+ {
+ Assert.fail("Should not have any external tags to end.");
+ }
+ }
+
+ // Parse some XML containing subtags that are handled internally by SimpleParse
+ @Test
+ public void test_parseWithInternalSubtags() throws IOException, SAXException
+ {
+ SimpleParse sp = new SimpleParse();
+ ByteArrayInputStream bais = null;
+
+ String[] data = {
+ SIMPLE_INTERNAL_TAGS
+ };
+
+ for (String datum : data) {
+ try {
+ bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
+ XMLReader reader = XMLReaderFactory.createXMLReader();
+ ParseHandler ph = new ParseHandler(sp);
+ reader.setContentHandler(ph);
+ reader.parse(new InputSource(bais));
+
+ Assert.assertEquals("", sp.getOne());
+ Assert.assertEquals("content of two", sp.getTwo());
+ Assert.assertEquals("3", sp.getThree());
+ }
+ finally {
+ if (null != bais) {
+ bais.close();
+ }
+ }
+ }
+ }
+
+ // Parse sub-tags, handling some internally and some externally
+ public static class RouteSummaryParse extends ParseResult {
+ private static final String STOP_NO = "StopNo";
+ private static final String STOP_DESCR = "StopDescription";
+ private static final String ERROR = "Error";
+ private static final String ROUTES = "Routes";
+ private static final String ROUTE = "Route";
+
+ private static final String[] INTERNAL = {STOP_NO, STOP_DESCR, ERROR, ROUTES};
+ private static final Object[][] EXTERNAL = { {ROUTE, RouteParse.class} };
+
+ // Data gleaned from parsing
+ int m_stopNo;
+ String m_stopDescr;
+ String m_error;
+ ArrayList<RouteParse> m_routes;
+
+ public RouteSummaryParse() {
+ super("GetRouteSummaryForStopResult", INTERNAL, EXTERNAL);
+
+ m_stopNo = 0;
+ m_stopDescr = m_error = null;
+ m_routes = new ArrayList<RouteParse>();
+ }
+
+ public int getStopNo() { return m_stopNo; }
+ public String getStopDescription() { return m_stopDescr; }
+ public String getError() { return m_error; }
+ public int getNumRoutes() { return m_routes.size(); }
+ public RouteParse getRoute(int idx) { return m_routes.get(idx); }
+
+ @Override
+ public void endContents(String uri, String localName, String qName,
+ String chars, Attributes attr) throws XmlParseException
+ {
+ if (localName.equals(STOP_NO)) {
+ m_stopNo = Integer.parseInt(chars);
+ }
+ else if (localName.equals(STOP_DESCR)) {
+ m_stopDescr = chars;
+ }
+ else if (localName.equals(ERROR)) {
+ m_error = chars;
+ }
+ }
+
+ @Override
+ public void endExternal(String uri, String localName, String qName)
+ throws XmlParseException
+ {
+ if (localName.equals(ROUTE)) {
+ ParseResult[] collected = collectParsedChildren(RouteParse.class);
+ for (ParseResult pr : collected) {
+ Assert.assertTrue(pr instanceof RouteParse);
+ m_routes.add((RouteParse)pr);
+ }
+ }
+ }
+ }
+ public static class RouteParse extends ParseResult {
+ private static final String ROUTE = "Route";
+ private static final String ROUTE_NO = "RouteNo";
+ private static final String DIR_ID = "DirectionID";
+ private static final String DIR = "Direction";
+ private static final String HEADING = "RouteHeading";
+ private static final String TRIPS = "Trips";
+ private static final String TRIP = "Trip";
+
+ private static final String[] INTERNAL = {ROUTE_NO, DIR_ID, DIR, HEADING, TRIPS};
+ private static final Object[][] EXTERNAL = { {TRIP, TripParse.class} };
+
+ // Data gleaned from parsing
+ int m_routeNo;
+ int m_dirID;
+ String m_dir;
+ String m_heading;
+ ArrayList<TripParse> m_trips;
+
+ public RouteParse() {
+ super(ROUTE, INTERNAL, EXTERNAL);
+
+ m_routeNo = m_dirID = 0;
+ m_dir = m_heading = null;
+ m_trips = new ArrayList<TripParse>();
+ }
+
+ public int getRouteNo() { return m_routeNo; }
+ public int getDirectionID() { return m_dirID; }
+ public String getDirection() { return m_dir; }
+ public String getHeading() { return m_heading; }
+ public int getNumTrips() { return m_trips.size(); }
+ public TripParse getTrip(int idx) { return m_trips.get(idx); }
+
+ @Override
+ public void endContents(String uri, String localName, String qName,
+ String chars, Attributes attr) throws XmlParseException
+ {
+ if (localName.equals(ROUTE_NO)) {
+ m_routeNo = Integer.parseInt(chars);
+ }
+ else if (localName.equals(DIR_ID)) {
+ m_dirID = Integer.parseInt(chars);
+ }
+ else if (localName.equals(DIR)) {
+ m_dir = chars;
+ }
+ else if (localName.equals(HEADING)) {
+ m_heading = chars;
+ }
+ }
+
+ @Override
+ public void endExternal(String uri, String localName, String qName)
+ throws XmlParseException
+ {
+ if (localName.equals(TRIP)) {
+ ParseResult[] collected = collectParsedChildren(TripParse.class);
+ for (ParseResult pr : collected) {
+ Assert.assertTrue(pr instanceof TripParse);
+ m_trips.add((TripParse)pr);
+ }
+ }
+
+ }
+ }
+ public static class TripParse extends ParseResult {
+ private static final String TRIP = "Trip";
+ private static final String TRIP_DEST = "TripDestination";
+ private static final String TRIP_START = "TripStartTime";
+ private static final String ADJ_SCHED_TIME = "AdjustedScheduleTime";
+
+ private static final String[] INTERNAL = {TRIP_DEST, TRIP_START, ADJ_SCHED_TIME };
+ private static final Object[][] EXTERNAL = { };
+
+ // Data gleaned from parsing
+ String m_dest;
+ String m_startTime;
+ int m_adjSchedTime;;
+
+ public TripParse() {
+ super(TRIP, INTERNAL, EXTERNAL);
+
+ m_dest = m_startTime = null;
+ m_adjSchedTime = 0;
+ }
+
+ public String getDestination() { return m_dest; }
+ public String getStartTime() { return m_startTime; }
+ public int getAdjustedScheduleTime() { return m_adjSchedTime; }
+
+ @Override
+ public void endContents(String uri, String localName, String qName,
+ String chars, Attributes attr) throws XmlParseException
+ {
+ if (localName.equals(TRIP_DEST)) {
+ m_dest = chars;
+ }
+ else if (localName.equals(TRIP_START)) {
+ m_startTime = chars;
+ }
+ else if (localName.equals(ADJ_SCHED_TIME)) {
+ m_adjSchedTime = Integer.parseInt(chars);
+ }
+ }
+
+ @Override
+ public void endExternal(String uri, String localName, String qName)
+ throws XmlParseException
+ {
+ Assert.fail("Should not be attempting to parse external tags.");
+ }
+ }
+
+ // Parse some XML containing subtags that are handled both internally and externally
+ @Test
+ public void test_parseRouteSummary() throws IOException, SAXException
+ {
+ RouteSummaryParse rsp = new RouteSummaryParse();
+ ByteArrayInputStream bais = null;
+
+ try {
+ RouteParse rp;
+
+ bais = new ByteArrayInputStream(ROUTE_SUMMARY_FOR_STOP.getBytes("UTF-8"));
+ XMLReader reader = XMLReaderFactory.createXMLReader();
+ ParseHandler ph = new ParseHandler(rsp);
+ reader.setContentHandler(ph);
+ reader.parse(new InputSource(bais));
+
+ Assert.assertEquals(1234, rsp.getStopNo());
+ Assert.assertEquals("ONE-TWO-THREE-FOUR", rsp.getStopDescription());
+ Assert.assertEquals("", rsp.getError());
+
+ Assert.assertEquals(2, rsp.getNumRoutes());
+
+ rp = rsp.getRoute(0);
+ Assert.assertNotNull(rp);
+ Assert.assertEquals(123, rp.getRouteNo());
+ Assert.assertEquals(0, rp.getDirectionID());
+ Assert.assertEquals("NORTH", rp.getDirection());
+ Assert.assertEquals("First Mall", rp.getHeading());
+
+ rp = rsp.getRoute(1);
+ Assert.assertNotNull(rp);
+ Assert.assertEquals(123, rp.getRouteNo());
+ Assert.assertEquals(1, rp.getDirectionID());
+ Assert.assertEquals("SOUTH", rp.getDirection());
+ Assert.assertEquals("Second Mall", rp.getHeading());
+ }
+ finally {
+ if (null != bais) {
+ bais.close();
+ }
+ }
+ }
+
+ // Parse a 3-level external-tag hierarchy: RouteSummary contains Routes contains Trips
+ @Test
+ public void test_parseThreeLevels() throws IOException, SAXException
+ {
+ RouteSummaryParse rsp = new RouteSummaryParse();
+ ByteArrayInputStream bais = null;
+
+ try {
+ RouteParse rp;
+ TripParse tp;
+
+ bais = new ByteArrayInputStream(NEXT_TRIPS_FOR_STOP.getBytes("UTF-8"));
+ XMLReader reader = XMLReaderFactory.createXMLReader();
+ ParseHandler ph = new ParseHandler(rsp);
+ reader.setContentHandler(ph);
+ reader.parse(new InputSource(bais));
+
+ Assert.assertEquals(2438, rsp.getStopNo());
+ Assert.assertEquals("BRONSON SUNNYSIDE", rsp.getStopDescription());
+ Assert.assertEquals("", rsp.getError());
+
+ Assert.assertEquals(1, rsp.getNumRoutes());
+
+ rp = rsp.getRoute(0);
+ Assert.assertNotNull(rp);
+ Assert.assertEquals(4, rp.getRouteNo());
+ Assert.assertEquals(1, rp.getDirectionID());
+ Assert.assertEquals("Northbound", rp.getDirection());
+ Assert.assertEquals("Rideau C / Ctr Rideau", rp.getHeading());
+
+ Assert.assertEquals(3, rp.getNumTrips());
+
+ tp = rp.getTrip(0);
+ Assert.assertNotNull(tp);
+ Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
+ Assert.assertEquals("19:00", tp.getStartTime());
+ Assert.assertEquals(16, tp.getAdjustedScheduleTime());
+
+ tp = rp.getTrip(1);
+ Assert.assertNotNull(tp);
+ Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
+ Assert.assertEquals("19:30", tp.getStartTime());
+ Assert.assertEquals(40, tp.getAdjustedScheduleTime());
+
+ tp = rp.getTrip(2);
+ Assert.assertNotNull(tp);
+ Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
+ Assert.assertEquals("20:00", tp.getStartTime());
+ Assert.assertEquals(70, tp.getAdjustedScheduleTime());
+ }
+ finally {
+ if (null != bais) {
+ bais.close();
+ }
+ }
+ }
+}