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 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 }).contains(qName)) { 146 newState = State.ENTRY_ATTRIBUTE; 147 lang = atts.getValue("lang"); 148 } else if ("bounds".equals(qName)) { 149 try { 150 bounds = new ImageryBounds( 151 atts.getValue("min-lat") + ',' + 152 atts.getValue("min-lon") + ',' + 153 atts.getValue("max-lat") + ',' + 154 atts.getValue("max-lon"), ","); 155 } catch (IllegalArgumentException e) { 156 break; 157 } 158 newState = State.BOUNDS; 159 } else if ("projections".equals(qName)) { 160 projections = new ArrayList<>(); 161 newState = State.PROJECTIONS; 162 } else if ("no-tile-header".equals(qName)) { 163 noTileHeaders.put(atts.getValue("name"), atts.getValue("value")); 164 newState = State.NO_TILE; 165 } else if ("metadata-header".equals(qName)) { 166 metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key")); 167 newState = State.METADATA; 168 } 169 break; 170 case BOUNDS: 171 if ("shape".equals(qName)) { 172 shape = new Shape(); 173 newState = State.SHAPE; 174 } 175 break; 176 case SHAPE: 177 if ("point".equals(qName)) { 178 try { 179 shape.addPoint(atts.getValue("lat"), atts.getValue("lon")); 180 } catch (IllegalArgumentException e) { 181 break; 182 } 183 } 184 break; 185 case PROJECTIONS: 186 if ("code".equals(qName)) { 187 newState = State.CODE; 188 } 189 break; 190 } 191 /** 192 * Did not recognize the element, so the new state is UNKNOWN. 193 * This includes the case where we are already inside an unknown 194 * element, i.e. we do not try to understand the inner content 195 * of an unknown element, but wait till it's over. 196 */ 197 if (newState == null) { 198 newState = State.UNKNOWN; 199 } 200 states.push(newState); 201 if (newState == State.UNKNOWN && "true".equals(atts.getValue("mandatory"))) { 202 skipEntry = true; 203 } 204 } 205 206 @Override 207 public void characters(char[] ch, int start, int length) { 208 accumulator.append(ch, start, length); 209 } 210 211 @Override 212 public void endElement(String namespaceURI, String qName, String rqName) { 213 switch (states.pop()) { 214 case INIT: 215 throw new RuntimeException("parsing error: more closing than opening elements"); 216 case ENTRY: 217 if ("entry".equals(qName)) { 218 entry.setNoTileHeaders(noTileHeaders); 219 noTileHeaders = null; 220 entry.setMetadataHeaders(metadataHeaders); 221 metadataHeaders = null; 222 223 if (!skipEntry) { 224 entries.add(entry); 225 } 226 entry = null; 227 } 228 break; 229 case ENTRY_ATTRIBUTE: 230 switch(qName) { 231 case "name": 232 entry.setName(lang == null ? LanguageInfo.getJOSMLocaleCode(null) : lang, accumulator.toString()); 233 break; 234 case "description": 235 entry.setDescription(lang, accumulator.toString()); 236 break; 237 case "id": 238 entry.setId(accumulator.toString()); 239 break; 240 case "type": 241 boolean found = false; 242 for (ImageryType type : ImageryType.values()) { 243 if (Objects.equals(accumulator.toString(), type.getTypeString())) { 244 entry.setImageryType(type); 245 found = true; 246 break; 247 } 248 } 249 if (!found) { 250 skipEntry = true; 251 } 252 break; 253 case "default": 254 switch (accumulator.toString()) { 255 case "true": 256 entry.setDefaultEntry(true); 257 break; 258 case "false": 259 entry.setDefaultEntry(false); 260 break; 261 default: 262 skipEntry = true; 263 } 264 break; 265 case "url": 266 entry.setUrl(accumulator.toString()); 267 break; 268 case "eula": 269 entry.setEulaAcceptanceRequired(accumulator.toString()); 270 break; 271 case "min-zoom": 272 case "max-zoom": 273 Integer val = null; 274 try { 275 val = Integer.valueOf(accumulator.toString()); 276 } catch (NumberFormatException e) { 277 val = null; 278 } 279 if (val == null) { 280 skipEntry = true; 281 } else { 282 if ("min-zoom".equals(qName)) { 283 entry.setDefaultMinZoom(val); 284 } else { 285 entry.setDefaultMaxZoom(val); 286 } 287 } 288 break; 289 case "attribution-text": 290 entry.setAttributionText(accumulator.toString()); 291 break; 292 case "attribution-url": 293 entry.setAttributionLinkURL(accumulator.toString()); 294 break; 295 case "logo-image": 296 entry.setAttributionImage(accumulator.toString()); 297 break; 298 case "logo-url": 299 entry.setAttributionImageURL(accumulator.toString()); 300 break; 301 case "terms-of-use-text": 302 entry.setTermsOfUseText(accumulator.toString()); 303 break; 304 case "terms-of-use-url": 305 entry.setTermsOfUseURL(accumulator.toString()); 306 break; 307 case "country-code": 308 entry.setCountryCode(accumulator.toString()); 309 break; 310 case "icon": 311 entry.setIcon(accumulator.toString()); 312 break; 313 case "tile-size": 314 Integer tileSize = null; 315 try { 316 tileSize = Integer.valueOf(accumulator.toString()); 317 } catch (NumberFormatException e) { 318 tileSize = null; 319 } 320 if (tileSize == null) { 321 skipEntry = true; 322 } else { 323 entry.setTileSize(tileSize.intValue()); 324 } 325 break; 326 } 327 break; 328 case BOUNDS: 329 entry.setBounds(bounds); 330 bounds = null; 331 break; 332 case SHAPE: 333 bounds.addShape(shape); 334 shape = null; 335 break; 336 case CODE: 337 projections.add(accumulator.toString()); 338 break; 339 case PROJECTIONS: 340 entry.setServerProjections(projections); 341 projections = null; 342 break; 343 case NO_TILE: 344 break; 345 346 } 347 } 348 } 349}