Improve XML parsing to handle attributes as well.
[cfb.git] / test / net / jaekl / qd / xml / ParseResultTest.java
1 // Copyright (C) 2004, 2015 Christian Jaekl
2
3 package net.jaekl.qd.xml;
4
5 import java.io.ByteArrayInputStream;
6 import java.io.IOException;
7 import java.util.ArrayList;
8 import java.util.Locale;
9
10 import org.junit.Assert;
11
12 import org.junit.Test;
13 import org.xml.sax.Attributes;
14 import org.xml.sax.InputSource;
15 import org.xml.sax.SAXException;
16 import org.xml.sax.XMLReader;
17 import org.xml.sax.helpers.XMLReaderFactory;
18
19 public class ParseResultTest {
20         // Some samples of XML that we're going to (try to) parse\
21         private static final String MINIMAL_XML = 
22                         "<Root/>";
23         private static final String MINIMAL_XML_WITH_PROLOGUE = 
24                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root/>";
25         private static final String XML_WITH_MINOR_CONTENT = 
26                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root><One/></Root>";
27         private static final String ROOT_INSIDE_SECONDARY_ELEMENT = 
28                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Secondary><Root><One/></Root></Secondary>";
29         private static final String PROLOGUE_AND_SECONDARY_ELEMENT = 
30                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Secondary/>";
31         private static final String SIMPLE_INTERNAL_TAGS = 
32                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Root><One/><Two>content of two</Two><Three>3</Three></Root>";
33         private static final String ROUTE_SUMMARY_FOR_STOP = 
34                         "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
35                                         + "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" 
36                                         +  "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n" 
37                                         +  "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"
38                                         + "     <soap:Body>\n"
39                                         + "             <GetRouteSummaryForStopResponse xmlns=\"http://octranspo.com\">\n"
40                                         + "                     <GetRouteSummaryForStopResult>\n"
41                                         + "                             <StopNo xmlns=\"http://tempuri.org/\">1234</StopNo>\n"
42                                         + "                             <StopDescription" 
43                                         + "                              xmlns=\"http://tempuri.org/\">ONE-TWO-THREE-FOUR</StopDescription>\n"
44                                         + "                             <Error xmlns=\"http://tempuri.org/\"/>\n"
45                                         + "                             <Routes xmlns=\"http://tempuri.org/\">\n"
46                                         + "                                     <Route>\n"
47                                         + "                                             <RouteNo>123</RouteNo>\n"
48                                         + "                                             <DirectionID>0</DirectionID>\n"
49                                         + "                                             <Direction>NORTH</Direction>\n"
50                                         + "                                             <RouteHeading>First Mall</RouteHeading>\n"
51                                         + "                                     </Route>\n"
52                                         + "                                     <Route>\n"
53                                         + "                                             <RouteNo>123</RouteNo>\n"
54                                         + "                                             <DirectionID>1</DirectionID>\n"
55                                         + "                                             <Direction>SOUTH</Direction>\n"
56                                         + "                                             <RouteHeading>Second Mall</RouteHeading>\n"
57                                         + "                                     </Route>\n"
58                                         + "                             </Routes>\n"
59                                         + "                     </GetRouteSummaryForStopResult>\n"
60                                         + "             </GetRouteSummaryForStopResponse>\n"
61                                         + "     </soap:Body>\n"
62                                         + "</soap:Envelope>\n";
63         private static final String NEXT_TRIPS_FOR_STOP = 
64                         "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
65                         + "<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\">"
66                         + "<soap:Body><GetRouteSummaryForStopResponse xmlns=\"http://octranspo.com\">"
67                         + "<GetRouteSummaryForStopResult>"
68                         + "<StopNo xmlns=\"http://tempuri.org/\">2438</StopNo>"
69                         + "<StopDescription xmlns=\"http://tempuri.org/\">BRONSON SUNNYSIDE</StopDescription>"
70                         + "<Error xmlns=\"http://tempuri.org/\"/><Routes xmlns=\"http://tempuri.org/\">"
71                         + "<Route><RouteNo>4</RouteNo><DirectionID>1</DirectionID><Direction>Northbound</Direction>"
72                         + "<RouteHeading>Rideau C / Ctr Rideau</RouteHeading>"
73                         + "<Trips>"
74                         + "<Trip ghost=\"false\"><TripDestination>Rideau Centre / Centre Rideau</TripDestination><TripStartTime>19:00</TripStartTime>"
75                         + "<AdjustedScheduleTime>16</AdjustedScheduleTime><AdjustmentAge>0.45</AdjustmentAge><LastTripOfSchedule/>"
76                         + "<BusType>4LB - IN</BusType><Latitude>45.408957</Latitude><Longitude>-75.664125</Longitude>"
77                         + "<GPSSpeed>66.4</GPSSpeed></Trip>"
78                         + "<Trip ghost=\"true\"><TripDestination>Rideau Centre / Centre Rideau</TripDestination>"
79                         + "<TripStartTime>19:30</TripStartTime><AdjustedScheduleTime>40</AdjustedScheduleTime><AdjustmentAge>-1</AdjustmentAge>"
80                         + "<LastTripOfSchedule/><BusType>4LB - IN</BusType><Latitude/><Longitude/><GPSSpeed/></Trip>"
81                         + "<Trip ghost=\"true\"><TripDestination>Rideau Centre / Centre Rideau</TripDestination><TripStartTime>20:00</TripStartTime>"
82                         + "<AdjustedScheduleTime>70</AdjustedScheduleTime><AdjustmentAge>-1</AdjustmentAge><LastTripOfSchedule/>"
83                         + "<BusType>4LB - IN</BusType><Latitude/><Longitude/><GPSSpeed/></Trip>"
84                         + "</Trips></Route></Routes></GetRouteSummaryForStopResult></GetRouteSummaryForStopResponse>"
85                         + "</soap:Body></soap:Envelope>";
86         
87         // Do the least possible parsing:  check for the <Root/> element only.
88         public static class MinimalParse extends ParseResult {
89                 private static final String[] INTERNAL = {};
90                 private static final Object[][] EXTERNAL = {} ;
91                 
92                 public MinimalParse() {
93                         super("Root", INTERNAL, EXTERNAL);
94                 }
95
96                 @Override
97                 public void endContents(String uri, String localName, String qName,
98                                 String chars) throws XmlParseException 
99                 {
100                         Assert.fail("Should not have any contents to end.");
101                 }
102
103                 @Override
104                 public void endExternal(String uri, String localName, String qName)
105                                 throws XmlParseException 
106                 {
107                         Assert.fail("Should not have any external tags to end.");
108                 }
109         }
110         
111         // Check that we can parse a minimal document without errors.
112         // Because there's no content being parsed (beyond the root element), there is 
113         // no "correct" behaviour to assert.  The test is to confirm that we 
114         // don't do anything incorrect--no calls to endContent() nor endExternal(),
115         // and no exceptions thrown along the way.
116         @Test
117         public void test_withMinimalParse() throws IOException, SAXException {
118                 MinimalParse mp = new MinimalParse();
119                 ByteArrayInputStream bais = null;
120                 
121                 String[] data = { 
122                                         MINIMAL_XML, 
123                                         MINIMAL_XML_WITH_PROLOGUE, 
124                                         XML_WITH_MINOR_CONTENT,
125                                         ROOT_INSIDE_SECONDARY_ELEMENT 
126                                 };
127                 
128                 for (String datum : data) {
129                         try {
130                                 bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
131                                 XMLReader reader = XMLReaderFactory.createXMLReader();
132                                 ParseHandler ph = new ParseHandler(mp);
133                                 reader.setContentHandler(ph);
134                                 reader.parse(new InputSource(bais));
135                         }
136                         finally {
137                                 if (null != bais) { 
138                                         bais.close();
139                                 }
140                         }
141                 }
142         }
143         
144         // If we parse something that doesn't have the expected root element, we should generate an exception
145         @Test
146         public void test_minimalParseWithMismatchedRootElement() throws IOException {
147                 MinimalParse mp = new MinimalParse();
148                 ByteArrayInputStream bais = null;
149                 
150                 String[] data = { PROLOGUE_AND_SECONDARY_ELEMENT };
151                 
152                 for (String datum : data) {
153                         try {
154                                 bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
155                                 XMLReader reader = XMLReaderFactory.createXMLReader();
156                                 ParseHandler ph = new ParseHandler(mp);
157                                 reader.setContentHandler(ph);
158                                 reader.parse(new InputSource(bais));
159                                 Assert.fail("Should have thrown an exception.");
160                         }
161                         catch ( SAXException se ) {
162                                 Throwable cause = se.getCause();
163                                 Assert.assertNotNull(cause);
164                                 Assert.assertTrue(cause instanceof MissingInfoException);
165                                 MissingInfoException mie = (MissingInfoException) cause;
166                                 Assert.assertEquals("Root", mie.getTagName());
167                         }
168                         finally {
169                                 if (null != bais) { 
170                                         bais.close();
171                                 }
172                         }
173                 }
174         }
175         
176         // Do the some simple parsing:  <Root/> and some subtags that are processed internally
177         public static class SimpleParse extends ParseResult {
178                 private static final String ONE = "One";
179                 private static final String TWO = "Two";
180                 private static final String THREE = "Three";
181                 
182                 private static final String[] INTERNAL = {ONE, TWO, THREE};
183                 private static final Object[][] EXTERNAL = {} ;
184                 
185                 String m_one;
186                 String m_two;
187                 String m_three;
188                 
189                 public SimpleParse() {
190                         super("Root", INTERNAL, EXTERNAL);
191                         
192                         m_one = m_two = m_three = null;
193                 }
194                 
195                 public String getOne() { return m_one; }
196                 public String getTwo() { return m_two; }
197                 public String getThree() { return m_three; }
198
199                 @Override
200                 public void endContents(String uri, String localName, String qName,
201                                 String chars) throws XmlParseException 
202                 {
203                         if (localName.equals(ONE)) {
204                                 m_one = chars;
205                         }
206                         else if (localName.equals(TWO)) {
207                                 m_two = chars;
208                         }
209                         else if (localName.equals(THREE)) {
210                                 m_three = chars;
211                         }
212                 }
213
214                 @Override
215                 public void endExternal(String uri, String localName, String qName)
216                                 throws XmlParseException 
217                 {
218                         Assert.fail("Should not have any external tags to end.");
219                 }
220         }
221         
222         // Parse some XML containing subtags that are handled internally by SimpleParse
223         @Test
224         public void test_parseWithInternalSubtags() throws IOException, SAXException 
225         {
226                 SimpleParse sp = new SimpleParse();
227                 ByteArrayInputStream bais = null;
228                 
229                 String[] data = {
230                                         SIMPLE_INTERNAL_TAGS
231                                 };
232                 
233                 for (String datum : data) {
234                         try {
235                                 bais = new ByteArrayInputStream(datum.getBytes("UTF-8"));
236                                 XMLReader reader = XMLReaderFactory.createXMLReader();
237                                 ParseHandler ph = new ParseHandler(sp);
238                                 reader.setContentHandler(ph);
239                                 reader.parse(new InputSource(bais));
240                                 
241                                 Assert.assertEquals("", sp.getOne());
242                                 Assert.assertEquals("content of two", sp.getTwo());
243                                 Assert.assertEquals("3", sp.getThree());
244                         }
245                         finally {
246                                 if (null != bais) { 
247                                         bais.close();
248                                 }
249                         }
250                 }
251         }
252         
253         // Parse sub-tags, handling some internally and some externally
254         public static class RouteSummaryParse extends ParseResult {
255                 private static final String STOP_NO = "StopNo";
256                 private static final String STOP_DESCR = "StopDescription";
257                 private static final String ERROR = "Error";
258                 private static final String ROUTES = "Routes";
259                 private static final String ROUTE = "Route";
260                 
261                 private static final String[] INTERNAL = {STOP_NO, STOP_DESCR, ERROR, ROUTES};
262                 private static final Object[][] EXTERNAL = { {ROUTE, RouteParse.class} };
263                 
264                 // Data gleaned from parsing
265                 int m_stopNo;
266                 String m_stopDescr;
267                 String m_error;
268                 ArrayList<RouteParse> m_routes;
269                 
270                 public RouteSummaryParse() {
271                         super("GetRouteSummaryForStopResult", INTERNAL, EXTERNAL);
272                         
273                         m_stopNo = 0;
274                         m_stopDescr = m_error = null;
275                         m_routes = new ArrayList<RouteParse>();
276                 }
277                 
278                 public int getStopNo() { return m_stopNo; }
279                 public String getStopDescription() { return m_stopDescr; }
280                 public String getError() { return m_error; }
281                 public int getNumRoutes() { return m_routes.size(); }
282                 public RouteParse getRoute(int idx) { return m_routes.get(idx); }
283
284                 @Override
285                 public void endContents(String uri, String localName, String qName,     String chars) throws XmlParseException 
286                 {
287                         if (localName.equals(STOP_NO)) {
288                                 m_stopNo = Integer.parseInt(chars);
289                         }
290                         else if (localName.equals(STOP_DESCR)) {
291                                 m_stopDescr = chars;
292                         }
293                         else if (localName.equals(ERROR)) {
294                                 m_error = chars;
295                         }
296                 }
297
298                 @Override
299                 public void endExternal(String uri, String localName, String qName)
300                                 throws XmlParseException 
301                 {
302                         if (localName.equals(ROUTE)) {
303                                 ParseResult[] collected = collectParsedChildren(RouteParse.class);
304                                 for (ParseResult pr : collected) {
305                                         Assert.assertTrue(pr instanceof RouteParse);
306                                         m_routes.add((RouteParse)pr);
307                                 }
308                         }
309                 }
310         }
311         public static class RouteParse extends ParseResult {
312                 private static final String ROUTE = "Route";
313                 private static final String ROUTE_NO = "RouteNo";
314                 private static final String DIR_ID = "DirectionID";
315                 private static final String DIR = "Direction";
316                 private static final String HEADING = "RouteHeading";
317                 private static final String TRIPS = "Trips";
318                 private static final String TRIP = "Trip";
319                 
320                 private static final String[] INTERNAL = {ROUTE_NO, DIR_ID, DIR, HEADING, TRIPS};
321                 private static final Object[][] EXTERNAL = { {TRIP, TripParse.class} };
322                 
323                 // Data gleaned from parsing
324                 int m_routeNo;
325                 int m_dirID;
326                 String m_dir;
327                 String m_heading;
328                 ArrayList<TripParse> m_trips;
329                 
330                 public RouteParse() {
331                         super(ROUTE, INTERNAL, EXTERNAL);
332                         
333                         m_routeNo = m_dirID = 0;
334                         m_dir = m_heading = null;
335                         m_trips = new ArrayList<TripParse>();
336                 }
337                 
338                 public int getRouteNo() { return m_routeNo; }
339                 public int getDirectionID() { return m_dirID; }
340                 public String getDirection() { return m_dir; }
341                 public String getHeading() { return m_heading; }
342                 public int getNumTrips() { return m_trips.size(); }
343                 public TripParse getTrip(int idx) { return m_trips.get(idx); }
344
345                 @Override
346                 public void endContents(String uri, String localName, String qName,
347                                 String chars) throws XmlParseException 
348                 {
349                         if (localName.equals(ROUTE_NO)) {
350                                 m_routeNo = Integer.parseInt(chars);
351                         }
352                         else if (localName.equals(DIR_ID)) {
353                                 m_dirID = Integer.parseInt(chars);
354                         }
355                         else if (localName.equals(DIR)) {
356                                 m_dir = chars;
357                         }
358                         else if (localName.equals(HEADING)) {
359                                 m_heading = chars;
360                         }
361                 }
362
363                 @Override
364                 public void endExternal(String uri, String localName, String qName)
365                                 throws XmlParseException 
366                 {
367                         if (localName.equals(TRIP)) {
368                                 ParseResult[] collected = collectParsedChildren(TripParse.class);
369                                 for (ParseResult pr : collected) {
370                                         Assert.assertTrue(pr instanceof TripParse);
371                                         m_trips.add((TripParse)pr);
372                                 }
373                         }
374                         
375                 }
376         }
377         public static class TripParse extends ParseResult {
378                 private static final String TRIP = "Trip";
379                 private static final String GHOST = "ghost";
380                 private static final String TRIP_DEST = "TripDestination";
381                 private static final String TRIP_START = "TripStartTime";
382                 private static final String ADJ_SCHED_TIME = "AdjustedScheduleTime";
383                 
384                 private static final String[] INTERNAL = {TRIP_DEST, TRIP_START, ADJ_SCHED_TIME };
385                 private static final Object[][] EXTERNAL = { };
386                 
387                 // Data gleaned from parsing
388                 String m_dest;
389                 String m_startTime;
390                 int m_adjSchedTime;
391                 boolean m_ghost;
392                 
393                 public TripParse() {
394                         super(TRIP, INTERNAL, EXTERNAL);
395                         
396                         m_dest = m_startTime = null;
397                         m_adjSchedTime = 0;
398                         m_ghost = false;
399                 }
400                 
401                 public String getDestination() { return m_dest; }
402                 public String getStartTime() { return m_startTime; }
403                 public int getAdjustedScheduleTime() { return m_adjSchedTime; }
404                 public boolean ghostAttrSet() { return m_ghost; }
405                 
406                 @Override
407                 public void handleMainAttributes(Attributes attr) throws MissingAttributeException
408                 {
409                         String scratch = this.getRequiredAttr(TRIP, attr, GHOST);
410                         Assert.assertNotNull(scratch);
411                         m_ghost = scratch.toLowerCase(Locale.CANADA).equals("true");
412                 }
413
414                 @Override
415                 public void endContents(String uri, String localName, String qName,
416                                 String chars) throws XmlParseException 
417                 {
418                         if (localName.equals(TRIP_DEST)) {
419                                 m_dest = chars;
420                         }
421                         else if (localName.equals(TRIP_START)) {
422                                 m_startTime = chars;
423                         }
424                         else if (localName.equals(ADJ_SCHED_TIME)) {
425                                 m_adjSchedTime = Integer.parseInt(chars);
426                         }
427                 }
428
429                 @Override
430                 public void endExternal(String uri, String localName, String qName)
431                                 throws XmlParseException 
432                 {
433                         Assert.fail("Should not be attempting to parse external tags.");
434                 }
435         }
436         
437         // Parse some XML containing subtags that are handled both internally and externally
438         @Test
439         public void test_parseRouteSummary() throws IOException, SAXException 
440         {
441                 RouteSummaryParse rsp = new RouteSummaryParse();
442                 ByteArrayInputStream bais = null;
443                 
444                 try {
445                         RouteParse rp;
446                         
447                         bais = new ByteArrayInputStream(ROUTE_SUMMARY_FOR_STOP.getBytes("UTF-8"));
448                         XMLReader reader = XMLReaderFactory.createXMLReader();
449                         ParseHandler ph = new ParseHandler(rsp);
450                         reader.setContentHandler(ph);
451                         reader.parse(new InputSource(bais));
452                         
453                         Assert.assertEquals(1234, rsp.getStopNo());
454                         Assert.assertEquals("ONE-TWO-THREE-FOUR", rsp.getStopDescription());
455                         Assert.assertEquals("", rsp.getError());
456                         
457                         Assert.assertEquals(2, rsp.getNumRoutes());
458                         
459                         rp = rsp.getRoute(0);
460                         Assert.assertNotNull(rp);
461                         Assert.assertEquals(123, rp.getRouteNo());
462                         Assert.assertEquals(0, rp.getDirectionID());
463                         Assert.assertEquals("NORTH", rp.getDirection());
464                         Assert.assertEquals("First Mall", rp.getHeading());
465                         
466                         rp = rsp.getRoute(1);
467                         Assert.assertNotNull(rp);
468                         Assert.assertEquals(123, rp.getRouteNo());
469                         Assert.assertEquals(1, rp.getDirectionID());
470                         Assert.assertEquals("SOUTH", rp.getDirection());
471                         Assert.assertEquals("Second Mall", rp.getHeading());
472                 }
473                 finally {
474                         if (null != bais) { 
475                                 bais.close();
476                         }
477                 }
478         }
479         
480         // Parse a 3-level external-tag hierarchy:  RouteSummary contains Routes contains Trips
481         @Test
482         public void test_parseThreeLevels() throws IOException, SAXException 
483         {
484                 RouteSummaryParse rsp = new RouteSummaryParse();
485                 ByteArrayInputStream bais = null;
486                 
487                 try {
488                         RouteParse rp;
489                         TripParse tp;
490                         
491                         bais = new ByteArrayInputStream(NEXT_TRIPS_FOR_STOP.getBytes("UTF-8"));
492                         XMLReader reader = XMLReaderFactory.createXMLReader();
493                         ParseHandler ph = new ParseHandler(rsp);
494                         reader.setContentHandler(ph);
495                         reader.parse(new InputSource(bais));
496                         
497                         Assert.assertEquals(2438, rsp.getStopNo());
498                         Assert.assertEquals("BRONSON SUNNYSIDE", rsp.getStopDescription());
499                         Assert.assertEquals("", rsp.getError());
500                         
501                         Assert.assertEquals(1, rsp.getNumRoutes());
502                         
503                         rp = rsp.getRoute(0);
504                         Assert.assertNotNull(rp);
505                         Assert.assertEquals(4, rp.getRouteNo());
506                         Assert.assertEquals(1, rp.getDirectionID());
507                         Assert.assertEquals("Northbound", rp.getDirection());
508                         Assert.assertEquals("Rideau C / Ctr Rideau", rp.getHeading());
509                         
510                         Assert.assertEquals(3, rp.getNumTrips());
511                         
512                         tp = rp.getTrip(0);
513                         Assert.assertNotNull(tp);
514                         Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
515                         Assert.assertEquals("19:00", tp.getStartTime());
516                         Assert.assertEquals(16, tp.getAdjustedScheduleTime());
517                         Assert.assertEquals(false, tp.ghostAttrSet());
518                         
519                         tp = rp.getTrip(1);
520                         Assert.assertNotNull(tp);
521                         Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
522                         Assert.assertEquals("19:30", tp.getStartTime());
523                         Assert.assertEquals(40, tp.getAdjustedScheduleTime());
524                         Assert.assertEquals(true, tp.ghostAttrSet());
525
526                         tp = rp.getTrip(2);
527                         Assert.assertNotNull(tp);
528                         Assert.assertEquals("Rideau Centre / Centre Rideau", tp.getDestination());
529                         Assert.assertEquals("20:00", tp.getStartTime());
530                         Assert.assertEquals(70, tp.getAdjustedScheduleTime());
531                         Assert.assertEquals(true, tp.ghostAttrSet());
532                 }
533                 finally {
534                         if (null != bais) { 
535                                 bais.close();
536                         }
537                 }
538         }
539 }