You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ju...@apache.org on 2014/06/25 16:32:53 UTC

svn commit: r1605423 - in /sling/trunk/bundles/resourceresolver/src/main: java/org/apache/sling/resourceresolver/impl/console/ java/org/apache/sling/resourceresolver/impl/mapping/ resources/ resources/res/ resources/res/ui/

Author: justin
Date: Wed Jun 25 14:32:53 2014
New Revision: 1605423

URL: http://svn.apache.org/r1605423
Log:
SLING-3505 - minimizing scope of change when a single vanity path changes or when the vanity order of an existing vanity path changes.

Added:
    sling/trunk/bundles/resourceresolver/src/main/resources/
    sling/trunk/bundles/resourceresolver/src/main/resources/res/
    sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/
    sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/resourceresolver.css
Modified:
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java?rev=1605423&r1=1605422&r2=1605423&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/console/ResourceResolverWebConsolePlugin.java Wed Jun 25 14:32:53 2014
@@ -21,12 +21,15 @@ package org.apache.sling.resourceresolve
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
+import java.net.URL;
 import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Dictionary;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Set;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
@@ -71,6 +74,7 @@ public class ResourceResolverWebConsoleP
         props.put(Constants.SERVICE_PID, getClass().getName());
         props.put("felix.webconsole.label", "jcrresolver");
         props.put("felix.webconsole.title", "Resource Resolver");
+        props.put("felix.webconsole.css", "/jcrresolver/res/ui/resourceresolver.css");
         props.put("felix.webconsole.category", "Sling");
         props.put("felix.webconsole.configprinter.modes", "always");
 
@@ -275,10 +279,17 @@ public class ResourceResolverWebConsoleP
         pw.println("<th class='content'>Redirect</th>");
         pw.println("</tr>");
 
+        Set<String> usedPatterns = new HashSet<String>();
+        
         for (MapEntry entry : list) {
-            pw.println("<tr class='content'>");
+            String pattern = entry.getPattern();
+            pw.print("<tr class='content");
+            if (!usedPatterns.add(pattern)) {
+                pw.print(" duplicate");
+            }
+            pw.println("'>");
             pw.println("<td class='content' style='vertical-align: top'>"
-                    + entry.getPattern() + "</td>");
+                    + pattern + "</td>");
 
             pw.print("<td class='content' style='vertical-align: top'>");
             String[] repls = entry.getRedirect();
@@ -338,6 +349,17 @@ public class ResourceResolverWebConsoleP
         pw.println();
     }
 
+    /**
+     * Method to retrieve static resources from this bundle.
+     */
+    @SuppressWarnings("unused")
+    private URL getResource(final String path) {
+        if (path.startsWith("/jcrresolver/res/ui/")) {
+            return this.getClass().getResource(path.substring(12));
+        }
+        return null;
+    }
+
     private static class ResolverRequest extends HttpServletRequestWrapper {
 
         private final URI uri;

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java?rev=1605423&r1=1605422&r2=1605423&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java Wed Jun 25 14:32:53 2014
@@ -25,18 +25,16 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Dictionary;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.NoSuchElementException;
-import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import java.util.concurrent.Semaphore;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -97,7 +95,7 @@ public class MapEntries implements Event
 
     private Collection<MapEntry> mapMaps;
 
-    private Collection<String> vanityTargets;
+    private Map <String,List <String>> vanityTargets;
 
     private Map<String, Map<String, String>> aliasMap;
 
@@ -105,8 +103,6 @@ public class MapEntries implements Event
 
     private EventAdmin eventAdmin;
 
-    private final Semaphore initTrigger = new Semaphore(0);
-
     private final ReentrantLock initializing = new ReentrantLock();
 
     private final boolean enabledVanityPaths;
@@ -125,7 +121,7 @@ public class MapEntries implements Event
 
         this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
         this.mapMaps = Collections.<MapEntry> emptyList();
-        this.vanityTargets = Collections.<String> emptySet();
+        this.vanityTargets = Collections.<String,List <String>>emptyMap();
         this.aliasMap = Collections.<String, Map<String, String>>emptyMap();
         this.registration = null;
         this.eventAdmin = null;
@@ -149,7 +145,7 @@ public class MapEntries implements Event
 
         this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
         this.mapMaps = Collections.<MapEntry> emptyList();
-        this.vanityTargets = Collections.<String> emptySet();
+        this.vanityTargets = Collections.<String,List <String>>emptyMap();
         this.aliasMap = Collections.<String, Map<String, String>>emptyMap();
 
         doInit();
@@ -160,41 +156,6 @@ public class MapEntries implements Event
         props.put(Constants.SERVICE_DESCRIPTION, "Map Entries Observation");
         props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
         this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props);
-
-        final Thread updateThread = new Thread(new Runnable() {
-            public void run() {
-                MapEntries.this.init();
-            }
-        }, "MapEntries Update");
-        updateThread.setDaemon(true);
-        updateThread.start();
-    }
-
-    /**
-     * Signals the init method that a the doInit method should be called.
-     */
-    private void triggerInit() {
-        // only release if there is not one in the queue already
-        if (initTrigger.availablePermits() < 1) {
-            initTrigger.release();
-        }
-    }
-
-    /**
-     * Runs as the method of the update thread. Waits for the triggerInit method
-     * to trigger a call to doInit. Terminates when the resolver has been
-     * null-ed after having been triggered.
-     */
-    private void init() {
-        while (this.resolver != null) {
-            try {
-                this.initTrigger.acquire();
-                this.doInit();
-            } catch (final InterruptedException ie) {
-                // just continue acquisition
-            }
-        }
-
     }
 
     /**
@@ -212,36 +173,22 @@ public class MapEntries implements Event
                 return;
             }
 
-            final Map<String, List<MapEntry>> newResolveMapsMap = new HashMap<String, List<MapEntry>>();
-            final List<MapEntry> globalResolveMap = new ArrayList<MapEntry>();
-            final SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
-
-            // load the /etc/map entries into the maps
-            loadResolverMap(resolver, globalResolveMap, newMapMaps);
-
-            // load the configuration into the resolver map
-            final Collection<String> vanityTargets = (this.enabledVanityPaths ? this.loadVanityPaths(resolver, newResolveMapsMap) : Collections.<String> emptySet());
-            loadConfiguration(factory, globalResolveMap);
-
-            // load the configuration into the mapper map
-            loadMapConfiguration(factory, newMapMaps);
-
-            // sort global list and add to map
-            Collections.sort(globalResolveMap);
-            newResolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
-
+            final Map<String, List<MapEntry>> newResolveMapsMap = new ConcurrentHashMap<String, List<MapEntry>>();  
+ 
+            final Map<String,List<String>> vanityTargets = (this.enabledVanityPaths ? this.loadVanityPaths(resolver, newResolveMapsMap) : Collections.<String,List <String>>emptyMap());
+            
             //optimization made in SLING-2521
             if (enableOptimizeAliasResolution){
                 final Map<String, Map<String, String>> aliasMap = this.loadAliases(resolver);
-                this.aliasMap = makeUnmodifiableMap(aliasMap);
+                this.aliasMap = aliasMap;
             }
 
-            this.vanityTargets = Collections.unmodifiableCollection(vanityTargets);
-            this.resolveMapsMap = Collections.unmodifiableMap(newResolveMapsMap);
-            this.mapMaps = Collections.unmodifiableSet(new TreeSet<MapEntry>(newMapMaps.values()));
+            this.vanityTargets = vanityTargets;
+            this.resolveMapsMap = newResolveMapsMap; 
 
-            sendChangeEvent();
+            doUpdateConfiguration();
 
+            sendChangeEvent();
         } catch (final Exception e) {
 
             log.warn("doInit: Unexpected problem during initialization", e);
@@ -253,17 +200,174 @@ public class MapEntries implements Event
         }
     }
 
-    public boolean isOptimizeAliasResolutionEnabled(){
-        return this.enableOptimizeAliasResolution;
+    private void doAddAttributes(String path, String[] addedAttributes) {
+        this.initializing.lock();
+        try {
+            boolean configurationUpdate = false;
+            for (String changedAttribute:addedAttributes){
+                if ("sling:vanityPath".equals(changedAttribute)) {
+                    doAddVanity(path); 
+                } else if ("sling:vanityOrder".equals(changedAttribute)) {
+                    doUpdateVanityOrder(path, false);
+                } else if ("sling:alias".equals(changedAttribute)) {
+                    doAddAlias(path);
+                } else {
+                    configurationUpdate = true;
+                }
+            }
+            if (configurationUpdate) {
+                doUpdateConfiguration();
+            }
+            sendChangeEvent();
+        } finally {
+            this.initializing.unlock();
+        }
+    }
+
+    private void doUpdateAttributes(String path, String[] changedAttributes) {
+        this.initializing.lock();
+        try {
+            boolean configurationUpdate = false;
+            for (String changedAttribute:changedAttributes){
+                if ("sling:vanityPath".equals(changedAttribute)) {
+                    doUpdateVanity(path);
+                } else if ("sling:vanityOrder".equals(changedAttribute)) {
+                    doUpdateVanityOrder(path, false);
+                } else if ("sling:alias".equals(changedAttribute)) {
+                    doUpdateAlias(path);
+                } else {
+                    configurationUpdate = true;
+                }
+            }
+            if (configurationUpdate) {
+                doUpdateConfiguration();
+            }
+            sendChangeEvent();
+        } finally {
+            this.initializing.unlock();
+        }
+    }
+
+    private void doRemoveAttributes(String path, String[] removedAttributes, boolean nodeDeletion) {
+        this.initializing.lock();
+        try {
+            boolean configurationUpdate = false;
+            for (String changedAttribute:removedAttributes){
+                if ("sling:vanityPath".equals(changedAttribute)){
+                    doRemoveVanity(path);
+                } else if ("sling:vanityOrder".equals(changedAttribute)) {
+                    doUpdateVanityOrder(path, true);
+                } else if ("sling:alias".equals(changedAttribute)) {
+                    doRemoveAlias(path, nodeDeletion);
+                } else {
+                    configurationUpdate = true;
+                }
+            }
+            if (configurationUpdate) {
+                doUpdateConfiguration();
+            }
+            sendChangeEvent();
+        } finally {
+            this.initializing.unlock();
+        }
+    }
+
+    private void doUpdateConfiguration(){
+        final List<MapEntry> globalResolveMap = new ArrayList<MapEntry>();
+        final SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
+        // load the /etc/map entries into the maps
+        loadResolverMap(resolver, globalResolveMap, newMapMaps);
+        // load the configuration into the resolver map
+        loadConfiguration(factory, globalResolveMap);
+        // load the configuration into the mapper map
+        loadMapConfiguration(factory, newMapMaps);
+        // sort global list and add to map
+        Collections.sort(globalResolveMap);
+        resolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
+        this.mapMaps = Collections.unmodifiableSet(new TreeSet<MapEntry>(newMapMaps.values()));
+    }
+
+    private void doAddVanity(String path) {
+        Resource resource = resolver.getResource(path);
+        loadVanityPath(resource, resolveMapsMap, vanityTargets);
+    }
+
+    private void doUpdateVanity(String path) {
+         doRemoveVanity(path);
+         doAddVanity(path);
+    }
+
+    private void doRemoveVanity(String path) {
+        String actualContentPath = getActualContentPath(path);
+        List <String> l = vanityTargets.get(actualContentPath);
+        if (l != null){
+            for (String s : l){
+                List<MapEntry> entries = this.resolveMapsMap.get(s);
+                for (Iterator<MapEntry> iterator =entries.iterator(); iterator.hasNext(); ) {
+                    MapEntry entry = iterator.next();
+                    String redirect = getMapEntryRedirect(entry);
+                    if (redirect != null && redirect.equals(path)) {
+                        iterator.remove();
+                    }
+                }
+            }
+        }
+        vanityTargets.remove(actualContentPath);
+    }
+
+    private void doUpdateVanityOrder(String path, boolean deletion) {
+        Resource resource = resolver.getResource(path);
+        final ValueMap props = resource.adaptTo(ValueMap.class);
+
+        long vanityOrder;
+        if (deletion) {
+            vanityOrder = 0;
+        } else {
+            vanityOrder = props.get("sling:vanityOrder", Long.class);
+        }
+
+        String actualContentPath = getActualContentPath(path);
+        List<String> vanityPaths = vanityTargets.get(actualContentPath);
+        boolean updatedOrder = false;
+        for (String vanityTarget : vanityPaths) {
+            List<MapEntry> entries = this.resolveMapsMap.get(vanityTarget);
+            for (MapEntry entry : entries) {
+                String redirect = getMapEntryRedirect(entry);
+                if (redirect != null && redirect.equals(actualContentPath)) {
+                    entry.setOrder(vanityOrder);
+                    updatedOrder = true;
+                }
+            }
+            if (updatedOrder) {
+                Collections.sort(entries);
+            }
+        }
     }
 
+    private void doAddAlias(String path) {
+        Resource resource = resolver.getResource(path);
+        loadAlias(resource, this.aliasMap);
+    }
+
+    private void doUpdateAlias(String path) {
+        doRemoveAlias(path, false);
+        doAddAlias(path);
+    }
 
-    private <K1, K2, V> Map<K1, Map<K2, V>> makeUnmodifiableMap(final Map<K1, Map<K2, V>> map) {
-        final Map<K1, Map<K2, V>> newMap = new HashMap<K1, Map<K2, V>>();
-        for (final K1 key : map.keySet()) {
-            newMap.put(key, Collections.unmodifiableMap(map.get(key)));
+    private void doRemoveAlias(String path, boolean nodeDeletion) {
+        if (nodeDeletion) { 
+            if (!"/".equals(path)){
+                path = path.substring(0, path.lastIndexOf("/"));
+            }
+        } else {
+            Resource resource = resolver.getResource(path); 
+            path = resource.getParent().getPath();
         }
-        return Collections.unmodifiableMap(newMap);
+        this.aliasMap.remove(path); 
+    }
+
+    public boolean isOptimizeAliasResolutionEnabled() {
+        return this.enableOptimizeAliasResolution;
     }
 
     /**
@@ -303,9 +407,6 @@ public class MapEntries implements Event
             final ResourceResolver oldResolver = this.resolver;
             this.resolver = null;
 
-            // trigger initialization to terminate init thread
-            triggerInit();
-
             if (oldResolver != null) {
                 oldResolver.close();
             } else {
@@ -331,7 +432,7 @@ public class MapEntries implements Event
             entries.addAll(list);
         }
         Collections.sort(entries);
-        return entries;
+        return Collections.unmodifiableList(entries);
     }
 
     /**
@@ -382,38 +483,78 @@ public class MapEntries implements Event
             return;
         }
 
-        // check whether a remove event has an influence on vanity paths
-        boolean doInit = true;
-        if (SlingConstants.TOPIC_RESOURCE_REMOVED.equals(event.getTopic()) && !path.startsWith(this.mapRoot)) {
-            final String checkPath;
-            if ( path.endsWith("/jcr:content") ) {
-                checkPath = path.substring(0, path.length() - 12);
-            } else {
-                checkPath = path;
-            }
-            doInit = false;
-            for (final String target : this.vanityTargets) {
-                if (target.startsWith(checkPath)) {
-                    doInit = true;
-                    break;
+        //removal of a node is handled differently
+        if (SlingConstants.TOPIC_RESOURCE_REMOVED.equals(event.getTopic())) {
+            final String actualContentPath = getActualContentPath(path);
+            for (final String target : this.vanityTargets.keySet()) {
+                if (target.startsWith(actualContentPath)) {
+                    doRemoveAttributes(actualContentPath, new String [] {"sling:vanityPath"}, true);
                 }
             }
             for (final String target : this.aliasMap.keySet()) {
-                if (checkPath.startsWith(target)) { 
-                    doInit = true;
-                    break;
+                if (actualContentPath.startsWith(target)) {
+                    doRemoveAttributes(actualContentPath, new String [] {"sling:alias"}, true);
                 }
             }
-        }
+        } else {
+            String [] addedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_ADDED_ATTRIBUTES);    
+            if (addedAttributes != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("found added attributes {}", addedAttributes);
+                }
+                doAddAttributes(path, addedAttributes);
+            }
+
+            String [] changedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_CHANGED_ATTRIBUTES);        
+            if (changedAttributes != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("found changed attributes {}", changedAttributes);
+                }
+                doUpdateAttributes(path, changedAttributes);
+            }
 
-        // trigger an update
-        if (doInit) {
-            triggerInit();
+            String [] removedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_REMOVED_ATTRIBUTES);        
+            if (removedAttributes != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("found removed attributes {}", removedAttributes);
+                }
+                final String checkPath = getActualContentPath(path);
+                doRemoveAttributes(checkPath, removedAttributes, false);
+            } 
         }
     }
 
     // ---------- internal
 
+    //TODO add unit test
+    private String getActualContentPath(String path){
+        final String checkPath;
+        if ( path.endsWith("/jcr:content") ) {
+            checkPath = path.substring(0, path.length() - "/jcr:content".length());
+        } else {
+            checkPath = path;
+        }
+        return checkPath;
+    }
+
+    //TODO add unit test
+    private String getMapEntryRedirect(MapEntry mapEntry) {
+        String[] redirect = mapEntry.getRedirect();
+        if (redirect.length > 1) {
+            log.warn("something went wrong, please restart the bundle");
+            return null;
+        }
+
+        String path = redirect[0];
+        if (path.endsWith("$1")) {
+            path = path.substring(0, path.length() - "$1".length());
+        } else if (path.endsWith(".html")) {
+            path = path.substring(0, path.length() - ".html".length());
+        }
+
+        return path;
+    }
+
     /**
      * Send an OSGi event
      */
@@ -508,7 +649,7 @@ public class MapEntries implements Event
      * property
      */
     private Map<String, Map<String, String>> loadAliases(final ResourceResolver resolver) {
-        final Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>();
+        final Map<String, Map<String, String>> map = new ConcurrentHashMap<String, Map<String, String>>();
         final String queryString = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL";
         final Iterator<Resource> i = resolver.findResources(queryString, "sql");
         while (i.hasNext()) {
@@ -579,23 +720,21 @@ public class MapEntries implements Event
             }
         }
     }
-    
+
     /**
      * Load vanity paths Search for all nodes inheriting the sling:VanityPath
      * mixin
      */
-    private Collection<String> loadVanityPaths(final ResourceResolver resolver, final Map<String, List<MapEntry>> entryMap) {
+    private Map <String, List<String>> loadVanityPaths(final ResourceResolver resolver, final Map<String, List<MapEntry>> entryMap) {
         // sling:VanityPath (uppercase V) is the mixin name
         // sling:vanityPath (lowercase) is the property name
-        final Set<String> targetPaths = new HashSet<String>();
+        final Map <String, List<String>> targetPaths = new ConcurrentHashMap <String, List<String>>();
         final String queryString = "SELECT sling:vanityPath, sling:redirect, sling:redirectStatus FROM sling:VanityPath WHERE sling:vanityPath IS NOT NULL ORDER BY sling:vanityOrder DESC";
         final Iterator<Resource> i = resolver.findResources(queryString, "sql");
 
-        final Set<String> processedVanityPaths = new HashSet<String>();
-
         while (i.hasNext()) {
             final Resource resource = i.next();
-            loadVanityPath(resource, entryMap, processedVanityPaths, targetPaths);
+            loadVanityPath(resource, entryMap, targetPaths);
         }
 
         return targetPaths;
@@ -604,14 +743,14 @@ public class MapEntries implements Event
     /**
      * Load vanity path given a resource
      */
-    private void loadVanityPath(final Resource resource, final Map<String, List<MapEntry>> entryMap, final Set<String> processedVanityPaths, final Set<String> targetPaths) {
+    private void loadVanityPath(final Resource resource, final Map<String, List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths) {
         // ignore system tree
         if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
             log.debug("loadVanityPaths: Ignoring {}", resource);
             return;
         }
 
-        // check whitelist
+        // check white list
         if ( this.vanityPathConfig != null ) {
             boolean allowed = false;
             for(final VanityPathConfig config : this.vanityPathConfig) {
@@ -632,6 +771,11 @@ public class MapEntries implements Event
             return;
         }
 
+        long vanityOrder = 0;
+        if (props.containsKey("sling:vanityOrder")) {
+            vanityOrder = props.get("sling:vanityOrder", Long.class);
+        }   
+
         // url is ignoring scheme and host.port and the path is
         // what is stored in the sling:vanityPath property
         final String[] pVanityPaths = props.get("sling:vanityPath", new String[0]);
@@ -639,54 +783,62 @@ public class MapEntries implements Event
             final String[] result = this.getVanityPathDefinition(pVanityPath);
             if (result != null) {
                 final String url = result[0] + result[1];
+                // redirect target is the node providing the
+                // sling:vanityPath
+                // property (or its parent if the node is called
+                // jcr:content)
+                final Resource redirectTarget;
+                if (resource.getName().equals("jcr:content")) {
+                    redirectTarget = resource.getParent();
+                } else {
+                    redirectTarget = resource;
+                }
+                final String redirect = redirectTarget.getPath();
+                final String redirectName = redirectTarget.getName();
 
-                if ( !processedVanityPaths.contains(url) ) {
-                    processedVanityPaths.add(url);
-                    // redirect target is the node providing the
-                    // sling:vanityPath
-                    // property (or its parent if the node is called
-                    // jcr:content)
-                    final Resource redirectTarget;
-                    if (resource.getName().equals("jcr:content")) {
-                        redirectTarget = resource.getParent();
-                    } else {
-                        redirectTarget = resource;
-                    }
-                    final String redirect = redirectTarget.getPath();
-                    final String redirectName = redirectTarget.getName();
+                // whether the target is attained by a external redirect or
+                // by an internal redirect is defined by the sling:redirect
+                // property
+                final int status = props.get("sling:redirect", false) ? props.get(
+                        PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, factory.getDefaultVanityPathRedirectStatus())
+                        : -1;
+
+                final String checkPath = result[1];
+
+                if (redirectName.indexOf('.') > -1) {
+                    // 1. entry with exact match
+                    this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect));
+
+                    final int idx = redirectName.lastIndexOf('.');
+                    final String extension = redirectName.substring(idx + 1);
 
-                    // whether the target is attained by a external redirect or
-                    // by an internal redirect is defined by the sling:redirect
-                    // property
-                    final int status = props.get("sling:redirect", false) ? props.get(
-                            PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, factory.getDefaultVanityPathRedirectStatus())
-                            : -1;
-
-                            final String checkPath = result[1];
-
-                            if (redirectName.indexOf('.') > -1) {
-                                // 1. entry with exact match
-                                this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, redirect));
-
-                                final int idx = redirectName.lastIndexOf('.');
-                                final String extension = redirectName.substring(idx + 1);
-
-                                // 2. entry with extension
-                                this.addEntry(entryMap, checkPath, getMapEntry(url + "\\." + extension, status, false, redirect));
-                            } else {
-                                // 1. entry with exact match
-                                this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, redirect + ".html"));
-
-                                // 2. entry with match supporting selectors and extension
-                                this.addEntry(entryMap, checkPath, getMapEntry(url + "(\\..*)", status, false, redirect + "$1"));
-                            }
-                            // 3. keep the path to return
-                            targetPaths.add(redirect);
+                    // 2. entry with extension
+                    this.addEntry(entryMap, checkPath, getMapEntry(url + "\\." + extension, status, false, vanityOrder, redirect));
+                } else {
+                    // 1. entry with exact match
+                    this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect + ".html"));
+
+                    // 2. entry with match supporting selectors and extension
+                    this.addEntry(entryMap, checkPath, getMapEntry(url + "(\\..*)", status, false, vanityOrder, redirect + "$1"));
                 }
+                // 3. keep the path to return
+                this.updateTargetPaths(targetPaths, redirect, checkPath);
             }
         }
     }
     
+    private void updateTargetPaths(final Map<String, List<String>> targetPaths, final String key, final String entry) {
+        if (entry==null){
+            return;
+        }
+        List<String> entries = targetPaths.get(key);
+        if (entries == null) {
+            entries = new ArrayList<String>();
+            targetPaths.put(key, entries);
+        }
+        entries.add(entry); 
+    }
+    
     /**
      * Create the vanity path definition. String array containing:
      * {protocol}/{host}[.port] {absolute path}
@@ -969,7 +1121,20 @@ public class MapEntries implements Event
 
         MapEntry mapEntry = null;
         try{
-            mapEntry = new MapEntry(url, status, trailingSlash, redirect);
+            mapEntry = new MapEntry(url, status, trailingSlash, 0, redirect);
+        }catch (IllegalArgumentException iae){
+            //ignore this entry
+            log.debug("ignored entry due exception ",iae);
+        }
+        return mapEntry;
+    }
+    
+    private MapEntry getMapEntry(String url, final int status, final boolean trailingSlash, long order,
+            final String... redirect){
+
+        MapEntry mapEntry = null;
+        try{
+            mapEntry = new MapEntry(url, status, trailingSlash, order, redirect);
         }catch (IllegalArgumentException iae){
             //ignore this entry
             log.debug("ignored entry due exception ",iae);

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java?rev=1605423&r1=1605422&r2=1605423&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntry.java Wed Jun 25 14:32:53 2014
@@ -63,6 +63,8 @@ public class MapEntry implements Compara
     private final String[] redirect;
 
     private final int status;
+    
+    private long order;
 
     public static String appendSlash(String path) {
         if (!path.endsWith("/")) {
@@ -143,14 +145,14 @@ public class MapEntry implements Compara
                 final int status = props
                                 .get(MapEntries.PROP_REDIRECT_EXTERNAL_STATUS,
                                                 302);
-                return new MapEntry(url, status, trailingSlash, redirect);
+                return new MapEntry(url, status, trailingSlash, 0, redirect);
             }
 
             final String[] internalRedirectProps = props.get(ResourceResolverImpl.PROP_REDIRECT_INTERNAL,
                 String[].class);
             final String[] internalRedirect = filterRegExp(internalRedirectProps);
             if (internalRedirect != null) {
-                return new MapEntry(url, -1, trailingSlash, internalRedirect);
+                return new MapEntry(url, -1, trailingSlash, 0, internalRedirect);
             }
         }
 
@@ -213,7 +215,7 @@ public class MapEntry implements Compara
                     if (!redir.contains("$")) {
                     	MapEntry mapEntry = null;
                     	try{
-                    		mapEntry = new MapEntry(redir.concat(endHook), status, trailingSlash, url);
+                    		mapEntry = new MapEntry(redir.concat(endHook), status, trailingSlash, 0, url);
                     	}catch (IllegalArgumentException iae){
                     		//ignore this entry
                             LoggerFactory
@@ -236,7 +238,7 @@ public class MapEntry implements Compara
     }
 
     public MapEntry(String url, final int status, final boolean trailingSlash,
-                    final String... redirect) {
+                    final long order, final String... redirect) {
 
         // ensure trailing slashes on redirects if the url
         // ends with a trailing slash
@@ -260,6 +262,7 @@ public class MapEntry implements Compara
 
         this.redirect = redirect;
         this.status = status;
+        this.order = order;
     }
 
     // Returns the replacement or null if the value does not match
@@ -317,7 +320,15 @@ public class MapEntry implements Compara
 
         // lengths are equal, but the entries are not
         // so order based on the pattern
-        return ownPatternString.toString().compareTo(mPatternString);
+        int stringComparison = ownPatternString.toString().compareTo(mPatternString);
+        if (stringComparison == 0 && order != m.order) {
+            if (m.order > order) {
+                return 1;
+            } else {
+                return -1;
+            }
+        }
+        return stringComparison;
     }
 
     // ---------- Object overwrite
@@ -384,4 +395,8 @@ public class MapEntry implements Compara
 
         return list.isEmpty() ? null : (String[]) list.toArray(new String[list.size()]);
     }
+
+    void setOrder(long order) {
+        this.order = order;
+    }
 }

Added: sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/resourceresolver.css
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/resourceresolver.css?rev=1605423&view=auto
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/resourceresolver.css (added)
+++ sling/trunk/bundles/resourceresolver/src/main/resources/res/ui/resourceresolver.css Wed Jun 25 14:32:53 2014
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+ 
+tr.content.duplicate td.content {
+    color: grey;
+    text-decoration: line-through;
+}
\ No newline at end of file