001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.imagery; 003 004import java.io.IOException; 005import java.io.InputStream; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Objects; 012import java.util.Stack; 013 014import javax.xml.parsers.ParserConfigurationException; 015 016import org.openstreetmap.josm.Main; 017import org.openstreetmap.josm.data.imagery.ImageryInfo; 018import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds; 019import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 020import org.openstreetmap.josm.data.imagery.Shape; 021import org.openstreetmap.josm.io.CachedFile; 022import org.openstreetmap.josm.io.UTFInputStreamReader; 023import org.openstreetmap.josm.tools.LanguageInfo; 024import org.openstreetmap.josm.tools.Utils; 025import org.xml.sax.Attributes; 026import org.xml.sax.InputSource; 027import org.xml.sax.SAXException; 028import org.xml.sax.helpers.DefaultHandler; 029 030public class ImageryReader { 031 032 private final String source; 033 034 private enum State { 035 INIT, // initial state, should always be at the bottom of the stack 036 IMAGERY, // inside the imagery element 037 ENTRY, // inside an entry 038 ENTRY_ATTRIBUTE, // note we are inside an entry attribute to collect the character data 039 PROJECTIONS, 040 CODE, 041 BOUNDS, 042 SHAPE, 043 NO_TILE, 044 METADATA, 045 UNKNOWN, // element is not recognized in the current context 046 } 047 048 public ImageryReader(String source) { 049 this.source = source; 050 } 051 052 public List<ImageryInfo> parse() throws SAXException, IOException { 053 Parser parser = new Parser(); 054 try { 055 try (InputStream in = new CachedFile(source) 056 .setMaxAge(1*CachedFile.DAYS) 057 .setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince) 058 .getInputStream()) { 059 InputSource is = new InputSource(UTFInputStreamReader.create(in)); 060 Utils.parseSafeSAX(is, parser); 061 return parser.entries; 062 } 063 } catch (SAXException e) { 064 throw e; 065 } catch (ParserConfigurationException e) { 066 Main.error(e); // broken SAXException chaining 067 throw new SAXException(e); 068 } 069 } 070 071 private static class Parser extends DefaultHandler { 072 private StringBuilder accumulator = new StringBuilder(); 073 074 private Stack<State> states; 075 076 private List<ImageryInfo> entries; 077 078 /** 079 * Skip the current entry because it has mandatory attributes 080 * that this version of JOSM cannot process. 081 */ 082 private boolean skipEntry; 083 084 private ImageryInfo entry; 085 private ImageryBounds bounds; 086 private Shape shape; 087 // language of last element, does only work for simple ENTRY_ATTRIBUTE's 088 private String lang; 089 private List<String> projections; 090 private Map<String, String> noTileHeaders; 091 private Map<String, String> metadataHeaders; 092 093 @Override 094 public void startDocument() { 095 accumulator = new StringBuilder(); 096 skipEntry = false; 097 states = new Stack<>(); 098 states.push(State.INIT); 099 entries = new ArrayList<>(); 100 entry = null; 101 bounds = null; 102 projections = null; 103 noTileHeaders = null; 104 } 105 106 @Override 107 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 108 accumulator.setLength(0); 109 State newState = null; 110 switch (states.peek()) { 111 case INIT: 112 if ("imagery".equals(qName)) { 113 newState = State.IMAGERY; 114 } 115 break; 116 case IMAGERY: 117 if ("entry".equals(qName)) { 118 entry = new ImageryInfo(); 119 skipEntry = false; 120 newState = State.ENTRY; 121 noTileHeaders = new HashMap<>(); 122 metadataHeaders = new HashMap<>(); 123 } 124 break; 125 case ENTRY: 126 if (Arrays.asList(new String[] { 127 "name", 128 "id", 129 "type", 130 "description", 131 "default", 132 "url", 133 "eula", 134 "min-zoom", 135 "max-zoom", 136 "attribution-text", 137 "attribution-url", 138 "logo-image", 139 "logo-url", 140 "terms-of-use-text", 141 "terms-of-use-url", 142 "country-code", 143 "icon", 144 "tile-size", 145 "validGeoreference", 146 "epsg4326to3857Supported", 147 }).contains(qName)) { 148 newState = State.ENTRY_ATTRIBUTE; 149 lang = atts.getValue("lang"); 150 } else if ("bounds".equals(qName)) { 151 try { 152 bounds = new ImageryBounds( 153 atts.getValue("min-lat") + ',' + 154 atts.getValue("min-lon") + ',' + 155 atts.getValue("max-lat") + ',' + 156 atts.getValue("max-lon"), ","); 157 } catch (IllegalArgumentException e) { 158 break; 159 } 160 newState = State.BOUNDS; 161 } else if ("projections".equals(qName)) { 162 projections = new ArrayList<>(); 163 newState = State.PROJECTIONS; 164 } else if ("no-tile-header".equals(qName)) { 165 noTileHeaders.put(atts.getValue("name"), atts.getValue("value")); 166 newState = State.NO_TILE; 167 } else if ("metadata-header".equals(qName)) { 168 metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key")); 169 newState = State.METADATA; 170 } 171 break; 172 case BOUNDS: 173 if ("shape".equals(qName)) { 174 shape = new Shape(); 175 newState = State.SHAPE; 176 } 177 break; 178 case SHAPE: 179 if ("point".equals(qName)) { 180 try { 181 shape.addPoint(atts.getValue("lat"), atts.getValue("lon")); 182 } catch (IllegalArgumentException e) { 183 break; 184 } 185 } 186 break; 187 case PROJECTIONS: 188 if ("code".equals(qName)) { 189 newState = State.CODE; 190 } 191 break; 192 } 193 /** 194 * Did not recognize the element, so the new state is UNKNOWN. 195 * This includes the case where we are already inside an unknown 196 * element, i.e. we do not try to understand the inner content 197 * of an unknown element, but wait till it's over. 198 */ 199 if (newState == null) { 200 newState = State.UNKNOWN; 201 } 202 states.push(newState); 203 if (newState == State.UNKNOWN && "true".equals(atts.getValue("mandatory"))) { 204 skipEntry = true; 205 } 206 } 207 208 @Override 209 public void characters(char[] ch, int start, int length) { 210 accumulator.append(ch, start, length); 211 } 212 213 @Override 214 public void endElement(String namespaceURI, String qName, String rqName) { 215 switch (states.pop()) { 216 case INIT: 217 throw new RuntimeException("parsing error: more closing than opening elements"); 218 case ENTRY: 219 if ("entry".equals(qName)) { 220 entry.setNoTileHeaders(noTileHeaders); 221 noTileHeaders = null; 222 entry.setMetadataHeaders(metadataHeaders); 223 metadataHeaders = null; 224 225 if (!skipEntry) { 226 entries.add(entry); 227 } 228 entry = null; 229 } 230 break; 231 case ENTRY_ATTRIBUTE: 232 switch(qName) { 233 case "name": 234 entry.setName(lang == null ? LanguageInfo.getJOSMLocaleCode(null) : lang, accumulator.toString()); 235 break; 236 case "description": 237 entry.setDescription(lang, accumulator.toString()); 238 break; 239 case "id": 240 entry.setId(accumulator.toString()); 241 break; 242 case "type": 243 boolean found = false; 244 for (ImageryType type : ImageryType.values()) { 245 if (Objects.equals(accumulator.toString(), type.getTypeString())) { 246 entry.setImageryType(type); 247 found = true; 248 break; 249 } 250 } 251 if (!found) { 252 skipEntry = true; 253 } 254 break; 255 case "default": 256 switch (accumulator.toString()) { 257 case "true": 258 entry.setDefaultEntry(true); 259 break; 260 case "false": 261 entry.setDefaultEntry(false); 262 break; 263 default: 264 skipEntry = true; 265 } 266 break; 267 case "url": 268 entry.setUrl(accumulator.toString()); 269 break; 270 case "eula": 271 entry.setEulaAcceptanceRequired(accumulator.toString()); 272 break; 273 case "min-zoom": 274 case "max-zoom": 275 Integer val = null; 276 try { 277 val = Integer.valueOf(accumulator.toString()); 278 } catch (NumberFormatException e) { 279 val = null; 280 } 281 if (val == null) { 282 skipEntry = true; 283 } else { 284 if ("min-zoom".equals(qName)) { 285 entry.setDefaultMinZoom(val); 286 } else { 287 entry.setDefaultMaxZoom(val); 288 } 289 } 290 break; 291 case "attribution-text": 292 entry.setAttributionText(accumulator.toString()); 293 break; 294 case "attribution-url": 295 entry.setAttributionLinkURL(accumulator.toString()); 296 break; 297 case "logo-image": 298 entry.setAttributionImage(accumulator.toString()); 299 break; 300 case "logo-url": 301 entry.setAttributionImageURL(accumulator.toString()); 302 break; 303 case "terms-of-use-text": 304 entry.setTermsOfUseText(accumulator.toString()); 305 break; 306 case "terms-of-use-url": 307 entry.setTermsOfUseURL(accumulator.toString()); 308 break; 309 case "country-code": 310 entry.setCountryCode(accumulator.toString()); 311 break; 312 case "icon": 313 entry.setIcon(accumulator.toString()); 314 break; 315 case "tile-size": 316 Integer tileSize = null; 317 try { 318 tileSize = Integer.valueOf(accumulator.toString()); 319 } catch (NumberFormatException e) { 320 tileSize = null; 321 } 322 if (tileSize == null) { 323 skipEntry = true; 324 } else { 325 entry.setTileSize(tileSize.intValue()); 326 } 327 break; 328 case "valid-georeference": 329 entry.setGeoreferenceValid(new Boolean(accumulator.toString())); 330 break; 331 case "epsg4326to3857Supported": 332 entry.setEpsg4326To3857Supported(new Boolean(accumulator.toString())); 333 break; 334 } 335 break; 336 case BOUNDS: 337 entry.setBounds(bounds); 338 bounds = null; 339 break; 340 case SHAPE: 341 bounds.addShape(shape); 342 shape = null; 343 break; 344 case CODE: 345 projections.add(accumulator.toString()); 346 break; 347 case PROJECTIONS: 348 entry.setServerProjections(projections); 349 projections = null; 350 break; 351 case NO_TILE: 352 break; 353 354 } 355 } 356 } 357}