Skip to content

Commit f1bf8cd

Browse files
committed
Close the stream parse(Path) and parse(URL) open on construction failure
(#630).
1 parent 98c3365 commit f1bf8cd

3 files changed

Lines changed: 72 additions & 3 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<action type="fix" dev="ggregory" due-to="Gary Gregory, saleem malik">Fill the lookahead buffer on short reads from a chunked source (#625).</action>
7373
<action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed Khan">Handle null name in CSVRecord accessors under ignoreHeaderCase (#628).</action>
7474
<action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed Khan">Quote null value that starts a record in minimal quote mode (#629).</action>
75+
<action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed Khan">Close the stream parse(Path) and parse(URL) open on construction failure (#630).</action>
7576
<!-- ADD -->
7677
<action type="add" dev="ggregory" due-to="Gary Gregory, Indy, Sylvia van Os" issue="CSV-307">Add an "Android Compatibility" section to the web site.</action>
7778
<action type="add" dev="ggregory" due-to="Ruiqi Dong, Gary Gregory" issue="CSV-325">Add CSVParser.Builder.setByteOffset(long) (#604).</action>

src/main/java/org/apache/commons/csv/CSVParser.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
import java.util.stream.StreamSupport;
5151

5252
import org.apache.commons.io.Charsets;
53+
import org.apache.commons.io.IOUtils;
5354
import org.apache.commons.io.build.AbstractStreamBuilder;
55+
import org.apache.commons.io.function.IOSupplier;
5456
import org.apache.commons.io.function.Uncheck;
5557

5658
/**
@@ -371,6 +373,18 @@ public static CSVParser parse(final InputStream inputStream, final Charset chars
371373
return parse(new InputStreamReader(inputStream, Charsets.toCharset(charset)), format);
372374
}
373375

376+
private static CSVParser parse(final IOSupplier<InputStream> supplier, final Charset charset, final CSVFormat format) throws IOException {
377+
Objects.requireNonNull(supplier, "supplier");
378+
final InputStream inputStream = supplier.get();
379+
try {
380+
return parse(inputStream, charset, format);
381+
} catch (final IOException | RuntimeException e) {
382+
// This method allocated the stream and the caller never gets a parser to close, so close it here.
383+
IOUtils.closeQuietlySuppress(inputStream, e);
384+
throw e;
385+
}
386+
}
387+
374388
/**
375389
* Creates and returns a parser for the given {@link Path}, which the caller MUST close.
376390
*
@@ -392,7 +406,7 @@ public static CSVParser parse(final InputStream inputStream, final Charset chars
392406
@SuppressWarnings("resource")
393407
public static CSVParser parse(final Path path, final Charset charset, final CSVFormat format) throws IOException {
394408
Objects.requireNonNull(path, "path");
395-
return parse(Files.newInputStream(path), charset, format);
409+
return parse(() -> Files.newInputStream(path), charset, format);
396410
}
397411

398412
/**
@@ -461,10 +475,9 @@ public static CSVParser parse(final String string, final CSVFormat format) throw
461475
* @throws CSVException Thrown on invalid CSV input data.
462476
* @throws NullPointerException if {@code url} is {@code null}.
463477
*/
464-
@SuppressWarnings("resource")
465478
public static CSVParser parse(final URL url, final Charset charset, final CSVFormat format) throws IOException {
466479
Objects.requireNonNull(url, "url");
467-
return parse(url.openStream(), charset, format);
480+
return parse(url::openStream, charset, format);
468481
}
469482

470483
private String headerComment;

src/test/java/org/apache/commons/csv/CSVParserTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import static org.junit.jupiter.api.Assertions.assertNull;
3131
import static org.junit.jupiter.api.Assertions.assertThrows;
3232
import static org.junit.jupiter.api.Assertions.assertTrue;
33+
import static org.junit.jupiter.api.Assertions.fail;
3334

3435
import java.io.ByteArrayInputStream;
3536
import java.io.File;
@@ -45,6 +46,8 @@
4546
import java.io.StringWriter;
4647
import java.io.UncheckedIOException;
4748
import java.net.URL;
49+
import java.net.URLConnection;
50+
import java.net.URLStreamHandler;
4851
import java.nio.charset.Charset;
4952
import java.nio.charset.StandardCharsets;
5053
import java.nio.file.Files;
@@ -308,6 +311,57 @@ void testClose() throws Exception {
308311
assertThrows(NoSuchElementException.class, records::next);
309312
}
310313

314+
@Test
315+
void testClosesInputStreamOnParsePathException() throws IOException {
316+
final Path path = Files.createTempFile(getClass().getName(), ".csv");
317+
try {
318+
Files.write(path, "A,,C\n1,2,3\n".getBytes(UTF_8));
319+
final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get();
320+
assertThrows(IllegalArgumentException.class, () -> CSVParser.parse(path, UTF_8, format));
321+
} finally {
322+
Files.delete(path);
323+
}
324+
}
325+
326+
@Test
327+
void testClosesInputStreamOnParseUrlException() throws IOException {
328+
final AtomicBoolean closed = new AtomicBoolean();
329+
final URLStreamHandler handler = new URLStreamHandler() {
330+
331+
@Override
332+
protected URLConnection openConnection(final URL u) {
333+
return new URLConnection(u) {
334+
335+
@Override
336+
public void connect() {
337+
// noop
338+
}
339+
340+
@Override
341+
public InputStream getInputStream() {
342+
return new FilterInputStream(new ByteArrayInputStream("A,,C\n1,2,3\n".getBytes(UTF_8))) {
343+
344+
@Override
345+
public void close() throws IOException {
346+
closed.set(true);
347+
super.close();
348+
}
349+
};
350+
}
351+
};
352+
}
353+
};
354+
final URL url = new URL("csv", null, -1, "test.csv", handler);
355+
final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get();
356+
assertThrows(IllegalArgumentException.class, () -> {
357+
try (CSVParser parser = CSVParser.parse(url, UTF_8, format)) {
358+
// we never get here
359+
fail("The parser should not be constructed when the header is invalid");
360+
}
361+
});
362+
assertTrue(closed.get(), "The stream opened from the URL must be closed when the parser cannot be constructed");
363+
}
364+
311365
@Test
312366
void testCSV141CSVFormat_DEFAULT() throws Exception {
313367
testCSV141Failure(CSVFormat.DEFAULT, 3);
@@ -2065,6 +2119,7 @@ void testTrim() throws Exception {
20652119
}
20662120
}
20672121

2122+
20682123
@Test
20692124
void testTryWithResourcesParseInputStreamWhenHeaderIsInvalid() throws IOException {
20702125
final AtomicBoolean closed = new AtomicBoolean();

0 commit comments

Comments
 (0)