SLING-9620 - ResourceMapperImpl.getAllMappings does not respect multi-valued sling:alias

Generate all possible alias paths in case of multiple aliases.
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilder.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilder.java
index bc37a86..207e557 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilder.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilder.java
@@ -19,6 +19,7 @@
 package org.apache.sling.resourceresolver.impl.mapping;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -31,10 +32,15 @@
  */
 public class PathBuilder {
     
-    // visible for testing
-    static List<String> cartesianJoin(List<List<String>> segments) {
+    private static List<String> cartesianJoin(List<List<String>> segments, String toAppend) {
         
         return cartesianJoin0(0, segments).stream()
+                .map ( sb -> {
+                    if ( toAppend != null )
+                        sb.append(toAppend);
+                    
+                    return sb;
+                })
                 .map( StringBuilder::toString )
                 .collect( Collectors.toList() );
     }
@@ -42,12 +48,13 @@
     private static List<StringBuilder> cartesianJoin0(int index, List<List<String>> segments) {
         List<StringBuilder> out = new ArrayList<>();
         if ( index == segments.size() ) {
-            out.add(new StringBuilder());
+            out.add(new StringBuilder("/"));
         } else {
             for ( String segment : segments.get(index) ) {
                 for (StringBuilder sb : cartesianJoin0(index + 1, segments) ) {
-                    // TODO - this is sub-optimal, as we are copying the array for each move
-                    sb.insert(0, '/' + segment);
+                    sb.append(segment);
+                    if ( index != 0 )
+                        sb.append('/');
                     out.add(sb);
                 }
             }
@@ -56,7 +63,7 @@
         return out;
     }
 
-    private List<String> segments = new ArrayList<>();
+    private List<List<String>> segments = new ArrayList<>();
     private String toAppend;
     
     /**
@@ -65,8 +72,14 @@
      * @param alias the alias, ignored if null or empty
      * @param name the name
      */
-    public void insertSegment(@Nullable String alias, @NotNull String name) {
-        segments.add(alias != null && alias.length() != 0 ? alias : name);
+    public void insertSegment(@NotNull List<String> alias, @NotNull String name) {
+        
+        // TODO - can we avoid filtering here?
+        List<String> filtered = alias.stream()
+            .filter( e -> e != null && ! e.isEmpty() )
+            .collect(Collectors.toList());
+        
+        segments.add(!filtered.isEmpty() ? alias : Collections.singletonList(name));
     }
     
     /**
@@ -83,18 +96,7 @@
      * 
      * @return a path in string form
      */
-    public String toPath() {
-        StringBuilder sb = new StringBuilder();
-        sb.append('/');
-        for ( int i = segments.size() - 1 ; i >= 0 ; i-- ) {
-            sb.append(segments.get(i));
-            if ( i == 1 )
-                sb.append('/');
-        }
-        
-        if ( toAppend != null )
-            sb.append(toAppend);
-        
-        return sb.toString();
+    public List<String> toPaths() {
+        return cartesianJoin(segments, toAppend);
     }
 }
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 6fdd883..acc4707 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,6 +19,7 @@
 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;
@@ -95,21 +96,21 @@
         // Therefore we take care to add the entries in a very particular order, which preserves backwards 
         // compatibility with the existing implementation. Previously the order was
         //
-        //   resource path → alias → mapping (with alias potentially being null)
+        //   resource path → aliases → mapping (with aliases potentially being empty)
         //
         // To ensure we keep the same behaviour but expose all possible mappings, we now have the following
         // flow
         // 
         //  resource path → mapping
-        //  resource path → alias
-        //  alias → mapping
+        //  resource path → aliases
+        //  aliases → mappings
         //
         // After all are processed we reverse the order to preserve the logic of the old ResourceResolver.map() method (last
         // found wins) and also make sure that no duplicates are added.
         //
         // There is some room for improvement here by using a data structure that does not need reversing ( ArrayList
         // .add moves the elements every time ) or reversal of duplicates but since we will have a small number of 
-        // entries ( <= 4 ) the time spent here should be negligible.
+        // entries ( <= 4 in case of single aliases) the time spent here should be negligible.
         
         List<String> mappings = new ArrayList<>();
         
@@ -141,18 +142,18 @@
         mappings.add(mappedPath);
 
         // 3. load mappings from the resource path
-        populateMappingsFromMapEntries(mappings, mappedPath, requestContext);
-        
+        populateMappingsFromMapEntries(mappings, Collections.singletonList(mappedPath), requestContext);
         
         // 4. load aliases
         final Resource nonDecoratedResource = resolver.resolveInternal(parsed.getRawPath(), parsed.getParameters());
         if (nonDecoratedResource != null) {
-            String alias = loadAliasIfApplicable(nonDecoratedResource);
+            List<String> aliases = loadAliasesIfApplicable(nonDecoratedResource);
+            // ensure that the first declared alias will be returned first
+            Collections.reverse(aliases);
             
             // 5. load mappings for alias
-            if ( alias != null )
-                mappings.add(alias);
-            populateMappingsFromMapEntries(mappings, alias, requestContext);
+            mappings.addAll(aliases);
+            populateMappingsFromMapEntries(mappings, aliases, requestContext);
         }
 
         // 6. apply context path if needed
@@ -172,7 +173,7 @@
         return new LinkedHashSet<>(mappings);
     }
 
-    private String loadAliasIfApplicable(final Resource nonDecoratedResource) {
+    private List<String> loadAliasesIfApplicable(final Resource nonDecoratedResource) {
         //Invoke the decorator for the resolved resource
         Resource res = resourceDecorator.decorate(nonDecoratedResource); 
 
@@ -192,13 +193,13 @@
         Resource current = res;
         String path = res.getPath();
         while (path != null) {
-            String alias = 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)) {
-                alias = readAlias(path, current);
+                aliases = readAliases(path, current);
             }
             // build the path from the name segments or aliases
-            pathBuilder.insertSegment(alias, ResourceUtil.getName(path));
+            pathBuilder.insertSegment(aliases, ResourceUtil.getName(path));
             path = ResourceUtil.getParent(path);
             if ("/".equals(path)) {
                 path = null;
@@ -208,82 +209,90 @@
         }
         
         // and then we have the mapped path to work on
-        String mappedPath = pathBuilder.toPath();
+        List<String> mappedPaths = pathBuilder.toPaths();
 
-        logger.debug("map: Alias mapping resolves to path {}", mappedPath);
+        logger.debug("map: Alias mapping resolves to paths {}", mappedPaths);
         
-        return mappedPath;
+        return mappedPaths;
     }
     
-    private String readAlias(String path, Resource current) {
+    private List<String> readAliases(String path, Resource current) {
         if (optimizedAliasResolutionEnabled) {
             logger.debug("map: Optimize Alias Resolution is Enabled");
             String parentPath = ResourceUtil.getParent(path);
             
             if ( parentPath == null )
-                return null;
+                return Collections.emptyList();
             
             final Map<String, String> aliases = mapEntries.getAliasMap(parentPath);
             
             if ( aliases == null ) 
-                return null;
+                return Collections.emptyList();
             
             if ( aliases.containsValue(current.getName()) ) {
                 for ( Map.Entry<String,String> entry : aliases.entrySet() ) {
                     if (current.getName().equals(entry.getValue())) {
-                        return entry.getKey();
+                        // TODO - support multi-valued entries
+                        return Collections.singletonList(entry.getKey());
                     }
                 }
             }
-            return null;
+            return Collections.emptyList();
         } else {
             logger.debug("map: Optimize Alias Resolution is Disabled");
-            return ResourceResolverControl.getProperty(current, ResourceResolverImpl.PROP_ALIAS);
+            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);
         }        
     }
 
-    private void populateMappingsFromMapEntries(List<String> mappings, String mappedPath,
+    private void populateMappingsFromMapEntries(List<String> mappings, List<String> mappedPathList,
             final RequestContext requestContext) {
         boolean mappedPathIsUrl = false;
-        for (final MapEntry mapEntry : mapEntries.getMapMaps()) {
-            final String[] mappedPaths = mapEntry.replace(mappedPath);
-            if (mappedPaths != null) {
-
-                logger.debug("map: Match for Entry {}", mapEntry);
-
-                mappedPathIsUrl = !mapEntry.isInternal();
-
-                if (mappedPathIsUrl && requestContext.hasUri() ) {
-
-                    mappedPath = null;
-
-                    for (final String candidate : mappedPaths) {
-                        if (candidate.startsWith(requestContext.getUri())) {
-                            mappedPath = candidate.substring(requestContext.getUri().length() - 1);
-                            mappedPathIsUrl = false;
-                            logger.debug("map: Found host specific mapping {} resolving to {}", candidate, mappedPath);
-                            break;
-                        } else if (candidate.startsWith(requestContext.getSchemeWithPrefix()) && mappedPath == null) {
-                            mappedPath = candidate;
+        for ( String mappedPath : mappedPathList ) {
+            for (final MapEntry mapEntry : mapEntries.getMapMaps()) {
+                final String[] mappedPaths = mapEntry.replace(mappedPath);
+                if (mappedPaths != null) {
+    
+                    logger.debug("map: Match for Entry {}", mapEntry);
+    
+                    mappedPathIsUrl = !mapEntry.isInternal();
+    
+                    if (mappedPathIsUrl && requestContext.hasUri() ) {
+    
+                        mappedPath = null;
+    
+                        for (final String candidate : mappedPaths) {
+                            if (candidate.startsWith(requestContext.getUri())) {
+                                mappedPath = candidate.substring(requestContext.getUri().length() - 1);
+                                mappedPathIsUrl = false;
+                                logger.debug("map: Found host specific mapping {} resolving to {}", candidate, mappedPath);
+                                break;
+                            } else if (candidate.startsWith(requestContext.getSchemeWithPrefix()) && mappedPath == null) {
+                                mappedPath = candidate;
+                            }
                         }
-                    }
-
-                    if (mappedPath == null) {
+    
+                        if (mappedPath == null) {
+                            mappedPath = mappedPaths[0];
+                        }
+    
+                    } else {
+    
+                        // we can only go with assumptions selecting the first entry
                         mappedPath = mappedPaths[0];
+    
                     }
-
-                } else {
-
-                    // we can only go with assumptions selecting the first entry
-                    mappedPath = mappedPaths[0];
-
+    
+                    logger.debug("map: MapEntry {} matches, mapped path is {}", mapEntry, mappedPath);
+                    
+                    mappings.add(mappedPath);
+    
+                    break;
                 }
-
-                logger.debug("map: MapEntry {} matches, mapped path is {}", mapEntry, mappedPath);
-                
-                mappings.add(mappedPath);
-
-                break;
             }
         }
     }
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilderTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilderTest.java
index 6f4ba78..79b454b 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilderTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PathBuilderTest.java
@@ -18,9 +18,11 @@
  */
 package org.apache.sling.resourceresolver.impl.mapping;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
 import static org.junit.Assert.*;
 
-import java.util.Arrays;
 import java.util.List;
 
 import org.hamcrest.Matchers;
@@ -30,51 +32,75 @@
 
     @Test
     public void buildRootPath() {
-        assertThat(new PathBuilder().toPath(), Matchers.equalTo("/"));
+        
+        List<String> paths = new PathBuilder().toPaths();
+        
+        assertThat(paths, Matchers.hasSize(1));
+        assertThat(paths, Matchers.hasItem("/"));
     }
 
     @Test
     public void buildSubPathWithMissingAliases() {
+        
         PathBuilder builder = new PathBuilder();
-        builder.insertSegment(null, "bar");
-        builder.insertSegment("", "foo");
-        assertThat(builder.toPath(), Matchers.equalTo("/foo/bar"));
+        builder.insertSegment(singletonList(null), "bar");
+        builder.insertSegment(singletonList(""), "foo");
+        List<String> paths = builder.toPaths();
+        
+        assertThat(paths, Matchers.hasSize(1));
+        assertThat(paths, Matchers.hasItem("/foo/bar"));
     }
 
     @Test
     public void buildSubPathWithMixedAliases() {
+        
         PathBuilder builder = new PathBuilder();
-        builder.insertSegment(null, "bar");
-        builder.insertSegment("super", "foo");
-        assertThat(builder.toPath(), Matchers.equalTo("/super/bar"));
+        builder.insertSegment(emptyList(), "bar");
+        builder.insertSegment(singletonList("super"), "foo");
+        List<String> paths = builder.toPaths();
+        
+        assertThat(paths, Matchers.hasSize(1));
+        assertThat(paths, Matchers.hasItem("/super/bar"));
     }
     
     @Test
     public void buildSubPathWithResolutionInfo() {
+        
         PathBuilder builder = new PathBuilder();
-        builder.insertSegment(null, "bar");
-        builder.insertSegment(null, "foo");
+        builder.insertSegment(emptyList(), "bar");
+        builder.insertSegment(emptyList(), "foo");
         builder.setResolutionPathInfo("/baz");
-        assertThat(builder.toPath(), Matchers.equalTo("/foo/bar/baz"));
+        
+        List<String> paths = builder.toPaths();
+        
+        assertThat(paths, Matchers.hasSize(1));
+        assertThat(paths, Matchers.hasItem("/foo/bar/baz"));        
     }
     
     @Test
-    public void cartesianJoin_simple() {
-        List<String> paths = PathBuilder.cartesianJoin(Arrays.asList( Arrays.asList("1"), Arrays.asList("2") ));
-        assertThat(paths, Matchers.hasSize(1));
-        assertThat(paths, Matchers.hasItem("/1/2"));
+    public void buildSubPathWithMultipleAliases() {
+        
+        PathBuilder builder = new PathBuilder();
+        builder.insertSegment(emptyList(), "bar");
+        builder.insertSegment(asList("alias1", "alias2"), "foo");
+        
+        List<String> paths = builder.toPaths();
+        
+        assertThat(paths, Matchers.hasSize(2));
+        assertThat(paths, Matchers.hasItems("/alias1/bar", "/alias2/bar"));
     }
 
     @Test
-    public void cartesianJoin_multiple() {
-        List<String> paths = PathBuilder.cartesianJoin(Arrays.asList( Arrays.asList("1a", "1b"), Arrays.asList("2") ));
-        assertThat(paths, Matchers.hasSize(2));
-        assertThat(paths, Matchers.hasItems("/1a/2", "/1b/2"));
-    }
-    
-    @Test
-    public void cartesianJoin_complex() {
-        List<String> paths = PathBuilder.cartesianJoin(Arrays.asList( Arrays.asList("1a", "1b"), Arrays.asList("2a", "2b"), Arrays.asList("3"), Arrays.asList("4a", "4b", "4c") ));
+    public void buildSubPathWithComplexAliasesSetup() {
+        
+        PathBuilder builder = new PathBuilder();
+        builder.insertSegment(asList("4a", "4b", "4c"), "4");
+        builder.insertSegment(emptyList(), "3");
+        builder.insertSegment(asList("2a", "2b"), "2");
+        builder.insertSegment(asList("1a", "1b"), "1");
+        
+        List<String> paths = builder.toPaths();
+        
         assertThat(paths, Matchers.hasSize(12));
         assertThat(paths, Matchers.hasItems(
                 "/1a/2a/3/4a",
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 5754143..e267be5 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
@@ -25,6 +25,7 @@
 import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -205,8 +206,9 @@
      * @throws LoginException
      */
     @Test
-    @Ignore("SLING-9620")
     public void mapResourceWithMultivaluedAlias() {
+        
+        assumeFalse(optimiseAliasResolution);
 
         ExpectedMappings.existingResource("/there-multiple")
                 .singleMapping("/alias-value-3")