Repository: apache/sling-org-apache-sling-resourceresolver
Pull request #175: SLING-12777: alias refactoring - make ResourceMapperImpl ignorant of optimimized/non-optimized case
Author: login: reschke
Created: 2025-05-06T16:30:50Z | Merged: 2025-05-14T09:00:39Z

This removes logic from `ResourceMapperImpl` that previously took two different code paths, depending on the mode.

To get there, `AliasHandler` now handles `getAliasMap` even when the alias cache is not in use.

Note: for that, it uses its own resolver instance, thus the test in `ResourceMapperImplTest` does not "work" anymore (code removed for now). Suggestions how to resurrect that check (do we even need it anymore?) are welcome.


Diff:
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java
index 7b36f014e..af5cb29f9 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -54,6 +55,8 @@ class AliasHandler {
 
     private static final String JCR_CONTENT_SUFFIX = "/" + JCR_CONTENT;
 
+    private static final String SERVICE_USER = "mapping";
+
     private MapConfigurationProvider factory;
 
     private final ReentrantLock initializing;
@@ -66,8 +69,8 @@ class AliasHandler {
     private final Runnable doUpdateConfiguration;
     private final Runnable sendChangeEvent;
 
-    // static value when for the case when cache is not (yet) not initialized
-    private static Map<String, Map<String, Collection<String>>> UNITIALIZED_MAP = Collections.emptyMap();
+    // static value for the case when cache is not (yet) not initialized
+    private static final Map<String, Map<String, Collection<String>>> UNITIALIZED_MAP = Collections.emptyMap();
 
     /**
      * The key of the map is the parent path, while the value is a map with the
@@ -76,7 +79,7 @@ class AliasHandler {
      * The only way this map changes away from {@link #UNITIALIZED_MAP} is when
      * alias initialization finished successfully.
      */
-    // TODO: check for pontential concurrency issues (SLING-12771)
+    // TODO: check for potential concurrency issues (SLING-12771)
     @NotNull
     Map<String, Map<String, Collection<String>>> aliasMapsMap = UNITIALIZED_MAP;
 
@@ -85,10 +88,10 @@ class AliasHandler {
     final AtomicLong detectedInvalidAliases;
 
     public AliasHandler(
-            MapConfigurationProvider factory,
-            ReentrantLock initializing,
-            Runnable doUpdateConfiguration,
-            Runnable sendChangeEvent) {
+            @NotNull MapConfigurationProvider factory,
+            @NotNull ReentrantLock initializing,
+            @NotNull Runnable doUpdateConfiguration,
+            @NotNull Runnable sendChangeEvent) {
         this.factory = factory;
         this.initializing = initializing;
         this.doUpdateConfiguration = doUpdateConfiguration;
@@ -145,7 +148,7 @@ protected void initializeAliases() {
                     } else if (!invalidAliases.isEmpty()) {
                         log.warn("There are {} invalid aliases: {}", invalidAliases.size(), invalidAliases);
                     }
-                } catch (final Exception e) {
+                } catch (Exception e) {
                     this.aliasMapsMap = UNITIALIZED_MAP;
                     logDisableAliasOptimization(e);
                 }
@@ -162,7 +165,7 @@ boolean usesCache() {
         return this.aliasMapsMap != UNITIALIZED_MAP;
     }
 
-    boolean doAddAlias(final Resource resource) {
+    boolean doAddAlias(@NotNull Resource resource) {
         if (this.aliasMapsMap != UNITIALIZED_MAP) {
             return loadAlias(resource, this.aliasMapsMap, null, null);
         } else {
@@ -178,7 +181,10 @@ boolean doAddAlias(final Resource resource) {
      * @return {@code true} if a change happened
      */
     boolean removeAlias(
-            ResourceResolver resolver, final String contentPath, final String path, final Runnable notifyOfChange) {
+            @Nullable ResourceResolver resolver,
+            @NotNull String contentPath,
+            @Nullable String path,
+            @NotNull Runnable notifyOfChange) {
         if (this.aliasMapsMap != UNITIALIZED_MAP) {
             return removeAliasInMap(resolver, contentPath, path, notifyOfChange);
         } else {
@@ -187,9 +193,12 @@ boolean removeAlias(
     }
 
     private boolean removeAliasInMap(
-            ResourceResolver resolver, final String contentPath, final String path, final Runnable notifyOfChange) {
+            @Nullable ResourceResolver resolver,
+            @NotNull String contentPath,
+            @Nullable String path,
+            @NotNull Runnable notifyOfChange) {
 
-        final String resourcePath = computeResourcePath(contentPath, path);
+        String resourcePath = computeResourcePath(contentPath, path);
 
         if (resourcePath == null) {
             // early exit
@@ -199,7 +208,7 @@ private boolean removeAliasInMap(
         this.initializing.lock();
 
         try {
-            final Map<String, Collection<String>> aliasMapEntry = aliasMapsMap.get(contentPath);
+            Map<String, Collection<String>> aliasMapEntry = aliasMapsMap.get(contentPath);
             if (aliasMapEntry != null) {
                 notifyOfChange.run();
                 handleAliasRemoval(resolver, contentPath, resourcePath, aliasMapEntry);
@@ -218,8 +227,8 @@ private boolean removeAliasInMap(
 
         if (path != null && path.length() > contentPath.length()) {
             // path -> (contentPath + subPath)
-            final String subPath = path.substring(contentPath.length() + 1);
-            final int firstSlash = subPath.indexOf('/');
+            String subPath = path.substring(contentPath.length() + 1);
+            int firstSlash = subPath.indexOf('/');
 
             if (firstSlash == -1) {
                 // no slash in subPath
@@ -256,7 +265,7 @@ private void handleAliasRemoval(
             if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 doAddAlias(containingResource);
             }
-            final Resource child = containingResource.getChild(JCR_CONTENT);
+            Resource child = containingResource.getChild(JCR_CONTENT);
             if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 doAddAlias(child);
             }
@@ -269,7 +278,7 @@ private void handleAliasRemoval(
      * @param resource The resource
      * @return {@code true} if any change
      */
-    boolean doUpdateAlias(final Resource resource) {
+    boolean doUpdateAlias(@NotNull Resource resource) {
         if (this.aliasMapsMap != UNITIALIZED_MAP) {
             return doUpdateAliasInMap(resource);
         } else {
@@ -277,17 +286,16 @@ boolean doUpdateAlias(final Resource resource) {
         }
     }
 
-    private boolean doUpdateAliasInMap(final Resource resource) {
+    private boolean doUpdateAliasInMap(@NotNull Resource resource) {
 
         // resource containing the alias
-        final Resource containingResource = getResourceToBeAliased(resource);
+        Resource containingResource = getResourceToBeAliased(resource);
 
         if (containingResource != null) {
-            final String containingResourceName = containingResource.getName();
-            final String parentPath = ResourceUtil.getParent(containingResource.getPath());
+            String containingResourceName = containingResource.getName();
+            String parentPath = ResourceUtil.getParent(containingResource.getPath());
 
-            final Map<String, Collection<String>> aliasMapEntry =
-                    parentPath == null ? null : aliasMapsMap.get(parentPath);
+            Map<String, Collection<String>> aliasMapEntry = parentPath == null ? null : aliasMapsMap.get(parentPath);
             if (aliasMapEntry != null) {
                 aliasMapEntry.remove(containingResourceName);
                 if (aliasMapEntry.isEmpty()) {
@@ -300,7 +308,7 @@ private boolean doUpdateAliasInMap(final Resource resource) {
             if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 changed |= doAddAlias(containingResource);
             }
-            final Resource child = containingResource.getChild(JCR_CONTENT);
+            Resource child = containingResource.getChild(JCR_CONTENT);
             if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 changed |= doAddAlias(child);
             }
@@ -308,32 +316,72 @@ private boolean doUpdateAliasInMap(final Resource resource) {
             return changed;
         } else {
             log.warn("containingResource is null for alias on {}, skipping.", resource.getPath());
+            return false;
         }
+    }
 
-        return false;
+    public @NotNull Map<String, Collection<String>> getAliasMap(@Nullable String parentPath) {
+        Map<String, Collection<String>> result = this.aliasMapsMap != UNITIALIZED_MAP
+                ? getAliasMapFromCache(parentPath)
+                : getAliasMapFromRepo(parentPath);
+        return result != null ? result : Collections.emptyMap();
     }
 
-    public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) {
-        Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath);
-        return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap();
+    private @Nullable Map<String, Collection<String>> getAliasMapFromCache(@Nullable String parentPath) {
+        return aliasMapsMap.get(parentPath);
+    }
+
+    // TODO: there's an opportunity for optimization when the caller already has a Resource
+    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(@Nullable String parentPath) {
+
+        if (parentPath == null) {
+            return null;
+        } else {
+            try (ResourceResolver resolver =
+                    factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo(SERVICE_USER))) {
+
+                Resource parent = resolver.getResource(parentPath);
+                return getAliasMapFromRepo(parent);
+            } catch (LoginException ex) {
+                log.error("Could not obtain resolver to resolve any aliases from repository", ex);
+                return null;
+            }
+        }
+    }
+
+    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(@Nullable Resource parent) {
+
+        Map<String, Collection<String>> result = null;
+
+        if (parent != null) {
+            Map<String, Map<String, Collection<String>>> localMap = new HashMap<>();
+            List<String> throwAwayDiagnostics = new ArrayList<>();
+            for (Resource child : parent.getChildren()) {
+                loadAlias(child, localMap, throwAwayDiagnostics, throwAwayDiagnostics);
+            }
+            result = localMap.get(parent.getPath());
+        }
+
+        return result;
     }
 
     /**
      * Load aliases - Search for all nodes (except under /jcr:system) below
      * configured alias locations having the sling:alias property
      */
+    @NotNull
     private Map<String, Map<String, Collection<String>>> loadAliases(
-            List<String> conflictingAliases, List<String> invalidAliases) {
+            @Nullable List<String> conflictingAliases, @Nullable List<String> invalidAliases) {
 
-        final Map<String, Map<String, Collection<String>>> map = new ConcurrentHashMap<>();
+        Map<String, Map<String, Collection<String>>> map = new ConcurrentHashMap<>();
 
-        try (final ResourceResolver resolver =
-                factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"))) {
-            final String baseQueryString = generateAliasQuery();
+        try (ResourceResolver resolver =
+                factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo(SERVICE_USER))) {
+            String baseQueryString = generateAliasQuery();
 
             Iterator<Resource> it;
             try {
-                final String queryStringWithSort =
+                String queryStringWithSort =
                         baseQueryString + " AND FIRST([sling:alias]) >= '%s' ORDER BY FIRST([sling:alias])";
                 it = new PagedQueryIterator("alias", "sling:alias", resolver, queryStringWithSort, 2000);
             } catch (QuerySyntaxException ex) {
@@ -383,8 +431,9 @@ private Map<String, Map<String, Collection<String>>> loadAliases(
     /*
      * generate alias query based on configured alias locations
      */
+    @NotNull
     private String generateAliasQuery() {
-        final Set<String> allowedLocations = this.factory.getAllowedAliasLocations();
+        Set<String> allowedLocations = this.factory.getAllowedAliasLocations();
 
         StringBuilder baseQuery = new StringBuilder("SELECT [sling:alias] FROM [nt:base] WHERE");
 
@@ -414,19 +463,19 @@ private String generateAliasQuery() {
      * Load alias given a resource
      */
     private boolean loadAlias(
-            final Resource resource,
-            Map<String, Map<String, Collection<String>>> map,
-            List<String> conflictingAliases,
-            List<String> invalidAliases) {
+            @NotNull Resource resource,
+            @NotNull Map<String, Map<String, Collection<String>>> map,
+            @Nullable List<String> conflictingAliases,
+            @Nullable List<String> invalidAliases) {
 
         // resource containing the alias
-        final Resource containingResource = getResourceToBeAliased(resource);
+        Resource containingResource = getResourceToBeAliased(resource);
 
         if (containingResource == null) {
             log.warn("containingResource is null for alias on {}, skipping.", resource.getPath());
             return false;
         } else {
-            final Resource parent = containingResource.getParent();
+            Resource parent = containingResource.getParent();
 
             if (parent == null) {
                 log.warn(
@@ -435,7 +484,7 @@ private boolean loadAlias(
                         resource.getPath());
                 return false;
             } else {
-                final String[] aliasArray = resource.getValueMap().get(ResourceResolverImpl.PROP_ALIAS, String[].class);
+                String[] aliasArray = resource.getValueMap().get(ResourceResolverImpl.PROP_ALIAS, String[].class);
                 if (aliasArray == null) {
                     return false;
                 } else {
@@ -455,19 +504,19 @@ private boolean loadAlias(
      * Load alias given an alias array, return success flag.
      */
     private boolean loadAliasFromArray(
-            final String[] aliasArray,
-            Map<String, Map<String, Collection<String>>> map,
-            List<String> conflictingAliases,
-            List<String> invalidAliases,
-            final String resourceName,
-            final String parentPath) {
+            @Nullable String[] aliasArray,
+            @NotNull Map<String, Map<String, Collection<String>>> map,
+            @Nullable List<String> conflictingAliases,
+            @Nullable List<String> invalidAliases,
+            @NotNull String resourceName,
+            @NotNull String parentPath) {
 
         boolean hasAlias = false;
 
         log.debug("Found alias, total size {}", aliasArray.length);
 
         // the order matters here, the first alias in the array must come first
-        for (final String alias : aliasArray) {
+        for (String alias : aliasArray) {
             if (isAliasInvalid(alias)) {
                 long invalids = detectedInvalidAliases.incrementAndGet();
                 log.warn(
@@ -516,10 +565,10 @@ private boolean loadAliasFromArray(
      * Given a resource, check whether the name is "jcr:content", in which case return the parent resource
      *
      * @param resource resource to check
-     * @return parent of jcr:content resource (may be null), otherwise the resource itself
+     * @return parent of jcr:content resource (can be null), otherwise the resource itself
      */
     @Nullable
-    private Resource getResourceToBeAliased(Resource resource) {
+    private Resource getResourceToBeAliased(@NotNull Resource resource) {
         if (JCR_CONTENT.equals(resource.getName())) {
             return resource.getParent();
         } else {
@@ -530,24 +579,30 @@ private Resource getResourceToBeAliased(Resource resource) {
     /**
      * Check alias syntax
      */
-    private boolean isAliasInvalid(String alias) {
-        boolean invalid = alias.equals("..") || alias.equals(".") || alias.isEmpty();
-        if (!invalid) {
-            for (final char c : alias.toCharArray()) {
-                // invalid if / or # or a ?
-                if (c == '/' || c == '#' || c == '?') {
-                    invalid = true;
-                    break;
+    private boolean isAliasInvalid(@Nullable String alias) {
+        boolean invalid;
+        if (alias == null) {
+            invalid = true;
+        } else {
+            invalid = alias.equals("..") || alias.equals(".") || alias.isEmpty();
+            if (!invalid) {
+                for (char c : alias.toCharArray()) {
+                    // invalid if / or # or a ?
+                    if (c == '/' || c == '#' || c == '?') {
+                        invalid = true;
+                        break;
+                    }
                 }
             }
         }
         return invalid;
     }
 
-    private Iterator<Resource> queryUnpaged(String query, ResourceResolver resolver) {
+    @NotNull
+    private Iterator<Resource> queryUnpaged(@NotNull String query, @NotNull ResourceResolver resolver) {
         log.debug("start alias query: {}", query);
         long queryStart = System.nanoTime();
-        final Iterator<Resource> it = resolver.findResources(query, "JCR-SQL2");
+        Iterator<Resource> it = resolver.findResources(query, "JCR-SQL2");
         long queryElapsed = System.nanoTime() - queryStart;
         log.debug("end alias query; elapsed {}ms", TimeUnit.NANOSECONDS.toMillis(queryElapsed));
         return it;
@@ -555,7 +610,7 @@ private Iterator<Resource> queryUnpaged(String query, ResourceResolver resolver)
 
     private final AtomicLong lastTimeLogged = new AtomicLong(-1);
 
-    void logDisableAliasOptimization(final Exception e) {
+    void logDisableAliasOptimization(@Nullable Exception e) {
         if (e != null) {
             log.error(
                     "Unexpected problem during initialization of optimize alias resolution. Therefore disabling optimize alias resolution. Please fix the problem.",
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 c84abd081..ab9f8fc5c 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
@@ -200,7 +200,6 @@ private boolean updateResource(final String path, final AtomicBoolean resolverRe
             if (resource != null) {
 
                 boolean vanityPathChanged = false;
-                boolean aliasChanged = false;
 
                 if (isValidVanityPath) {
                     // we remove the old vanity path first
@@ -215,7 +214,7 @@ private boolean updateResource(final String path, final AtomicBoolean resolverRe
                     vanityPathChanged |= vph.doAddVanity(contentRsrc != null ? contentRsrc : resource);
                 }
 
-                aliasChanged |= ah.doUpdateAlias(resource);
+                boolean aliasChanged = ah.doUpdateAlias(resource);
                 return vanityPathChanged || aliasChanged;
             }
         } finally {
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
index ca4eafd72..2366ebc39 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImpl.java
@@ -19,7 +19,6 @@
 package org.apache.sling.resourceresolver.impl.mapping;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -33,7 +32,6 @@
 import org.apache.sling.resourceresolver.impl.JcrNamespaceMangler;
 import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
 import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
-import org.apache.sling.resourceresolver.impl.helper.ResourceResolverControl;
 import org.apache.sling.resourceresolver.impl.helper.URI;
 import org.apache.sling.resourceresolver.impl.helper.URIException;
 import org.apache.sling.resourceresolver.impl.params.ParsedParameters;
@@ -234,73 +232,35 @@ private List<String> loadAliasesIfApplicable(final Resource nonDecoratedResource
     }
 
     private void resolveAliases(Resource res, PathGenerator pathBuilder) {
-        Resource current = res;
         String path = res.getPath();
-        if (this.mapEntries.isOptimizeAliasResolutionEnabled()) {
-            // this code path avoids any creation of Sling Resource objects
-            while (path != null) {
-                Collection<String> aliases = Collections.emptyList();
-                // read alias only if we can read the resources and it's not a jcr:content leaf
-                if (!path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) {
-                    aliases = readAliasesOptimized(path);
-                }
-                // build the path from the name segments or aliases
-                pathBuilder.insertSegment(aliases, ResourceUtil.getName(path));
-                path = ResourceUtil.getParent(path);
-                if ("/".equals(path)) {
-                    path = null;
-                }
+
+        while (path != null) {
+            Collection<String> aliases = Collections.emptyList();
+            // read alias only if we can read the resources and it's not a jcr:content leaf
+            if (!path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) {
+                aliases = readAliases(path);
             }
-        } else {
-            // while here there Resources are resolved
-            while (path != null) {
-                List<String> aliases = Collections.emptyList();
-                // read alias only if we can read the resources and it's not a jcr:content leaf
-                if (current != null && !path.endsWith(ResourceResolverImpl.JCR_CONTENT_LEAF)) {
-                    aliases = readAliases(path, current);
-                }
-                // build the path from the name segments or aliases
-                pathBuilder.insertSegment(aliases, ResourceUtil.getName(path));
-                path = ResourceUtil.getParent(path);
-                if ("/".equals(path)) {
-                    path = null;
-                } else if (path != null) {
-                    current = resolver.resolve(path);
-                }
+            // build the path from the name segments or aliases
+            pathBuilder.insertSegment(aliases, ResourceUtil.getName(path));
+            path = ResourceUtil.getParent(path);
+            if ("/".equals(path)) {
+                path = null;
             }
         }
     }
 
     /**
-     * Resolve the aliases for the given resource by directly reading the sling:alias property
-     * @param path the path of the resource
-     * @param current the resource
-     * @return
+     * Resolve the aliases for the given resource by a lookup in MapEntries
+     * @param path path for which to lookup aliases
+     * @return collection of aliases for that resource
      */
-    private List<String> readAliases(String path, Resource current) {
-        logger.debug("map: Optimize Alias Resolution is Disabled");
-        String[] aliases =
-                ResourceResolverControl.getProperty(current, ResourceResolverImpl.PROP_ALIAS, String[].class);
-        if (aliases == null || aliases.length == 0) return Collections.emptyList();
-        if (aliases.length == 1) return Collections.singletonList(aliases[0]);
-        return Arrays.asList(aliases);
-    }
-
-    /**
-     * Resolve teh aliases for the given resource by a lookup in the mapEntries structure, avoiding
-     * any repository access
-     * @param path
-     * @return
-     */
-    private Collection<String> readAliasesOptimized(String path) {
-        logger.debug("map: Optimize Alias Resolution is Enabled");
+    private Collection<String> readAliases(String path) {
         String parentPath = ResourceUtil.getParent(path);
         if (parentPath == null) {
             return Collections.emptyList();
+        } else {
+            return mapEntries.getAliasMap(parentPath).getOrDefault(ResourceUtil.getName(path), Collections.emptyList());
         }
-        String name = ResourceUtil.getName(path);
-
-        return mapEntries.getAliasMap(parentPath).getOrDefault(name, Collections.emptyList());
     }
 
     private void populateMappingsFromMapEntries(
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java
index aee76b6a6..9157dee0f 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java
@@ -1173,6 +1173,11 @@ public void test_doRemoveAliasFromSibling() throws Exception {
         assertEquals(Collections.emptyMap(), aliasMapEntry);
     }
 
+    @Test
+    public void test_doRemoveAliasNullResolver() throws Exception {
+        removeAlias(mapEntries, null, "/x", null, NOOP);
+    }
+
     @Test
     public void test_initAliasesAfterDispose() {
         AliasHandler ah = mapEntries.ah;
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
index f45fef2be..c0fb72147 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResourceMapperImplTest.java
@@ -547,15 +547,14 @@ public void mapAliasLookupOptimization() throws Exception {
      */
     private void assertResourceResolverAccess(
             ResourceResolverImpl spyResolver, ResourceMapperImpl mapper, String path) {
-        int pathSegments = (int) path.chars().filter(c -> c == '/').count();
         mapper.getMapping(path);
-        if (this.optimiseAliasResolution) {
-            Mockito.verify(spyResolver, Mockito.times(0)).resolve(Mockito.any(String.class));
-            Mockito.verify(spyResolver, Mockito.times(1)).resolveInternal(Mockito.any(String.class), Mockito.anyMap());
-        } else {
-            Mockito.verify(spyResolver, Mockito.times(pathSegments - 1)).resolve(Mockito.any(String.class));
-            Mockito.verify(spyResolver, Mockito.times(pathSegments))
-                    .resolveInternal(Mockito.any(String.class), Mockito.anyMap());
+        Mockito.verify(spyResolver, Mockito.times(0)).resolve(Mockito.any(String.class));
+        Mockito.verify(spyResolver, Mockito.times(1)).resolveInternal(Mockito.any(String.class), Mockito.anyMap());
+        if (!this.optimiseAliasResolution) {
+            // int pathSegments = (int) path.chars().filter(c -> c == '/').count();
+            // we should see here multiple calls to getResource, but the alias
+            // handler uses a different instance that is acquired for each interaction
+            // maybe something to check
         }
         Mockito.clearInvocations(spyResolver);
     }


Reviews:
user: login: joerghoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:49:17Z
user: login: joerghoh
body: LGTM ... maybe a few more @Nullable/@NotNull annotation could be helpful.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:54:54Z
user: login: joerghoh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:36:30Z
user: login: joerghoh
body: +1 (besides that single suggestion to improve the exception message ...)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:16:51Z

Review comments:
diff_hunk: @@ -308,14 +311,52 @@ private boolean doUpdateAliasInMap(final Resource resource) {
             return changed;
         } else {
             log.warn("containingResource is null for alias on {}, skipping.", resource.getPath());
+            return false;
         }
-
-        return false;
     }
 
     public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) {
-        Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath);
-        return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap();
+        Map<String, Collection<String>> result = this.aliasMapsMap != UNITIALIZED_MAP
+                ? getAliasMapFromCache(parentPath)
+                : getAliasMapFromRepo(parentPath);
+        return result != null ? result : Collections.emptyMap();
+    }
+
+    private @Nullable Map<String, Collection<String>> getAliasMapFromCache(final String parentPath) {
+        return aliasMapsMap.get(parentPath);
+    }
+
+    // TODO: there's an opportunity for optimization when the caller already has a Resource
+    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(final String parentPath) {
+
+        Map<String, Collection<String>> result = null;
+
+        try (final ResourceResolver resolver =
+                factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo(SERVICE_USER))) {
+
+            Resource parent = resolver.getResource(parentPath);
+            result = getAliasMapFromRepo(parent);
+        } catch (LoginException ex) {
+            log.error("Could not obtain resolver", ex);
+        }
+
+        return result;
+    }
+
+    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(final Resource parent) {
path: src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java
user: login: joerghoh
body: ```suggestion
    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(@Nullable final Resource parent) {
```
created_at: 2025-05-06T17:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,40 +308,80 @@ private boolean doUpdateAliasInMap(final Resource resource) {
             if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 changed |= doAddAlias(containingResource);
             }
-            final Resource child = containingResource.getChild(JCR_CONTENT);
+            Resource child = containingResource.getChild(JCR_CONTENT);
             if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                 changed |= doAddAlias(child);
             }
 
             return changed;
         } else {
             log.warn("containingResource is null for alias on {}, skipping.", resource.getPath());
+            return false;
         }
+    }
 
-        return false;
+    public @NotNull Map<String, Collection<String>> getAliasMap(@Nullable String parentPath) {
+        Map<String, Collection<String>> result = this.aliasMapsMap != UNITIALIZED_MAP
+                ? getAliasMapFromCache(parentPath)
+                : getAliasMapFromRepo(parentPath);
+        return result != null ? result : Collections.emptyMap();
     }
 
-    public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) {
-        Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath);
-        return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap();
+    private @Nullable Map<String, Collection<String>> getAliasMapFromCache(@Nullable String parentPath) {
+        return aliasMapsMap.get(parentPath);
+    }
+
+    // TODO: there's an opportunity for optimization when the caller already has a Resource
+    private @Nullable Map<String, Collection<String>> getAliasMapFromRepo(@Nullable String parentPath) {
+
+        if (parentPath == null) {
+            return null;
+        } else {
+            try (ResourceResolver resolver =
+                    factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo(SERVICE_USER))) {
+
+                Resource parent = resolver.getResource(parentPath);
+                return getAliasMapFromRepo(parent);
+            } catch (LoginException ex) {
+                log.error("Could not obtain resolver", ex);
path: src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java
user: login: joerghoh
body: I would make this log message a bit more helpful
```suggestion
                log.error("Could not obtain resolver to resolve any aliases from repository", ex);
```
created_at: 2025-05-13T16:36:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T16:46:46Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [89.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=apache_sling-org-apache-sling-resourceresolver&pullRequest=175)


author_association: NONE