diff --git a/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java new file mode 100644 index 000000000..29b94b7aa --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.lang3.builder; + +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Abstracts supplying an instance of {@code T}. Use to implement the builder pattern. + * + * @param the type of instances to build. + * @param the type of builder. + * @param The kind of thrown exception or error. + * @since 3.14.0 + */ +public abstract class AbstractSupplier, E extends Throwable> implements FailableSupplier { + + /** + * Returns this instance typed as the proper subclass type. + * + * @return this instance typed as the proper subclass type. + */ + @SuppressWarnings("unchecked") + protected B asThis() { + return (B) this; + } + +} diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java index 7786c8057..fb61e778f 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java @@ -17,6 +17,13 @@ package org.apache.commons.lang3.concurrent; +import java.util.Objects; + +import org.apache.commons.lang3.builder.AbstractSupplier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * Abstracts and defines operations for ConcurrentInitializer implementations. * @@ -26,15 +33,150 @@ package org.apache.commons.lang3.concurrent; */ public abstract class AbstractConcurrentInitializer implements ConcurrentInitializer { + /** + * Builds a new instance for subclasses. + * + * @param the type of the object managed by the initializer class. + * @param the type of the initializer class. + * @param the type of builder. + * @param The exception type thrown by {@link #initialize()}. + */ + public abstract static class AbstractBuilder, T, B extends AbstractBuilder, E extends Exception> + extends AbstractSupplier { + + /** + * Closer consumer called by {@link #close()}. + */ + private FailableConsumer closer = FailableConsumer.nop(); + + /** + * Initializer supplier called by {@link #initialize()}. + */ + private FailableSupplier initializer = FailableSupplier.nul(); + + /** + * Gets the closer consumer called by {@link #close()}. + * + * @return the closer consumer called by {@link #close()}. + */ + public FailableConsumer getCloser() { + return closer; + } + + /** + * Gets the initializer supplier called by {@link #initialize()}. + * + * @return the initializer supplier called by {@link #initialize()}. + */ + public FailableSupplier getInitializer() { + return initializer; + } + + /** + * Sets the closer consumer called by {@link #close()}. + * + * @param closer the consumer called by {@link #close()}. + * @return this + */ + public B setCloser(final FailableConsumer closer) { + this.closer = closer != null ? closer : FailableConsumer.nop(); + return asThis(); + } + + /** + * Sets the initializer supplier called by {@link #initialize()}. + * + * @param initializer the supplier called by {@link #initialize()}. + * @return this + */ + public B setInitializer(final FailableSupplier initializer) { + this.initializer = initializer != null ? initializer : FailableSupplier.nul(); + return asThis(); + } + + } + + /** + * Closer consumer called by {@link #close()}. + */ + private final FailableConsumer closer; + + /** + * Initializer supplier called by {@link #initialize()}. + */ + private final FailableSupplier initializer; + + /** + * Constructs a new instance. + */ + public AbstractConcurrentInitializer() { + this(FailableSupplier.nul(), FailableConsumer.nop()); + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + AbstractConcurrentInitializer(final FailableSupplier initializer, final FailableConsumer closer) { + this.closer = Objects.requireNonNull(closer, "closer"); + this.initializer = Objects.requireNonNull(initializer, "initializer"); + } + + /** + * Calls the closer with the manager object. + * + * @throws ConcurrentException Thrown by the closer. + * @since 3.14.0 + */ + public void close() throws ConcurrentException { + if (isInitialized()) { + try { + closer.accept(get()); + } catch (final Exception e) { + // This intentionally does not duplicate the logic in initialize + // or care about the generic type E. + // + // initialize may run inside a Future and it does not make sense + // to wrap an exception stored inside a Future. However close() + // always runs on the current thread so it always wraps in a + // ConcurrentException + throw new ConcurrentException(ExceptionUtils.throwUnchecked(e)); + } + } + } + /** * Creates and initializes the object managed by this {@code * ConcurrentInitializer}. This method is called by {@link #get()} when the object is accessed for the first time. An implementation can focus on the * creation of the object. No synchronization is needed, as this is already handled by {@code get()}. + *

+ * Subclasses and clients that do not provide an initializer are expected to implement this method. + *

* * @return the managed data object * @throws E if an error occurs during object creation */ - protected abstract T initialize() throws E; + @SuppressWarnings("unchecked") + protected T initialize() throws E { + try { + return initializer.get(); + } catch (final Exception e) { + // Do this first so we don't pass a RuntimeException or Error into an exception constructor + ExceptionUtils.throwUnchecked(e); + + // Depending on the subclass of AbstractConcurrentInitializer E can be Exception or ConcurrentException + // if E is Exception the if statement below will always be true, and the new Exception object created + // in getTypedException will never be thrown. If E is ConcurrentException and the if statement is false + // we throw the ConcurrentException returned from getTypedException, which wraps the original exception. + final E typedException = getTypedException(e); + if (typedException.getClass().isAssignableFrom(e.getClass())) { + throw (E) e; + } + throw typedException; + } + } /** * Returns true if initialization has been completed. If initialization threw an exception this will return false, but it will return true if a subsequent @@ -45,4 +187,12 @@ public abstract class AbstractConcurrentInitializer impl */ protected abstract boolean isInitialized(); + /** + * Gets an Exception with a type of E as defined by a concrete subclass of this class. + * + * @param e The actual exception that was thrown + * @return a new exception with the actual type of E, that wraps e. + */ + protected abstract E getTypedException(Exception e); + } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java index 42706f79c..affd8c81d 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java @@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A specialized implementation of the {@link ConcurrentInitializer} interface * based on an {@link AtomicReference} variable. @@ -62,13 +65,58 @@ import java.util.concurrent.atomic.AtomicReference; * @since 3.0 * @param the type of the object managed by this initializer class */ -public abstract class AtomicInitializer extends AbstractConcurrentInitializer { +public class AtomicInitializer extends AbstractConcurrentInitializer { + + /** + * Builds a new instance. + * + * @param the type of the object managed by the initializer. + * @param the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder, T> extends AbstractBuilder, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new AtomicInitializer(getInitializer(), getCloser()); + } + + } private static final Object NO_INIT = new Object(); /** Holds the reference to the managed object. */ private final AtomicReference reference = new AtomicReference<>(getNoInit()); + /** + * Creates a new builder. + * + * @param the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static Builder, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + */ + public AtomicInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private AtomicInitializer(final FailableSupplier initializer, final FailableConsumer closer) { + super(initializer, closer); + } + /** * Returns the object managed by this initializer. The object is created if * it is not available yet and stored internally. This method always returns @@ -109,4 +157,12 @@ public abstract class AtomicInitializer extends AbstractConcurrentInitializer public boolean isInitialized() { return reference.get() != NO_INIT; } + + /** + * {@inheritDoc} + */ + @Override + protected ConcurrentException getTypedException(Exception e) { + return new ConcurrentException(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java index 5e2911b50..7015c53f0 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java @@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A specialized {@link ConcurrentInitializer} implementation which is similar * to {@link AtomicInitializer}, but ensures that the {@link #initialize()} @@ -51,7 +54,24 @@ import java.util.concurrent.atomic.AtomicReference; * @since 3.0 * @param the type of the object managed by this initializer class */ -public abstract class AtomicSafeInitializer extends AbstractConcurrentInitializer { +public class AtomicSafeInitializer extends AbstractConcurrentInitializer { + + /** + * Builds a new instance. + * + * @param the type of the object managed by the initializer. + * @param the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder, T> extends AbstractBuilder, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new AtomicSafeInitializer(getInitializer(), getCloser()); + } + + } private static final Object NO_INIT = new Object(); @@ -61,6 +81,34 @@ public abstract class AtomicSafeInitializer extends AbstractConcurrentInitial /** Holds the reference to the managed object. */ private final AtomicReference reference = new AtomicReference<>(getNoInit()); + /** + * Creates a new builder. + * + * @param the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static Builder, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + */ + public AtomicSafeInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private AtomicSafeInitializer(final FailableSupplier initializer, final FailableConsumer closer) { + super(initializer, closer); + } + /** * Gets (and initialize, if not initialized yet) the required object * @@ -97,4 +145,12 @@ public abstract class AtomicSafeInitializer extends AbstractConcurrentInitial public boolean isInitialized() { return reference.get() != NO_INIT; } + + /** + * {@inheritDoc} + */ + @Override + protected ConcurrentException getTypedException(Exception e) { + return new ConcurrentException(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java index 91ee1c015..dd95ed551 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java @@ -23,6 +23,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A class that allows complex initialization operations in a background task. * @@ -82,7 +85,42 @@ import java.util.concurrent.Future; * @since 3.0 * @param the type of the object managed by this initializer class */ -public abstract class BackgroundInitializer extends AbstractConcurrentInitializer { +public class BackgroundInitializer extends AbstractConcurrentInitializer { + + /** + * Builds a new instance. + * + * @param the type of the object managed by the initializer. + * @param the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder, T> extends AbstractBuilder, Exception> { + + /** + * The external executor service for executing tasks. null is an permitted value. + */ + private ExecutorService externalExecutor; + + /** + * Sets the external executor service for executing tasks. null is an permitted value. + * + * @see org.apache.commons.lang3.concurrent.BackgroundInitializer#setExternalExecutor(ExecutorService) + * + * @param externalExecutor the {@link ExecutorService} to be used. + * @return this + */ + public Builder setExternalExecutor(final ExecutorService externalExecutor) { + this.externalExecutor = externalExecutor; + return asThis(); + } + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new BackgroundInitializer(getInitializer(), getCloser(), externalExecutor); + } + + } /** The external executor service for executing tasks. */ private ExecutorService externalExecutor; // @GuardedBy("this") @@ -115,6 +153,29 @@ public abstract class BackgroundInitializer extends AbstractConcurrentInitial setExternalExecutor(exec); } + /** + * Creates a new builder. + * + * @param the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static Builder, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + * @param exec the {@link ExecutorService} to be used @see #setExternalExecutor(ExecutorService) + */ + private BackgroundInitializer(final FailableSupplier initializer, final FailableConsumer closer, final ExecutorService exec) { + super(initializer, closer); + setExternalExecutor(exec); + } + /** * Returns the external {@link ExecutorService} to be used by this class. * @@ -341,4 +402,13 @@ public abstract class BackgroundInitializer extends AbstractConcurrentInitial } } } + + /** + * {@inheritDoc} + */ + @Override + protected Exception getTypedException(Exception e) { + //This Exception object will be used for type comparison in AbstractConcurrentInitializer.initialize but not thrown + return new Exception(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java index cccb43d92..81d6efab0 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java @@ -119,4 +119,13 @@ public class CallableBackgroundInitializer extends BackgroundInitializer { private void checkCallable(final Callable callable) { Objects.requireNonNull(callable, "callable"); } + + /** + * {@inheritDoc} + */ + @Override + protected Exception getTypedException(Exception e) { + //This Exception object will be used for type comparison in AbstractConcurrentInitializer.initialize but not thrown + return new Exception(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java index 7a69bfd42..acdfead21 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java @@ -16,29 +16,25 @@ */ package org.apache.commons.lang3.concurrent; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** - * This class provides a generic implementation of the lazy initialization - * pattern. + * This class provides a generic implementation of the lazy initialization pattern. * *

- * Sometimes an application has to deal with an object only under certain - * circumstances, e.g. when the user selects a specific menu item or if a - * special event is received. If the creation of the object is costly or the - * consumption of memory or other system resources is significant, it may make - * sense to defer the creation of this object until it is really needed. This is - * a use case for the lazy initialization pattern. + * Sometimes an application has to deal with an object only under certain circumstances, e.g. when the user selects a specific menu item or if a special event + * is received. If the creation of the object is costly or the consumption of memory or other system resources is significant, it may make sense to defer the + * creation of this object until it is really needed. This is a use case for the lazy initialization pattern. *

*

- * This abstract base class provides an implementation of the double-check idiom - * for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd - * edition, item 71. The class already implements all necessary synchronization. - * A concrete subclass has to implement the {@code initialize()} method, which + * This abstract base class provides an implementation of the double-check idiom for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd + * edition, item 71. The class already implements all necessary synchronization. A concrete subclass has to implement the {@code initialize()} method, which * actually creates the wrapped data object. *

*

- * As an usage example consider that we have a class {@code ComplexObject} whose - * instantiation is a complex operation. In order to apply lazy initialization - * to this class, a subclass of {@link LazyInitializer} has to be created: + * As an usage example consider that we have a class {@code ComplexObject} whose instantiation is a complex operation. In order to apply lazy initialization to + * this class, a subclass of {@link LazyInitializer} has to be created: *

* *
@@ -51,9 +47,8 @@ package org.apache.commons.lang3.concurrent;
  * 
* *

- * Access to the data object is provided through the {@code get()} method. So, - * code that wants to obtain the {@code ComplexObject} instance would simply - * look like this: + * Access to the data object is provided through the {@code get()} method. So, code that wants to obtain the {@code ComplexObject} instance would simply look + * like this: *

* *
@@ -65,32 +60,75 @@ package org.apache.commons.lang3.concurrent;
  * 
* *

- * If multiple threads call the {@code get()} method when the object has not yet - * been created, they are blocked until initialization completes. The algorithm - * guarantees that only a single instance of the wrapped object class is - * created, which is passed to all callers. Once initialized, calls to the - * {@code get()} method are pretty fast because no synchronization is needed - * (only an access to a volatile member field). + * If multiple threads call the {@code get()} method when the object has not yet been created, they are blocked until initialization completes. The algorithm + * guarantees that only a single instance of the wrapped object class is created, which is passed to all callers. Once initialized, calls to the {@code get()} + * method are pretty fast because no synchronization is needed (only an access to a volatile member field). *

* * @since 3.0 - * @param the type of the object managed by this initializer class + * @param the type of the object managed by the initializer. */ -public abstract class LazyInitializer extends AbstractConcurrentInitializer { +public class LazyInitializer extends AbstractConcurrentInitializer { + /** + * Builds a new instance. + * + * @param the type of the object managed by the initializer. + * @param the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder, T> extends AbstractBuilder, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new LazyInitializer(getInitializer(), getCloser()); + } + + } + + /** + * A unique value indicating an un-initialzed instance. + */ private static final Object NO_INIT = new Object(); + /** + * Creates a new builder. + * + * @param the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static Builder, T> builder() { + return new Builder<>(); + } + /** Stores the managed object. */ @SuppressWarnings("unchecked") private volatile T object = (T) NO_INIT; /** - * Returns the object wrapped by this instance. On first access the object - * is created. After that it is cached and can be accessed pretty fast. + * Constructs a new instance. + */ + public LazyInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private LazyInitializer(final FailableSupplier initializer, final FailableConsumer closer) { + super(initializer, closer); + } + + /** + * Returns the object wrapped by this instance. On first access the object is created. After that it is cached and can be accessed pretty fast. * * @return the object initialized by this {@link LazyInitializer} - * @throws ConcurrentException if an error occurred during initialization of - * the object + * @throws ConcurrentException if an error occurred during initialization of the object */ @Override public T get() throws ConcurrentException { @@ -121,4 +159,12 @@ public abstract class LazyInitializer extends AbstractConcurrentInitializer child : childInitializers.values()) { + try { + child.close(); + } catch (Exception e) { + if (exception == null) { + exception = new ConcurrentException(); + } + + if (e instanceof ConcurrentException) { + // Because ConcurrentException is only created by classes in this package + // we can safely unwrap it. + exception.addSuppressed(e.getCause()); + } else { + exception.addSuppressed(e); + } + } + } + + if (exception != null) { + throw exception; + } + } + /** * A data class for storing the results of the background initialization * performed by {@link MultiBackgroundInitializer}. Objects of this inner diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java new file mode 100644 index 000000000..af77f3c61 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.sql.SQLException; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.Test; + +/** + * An abstract base class for tests of exceptions thrown during initialize and close methods + * on concrete {@code ConcurrentInitializer} implementations. + * + * This class provides some basic tests for initializer implementations. Derived + * class have to create a {@link ConcurrentInitializer} object on which the + * tests are executed. + */ +public abstract class AbstractConcurrentInitializerCloseAndExceptionsTest extends AbstractConcurrentInitializerTest { + + /** + * This method tests that if a AbstractConcurrentInitializer.initialize method catches a + * ConcurrentException it will rethrow it without wrapping it. + */ + @Test + public void testSupplierThrowsConcurrentException() { + final ConcurrentException concurrentException = new ConcurrentException(); + + @SuppressWarnings("unchecked") + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + () -> { + if ("test".equals("test")) { + throw concurrentException; + } + return new CloseableObject(); + }, + FailableConsumer.NOP); + try { + initializer.get(); + fail(); + } catch (ConcurrentException e) { + assertEquals(concurrentException, e); + } + } + + /** + * This method tests that if AbstractConcurrentInitializer.initialize catches a checked + * exception it will rethrow it wrapped in a ConcurrentException + */ + @SuppressWarnings("unchecked") //for NOP + @Test + public void testSupplierThrowsCheckedException() { + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + () -> methodThatThrowsException(ExceptionToThrow.IOException), + FailableConsumer.NOP); + assertThrows(ConcurrentException.class, () -> initializer.get()); + } + + /** + * This method tests that if AbstractConcurrentInitializer.initialize catches a runtime exception + * it will not be wrapped in a ConcurrentException + */ + @SuppressWarnings("unchecked") + @Test + public void testSupplierThrowsRuntimeException() { + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + () -> methodThatThrowsException(ExceptionToThrow.NullPointerException), + FailableConsumer.NOP); + assertThrows(NullPointerException.class, () -> initializer.get()); + } + + /** + * This method tests that if AbstractConcurrentInitializer.close catches a + * ConcurrentException it will rethrow it wrapped in a ConcurrentException + */ + @SuppressWarnings("rawtypes") + @Test + public void testCloserThrowsCheckedException() throws ConcurrentException { + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + CloseableObject::new, + (CloseableObject) -> methodThatThrowsException(ExceptionToThrow.IOException)); + try { + initializer.get(); + ((AbstractConcurrentInitializer) initializer).close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertThat(e.getCause(), instanceOf(IOException.class)); + } + } + + /** + * This method tests that if AbstractConcurrentInitializer.close catches a + * RuntimeException it will throw it withuot wrapping it in a ConcurrentException + */ + @SuppressWarnings("rawtypes") + @Test + public void testCloserThrowsRuntimeException() throws ConcurrentException { + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + CloseableObject::new, + (CloseableObject) -> methodThatThrowsException(ExceptionToThrow.NullPointerException)); + + initializer.get(); + assertThrows(NullPointerException.class, () -> { + ((AbstractConcurrentInitializer) initializer).close(); + }); + } + + /** + * This method tests that if AbstractConcurrentInitializer.close actually closes the wrapped object + */ + @SuppressWarnings("rawtypes") + @Test + public void testWorkingCloser() throws Exception { + final ConcurrentInitializer initializer = createInitializerThatThrowsException( + CloseableObject::new, + CloseableObject::close); + + CloseableObject cloesableObject = initializer.get(); + assertFalse(cloesableObject.isClosed()); + ((AbstractConcurrentInitializer) initializer).close(); + assertTrue(cloesableObject.isClosed()); + } + + protected enum ExceptionToThrow { + IOException, + SQLException, + NullPointerException + } + + // The use of enums rather than accepting an Exception as the input means we can have + // multiple exception types on the method signature. + protected static CloseableObject methodThatThrowsException(ExceptionToThrow input) throws IOException, SQLException, ConcurrentException { + switch (input) { + case IOException: + throw new IOException(); + case SQLException: + throw new SQLException(); + case NullPointerException: + throw new NullPointerException(); + default: + fail(); + return new CloseableObject(); + } + } + + protected abstract ConcurrentInitializer createInitializerThatThrowsException( + FailableSupplier supplier, FailableConsumer closer); + + protected static final class CloseableObject { + boolean closed; + + public boolean isClosed() { + return closed; + } + + public void close() { + closed = true; + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java new file mode 100644 index 000000000..41413ff84 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Test class for {@code AtomicInitializer}. + */ +public class AtomicInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + /** + * Returns the initializer to be tested. + * + * @return the {@code AtomicInitializer} + */ + @Override + protected ConcurrentInitializer createInitializer() { + return AtomicInitializer.builder().setInitializer(() -> new Object()).get(); + } + + @Override + protected ConcurrentInitializer createInitializerThatThrowsException( + final FailableSupplier supplier, + final FailableConsumer closer) { + return AtomicInitializer.builder().setInitializer(supplier).setCloser(closer).get(); + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java new file mode 100644 index 000000000..9bbfde19b --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.concurrent.AbstractConcurrentInitializerCloseAndExceptionsTest.CloseableObject; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@code AtomicSafeInitializer} which also serves as a simple example. + */ +public class AtomicSafeInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + + /** An initCounter used in testing. Reset before each test */ + private AtomicInteger initCounter = new AtomicInteger(); + + /** A supplier method used in testing */ + private Object incAndMakeObject() { + initCounter.incrementAndGet(); + return new Object(); + } + + @Override + protected ConcurrentInitializer createInitializerThatThrowsException( + final FailableSupplier supplier, + final FailableConsumer closer) { + return AtomicSafeInitializer.builder().setInitializer(supplier).setCloser(closer).get(); + } + + @BeforeEach + public void setUp() { + initCounter = new AtomicInteger(); + } + + /** + * Creates the initializer to be tested. + * + * @return the {@code AtomicSafeInitializer} under test + */ + @Override + protected ConcurrentInitializer createInitializer() { + return AtomicSafeInitializer.builder().setInitializer(this::incAndMakeObject).get(); + } + + /** + * Tests that initialize() is called only once. + * + * @throws org.apache.commons.lang3.concurrent.ConcurrentException because {@link #testGetConcurrent()} may throw it + * @throws InterruptedException because {@link #testGetConcurrent()} may throw it + */ + @Test + public void testNumberOfInitializeInvocations() throws ConcurrentException, InterruptedException { + testGetConcurrent(); + assertEquals(1, initCounter.get(), "Wrong number of invocations"); + } + + @Test + public void testGetThatReturnsNullFirstTime() throws ConcurrentException { + final AtomicSafeInitializer initializer = new AtomicSafeInitializer() { + final AtomicBoolean firstRun = new AtomicBoolean(true); + + @Override + protected Object initialize() { + if (firstRun.getAndSet(false)) { + return null; + } else { + return new Object(); + } + } + }; + + assertNull(initializer.get()); + assertNull(initializer.get()); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java new file mode 100644 index 000000000..3a6b1da1a --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.Test; + +public class BackgroundInitializerSupplierTest extends BackgroundInitializerTest { + + /** + * Tests that close() method closes the wrapped object + * + * @throws Exception + */ + @Test + public void testClose() throws Exception { + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); + assertFalse(init.getCloseableCounter().isClosed(), "closed without close() call"); + init.close(); + assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded before start()"); + init.start(); + init.get(); //ensure the Future has completed. + assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded after start() but before close()"); + init.close(); + assertTrue(init.getCloseableCounter().isClosed(), "closed() did not succeed"); + } + + /** + * Tests that close() wraps a checked exception in a ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithCheckedException() throws Exception { + + final IOException ioException = new IOException(); + final FailableConsumer IOExceptionConsumer = (CloseableCounter cc) -> { + throw ioException; + }; + + final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(IOExceptionConsumer); + init.start(); + init.get(); //ensure the Future has completed. + try { + init.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(ioException, e.getCause()); + } + } + + /** + * Tests that close() throws a runtime exception + * + * @throws Exception + */ + @Test + public void testCloseWithRuntimeException() throws Exception { + + final NullPointerException npe = new NullPointerException(); + final FailableConsumer NullPointerExceptionConsumer = (CloseableCounter cc) -> { + throw npe; + }; + + final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(NullPointerExceptionConsumer); + init.start(); + init.get(); //ensure the Future has completed. + try { + init.close(); + fail(); + } catch (Exception e) { + assertSame(npe, e); + } + } + + /** + * A concrete implementation of BackgroundInitializer. It is designed as a warpper so the test can + * use the same builder pattern that real code will. + */ + protected static final class SupplierBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl { + + SupplierBackgroundInitializerTestImpl() { + super(); + setSupplierAndCloser((CloseableCounter cc) -> cc.close()); + } + + SupplierBackgroundInitializerTestImpl(FailableConsumer consumer) { + super(); + setSupplierAndCloser(consumer); + } + + SupplierBackgroundInitializerTestImpl(final ExecutorService exec) { + super(exec); + setSupplierAndCloser((CloseableCounter cc) -> cc.close()); + } + + private void setSupplierAndCloser(FailableConsumer consumer) { + try { + // Use reflection here because the constructors we need are private + FailableSupplier supplier = () -> initializeInternal(); + Field initializer = AbstractConcurrentInitializer.class.getDeclaredField("initializer"); + initializer.setAccessible(true); + initializer.set(this, supplier); + + Field closer = AbstractConcurrentInitializer.class.getDeclaredField("closer"); + closer.setAccessible(true); + closer.set(this, consumer); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + } + } + + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() { + return new SupplierBackgroundInitializerTestImpl(); + } + + protected SupplierBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) { + return new SupplierBackgroundInitializerTestImpl(exec); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java index 90193f2af..8714beaaf 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java @@ -29,6 +29,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.AbstractLangTest; @@ -42,10 +44,10 @@ public class BackgroundInitializerTest extends AbstractLangTest { * * @param init the initializer to test */ - private void checkInitialize(final BackgroundInitializerTestImpl init) throws ConcurrentException { - final Integer result = init.get(); + private void checkInitialize(final AbstractBackgroundInitializerTestImpl init) throws ConcurrentException { + final Integer result = init.get().getInitializeCalls(); assertEquals(1, result.intValue(), "Wrong result"); - assertEquals(1, init.initializeCalls, "Wrong number of invocations"); + assertEquals(1, init.getCloseableCounter().getInitializeCalls(), "Wrong number of invocations"); assertNotNull(init.getFuture(), "No future"); } @@ -54,7 +56,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitialize() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); checkInitialize(init); } @@ -65,7 +67,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetActiveExecutorBeforeStart() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertNull(init.getActiveExecutor(), "Got an executor"); } @@ -76,7 +78,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { public void testGetActiveExecutorExternal() throws InterruptedException, ConcurrentException { final ExecutorService exec = Executors.newSingleThreadExecutor(); try { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl( + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl( exec); init.start(); assertSame(exec, init.getActiveExecutor(), "Wrong executor"); @@ -92,7 +94,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetActiveExecutorTemp() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); assertNotNull(init.getActiveExecutor(), "No active executor"); checkInitialize(init); @@ -104,7 +106,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeTempExecutor() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertTrue(init.start(), "Wrong result of start()"); checkInitialize(init); assertTrue(init.getActiveExecutor().isShutdown(), "Executor not shutdown"); @@ -118,7 +120,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { public void testSetExternalExecutor() throws ConcurrentException { final ExecutorService exec = Executors.newCachedThreadPool(); try { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.setExternalExecutor(exec); assertEquals(exec, init.getExternalExecutor(), "Wrong executor service"); assertTrue(init.start(), "Wrong result of start()"); @@ -137,7 +139,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testSetExternalExecutorAfterStart() throws ConcurrentException, InterruptedException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); final ExecutorService exec = Executors.newSingleThreadExecutor(); try { @@ -155,7 +157,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testStartMultipleTimes() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertTrue(init.start(), "Wrong result for start()"); for (int i = 0; i < 10; i++) { assertFalse(init.start(), "Could start again"); @@ -168,7 +170,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetBeforeStart() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertThrows(IllegalStateException.class, init::get); } @@ -178,7 +180,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetRuntimeException() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); final RuntimeException rex = new RuntimeException(); init.ex = rex; init.start(); @@ -192,7 +194,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetCheckedException() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); final Exception ex = new Exception(); init.ex = ex; init.start(); @@ -208,7 +210,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { @Test public void testGetInterruptedException() throws InterruptedException { final ExecutorService exec = Executors.newSingleThreadExecutor(); - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl( + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl( exec); final CountDownLatch latch1 = new CountDownLatch(1); init.shouldSleep = true; @@ -242,7 +244,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedFalse() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertFalse(init.isStarted(), "Already started"); } @@ -251,7 +253,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedTrue() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); assertTrue(init.isStarted(), "Not started"); } @@ -261,7 +263,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedAfterGet() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); checkInitialize(init); assertTrue(init.isStarted(), "Not started"); @@ -272,7 +274,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsInitialized() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.enableLatch(); init.start(); assertTrue(init.isStarted(), "Not started"); //Started and Initialized should return opposite values @@ -282,29 +284,37 @@ public class BackgroundInitializerTest extends AbstractLangTest { assertTrue(init.isInitialized(), "Not initalized after releasing latch"); } + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() { + return new MethodBackgroundInitializerTestImpl(); + } + + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) { + return new MethodBackgroundInitializerTestImpl(exec); + } + /** * A concrete implementation of BackgroundInitializer. It also overloads * some methods that simplify testing. */ - private static final class BackgroundInitializerTestImpl extends - BackgroundInitializer { + protected static class AbstractBackgroundInitializerTestImpl extends + BackgroundInitializer { /** An exception to be thrown by initialize(). */ Exception ex; /** A flag whether the background task should sleep a while. */ boolean shouldSleep; - /** The number of invocations of initialize(). */ - volatile int initializeCalls; - /** A latch tests can use to control when initialize completes. */ final CountDownLatch latch = new CountDownLatch(1); boolean waitForLatch = false; - BackgroundInitializerTestImpl() { + /** An object containing the state we are testing */ + CloseableCounter counter = new CloseableCounter(); + + AbstractBackgroundInitializerTestImpl() { } - BackgroundInitializerTestImpl(final ExecutorService exec) { + AbstractBackgroundInitializerTestImpl(final ExecutorService exec) { super(exec); } @@ -316,14 +326,17 @@ public class BackgroundInitializerTest extends AbstractLangTest { latch.countDown(); } + public CloseableCounter getCloseableCounter() { + return counter; + } + /** * Records this invocation. Optionally throws an exception or sleeps a * while. * * @throws Exception in case of an error */ - @Override - protected Integer initialize() throws Exception { + protected CloseableCounter initializeInternal() throws Exception { if (ex != null) { throw ex; } @@ -333,7 +346,47 @@ public class BackgroundInitializerTest extends AbstractLangTest { if (waitForLatch) { latch.await(); } - return Integer.valueOf(++initializeCalls); + return counter.increment(); + } + } + + protected static class MethodBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl { + + MethodBackgroundInitializerTestImpl() { + } + + MethodBackgroundInitializerTestImpl(final ExecutorService exec) { + super(exec); + } + + @Override + protected CloseableCounter initialize() throws Exception { + return initializeInternal(); + } + } + + protected static class CloseableCounter { + /** The number of invocations of initialize(). */ + AtomicInteger initializeCalls = new AtomicInteger(); + + /** Has the close consumer successfully reached this object. */ + AtomicBoolean closed = new AtomicBoolean(); + + public CloseableCounter increment() { + initializeCalls.incrementAndGet(); + return this; + } + + public int getInitializeCalls() { + return initializeCalls.get(); + } + + public void close() { + closed.set(true); + } + + public boolean isClosed() { + return closed.get(); } } } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java new file mode 100644 index 000000000..3bdaf0c77 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerCloserTest extends AbstractConcurrentInitializerTest { + + private final AtomicBoolean closed = new AtomicBoolean(); + + /** + * Creates the initializer to be tested. This implementation returns the {@code LazyInitializer} created in the {@code setUp()} method. + * + * @return the initializer to be tested + */ + @Override + protected LazyInitializer createInitializer() { + return LazyInitializer.builder().setInitializer(Object::new).setCloser(e -> closed.set(true)).get(); + } + + @Test + public void testIsInitialized() throws ConcurrentException { + final LazyInitializer initializer = createInitializer(); + assertFalse(initializer.isInitialized()); + initializer.get(); + assertTrue(initializer.isInitialized()); + assertFalse(closed.get()); + initializer.close(); + assertTrue(closed.get()); + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java new file mode 100644 index 000000000..c1e6137fc --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerFailableCloserTest extends AbstractConcurrentInitializerTest { + + private final AtomicBoolean closed = new AtomicBoolean(); + + /** + * Creates the initializer to be tested. This implementation returns the {@code LazyInitializer} created in the {@code setUp()} method. + * + * @return the initializer to be tested + */ + @Override + protected LazyInitializer createInitializer() { + return LazyInitializer.builder().setInitializer(this::makeObject).setCloser(e -> throwingCloser()).get(); + } + + private Object makeObject() throws ConcurrentException { + if (closed.get()) { + // Doesn't actually throw, just for the method sig without unused warning. + throw new ConcurrentException("test", new IOException()); + } + return new Object(); + } + + @Test + public void testIsInitialized() throws ConcurrentException { + final LazyInitializer initializer = createInitializer(); + assertFalse(initializer.isInitialized()); + initializer.get(); + assertTrue(initializer.isInitialized()); + assertFalse(closed.get()); + initializer.close(); + assertTrue(closed.get()); + } + + private void throwingCloser() throws ConcurrentException { + closed.set(true); + // always false: + if (!closed.get()) { + // Doesn't actually throw, just for the method sig without unused warning. + throw new ConcurrentException("test", new IOException()); + } + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java new file mode 100644 index 000000000..0ac26a4bf --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + + /** + * Creates the initializer to be tested. + * + * @return the initializer to be tested + */ + @Override + protected ConcurrentInitializer createInitializer() { + return LazyInitializer.builder().setInitializer(() -> new Object()).get(); + } + + @Override + protected ConcurrentInitializer createInitializerThatThrowsException( + final FailableSupplier supplier, + final FailableConsumer closer) { + return LazyInitializer.builder().setInitializer(supplier).setCloser(closer).get(); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java new file mode 100644 index 000000000..beca37ed5 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link MultiBackgroundInitializer}. + */ +public class MultiBackgroundInitializerSupplierTest extends MultiBackgroundInitializerTest { + + private NullPointerException npe; + private IOException ioException; + private FailableConsumer ioExceptionConsumer; + private FailableConsumer nullPointerExceptionConsumer; + + @BeforeEach + public void setUpException() throws Exception { + npe = new NullPointerException(); + ioException = new IOException(); + ioExceptionConsumer = (CloseableCounter cc) -> { + throw ioException; + }; + nullPointerExceptionConsumer = (CloseableCounter cc) -> { + throw npe; + }; + } + + /** + * {@inheritDoc} + */ + @Override + protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() { + return new SupplierChildBackgroundInitializer(); + } + + /** + * Tests that close() method closes the wrapped object + * + * @throws Exception + */ + @Test + public void testClose() + throws ConcurrentException, InterruptedException { + final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer(); + + assertFalse(initializer.isInitialized(), "Initalized without having anything to initalize"); + + initializer.addInitializer("child one", childOne); + initializer.addInitializer("child two", childTwo); + + assertFalse(childOne.getCloseableCounter().isClosed(), "child one closed() succeeded before start()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two closed() succeeded before start()"); + + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (!childOne.isStarted() || !childTwo.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + assertFalse(childOne.getCloseableCounter().isClosed(), "child one close() succeeded after start() but before close()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two close() succeeded after start() but before close()"); + + childOne.get(); // ensure this child finishes initializing + childTwo.get(); // ensure this child finishes initializing + + assertFalse(childOne.getCloseableCounter().isClosed(), "child one initializing succeeded after start() but before close()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two initializing succeeded after start() but before close()"); + + try { + initializer.close(); + } catch (Exception e) { + fail(); + } + + assertTrue(childOne.getCloseableCounter().isClosed(), "child one close() did not succeed"); + assertTrue(childOne.getCloseableCounter().isClosed(), "child two close() did not succeed"); + } + + /** + * Tests that close() wraps a checked exception from a child initializer in an ConcurrentException as the first suppressed under in an ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithCheckedException() throws Exception { + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(ioExceptionConsumer); + + initializer.addInitializer("child one", childOne); + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + childOne.get(); // ensure the Future has completed. + try { + initializer.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(ioException, e.getSuppressed()[0]); + } + } + + /** + * Tests that close() wraps a runtime exception from a child initializer as the first suppressed under in an ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithRuntimeException() throws Exception { + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(nullPointerExceptionConsumer); + + initializer.addInitializer("child one", childOne); + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + childOne.get(); // ensure the Future has completed. + try { + initializer.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(npe, e.getSuppressed()[0]); + } + } + + /** + * Tests that calling close() on a MultiBackgroundInitializer with two children that both throw exceptions throws + * an ConcurrentException and both the child exceptions are present + * + * @throws Exception + */ + @Test + public void testCloseWithTwoExceptions() + throws ConcurrentException, InterruptedException { + + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(ioExceptionConsumer); + final AbstractChildBackgroundInitializer childTwo = new SupplierChildBackgroundInitializer(nullPointerExceptionConsumer); + + initializer.addInitializer("child one", childOne); + initializer.addInitializer("child two", childTwo); + + initializer.start(); + + final long startTime = System.currentTimeMillis(); + final long waitTime = 3000; + final long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted() || ! childTwo.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + childOne.get(); // ensure this child finishes initializing + childTwo.get(); // ensure this child finishes initializing + + try { + initializer.close(); + fail(); + } catch (Exception e) { + // We don't actually know which order the children will be closed in + boolean foundChildOneException = false; + boolean foundChildTwoException = false; + + for (Throwable t : e.getSuppressed()) { + if (t.equals(ioException)) { + foundChildOneException = true; + } + if (t.equals(npe)) { + foundChildTwoException = true; + } + } + + assertTrue(foundChildOneException); + assertTrue(foundChildTwoException); + } + } + + /** + * A concrete implementation of {@code BackgroundInitializer} used for + * defining background tasks for {@code MultiBackgroundInitializer}. + */ + private static final class SupplierChildBackgroundInitializer extends AbstractChildBackgroundInitializer { + + SupplierChildBackgroundInitializer() { + this((CloseableCounter cc) -> cc.close()); + } + + SupplierChildBackgroundInitializer(FailableConsumer consumer) { + try { + // Use reflection here because the constructors we need are private + final FailableSupplier supplier = () -> initializeInternal(); + final Field initializer = AbstractConcurrentInitializer.class.getDeclaredField("initializer"); + initializer.setAccessible(true); + initializer.set(this, supplier); + + final Field closer = AbstractConcurrentInitializer.class.getDeclaredField("closer"); + closer.setAccessible(true); + closer.set(this, consumer); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java index f861e0302..4260f9b61 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java @@ -42,10 +42,10 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { private static final String CHILD_INIT = "childInitializer"; /** The initializer to be tested. */ - private MultiBackgroundInitializer initializer; + protected MultiBackgroundInitializer initializer; /** A short time to wait for background threads to run. */ - private static final long PERIOD_MILLIS = 50; + protected static final long PERIOD_MILLIS = 50; @BeforeEach public void setUp() { @@ -63,8 +63,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ private void checkChild(final BackgroundInitializer child, final ExecutorService expExec) throws ConcurrentException { - final ChildBackgroundInitializer cinit = (ChildBackgroundInitializer) child; - final Integer result = cinit.get(); + final AbstractChildBackgroundInitializer cinit = (AbstractChildBackgroundInitializer) child; + final Integer result = cinit.get().getInitializeCalls(); assertEquals(1, result.intValue(), "Wrong result"); assertEquals(1, cinit.initializeCalls, "Wrong number of executions"); if (expExec != null) { @@ -78,7 +78,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testAddInitializerNullName() { - assertThrows(NullPointerException.class, () -> initializer.addInitializer(null, new ChildBackgroundInitializer())); + assertThrows(NullPointerException.class, () -> initializer.addInitializer(null, createChildBackgroundInitializer())); } /** @@ -117,7 +117,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { final int count = 5; for (int i = 0; i < count; i++) { initializer.addInitializer(CHILD_INIT + i, - new ChildBackgroundInitializer()); + createChildBackgroundInitializer()); } initializer.start(); final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer @@ -126,7 +126,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { for (int i = 0; i < count; i++) { final String key = CHILD_INIT + i; assertTrue(res.initializerNames().contains(key), "Name not found: " + key); - assertEquals(Integer.valueOf(1), res.getResultObject(key), "Wrong result object"); + assertEquals(CloseableCounter.wrapInteger(1), res.getResultObject(key), "Wrong result object"); assertFalse(res.isException(key), "Exception flag"); assertNull(res.getException(key), "Got an exception"); checkChild(res.getInitializer(key), initializer.getActiveExecutor()); @@ -175,8 +175,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { final String initExec = "childInitializerWithExecutor"; final ExecutorService exec = Executors.newSingleThreadExecutor(); try { - final ChildBackgroundInitializer c1 = new ChildBackgroundInitializer(); - final ChildBackgroundInitializer c2 = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer c1 = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer c2 = createChildBackgroundInitializer(); c2.setExternalExecutor(exec); initializer.addInitializer(CHILD_INIT, c1); initializer.addInitializer(initExec, c2); @@ -201,7 +201,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { initializer.start(); assertThrows( IllegalStateException.class, - () -> initializer.addInitializer(CHILD_INIT, new ChildBackgroundInitializer()), + () -> initializer.addInitializer(CHILD_INIT, createChildBackgroundInitializer()), "Could add initializer after start()!"); initializer.get(); } @@ -275,7 +275,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeRuntimeEx() { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new RuntimeException(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -291,7 +291,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeEx() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new Exception(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -312,7 +312,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testInitializeResultsIsSuccessfulTrue() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer @@ -329,7 +329,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testInitializeResultsIsSuccessfulFalse() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new Exception(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -348,13 +348,13 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { public void testInitializeNested() throws ConcurrentException { final String nameMulti = "multiChildInitializer"; initializer - .addInitializer(CHILD_INIT, new ChildBackgroundInitializer()); + .addInitializer(CHILD_INIT, createChildBackgroundInitializer()); final MultiBackgroundInitializer mi2 = new MultiBackgroundInitializer(); final int count = 3; for (int i = 0; i < count; i++) { mi2 .addInitializer(CHILD_INIT + i, - new ChildBackgroundInitializer()); + createChildBackgroundInitializer()); } initializer.addInitializer(nameMulti, mi2); initializer.start(); @@ -374,8 +374,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testIsInitialized() throws ConcurrentException, InterruptedException { - final ChildBackgroundInitializer childOne = new ChildBackgroundInitializer(); - final ChildBackgroundInitializer childTwo = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer(); childOne.enableLatch(); childTwo.enableLatch(); @@ -409,14 +409,28 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { } /** - * A concrete implementation of {@code BackgroundInitializer} used for - * defining background tasks for {@code MultiBackgroundInitializer}. + * An overrideable method to create concrete implementations of + * {@code BackgroundInitializer} used for defining background tasks + * for {@code MultiBackgroundInitializer}. */ - private static final class ChildBackgroundInitializer extends - BackgroundInitializer { + protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() { + return new MethodChildBackgroundInitializer(); + } + + /** + * A mostly complete implementation of {@code BackgroundInitializer} used for + * defining background tasks for {@code MultiBackgroundInitializer}. + * + * Subclasses will contain the initializer, either as an method implementation + * or by using a supplier. + */ + protected static class AbstractChildBackgroundInitializer extends BackgroundInitializer { /** Stores the current executor service. */ volatile ExecutorService currentExecutor; + /** An object containing the state we are testing */ + CloseableCounter counter = new CloseableCounter(); + /** A counter for the invocations of initialize(). */ volatile int initializeCalls; @@ -435,23 +449,76 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { latch.countDown(); } + public CloseableCounter getCloseableCounter() { + return counter; + } + /** * Records this invocation. Optionally throws an exception. */ - @Override - protected Integer initialize() throws Exception { - currentExecutor = getActiveExecutor(); + protected CloseableCounter initializeInternal() throws Exception { initializeCalls++; - - if (ex != null) { - throw ex; - } + currentExecutor = getActiveExecutor(); if (waitForLatch) { latch.await(); } - return Integer.valueOf(initializeCalls); + if (ex != null) { + throw ex; + } + + return counter.increment(); + } + } + + protected static class MethodChildBackgroundInitializer extends AbstractChildBackgroundInitializer { + @Override + protected CloseableCounter initialize() throws Exception { + return initializeInternal(); + } + } + + protected static class CloseableCounter { + /** The number of invocations of initialize(). */ + volatile int initializeCalls; + + /** Has the close consumer successfully reached this object. */ + volatile boolean closed; + + public CloseableCounter increment() { + initializeCalls++; + return this; + } + + public int getInitializeCalls() { + return initializeCalls; + } + + public CloseableCounter setInitializeCalls(int i) { + initializeCalls = i; + return this; + } + + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + + @Override + public boolean equals(final Object other) { + if (other instanceof CloseableCounter) { + return initializeCalls == ((CloseableCounter) other).getInitializeCalls(); + } + return false; + } + + // A convenience for testing that a CloseableCounter typed as Object has a specific initializeCalls value + public static CloseableCounter wrapInteger(int i) { + return new CloseableCounter().setInitializeCalls(i); } } }