Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java
r19152 r19153 8 8 import java.util.Collection; 9 9 import java.util.Collections; 10 import java.util.HashMap; 10 11 import java.util.LinkedHashSet; 11 12 import java.util.List; … … 16 17 import java.util.concurrent.TimeUnit; 17 18 import java.util.concurrent.TimeoutException; 19 import java.util.concurrent.locks.ReadWriteLock; 20 import java.util.concurrent.locks.ReentrantReadWriteLock; 18 21 import java.util.stream.Collectors; 19 22 import java.util.stream.Stream; … … 73 76 private static final String SEARCH = "search"; 74 77 78 private static final Map<String, ReadWriteLock> layerLockMap = new HashMap<>(); 79 75 80 // Mandatory arguments 76 81 private double minlat; … … 161 166 private void download() throws RequestHandlerErrorException { 162 167 DownloadOsmTask osmTask = new DownloadOsmTask(); 168 ReadWriteLock lock = null; 169 DownloadParams settings = null; 163 170 try { 164 DownloadParamssettings = getDownloadParams();171 settings = getDownloadParams(); 165 172 166 173 if (command.equals(myCommand)) { … … 168 175 Logging.info("RemoteControl: download forbidden by preferences"); 169 176 } else { 177 // The lock ensures that a download that creates a new layer will finish before 178 // downloads that do not create a new layer. This should be thread-safe. 179 lock = obtainLock(settings); 180 // We need to ensure that we only try to download new areas 170 181 Area toDownload = null; 171 182 if (!settings.isNewLayer()) { … … 179 190 } 180 191 } 192 } catch (InterruptedException ex) { 193 Thread.currentThread().interrupt(); 194 throw new RequestHandlerErrorException(ex); 181 195 } catch (RuntimeException ex) { // NOPMD 182 196 Logging.warn("RemoteControl: Error parsing load_and_zoom remote control request:"); 183 197 Logging.error(ex); 184 198 throw new RequestHandlerErrorException(ex); 199 } finally { 200 releaseLock(settings, lock); 201 } 202 } 203 204 /** 205 * Obtain a lock to ensure that a new layer is created before downloading non-new layers 206 * @param settings The settings with the appropriate layer name; if no layer name is given, we assume that 207 * the caller doesn't care where the data goes. 208 * @return The lock to pass to {@link #releaseLock(DownloadParams, ReadWriteLock)} or {@code null} if no lock is needed. 209 * @throws InterruptedException If the lock could not be obtained. 210 */ 211 private static ReadWriteLock obtainLock(DownloadParams settings) throws InterruptedException { 212 final ReadWriteLock lock; 213 if (settings.isNewLayer() && !Utils.isEmpty(settings.getLayerName())) { 214 synchronized (layerLockMap) { 215 lock = layerLockMap.computeIfAbsent(settings.getLayerName(), k -> new ReentrantReadWriteLock()); 216 lock.writeLock().lock(); 217 } 218 } else { 219 synchronized (layerLockMap) { 220 lock = layerLockMap.get(settings.getLayerName()); 221 } 222 if (lock != null) { 223 lock.readLock().lockInterruptibly(); 224 } 225 } 226 return lock; 227 } 228 229 /** 230 * Release the lock preventing data from being downloaded into an old layer 231 * @param settings The settings with information on the new layer status 232 * @param lock The lock to unlock 233 */ 234 private static void releaseLock(DownloadParams settings, ReadWriteLock lock) { 235 if (lock != null) { 236 if (settings != null && settings.isNewLayer()) { 237 lock.writeLock().unlock(); 238 synchronized (layerLockMap) { 239 layerLockMap.remove(settings.getLayerName()); 240 } 241 } else { 242 lock.readLock().unlock(); 243 } 185 244 } 186 245 } … … 213 272 } 214 273 215 private void performDownload(DownloadOsmTask osmTask, DownloadParams settings) { 274 /** 275 * Perform the actual download; this is synchronized to ensure that we only have one download going on at a time 276 * @param osmTask The task that will show a dialog 277 * @param settings The download settings 278 * @throws RequestHandlerErrorException If there is an issue getting data 279 */ 280 private void performDownload(DownloadOsmTask osmTask, DownloadParams settings) throws RequestHandlerErrorException { 216 281 Future<?> future = MainApplication.worker.submit( 217 282 new PostDownloadHandler(osmTask, osmTask.download(settings, new Bounds(minlat, minlon, maxlat, maxlon), … … 245 310 } 246 311 }); 312 // Don't block forever, but do wait some period of time. 313 try { 314 future.get(OSM_DOWNLOAD_TIMEOUT.get(), TimeUnit.SECONDS); 315 } catch (InterruptedException e) { 316 Thread.currentThread().interrupt(); 317 throw new RequestHandlerErrorException(e); 318 } catch (TimeoutException | ExecutionException e) { 319 throw new RequestHandlerErrorException(e); 320 } 247 321 } 248 322 -
trunk/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandlerTest.java
r19152 r19153 5 5 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 6 6 import static org.junit.jupiter.api.Assertions.assertEquals; 7 import static org.junit.jupiter.api.Assertions.assertNotEquals; 7 8 import static org.junit.jupiter.api.Assertions.assertNotNull; 8 9 import static org.junit.jupiter.api.Assertions.assertNull; … … 13 14 import java.nio.charset.StandardCharsets; 14 15 import java.util.Collection; 16 import java.util.concurrent.ForkJoinPool; 17 import java.util.concurrent.ForkJoinTask; 18 import java.util.concurrent.atomic.AtomicBoolean; 15 19 16 20 import com.github.tomakehurst.wiremock.client.WireMock; 17 21 import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 18 22 import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; 23 import org.awaitility.Awaitility; 19 24 import org.junit.jupiter.api.BeforeEach; 20 25 import org.junit.jupiter.api.extension.ExtendWith; … … 24 29 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 25 30 import org.openstreetmap.josm.gui.MainApplication; 31 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 32 import org.openstreetmap.josm.gui.util.GuiHelper; 26 33 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException; 27 34 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; … … 41 48 class LoadAndZoomHandlerTest { 42 49 private static final String DEFAULT_BBOX_URL = "https://localhost/load_and_zoom?left=0&bottom=0&right=0.001&top=0.001"; 50 private static final String DEFAULT_BBOX_URL_2 = "https://localhost/load_and_zoom?left=0.00025&bottom=0.00025&right=0.00075&top=0.00125"; 43 51 private static LoadAndZoomHandler newHandler(String url) throws RequestHandlerBadRequestException { 44 52 LoadAndZoomHandler req = new LoadAndZoomHandler(); … … 65 73 " <node id=\"3\" " + common + " lat=\"0.0002\" lon=\"0.0002\"/>\n" + 66 74 "</osm>"))); 75 // The scientific notation is ok server-side. 76 wireMockRuntimeInfo.getWireMock().register(WireMock.get("/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125") 77 .willReturn(WireMock.aResponse().withBody("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 78 "<osm version=\"0.6\" generator=\"hand\" copyright=\"JOSM\" attribution=\"\" license=\"\">\n" + 79 " <bounds minlat=\"0.001\" minlon=\"0.00025\" maxlat=\"0.00125\" maxlon=\"0.00075\"/>\n" + 80 " <node id=\"4\" " + common + " lat=\"0.00111\" lon=\"0.00026\"/>\n" + 81 " <node id=\"5\" " + common + " lat=\"0.0011\" lon=\"0.00025\"/>\n" + 82 " <node id=\"6\" " + common + " lat=\"0.0012\" lon=\"0.000251\"/>\n" + 83 "</osm>"))); 67 84 } 68 85 … … 141 158 assertDoesNotThrow(handler::handle); 142 159 syncThreads(); 143 // The scientific notation is ok server-side. 144 final String mapCall = "/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125"; 145 final String commonNode = "visible=\"true\" version=\"1\" changeset=\"1\" timestamp=\"2000-01-01T00:00:00Z\" user=\"tsmock\" uid=\"1\""; 146 wireMockRuntimeInfo.getWireMock().register(WireMock.get(mapCall) 147 .willReturn(WireMock.aResponse().withBody("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 148 "<osm version=\"0.6\" generator=\"hand\" copyright=\"JOSM\" attribution=\"\" license=\"\">\n" + 149 " <bounds minlat=\"0.001\" minlon=\"0.00025\" maxlat=\"0.00125\" maxlon=\"0.00075\"/>\n" + 150 " <node id=\"4\" " + commonNode + " lat=\"0.00111\" lon=\"0.00026\"/>\n" + 151 " <node id=\"5\" " + commonNode + " lat=\"0.0011\" lon=\"0.00025\"/>\n" + 152 " <node id=\"6\" " + commonNode + " lat=\"0.0012\" lon=\"0.000251\"/>\n" + 153 "</osm>"))); 154 String request = "https://localhost/load_and_zoom?left=0.00025&bottom=0.00025&right=0.00075&top=0.00125"; 155 handler = newHandler(request); 156 assertDoesNotThrow(handler::handle); 157 syncThreads(); 158 wireMockRuntimeInfo.getWireMock().verifyThat(1, RequestPatternBuilder.newRequestPattern().withUrl(mapCall)); 160 161 handler = newHandler(DEFAULT_BBOX_URL_2); 162 assertDoesNotThrow(handler::handle); 163 syncThreads(); 164 wireMockRuntimeInfo.getWireMock().verifyThat(1, RequestPatternBuilder.newRequestPattern() 165 .withUrl("/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125")); 159 166 final DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 160 167 assertNotNull(ds); … … 227 234 assertEquals("here", ds.getChangeSetTags().get("is")); 228 235 } 236 237 /** 238 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/23821">#23821</a> 239 * @throws RequestHandlerBadRequestException If there is an issue with the handler 240 */ 241 @Test 242 void testNonRegression23821() throws RequestHandlerBadRequestException { 243 final AtomicBoolean block = new AtomicBoolean(false); 244 final Runnable runnable = () -> { 245 synchronized (block) { 246 while (!block.get()) { 247 try { 248 block.wait(); 249 } catch (InterruptedException e) { 250 Thread.currentThread().interrupt();; 251 throw new RuntimeException(e); 252 } 253 } 254 } 255 }; 256 final DataSet wrongDataset = new DataSet(); 257 MainApplication.getLayerManager().addLayer(new OsmDataLayer(wrongDataset, 258 "LoadAndZoomHandlerTest#testNonRegression23821", null)); 259 ForkJoinTask<?> task1; 260 ForkJoinTask<?> task2; 261 try { 262 GuiHelper.runInEDT(runnable); 263 MainApplication.worker.submit(runnable); 264 // The processor makes a new handler for each request 265 // It is single-threaded, so blocking on one handler would fix the problem with the other handler. 266 // But we might as well work on multi-threading, since it is easier to test. :) 267 final LoadAndZoomHandler handler1 = newHandler(DEFAULT_BBOX_URL + "&new_layer=true&layer_name=OSMData"); 268 final LoadAndZoomHandler handler2 = newHandler(DEFAULT_BBOX_URL_2 + "&new_layer=false&layer_name=OSMData"); 269 // Use a separate threads to avoid blocking on this thread 270 final ForkJoinPool pool = ForkJoinPool.commonPool(); 271 task1 = pool.submit(() -> assertDoesNotThrow(handler1::handle)); 272 273 // Make certain there is enough time for the first task to block 274 Awaitility.await().until(() -> true); 275 task2 = pool.submit(() -> assertDoesNotThrow(handler2::handle)); 276 } finally { 277 // Unblock UI/worker threads 278 synchronized (block) { 279 block.set(true); 280 block.notifyAll(); 281 } 282 } 283 284 task1.join(); 285 task2.join(); 286 287 syncThreads(); 288 assertEquals(2, MainApplication.getLayerManager().getLayers().size()); 289 final DataSet ds = MainApplication.getLayerManager().getEditDataSet(); 290 assertNotEquals(wrongDataset, ds); 291 assertTrue(wrongDataset.isEmpty()); 292 assertEquals(6, ds.allPrimitives().size()); 293 } 229 294 }
Note:
See TracChangeset
for help on using the changeset viewer.