source: josm/trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java@ 18989

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

Fix #23485: JOSM crashes when opening Imagery Preferences

  • SyncEditorLayerIndex.java now checks to see if imagery entries are valid; if not, it prints the missing fields and the standard description.
  • ImageryInfo now has isValid and getMissingFields; the latter method should only be called in tests or SyncEditorLayerIndex.
  • ImageryLayerInfo removes invalid ImageryInfo objects after parsing the source
  • Property svn:eol-style set to native
File size: 14.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Objects;
16import java.util.Set;
17import java.util.TreeSet;
18import java.util.concurrent.ExecutorService;
19import java.util.stream.Collectors;
20
21import org.openstreetmap.josm.data.StructUtils;
22import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
23import org.openstreetmap.josm.gui.PleaseWaitRunnable;
24import org.openstreetmap.josm.io.CachedFile;
25import org.openstreetmap.josm.io.NetworkManager;
26import org.openstreetmap.josm.io.imagery.ImageryReader;
27import org.openstreetmap.josm.spi.preferences.Config;
28import org.openstreetmap.josm.tools.Logging;
29import org.openstreetmap.josm.tools.Utils;
30import org.xml.sax.SAXException;
31
32/**
33 * Manages the list of imagery entries that are shown in the imagery menu.
34 */
35public class ImageryLayerInfo {
36
37 /** Unique instance */
38 public static final ImageryLayerInfo instance = new ImageryLayerInfo();
39 /** List of all usable layers */
40 private final List<ImageryInfo> layers = new ArrayList<>();
41 /** List of layer ids of all usable layers */
42 private final Map<String, ImageryInfo> layerIds = new HashMap<>();
43 /** List of all available default layers */
44 static final List<ImageryInfo> defaultLayers = new ArrayList<>();
45 /** List of all available default layers (including mirrors) */
46 static final List<ImageryInfo> allDefaultLayers = new ArrayList<>();
47 /** List of all layer ids of available default layers (including mirrors) */
48 static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>();
49
50 private static final String[] DEFAULT_LAYER_SITES = {
51 Config.getUrls().getJOSMWebsite()+"/maps%<?ids=>"
52 };
53
54 /**
55 * Returns the list of imagery layers sites.
56 * @return the list of imagery layers sites
57 * @since 7434
58 */
59 public static Collection<String> getImageryLayersSites() {
60 return Config.getPref().getList("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES));
61 }
62
63 private ImageryLayerInfo() {
64 }
65
66 /**
67 * Constructs a new {@code ImageryLayerInfo} from an existing one.
68 * @param info info to copy
69 */
70 public ImageryLayerInfo(ImageryLayerInfo info) {
71 layers.addAll(info.layers);
72 }
73
74 /**
75 * Clear the lists of layers.
76 */
77 public void clear() {
78 layers.clear();
79 layerIds.clear();
80 }
81
82 /**
83 * Loads the custom as well as default imagery entries.
84 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
85 */
86 public void load(boolean fastFail) {
87 clear();
88 List<ImageryPreferenceEntry> entries = StructUtils.getListOfStructs(
89 Config.getPref(), "imagery.entries", null, ImageryPreferenceEntry.class);
90 if (entries != null) {
91 for (ImageryPreferenceEntry prefEntry : entries) {
92 try {
93 ImageryInfo i = new ImageryInfo(prefEntry);
94 add(i);
95 } catch (IllegalArgumentException e) {
96 Logging.warn("Unable to load imagery preference entry:"+e);
97 }
98 }
99 Collections.sort(layers);
100 }
101 loadDefaults(false, null, fastFail);
102 }
103
104 /**
105 * Loads the available imagery entries.
106 *
107 * The data is downloaded from the JOSM website (or loaded from cache).
108 * Entries marked as "default" are added to the user selection, if not already present.
109 *
110 * @param clearCache if true, clear the cache and start a fresh download.
111 * @param worker executor service which will perform the loading.
112 * If null, it should be performed using a {@link PleaseWaitRunnable} in the background
113 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
114 * @since 12634
115 */
116 public void loadDefaults(boolean clearCache, ExecutorService worker, boolean fastFail) {
117 final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail);
118 if (worker == null) {
119 loader.realRun();
120 loader.finish();
121 } else {
122 worker.execute(loader);
123 }
124 }
125
126 /**
127 * Loader/updater of the available imagery entries
128 */
129 class DefaultEntryLoader extends PleaseWaitRunnable {
130
131 private final boolean clearCache;
132 private final boolean fastFail;
133 private final List<ImageryInfo> newLayers = new ArrayList<>();
134 private ImageryReader reader;
135 private boolean canceled;
136 private boolean loadError;
137
138 DefaultEntryLoader(boolean clearCache, boolean fastFail) {
139 super(tr("Update default entries"));
140 this.clearCache = clearCache;
141 this.fastFail = fastFail;
142 }
143
144 @Override
145 protected void cancel() {
146 canceled = true;
147 Utils.close(reader);
148 }
149
150 @Override
151 protected void realRun() {
152 for (String source : getImageryLayersSites()) {
153 if (canceled) {
154 return;
155 }
156 loadSource(source);
157 }
158 }
159
160 protected void loadSource(String source) {
161 boolean online = !NetworkManager.isOffline(source);
162 if (clearCache && online) {
163 CachedFile.cleanup(source);
164 }
165 try {
166 reader = new ImageryReader(source);
167 reader.setFastFail(fastFail);
168 Collection<ImageryInfo> result = reader.parse();
169 // See #23485 (remove invalid source entries)
170 result.removeIf(info -> !info.isValid());
171 newLayers.addAll(result);
172 } catch (IOException ex) {
173 loadError = true;
174 Logging.log(Logging.LEVEL_ERROR, ex);
175 } catch (SAXException ex) {
176 loadError = true;
177 Logging.error(ex);
178 }
179 }
180
181 @Override
182 protected void finish() {
183 defaultLayers.clear();
184 allDefaultLayers.clear();
185 defaultLayers.addAll(newLayers);
186 for (ImageryInfo layer : newLayers) {
187 allDefaultLayers.add(layer);
188 allDefaultLayers.addAll(layer.getMirrors());
189 }
190 defaultLayerIds.clear();
191 Collections.sort(defaultLayers);
192 Collections.sort(allDefaultLayers);
193 buildIdMap(allDefaultLayers, defaultLayerIds);
194 updateEntriesFromDefaults(!loadError);
195 buildIdMap(layers, layerIds);
196 if (!loadError && !defaultLayerIds.isEmpty()) {
197 dropOldEntries();
198 }
199 }
200 }
201
202 /**
203 * Build the mapping of unique ids to {@link ImageryInfo}s.
204 * @param lst input list
205 * @param idMap output map
206 */
207 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
208 idMap.clear();
209 Set<String> notUnique = new HashSet<>();
210 for (ImageryInfo i : lst) {
211 if (i.getId() != null) {
212 if (idMap.containsKey(i.getId())) {
213 notUnique.add(i.getId());
214 Logging.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
215 i.getId(), i.getName(), idMap.get(i.getId()).getName());
216 continue;
217 }
218 idMap.put(i.getId(), i);
219 Collection<String> old = i.getOldIds();
220 if (old != null) {
221 for (String id : old) {
222 if (idMap.containsKey(id)) {
223 Logging.error("Old Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
224 i.getId(), i.getName(), idMap.get(i.getId()).getName());
225 } else {
226 idMap.put(id, i);
227 }
228 }
229 }
230 }
231 }
232 for (String i : notUnique) {
233 idMap.remove(i);
234 }
235 }
236
237 /**
238 * Update user entries according to the list of default entries.
239 * @param dropold if <code>true</code> old entries should be removed
240 * @since 11706
241 */
242 public void updateEntriesFromDefaults(boolean dropold) {
243 // add new default entries to the user selection
244 boolean changed = false;
245 Collection<String> knownDefaults = new TreeSet<>(Config.getPref().getList("imagery.layers.default"));
246 Collection<String> newKnownDefaults = new TreeSet<>();
247 for (ImageryInfo def : defaultLayers) {
248 if (def.isDefaultEntry()) {
249 boolean isKnownDefault = false;
250 for (String entry : knownDefaults) {
251 if (entry.equals(def.getId())) {
252 isKnownDefault = true;
253 newKnownDefaults.add(entry);
254 knownDefaults.remove(entry);
255 break;
256 } else if (isSimilar(entry, def.getUrl())) {
257 isKnownDefault = true;
258 if (def.getId() != null) {
259 newKnownDefaults.add(def.getId());
260 }
261 knownDefaults.remove(entry);
262 break;
263 }
264 }
265 boolean isInUserList = false;
266 if (!isKnownDefault) {
267 if (def.getId() != null) {
268 newKnownDefaults.add(def.getId());
269 isInUserList = layers.stream().anyMatch(i -> isSimilar(def, i));
270 } else {
271 Logging.error("Default imagery ''{0}'' has no id. Skipping.", def.getName());
272 }
273 }
274 if (!isKnownDefault && !isInUserList) {
275 add(new ImageryInfo(def));
276 changed = true;
277 }
278 }
279 }
280 if (!dropold && !knownDefaults.isEmpty()) {
281 newKnownDefaults.addAll(knownDefaults);
282 }
283 Config.getPref().putList("imagery.layers.default", new ArrayList<>(newKnownDefaults));
284
285 // automatically update user entries with same id as a default entry
286 for (int i = 0; i < layers.size(); i++) {
287 ImageryInfo info = layers.get(i);
288 if (info.getId() == null) {
289 continue;
290 }
291 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
292 if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
293 layers.set(i, matchingDefault);
294 Logging.info(tr("Update imagery ''{0}''", info.getName()));
295 changed = true;
296 }
297 }
298
299 if (changed) {
300 save();
301 }
302 }
303
304 /**
305 * Drop entries with Id which do no longer exist (removed from defaults).
306 * @since 11527
307 */
308 public void dropOldEntries() {
309 List<String> drop = new ArrayList<>();
310
311 for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) {
312 if (!defaultLayerIds.containsKey(info.getKey())) {
313 remove(info.getValue());
314 drop.add(info.getKey());
315 Logging.info(tr("Drop old imagery ''{0}''", info.getValue().getName()));
316 }
317 }
318
319 if (!drop.isEmpty()) {
320 for (String id : drop) {
321 layerIds.remove(id);
322 }
323 save();
324 }
325 }
326
327 private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
328 if (iiA == null || iiA.getImageryType() != iiB.getImageryType())
329 return false;
330 if (iiA.getId() != null && iiB.getId() != null)
331 return iiA.getId().equals(iiB.getId());
332 return isSimilar(iiA.getUrl(), iiB.getUrl());
333 }
334
335 // some additional checks to respect extended URLs in preferences (legacy workaround)
336 private static boolean isSimilar(String a, String b) {
337 return Objects.equals(a, b) || (!Utils.isEmpty(a) && !Utils.isEmpty(b) && (a.contains(b) || b.contains(a)));
338 }
339
340 /**
341 * Add a new imagery entry.
342 * @param info imagery entry to add
343 */
344 public void add(ImageryInfo info) {
345 layers.add(info);
346 }
347
348 /**
349 * Remove an imagery entry.
350 * @param info imagery entry to remove
351 */
352 public void remove(ImageryInfo info) {
353 layers.remove(info);
354 }
355
356 /**
357 * Save the list of imagery entries to preferences.
358 */
359 public void save() {
360 List<ImageryPreferenceEntry> entries = layers.stream()
361 .map(ImageryPreferenceEntry::new)
362 .collect(Collectors.toList());
363 StructUtils.putListOfStructs(Config.getPref(), "imagery.entries", entries, ImageryPreferenceEntry.class);
364 }
365
366 /**
367 * List of usable layers
368 * @return unmodifiable list containing usable layers
369 */
370 public List<ImageryInfo> getLayers() {
371 return Collections.unmodifiableList(layers);
372 }
373
374 /**
375 * List of available default layers
376 * @return unmodifiable list containing available default layers
377 */
378 public List<ImageryInfo> getDefaultLayers() {
379 return Collections.unmodifiableList(defaultLayers);
380 }
381
382 /**
383 * List of all available default layers (including mirrors)
384 * @return unmodifiable list containing available default layers
385 * @since 11570
386 */
387 public List<ImageryInfo> getAllDefaultLayers() {
388 return Collections.unmodifiableList(allDefaultLayers);
389 }
390
391 public static void addLayer(ImageryInfo info) {
392 instance.add(info);
393 instance.save();
394 }
395
396 public static void addLayers(Collection<ImageryInfo> infos) {
397 for (ImageryInfo i : infos) {
398 instance.add(i);
399 }
400 instance.save();
401 Collections.sort(instance.layers);
402 }
403
404 /**
405 * Get unique id for ImageryInfo.
406 *
407 * This takes care, that no id is used twice (due to a user error)
408 * @param info the ImageryInfo to look up
409 * @return null, if there is no id or the id is used twice,
410 * the corresponding id otherwise
411 */
412 public String getUniqueId(ImageryInfo info) {
413 if (info.getId() != null && layerIds.get(info.getId()) == info) {
414 return info.getId();
415 }
416 return null;
417 }
418
419 /**
420 * Returns imagery layer info for the given id.
421 * @param id imagery layer id.
422 * @return imagery layer info for the given id, or {@code null}
423 * @since 13797
424 */
425 public ImageryInfo getLayer(String id) {
426 return layerIds.get(id);
427 }
428}
Note: See TracBrowser for help on using the repository browser.