// License: WTFPL. For details, see LICENSE file. package iodb; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.io.UTFInputStreamReader; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * Parses the server response. It expects XML in UTF-8 with several <offset> * and <calibration> elements. * * @author Zverik * @license WTFPL */ public class IODBReader { private List<ImageryOffsetBase> offsets; private InputSource source; /** * Initializes the parser. This constructor creates an input source on the input * stream, so it may throw an exception (though it's highly improbable). * @param source An input stream with XML. * @throws IOException Thrown when something's wrong with the stream. */ public IODBReader(InputStream source) throws IOException { this.source = new InputSource(UTFInputStreamReader.create(source, "UTF-8")); this.offsets = new ArrayList<>(); } /** * Parses the XML input stream. Creates {@link Parser} to do it. * @return The list of offsets. * @throws SAXException Thrown when the XML is malformed. * @throws IOException Thrown when the input stream fails. */ public List<ImageryOffsetBase> parse() throws SAXException, IOException { Parser parser = new Parser(); try { SAXParserFactory factory = SAXParserFactory.newInstance(); factory.newSAXParser().parse(source, parser); return offsets; } catch (ParserConfigurationException e) { throw new SAXException(e); } } /** * The SAX handler for XML from the imagery offset server. * Calls {@link IOFields#constructObject()} for every complete object * and appends the result to offsets array. */ private class Parser extends DefaultHandler { private StringBuffer accumulator = new StringBuffer(); private IOFields fields; private boolean parsingOffset; private boolean parsingDeprecate; private SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd"); /** * Initialize all fields. */ @Override public void startDocument() throws SAXException { fields = new IOFields(); offsets.clear(); parsingOffset = false; } /** * Parses latitude and longitude from tag attributes. * It expects to find them in "lat" and "lon" attributes * as decimal degrees. Note that it does not check whether * the resulting object is valid: it may not be, especially * for locations near the Poles and 180th meridian. */ private LatLon parseLatLon(Attributes atts) { return new LatLon( Double.parseDouble(atts.getValue("lat")), Double.parseDouble(atts.getValue("lon"))); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (!parsingOffset) { if (qName.equals("offset") || qName.equals("calibration")) { parsingOffset = true; parsingDeprecate = false; fields.clear(); fields.position = parseLatLon(attributes); fields.id = Integer.parseInt(attributes.getValue("id")); if (attributes.getValue("flagged") != null && attributes.getValue("flagged").equals("yes")) fields.flagged = true; } } else { if (qName.equals("node")) { fields.geometry.add(parseLatLon(attributes)); } else if (qName.equals("imagery-position")) { fields.imageryPos = parseLatLon(attributes); } else if (qName.equals("imagery")) { String minZoom = attributes.getValue("minzoom"); String maxZoom = attributes.getValue("maxzoom"); if (minZoom != null) fields.minZoom = Integer.parseInt(minZoom); if (maxZoom != null) fields.maxZoom = Integer.parseInt(maxZoom); } else if (qName.equals("deprecated")) { parsingDeprecate = true; } } accumulator.setLength(0); } @Override public void characters(char[] ch, int start, int length) throws SAXException { if (parsingOffset) accumulator.append(ch, start, length); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (parsingOffset) { if (qName.equals("author")) { if (!parsingDeprecate) fields.author = accumulator.toString(); else fields.abandonAuthor = accumulator.toString(); } else if (qName.equals("description")) { fields.description = accumulator.toString(); } else if (qName.equals("reason") && parsingDeprecate) { fields.abandonReason = accumulator.toString(); } else if (qName.equals("date")) { try { if (!parsingDeprecate) fields.date = dateParser.parse(accumulator.toString()); else fields.abandonDate = dateParser.parse(accumulator.toString()); } catch (ParseException ex) { throw new SAXException(ex); } } else if (qName.equals("deprecated")) { parsingDeprecate = false; } else if (qName.equals("imagery")) { fields.imagery = accumulator.toString(); } else if (qName.equals("offset") || qName.equals("calibration")) { // store offset try { offsets.add(fields.constructObject()); } catch (IllegalArgumentException ex) { // On one hand, we don't care, but this situation is one // of those "it can never happen" cases. System.err.println(ex.getMessage()); } parsingOffset = false; } } } } /** * An accumulator for parsed fields. When there's enough data, it can construct * an offset object. All fields are public to deliver us from tons of getters * and setters. */ private static class IOFields { public int id; public LatLon position; public Date date; public String author; public String description; public Date abandonDate; public String abandonAuthor; public String abandonReason; public LatLon imageryPos; public String imagery; public int minZoom, maxZoom; public boolean flagged; public List<LatLon> geometry; /** * A constructor just calls {@link #clear()}. */ IOFields() { clear(); } /** * Clear all fields to <tt>null</tt> and <tt>-1</tt>. */ public void clear() { id = -1; position = null; date = null; author = null; description = null; abandonDate = null; abandonAuthor = null; abandonReason = null; imageryPos = null; imagery = null; minZoom = -1; maxZoom = -1; flagged = false; geometry = new ArrayList<>(); } /** * Creates an offset object from the fields. Also validates them, but not vigorously. * @return A new offset object. */ public ImageryOffsetBase constructObject() { if (author == null || description == null || position == null || date == null) throw new IllegalArgumentException("Not enought arguments to build an object"); ImageryOffsetBase result; if (geometry.isEmpty()) { if (imagery == null || imageryPos == null) throw new IllegalArgumentException("Both imagery and imageryPos should be specified for the offset"); result = new ImageryOffset(imagery, imageryPos); if (minZoom >= 0) ((ImageryOffset) result).setMinZoom(minZoom); if (maxZoom >= 0) ((ImageryOffset) result).setMaxZoom(maxZoom); } else { result = new CalibrationObject(geometry.toArray(new LatLon[0])); } if (id >= 0) result.setId(id); result.setBasicInfo(position, author, description, date); result.setDeprecated(abandonDate, abandonAuthor, abandonReason); if (flagged) result.setFlagged(flagged); return result; } } }