Subject: [PATCH] Fix #23721: JOSM hangs indefinitely if Bing API unavailable
---
Index: src/org/openstreetmap/josm/data/imagery/CachedAttributionBingAerialTileSource.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/data/imagery/CachedAttributionBingAerialTileSource.java b/src/org/openstreetmap/josm/data/imagery/CachedAttributionBingAerialTileSource.java
--- a/src/org/openstreetmap/josm/data/imagery/CachedAttributionBingAerialTileSource.java	(revision 19099)
+++ b/src/org/openstreetmap/josm/data/imagery/CachedAttributionBingAerialTileSource.java	(date 1718052284353)
@@ -1,17 +1,24 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.imagery;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.io.IOException;
 import java.io.StringReader;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
 import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
 import org.openstreetmap.gui.jmapviewer.tilesources.TileSourceInfo;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.CacheCustomContent;
 import org.openstreetmap.josm.io.NetworkManager;
@@ -80,24 +87,83 @@
 
     @Override
     protected Callable<List<Attribution>> getAttributionLoaderCallable() {
+        final AtomicReference<List<Attribution>> attributions = new AtomicReference<>();
+        final AtomicBoolean finished = new AtomicBoolean();
         return () -> {
+            final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor();
+            monitor.beginTask(tr("Attempting to fetch Bing attribution information"), ProgressMonitor.ALL_TICKS);
+            final Timer timer = new Timer(Thread.currentThread().getName() + "-timer", true);
+            timer.schedule(new AttributionTimerTask(monitor, timer, 1, attributions, finished), 0);
+            synchronized (finished) {
+                while (!finished.get() && !monitor.isCanceled()) {
+                    finished.wait(1000);
+                }
+            }
+            monitor.finishTask();
+            monitor.close();
+            return attributions.get();
+        };
+    }
+
+    /**
+     * A timer task for fetching Bing attribution information
+     */
+    private class AttributionTimerTask extends TimerTask {
+        private final ProgressMonitor monitor;
+        private final Timer timer;
+        private final int waitTimeSec;
+        private final AtomicReference<List<Attribution>> attributions;
+        private final AtomicBoolean finished;
+
+        /**
+         * Create a new task for fetching Bing attribution data
+         * @param monitor The monitor to update and use for cancellations
+         * @param timer The timer thread to add the next task to, if this task failed to fetch the attribution data
+         * @param waitTimeSec The time this task is waiting in seconds prior to execution
+         * @param attributions The reference to put attributions in
+         * @param finished Set to {@code true} when we have successfully fetched attributions <i>or</i> fetching has been cancelled.
+         */
+        AttributionTimerTask(ProgressMonitor monitor, Timer timer, int waitTimeSec,
+                             AtomicReference<List<Attribution>> attributions, AtomicBoolean finished) {
+            this.monitor = monitor;
+            this.timer = timer;
+            this.waitTimeSec = waitTimeSec;
+            this.attributions = attributions;
+            this.finished = finished;
+        }
+
+        @Override
+        public void run() {
             BingAttributionData attributionLoader = new BingAttributionData();
-            int waitTimeSec = 1;
-            while (true) {
-                try {
-                    String xml = attributionLoader.updateIfRequiredString();
-                    List<Attribution> ret = parseAttributionText(new InputSource(new StringReader(xml)));
-                    if (attributionDownloadedTask != null) {
-                        GuiHelper.runInEDT(attributionDownloadedTask);
-                        attributionDownloadedTask = null;
-                    }
-                    return ret;
-                } catch (IOException ex) {
-                    Logging.log(Logging.LEVEL_WARN, "Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.", ex);
-                    Thread.sleep(TimeUnit.SECONDS.toMillis(waitTimeSec));
-                    waitTimeSec *= 2;
+            try {
+                String xml = attributionLoader.updateIfRequiredString();
+                List<Attribution> ret;
+                try (StringReader sr = new StringReader(xml)) {
+                    ret = parseAttributionText(new InputSource(sr));
+                }
+                if (attributionDownloadedTask != null) {
+                    GuiHelper.runInEDT(attributionDownloadedTask);
+                    attributionDownloadedTask = null;
+                }
+                this.attributions.set(ret);
+                this.finished.set(true);
+            } catch (IOException ex) {
+                final String message = tr("Could not connect to Bing API. Will retry in {0} seconds.", waitTimeSec);
+                Logging.log(Logging.LEVEL_WARN, message, ex);
+                if (this.monitor.isCanceled()) {
+                    this.finished.set(true);
+                    return;
+                }
+                this.monitor.setCustomText(message);
+                this.monitor.worked(1);
+                final int newWaitTimeSec = 2 * this.waitTimeSec;
+                this.timer.schedule(new AttributionTimerTask(this.monitor, this.timer, newWaitTimeSec, this.attributions, this.finished),
+                        newWaitTimeSec * 1000L);
+            } finally {
+                synchronized (this.finished) {
+                    this.finished.notifyAll();
                 }
             }
-        };
+        }
     }
 }
