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