Ignore:
Timestamp:
2023-08-22T17:26:18+02:00 (16 months ago)
Author:
taylor.smock
Message:

Fix #23134: Cancelling a json download will cause an exception dialog

We extract the underlying IOException from the JsonException into an
IllegalDataException which can be parsed by calling methods and properly handled.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/test/unit/org/openstreetmap/josm/io/OsmJsonReaderTest.java

    r18723 r18816  
    1515import java.time.Instant;
    1616import java.util.Iterator;
    17 import java.util.concurrent.atomic.AtomicBoolean;
     17import java.util.function.Supplier;
     18import java.util.stream.Stream;
    1819
    1920import jakarta.json.JsonException;
    2021
    2122import org.junit.jupiter.api.Test;
     23import org.junit.jupiter.params.ParameterizedTest;
     24import org.junit.jupiter.params.provider.Arguments;
     25import org.junit.jupiter.params.provider.MethodSource;
    2226import org.openstreetmap.josm.data.coor.LatLon;
    2327import org.openstreetmap.josm.data.osm.DataSet;
     
    2933import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
    3034import org.openstreetmap.josm.tools.ExceptionUtil;
     35import org.openstreetmap.josm.tools.JosmRuntimeException;
    3136
    3237/**
     
    255260    }
    256261
     262    static Stream<Arguments> testException() {
     263        final byte[] smallJson = "{\"type\", \"node\", \"id\": 1, \"lat\": 1.0, \"lon\": 2.0}".getBytes(StandardCharsets.UTF_8);
     264        return Stream.of(
     265                // Check that a SocketException is properly reported
     266                Arguments.of(IllegalDataException.class, new ThrowableInputStream<>(() -> new SocketException("Read timed out"), smallJson),
     267                        "java.net.SocketException: Read timed out"),
     268                // Check that a random JsonException is reported
     269                Arguments.of(JsonException.class, new ThrowableInputStream<>(() -> new JsonException("Some random json exception"), smallJson),
     270                        "Some random json exception"),
     271                // Check that bad data still throws an IllegalDataException
     272                Arguments.of(IllegalDataException.class, new ThrowableInputStream<>(() -> null, smallJson),
     273                        "jakarta.json.stream.JsonParsingException: Invalid token=COMMA at (line no=1, column no=8, offset=7). " +
     274                        "Expected tokens are: [COLON]"),
     275                // Check that an IOException is properly thrown when the stream is closed
     276                Arguments.of(IllegalDataException.class, new ThrowableInputStream<>(() -> new JsonException("I/O error while parsing JSON",
     277                        new IOException("stream is closed")), smallJson), "java.io.IOException: stream is closed")
     278        );
     279    }
    257280
    258281    /**
     
    266289     * </ul>
    267290     */
    268     @SuppressWarnings("resource")
    269     @Test
    270     void testException() {
    271         final ByteArrayInputStream bais =
    272                 new ByteArrayInputStream("{\"type\", \"node\", \"id\": 1, \"lat\": 1.0, \"lon\": 2.0}".getBytes(StandardCharsets.UTF_8));
    273         final AtomicBoolean throwJson = new AtomicBoolean();
    274         final InputStream socketExceptionStream = new InputStream() {
    275             int read = 0; // Necessary, since otherwise the exception might not be wrapped in a Json exception
    276             @Override
    277             public int read() throws IOException {
    278                 try {
    279                     if (read > 0 && !throwJson.get()) {
    280                         throw new SocketException("Read timed out");
    281                     } else if (read > 0 && throwJson.get()) {
    282                         throw new JsonException("Some random json exception");
     291    @ParameterizedTest
     292    @MethodSource
     293    <E extends Exception> void testException(Class<E> exceptionClass, ThrowableInputStream<?> exceptionStream, String exceptionMessage) {
     294        E exception = assertThrows(exceptionClass, () -> OsmJsonReader.parseDataSet(exceptionStream, NullProgressMonitor.INSTANCE));
     295        assertEquals(exceptionMessage, ExceptionUtil.explainException(exception));
     296        assertDoesNotThrow(exceptionStream::close);
     297    }
     298
     299    /**
     300     * An {@link InputStream} that will throw a specified exception after the first byte is read
     301     * @param <E> The exception to be thrown
     302     */
     303    private static class ThrowableInputStream<E extends Throwable> extends InputStream {
     304        private final Supplier<E> exceptionSupplier;
     305        private final ByteArrayInputStream bais;
     306        private int read = 0; // Necessary, since otherwise the exception might not be wrapped in a Json exception
     307
     308        ThrowableInputStream(Supplier<E> exceptionSupplier, byte[] source) {
     309            this.exceptionSupplier = exceptionSupplier;
     310            this.bais = new ByteArrayInputStream(source);
     311        }
     312
     313        @Override
     314        public int read() throws IOException {
     315            try {
     316                if (read > 0) {
     317                    E exception = this.exceptionSupplier.get();
     318                    if (exception instanceof IOException) {
     319                        throw (IOException) exception;
     320                    } else if (exception instanceof RuntimeException) {
     321                        throw (RuntimeException) exception;
     322                    } else if (exception != null) {
     323                        // This shouldn't be possible in actual code, so if something hits this, it is a failure.
     324                        throw new JosmRuntimeException(exception);
    283325                    }
    284                     return bais.read();
    285                 } finally {
    286                     read++;
    287326                }
     327                return bais.read();
     328            } finally {
     329                read++;
    288330            }
    289         };
    290         // Check that a SocketException is properly reported
    291         IllegalDataException ide = assertThrows(IllegalDataException.class,
    292                 () -> OsmJsonReader.parseDataSet(socketExceptionStream, NullProgressMonitor.INSTANCE));
    293         assertEquals("java.net.SocketException: Read timed out", ExceptionUtil.explainException(ide));
    294         assertDoesNotThrow(socketExceptionStream::close);
    295         bais.reset();
    296         // Check that a generic exception is properly thrown -- we only want to handle known "good" cases specially
    297         throwJson.set(true);
    298         assertThrows(JsonException.class, () -> OsmJsonReader.parseDataSet(socketExceptionStream, NullProgressMonitor.INSTANCE));
    299         bais.reset();
    300         // Check that a generic parsing error is properly reported
    301         ide = assertThrows(IllegalDataException.class, () -> OsmJsonReader.parseDataSet(bais, NullProgressMonitor.INSTANCE));
    302         assertEquals("jakarta.json.stream.JsonParsingException: Invalid token=COMMA at (line no=1, column no=8, offset=7). " +
    303                 "Expected tokens are: [COLON]", ExceptionUtil.explainException(ide));
    304         bais.reset();
    305         // Check that an unknown exception is thrown properly
     331        }
     332
     333        @Override
     334        public void close() throws IOException {
     335            this.bais.close();
     336            super.close();
     337        }
    306338    }
    307339}
Note: See TracChangeset for help on using the changeset viewer.