AnnotatedTestExecutor.java
/*
*
* The DbUnit Database Testing Framework
* Copyright (C)2002-2026, DbUnit.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package org.dbunit.annotation.runtime;
import org.dbunit.DatabaseUnitException;
import org.dbunit.DefaultOperationListener;
import org.dbunit.DefaultPrepAndExpectedTestCase;
import org.dbunit.IDatabaseTester;
import org.dbunit.IOperationListener;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConfigOverrides;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.connection.AutoCommitOffWarning;
import org.dbunit.database.connection.ConnectionOwnership;
import org.dbunit.database.connection.LeftOpenConnectionWarning;
import org.dbunit.database.connection.TestScopedConnection;
/**
* Drives one test's dbUnit lifecycle from a resolved {@link AnnotatedTestConfiguration},
* dispatching {@link #beforeTest()}/{@link #afterTest(boolean)} to one of two path objects -
* {@link SetupTeardownLifecycle} when {@link AnnotatedTestConfiguration#isExpected()} is false,
* {@link ExpectedLifecycle} when it is true - and owning what both share: the one
* {@link TestScopedConnection} this test's steps reuse, its {@link ConnectionOwnership}
* decision, the {@code @DbUnitProperty} application, and (for an annotation-driven test) the
* {@link ExecutorOperationListener} installed on the tester.
*
* <p>Not intended for direct use by test code; this is machinery consumed by a binding such as
* {@code DbUnitExtension} or the Spring {@code DbUnitTestExecutionListener}, which resolves the
* {@link IDatabaseTester} and any injected {@link PrepAndExpectedTestCase} - through
* {@link TestInstanceTesterResolver}'s shared field rules, from the test instances only the
* binding knows how to find - and hands them here already resolved.
*
* <p>The binding tells this executor, through the constructor's {@code annotationDriven} flag,
* whether the test opted into the {@code org.dbunit.annotation} family at all - any
* {@code @DbUnit*} annotation, or a {@code @DbUnitTester}/{@code @DbUnitTestCase} field. When it
* did not - a bare {@code @ExtendWith(DbUnitExtension.class)} class with one plain, unannotated
* {@link IDatabaseTester} field, the 3.5.0 lifecycle-only style - this executor takes the
* <em>classic path</em>: {@link #installOperationListener()} does not run, so the tester's
* {@link IOperationListener} is left untouched, and {@link AnnotatedRowCountCheck} never
* piggybacks (it captures its baseline eagerly and {@link SetupTeardownLifecycle} lets
* {@code onSetup()}/{@code onTearDown()} manage their own connections) - as
* {@code DbUnitExtension} did before this class existed, with one addition: the connection
* captured eagerly is first offered to the tester's own listener's
* {@link IOperationListener#connectionRetrieved(IDatabaseConnection)}, as every connection
* retrieved from the tester is, so a {@code DatabaseConfig} customization made there - such as
* enabling the row count check - applies to it. The prep/expected path is always
* annotation-driven (it needs {@code @DbUnitExpected}).
*
* <p>The connection {@link #getConnection()} memoizes - the tester's own on the setup/teardown
* path, {@link PrepAndExpectedTestCase#getReusableConnection()}'s on the prep/expected path so a
* {@code Connection}/{@code IDatabaseConnection} parameter shares the test case's own - is used
* by the row count check and by a binding's parameter injection, and closed in
* {@link #afterTest(boolean)} when {@link ConnectionOwnership#mayClose()} allows it. On an
* annotation-driven test the {@link ExecutorOperationListener} additionally shields that one
* connection, by identity, from a premature close by the tester's own {@code onSetup()}/
* {@code onTearDown()} machinery while still forwarding a close for any other connection the
* tester hands out.
*
* <p>The row count check (see {@code org.dbunit.database.rowcount.RowCountCheck}) runs, on the
* setup/teardown path, through {@link AnnotatedRowCountCheck}; the prep/expected path already
* has its own via {@link DefaultPrepAndExpectedTestCase#preTest()} and {@code cleanupData()}.
* Either way, when {@code @DbUnitRowCountCheck} is declared its resolved {@code RowCountCheck}
* overrides whichever of the two would otherwise resolve one from the connection's own
* {@link DatabaseConfig} - for the prep/expected path via
* {@link DefaultPrepAndExpectedTestCase#setRowCountCheckOverride(boolean, String[])} (applied
* by {@link InjectedTestCaseConfigurer}), which resolves that connection lazily on its own so
* this executor needs none just to build the override.
*
* @author Jeff Jensen
* @since 3.6.0
*/
public class AnnotatedTestExecutor
{
private final AnnotatedTestConfiguration configuration;
private final IDatabaseTester tester;
private final boolean annotationDriven;
private final AutoCommitOffWarning autoCommitOffWarning = new AutoCommitOffWarning();
private final LeftOpenConnectionWarning leftOpenConnectionWarning;
private final TestScopedConnection testScopedConnection;
private final ExpectedLifecycle expectedLifecycle;
private final SetupTeardownLifecycle setupTeardownLifecycle;
private final DatabaseConfigOverrides databaseConfigOverrides;
private boolean afterTestRan;
private ExecutorOperationListener installedListener;
private IOperationListener listenerToRestore;
/**
* Creates an executor for an annotation-driven test - equivalent to
* {@link #AnnotatedTestExecutor(AnnotatedTestConfiguration, IDatabaseTester, PrepAndExpectedTestCase, boolean)}
* with {@code annotationDriven} {@code true}.
*
* @param configuration The resolved configuration to execute.
* @param tester The tester to drive the setup/teardown path with, or to construct a
* {@link PrepAndExpectedTestCase} around when {@code prepAndExpectedTestCase}
* is {@code null} and the prep/expected path is configured.
* @param prepAndExpectedTestCase An already-injected test case to drive instead of
* constructing one, or {@code null} to have this executor construct one from
* {@link AnnotatedTestConfiguration#getPrepAndExpectedTestCaseClass()}.
*/
public AnnotatedTestExecutor(final AnnotatedTestConfiguration configuration,
final IDatabaseTester tester,
final PrepAndExpectedTestCase prepAndExpectedTestCase)
{
this(configuration, tester, prepAndExpectedTestCase, true);
}
/**
* Creates an executor for one test.
*
* <p>Mutates {@code tester} only when {@code annotationDriven} is true:
* {@link #installOperationListener()} runs here, replacing {@code tester}'s
* {@link IOperationListener} with an {@code ExecutorOperationListener} wrapping the previous
* one, which {@link #afterTest(boolean)} - or {@link #releaseIfAfterTestDidNotRun()} - puts
* back, so a {@code tester} shared across methods (e.g. a {@code static @DbUnitTester}
* field) carries nothing onto the next test or the next user of the tester. A
* {@code tester} shared beyond one test class - a bean in a cached application context - is
* left as it was found the same way, but is still not safe to share between tests running at
* the same time: each installs its own wrapper and sets its own dataset and operations on
* the one tester. When {@code annotationDriven} is false - the classic path - the tester's
* listener is left untouched; see the class Javadoc.
*
* @param configuration The resolved configuration to execute.
* @param tester The tester to drive the setup/teardown path with, or to construct a
* {@link PrepAndExpectedTestCase} around when {@code prepAndExpectedTestCase}
* is {@code null} and the prep/expected path is configured.
* @param prepAndExpectedTestCase An already-injected test case to drive instead of
* constructing one, or {@code null} to have this executor construct one from
* {@link AnnotatedTestConfiguration#getPrepAndExpectedTestCaseClass()}.
* @param annotationDriven Whether the test opted into the {@code org.dbunit.annotation}
* family - any {@code @DbUnit*} annotation, or a
* {@code @DbUnitTester}/{@code @DbUnitTestCase} field. False for a bare
* {@code @ExtendWith(DbUnitExtension.class)} class with one plain, unannotated
* {@link IDatabaseTester} field, whose tester listener and connection lifecycle
* are then left as in the 3.5.0 lifecycle, apart from the tester's listener being
* offered the connection this executor resolves itself. Always true for the
* prep/expected path, which needs {@code @DbUnitExpected}.
*/
public AnnotatedTestExecutor(final AnnotatedTestConfiguration configuration,
final IDatabaseTester tester,
final PrepAndExpectedTestCase prepAndExpectedTestCase,
final boolean annotationDriven)
{
this.configuration = configuration;
this.tester = tester;
this.annotationDriven = annotationDriven;
this.databaseConfigOverrides =
new DatabaseConfigOverrides(configuration.getDatabaseConfigProperties());
this.expectedLifecycle =
new ExpectedLifecycle(configuration, tester, prepAndExpectedTestCase);
final ConnectionOwnership ownership = new ConnectionOwnership(
this::mayCloseConnectionAfterTest, tester::getOperationListener,
this::borrowingLifecycleRan);
this.leftOpenConnectionWarning = new LeftOpenConnectionWarning(ownership, () -> tester);
this.testScopedConnection = new TestScopedConnection(this::acquireConnection, ownership,
this::onConnectionAcquired);
this.setupTeardownLifecycle = new SetupTeardownLifecycle(configuration, tester,
testScopedConnection, annotationDriven);
if (annotationDriven)
{
installOperationListener();
}
}
/**
* The {@code closeConnectionAfterTest} input to this executor's {@link ConnectionOwnership}:
* the {@code @DbUnitConfig} flag, and on the prep/expected path also the test case's own
* {@link PrepAndExpectedTestCase#isCloseConnectionAfterTest()}. The connection a binding
* injects there is the test case's {@link PrepAndExpectedTestCase#getReusableConnection()},
* so an injected instance built to keep its connection open for its real owner must not
* have it closed here just because {@code @DbUnitConfig} says nothing about it.
*/
private boolean mayCloseConnectionAfterTest()
{
final boolean configuredToClose = configuration.isCloseConnectionAfterTest();
final PrepAndExpectedTestCase testCase = expectedLifecycle.getPrepAndExpectedTestCase();
final boolean testCaseCloses = !configuration.isExpected() || testCase == null
|| testCase.isCloseConnectionAfterTest();
return configuredToClose && testCaseCloses;
}
/**
* The {@code borrowingLifecycleRan} input to this executor's {@link ConnectionOwnership}:
* true unless the connection is held by a reused injected {@code @DbUnitTestCase} instance
* whose {@link ExpectedLifecycle#before()} failed before {@code configureTest()} - see
* {@link ExpectedLifecycle#isConnectionOwnedByUnconfiguredInjectedTestCase()}.
*/
private boolean borrowingLifecycleRan()
{
return !(configuration.isExpected()
&& expectedLifecycle.isConnectionOwnedByUnconfiguredInjectedTestCase());
}
private IDatabaseConnection acquireConnection() throws Exception
{
return configuration.isExpected()
? expectedLifecycle.ensureConfiguredTestCase().getReusableConnection()
: tester.getConnection();
}
/**
* Runs on each connection {@link #testScopedConnection} acquires: applies
* {@code @DbUnitProperty} values, and - on the annotation-driven setup/teardown path, where
* this executor's prep and teardown operations run through the tester rather than a
* transaction-managed {@link org.dbunit.operation.TransactionOperation} - warns once if
* that connection has autocommit off, and once if it is going to be left open with a tester
* that opens a new connection on every call (see {@link LeftOpenConnectionWarning}), the
* same checks {@link DefaultPrepAndExpectedTestCase} makes for the prep/expected path. The
* classic path resolves this connection only for the read-only row count check baseline,
* so an autocommit-off connection there is not a problem and neither warning is made; the
* prep/expected path warns via {@link DefaultPrepAndExpectedTestCase}'s own acquisition of
* the same connection.
*
* <p>On the setup/teardown path it first tells the tester's own listener about the
* connection - see {@link #notifyTesterListenerOfRetrieval}.
*/
private void onConnectionAcquired(final IDatabaseConnection connection)
{
if (!configuration.isExpected())
{
notifyTesterListenerOfRetrieval(connection);
}
applyDatabaseConfigProperties(connection);
if (annotationDriven && !configuration.isExpected())
{
autoCommitOffWarning.accept(connection);
leftOpenConnectionWarning.accept(connection);
}
}
/**
* Calls {@link IOperationListener#connectionRetrieved(IDatabaseConnection)} on the tester's
* own listener for a connection this executor resolved itself rather than through the
* tester's {@code onSetup()}/{@code onTearDown()}, which would have notified it. That
* callback is the documented place to configure a connection's {@code DatabaseConfig}, for
* example to enable the row count check, so skipping it here would leave a check enabled
* that way running on one route and silently off on another. The executor's own listener
* is bypassed: it would offer the connection to the baseline capture again, and
* {@link #onConnectionAcquired} applies the {@code @DbUnitProperty} values itself, after
* the tester's listener, as the executor's listener does. Not used on the prep/expected
* path, where {@link org.dbunit.DefaultPrepAndExpectedTestCase} tells the tester's own
* listener about the connection each of its set up and tear down operations retrieves.
*/
private void notifyTesterListenerOfRetrieval(final IDatabaseConnection connection)
{
final IOperationListener testersOwnListener =
withoutExecutorListener(tester.getOperationListener());
if (testersOwnListener == null)
{
return;
}
testersOwnListener.connectionRetrieved(connection);
}
/**
* Returns the tester this executor drives, so a binding can inject it as a parameter
* without resolving a second, independent instance.
*
* @return The tester passed to the constructor.
*/
public IDatabaseTester getTester()
{
return tester;
}
/**
* Returns the {@link PrepAndExpectedTestCase} this executor drives, or {@code null} when
* {@link AnnotatedTestConfiguration#isExpected()} is false, or it is true but nothing has
* constructed one yet - a binding resolving a parameter before {@link #beforeTest()} runs
* (e.g. a {@code @BeforeEach} parameter) sees {@code null} unless one was already injected
* through the constructor, or an earlier parameter resolution this same test already
* triggered {@link ExpectedLifecycle#ensureTestCase()} (e.g. a {@code Connection} parameter
* resolved via {@link #getConnection()}).
*
* @return The test case this executor drives, or {@code null} if none exists yet.
*/
public PrepAndExpectedTestCase getPrepAndExpectedTestCase()
{
return expectedLifecycle.getPrepAndExpectedTestCase();
}
/**
* Returns the connection this test's steps use, resolved at most once per test and reused
* by every caller instead of each asking independently - the row count check's own
* baseline/verify calls on the setup/teardown path, and a binding injecting an
* {@code IDatabaseConnection}/{@code Connection} parameter on either path. On the
* prep/expected path ({@link AnnotatedTestConfiguration#isExpected()} true), delegates to
* {@link PrepAndExpectedTestCase#getReusableConnection()} - constructing the test case first
* via {@link ExpectedLifecycle#ensureTestCase()} if it does not exist yet - instead of
* asking {@link #tester} directly, so a parameter injection shares that test case's own
* connection; on the setup/teardown path, asks {@link #tester} directly. For a tester with
* no connection caching of its own (e.g. a plain {@code JdbcDatabaseTester}), asking
* independently on either path would otherwise open one new physical connection per call.
* Closed in {@link #afterTest(boolean)} - see the class Javadoc.
*
* @return The connection to reuse, or {@code null} if none is available (e.g. a test
* double).
* @throws Exception If resolving the connection fails.
*/
public IDatabaseConnection getConnection() throws Exception
{
return testScopedConnection.getConnection();
}
/**
* Returns the connection {@link #getConnection()} has already resolved this test, without
* triggering resolution - {@code null} both before {@link #getConnection()} first runs and
* when the tester has no connection to offer. Used only by
* {@link ExecutorOperationListener} to recognize, at whatever later moment the tester's own
* {@code onSetup()}/{@code onTearDown()} happens to ask, whether the connection they are
* about to close is the one this executor is still holding onto.
*/
private IDatabaseConnection peekResolvedConnection()
{
return testScopedConnection.peekConnection();
}
/**
* Wraps the tester's operation listener in an {@link ExecutorOperationListener}, so that
* regardless of whether {@code @DbUnitProperty} is configured, this executor's own memoized
* connection (see the class Javadoc) is protected from a premature close by the tester's own
* setup/teardown machinery, and so the row count check baseline can be captured from
* whatever connection {@code onSetup()} retrieves on its own - see
* {@link #onListenerFirstConnectionRetrieved(IDatabaseConnection)}. Called from the
* constructor only for an {@code annotationDriven} test; the classic path leaves the
* tester's listener untouched (see the class Javadoc). {@link #restoreOperationListener()}
* puts the tester's own listener back once the test is over.
*/
private void installOperationListener()
{
listenerToRestore = withoutExecutorListener(tester.getOperationListener());
final IOperationListener delegate = listenerToRestore == null
? new DefaultOperationListener()
: listenerToRestore;
installedListener = new ExecutorOperationListener(this::applyDatabaseConfigProperties,
this::peekResolvedConnection, delegate, this::onListenerFirstConnectionRetrieved);
tester.setOperationListener(installedListener);
}
/**
* Returns {@code listener} with a wrapper an executor installed on an earlier test peeled
* off, so re-wrapping a tester shared across tests does not nest one layer per test.
* Anything else is returned as it is - including a caller's own
* {@link org.dbunit.ConnectionPreservingOperationListener}, which expresses who owns the
* connections and must stay in force beneath this executor's wrapper.
*/
private static IOperationListener withoutExecutorListener(final IOperationListener listener)
{
if (listener instanceof ExecutorOperationListener)
{
return ((ExecutorOperationListener) listener).getDelegate();
}
return listener;
}
/**
* Puts back the listener the tester carried before {@link #installOperationListener()},
* unless something else has replaced the executor's since - a test that installs a listener
* of its own mid-test owns it, and the executor's listener, left in its chain, is
* deactivated. A no-op when nothing was installed, or it was already put back.
*/
private void restoreOperationListener()
{
if (installedListener == null)
{
return;
}
installedListener.deactivate();
if (tester.getOperationListener() == installedListener)
{
tester.setOperationListener(listenerToRestore);
}
installedListener = null;
}
/**
* Runs when the tester's own {@code onSetup()}/{@code onTearDown()} retrieves a connection
* and notifies the installed {@link ExecutorOperationListener}: on the setup/teardown path,
* warns once on an autocommit-off connection (the single point every connection a prep or
* teardown operation runs on passes through - see {@link AutoCommitOffWarning}) and once on
* one that is going to be left open with nothing to close it (see
* {@link LeftOpenConnectionWarning}), then offers it to {@link AnnotatedRowCountCheck} for
* the piggybacked baseline capture. A no-op on the prep/expected path: the row count check
* there is the {@link PrepAndExpectedTestCase}'s own, and the connection identity is
* {@link PrepAndExpectedTestCase#getReusableConnection()}'s - adopting a
* directly-retrieved one here would reintroduce, for a
* {@link PrepAndExpectedTestCase} implementation that drives {@code tester.onSetup()}
* itself, the connection-identity split {@link #getConnection()}'s path split exists to
* prevent.
*/
private void onListenerFirstConnectionRetrieved(final IDatabaseConnection connection)
{
if (configuration.isExpected())
{
return;
}
autoCommitOffWarning.accept(connection);
leftOpenConnectionWarning.accept(connection);
setupTeardownLifecycle.adoptAndCaptureBaselineOnFirstConnection(connection);
}
/**
* Applies {@code @DbUnitProperty} values to {@code connection}'s {@link DatabaseConfig}; a
* no-op when there are none. Called from both {@link #getConnection()}, so the row count
* check's baseline/verify/override calls always see property state already applied before
* they read it, and from {@link ExecutorOperationListener}, since a tester's
* {@code onSetup()}/{@code onTearDown()} may resolve their own connection independently of
* {@link #getConnection()}'s memoized one. {@link #restoreWhatThisExecutorChanged()} puts
* back what this replaced, so a connection kept past the test carries none of the values
* forward.
*
* <p>A no-op on the prep/expected path, where the {@link PrepAndExpectedTestCase} applies the
* values to the connection it owns and puts them back itself: this executor applying them as
* well, after it, would take the test case's values for the ones to restore.
*
* @param connection The connection whose {@link DatabaseConfig} to apply the values to.
*/
private void applyDatabaseConfigProperties(final IDatabaseConnection connection)
{
if (configuration.isExpected())
{
return;
}
try
{
databaseConfigOverrides.applyTo(connection.getConfig());
} catch (final DatabaseUnitException e)
{
throw new IllegalStateException("Failed to apply @DbUnitProperty values.", e);
}
}
/**
* Puts back what {@link #applyDatabaseConfigProperties(IDatabaseConnection)} replaced, then
* the tester's own listener - see {@link #restoreOperationListener()} - so the one failing
* cannot leave the other undone.
*/
private void restoreWhatThisExecutorChanged()
{
try
{
databaseConfigOverrides.restore();
} finally
{
restoreOperationListener();
}
}
/**
* Runs every before-test step by delegating to the path {@link AnnotatedTestConfiguration#isExpected()}
* selects: {@link SetupTeardownLifecycle#before()} or {@link ExpectedLifecycle#before()}.
*
* @throws Exception If any step fails.
*/
public void beforeTest() throws Exception
{
if (configuration.isExpected())
{
expectedLifecycle.before();
} else
{
setupTeardownLifecycle.before();
}
}
/**
* Runs every after-test step by delegating to the path {@link AnnotatedTestConfiguration#isExpected()}
* selects - {@link SetupTeardownLifecycle#after(boolean)} or
* {@link ExpectedLifecycle#after(boolean)} - then closes the connection
* {@link #testScopedConnection} memoized, if one was ever resolved, so a difference
* reported by the row count check still leaves it closed.
*
* <p>When both the step and closing the connection fail, the step's failure is the one
* thrown, with the close failure attached to it via
* {@link Throwable#addSuppressed(Throwable)} rather than replacing it - a plain {@code
* finally} block would otherwise let the close failure silently discard the more useful
* diagnostic (e.g. which table the row count check found unexpectedly changed). The step's
* failure is caught as {@link Throwable}, not just {@link Exception}, and rethrown with its
* original static type preserved: a comparison mismatch on the prep/expected path fails via
* {@link org.dbunit.assertion.DbComparisonFailure}, an {@link Error} subclass, not an
* {@code Exception} - catching only {@code Exception} would skip closing the connection on
* every ordinary verification failure, the single most common way this method's step throws
* at all.
*
* @param testFailed Whether the test method itself already threw; when {@code true},
* verification is skipped so a difference does not mask the real failure.
* @throws Exception If any step fails with a checked exception.
* @throws Error If any step fails with an {@code Error}, e.g. a comparison mismatch.
*/
public void afterTest(final boolean testFailed) throws Exception
{
afterTestRan = true;
try
{
runAfterStepThenRelease(testFailed);
} finally
{
restoreWhatThisExecutorChanged();
}
}
private void runAfterStepThenRelease(final boolean testFailed) throws Exception
{
try
{
if (configuration.isExpected())
{
expectedLifecycle.after(testFailed);
} else
{
setupTeardownLifecycle.after(testFailed);
}
} catch (final Throwable primaryFailure)
{
try
{
testScopedConnection.release();
} catch (final Exception closeFailure)
{
primaryFailure.addSuppressed(closeFailure);
}
throw primaryFailure;
}
testScopedConnection.release();
}
/**
* Closes the connection {@link #testScopedConnection} memoized, when {@link #afterTest(boolean)}
* never ran. A binding calls this once the test method's whole lifecycle is over, to cover
* the one path {@link #afterTest(boolean)} cannot: a {@code @BeforeEach} method that fails
* after a {@code Connection}/{@code IDatabaseConnection} parameter already resolved this
* executor's connection, so the test method - and with it the binding's after-test callback -
* never runs. A no-op when {@link #afterTest(boolean)} already ran, which released the
* connection itself, and when no connection was ever resolved, which it never acquires.
* Closes under the same {@link ConnectionOwnership#mayClose()} rules as
* {@link #afterTest(boolean)}.
*
* @throws Exception If closing the connection fails.
*/
public void releaseIfAfterTestDidNotRun() throws Exception
{
if (afterTestRan)
{
return;
}
Exception failure = null;
try
{
testScopedConnection.release();
} catch (final Exception releaseFailure)
{
failure = releaseFailure;
}
try
{
expectedLifecycle.undoConfiguration();
} catch (final RuntimeException undoFailure)
{
failure = withSuppressed(failure, undoFailure);
}
try
{
restoreWhatThisExecutorChanged();
} catch (final RuntimeException restoreFailure)
{
failure = withSuppressed(failure, restoreFailure);
}
if (failure != null)
{
throw failure;
}
}
/**
* Returns {@code primary}, with {@code later} attached to it as suppressed, or {@code later}
* itself when nothing has failed yet - so the first failure is the one that surfaces and a
* cleanup step after it cannot replace it.
*/
private static Exception withSuppressed(final Exception primary, final Exception later)
{
if (primary == null)
{
return later;
}
primary.addSuppressed(later);
return primary;
}
}