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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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