source: josm/trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java@ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 20.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.imagery;
3
4import java.io.BufferedReader;
5import java.io.Closeable;
6import java.io.IOException;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12import java.util.Objects;
13import java.util.Stack;
14
15import javax.xml.parsers.ParserConfigurationException;
16
17import org.openstreetmap.josm.data.imagery.ImageryInfo;
18import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
19import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
20import org.openstreetmap.josm.data.imagery.Shape;
21import org.openstreetmap.josm.io.CachedFile;
22import org.openstreetmap.josm.tools.HttpClient;
23import org.openstreetmap.josm.tools.JosmRuntimeException;
24import org.openstreetmap.josm.tools.LanguageInfo;
25import org.openstreetmap.josm.tools.Logging;
26import org.openstreetmap.josm.tools.MultiMap;
27import org.openstreetmap.josm.tools.Utils;
28import org.xml.sax.Attributes;
29import org.xml.sax.InputSource;
30import org.xml.sax.SAXException;
31import org.xml.sax.helpers.DefaultHandler;
32
33/**
34 * Reader to parse the list of available imagery servers from an XML definition file.
35 * <p>
36 * The format is specified in the <a href="https://josm.openstreetmap.de/wiki/Maps">JOSM wiki</a>.
37 */
38public class ImageryReader implements Closeable {
39
40 private final String source;
41 private CachedFile cachedFile;
42 private boolean fastFail;
43
44 private enum State {
45 INIT, // initial state, should always be at the bottom of the stack
46 IMAGERY, // inside the imagery element
47 ENTRY, // inside an entry
48 ENTRY_ATTRIBUTE, // note we are inside an entry attribute to collect the character data
49 PROJECTIONS, // inside projections block of an entry
50 MIRROR, // inside an mirror entry
51 MIRROR_ATTRIBUTE, // note we are inside an mirror attribute to collect the character data
52 MIRROR_PROJECTIONS, // inside projections block of an mirror entry
53 CODE,
54 BOUNDS,
55 SHAPE,
56 NO_TILE,
57 NO_TILESUM,
58 METADATA,
59 UNKNOWN, // element is not recognized in the current context
60 }
61
62 /**
63 * Constructs a {@code ImageryReader} from a given filename, URL or internal resource.
64 *
65 * @param source can be:<ul>
66 * <li>relative or absolute file name</li>
67 * <li>{@code file:///SOME/FILE} the same as above</li>
68 * <li>{@code http://...} a URL. It will be cached on disk.</li>
69 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
70 * <li>{@code josmdir://SOME/FILE} file inside josm user data directory (since r7058)</li>
71 * <li>{@code josmplugindir://SOME/FILE} file inside josm plugin directory (since r7834)</li></ul>
72 */
73 public ImageryReader(String source) {
74 this.source = source;
75 }
76
77 /**
78 * Parses imagery source.
79 * @return list of imagery info
80 * @throws SAXException if any SAX error occurs
81 * @throws IOException if any I/O error occurs
82 */
83 public List<ImageryInfo> parse() throws SAXException, IOException {
84 Parser parser = new Parser();
85 try {
86 cachedFile = new CachedFile(source);
87 cachedFile.setFastFail(fastFail);
88 try (BufferedReader in = cachedFile
89 .setMaxAge(CachedFile.DAYS)
90 .setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince)
91 .getContentReader()) {
92 InputSource is = new InputSource(in);
93 Utils.parseSafeSAX(is, parser);
94 return parser.entries;
95 }
96 } catch (SAXException e) {
97 throw e;
98 } catch (ParserConfigurationException e) {
99 Logging.error(e); // broken SAXException chaining
100 throw new SAXException(e);
101 }
102 }
103
104 private static class Parser extends DefaultHandler {
105 private static final String MAX_ZOOM = "max-zoom";
106 private static final String MIN_ZOOM = "min-zoom";
107 private static final String TILE_SIZE = "tile-size";
108 private static final String TRUE = "true";
109
110 private StringBuilder accumulator = new StringBuilder();
111
112 private Stack<State> states;
113
114 private List<ImageryInfo> entries;
115
116 /**
117 * Skip the current entry because it has mandatory attributes
118 * that this version of JOSM cannot process.
119 */
120 private boolean skipEntry;
121
122 private ImageryInfo entry;
123 /** In case of mirror parsing this contains the mirror entry */
124 private ImageryInfo mirrorEntry;
125 private ImageryBounds bounds;
126 private Shape shape;
127 // language of last element, does only work for simple ENTRY_ATTRIBUTE's
128 private String lang;
129 private List<String> projections;
130 private MultiMap<String, String> noTileHeaders;
131 private MultiMap<String, String> noTileChecksums;
132 private Map<String, String> metadataHeaders;
133
134 @Override
135 public void startDocument() {
136 accumulator = new StringBuilder();
137 skipEntry = false;
138 states = new Stack<>();
139 states.push(State.INIT);
140 entries = new ArrayList<>();
141 entry = null;
142 bounds = null;
143 projections = null;
144 noTileHeaders = null;
145 noTileChecksums = null;
146 }
147
148 @Override
149 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
150 accumulator.setLength(0);
151 State newState = null;
152 switch (states.peek()) {
153 case INIT:
154 if ("imagery".equals(qName)) {
155 newState = State.IMAGERY;
156 }
157 break;
158 case IMAGERY:
159 if ("entry".equals(qName)) {
160 entry = new ImageryInfo();
161 skipEntry = false;
162 newState = State.ENTRY;
163 noTileHeaders = new MultiMap<>();
164 noTileChecksums = new MultiMap<>();
165 metadataHeaders = new HashMap<>();
166 String best = atts.getValue("eli-best");
167 if (TRUE.equals(best)) {
168 entry.setBestMarked(true);
169 }
170 }
171 break;
172 case MIRROR:
173 if (Arrays.asList(
174 "type",
175 "url",
176 MIN_ZOOM,
177 MAX_ZOOM,
178 TILE_SIZE
179 ).contains(qName)) {
180 newState = State.MIRROR_ATTRIBUTE;
181 lang = atts.getValue("lang");
182 } else if ("projections".equals(qName)) {
183 projections = new ArrayList<>();
184 newState = State.MIRROR_PROJECTIONS;
185 }
186 break;
187 case ENTRY:
188 if (Arrays.asList(
189 "name",
190 "id",
191 "type",
192 "description",
193 "default",
194 "url",
195 "eula",
196 MIN_ZOOM,
197 MAX_ZOOM,
198 "attribution-text",
199 "attribution-url",
200 "logo-image",
201 "logo-url",
202 "terms-of-use-text",
203 "terms-of-use-url",
204 "permission-ref",
205 "country-code",
206 "icon",
207 "date",
208 TILE_SIZE,
209 "valid-georeference"
210 ).contains(qName)) {
211 newState = State.ENTRY_ATTRIBUTE;
212 lang = atts.getValue("lang");
213 } else if ("bounds".equals(qName)) {
214 try {
215 bounds = new ImageryBounds(
216 atts.getValue("min-lat") + ',' +
217 atts.getValue("min-lon") + ',' +
218 atts.getValue("max-lat") + ',' +
219 atts.getValue("max-lon"), ",");
220 } catch (IllegalArgumentException e) {
221 Logging.trace(e);
222 break;
223 }
224 newState = State.BOUNDS;
225 } else if ("projections".equals(qName)) {
226 projections = new ArrayList<>();
227 newState = State.PROJECTIONS;
228 } else if ("mirror".equals(qName)) {
229 projections = new ArrayList<>();
230 newState = State.MIRROR;
231 mirrorEntry = new ImageryInfo();
232 } else if ("no-tile-header".equals(qName)) {
233 noTileHeaders.put(atts.getValue("name"), atts.getValue("value"));
234 newState = State.NO_TILE;
235 } else if ("no-tile-checksum".equals(qName)) {
236 noTileChecksums.put(atts.getValue("type"), atts.getValue("value"));
237 newState = State.NO_TILESUM;
238 } else if ("metadata-header".equals(qName)) {
239 metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
240 newState = State.METADATA;
241 }
242 break;
243 case BOUNDS:
244 if ("shape".equals(qName)) {
245 shape = new Shape();
246 newState = State.SHAPE;
247 }
248 break;
249 case SHAPE:
250 if ("point".equals(qName)) {
251 try {
252 shape.addPoint(atts.getValue("lat"), atts.getValue("lon"));
253 } catch (IllegalArgumentException e) {
254 Logging.trace(e);
255 break;
256 }
257 }
258 break;
259 case PROJECTIONS:
260 case MIRROR_PROJECTIONS:
261 if ("code".equals(qName)) {
262 newState = State.CODE;
263 }
264 break;
265 default: // Do nothing
266 }
267 /**
268 * Did not recognize the element, so the new state is UNKNOWN.
269 * This includes the case where we are already inside an unknown
270 * element, i.e. we do not try to understand the inner content
271 * of an unknown element, but wait till it's over.
272 */
273 if (newState == null) {
274 newState = State.UNKNOWN;
275 }
276 states.push(newState);
277 if (newState == State.UNKNOWN && TRUE.equals(atts.getValue("mandatory"))) {
278 skipEntry = true;
279 }
280 }
281
282 @Override
283 public void characters(char[] ch, int start, int length) {
284 accumulator.append(ch, start, length);
285 }
286
287 @Override
288 public void endElement(String namespaceURI, String qName, String rqName) {
289 switch (states.pop()) {
290 case INIT:
291 throw new JosmRuntimeException("parsing error: more closing than opening elements");
292 case ENTRY:
293 if ("entry".equals(qName)) {
294 entry.setNoTileHeaders(noTileHeaders);
295 noTileHeaders = null;
296 entry.setNoTileChecksums(noTileChecksums);
297 noTileChecksums = null;
298 entry.setMetadataHeaders(metadataHeaders);
299 metadataHeaders = null;
300
301 if (!skipEntry) {
302 entries.add(entry);
303 }
304 entry = null;
305 }
306 break;
307 case MIRROR:
308 if (mirrorEntry != null && "mirror".equals(qName)) {
309 entry.addMirror(mirrorEntry);
310 mirrorEntry = null;
311 }
312 break;
313 case MIRROR_ATTRIBUTE:
314 if (mirrorEntry != null) {
315 switch(qName) {
316 case "type":
317 boolean found = false;
318 for (ImageryType type : ImageryType.values()) {
319 if (Objects.equals(accumulator.toString(), type.getTypeString())) {
320 mirrorEntry.setImageryType(type);
321 found = true;
322 break;
323 }
324 }
325 if (!found) {
326 mirrorEntry = null;
327 }
328 break;
329 case "url":
330 mirrorEntry.setUrl(accumulator.toString());
331 break;
332 case MIN_ZOOM:
333 case MAX_ZOOM:
334 Integer val = null;
335 try {
336 val = Integer.valueOf(accumulator.toString());
337 } catch (NumberFormatException e) {
338 val = null;
339 }
340 if (val == null) {
341 mirrorEntry = null;
342 } else {
343 if (MIN_ZOOM.equals(qName)) {
344 mirrorEntry.setDefaultMinZoom(val);
345 } else {
346 mirrorEntry.setDefaultMaxZoom(val);
347 }
348 }
349 break;
350 case TILE_SIZE:
351 Integer tileSize = null;
352 try {
353 tileSize = Integer.valueOf(accumulator.toString());
354 } catch (NumberFormatException e) {
355 tileSize = null;
356 }
357 if (tileSize == null) {
358 mirrorEntry = null;
359 } else {
360 entry.setTileSize(tileSize.intValue());
361 }
362 break;
363 default: // Do nothing
364 }
365 }
366 break;
367 case ENTRY_ATTRIBUTE:
368 switch(qName) {
369 case "name":
370 entry.setName(lang == null ? LanguageInfo.getJOSMLocaleCode(null) : lang, accumulator.toString());
371 break;
372 case "description":
373 entry.setDescription(lang, accumulator.toString());
374 break;
375 case "date":
376 entry.setDate(accumulator.toString());
377 break;
378 case "id":
379 entry.setId(accumulator.toString());
380 break;
381 case "type":
382 boolean found = false;
383 for (ImageryType type : ImageryType.values()) {
384 if (Objects.equals(accumulator.toString(), type.getTypeString())) {
385 entry.setImageryType(type);
386 found = true;
387 break;
388 }
389 }
390 if (!found) {
391 skipEntry = true;
392 }
393 break;
394 case "default":
395 switch (accumulator.toString()) {
396 case TRUE:
397 entry.setDefaultEntry(true);
398 break;
399 case "false":
400 entry.setDefaultEntry(false);
401 break;
402 default:
403 skipEntry = true;
404 }
405 break;
406 case "url":
407 entry.setUrl(accumulator.toString());
408 break;
409 case "eula":
410 entry.setEulaAcceptanceRequired(accumulator.toString());
411 break;
412 case MIN_ZOOM:
413 case MAX_ZOOM:
414 Integer val = null;
415 try {
416 val = Integer.valueOf(accumulator.toString());
417 } catch (NumberFormatException e) {
418 val = null;
419 }
420 if (val == null) {
421 skipEntry = true;
422 } else {
423 if (MIN_ZOOM.equals(qName)) {
424 entry.setDefaultMinZoom(val);
425 } else {
426 entry.setDefaultMaxZoom(val);
427 }
428 }
429 break;
430 case "attribution-text":
431 entry.setAttributionText(accumulator.toString());
432 break;
433 case "attribution-url":
434 entry.setAttributionLinkURL(accumulator.toString());
435 break;
436 case "logo-image":
437 entry.setAttributionImage(accumulator.toString());
438 break;
439 case "logo-url":
440 entry.setAttributionImageURL(accumulator.toString());
441 break;
442 case "terms-of-use-text":
443 entry.setTermsOfUseText(accumulator.toString());
444 break;
445 case "permission-ref":
446 entry.setPermissionReferenceURL(accumulator.toString());
447 break;
448 case "terms-of-use-url":
449 entry.setTermsOfUseURL(accumulator.toString());
450 break;
451 case "country-code":
452 entry.setCountryCode(accumulator.toString());
453 break;
454 case "icon":
455 entry.setIcon(accumulator.toString());
456 break;
457 case TILE_SIZE:
458 Integer tileSize = null;
459 try {
460 tileSize = Integer.valueOf(accumulator.toString());
461 } catch (NumberFormatException e) {
462 tileSize = null;
463 }
464 if (tileSize == null) {
465 skipEntry = true;
466 } else {
467 entry.setTileSize(tileSize.intValue());
468 }
469 break;
470 case "valid-georeference":
471 entry.setGeoreferenceValid(Boolean.parseBoolean(accumulator.toString()));
472 break;
473 default: // Do nothing
474 }
475 break;
476 case BOUNDS:
477 entry.setBounds(bounds);
478 bounds = null;
479 break;
480 case SHAPE:
481 bounds.addShape(shape);
482 shape = null;
483 break;
484 case CODE:
485 projections.add(accumulator.toString());
486 break;
487 case PROJECTIONS:
488 entry.setServerProjections(projections);
489 projections = null;
490 break;
491 case MIRROR_PROJECTIONS:
492 mirrorEntry.setServerProjections(projections);
493 projections = null;
494 break;
495 case NO_TILE:
496 case NO_TILESUM:
497 case METADATA:
498 case UNKNOWN:
499 default:
500 // nothing to do for these or the unknown type
501 }
502 }
503 }
504
505 /**
506 * Sets whether opening HTTP connections should fail fast, i.e., whether a
507 * {@link HttpClient#setConnectTimeout(int) low connect timeout} should be used.
508 * @param fastFail whether opening HTTP connections should fail fast
509 * @see CachedFile#setFastFail(boolean)
510 */
511 public void setFastFail(boolean fastFail) {
512 this.fastFail = fastFail;
513 }
514
515 @Override
516 public void close() throws IOException {
517 Utils.close(cachedFile);
518 }
519}
Note: See TracBrowser for help on using the repository browser.