ReadValuesLeakTest.java
package tools.jackson.databind.seq;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.ValueDeserializerModifier;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests to verify that {@code ObjectReader.readValues()} does not leak the
* "managed" (created by {@code ObjectReader}, owning the underlying input source)
* parser if initialization of the {@code MappingIterator} fails.
*/
public class ReadValuesLeakTest extends DatabindTestUtil
{
// Content whose very first `nextToken()` fails, so that no `MappingIterator`
// is ever constructed
private final static String INVALID_JSON = "@@@";
// Valid content, for cases where failure comes from deserializer construction
private final static String VALID_JSON = "{}";
static class CloseTrackingInputStream extends ByteArrayInputStream {
public boolean closed = false;
public CloseTrackingInputStream(String src) {
super(utf8Bytes(src));
}
@Override
public void close() throws IOException {
closed = true;
super.close();
}
}
static class CloseTrackingReader extends StringReader {
public boolean closed = false;
public CloseTrackingReader(String src) { super(src); }
@Override
public void close() {
closed = true;
super.close();
}
}
static class FailingCloseInputStream extends ByteArrayInputStream {
public FailingCloseInputStream(String src) {
super(utf8Bytes(src));
}
@Override
public void close() throws IOException {
throw new IOException("close failed");
}
}
static class Point {
public int x, y;
}
// Type for which root deserializer cannot be constructed
static class ConflictingCreators {
@JsonCreator
public ConflictingCreators(@JsonProperty("a") int a) { }
@JsonCreator
public ConflictingCreators(@JsonProperty("b") String b) { }
}
private final ObjectMapper MAPPER = newJsonMapper();
@Test
public void inputStreamClosedOnInitFailure() throws Exception
{
CloseTrackingInputStream in = new CloseTrackingInputStream(INVALID_JSON);
assertThrows(JacksonException.class,
() -> MAPPER.readerFor(Object.class).readValues(in));
assertTrue(in.closed, "InputStream should have been closed by failed readValues()");
}
@Test
public void readerClosedOnInitFailure() throws Exception
{
CloseTrackingReader r = new CloseTrackingReader(INVALID_JSON);
assertThrows(JacksonException.class,
() -> MAPPER.readerFor(Object.class).readValues(r));
assertTrue(r.closed, "Reader should have been closed by failed readValues()");
}
@Test
public void inputStreamClosedOnDeserializerFailure() throws Exception
{
CloseTrackingInputStream in = new CloseTrackingInputStream(VALID_JSON);
assertThrows(InvalidDefinitionException.class,
() -> MAPPER.readerFor(ConflictingCreators.class).readValues(in));
assertTrue(in.closed, "InputStream should have been closed by failed readValues()");
}
// Failure to close must not mask the primary failure
@Test
public void closeFailureAddedAsSuppressed() throws Exception
{
FailingCloseInputStream in = new FailingCloseInputStream(INVALID_JSON);
StreamReadException e = assertThrows(StreamReadException.class,
() -> MAPPER.readerFor(Object.class).readValues(in));
Throwable[] suppressed = e.getSuppressed();
assertEquals(1, suppressed.length);
assertEquals("close failed", suppressed[0].getCause().getMessage());
}
// Parser must be closed, and `Error` rethrown as-is, for non-`Exception` failures too
@Test
public void inputStreamClosedOnError() throws Exception
{
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new ValueDeserializerModifier() {
@Override
public ValueDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription.Supplier beanDescRef, ValueDeserializer<?> deserializer) {
throw new AssertionError("deserializer construction failed");
}
});
ObjectMapper mapper = jsonMapperBuilder()
// otherwise failure would occur (and escape) on `readerFor()`
.disable(DeserializationFeature.EAGER_DESERIALIZER_FETCH)
.addModule(module)
.build();
CloseTrackingInputStream in = new CloseTrackingInputStream(VALID_JSON);
AssertionError e = assertThrows(AssertionError.class,
() -> mapper.readerFor(Point.class).readValues(in));
assertEquals("deserializer construction failed", e.getMessage());
assertTrue(in.closed, "InputStream should have been closed by failed readValues()");
}
// And for comparison: single-value read has always closed correctly
@Test
public void singleValueReadClosesToo() throws Exception
{
CloseTrackingInputStream in = new CloseTrackingInputStream(INVALID_JSON);
assertThrows(JacksonException.class,
() -> MAPPER.readValue(in, Object.class));
assertTrue(in.closed, "InputStream should have been closed by failed readValue()");
}
// Caller-provided parser, however, must NOT be closed by us, even on failure
@Test
public void callerSuppliedParserNotClosedOnFailure() throws Exception
{
CloseTrackingInputStream in = new CloseTrackingInputStream(VALID_JSON);
try (JsonParser p = MAPPER.createParser(in)) {
assertThrows(InvalidDefinitionException.class,
() -> MAPPER.readerFor(ConflictingCreators.class).readValues(p));
assertFalse(p.isClosed(), "Caller-supplied parser should not have been closed");
assertFalse(in.closed, "InputStream should not have been closed");
}
}
}