diff --git a/src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloader.java b/src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloader.java index 6db5e84..9d23d06 100644 --- a/src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloader.java +++ b/src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloader.java @@ -8,6 +8,7 @@ import javax.json.JsonValue; import javax.json.stream.JsonParser; import java.awt.geom.Area; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.SocketTimeoutException; @@ -180,30 +181,31 @@ public class BoundingBoxMapWithAIDownloader extends BoundingBoxDownloader { // Fall back to Esri Feature Server check. They don't always indicate a json // return type. :( || this.info.getSourceType() == MapWithAIType.ESRI_FEATURE_SERVER) { - if (source.markSupported()) { - source.mark(1024); - try (JsonParser parser = Json.createParser(source)) { - while (parser.hasNext()) { - JsonParser.Event event = parser.next(); - if (event == JsonParser.Event.START_OBJECT) { - parser.getObjectStream().filter(entry -> "properties".equals(entry.getKey())) - .filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) - .map(entry -> entry.getValue().asJsonObject()) - .filter(value -> value.containsKey("exceededTransferLimit") && value - .get("exceededTransferLimit").getValueType() == JsonValue.ValueType.TRUE) - .findFirst().ifPresent(val -> { - Logging.error("Could not fully download {0}", this.downloadArea); - }); - } - } - source.reset(); - } catch (IOException e) { - throw new JosmRuntimeException(e); - } + if (!source.markSupported()) { + source = new BufferedInputStream(source); } - ds = GeoJSONReader.parseDataSet(source, progressMonitor); - if (info.getReplacementTags() != null) { - GetDataRunnable.replaceKeys(ds, info.getReplacementTags()); + source.mark(1024); + try (JsonParser parser = Json.createParser(source)) { + while (parser.hasNext()) { + JsonParser.Event event = parser.next(); + if (event == JsonParser.Event.START_OBJECT) { + parser.getObjectStream().filter(entry -> "properties".equals(entry.getKey())) + .filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) + .map(entry -> entry.getValue().asJsonObject()) + .filter(value -> value.containsKey("exceededTransferLimit") && value + .get("exceededTransferLimit").getValueType() == JsonValue.ValueType.TRUE) + .findFirst().ifPresent(val -> { + Logging.error("Could not fully download {0}", this.downloadArea); + }); + } + } + source.reset(); + ds = GeoJSONReader.parseDataSet(source, progressMonitor); + if (info.getReplacementTags() != null) { + GetDataRunnable.replaceKeys(ds, info.getReplacementTags()); + } + } catch (IOException e) { + throw new JosmRuntimeException(e); } } else { // Fall back to XML parsing diff --git a/test/unit/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloaderTest.java b/test/unit/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloaderTest.java new file mode 100644 index 0000000..82eccf1 --- /dev/null +++ b/test/unit/org/openstreetmap/josm/plugins/mapwithai/backend/BoundingBoxMapWithAIDownloaderTest.java @@ -0,0 +1,76 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.mapwithai.backend; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; + +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openstreetmap.josm.data.Bounds; +import org.openstreetmap.josm.data.osm.DataSet; +import org.openstreetmap.josm.gui.progress.NullProgressMonitor; +import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo; +import org.openstreetmap.josm.plugins.mapwithai.testutils.annotations.MapWithAIConfig; +import org.openstreetmap.josm.plugins.mapwithai.testutils.annotations.Wiremock; +import org.openstreetmap.josm.testutils.JOSMTestRules; +import org.openstreetmap.josm.testutils.annotations.BasicPreferences; +import org.openstreetmap.josm.testutils.annotations.BasicWiremock; +import org.openstreetmap.josm.testutils.annotations.HTTP; +import org.openstreetmap.josm.tools.Logging; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +/** + * Test class for {@link BoundingBoxMapWithAIDownloader} + * + * @author Taylor Smock + */ +@BasicPreferences +@HTTP +@Wiremock +@MapWithAIConfig +class BoundingBoxMapWithAIDownloaderTest { + @RegisterExtension + JOSMTestRules josmTestRules = new JOSMTestRules().fakeAPI(); + + @BasicWiremock + public WireMockServer wireMockServer; + + @ParameterizedTest + @ValueSource(strings = { "text/json", "application/json", "application/geo+json" }) + void testEsriExceededTransferLimit(String responseType) { + final Bounds downloadBounds = new Bounds(10, 10, 20, 20); + final MapWithAIInfo info = new MapWithAIInfo("testEsriExceededTransferLimit", + wireMockServer.baseUrl() + "/esriExceededLimit"); + final BoundingBoxMapWithAIDownloader boundingBoxMapWithAIDownloader = new BoundingBoxMapWithAIDownloader( + downloadBounds, info, false); + final JsonObjectBuilder objectBuilder = Json.createObjectBuilder(); + objectBuilder.add("type", "FeatureCollection"); + objectBuilder.add("properties", Json.createObjectBuilder().add("exceededTransferLimit", true)); + objectBuilder.add("features", Json.createArrayBuilder()); + wireMockServer.stubFor(WireMock + .get(boundingBoxMapWithAIDownloader + .getRequestForBbox(downloadBounds.getMinLon(), downloadBounds.getMinLat(), + downloadBounds.getMinLon(), downloadBounds.getMinLat()) + .replace(wireMockServer.baseUrl(), "")) + .willReturn(WireMock.aResponse().withHeader("Content-Type", responseType) + .withBody(objectBuilder.build().toString()))); + + Logging.clearLastErrorAndWarnings(); + final DataSet ds = assertDoesNotThrow( + () -> boundingBoxMapWithAIDownloader.parseOsm(NullProgressMonitor.INSTANCE)); + List errors = Logging.getLastErrorAndWarnings(); + assertEquals(1, errors.size(), + "We weren't handling transfer limit issues. Are we now?\n" + String.join("\n", errors)); + assertTrue(errors.get(0).contains("Could not fully download")); + assertTrue(ds.isEmpty()); + } +} diff --git a/test/unit/org/openstreetmap/josm/plugins/mapwithai/testutils/annotations/MapWithAIConfig.java b/test/unit/org/openstreetmap/josm/plugins/mapwithai/testutils/annotations/MapWithAIConfig.java new file mode 100644 index 0000000..9361395 --- /dev/null +++ b/test/unit/org/openstreetmap/josm/plugins/mapwithai/testutils/annotations/MapWithAIConfig.java @@ -0,0 +1,96 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.mapwithai.testutils.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.openstreetmap.josm.plugins.mapwithai.spi.preferences.IMapWithAIUrls; + +import com.github.tomakehurst.wiremock.WireMockServer; + +/** + * Set the MapWithAI config for the test + * + * @author Taylor Smock + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtendWith(MapWithAIConfig.MapWithAIConfigExtension.class) +@Wiremock +public @interface MapWithAIConfig { + class MapWithAIConfigExtension extends Wiremock.WiremockExtension implements BeforeEachCallback, AfterEachCallback { + @Override + public void beforeEach(ExtensionContext context) throws Exception { + org.openstreetmap.josm.plugins.mapwithai.spi.preferences.MapWithAIConfig + .setUrlsProvider(new WireMockMapWithAIUrls(getWiremock(context))); + } + + @Override + public void afterEach(ExtensionContext context) { + org.openstreetmap.josm.plugins.mapwithai.spi.preferences.MapWithAIConfig + .setUrlsProvider(new UnsupportedMapWithAIUrls( + org.openstreetmap.josm.plugins.mapwithai.spi.preferences.MapWithAIConfig.getUrls())); + } + + private static final class WireMockMapWithAIUrls implements IMapWithAIUrls { + private final WireMockServer wireMockServer; + + public WireMockMapWithAIUrls(final WireMockServer wireMockServer) { + this.wireMockServer = wireMockServer; + } + + @Override + public String getConflationServerJson() { + return this.wireMockServer.baseUrl() + "/JOSM_MapWithAI/json/conflation_servers.json"; + } + + @Override + public String getMapWithAISourcesJson() { + return this.wireMockServer.baseUrl() + "/JOSM_MapWithAI/json/sources.json"; + } + + @Override + public String getMapWithAIPaintStyle() { + return this.wireMockServer.baseUrl() + "/josmfile?page=Styles/MapWithAI&zip=1"; + } + } + + private static final class UnsupportedMapWithAIUrls implements IMapWithAIUrls { + private IMapWithAIUrls oldUrls; + + public UnsupportedMapWithAIUrls(IMapWithAIUrls oldUrls) { + this.oldUrls = oldUrls; + } + + @Override + public String getConflationServerJson() { + throw new UnsupportedOperationException("Use @MapWithAIConfig"); + } + + @Override + public String getMapWithAISourcesJson() { + throw new UnsupportedOperationException("Use @MapWithAIConfig"); + } + + @Override + public String getMapWithAIPaintStyle() { + if (this.oldUrls != null && Stream.of(Thread.currentThread().getStackTrace()) + .map(StackTraceElement::getMethodName).anyMatch("afterAll"::equals)) { + final IMapWithAIUrls urls = this.oldUrls; + this.oldUrls = null; + return urls.getMapWithAIPaintStyle(); + } + throw new UnsupportedOperationException("Use @MapWithAIConfig"); + } + } + } +}