You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2012/03/14 14:45:57 UTC

svn commit: r1300543 - in /sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal: JcrResourceResolver.java helper/MapEntries.java

Author: cziegeler
Date: Wed Mar 14 13:45:57 2012
New Revision: 1300543

URL: http://svn.apache.org/viewvc?rev=1300543&view=rev
Log:
SLING-2255 : Improve JcrResourceResolver#resolve performance when big number of vanityPath are present

Modified:
    sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/JcrResourceResolver.java
    sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java

Modified: sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/JcrResourceResolver.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/JcrResourceResolver.java?rev=1300543&r1=1300542&r2=1300543&view=diff
==============================================================================
--- sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/JcrResourceResolver.java (original)
+++ sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/JcrResourceResolver.java Wed Mar 14 13:45:57 2012
@@ -348,7 +348,7 @@ public class JcrResourceResolver
     /**
      * @see org.apache.sling.api.resource.ResourceResolver#resolve(javax.servlet.http.HttpServletRequest, java.lang.String)
      */
-    public Resource resolve(HttpServletRequest request, String absPath) {
+    public Resource resolve(final HttpServletRequest request, String absPath) {
         checkClosed();
 
         String workspaceName = null;
@@ -423,7 +423,10 @@ public class JcrResourceResolver
         for (int i = 0; i < 100; i++) {
 
             String[] mappedPath = null;
-            for (MapEntry mapEntry : this.factory.getMapEntries().getResolveMaps()) {
+
+            final Iterator<MapEntry> mapEntriesIterator = this.factory.getMapEntries().getResolveMapsIterator(requestPath);
+            while ( mapEntriesIterator.hasNext() ) {
+                final MapEntry mapEntry = mapEntriesIterator.next();
                 mappedPath = mapEntry.replace(requestPath);
                 if (mappedPath != null) {
                     if ( LOGGER.isDebugEnabled() ) {
@@ -464,7 +467,7 @@ public class JcrResourceResolver
             // otherwise the mapped path is an URI and we have to try to
             // resolve that URI now, using the URI's path as the real path
             try {
-                URI uri = new URI(mappedPath[0], false);
+                final URI uri = new URI(mappedPath[0], false);
                 requestPath = getMapPath(uri.getScheme(), uri.getHost(),
                     uri.getPort(), uri.getPath());
                 realPathList = new String[] { uri.getPath() };
@@ -472,7 +475,7 @@ public class JcrResourceResolver
                 LOGGER.debug(
                     "resolve: Mapped path is an URL, using new request path {}",
                     requestPath);
-            } catch (URIException use) {
+            } catch (final URIException use) {
                 // TODO: log and fail
                 throw new ResourceNotFoundException(absPath);
             }
@@ -541,7 +544,7 @@ public class JcrResourceResolver
      * calls map(HttpServletRequest, String) as map(null, resourcePath)
      * @see org.apache.sling.api.resource.ResourceResolver#map(java.lang.String)
      */
-    public String map(String resourcePath) {
+    public String map(final String resourcePath) {
         checkClosed();
         return map(null, resourcePath);
     }

Modified: sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java?rev=1300543&r1=1300542&r2=1300543&view=diff
==============================================================================
--- sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java (original)
+++ sling/trunk/bundles/jcr/resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java Wed Mar 14 13:45:57 2012
@@ -31,6 +31,8 @@ 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;
@@ -61,7 +63,10 @@ import org.slf4j.LoggerFactory;
 
 public class MapEntries implements EventHandler {
 
-    public static MapEntries EMPTY = new MapEntries();
+    public static final MapEntries EMPTY = new MapEntries();
+
+    /** Key for the global list. */
+    private static final String GLOBAL_LIST_KEY = "*";
 
     public static final String DEFAULT_MAP_ROOT = "/etc/map";
 
@@ -78,7 +83,7 @@ public class MapEntries implements Event
 
     private final String mapRoot;
 
-    private List<MapEntry> resolveMaps;
+    private Map<String, List<MapEntry>> resolveMapsMap;
 
     private Collection<MapEntry> mapMaps;
 
@@ -97,7 +102,7 @@ public class MapEntries implements Event
         this.resolver = null;
         this.mapRoot = DEFAULT_MAP_ROOT;
 
-        this.resolveMaps = Collections.<MapEntry> emptyList();
+        this.resolveMapsMap = Collections.emptyMap();
         this.mapMaps = Collections.<MapEntry> emptyList();
         this.vanityTargets = Collections.<String> emptySet();
         this.registration = null;
@@ -113,7 +118,7 @@ public class MapEntries implements Event
         this.mapRoot = factory.getMapRoot();
         this.eventAdminTracker = eventAdminTracker;
 
-        this.resolveMaps = Collections.<MapEntry> emptyList();
+        this.resolveMapsMap = Collections.emptyMap();
         this.mapMaps = Collections.<MapEntry> emptyList();
         this.vanityTargets = Collections.<String> emptySet();
 
@@ -151,11 +156,12 @@ public class MapEntries implements Event
         props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
         this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props);
 
-        Thread updateThread = new Thread(new Runnable() {
+        final Thread updateThread = new Thread(new Runnable() {
             public void run() {
                 MapEntries.this.init();
             }
         }, "MapEntries Update");
+        updateThread.setDaemon(true);
         updateThread.start();
     }
 
@@ -180,7 +186,7 @@ public class MapEntries implements Event
             try {
                 this.initTrigger.acquire();
                 this.doInit();
-            } catch (InterruptedException ie) {
+            } catch (final InterruptedException ie) {
                 // just continue acquisition
             }
         }
@@ -197,33 +203,36 @@ public class MapEntries implements Event
         this.initializing.lock();
         try {
             final ResourceResolver resolver = this.resolver;
-            if (resolver == null) {
+            final JcrResourceResolverFactoryImpl factory = this.factory;
+            if (resolver == null || factory == null) {
                 return;
             }
 
-            List<MapEntry> newResolveMaps = new ArrayList<MapEntry>();
-            SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
+            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, newResolveMaps, newMapMaps);
+            loadResolverMap(resolver, globalResolveMap, newMapMaps);
 
             // load the configuration into the resolver map
-            Collection<String> vanityTargets = loadVanityPaths(resolver, newResolveMaps);
-            loadConfiguration(factory, newResolveMaps);
+            final Collection<String> vanityTargets = this.loadVanityPaths(resolver, newResolveMapsMap);
+            loadConfiguration(factory, globalResolveMap);
 
             // load the configuration into the mapper map
             loadMapConfiguration(factory, newMapMaps);
 
-            // sort List
-            Collections.sort(newResolveMaps);
+            // sort global list and add to map
+            Collections.sort(globalResolveMap);
+            newResolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
 
             this.vanityTargets = Collections.unmodifiableCollection(vanityTargets);
-            this.resolveMaps = Collections.unmodifiableList(newResolveMaps);
+            this.resolveMapsMap = Collections.unmodifiableMap(newResolveMapsMap);
             this.mapMaps = Collections.unmodifiableSet(new TreeSet<MapEntry>(newMapMaps.values()));
 
             sendChangeEvent();
 
-        } catch (Exception e) {
+        } catch (final Exception e) {
 
             log.warn("doInit: Unexpected problem during initialization", e);
 
@@ -257,7 +266,7 @@ public class MapEntries implements Event
         boolean initLocked;
         try {
             initLocked = this.initializing.tryLock(10, TimeUnit.SECONDS);
-        } catch (InterruptedException ie) {
+        } catch (final InterruptedException ie) {
             initLocked = false;
         }
 
@@ -291,8 +300,33 @@ public class MapEntries implements Event
         this.eventAdminTracker = null;
     }
 
+    /**
+     * This is for the web console plugin
+     */
     public List<MapEntry> getResolveMaps() {
-        return resolveMaps;
+        final List<MapEntry> entries = new ArrayList<MapEntry>();
+        for(final List<MapEntry> list : this.resolveMapsMap.values()) {
+            entries.addAll(list);
+        }
+        Collections.sort(entries);
+        return entries;
+    }
+
+    /**
+     * Calculate the resolve maps.
+     * As the entries have to be sorted by pattern length,
+     * we have to create a new list containing all
+     * relevant entries.
+     */
+    public Iterator<MapEntry> getResolveMapsIterator(final String requestPath) {
+        String key = null;
+        final int firstIndex = requestPath.indexOf('/');
+        final int secondIndex = requestPath.indexOf('/', firstIndex + 1);
+        if ( secondIndex != -1 ) {
+            key = requestPath.substring(secondIndex);
+        }
+
+        return new MapEntryIterator(key, resolveMapsMap);
     }
 
     public Collection<MapEntry> getMapMaps() {
@@ -361,17 +395,17 @@ public class MapEntries implements Event
     }
 
     private void loadResolverMap(final ResourceResolver resolver,
-            Collection<MapEntry> resolveEntries,
+            List<MapEntry> entries,
             Map<String, MapEntry> mapEntries) {
         // the standard map configuration
         Resource res = resolver.getResource(mapRoot);
         if (res != null) {
-            gather(resolver, resolveEntries, mapEntries, res, "");
+            gather(resolver, entries, mapEntries, res, "");
         }
     }
 
     private void gather(final ResourceResolver resolver,
-            Collection<MapEntry> resolveEntries,
+            List<MapEntry> entries,
             Map<String, MapEntry> mapEntries, Resource parent, String parentPath) {
         // scheme list
         Iterator<Resource> children = ResourceUtil.listChildren(parent);
@@ -397,14 +431,14 @@ public class MapEntries implements Event
                     childParent = childParent.concat("/");
                 }
 
-                gather(resolver, resolveEntries, mapEntries, child, childParent);
+                gather(resolver, entries, mapEntries, child, childParent);
             }
 
             // add resolution entries for this node
-            MapEntry childResolveEntry = MapEntry.createResolveEntry(childPath,
+            final MapEntry childResolveEntry = MapEntry.createResolveEntry(childPath,
                 child, trailingSlash);
             if (childResolveEntry != null) {
-                resolveEntries.add(childResolveEntry);
+                entries.add(childResolveEntry);
             }
 
             // add map entries for this node
@@ -420,16 +454,35 @@ public class MapEntries implements Event
         }
     }
 
+    /**
+     * Add an entry to the resolve map.
+     */
+    private void addEntry(final Map<String, List<MapEntry>> entryMap,
+            final String key, final MapEntry entry) {
+        List<MapEntry> entries = entryMap.get(key);
+        if ( entries == null ) {
+            entries = new ArrayList<MapEntry>();
+            entryMap.put(key, entries);
+        }
+        entries.add(entry);
+        // and finally sort list
+        Collections.sort(entries);
+    }
+
+    /**
+     * Load vanity paths
+     * Search for all nodes inheriting the sling:VanityPath mixin
+     */
     private Collection<String> loadVanityPaths(final ResourceResolver resolver,
-            List<MapEntry> entries) {
+            final Map<String, List<MapEntry>> entryMap) {
         // sling:VanityPath (uppercase V) is the mixin name
         // sling:vanityPath (lowercase) is the property name
-        final HashSet<String> targetPaths = new HashSet<String>();
+        final Set<String> targetPaths = new HashSet<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 Iterator<Resource> i = resolver.findResources(queryString, "sql");
+
         while (i.hasNext()) {
-            Resource resource = i.next();
+            final Resource resource = i.next();
 
             // ignore system tree
             if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
@@ -438,38 +491,43 @@ public class MapEntries implements Event
             }
 
             // require properties
-            ValueMap row = resource.adaptTo(ValueMap.class);
-            if (row == null) {
+            final ValueMap props = resource.adaptTo(ValueMap.class);
+            if (props == null) {
                 log.debug("loadVanityPaths: Ignoring {} without properties", resource);
                 continue;
             }
 
             // url is ignoring scheme and host.port and the path is
             // what is stored in the sling:vanityPath property
-            String[] pVanityPaths = row.get("sling:vanityPath", new String[0]);
-            for (String pVanityPath : pVanityPaths) {
-                final String url = getVanityPath(pVanityPath);
-                if ( url != null ) {
+            final String[] pVanityPaths = props.get("sling:vanityPath", new String[0]);
+            for (final String pVanityPath : pVanityPaths) {
+                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)
-                    String redirect = resource.getPath();
-                    if (ResourceUtil.getName(redirect).equals("jcr:content")) {
-                        redirect = ResourceUtil.getParent(redirect);
+                    final String redirect;
+                    if (resource.getName().equals("jcr:content")) {
+                        redirect = resource.getParent().getPath();
+                    } else {
+                        redirect = resource.getPath();
                     }
 
                     // whether the target is attained by a 302/FOUND or by an
                     // internal redirect is defined by the sling:redirect property
-                    int status = row.get("sling:redirect", false)
-                            ? row.get(JcrResourceResolver.PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, HttpServletResponse.SC_FOUND)
+                    final int status = props.get("sling:redirect", false)
+                            ? props.get(JcrResourceResolver.PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, HttpServletResponse.SC_FOUND)
                             : -1;
 
+                    final String checkPath = result[1];
                     // 1. entry with exact match
-                    entries.add(new MapEntry(url + "$", status, false, redirect
-                        + ".html"));
+                    this.addEntry(entryMap, checkPath, new MapEntry(url + "$", status, false, redirect
+                            + ".html"));
 
                     // 2. entry with match supporting selectors and extension
-                    entries.add(new MapEntry(url + "(\\..*)", status, false,
-                        redirect + "$1"));
+                    this.addEntry(entryMap, checkPath, new MapEntry(url + "(\\..*)", status, false,
+                            redirect + "$1"));
 
                     // 3. keep the path to return
                     targetPaths.add(redirect);
@@ -479,61 +537,70 @@ public class MapEntries implements Event
         return targetPaths;
     }
 
-    private String getVanityPath(final String pVanityPath) {
-        String result = null;
+    /**
+     * Create the vanity path definition. String array containing:
+     * {protocol}/{host}[.port]
+     * {absolute path}
+     */
+    private String[] getVanityPathDefinition(final String pVanityPath) {
+        String[] result = null;
         if ( pVanityPath != null ) {
-            String path = pVanityPath.trim();
-            if ( path.length() > 0 ) {
+            final String info = pVanityPath.trim();
+            if ( info.length() > 0 ) {
+                String prefix = null;
+                String path = null;
                 // check for url
-                if ( path.indexOf(":/") > - 1 ) {
+                if ( info.indexOf(":/") > - 1 ) {
                     try {
-                        final URL u = new URL(path);
-                        path = u.getProtocol() + '/' + u.getHost() + '.' + u.getPort() + u.getPath();
-                    } catch (MalformedURLException e) {
+                        final URL u = new URL(info);
+                        prefix = u.getProtocol() + '/' + u.getHost() + '.' + u.getPort();
+                        path = u.getPath();
+                    } catch (final MalformedURLException e) {
                         log.warn("Ignoring malformed vanity path {}", pVanityPath);
-                        path = null;
                     }
                 } else {
-                    if ( !path.startsWith("/") ) {
-                        path = "/" + path;
+                    prefix = "^" + ANY_SCHEME_HOST;
+                    if ( !info.startsWith("/") ) {
+                        path = "/" + info;
+                    } else {
+                        path = info;
                     }
-                    path = "^" + ANY_SCHEME_HOST + path;
                 }
 
                 // remove extension
-                if ( path != null ) {
+                if ( prefix != null ) {
                     final int lastSlash = path.lastIndexOf('/');
                     final int firstDot = path.indexOf('.', lastSlash + 1);
                     if ( firstDot != -1 ) {
                         path = path.substring(0, firstDot);
                         log.warn("Removing extension from vanity path {}", pVanityPath);
                     }
-                    result = path;
+                    result = new String[] {prefix, path};
                 }
             }
         }
         return result;
     }
 
-    private void loadConfiguration(JcrResourceResolverFactoryImpl factory,
-            List<MapEntry> entries) {
+    private void loadConfiguration(final JcrResourceResolverFactoryImpl factory,
+            final List<MapEntry> entries) {
         // virtual uris
-        Map<?, ?> virtuals = factory.getVirtualURLMap();
+        final Map<?, ?> virtuals = factory.getVirtualURLMap();
         if (virtuals != null) {
-            for (Entry<?, ?> virtualEntry : virtuals.entrySet()) {
-                String extPath = (String) virtualEntry.getKey();
-                String intPath = (String) virtualEntry.getValue();
+            for (final Entry<?, ?> virtualEntry : virtuals.entrySet()) {
+                final String extPath = (String) virtualEntry.getKey();
+                final String intPath = (String) virtualEntry.getValue();
                 if (!extPath.equals(intPath)) {
                     // this regular expression must match the whole URL !!
-                    String url = "^" + ANY_SCHEME_HOST + extPath + "$";
-                    String redirect = intPath;
+                    final String url = "^" + ANY_SCHEME_HOST + extPath + "$";
+                    final String redirect = intPath;
                     entries.add(new MapEntry(url, -1, false, redirect));
                 }
             }
         }
 
         // URL Mappings
-        Mapping[] mappings = factory.getMappings();
+        final Mapping[] mappings = factory.getMappings();
         if (mappings != null) {
             Map<String, List<String>> map = new HashMap<String, List<String>>();
             for (Mapping mapping : mappings) {
@@ -550,9 +617,10 @@ public class MapEntries implements Event
                     }
                 }
             }
-            for (Entry<String, List<String>> entry : map.entrySet()) {
+
+            for (final Entry<String, List<String>> entry : map.entrySet()) {
                 entries.add(new MapEntry(ANY_SCHEME_HOST + entry.getKey(),
-                    -1, false, entry.getValue().toArray(new String[0])));
+                        -1, false, entry.getValue().toArray(new String[0])));
             }
         }
     }
@@ -605,4 +673,103 @@ public class MapEntries implements Event
         }
         entries.put(path, entry);
     }
+
+    private static final class MapEntryIterator implements Iterator<MapEntry> {
+
+        private final Map<String, List<MapEntry>> resolveMapsMap;
+
+        private String key;
+
+        private MapEntry next;
+
+        private Iterator<MapEntry> globalListIterator;
+        private MapEntry nextGlobal;
+
+        private Iterator<MapEntry> specialIterator;
+        private MapEntry nextSpecial;
+
+        public MapEntryIterator(final String startKey, final Map<String, List<MapEntry>> resolveMapsMap) {
+            this.key = startKey;
+            this.resolveMapsMap = resolveMapsMap;
+            this.globalListIterator = this.resolveMapsMap.get(GLOBAL_LIST_KEY).iterator();
+            this.seek();
+        }
+
+
+        /**
+         * @see java.util.Iterator#hasNext()
+         */
+        public boolean hasNext() {
+            return this.next != null;
+        }
+
+        /**
+         * @see java.util.Iterator#next()
+         */
+        public MapEntry next() {
+            if ( this.next == null ) {
+                throw new NoSuchElementException();
+            }
+            final MapEntry result = this.next;
+            this.seek();
+            return result;
+        }
+
+        /**
+         * @see java.util.Iterator#remove()
+         */
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        private void seek() {
+            if ( this.nextGlobal == null && this.globalListIterator.hasNext() ) {
+                this.nextGlobal = this.globalListIterator.next();
+            }
+            if ( this.nextSpecial == null ) {
+                if ( specialIterator != null && !specialIterator.hasNext() ) {
+                    specialIterator = null;
+                }
+                while ( specialIterator == null && key != null ) {
+                    // remove selectors and extension
+                    final int lastSlashPos = key.lastIndexOf('/');
+                    final int lastDotPos = key.indexOf('.', lastSlashPos);
+                    if ( lastDotPos != -1 ) {
+                        key = key.substring(0, lastDotPos);
+                    }
+                    final List<MapEntry> special = this.resolveMapsMap.get(key);
+                    if ( special != null ) {
+                        specialIterator = special.iterator();
+                    }
+                    // recurse to the parent
+                    if ( key.length() > 1 ) {
+                        final int lastSlash = key.lastIndexOf("/");
+                        if ( lastSlash == 0 ) {
+                            key = null;
+                        } else {
+                            key = key.substring(0, lastSlash);
+                        }
+                    } else {
+                        key = null;
+                    }
+                }
+                if ( this.specialIterator != null && this.specialIterator.hasNext() ) {
+                    this.nextSpecial = this.specialIterator.next();
+                }
+            }
+            if ( this.nextSpecial == null ) {
+                this.next = this.nextGlobal;
+                this.nextGlobal = null;
+            } else if ( this.nextGlobal == null ) {
+                this.next = this.nextSpecial;
+                this.nextSpecial = null;
+            } else if ( this.nextGlobal.getPattern().length() >= this.nextSpecial.getPattern().length() ) {
+                this.next = this.nextGlobal;
+                this.nextGlobal = null;
+            } else {
+                this.next = this.nextSpecial;
+                this.nextSpecial = null;
+            }
+        }
+    };
 }