This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch SLING-13236
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
The following commit(s) were added to refs/heads/SLING-13236 by this push:
new 4e61eaa2 SLING-13236 - MapEntries: NPE when resource change is
delivered during construction or disposal (#210)
4e61eaa2 is described below
commit 4e61eaa231878eea513104dfd7ebda82804a0423
Author: Herman Ciechanowiec <[email protected]>
AuthorDate: Fri Jul 10 12:48:45 2026 +0200
SLING-13236 - MapEntries: NPE when resource change is delivered during
construction or disposal (#210)
* SLING-13236 - MapEntries: NPE when resource change is delivered during
construction or disposal
The MapEntries constructor registered itself as a ResourceChangeListener
before assigning the VanityPathHandler field, so a change delivered by the
asynchronous "Apache Sling Resource Provider Change Notifier" thread in
that window caused:
java.lang.NullPointerException: Cannot invoke
"...VanityPathHandler.isReady()" because "this.vph" is null
at ...MapEntries.onChange(MapEntries.java:460)
Symmetrically, dispose() tore down the AliasHandler before unregistering
the listener, allowing the equivalent NPE on "this.ah" during shutdown.
Fix: assign the VanityPathHandler before registering the listener (events
arriving before initializeVanityPaths completes are queued and drained by
the initializer, as designed), and unregister the listener first in
dispose(). Add deterministic regression tests for both races.
* Update
src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
Co-authored-by: Jörg Hoh <[email protected]>
* Update
src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
Co-authored-by: Jörg Hoh <[email protected]>
* SLING-13236 - Apply spotless formatting to MapEntriesTest
---------
Co-authored-by: Jörg Hoh <[email protected]>
---
.../resourceresolver/impl/mapping/MapEntries.java | 22 ++++++---
.../impl/mapping/MapEntriesTest.java | 54 ++++++++++++++++++++++
2 files changed, 69 insertions(+), 7 deletions(-)
diff --git
a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
index 55b06869..1784481b 100644
---
a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
+++
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
@@ -142,10 +142,15 @@ public class MapEntries implements MapEntriesHandler,
ResourceChangeListener, Ex
this::drainAliasQueue);
this.ah.initializeAliases();
- this.registration = registerResourceChangeListener(bundleContext);
-
+ // create the vanity path handler before registering the resource
change listener: the
+ // listener may be called as soon as it is registered, and onChange
requires both handlers
+ // to be present (events arriving before initializeVanityPaths
completes are queued and
+ // drained by the initializer)
this.vph =
new VanityPathHandler(this.factory, this.resolveMapsMap,
this.initializing, this::drainVanityPathQueue);
+
+ this.registration = registerResourceChangeListener(bundleContext);
+
this.vph.initializeVanityPaths();
if (metrics.isPresent()) {
@@ -293,16 +298,19 @@ public class MapEntries implements MapEntriesHandler,
ResourceChangeListener, Ex
*/
public void dispose() {
- if (this.ah != null) {
- ah.dispose();
- ah = null;
- }
-
+ // unregister the resource change listener before disposing the alias
handler: onChange
+ // requires the handlers to be present, so they must not be torn down
while the listener
+ // can still be called
if (this.registration != null) {
this.registration.unregister();
this.registration = null;
}
+ if (this.ah != null) {
+ ah.dispose();
+ ah = null;
+ }
+
/*
* Cooperation with doInit: The same lock as used by doInit is acquired
* thus preventing doInit from running and waiting for a concurrent
diff --git
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
index 237c5109..387a3657 100644
---
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
+++
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
@@ -37,6 +37,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.resourceresolver.impl.ResourceResolverMetrics;
import org.junit.After;
@@ -48,6 +50,7 @@ import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.service.event.EventAdmin;
import static org.junit.Assert.assertEquals;
@@ -223,6 +226,57 @@ public class MapEntriesTest extends
AbstractMappingMapEntriesTest {
mapEntries.ah.initializeAliases();
}
+ // tests SLING-13236: a resource change delivered while the MapEntries
constructor is still
+ // running (the listener may be called as soon as it is registered) must
not cause an NPE on
+ // the not-yet-assigned VanityPathHandler
+ @Test
+ public void testChangeDeliveredDuringListenerRegistration() throws
Exception {
+ final AtomicBoolean changeDelivered = new AtomicBoolean(false);
+ when(bundleContext.registerService(eq(ResourceChangeListener.class),
any(ResourceChangeListener.class), any()))
+
.thenAnswer((Answer<ServiceRegistration<ResourceChangeListener>>) invocation ->
{
+ final ResourceChangeListener listener =
invocation.getArgument(1);
+ listener.onChange(List.of(new
ResourceChange(ResourceChange.ChangeType.ADDED, "/resource", false)));
+ changeDelivered.set(true);
+ return null;
+ });
+
+ final MapEntries entries = new MapEntries(
+ resourceResolverFactory, bundleContext, eventAdmin,
stringInterpolationProvider, Optional.empty());
+ try {
+ assertTrue("change should have been delivered during listener
registration", changeDelivered.get());
+ } finally {
+ entries.dispose();
+ }
+ }
+
+ // tests SLING-13236: a resource change delivered while dispose() is
unregistering the
+ // listener must not cause an NPE on an already torn down AliasHandler
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testChangeDeliveredDuringDispose() throws Exception {
+ final AtomicBoolean changeDelivered = new AtomicBoolean(false);
+ when(bundleContext.registerService(eq(ResourceChangeListener.class),
any(ResourceChangeListener.class), any()))
+
.thenAnswer((Answer<ServiceRegistration<ResourceChangeListener>>) invocation ->
{
+ final ResourceChangeListener listener =
invocation.getArgument(1);
+ final ServiceRegistration<ResourceChangeListener>
registration =
+ Mockito.mock(ServiceRegistration.class);
+ Mockito.doAnswer(unregisterInvocation -> {
+ listener.onChange(List.of(
+ new
ResourceChange(ResourceChange.ChangeType.ADDED, "/resource", false)));
+ changeDelivered.set(true);
+ return null;
+ })
+ .when(registration)
+ .unregister();
+ return registration;
+ });
+
+ final MapEntries entries = new MapEntries(
+ resourceResolverFactory, bundleContext, eventAdmin,
stringInterpolationProvider, Optional.empty());
+ entries.dispose();
+ assertTrue("change should have been delivered during listener
unregistration", changeDelivered.get());
+ }
+
@Test
public void testTimingFormatter() {
assertEquals(