This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
The following commit(s) were added to refs/heads/master by this push:
new 170d3a7 SLING-12019 - Avoid duplicate ResourceResolverFactory
registrations (#104)
170d3a7 is described below
commit 170d3a7b415a3c147b2381c8919fd9dfa7eb427c
Author: Julian Sedding <[email protected]>
AuthorDate: Mon Sep 18 13:04:50 2023 +0200
SLING-12019 - Avoid duplicate ResourceResolverFactory registrations (#104)
- reworked synchronization model from relying on worker queue
to locks for "configuration" and an AtomicReference for the
factoryRegistration
- removed unnecessary null checks for factoryRegistration, simplifying the
code
- increased test coverage, particularly for concurrent scenarios
- switched to junit-platform to allow mixing junit4 and junit5 tests
- enabled parallel test execution for junit-platform
---
pom.xml | 36 +++-
.../impl/FactoryRegistrationHandler.java | 127 ++++++++----
.../impl/ResourceResolverFactoryActivator.java | 16 +-
.../impl/FactoryRegistrationHandlerTest.java | 230 ++++++++++++---------
.../resourceresolver/util/CustomMatchers.java | 123 +++++++++++
.../util/events/AbstractAwaitingListener.java | 75 +++++++
.../util/events/RecordingListener.java | 99 +++++++++
.../util/events/ServiceEventUtil.java | 105 ++++++++++
src/test/resources/junit-platform.properties | 19 ++
9 files changed, 683 insertions(+), 147 deletions(-)
diff --git a/pom.xml b/pom.xml
index 75d9d34..302d3de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,20 +188,32 @@
<!-- Testing -->
<dependency>
- <groupId>org.hamcrest</groupId>
- <artifactId>hamcrest-library</artifactId>
- <version>1.3</version>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-simple</artifactId>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.9.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.vintage</groupId>
+ <artifactId>junit-vintage-engine</artifactId>
+ <version>5.9.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>junit-addons</groupId>
- <artifactId>junit-addons</artifactId>
- <version>1.4</version>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -216,10 +228,16 @@
<version>5.5.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.osgi-mock.junit5</artifactId>
+ <version>3.3.8</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
- <version>3.2.2</version>
+ <version>3.3.8</version>
<scope>test</scope>
</dependency>
<dependency>
diff --git
a/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
b/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
index 94d8002..1227f3c 100644
---
a/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
+++
b/src/main/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandler.java
@@ -30,45 +30,78 @@ import org.slf4j.LoggerFactory;
import java.util.Dictionary;
import java.util.Hashtable;
+import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
public class FactoryRegistrationHandler implements AutoCloseable {
private static final Logger LOG =
LoggerFactory.getLogger(FactoryRegistrationHandler.class);
- private final ResourceResolverFactoryActivator activator;
+ private final ExecutorService factoryRegistrationWorker;
+
+ /**
+ * The configurationLock serializes access to the fields {@code activator}
+ * and {@code factoryPreconditions}. Each access to these fields must be
+ * guarded by this lock.
+ */
+ private final ReentrantLock configurationLock = new ReentrantLock();
- private final FactoryPreconditions factoryPreconditions;
+ /** Access only when holding configurationLock */
+ private ResourceResolverFactoryActivator activator;
- private final ExecutorService factoryRegistrationWorker;
+ /** Access only when holding configurationLock */
+ private FactoryPreconditions factoryPreconditions;
- @SuppressWarnings("java:S3077") // The field is only ever set to null or
to a new FactoryRegistration instance, which is safe.
- private volatile FactoryRegistration factoryRegistration;
+ private final AtomicReference<FactoryRegistration> factoryRegistration =
new AtomicReference<>(null);
- public FactoryRegistrationHandler(
- ResourceResolverFactoryActivator activator, FactoryPreconditions
factoryPreconditions) {
- this.factoryPreconditions = factoryPreconditions;
- this.activator = activator;
+ public FactoryRegistrationHandler() {
this.factoryRegistrationWorker = Executors.newSingleThreadExecutor(
r -> new Thread(r,
ResourceResolverFactory.class.getSimpleName() + "
registration/deregistration"));
}
+ public void configure(ResourceResolverFactoryActivator activator,
FactoryPreconditions factoryPreconditions) {
+ checkClosed();
+
+ boolean reRegister;
+ try {
+ configurationLock.lock();
+ reRegister = this.activator != activator ||
!Objects.equals(this.factoryPreconditions, factoryPreconditions);
+ LOG.debug("activator differs = {}, factoryPreconditions differ =
{}", this.activator != activator, !Objects.equals(this.factoryPreconditions,
factoryPreconditions));
+ LOG.debug("factoryPreconditions {} vs {}",
this.factoryPreconditions, factoryPreconditions);
+ this.factoryPreconditions = factoryPreconditions;
+ this.activator = activator;
+ } finally {
+ configurationLock.unlock();
+ }
+
+ if (reRegister) {
+ unregisterFactory();
+ maybeRegisterFactory(null, null);
+ }
+ }
+
+ private void checkClosed() {
+ if (factoryRegistrationWorker.isShutdown()) {
+ throw new IllegalStateException("FactoryRegistrationHandler is
already closed");
+ }
+ }
+
@Override
public void close() {
- unregisterFactory();
factoryRegistrationWorker.shutdown();
try {
- if (!factoryRegistrationWorker.awaitTermination(5,
TimeUnit.SECONDS)) {
+ if (!factoryRegistrationWorker.awaitTermination(1,
TimeUnit.MINUTES)) {
factoryRegistrationWorker.shutdownNow();
- // make sure everything is unregistered, even if
- // the factoryRegistrationWorker did not complete
- doUnregisterFactory();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
+ runWithThreadName("deregistration on close",
this::doUnregisterFactory);
}
/**
@@ -77,15 +110,23 @@ public class FactoryRegistrationHandler implements
AutoCloseable {
void maybeRegisterFactory(final String unavailableName, final String
unavailableServicePid) {
if (!factoryRegistrationWorker.isShutdown()) {
LOG.debug("submitting maybeRegisterFactory");
- factoryRegistrationWorker.submit(() -> {
- final boolean preconditionsOk =
factoryPreconditions.checkPreconditions(unavailableName, unavailableServicePid);
- if ( preconditionsOk && this.factoryRegistration == null ) {
+ factoryRegistrationWorker.execute(() -> {
+ final boolean preconditionsOk;
+ final ResourceResolverFactoryActivator localActivator;
+ try {
+ configurationLock.lock();
+ preconditionsOk =
factoryPreconditions.checkPreconditions(unavailableName, unavailableServicePid);
+ localActivator = activator;
+ } finally {
+ configurationLock.unlock();
+ }
+ if (preconditionsOk) {
+ final Bundle systemBundle =
localActivator.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
// check system bundle state - if stopping, don't register
new factory
- final Bundle systemBundle =
activator.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
- if ( systemBundle != null && systemBundle.getState() !=
Bundle.STOPPING ) {
- runWithThreadName("registration",
this::doRegisterFactory);
+ if (systemBundle != null && systemBundle.getState() !=
Bundle.STOPPING) {
+ runWithThreadName("registration", () ->
this.doRegisterFactory(localActivator));
}
- } else if ( !preconditionsOk && this.factoryRegistration !=
null ) {
+ } else {
LOG.debug("performing unregisterFactory via
maybeRegisterFactory");
runWithThreadName("deregistration",
this::doUnregisterFactory);
}
@@ -94,35 +135,40 @@ public class FactoryRegistrationHandler implements
AutoCloseable {
}
void unregisterFactory() {
- LOG.debug("submitting unregisterFactory");
- factoryRegistrationWorker.submit(() ->
runWithThreadName("deregistration",
- this::doUnregisterFactory));
+ if (!factoryRegistrationWorker.isShutdown()) {
+ LOG.debug("submitting unregisterFactory");
+ factoryRegistrationWorker.execute(
+ () -> runWithThreadName("deregistration",
this::doUnregisterFactory));
+ }
}
/**
* Register the factory.
*/
- private void doRegisterFactory() {
- LOG.debug("performing registerFactory, factoryRegistration == {}",
factoryRegistration);
- if (this.factoryRegistration == null) {
- this.factoryRegistration = new
FactoryRegistration(activator.getBundleContext(), activator);
- }
- LOG.debug("finished performing registerFactory, factoryRegistration ==
{}", factoryRegistration);
+ private void doRegisterFactory(ResourceResolverFactoryActivator activator)
{
+ final FactoryRegistration newRegistration =
factoryRegistration.updateAndGet(oldRegistration -> {
+ LOG.debug("performing registerFactory, factoryRegistration == {}",
oldRegistration);
+ return Objects.requireNonNullElseGet(oldRegistration,
+ () -> new
FactoryRegistration(activator.getBundleContext(), activator));
+ });
+ LOG.debug("finished performing registerFactory, factoryRegistration ==
{}", newRegistration);
}
/**
* Unregister the factory (if registered).
*/
private void doUnregisterFactory() {
- LOG.debug("performing unregisterFactory, factoryRegistration == {}",
factoryRegistration);
- if (this.factoryRegistration != null) {
- this.factoryRegistration.unregister();
- this.factoryRegistration = null;
- }
- LOG.debug("finished performing unregisterFactory, factoryRegistration
== {}", factoryRegistration);
+ final FactoryRegistration newRegistration =
factoryRegistration.updateAndGet(oldRegistration -> {
+ LOG.debug("performing unregisterFactory, factoryRegistration ==
{}", factoryRegistration);
+ Optional.ofNullable(oldRegistration)
+ .ifPresent(FactoryRegistration::unregister);
+ LOG.debug("setting factoryRegistration = null");
+ return null;
+ });
+ LOG.debug("finished performing unregisterFactory, factoryRegistration
== {}", newRegistration);
}
- private static void runWithThreadName(String threadNameSuffix, Runnable
task) {
+ private void runWithThreadName(String threadNameSuffix, Runnable task) {
final String name = Thread.currentThread().getName();
try {
Thread.currentThread().setName(ResourceResolverFactory.class.getSimpleName() +
" " + threadNameSuffix);
@@ -168,9 +214,16 @@ public class FactoryRegistrationHandler implements
AutoCloseable {
}
void unregister() {
+ LOG.debug("Unregister runtimeRegistration");
runtimeRegistration.unregister();
+
+ LOG.debug("Unregister factoryRegistration");
factoryRegistration.unregister();
+
+ LOG.debug("Unregister commonFactory");
commonFactory.deactivate();
+
+ LOG.debug("Unregister completed");
}
}
}
diff --git
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
index 1651e06..39bc17c 100644
---
a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
+++
b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
@@ -124,8 +124,7 @@ public class ResourceResolverFactoryActivator {
/** Observation paths */
private volatile Path[] observationPaths;
- @SuppressWarnings("java:S3077")
- private volatile FactoryRegistrationHandler factoryRegistrationHandler;
+ private final FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler();
private final VanityPathConfigurer vanityPathConfigurer = new
VanityPathConfigurer();
{
@@ -313,8 +312,7 @@ public class ResourceResolverFactoryActivator {
resourceProviderTracker,
requiredResourceProviderNames,
requiredResourceProvidersLegacy);
- factoryRegistrationHandler = new FactoryRegistrationHandler(this,
factoryPreconditions);
- factoryRegistrationHandler.maybeRegisterFactory(null, null);
+ factoryRegistrationHandler.configure(this, factoryPreconditions);
if (!hasPreRegisteredResourceProviderTracker) {
this.resourceProviderTracker.activate(this.bundleContext,
this.eventAdmin,
@@ -343,7 +341,7 @@ public class ResourceResolverFactoryActivator {
protected void modified(final BundleContext bundleContext,
final ResourceResolverFactoryConfig config,
final VanityPathConfigurer.DeprecatedVanityConfig
deprecatedVanityConfig) {
- this.deactivate();
+ this.deactivateInternal();
this.activate(bundleContext, config, deprecatedVanityConfig);
}
@@ -352,11 +350,13 @@ public class ResourceResolverFactoryActivator {
*/
@Deactivate
protected void deactivate() {
- this.factoryRegistrationHandler.close();
- this.factoryRegistrationHandler = null;
-
// factoryRegistrationHandler must be closed before bundleContext is
set to null
+ this.factoryRegistrationHandler.close();
this.bundleContext = null;
+ deactivateInternal();
+ }
+
+ private void deactivateInternal() {
this.config = DEFAULT_CONFIG;
this.vanityPathConfigurer.setConfiguration(DEFAULT_CONFIG, null);
this.changeListenerWhiteboard.deactivate();
diff --git
a/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
b/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
index 0f66b7f..04fb408 100644
---
a/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
+++
b/src/test/java/org/apache/sling/resourceresolver/impl/FactoryRegistrationHandlerTest.java
@@ -22,160 +22,204 @@ import
org.apache.commons.collections4.bidimap.TreeBidiMap;
import org.apache.sling.api.resource.ResourceResolverFactory;
import
org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
import
org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
+import org.apache.sling.resourceresolver.util.events.RecordingListener;
+import
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
import org.apache.sling.serviceusermapping.ServiceUserMapper;
import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.hamcrest.Matcher;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.internal.stubbing.defaultanswers.ReturnsSmartNulls;
import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceEvent;
-import org.osgi.framework.ServiceListener;
-import org.osgi.framework.ServiceReference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
-
-import static org.junit.Assert.assertTrue;
+import static org.apache.sling.resourceresolver.util.CustomMatchers.allOf;
+import static org.apache.sling.resourceresolver.util.CustomMatchers.hasItem;
+import static
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.registration;
+import static
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.unregistration;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
-public class FactoryRegistrationHandlerTest {
+@ExtendWith(OsgiContextExtension.class)
+class FactoryRegistrationHandlerTest {
+
+ private static final int DEFAULT_TEST_ITERATIONS = 20;
+
+ private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>>
RRF_REGISTRATION = allOf(
+ hasSize(4),
+ hasItem(registration(ResourceResolverFactory.class))
+ );
- private static final Logger LOG =
LoggerFactory.getLogger(FactoryRegistrationHandlerTest.class);
+ private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>>
RRF_UNREGISTRATION = allOf(
+ hasSize(4),
+ hasItem(unregistration(ResourceResolverFactory.class))
+ );
- public static final ReturnsSmartNulls DEFAULT_ANSWER = new
ReturnsSmartNulls();
+ private static final @NotNull Matcher<Iterable<? extends ServiceEventDTO>>
RRF_REREGISTRATION = allOf(
+ hasSize(8),
+ hasItem(unregistration(ResourceResolverFactory.class)),
+ hasItem(registration(ResourceResolverFactory.class))
+ );
- @Rule
- public OsgiContext osgi = new OsgiContext();
+ OsgiContext osgi = new OsgiContext();
private ResourceResolverFactoryActivator activator;
- @Before
- public void setUp() {
+ @BeforeEach
+ void setUp() {
final ResourceProviderTracker resourceProviderTracker =
mock(ResourceProviderTracker.class);
doReturn(mock(ResourceProviderStorage.class)).when(resourceProviderTracker).getResourceProviderStorage();
+ final VanityPathConfigurer vanityPathConfigurer =
mock(VanityPathConfigurer.class);
+ doReturn(false).when(vanityPathConfigurer).isVanityPathEnabled();
+
activator = mock(ResourceResolverFactoryActivator.class);
doReturn(osgi.bundleContext()).when(activator).getBundleContext();
doReturn(resourceProviderTracker).when(activator).getResourceProviderTracker();
doReturn(mock(ServiceUserMapper.class)).when(activator).getServiceUserMapper();
doReturn(new TreeBidiMap<>()).when(activator).getVirtualURLMap();
doReturn(null).when(activator).getEventAdmin();
-
+
doReturn(vanityPathConfigurer).when(activator).getVanityPathConfigurer();
+ doCallRealMethod().when(activator).getRuntimeService();
}
private static <T> T mock(Class<T> classToMock) {
- return Mockito.mock(classToMock, DEFAULT_ANSWER);
+ return Mockito.mock(classToMock, new ReturnsSmartNulls());
}
- @Test
- public void testFactoryRegistration() throws InterruptedException {
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testFactoryRegistrationDeregistration() throws InterruptedException {
+ final BundleContext bundleContext = osgi.bundleContext();
final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
- try (AwaitingListener unregistration =
- AwaitingListener.unregistration(osgi.bundleContext(),
ResourceResolverFactory.class)) {
- try (FactoryRegistrationHandler factoryRegistrationHandler =
- new FactoryRegistrationHandler(activator,
preconditions);
- AwaitingListener registration =
AwaitingListener.registration(osgi.bundleContext(),
ResourceResolverFactory.class)) {
+ try (FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler()) {
+
+ try (RecordingListener listener =
RecordingListener.of(bundleContext)) {
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
+ }
+
+ try (RecordingListener listener =
RecordingListener.of(bundleContext)) {
+ factoryRegistrationHandler.unregisterFactory();
+ listener.assertRecorded(RRF_UNREGISTRATION);
+ }
+
+ try (RecordingListener listener =
RecordingListener.of(bundleContext)) {
factoryRegistrationHandler.maybeRegisterFactory(null, null);
- assertTrue("Expected ResourceResolverFactory service to be
registered",
- registration.await(5, TimeUnit.SECONDS));
+ listener.assertRecorded(RRF_REGISTRATION);
}
- assertTrue("Expected ResourceResolverFactory service to be
unregistered",
- unregistration.await(5, TimeUnit.SECONDS));
}
}
- @Test
- public void testFactoryDeregistrationWhenConditionsUnsatisfied() throws
InterruptedException {
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testConditionChangeLeadingToUnregistration() throws
InterruptedException {
final BundleContext ctx = osgi.bundleContext();
final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
- try (FactoryRegistrationHandler factoryRegistrationHandler =
- new FactoryRegistrationHandler(activator, preconditions);
- AwaitingListener registration =
AwaitingListener.registration(ctx, ResourceResolverFactory.class);
- AwaitingListener unregistration =
AwaitingListener.unregistration(ctx, ResourceResolverFactory.class)) {
- when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
- factoryRegistrationHandler.maybeRegisterFactory(null, null);
- assertTrue("Expected ResourceResolverFactory service to be
registered",
- registration.await(5, TimeUnit.SECONDS));
+ try (FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler()) {
+ try (final RecordingListener listener = RecordingListener.of(ctx))
{
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
+ }
- when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(false);
- factoryRegistrationHandler.maybeRegisterFactory(null, null);
- assertTrue("Expected ResourceResolverFactory service to be
unregistered",
- unregistration.await(5, TimeUnit.SECONDS));
+ try (final RecordingListener listener = RecordingListener.of(ctx))
{
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(false);
+ factoryRegistrationHandler.maybeRegisterFactory(null, null);
+ listener.assertRecorded(RRF_UNREGISTRATION);
+ }
}
}
- private static class AwaitingListener implements ServiceListener,
AutoCloseable {
- private final BundleContext bundleContext;
-
- private final Predicate<ServiceEvent> expectedEventPredicate;
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testReconfigurationLeadingToUnregsitration() throws
InterruptedException {
+ final BundleContext ctx = osgi.bundleContext();
+ final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
- private final CountDownLatch latch;
+ try (final FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler()) {
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
+ }
- public AwaitingListener(BundleContext ctx, Predicate<ServiceEvent>
expectedEventPredicate) {
- this(ctx, expectedEventPredicate, 1);
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ final FactoryPreconditions failingPreconditions =
mock(FactoryPreconditions.class); // new instance
+ when(failingPreconditions.checkPreconditions(isNull(),
isNull())).thenReturn(false);
+ factoryRegistrationHandler.configure(activator,
failingPreconditions);
+ listener.assertRecorded(RRF_UNREGISTRATION);
+ }
}
+ }
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testReconfigurationWithNoChanges() throws InterruptedException {
+ final BundleContext ctx = osgi.bundleContext();
+ final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
- public AwaitingListener(BundleContext ctx, Predicate<ServiceEvent>
expectedEventPredicate, int count) {
- this.bundleContext = ctx;
- this.expectedEventPredicate = expectedEventPredicate;
- this.latch = new CountDownLatch(count);
- this.bundleContext.addServiceListener(this);
- }
+ try (final FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler()) {
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
+ }
- public static AwaitingListener registration(BundleContext
bundleContext, Class<?> clazz) {
- return new AwaitingListener(bundleContext, serviceEvent -> {
- if (serviceEvent.getType() == ServiceEvent.REGISTERED) {
- return isInstance(bundleContext,
serviceEvent.getServiceReference(), clazz);
- }
- return false;
- });
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(empty());
+ }
}
+ }
- public static AwaitingListener unregistration(BundleContext
bundleContext, Class<?> clazz) {
- return new AwaitingListener(bundleContext, serviceEvent -> {
- if (serviceEvent.getType() == ServiceEvent.UNREGISTERING) {
- return isInstance(bundleContext,
serviceEvent.getServiceReference(), clazz);
- }
- return false;
- });
- }
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testReconfigurationLeadingToReregistration() throws
InterruptedException {
+ final BundleContext ctx = osgi.bundleContext();
- private static boolean isInstance(BundleContext bundleContext,
ServiceReference<?> ref, Class<?> clazz) {
- try {
- final Object service = bundleContext.getService(ref);
- final boolean isInstance = clazz.isInstance(service);
- LOG.info("{} == isInstance({}, {})", isInstance,
service.getClass(), clazz);
- return isInstance;
- } finally {
- bundleContext.ungetService(ref);
+ try (final FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler()) {
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
}
- }
- @Override
- public void serviceChanged(ServiceEvent event) {
- if (expectedEventPredicate.test(event)) {
- latch.countDown();
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(allOf(RRF_REREGISTRATION));
}
}
+ }
- public boolean await(long timeout, TimeUnit timeUnit) throws
InterruptedException {
- return latch.await(timeout, timeUnit);
+ @RepeatedTest(DEFAULT_TEST_ITERATIONS)
+ void testUnregisterOnClose() throws InterruptedException {
+ final BundleContext ctx = osgi.bundleContext();
+ final FactoryPreconditions preconditions =
mock(FactoryPreconditions.class);
+ final FactoryRegistrationHandler factoryRegistrationHandler = new
FactoryRegistrationHandler();
+
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(true);
+ factoryRegistrationHandler.configure(activator, preconditions);
+ listener.assertRecorded(RRF_REGISTRATION);
}
- @Override
- public void close() {
- bundleContext.removeServiceListener(this);
+ try (RecordingListener listener = RecordingListener.of(ctx)) {
+ when(preconditions.checkPreconditions(isNull(),
isNull())).thenReturn(false);
+ factoryRegistrationHandler.close();
+ listener.assertRecorded(allOf(RRF_UNREGISTRATION));
+
+ assertThrows(IllegalStateException.class, () ->
factoryRegistrationHandler.configure(activator, preconditions),
+ "Reconfiguration is no longer possible after a
FactoryRegistrationHandler is closed.");
}
}
}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java
b/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java
new file mode 100644
index 0000000..1f20694
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/util/CustomMatchers.java
@@ -0,0 +1,123 @@
+/*
+ * 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.sling.resourceresolver.util;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Custom version of Hamcrest matchers that fix the generics to make them
usable.
+ */
+public class CustomMatchers {
+
+
+ @SafeVarargs
+ @NotNull
+ public static <T> Matcher<T> allOf(Matcher<? extends T> first, Matcher<?
extends T>... more) {
+ final List<Matcher<? extends T>> matchers =
Stream.concat(Stream.of(first), Stream.of(more)).collect(Collectors.toList());
+ return new AllOfMatcher<>(matchers);
+ }
+
+ public static <T> Matcher<Iterable<? extends T>> hasItem(T item) {
+ return hasItem(Matchers.equalTo(item));
+ }
+
+ public static <T> Matcher<Iterable<? extends T>> hasItem(Matcher<? extends
T> elementMatcher) {
+ return new HasItemMatcher<>(elementMatcher);
+ }
+
+
+ private static <T> Stream<? extends T> toStream(Iterable<? extends T>
iterable) {
+ return StreamSupport.stream(iterable.spliterator(), false);
+ };
+
+ private static class AllOfMatcher<T> extends TypeSafeDiagnosingMatcher<T> {
+ private final Collection<Matcher<? extends T>> matchers;
+
+ AllOfMatcher(Collection<Matcher<? extends T>> matchers) {
+ this.matchers = matchers;
+ }
+
+ @Override
+ protected boolean matchesSafely(T item, Description
mismatchDescription) {
+ return matchers.stream()
+ .filter(matcher -> !matcher.matches(item))
+ .peek(matcher -> {
+
mismatchDescription.appendDescriptionOf(matcher).appendText(" ");
+ matcher.describeMismatch(item, mismatchDescription);
+ })
+ .findFirst()
+ .isEmpty();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendList("(", " " + "and" + " ", ")", matchers);
+ }
+ }
+
+ private static class HasItemMatcher<T> extends
TypeSafeDiagnosingMatcher<Iterable<? extends T>> {
+
+ private final Matcher<? extends T> elementMatcher;
+
+ public HasItemMatcher(Matcher<? extends T> elementMatcher) {
+ this.elementMatcher = elementMatcher;
+ }
+
+ @Override
+ protected boolean matchesSafely(Iterable<? extends T> iterable,
Description mismatchDescription) {
+ if (toStream(iterable).limit(1).count() == 0) {
+ mismatchDescription.appendText("was empty");
+ return false;
+ }
+
+ if (toStream(iterable).anyMatch(elementMatcher::matches)) {
+ return true;
+ }
+
+ mismatchDescription.appendText(
+ toStream(iterable)
+ .map(item -> describeItemMismatch(elementMatcher,
item))
+ .collect(Collectors.joining(", ", "mismatches
were: [", "]")));
+ return false;
+ }
+
+ @NotNull
+ private String describeItemMismatch(Matcher<? extends T>
elementMatcher1, T item) {
+ final StringDescription desc = new StringDescription();
+ elementMatcher1.describeMismatch(item, desc);
+ return desc.toString();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("an iterable containing
").appendDescriptionOf(elementMatcher);
+ }
+ }
+}
diff --git
a/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
b/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
new file mode 100644
index 0000000..6a03afb
--- /dev/null
+++
b/src/test/java/org/apache/sling/resourceresolver/util/events/AbstractAwaitingListener.java
@@ -0,0 +1,75 @@
+/*
+ * 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.sling.resourceresolver.util.events;
+
+import
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+abstract class AbstractAwaitingListener implements ServiceListener,
AutoCloseable {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AbstractAwaitingListener.class);
+
+ private final BundleContext bundleContext;
+
+ private final CountDownLatch latch;
+
+ public AbstractAwaitingListener(BundleContext ctx, int count) {
+ this.bundleContext = ctx;
+ this.latch = new CountDownLatch(count);
+ }
+
+ protected final void startListening() {
+ this.bundleContext.addServiceListener(this);
+ LOG.info("addServiceListener({})", this);
+ }
+
+ @Override
+ public void serviceChanged(ServiceEvent event) {
+ final ServiceEventDTO eventDTO = ServiceEventDTO.create(event);
+ final boolean matchingServiceEvent = isMatchingServiceEvent(event);
+ LOG.info("serviceChanged({}, {}) => {} | {}", eventDTO.getEventType(),
eventDTO.getClasses(), matchingServiceEvent, this);
+ if (matchingServiceEvent) {
+ latch.countDown();
+ }
+ }
+
+ protected abstract boolean isMatchingServiceEvent(ServiceEvent
serviceEvent);
+
+ protected boolean await(long timeout, TimeUnit timeUnit) throws
InterruptedException {
+ return latch.await(timeout, timeUnit);
+ }
+
+ @Override
+ public void close() {
+ stopListening();
+ }
+
+ protected void stopListening() {
+ bundleContext.removeServiceListener(this);
+ LOG.info("removeServiceListener({})", this);
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
b/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
new file mode 100644
index 0000000..dbeb60f
--- /dev/null
+++
b/src/test/java/org/apache/sling/resourceresolver/util/events/RecordingListener.java
@@ -0,0 +1,99 @@
+/*
+ * 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.sling.resourceresolver.util.events;
+
+import
org.apache.sling.resourceresolver.util.events.ServiceEventUtil.ServiceEventDTO;
+import org.hamcrest.Matcher;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceRegistration;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RecordingListener extends AbstractAwaitingListener {
+
+ private final Collection<ServiceEventDTO> serviceEvents = new
ConcurrentLinkedQueue<ServiceEventDTO>();
+
+ private ServiceRegistration<RecordingListener> signalRegistration;
+
+ private final Predicate<ServiceEvent> recordingFilter;
+
+ public static RecordingListener of(BundleContext ctx) {
+ final RecordingListener recordingListener = new RecordingListener(ctx,
serviceEvent -> true);
+ recordingListener.startListening();
+ return recordingListener;
+ }
+
+ public static RecordingListener of(BundleContext ctx,
Predicate<ServiceEvent> recordingFilter) {
+ final RecordingListener recordingListener = new RecordingListener(ctx,
recordingFilter);
+ recordingListener.startListening();
+ return recordingListener;
+ }
+
+ private RecordingListener(BundleContext ctx, Predicate<ServiceEvent>
recordingFilter) {
+ super(ctx, 1);
+ this.recordingFilter = recordingFilter;
+ this.signalRegistration = ctx.registerService(RecordingListener.class,
this,
+ new Hashtable<>(Map.of("::class::", RecordingListener.class)));
+ }
+
+ @Override
+ protected boolean isMatchingServiceEvent(ServiceEvent serviceEvent) {
+ return signalRegistration != null // is null when registering "this"
+ && serviceEvent.getServiceReference() ==
signalRegistration.getReference();
+ }
+
+ @Override
+ public void serviceChanged(ServiceEvent serviceEvent) {
+ super.serviceChanged(serviceEvent);
+ if (!isInternalEvent(serviceEvent) &&
recordingFilter.test(serviceEvent)) {
+ serviceEvents.add(ServiceEventDTO.create(serviceEvent));
+ }
+ }
+
+ private boolean isInternalEvent(ServiceEvent serviceEvent) {
+ return
Objects.equals(serviceEvent.getServiceReference().getProperty("::class::"),
RecordingListener.class);
+ }
+
+ public void assertRecorded(Matcher<? super Collection<? extends
ServiceEventDTO>> serviceEventDTOMatcher) throws InterruptedException {
+ if (signalRegistration != null) {
+ final long deadline = System.nanoTime() +
TimeUnit.SECONDS.toNanos(1);
+ while (!serviceEventDTOMatcher.matches(serviceEvents) &&
System.nanoTime() < deadline) {
+ // give other threads a chance
+ Thread.yield();
+ }
+ signalRegistration.unregister();
+ if (!await(1, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Unregistration event not
observed within 1 second.");
+ }
+ signalRegistration = null;
+ stopListening();
+ }
+
+ assertThat("Expected ServiceEvents", serviceEvents,
serviceEventDTOMatcher);
+ }
+}
diff --git
a/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
b/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
new file mode 100644
index 0000000..16b6262
--- /dev/null
+++
b/src/test/java/org/apache/sling/resourceresolver/util/events/ServiceEventUtil.java
@@ -0,0 +1,105 @@
+/*
+ * 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.sling.resourceresolver.util.events;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.osgi.framework.ServiceEvent;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class ServiceEventUtil {
+
+ public static Matcher<ServiceEventDTO> registration(Class<?> clazz) {
+ return
Matchers.equalTo(ServiceEventDTO.create(ServiceEvent.REGISTERED, clazz));
+ }
+
+ public static Matcher<ServiceEventDTO> unregistration(Class<?> clazz) {
+ return
Matchers.equalTo(ServiceEventDTO.create(ServiceEvent.UNREGISTERING, clazz));
+ }
+
+ public static class ServiceEventDTO {
+
+ private static final String[] SERVICE_EVENT_TYPES = new String[9];
+ static {
+ SERVICE_EVENT_TYPES[ServiceEvent.REGISTERED] = "REGISTERED";
+ SERVICE_EVENT_TYPES[ServiceEvent.UNREGISTERING] = "UNREGISTERING";
+ SERVICE_EVENT_TYPES[ServiceEvent.MODIFIED] = "MODIFIED";
+ SERVICE_EVENT_TYPES[ServiceEvent.MODIFIED_ENDMATCH] =
"MODIFIED_ENDMATCH";
+ }
+
+ private final int eventType;
+
+ private final Set<String> classNames;
+
+ private ServiceEventDTO(int eventType, Set<String> classNames) {
+ this.eventType = eventType;
+ this.classNames = Collections.unmodifiableSet(classNames);
+ }
+
+ public static ServiceEventDTO create(int eventType, Class<?>...
classes) {
+ final Set<String> classNames =
Stream.of(classes).map(Class::getName).collect(Collectors.toCollection(TreeSet::new));
+ return new ServiceEventDTO(eventType, classNames);
+ }
+
+ public static ServiceEventDTO create(ServiceEvent event) {
+ final int eventType = event.getType();
+ final Object objectClass =
event.getServiceReference().getProperty("objectClass");
+ final Set<String> classNames;
+ if (objectClass instanceof String[]) {
+ classNames = new TreeSet<>(Arrays.asList((String[])
objectClass));
+ } else {
+ classNames = Collections.emptySet();
+ }
+ return new ServiceEventDTO(eventType, classNames);
+ }
+
+ public String getEventType() {
+ return SERVICE_EVENT_TYPES[eventType];
+ }
+
+ public Set<String> getClasses() {
+ return classNames;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ServiceEventDTO that = (ServiceEventDTO) o;
+ return eventType == that.eventType && Objects.equals(classNames,
that.classNames);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(eventType, classNames);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ServiceEvent(%s, %s)", getEventType(),
classNames);
+ }
+ }
+}
diff --git a/src/test/resources/junit-platform.properties
b/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..07cb911
--- /dev/null
+++ b/src/test/resources/junit-platform.properties
@@ -0,0 +1,19 @@
+# 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.
+junit.jupiter.execution.parallel.enabled = true
+junit.jupiter.execution.parallel.mode.default = concurrent
+junit.jupiter.execution.parallel.mode.classes.default = concurrent
\ No newline at end of file