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