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, 6 weeks 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.