Changeset 18817 in josm


Ignore:
Timestamp:
2023-08-22T20:05:49+02:00 (11 months ago)
Author:
taylor.smock
Message:

Fix #22652: Stop parsing gpx files when wpt elements do not have valid coordinates

This does move the parsing code out of GpxReader into GpxParser and refactors it
so that it is (hopefully) easier to understand and debug.

Location:
trunk
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/io/GpxReader.java

    r18396 r18817  
    77import java.io.InputStream;
    88import java.io.Reader;
    9 import java.time.DateTimeException;
    10 import java.util.ArrayList;
    11 import java.util.Collection;
    12 import java.util.HashMap;
    13 import java.util.LinkedList;
    14 import java.util.List;
    15 import java.util.Map;
    16 import java.util.Stack;
    179
    1810import javax.xml.parsers.ParserConfigurationException;
    1911
    20 import org.openstreetmap.josm.data.Bounds;
    21 import org.openstreetmap.josm.data.coor.LatLon;
    2212import org.openstreetmap.josm.data.gpx.GpxConstants;
    2313import org.openstreetmap.josm.data.gpx.GpxData;
    24 import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
    25 import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
    26 import org.openstreetmap.josm.data.gpx.GpxLink;
    27 import org.openstreetmap.josm.data.gpx.GpxRoute;
    28 import org.openstreetmap.josm.data.gpx.GpxTrack;
    29 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
    30 import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    31 import org.openstreetmap.josm.data.gpx.WayPoint;
    3214import org.openstreetmap.josm.tools.Logging;
    33 import org.openstreetmap.josm.tools.UncheckedParseException;
    3415import org.openstreetmap.josm.tools.Utils;
    3516import org.openstreetmap.josm.tools.XmlUtils;
    36 import org.openstreetmap.josm.tools.date.DateUtils;
    37 import org.xml.sax.Attributes;
    3817import org.xml.sax.InputSource;
    3918import org.xml.sax.SAXException;
    4019import org.xml.sax.SAXParseException;
    41 import org.xml.sax.helpers.DefaultHandler;
    4220
    4321/**
    4422 * Read a gpx file.
    45  *
     23 * <p>
    4624 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br>
    4725 * Both GPX version 1.0 and 1.1 are supported.
     
    5129public class GpxReader implements GpxConstants, IGpxReader {
    5230
    53     private enum State {
    54         INIT,
    55         GPX,
    56         METADATA,
    57         WPT,
    58         RTE,
    59         TRK,
    60         EXT,
    61         AUTHOR,
    62         LINK,
    63         TRKSEG,
    64         COPYRIGHT
    65     }
    66 
    67     private String version;
    6831    /** The resulting gpx data */
    6932    private GpxData gpxData;
    7033    private final InputSource inputSource;
    71 
    72     private class Parser extends DefaultHandler {
    73 
    74         private GpxData data;
    75         private Collection<IGpxTrackSegment> currentTrack;
    76         private Map<String, Object> currentTrackAttr;
    77         private Collection<WayPoint> currentTrackSeg;
    78         private GpxRoute currentRoute;
    79         private WayPoint currentWayPoint;
    80 
    81         private State currentState = State.INIT;
    82 
    83         private GpxLink currentLink;
    84         private GpxExtensionCollection currentExtensionCollection;
    85         private GpxExtensionCollection currentTrackExtensionCollection;
    86         private Stack<State> states;
    87         private final Stack<String[]> elements = new Stack<>();
    88 
    89         private StringBuilder accumulator = new StringBuilder();
    90 
    91         private boolean nokiaSportsTrackerBug;
    92 
    93         @Override
    94         public void startDocument() {
    95             accumulator = new StringBuilder();
    96             states = new Stack<>();
    97             data = new GpxData(true);
    98             currentExtensionCollection = new GpxExtensionCollection();
    99             currentTrackExtensionCollection = new GpxExtensionCollection();
    100         }
    101 
    102         @Override
    103         public void startPrefixMapping(String prefix, String uri) throws SAXException {
    104             data.getNamespaces().add(new XMLNamespace(prefix, uri));
    105         }
    106 
    107         private double parseCoord(Attributes atts, String key) {
    108             String val = atts.getValue(key);
    109             if (val != null) {
    110                 return parseCoord(val);
    111             } else {
    112                 // Some software do not respect GPX schema and use "minLat" / "minLon" instead of "minlat" / "minlon"
    113                 return parseCoord(atts.getValue(key.replaceFirst("l", "L")));
    114             }
    115         }
    116 
    117         private double parseCoord(String s) {
    118             if (s != null) {
    119                 try {
    120                     return Double.parseDouble(s);
    121                 } catch (NumberFormatException ex) {
    122                     Logging.trace(ex);
    123                 }
    124             }
    125             return Double.NaN;
    126         }
    127 
    128         private LatLon parseLatLon(Attributes atts) {
    129             return new LatLon(
    130                     parseCoord(atts, "lat"),
    131                     parseCoord(atts, "lon"));
    132         }
    133 
    134         @Override
    135         public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
    136             elements.push(new String[] {namespaceURI, localName, qName});
    137             switch(currentState) {
    138             case INIT:
    139                 states.push(currentState);
    140                 currentState = State.GPX;
    141                 data.creator = atts.getValue("creator");
    142                 version = atts.getValue("version");
    143                 if (version != null && version.startsWith("1.0")) {
    144                     version = "1.0";
    145                 } else if (!"1.1".equals(version)) {
    146                     // unknown version, assume 1.1
    147                     version = "1.1";
    148                 }
    149                 String schemaLocation = atts.getValue(GpxConstants.XML_URI_XSD, "schemaLocation");
    150                 if (schemaLocation != null) {
    151                     String[] schemaLocations = schemaLocation.split(" ", -1);
    152                     for (int i = 0; i < schemaLocations.length - 1; i += 2) {
    153                         final String schemaURI = schemaLocations[i];
    154                         final String schemaXSD = schemaLocations[i + 1];
    155                         data.getNamespaces().stream().filter(xml -> xml.getURI().equals(schemaURI)).forEach(xml -> {
    156                             xml.setLocation(schemaXSD);
    157                         });
    158                     }
    159                 }
    160                 break;
    161             case GPX:
    162                 switch (localName) {
    163                 case "metadata":
    164                     states.push(currentState);
    165                     currentState = State.METADATA;
    166                     break;
    167                 case "wpt":
    168                     states.push(currentState);
    169                     currentState = State.WPT;
    170                     currentWayPoint = new WayPoint(parseLatLon(atts));
    171                     break;
    172                 case "rte":
    173                     states.push(currentState);
    174                     currentState = State.RTE;
    175                     currentRoute = new GpxRoute();
    176                     break;
    177                 case "trk":
    178                     states.push(currentState);
    179                     currentState = State.TRK;
    180                     currentTrack = new ArrayList<>();
    181                     currentTrackAttr = new HashMap<>();
    182                     break;
    183                 case "extensions":
    184                     states.push(currentState);
    185                     currentState = State.EXT;
    186                     break;
    187                 case "gpx":
    188                     if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {
    189                         nokiaSportsTrackerBug = true;
    190                     }
    191                     break;
    192                 default: // Do nothing
    193                 }
    194                 break;
    195             case METADATA:
    196                 switch (localName) {
    197                 case "author":
    198                     states.push(currentState);
    199                     currentState = State.AUTHOR;
    200                     break;
    201                 case "extensions":
    202                     states.push(currentState);
    203                     currentState = State.EXT;
    204                     break;
    205                 case "copyright":
    206                     states.push(currentState);
    207                     currentState = State.COPYRIGHT;
    208                     data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));
    209                     break;
    210                 case "link":
    211                     states.push(currentState);
    212                     currentState = State.LINK;
    213                     currentLink = new GpxLink(atts.getValue("href"));
    214                     break;
    215                 case "bounds":
    216                     data.put(META_BOUNDS, new Bounds(
    217                                 parseCoord(atts, "minlat"),
    218                                 parseCoord(atts, "minlon"),
    219                                 parseCoord(atts, "maxlat"),
    220                                 parseCoord(atts, "maxlon")));
    221                     break;
    222                 default: // Do nothing
    223                 }
    224                 break;
    225             case AUTHOR:
    226                 switch (localName) {
    227                 case "link":
    228                     states.push(currentState);
    229                     currentState = State.LINK;
    230                     currentLink = new GpxLink(atts.getValue("href"));
    231                     break;
    232                 case "email":
    233                     data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));
    234                     break;
    235                 default: // Do nothing
    236                 }
    237                 break;
    238             case TRK:
    239                 switch (localName) {
    240                 case "trkseg":
    241                     states.push(currentState);
    242                     currentState = State.TRKSEG;
    243                     currentTrackSeg = new ArrayList<>();
    244                     break;
    245                 case "link":
    246                     states.push(currentState);
    247                     currentState = State.LINK;
    248                     currentLink = new GpxLink(atts.getValue("href"));
    249                     break;
    250                 case "extensions":
    251                     states.push(currentState);
    252                     currentState = State.EXT;
    253                     break;
    254                 default: // Do nothing
    255                 }
    256                 break;
    257             case TRKSEG:
    258                 switch (localName) {
    259                 case "trkpt":
    260                     states.push(currentState);
    261                     currentState = State.WPT;
    262                     currentWayPoint = new WayPoint(parseLatLon(atts));
    263                     break;
    264                 case "extensions":
    265                     states.push(currentState);
    266                     currentState = State.EXT;
    267                     break;
    268                 default: // Do nothing
    269                 }
    270                 break;
    271             case WPT:
    272                 switch (localName) {
    273                 case "link":
    274                     states.push(currentState);
    275                     currentState = State.LINK;
    276                     currentLink = new GpxLink(atts.getValue("href"));
    277                     break;
    278                 case "extensions":
    279                     states.push(currentState);
    280                     currentState = State.EXT;
    281                     break;
    282                 default: // Do nothing
    283                 }
    284                 break;
    285             case RTE:
    286                 switch (localName) {
    287                 case "link":
    288                     states.push(currentState);
    289                     currentState = State.LINK;
    290                     currentLink = new GpxLink(atts.getValue("href"));
    291                     break;
    292                 case "rtept":
    293                     states.push(currentState);
    294                     currentState = State.WPT;
    295                     currentWayPoint = new WayPoint(parseLatLon(atts));
    296                     break;
    297                 case "extensions":
    298                     states.push(currentState);
    299                     currentState = State.EXT;
    300                     break;
    301                 default: // Do nothing
    302                 }
    303                 break;
    304             case EXT:
    305                 if (states.lastElement() == State.TRK) {
    306                     currentTrackExtensionCollection.openChild(namespaceURI, qName, atts);
    307                 } else {
    308                     currentExtensionCollection.openChild(namespaceURI, qName, atts);
    309                 }
    310                 break;
    311             default: // Do nothing
    312             }
    313             accumulator.setLength(0);
    314         }
    315 
    316         @Override
    317         public void characters(char[] ch, int start, int length) {
    318             /**
    319              * Remove illegal characters generated by the Nokia Sports Tracker device.
    320              * Don't do this crude substitution for all files, since it would destroy
    321              * certain unicode characters.
    322              */
    323             if (nokiaSportsTrackerBug) {
    324                 for (int i = 0; i < ch.length; ++i) {
    325                     if (ch[i] == 1) {
    326                         ch[i] = 32;
    327                     }
    328                 }
    329                 nokiaSportsTrackerBug = false;
    330             }
    331 
    332             accumulator.append(ch, start, length);
    333         }
    334 
    335         private Map<String, Object> getAttr() {
    336             switch (currentState) {
    337             case RTE: return currentRoute.attr;
    338             case METADATA: return data.attr;
    339             case WPT: return currentWayPoint.attr;
    340             case TRK: return currentTrackAttr;
    341             default: return null;
    342             }
    343         }
    344 
    345         @SuppressWarnings("unchecked")
    346         @Override
    347         public void endElement(String namespaceURI, String localName, String qName) {
    348             elements.pop();
    349             switch (currentState) {
    350             case GPX:       // GPX 1.0
    351             case METADATA:  // GPX 1.1
    352                 switch (localName) {
    353                 case "name":
    354                     data.put(META_NAME, accumulator.toString());
    355                     break;
    356                 case "desc":
    357                     data.put(META_DESC, accumulator.toString());
    358                     break;
    359                 case "time":
    360                     data.put(META_TIME, accumulator.toString());
    361                     break;
    362                 case "keywords":
    363                     data.put(META_KEYWORDS, accumulator.toString());
    364                     break;
    365                 case "author":
    366                     if ("1.0".equals(version)) {
    367                         // author is a string in 1.0, but complex element in 1.1
    368                         data.put(META_AUTHOR_NAME, accumulator.toString());
    369                     }
    370                     break;
    371                 case "email":
    372                     if ("1.0".equals(version)) {
    373                         data.put(META_AUTHOR_EMAIL, accumulator.toString());
    374                     }
    375                     break;
    376                 case "url":
    377                 case "urlname":
    378                     data.put(localName, accumulator.toString());
    379                     break;
    380                 case "metadata":
    381                 case "gpx":
    382                     if ((currentState == State.METADATA && "metadata".equals(localName)) ||
    383                         (currentState == State.GPX && "gpx".equals(localName))) {
    384                         convertUrlToLink(data.attr);
    385                         data.getExtensions().addAll(currentExtensionCollection);
    386                         currentExtensionCollection.clear();
    387                         currentState = states.pop();
    388                     }
    389                     break;
    390                 case "bounds":
    391                     // do nothing, has been parsed on startElement
    392                     break;
    393                 default:
    394                 }
    395                 break;
    396             case AUTHOR:
    397                 switch (localName) {
    398                 case "author":
    399                     currentState = states.pop();
    400                     break;
    401                 case "name":
    402                     data.put(META_AUTHOR_NAME, accumulator.toString());
    403                     break;
    404                 case "email":
    405                     // do nothing, has been parsed on startElement
    406                     break;
    407                 case "link":
    408                     data.put(META_AUTHOR_LINK, currentLink);
    409                     break;
    410                 default: // Do nothing
    411                 }
    412                 break;
    413             case COPYRIGHT:
    414                 switch (localName) {
    415                 case "copyright":
    416                     currentState = states.pop();
    417                     break;
    418                 case "year":
    419                     data.put(META_COPYRIGHT_YEAR, accumulator.toString());
    420                     break;
    421                 case "license":
    422                     data.put(META_COPYRIGHT_LICENSE, accumulator.toString());
    423                     break;
    424                 default: // Do nothing
    425                 }
    426                 break;
    427             case LINK:
    428                 switch (localName) {
    429                 case "text":
    430                     currentLink.text = accumulator.toString();
    431                     break;
    432                 case "type":
    433                     currentLink.type = accumulator.toString();
    434                     break;
    435                 case "link":
    436                     if (currentLink.uri == null && !accumulator.toString().isEmpty()) {
    437                         currentLink = new GpxLink(accumulator.toString());
    438                     }
    439                     currentState = states.pop();
    440                     break;
    441                 default: // Do nothing
    442                 }
    443                 if (currentState == State.AUTHOR) {
    444                     data.put(META_AUTHOR_LINK, currentLink);
    445                 } else if (currentState != State.LINK) {
    446                     Map<String, Object> attr = getAttr();
    447                     if (attr != null && !attr.containsKey(META_LINKS)) {
    448                         attr.put(META_LINKS, new LinkedList<GpxLink>());
    449                     }
    450                     if (attr != null)
    451                         ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);
    452                 }
    453                 break;
    454             case WPT:
    455                 switch (localName) {
    456                 case "ele":
    457                 case "magvar":
    458                 case "name":
    459                 case "src":
    460                 case "geoidheight":
    461                 case "type":
    462                 case "sym":
    463                 case "url":
    464                 case "urlname":
    465                 case "cmt":
    466                 case "desc":
    467                 case "fix":
    468                     currentWayPoint.put(localName, accumulator.toString());
    469                     break;
    470                 case "hdop":
    471                 case "vdop":
    472                 case "pdop":
    473                     try {
    474                         currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));
    475                     } catch (NumberFormatException e) {
    476                         currentWayPoint.put(localName, 0f);
    477                     }
    478                     break;
    479                 case PT_TIME:
    480                     try {
    481                         currentWayPoint.setInstant(DateUtils.parseInstant(accumulator.toString()));
    482                     } catch (UncheckedParseException | DateTimeException e) {
    483                         Logging.error(e);
    484                     }
    485                     break;
    486                 case "rtept":
    487                     currentState = states.pop();
    488                     convertUrlToLink(currentWayPoint.attr);
    489                     currentRoute.routePoints.add(currentWayPoint);
    490                     break;
    491                 case "trkpt":
    492                     currentState = states.pop();
    493                     convertUrlToLink(currentWayPoint.attr);
    494                     currentTrackSeg.add(currentWayPoint);
    495                     break;
    496                 case "wpt":
    497                     currentState = states.pop();
    498                     convertUrlToLink(currentWayPoint.attr);
    499                     currentWayPoint.getExtensions().addAll(currentExtensionCollection);
    500                     data.waypoints.add(currentWayPoint);
    501                     currentExtensionCollection.clear();
    502                     break;
    503                 default: // Do nothing
    504                 }
    505                 break;
    506             case TRKSEG:
    507                 if ("trkseg".equals(localName)) {
    508                     currentState = states.pop();
    509                     if (!currentTrackSeg.isEmpty()) {
    510                         GpxTrackSegment seg = new GpxTrackSegment(currentTrackSeg);
    511                         if (!currentExtensionCollection.isEmpty()) {
    512                             seg.getExtensions().addAll(currentExtensionCollection);
    513                         }
    514                         currentTrack.add(seg);
    515                     }
    516                     currentExtensionCollection.clear();
    517                 }
    518                 break;
    519             case TRK:
    520                 switch (localName) {
    521                 case "trk":
    522                     currentState = states.pop();
    523                     convertUrlToLink(currentTrackAttr);
    524                     GpxTrack trk = new GpxTrack(new ArrayList<>(currentTrack), currentTrackAttr);
    525                     if (!currentTrackExtensionCollection.isEmpty()) {
    526                         trk.getExtensions().addAll(currentTrackExtensionCollection);
    527                     }
    528                     data.addTrack(trk);
    529                     currentTrackExtensionCollection.clear();
    530                     break;
    531                 case "name":
    532                 case "cmt":
    533                 case "desc":
    534                 case "src":
    535                 case "type":
    536                 case "number":
    537                 case "url":
    538                 case "urlname":
    539                     currentTrackAttr.put(localName, accumulator.toString());
    540                     break;
    541                 default: // Do nothing
    542                 }
    543                 break;
    544             case EXT:
    545                 if ("extensions".equals(localName)) {
    546                     currentState = states.pop();
    547                 } else if (currentExtensionCollection != null) {
    548                     String acc = accumulator.toString().trim();
    549                     if (states.lastElement() == State.TRK) {
    550                         currentTrackExtensionCollection.closeChild(qName, acc); //a segment inside the track can have an extension too
    551                     } else {
    552                         currentExtensionCollection.closeChild(qName, acc);
    553                     }
    554                 }
    555                 break;
    556             default:
    557                 switch (localName) {
    558                 case "wpt":
    559                     currentState = states.pop();
    560                     break;
    561                 case "rte":
    562                     currentState = states.pop();
    563                     convertUrlToLink(currentRoute.attr);
    564                     data.addRoute(currentRoute);
    565                     break;
    566                 default: // Do nothing
    567                 }
    568             }
    569             accumulator.setLength(0);
    570         }
    571 
    572         @Override
    573         public void endDocument() throws SAXException {
    574             if (!states.empty())
    575                 throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
    576 
    577             data.getExtensions().stream("josm", "from-server").findAny().ifPresent(ext -> {
    578                 data.fromServer = "true".equals(ext.getValue());
    579             });
    580 
    581             data.getExtensions().stream("josm", "layerPreferences").forEach(prefs -> {
    582                 prefs.getExtensions().stream("josm", "entry").forEach(prefEntry -> {
    583                     Object key = prefEntry.get("key");
    584                     Object val = prefEntry.get("value");
    585                     if (key != null && val != null) {
    586                         data.getLayerPrefs().put(key.toString(), val.toString());
    587                     }
    588                 });
    589             });
    590             data.endUpdate();
    591             gpxData = data;
    592         }
    593 
    594         /**
    595          * convert url/urlname to link element (GPX 1.0 -&gt; GPX 1.1).
    596          * @param attr attributes
    597          */
    598         private void convertUrlToLink(Map<String, Object> attr) {
    599             String url = (String) attr.get("url");
    600             String urlname = (String) attr.get("urlname");
    601             if (url != null) {
    602                 if (!attr.containsKey(META_LINKS)) {
    603                     attr.put(META_LINKS, new LinkedList<GpxLink>());
    604                 }
    605                 GpxLink link = new GpxLink(url);
    606                 link.text = urlname;
    607                 @SuppressWarnings("unchecked")
    608                 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);
    609                 links.add(link);
    610             }
    611         }
    612 
    613         void tryToFinish() throws SAXException {
    614             List<String[]> remainingElements = new ArrayList<>(elements);
    615             for (int i = remainingElements.size() - 1; i >= 0; i--) {
    616                 String[] e = remainingElements.get(i);
    617                 endElement(e[0], e[1], e[2]);
    618             }
    619             endDocument();
    620         }
    621     }
    62234
    62335    /**
     
    64658    @Override
    64759    public boolean parse(boolean tryToFinish) throws SAXException, IOException {
    648         Parser parser = new Parser();
     60        GpxParser parser = new GpxParser();
    64961        try {
    65062            XmlUtils.parseSafeSAX(inputSource, parser);
     
    66375                        message += '.';
    66476                }
    665                 if (!Utils.isBlank(parser.data.creator)) {
    666                     message += "\n" + tr("The file was created by \"{0}\".", parser.data.creator);
     77                if (!Utils.isBlank(parser.getData().creator)) {
     78                    message += "\n" + tr("The file was created by \"{0}\".", parser.getData().creator);
    66779                }
    66880                SAXException ex = new SAXException(message, e);
    669                 if (parser.data.isEmpty())
     81                if (parser.getData().isEmpty())
    67082                    throw ex;
    67183                Logging.warn(ex);
     
    67688            Logging.error(e); // broken SAXException chaining
    67789            throw new SAXException(e);
     90        } finally {
     91            if (parser.getData() != null) {
     92                this.gpxData = parser.getData();
     93            }
    67894        }
    67995    }
  • trunk/test/unit/org/openstreetmap/josm/io/GpxReaderTest.java

    r18801 r18817  
    55import static org.junit.jupiter.api.Assertions.assertThrows;
    66import static org.junit.jupiter.api.Assertions.assertTrue;
     7import static org.junit.jupiter.api.Assertions.fail;
    78
    89import java.io.ByteArrayInputStream;
     
    1415import java.util.Map;
    1516
     17import org.junit.jupiter.params.ParameterizedTest;
     18import org.junit.jupiter.params.provider.ValueSource;
    1619import org.openstreetmap.josm.TestUtils;
    1720import org.openstreetmap.josm.data.Bounds;
     
    8285    /**
    8386     * Tests invalid data.
    84      * @throws Exception always SAXException
    8587     */
    8688    @Test
    87     void testException() throws Exception {
     89    void testException() {
    8890        assertThrows(SAXException.class,
    8991                () -> new GpxReader(new ByteArrayInputStream("--foo--bar--".getBytes(StandardCharsets.UTF_8))).parse(true));
     
    100102                GpxReaderTest.parseGpxData(TestUtils.getRegressionDataFile(15634, "drumlish.gpx")).getMetaBounds());
    101103    }
     104
     105    @ParameterizedTest
     106    @ValueSource(strings = {
     107            "<gpx><wpt></wpt></gpx>",
     108    })
     109    void testIncompleteLocations(String gpx) {
     110        SAXException saxException = assertThrows(SAXException.class,
     111                () -> new GpxReader(new ByteArrayInputStream(gpx.getBytes(StandardCharsets.UTF_8))).parse(true));
     112        final String type;
     113        if ("<wpt>".regionMatches(0, gpx, 5, 4)) {
     114            type = "wpt";
     115        } else {
     116            fail("You need to add code to tell us what the exception for \"" + gpx + "\" should be");
     117            type = null;
     118        }
     119        assertEquals(type + " element does not have valid latitude and/or longitude.", saxException.getMessage());
     120    }
    102121}
Note: See TracChangeset for help on using the changeset viewer.