FailedGeneratorConstructionCloseTest.java
package tools.jackson.core.unittest.json;
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.ContentReference;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.OutputDecorator;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.core.unittest.JacksonCoreTestBase;
import tools.jackson.core.util.JsonGeneratorDecorator;
import tools.jackson.core.util.JsonGeneratorDelegate;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests to verify that streams Jackson itself opens for {@link File} /
* {@link java.nio.file.Path} targets get closed if construction of generator
* fails after opening.
*/
class FailedGeneratorConstructionCloseTest extends JacksonCoreTestBase
{
static class CloseTrackingOutputStream extends FilterOutputStream {
public boolean closed;
public int closeCount;
CloseTrackingOutputStream(OutputStream out) { super(out); }
@Override
public void close() throws IOException {
closed = true;
++closeCount;
super.close();
}
}
static class CloseTrackingWriter extends FilterWriter {
public boolean closed;
public int closeCount;
CloseTrackingWriter(Writer out) { super(out); }
@Override
public void close() throws IOException {
closed = true;
++closeCount;
super.close();
}
}
static class FailingCloseOutputStream extends FilterOutputStream {
public boolean closeAttempted;
FailingCloseOutputStream(OutputStream out) { super(out); }
@Override
public void close() throws IOException {
closeAttempted = true;
throw new IOException("Test-induced output close failure");
}
}
static class FailingCloseWriter extends FilterWriter {
public boolean closeAttempted;
FailingCloseWriter(Writer out) { super(out); }
@Override
public void close() throws IOException {
closeAttempted = true;
throw new IOException("Test-induced writer close failure");
}
}
static class TrackingOutputDecorator extends OutputDecorator {
private static final long serialVersionUID = 1L;
public final List<CloseTrackingOutputStream> outputs = new ArrayList<>();
public final List<CloseTrackingWriter> writers = new ArrayList<>();
@Override
public OutputStream decorate(IOContext ctxt, OutputStream out) {
CloseTrackingOutputStream wrapped = new CloseTrackingOutputStream(out);
outputs.add(wrapped);
return wrapped;
}
@Override
public Writer decorate(IOContext ctxt, Writer w) {
CloseTrackingWriter wrapped = new CloseTrackingWriter(w);
writers.add(wrapped);
return wrapped;
}
}
static class FailingCloseOutputDecorator extends OutputDecorator {
private static final long serialVersionUID = 1L;
public final List<FailingCloseOutputStream> outputs = new ArrayList<>();
public final List<FailingCloseWriter> writers = new ArrayList<>();
@Override
public OutputStream decorate(IOContext ctxt, OutputStream out) {
FailingCloseOutputStream wrapped = new FailingCloseOutputStream(out);
outputs.add(wrapped);
return wrapped;
}
@Override
public Writer decorate(IOContext ctxt, Writer w) {
FailingCloseWriter wrapped = new FailingCloseWriter(w);
writers.add(wrapped);
return wrapped;
}
}
/**
* Factory that fails the way format backends can (say, when schema
* validation fails), that is, after source/target has been opened.
*/
static class FailingFactory extends JsonFactory {
private static final long serialVersionUID = 1L;
public final List<CloseTrackingOutputStream> outputs = new ArrayList<>();
public FailingFactory() { }
public FailingFactory(JsonFactoryBuilder b) {
super(b);
}
@Override
protected OutputStream _fileOutputStream(File f) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._fileOutputStream(f));
outputs.add(out);
return out;
}
@Override
protected OutputStream _pathOutputStream(Path p) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._pathOutputStream(p));
outputs.add(out);
return out;
}
@Override
protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt,
IOContext ioCtxt, Writer out) throws JacksonException {
throw new IllegalStateException("Test-induced construction failure");
}
@Override
protected JsonGenerator _createUTF8Generator(ObjectWriteContext writeCtxt,
IOContext ioCtxt, OutputStream out) throws JacksonException {
throw new IllegalStateException("Test-induced construction failure");
}
}
/**
* Factory that fails while creating the context, before generator
* construction itself starts.
*/
static class ContextFailingFactory extends FailingFactory {
private static final long serialVersionUID = 1L;
@Override
protected IOContext _createContext(ContentReference contentRef,
boolean resourceManaged, JsonEncoding enc) {
throw new IllegalStateException("Test-induced context failure");
}
}
static class TrackingFactory extends JsonFactory {
private static final long serialVersionUID = 1L;
public final List<CloseTrackingOutputStream> outputs = new ArrayList<>();
public TrackingFactory(JsonFactoryBuilder b) {
super(b);
}
@Override
protected OutputStream _fileOutputStream(File f) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._fileOutputStream(f));
outputs.add(out);
return out;
}
@Override
protected OutputStream _pathOutputStream(Path p) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._pathOutputStream(p));
outputs.add(out);
return out;
}
}
static class FailingGeneratorDecorator implements JsonGeneratorDecorator {
@Override
public JsonGenerator decorate(TokenStreamFactory factory, JsonGenerator generator) {
throw new IllegalStateException("Test-induced generator decorator failure");
}
}
static class FailingBinaryFactory extends BinaryTSFactory {
private static final long serialVersionUID = 1L;
public final List<CloseTrackingOutputStream> outputs = new ArrayList<>();
private final boolean _failContextCreation;
private final boolean _writeHeaderGenerator;
FailingBinaryFactory(FailingBinaryFactoryBuilder b,
boolean failContextCreation, boolean writeHeaderGenerator) {
super(b);
_failContextCreation = failContextCreation;
_writeHeaderGenerator = writeHeaderGenerator;
}
@Override
protected OutputStream _fileOutputStream(File f) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._fileOutputStream(f));
outputs.add(out);
return out;
}
@Override
protected OutputStream _pathOutputStream(Path p) throws JacksonException {
CloseTrackingOutputStream out = new CloseTrackingOutputStream(super._pathOutputStream(p));
outputs.add(out);
return out;
}
@Override
protected IOContext _createContext(ContentReference contentRef,
boolean resourceManaged, JsonEncoding enc) {
if (_failContextCreation) {
throw new IllegalStateException("Test-induced context failure");
}
return super._createContext(contentRef, resourceManaged, enc);
}
@Override
protected JsonParser _createParser(ObjectReadContext readCtxt,
IOContext ioCtxt, InputStream in) {
throw new UnsupportedOperationException();
}
@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) throws JacksonException {
if (_writeHeaderGenerator) {
return new HeaderCloseSideEffectGenerator(out);
}
throw new IllegalStateException("Test-induced construction failure");
}
@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 "test-binary"; }
@Override
public Version version() { return Version.unknownVersion(); }
}
static class FailingBinaryFactoryBuilder
extends DecorableTSFactory.DecorableTSFBuilder<FailingBinaryFactory, FailingBinaryFactoryBuilder>
{
FailingBinaryFactoryBuilder() {
super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(),
ErrorReportConfiguration.defaults(), 0, 0);
}
@Override
public FailingBinaryFactory build() {
return build(false);
}
public FailingBinaryFactory build(boolean failContextCreation) {
return new FailingBinaryFactory(this, failContextCreation, false);
}
public FailingBinaryFactory buildWithHeaderGenerator() {
return new FailingBinaryFactory(this, false, true);
}
}
static class HeaderCloseSideEffectGenerator extends JsonGeneratorDelegate {
private final OutputStream _out;
HeaderCloseSideEffectGenerator(OutputStream out) throws JacksonException {
super(new JsonFactory().createGenerator(ObjectWriteContext.empty(),
new ByteArrayOutputStream()));
_out = out;
_writeByte('H');
}
@Override
public void close() {
_writeByte('C');
super.close();
}
private void _writeByte(int b) {
try {
_out.write(b);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
private File _tempFile() throws IOException {
File f = File.createTempFile("jackson-core-test", ".json");
f.deleteOnExit();
Files.write(f.toPath(), utf8Bytes("{\"a\":1}"));
return f;
}
@Test
void closesFileOutputStreamOnFailedGeneratorConstruction() throws Exception
{
for (JsonEncoding enc : new JsonEncoding[] { JsonEncoding.UTF8, JsonEncoding.UTF16_BE }) {
FailingFactory f = new FailingFactory();
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, enc));
_verifyOneClosedOutput(f.outputs, enc.toString());
}
}
@Test
void closesPathOutputStreamOnFailedGeneratorConstruction() throws Exception
{
for (JsonEncoding enc : new JsonEncoding[] { JsonEncoding.UTF8, JsonEncoding.UTF16_BE }) {
FailingFactory f = new FailingFactory();
Path dst = _tempFile().toPath();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, enc));
_verifyOneClosedOutput(f.outputs, enc.toString());
}
}
@Test
void doesNotOpenFileOutputStreamOnContextCreationFailure() throws Exception
{
ContextFailingFactory f = new ContextFailingFactory();
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyNoOutputOpened(f.outputs);
}
@Test
void doesNotOpenPathOutputStreamOnContextCreationFailure() throws Exception
{
ContextFailingFactory f = new ContextFailingFactory();
Path dst = _tempFile().toPath();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyNoOutputOpened(f.outputs);
}
@Test
void closesDecoratedFileOutputStreamOnFailedGeneratorConstruction() throws Exception
{
TrackingOutputDecorator dec = new TrackingOutputDecorator();
FailingFactory f = new FailingFactory(JsonFactory.builder()
.outputDecorator(dec));
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
assertEquals(1, dec.outputs.size());
assertTrue(dec.outputs.get(0).closed,
"Decorated OutputStream should have been closed");
}
@Test
void closesDecoratedFileWriterOnFailedGeneratorConstruction() throws Exception
{
TrackingOutputDecorator dec = new TrackingOutputDecorator();
FailingFactory f = new FailingFactory(JsonFactory.builder()
.outputDecorator(dec));
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF16_BE));
_verifyOneClosedOutput(f.outputs);
assertEquals(1, dec.writers.size());
assertTrue(dec.writers.get(0).closed,
"Decorated Writer should have been closed");
}
@Test
void closesRawFileOutputStreamWhenDecoratedOutputCloseFails() throws Exception
{
FailingCloseOutputDecorator dec = new FailingCloseOutputDecorator();
FailingFactory f = new FailingFactory(JsonFactory.builder()
.outputDecorator(dec));
File dst = _tempFile();
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
assertEquals(1, dec.outputs.size());
assertTrue(dec.outputs.get(0).closeAttempted,
"Decorated OutputStream close should have been attempted");
_verifySuppressed(e, "Test-induced output close failure");
}
@Test
void closesRawFileOutputStreamWhenDecoratedWriterCloseFails() throws Exception
{
FailingCloseOutputDecorator dec = new FailingCloseOutputDecorator();
FailingFactory f = new FailingFactory(JsonFactory.builder()
.outputDecorator(dec));
File dst = _tempFile();
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF16_BE));
_verifyOneClosedOutput(f.outputs);
assertEquals(1, dec.writers.size());
assertTrue(dec.writers.get(0).closeAttempted,
"Decorated Writer close should have been attempted");
_verifySuppressed(e, "Test-induced writer close failure");
}
@Test
void closesFileOutputStreamOnGeneratorDecorationFailureWithAutoCloseTargetDisabled() throws Exception
{
TrackingFactory f = new TrackingFactory(JsonFactory.builder()
.configure(StreamWriteFeature.AUTO_CLOSE_TARGET, false)
.addDecorator(new FailingGeneratorDecorator()));
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
}
@Test
void closesPathOutputStreamOnGeneratorDecorationFailureWithAutoCloseTargetDisabled() throws Exception
{
TrackingFactory f = new TrackingFactory(JsonFactory.builder()
.configure(StreamWriteFeature.AUTO_CLOSE_TARGET, false)
.addDecorator(new FailingGeneratorDecorator()));
Path dst = _tempFile().toPath();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF16_BE));
_verifyOneClosedOutput(f.outputs);
}
@Test
void binaryClosesFileOutputStreamOnFailedGeneratorConstruction() throws Exception
{
FailingBinaryFactory f = new FailingBinaryFactoryBuilder().build();
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
}
@Test
void binaryClosesPathOutputStreamOnFailedGeneratorConstruction() throws Exception
{
FailingBinaryFactory f = new FailingBinaryFactoryBuilder().build();
Path dst = _tempFile().toPath();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
}
@Test
void binaryDoesNotOpenFileOutputStreamOnContextCreationFailure() throws Exception
{
FailingBinaryFactory f = new FailingBinaryFactoryBuilder().build(true);
File dst = _tempFile();
Files.write(dst.toPath(), new byte[] { 'a' });
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyNoOutputOpened(f.outputs);
// [core#1711] Target must not be truncated if context creation fails
assertArrayEquals(new byte[] { 'a' }, Files.readAllBytes(dst.toPath()));
}
@Test
void binaryDoesNotOpenPathOutputStreamOnContextCreationFailure() throws Exception
{
FailingBinaryFactory f = new FailingBinaryFactoryBuilder().build(true);
Path dst = _tempFile().toPath();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyNoOutputOpened(f.outputs);
}
@Test
void binaryClosesDecoratedFileOutputStreamOnFailedGeneratorConstruction() throws Exception
{
TrackingOutputDecorator dec = new TrackingOutputDecorator();
FailingBinaryFactory f = new FailingBinaryFactoryBuilder()
.outputDecorator(dec)
.build();
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
assertEquals(1, dec.outputs.size());
assertTrue(dec.outputs.get(0).closed,
"Decorated OutputStream should have been closed");
}
@Test
void binaryGeneratorDecoratorFailureDoesNotClosePartiallyConstructedGenerator() throws Exception
{
FailingBinaryFactory f = new FailingBinaryFactoryBuilder()
.addDecorator(new FailingGeneratorDecorator())
.buildWithHeaderGenerator();
File dst = _tempFile();
assertThrows(IllegalStateException.class,
() -> f.createGenerator(ObjectWriteContext.empty(), dst, JsonEncoding.UTF8));
_verifyOneClosedOutput(f.outputs);
assertArrayEquals(new byte[] { 'H' }, Files.readAllBytes(dst.toPath()));
}
private void _verifyOneClosedOutput(List<CloseTrackingOutputStream> outputs) {
_verifyOneClosedOutput(outputs, "Should have opened exactly one output");
}
private void _verifyOneClosedOutput(List<CloseTrackingOutputStream> outputs, String msg) {
assertEquals(1, outputs.size(), msg);
assertTrue(outputs.get(0).closed,
"OutputStream Jackson opened should have been closed");
// 16-Sep-2026, tatu: [core#1711] Closing decorated resource closes what it wraps,
// so target Jackson opened must not be closed a second time
assertEquals(1, outputs.get(0).closeCount,
"OutputStream Jackson opened should have been closed exactly once");
}
private void _verifyNoOutputOpened(List<CloseTrackingOutputStream> outputs) {
assertEquals(0, outputs.size(), "Should not have opened output");
}
private void _verifySuppressed(Throwable failure, String msg) {
for (Throwable suppressed : failure.getSuppressed()) {
if (suppressed.getMessage().contains(msg)) {
return;
}
}
fail("Expected suppressed exception containing: "+msg);
}
}