FailedParserConstructionCloseTest.java
package tools.jackson.core.unittest.base;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import tools.jackson.core.*;
import tools.jackson.core.base.BinaryTSFactory;
import tools.jackson.core.base.DecorableTSFactory;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.InputDecorator;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.core.unittest.JacksonCoreTestBase;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests to verify that streams Jackson itself opens -- for {@link File} /
* {@link Path} sources, or via {@link InputDecorator} -- get closed, exactly
* once, if construction of parser fails after opening. Failure may occur in
* decorator (before format backend is entered at all), or in backend's own
* {@code _createParser()}.
*/
@SuppressWarnings("serial")
class FailedParserConstructionCloseTest extends JacksonCoreTestBase
{
private final static String DECORATOR_FAIL = "Test-induced decorator failure";
private final static String CREATE_FAIL = "Test-induced construction failure";
private final static String READ_FAIL = "Will not read, ever!";
/**
* Where construction is to fail, for factories that open source themselves.
*/
enum Failure {
/** Fail before backend sees source at all (decorator failure) */
IN_DECORATOR,
/** Fail in backend's {@code _createParser()}, the way schema validation can */
IN_CREATE,
/** Fail deeper, on first actual read: exercises real backend code path */
ON_READ
}
static class CloseTrackingInputStream extends FilterInputStream {
public int closeCount;
CloseTrackingInputStream(InputStream in) { super(in); }
@Override
public void close() throws IOException {
++closeCount;
super.close();
}
}
static class UnreadableInputStream extends InputStream {
@Override
public int read() throws IOException { throw new IOException(READ_FAIL); }
}
/**
* Implemented by test factories that track streams Jackson opened for
* {@link File} / {@link Path} sources.
*/
interface SourceTracking {
List<CloseTrackingInputStream> openedSources();
}
/**
* Decorator that fails the way a user-provided one can, that is, after
* source has been opened but before backend gets to see it.
*/
static class FailingInputDecorator extends InputDecorator {
@Override
public InputStream decorate(IOContext ctxt, InputStream in) {
throw new IllegalStateException(DECORATOR_FAIL);
}
@Override
public InputStream decorate(IOContext ctxt, byte[] src, int offset, int length) {
throw new IllegalStateException(DECORATOR_FAIL);
}
@Override
public Reader decorate(IOContext ctxt, Reader r) {
throw new IllegalStateException(DECORATOR_FAIL);
}
}
/**
* Decorator that creates {@link InputStream} for {@code byte[]} source: stream
* is created by decorator, not caller, so factory must close it on failure.
*/
static class ByteArraySourceDecorator extends InputDecorator {
public final List<CloseTrackingInputStream> sources = new ArrayList<>();
@Override
public InputStream decorate(IOContext ctxt, InputStream in) { return in; }
@Override
public InputStream decorate(IOContext ctxt, byte[] src, int offset, int length) {
CloseTrackingInputStream wrapped = new CloseTrackingInputStream(
new ByteArrayInputStream(src, offset, length));
sources.add(wrapped);
return wrapped;
}
@Override
public Reader decorate(IOContext ctxt, Reader r) { return r; }
}
// // // Textual (JSON) factory
static class TrackingJsonFactory extends JsonFactory
implements SourceTracking
{
public final List<CloseTrackingInputStream> sources = new ArrayList<>();
private final Failure _failure;
TrackingJsonFactory(JsonFactoryBuilder b, Failure failure) {
super(b);
_failure = failure;
}
@Override
public List<CloseTrackingInputStream> openedSources() { return sources; }
@Override
protected InputStream _fileInputStream(File f) {
return _track(super._fileInputStream(f));
}
@Override
protected InputStream _pathInputStream(Path p) {
return _track(super._pathInputStream(p));
}
private InputStream _track(InputStream in) {
if (_failure == Failure.ON_READ) {
in = new UnreadableInputStream();
}
CloseTrackingInputStream wrapped = new CloseTrackingInputStream(in);
sources.add(wrapped);
return wrapped;
}
@Override
protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
InputStream in) {
if (_failure == Failure.IN_CREATE) {
throw new IllegalStateException(CREATE_FAIL);
}
return super._createParser(readCtxt, ioCtxt, in);
}
}
// // // Binary factory: minimal impl, fails the way format backends can
// // // (say, when schema validation fails)
static class ToyBinaryFactory extends BinaryTSFactory
implements SourceTracking
{
public final List<CloseTrackingInputStream> sources = new ArrayList<>();
ToyBinaryFactory(ToyBinaryFactoryBuilder b) { super(b); }
@Override
public List<CloseTrackingInputStream> openedSources() { return sources; }
@Override
protected InputStream _fileInputStream(File f) {
return _track(super._fileInputStream(f));
}
@Override
protected InputStream _pathInputStream(Path p) {
return _track(super._pathInputStream(p));
}
private InputStream _track(InputStream in) {
CloseTrackingInputStream wrapped = new CloseTrackingInputStream(in);
sources.add(wrapped);
return wrapped;
}
@Override
protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
InputStream in) {
throw new IllegalStateException(CREATE_FAIL);
}
@Override
protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
byte[] data, int offset, int len) {
throw new UnsupportedOperationException();
}
@Override
protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
DataInput input) {
throw new UnsupportedOperationException();
}
@Override
protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt,
IOContext ioCtxt, OutputStream out) {
throw new UnsupportedOperationException();
}
@Override
public TokenStreamFactory copy() { return this; }
@Override
public TokenStreamFactory snapshot() { return this; }
@Override
public TSFBuilder<?,?> rebuild() { return null; }
@Override
public boolean canParseAsync() { return false; }
@Override
public boolean canUseSchema(FormatSchema schema) { return false; }
@Override
public String getFormatName() { return "toy-binary"; }
@Override
public Version version() { return Version.unknownVersion(); }
}
static class ToyBinaryFactoryBuilder
extends DecorableTSFactory.DecorableTSFBuilder<ToyBinaryFactory, ToyBinaryFactoryBuilder>
{
ToyBinaryFactoryBuilder() {
super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(),
ErrorReportConfiguration.defaults(), 0, 0);
}
@Override
public ToyBinaryFactory build() { return new ToyBinaryFactory(this); }
}
/*
/**********************************************************************
/* Test methods: textual (JSON) factory
/**********************************************************************
*/
@Test
void jsonFileSourceClosedOnDecoratorFailure() throws Exception {
_verifyFileSourceClosed(_jsonFactory(Failure.IN_DECORATOR),
IllegalStateException.class, DECORATOR_FAIL);
}
@Test
void jsonPathSourceClosedOnDecoratorFailure() throws Exception {
_verifyPathSourceClosed(_jsonFactory(Failure.IN_DECORATOR),
IllegalStateException.class, DECORATOR_FAIL);
}
@Test
void jsonFileSourceClosedOnCreateFailure() throws Exception {
_verifyFileSourceClosed(_jsonFactory(Failure.IN_CREATE),
IllegalStateException.class, CREATE_FAIL);
}
@Test
void jsonPathSourceClosedOnCreateFailure() throws Exception {
_verifyPathSourceClosed(_jsonFactory(Failure.IN_CREATE),
IllegalStateException.class, CREATE_FAIL);
}
// [core#763] (and [databind#3455]): failure inside real backend, after
// hand-off -- that is, without test overriding `_createParser()` at all
@Test
void jsonFileSourceClosedOnReadFailure() throws Exception {
_verifyFileSourceClosed(_jsonFactory(Failure.ON_READ),
JacksonException.class, READ_FAIL);
}
@Test
void jsonPathSourceClosedOnReadFailure() throws Exception {
_verifyPathSourceClosed(_jsonFactory(Failure.ON_READ),
JacksonException.class, READ_FAIL);
}
// [core#763]: stream decorator creates from `byte[]` source is ours to close too
@Test
void jsonByteArraySourceClosedOnCreateFailure() throws Exception {
ByteArraySourceDecorator dec = new ByteArraySourceDecorator();
TrackingJsonFactory f = new TrackingJsonFactory(
JsonFactory.builder().inputDecorator(dec), Failure.IN_CREATE);
_verifyByteArraySourceClosed(f, dec);
}
/*
/**********************************************************************
/* Test methods: binary factory
/**********************************************************************
*/
@Test
void binaryFileSourceClosedOnDecoratorFailure() throws Exception {
_verifyFileSourceClosed(_binaryFactory(true),
IllegalStateException.class, DECORATOR_FAIL);
}
@Test
void binaryPathSourceClosedOnDecoratorFailure() throws Exception {
_verifyPathSourceClosed(_binaryFactory(true),
IllegalStateException.class, DECORATOR_FAIL);
}
@Test
void binaryFileSourceClosedOnCreateFailure() throws Exception {
_verifyFileSourceClosed(_binaryFactory(false),
IllegalStateException.class, CREATE_FAIL);
}
@Test
void binaryPathSourceClosedOnCreateFailure() throws Exception {
_verifyPathSourceClosed(_binaryFactory(false),
IllegalStateException.class, CREATE_FAIL);
}
// [core#763]: stream decorator creates from `byte[]` source is ours to close too
@Test
void binaryByteArraySourceClosedOnCreateFailure() throws Exception {
ByteArraySourceDecorator dec = new ByteArraySourceDecorator();
ToyBinaryFactory f = new ToyBinaryFactoryBuilder().inputDecorator(dec).build();
_verifyByteArraySourceClosed(f, dec);
}
/*
/**********************************************************************
/* Helper methods
/**********************************************************************
*/
private TrackingJsonFactory _jsonFactory(Failure failure) {
JsonFactoryBuilder b = JsonFactory.builder();
if (failure == Failure.IN_DECORATOR) {
b = b.inputDecorator(new FailingInputDecorator());
}
return new TrackingJsonFactory(b, failure);
}
private ToyBinaryFactory _binaryFactory(boolean failingDecorator) {
ToyBinaryFactoryBuilder b = new ToyBinaryFactoryBuilder();
if (failingDecorator) {
b = b.inputDecorator(new FailingInputDecorator());
}
return b.build();
}
private <F extends TokenStreamFactory & SourceTracking> void _verifyFileSourceClosed(F f,
Class<? extends Exception> expType, String expFailMsg)
throws Exception
{
final File src = _tempFile();
Exception e = assertThrows(expType,
() -> f.createParser(ObjectReadContext.empty(), src));
verifyException(e, expFailMsg);
_verifyClosed(f.openedSources());
}
private <F extends TokenStreamFactory & SourceTracking> void _verifyPathSourceClosed(F f,
Class<? extends Exception> expType, String expFailMsg)
throws Exception
{
final Path src = _tempFile().toPath();
Exception e = assertThrows(expType,
() -> f.createParser(ObjectReadContext.empty(), src));
verifyException(e, expFailMsg);
_verifyClosed(f.openedSources());
}
private void _verifyByteArraySourceClosed(TokenStreamFactory f,
ByteArraySourceDecorator dec)
{
final byte[] src = utf8Bytes("{\"a\":1}");
Exception e = assertThrows(IllegalStateException.class,
() -> f.createParser(ObjectReadContext.empty(), src));
verifyException(e, CREATE_FAIL);
_verifyClosed(dec.sources);
}
private void _verifyClosed(List<CloseTrackingInputStream> sources) {
assertEquals(1, sources.size(), "Should have opened exactly one source");
// Exactly once: not closing leaks, closing twice may fail for non-idempotent
// streams (and add bogus suppressed exceptions)
assertEquals(1, sources.get(0).closeCount,
"InputStream Jackson opened should have been closed exactly once");
}
private File _tempFile() throws IOException {
File f = File.createTempFile("jackson-core-test", ".json");
f.deleteOnExit();
Files.write(f.toPath(), utf8Bytes("{\"a\":1}"));
return f;
}
}