You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by vr...@apache.org on 2012/02/02 13:47:01 UTC

svn commit: r1239587 [4/9] - in /sling/whiteboard/resourceresolverfactory/jcr-resource: ./ src/ src/main/ src/main/appended-resources/ src/main/appended-resources/META-INF/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/a...

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntries.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,469 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolver;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventHandler;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MapEntries implements EventHandler {
+
+    public static MapEntries EMPTY = new MapEntries();
+
+    public static final String DEFAULT_MAP_ROOT = "/etc/map";
+
+    static final String ANY_SCHEME_HOST = "[^/]+/[^/]+";
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private JcrResourceResolverFactoryImpl factory;
+
+    private ResourceResolver resolver;
+
+    private final String mapRoot;
+
+    private final String mapRootPrefix;
+
+    private List<MapEntry> resolveMaps;
+
+    private Collection<MapEntry> mapMaps;
+
+    private boolean initializing = false;
+
+    private final ServiceRegistration registration;
+
+    private final ServiceTracker eventAdminTracker;
+
+    private MapEntries() {
+        factory = null;
+        resolver = null;
+        mapRoot = DEFAULT_MAP_ROOT;
+        mapRootPrefix = mapRoot + "/";
+
+        resolveMaps = Collections.<MapEntry> emptyList();
+        mapMaps = Collections.<MapEntry> emptyList();
+        this.registration = null;
+        this.eventAdminTracker = null;
+    }
+
+    public MapEntries(final JcrResourceResolverFactoryImpl factory,
+                      final BundleContext bundleContext,
+                      final ServiceTracker eventAdminTracker)
+    throws LoginException {
+        this.resolver = factory.getAdministrativeResourceResolver(null);
+        this.factory = factory;
+        this.mapRoot = factory.getMapRoot();
+        this.mapRootPrefix = this.mapRoot + "/";
+        this.eventAdminTracker = eventAdminTracker;
+
+        init();
+        final Dictionary<String, String> props = new Hashtable<String, String>();
+        props.put("event.topics","org/apache/sling/api/resource/*");
+        props.put("service.description","Map Entries Observation");
+        props.put("service.vendor","The Apache Software Foundation");
+        this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props);
+    }
+
+    private void init() {
+        synchronized (this) {
+            // no initialization if the session has already been reset
+            if (resolver == null) {
+                return;
+            }
+
+            // set the flag
+            initializing = true;
+        }
+
+        try {
+
+            List<MapEntry> newResolveMaps = new ArrayList<MapEntry>();
+            SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
+
+            // load the /etc/map entries into the maps
+            loadResolverMap(resolver, newResolveMaps, newMapMaps);
+
+            // load the configuration into the resolver map
+            loadVanityPaths(resolver, newResolveMaps);
+            loadConfiguration(factory, newResolveMaps);
+
+            // load the configuration into the mapper map
+            loadMapConfiguration(factory, newMapMaps);
+
+            // sort List
+            Collections.sort(newResolveMaps);
+
+            this.resolveMaps = newResolveMaps;
+            this.mapMaps = new TreeSet<MapEntry>(newMapMaps.values());
+
+            sendChangeEvent();
+
+        } finally {
+
+            // reset the flag and notify listeners
+            synchronized (this) {
+                initializing = false;
+                notifyAll();
+            }
+        }
+    }
+
+    public void dispose() {
+        final ResourceResolver oldResolver;
+
+        // wait at most 10 seconds for a notifcation during initialization
+        synchronized (this) {
+            if (initializing) {
+                try {
+                    wait(10L * 1000L);
+                } catch (InterruptedException ie) {
+                    // ignore
+                }
+            }
+
+            // immediately set the resolver field to null to indicate
+            // that we have been disposed (this also signals to the
+            // event handler to stop working
+            oldResolver = resolver;
+            resolver = null;
+        }
+        if ( this.registration != null ) {
+            this.registration.unregister();
+        }
+
+        if (oldResolver != null) {
+            oldResolver.close();
+        }
+
+        // clear the rest of the fields
+        factory = null;
+    }
+
+    public List<MapEntry> getResolveMaps() {
+        return resolveMaps;
+    }
+
+    public Collection<MapEntry> getMapMaps() {
+        return mapMaps;
+    }
+
+    // ---------- EventListener interface
+
+    public void handleEvent(final Event event) {
+        boolean handleEvent = false;
+        final String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH);
+        if ( this.resolver != null && path != null ) {
+            handleEvent = mapRoot.equals(path) || path.startsWith(mapRootPrefix);
+            if ( !handleEvent && !event.getTopic().equals(SlingConstants.TOPIC_RESOURCE_REMOVED) ) {
+                final Resource rsrc = this.resolver.getResource(path);
+                final ValueMap props = ResourceUtil.getValueMap(rsrc);
+                handleEvent = props.containsKey("sling:vanityPath")
+                              || props.containsKey("sling:vanityOrder")
+                              || props.containsKey("sling:redirect");
+            }
+        }
+        if (handleEvent) {
+            final Thread t = new Thread() {
+                public void run() {
+                    init();
+                }
+            };
+            t.start();
+        }
+    }
+
+    // ---------- internal
+
+    /**
+     * Send an OSGi event
+     */
+    private void sendChangeEvent() {
+        final EventAdmin ea = (EventAdmin) this.eventAdminTracker.getService();
+        if ( ea != null ) {
+            // we hard code the topic here and don't use SlingConstants.TOPIC_RESOURCE_RESOLVER_MAPPING_CHANGED
+            // to avoid requiring the latest API version for this bundle to work
+            final Event event = new Event("org/apache/sling/api/resource/ResourceResolverMapping/CHANGED", (Dictionary<?,?>)null);
+            ea.postEvent(event);
+        }
+    }
+
+    private void loadResolverMap(final ResourceResolver resolver,
+            Collection<MapEntry> resolveEntries,
+            Map<String, MapEntry> mapEntries) {
+        // the standard map configuration
+        Resource res = resolver.getResource(mapRoot);
+        if (res != null) {
+            gather(resolver, resolveEntries, mapEntries, res, "");
+        }
+    }
+
+    private void gather(final ResourceResolver resolver,
+            Collection<MapEntry> resolveEntries,
+            Map<String, MapEntry> mapEntries, Resource parent, String parentPath) {
+        // scheme list
+        Iterator<Resource> children = ResourceUtil.listChildren(parent);
+        while (children.hasNext()) {
+            final Resource child = children.next();
+            final ValueMap vm = ResourceUtil.getValueMap(child);
+
+            String name = vm.get(JcrResourceResolver.PROP_REG_EXP, String.class);
+            boolean trailingSlash = false;
+            if (name == null) {
+                name = ResourceUtil.getName(child).concat("/");
+                trailingSlash = true;
+            }
+
+            String childPath = parentPath.concat(name);
+
+            // gather the children of this entry (only if child is not end hooked)
+            if (!childPath.endsWith("$")) {
+
+                // add trailing slash to child path to append the child
+                String childParent = childPath;
+                if (!trailingSlash) {
+                    childParent = childParent.concat("/");
+                }
+
+                gather(resolver, resolveEntries, mapEntries, child, childParent);
+            }
+
+            // add resolution entries for this node
+            MapEntry childResolveEntry = MapEntry.createResolveEntry(childPath,
+                child, trailingSlash);
+            if (childResolveEntry != null) {
+                resolveEntries.add(childResolveEntry);
+            }
+
+            // add map entries for this node
+            List<MapEntry> childMapEntries = MapEntry.createMapEntry(childPath,
+                child, trailingSlash);
+            if (childMapEntries != null) {
+                for (MapEntry mapEntry : childMapEntries) {
+                    addMapEntry(mapEntries, mapEntry.getPattern(),
+                        mapEntry.getRedirect()[0], mapEntry.getStatus());
+                }
+            }
+
+        }
+    }
+
+    private void loadVanityPaths(final ResourceResolver resolver,
+            List<MapEntry> entries) {
+        // sling:VanityPath (uppercase V) is the mixin name
+        // sling:vanityPath (lowercase) is the property name
+        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");
+        while (i.hasNext()) {
+            Resource resource = i.next();
+            ValueMap row = resource.adaptTo(ValueMap.class);
+            if (row == null) {
+                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 ) {
+                    // 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);
+                    }
+
+                    // 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("sling:redirectStatus", HttpServletResponse.SC_FOUND)
+                            : -1;
+
+                    // 1. entry with exact match
+                    entries.add(new MapEntry(url + "$", status, false, redirect
+                        + ".html"));
+
+                    // 2. entry with match supporting selectors and extension
+                    entries.add(new MapEntry(url + "(\\..*)", status, false,
+                        redirect + "$1"));
+                }
+            }
+        }
+    }
+
+    private String getVanityPath(final String pVanityPath) {
+        String result = null;
+        if ( pVanityPath != null ) {
+            String path = pVanityPath.trim();
+            if ( path.length() > 0 ) {
+                // check for url
+                if ( path.indexOf(":/") > - 1 ) {
+                    try {
+                        final URL u = new URL(path);
+                        path = u.getProtocol() + '/' + u.getHost() + '.' + u.getPort() + u.getPath();
+                    } catch (MalformedURLException e) {
+                        log.warn("Ignoring malformed vanity path {}", pVanityPath);
+                        path = null;
+                    }
+                } else {
+                    if ( !path.startsWith("/") ) {
+                        path = "/" + path;
+                    }
+                    path = "^" + ANY_SCHEME_HOST + path;
+                }
+
+                // remove extension
+                if ( path != 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;
+                }
+            }
+        }
+        return result;
+    }
+
+    private void loadConfiguration(JcrResourceResolverFactoryImpl factory,
+            List<MapEntry> entries) {
+        // virtual uris
+        Map<?, ?> virtuals = factory.getVirtualURLMap();
+        if (virtuals != null) {
+            for (Entry<?, ?> virtualEntry : virtuals.entrySet()) {
+                String extPath = (String) virtualEntry.getKey();
+                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;
+                    entries.add(new MapEntry(url, -1, false, redirect));
+                }
+            }
+        }
+
+        // URL Mappings
+        Mapping[] mappings = factory.getMappings();
+        if (mappings != null) {
+            Map<String, List<String>> map = new HashMap<String, List<String>>();
+            for (Mapping mapping : mappings) {
+                if (mapping.mapsInbound()) {
+                    String url = mapping.getTo();
+                    String alias = mapping.getFrom();
+                    if (url.length() > 0) {
+                        List<String> aliasList = map.get(url);
+                        if (aliasList == null) {
+                            aliasList = new ArrayList<String>();
+                            map.put(url, aliasList);
+                        }
+                        aliasList.add(alias);
+                    }
+                }
+            }
+            for (Entry<String, List<String>> entry : map.entrySet()) {
+                entries.add(new MapEntry(ANY_SCHEME_HOST + entry.getKey(),
+                    -1, false, entry.getValue().toArray(new String[0])));
+            }
+        }
+    }
+
+    private void loadMapConfiguration(JcrResourceResolverFactoryImpl factory,
+            Map<String, MapEntry> entries) {
+        // URL Mappings
+        Mapping[] mappings = factory.getMappings();
+        if (mappings != null) {
+            for (int i = mappings.length - 1; i >= 0; i--) {
+                Mapping mapping = mappings[i];
+                if (mapping.mapsOutbound()) {
+                    String url = mapping.getTo();
+                    String alias = mapping.getFrom();
+                    if (!url.equals(alias)) {
+                        addMapEntry(entries, alias, url, -1);
+                    }
+                }
+            }
+        }
+
+        // virtual uris
+        Map<?, ?> virtuals = factory.getVirtualURLMap();
+        if (virtuals != null) {
+            for (Entry<?, ?> virtualEntry : virtuals.entrySet()) {
+                String extPath = (String) virtualEntry.getKey();
+                String intPath = (String) virtualEntry.getValue();
+                if (!extPath.equals(intPath)) {
+                    // this regular expression must match the whole URL !!
+                    String path = "^" + intPath + "$";
+                    String url = extPath;
+                    addMapEntry(entries, path, url, -1);
+                }
+            }
+        }
+    }
+
+    private void addMapEntry(Map<String, MapEntry> entries, String path,
+            String url, int status) {
+        MapEntry entry = entries.get(path);
+        if (entry == null) {
+            entry = new MapEntry(path, status, false, url);
+        } else {
+            String[] redir = entry.getRedirect();
+            String[] newRedir = new String[redir.length + 1];
+            System.arraycopy(redir, 0, newRedir, 0, redir.length);
+            newRedir[redir.length] = url;
+            entry = new MapEntry(entry.getPattern(), entry.getStatus(),
+                false, newRedir);
+        }
+        entries.put(path, entry);
+    }
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntry.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntry.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntry.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/MapEntry.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,298 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.internal.JcrResourceResolver;
+
+/**
+ * The <code>MapEntry</code> class represents a mapping entry in the mapping
+ * configuration tree at <code>/etc/map</code>.
+ * <p>
+ *
+ * @see "http://cwiki.apache.org/SLING/flexible-resource-resolution.html"
+ */
+public class MapEntry implements Comparable<MapEntry> {
+
+    private static final Pattern[] URL_WITH_PORT_MATCH = {
+        Pattern.compile("http/([^/]+)(\\.[^\\d/]+)(/.*)?$"),
+        Pattern.compile("https/([^/]+)(\\.[^\\d/]+)(/.*)?$") };
+
+    private static final String[] URL_WITH_PORT_REPLACEMENT = {
+        "http/$1$2.80$3", "https/$1$2.443$3" };
+
+    private static final Pattern[] PATH_TO_URL_MATCH = {
+        Pattern.compile("http/([^/]+)\\.80(/.*)?$"),
+        Pattern.compile("https/([^/]+)\\.443(/.*)?$"),
+        Pattern.compile("([^/]+)/([^/]+)\\.(\\d+)(/.*)?$"),
+        Pattern.compile("([^/]+)/([^/]+)(/.*)?$") };
+
+    private static final String[] PATH_TO_URL_REPLACEMENT = { "http://$1$2",
+        "https://$1$2", "$1://$2:$3$4", "$1://$2$3" };
+
+    private final Pattern urlPattern;
+
+    private final String[] redirect;
+
+    private final int status;
+
+    public static String appendSlash(String path) {
+        if (!path.endsWith("/")) {
+            path = path.concat("/");
+        }
+        return path;
+    }
+
+    /**
+     * Returns a string used for matching map entries against the given request
+     * or URI parts.
+     *
+     * @param scheme The URI scheme
+     * @param host The host name
+     * @param port The port number. If this is negative, the default value used
+     *            is 80 unless the scheme is "https" in which case the default
+     *            value is 443.
+     * @param path The (absolute) path
+     * @return The request path string {scheme}://{host}:{port}{path}.
+     */
+    public static String getURI(String scheme, String host, int port,
+            String path) {
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(scheme).append("://").append(host);
+        if (port > 0 && !(port == 80 && "http".equals(scheme))
+            && !(port == 443 && "https".equals(scheme))) {
+            sb.append(':').append(port);
+        }
+        sb.append(path);
+
+        return sb.toString();
+    }
+
+    public static String fixUriPath(String uriPath) {
+        for (int i = 0; i < URL_WITH_PORT_MATCH.length; i++) {
+            Matcher m = URL_WITH_PORT_MATCH[i].matcher(uriPath);
+            if (m.find()) {
+                return m.replaceAll(URL_WITH_PORT_REPLACEMENT[i]);
+            }
+        }
+
+        return uriPath;
+    }
+
+    public static URI toURI(String uriPath) {
+        for (int i = 0; i < PATH_TO_URL_MATCH.length; i++) {
+            Matcher m = PATH_TO_URL_MATCH[i].matcher(uriPath);
+            if (m.find()) {
+                String newUriPath = m.replaceAll(PATH_TO_URL_REPLACEMENT[i]);
+                try {
+                    return new URI(newUriPath);
+                } catch (URISyntaxException use) {
+                    // ignore, just don't return the uri as such
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static MapEntry createResolveEntry(String url, Resource resource,
+            boolean trailingSlash) {
+        ValueMap props = resource.adaptTo(ValueMap.class);
+        if (props != null) {
+
+            // ensure the url contains a port number (if possible)
+            url = fixUriPath(url);
+
+            String redirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_EXTERNAL, String.class);
+            if (redirect != null) {
+                int status = props.get(
+                    JcrResourceResolver.PROP_REDIRECT_EXTERNAL_STATUS, 302);
+                return new MapEntry(url, status, trailingSlash, redirect);
+            }
+
+            String[] internalRedirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_INTERNAL, String[].class);
+            if (internalRedirect != null) {
+                return new MapEntry(url, -1, trailingSlash, internalRedirect);
+            }
+        }
+
+        return null;
+    }
+
+    public static List<MapEntry> createMapEntry(String url, Resource resource,
+            boolean trailingSlash) {
+        ValueMap props = resource.adaptTo(ValueMap.class);
+        if (props != null) {
+            String redirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_EXTERNAL, String.class);
+            if (redirect != null) {
+                // ignoring external redirects for mapping
+                return null;
+            }
+
+            // check whether the url is a match hooked to then string end
+            String endHook = "";
+            if (url.endsWith("$")) {
+                endHook = "$";
+                url = url.substring(0, url.length()-1);
+            }
+
+            // check whether the url is for ANY_SCHEME_HOST
+            if (url.startsWith(MapEntries.ANY_SCHEME_HOST)) {
+                url = url.substring(MapEntries.ANY_SCHEME_HOST.length());
+            }
+
+            String[] internalRedirect = props.get(
+                JcrResourceResolver.PROP_REDIRECT_INTERNAL, String[].class);
+            if (internalRedirect != null) {
+
+                int status = -1;
+                URI extPathPrefix = toURI(url);
+                if (extPathPrefix != null) {
+                    url = getURI(extPathPrefix.getScheme(),
+                        extPathPrefix.getHost(), extPathPrefix.getPort(),
+                        extPathPrefix.getPath());
+                    status = 302;
+                }
+
+                List<MapEntry> prepEntries = new ArrayList<MapEntry>(
+                    internalRedirect.length);
+                for (String redir : internalRedirect) {
+                    if (!redir.contains("$")) {
+                        prepEntries.add(new MapEntry(redir.concat(endHook),
+                            status, trailingSlash, url));
+                    }
+                }
+
+                if (prepEntries.size() > 0) {
+                    return prepEntries;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public MapEntry(String url, int status, boolean trailingSlash,
+            String... redirect) {
+
+        // ensure trailing slashes on redirects if the url
+        // ends with a trailing slash
+        if (trailingSlash) {
+            url = appendSlash(url);
+            for (int i = 0; i < redirect.length; i++) {
+                redirect[i] = appendSlash(redirect[i]);
+            }
+        }
+
+        // ensure pattern is hooked to the start of the string
+        if (!url.startsWith("^")) {
+            url = "^".concat(url);
+        }
+
+        this.urlPattern = Pattern.compile(url);
+        this.redirect = redirect;
+        this.status = status;
+    }
+
+    // Returns the replacement or null if the value does not match
+    public String[] replace(String value) {
+        Matcher m = urlPattern.matcher(value);
+        if (m.find()) {
+            String[] redirects = getRedirect();
+            String[] results = new String[redirects.length];
+            for (int i = 0; i < redirects.length; i++) {
+                results[i] = m.replaceFirst(redirects[i]);
+            }
+            return results;
+        }
+
+        return null;
+    }
+
+    public String getPattern() {
+        return urlPattern.toString();
+    }
+
+    public String[] getRedirect() {
+        return redirect;
+    }
+
+    public boolean isInternal() {
+        return getStatus() < 0;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    // ---------- Comparable
+
+    public int compareTo(MapEntry m) {
+        if (this == m) {
+            return 0;
+        }
+
+        int tlen = urlPattern.toString().length();
+        int mlen = m.urlPattern.toString().length();
+        if (tlen < mlen) {
+            return 1;
+        } else if (tlen > mlen) {
+            return -1;
+        }
+
+        // lentghs are equal, but the entries are not
+        // so order m after this
+        return 1;
+    }
+
+    // ---------- Object overwrite
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("MapEntry: match:").append(urlPattern);
+
+        buf.append(", replacement:");
+        if (getRedirect().length == 1) {
+            buf.append(getRedirect()[0]);
+        } else {
+            buf.append(Arrays.asList(getRedirect()));
+        }
+
+        if (isInternal()) {
+            buf.append(", internal");
+        } else {
+            buf.append(", status:").append(getStatus());
+        }
+        return buf.toString();
+    }
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/Mapping.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/Mapping.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/Mapping.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/Mapping.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The <code>Mapping</code> class conveys the mapping configuration used by
+ * the
+ * {@link org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl}.
+ */
+public class Mapping {
+
+    /**
+     * defines the 'inbound' direction, that is mapping request path to item
+     * path
+     */
+    public static final int INBOUND = 1;
+
+    /** defined the 'outbound' direction, that is mapping item path to URL path */
+    public static final int OUTBOUND = 2;
+
+    /** defines the 'both' direction */
+    public static final int BOTH = 3;
+
+    /** Simple mapper instance mapping path to URLs 1:1 in both directions */
+    public static final Mapping DIRECT = new Mapping("", "", BOTH) {
+
+        @Override
+        public String mapHandle(String handle) {
+            return handle;
+        }
+
+        @Override
+        public boolean mapsInbound() {
+            return true;
+        }
+
+        @Override
+        public boolean mapsOutbound() {
+            return true;
+        }
+
+        @Override
+        public String mapUri(String uriPath) {
+            return uriPath;
+        }
+    };
+
+    // Regular expression to split mapping configuration strings into three
+    // groups:
+    //   1 - external path prefix
+    //   2 - direction (Outbound (>), Bidirectional (:), Inbound (>))
+    //   3 - internap path prefix
+    private static final Pattern CONFIG_SPLITTER = Pattern.compile("(.+)([:<>])(.+)");
+
+    /** the 'from' (inside, repository) mapping */
+    private final String from;
+
+    /** the 'to' (outside, URL) mapping */
+    private final String to;
+
+    /** the length of the 'from' field */
+    private final int fromLength;
+
+    /** the length of the 'to' field */
+    private final int toLength;
+
+    /** the mapping direction */
+    private final int direction;
+
+    public Mapping(String config) {
+        this(split(config));
+    }
+
+    public Mapping(String[] parts) {
+        this.from = parts[0];
+        this.to = parts[2];
+        this.fromLength = this.from.length();
+        this.toLength = this.to.length();
+
+        this.direction = ">".equals(parts[1])
+                ? Mapping.INBOUND
+                : ("<".equals(parts[1]) ? Mapping.OUTBOUND : Mapping.BOTH);
+    }
+
+    @Override
+    public String toString() {
+        return "Mapping (from=" + from + ", to=" + to + ", direction=" + direction
+            + ", lengths=" + fromLength + "/" + toLength;
+    }
+
+    /**
+     * Replaces the prefix <em>to</em> by the new prefix <em>from</em>, if
+     * and only if <code>uriPath</code> starts with the <em>to</em> prefix.
+     * If <code>uriPath</code> does not start with the <em>to</em> prefix,
+     * or if this mapping is not defined as a 'inward' mapping,
+     * <code>null</code> is returned.
+     *
+     * @param uriPath The URI path for which to replace the <em>to</em> prefix
+     *            by the <em>from</em> prefix.
+     * @return The string after replacement or <code>null</code> if the
+     *         <code>uriPath</code> does not start with the <em>to</em>
+     *         prefix, or {@link #mapsInbound()} returns <code>false</code>.
+     */
+    public String mapUri(String uriPath) {
+        return (this.mapsInbound() && uriPath.startsWith(this.to)) ? this.from
+            + uriPath.substring(this.toLength) : null;
+    }
+
+    /**
+     * Replaces the prefix <em>from</em> by the new prefix <em>to</em>, if
+     * and only if <code>handle</code> starts with the <em>from</em> prefix.
+     * If <code>uriPath</code> does not start with the <em>from</em> prefix,
+     * or if this mapping is not defined as a 'outward' mapping,
+     * <code>null</code> is returned.
+     *
+     * @param handle The URI path for which to replace the <em>from</em>
+     *            prefix by the <em>to</em> prefix.
+     * @return The string after replacement or <code>null</code> if the
+     *         <code>handle</code> does not start with the <em>from</em>
+     *         prefix, or {@link #mapsOutbound()} returns <code>false</code>.
+     */
+    public String mapHandle(String handle) {
+        return (this.mapsOutbound() && handle.startsWith(this.from)) ? this.to
+            + handle.substring(this.fromLength) : null;
+    }
+
+    // TODO: temporary
+    public String getFrom() {
+        return from;
+    }
+
+    // TODO: temporary
+    public String getTo() {
+        return to;
+    }
+
+    /**
+     * Checks, if this mapping is defined for inbound mapping.
+     *
+     * @return <code>true</code> if this mapping is defined for inbound
+     *         mapping; <code>false</code> otherwise
+     */
+    public boolean mapsInbound() {
+        return (this.direction & Mapping.INBOUND) > 0;
+    }
+
+    /**
+     * Checks, if this mapping is defined for outbound mapping.
+     *
+     * @return <code>true</code> if this mapping is defined for outbound
+     *         mapping; <code>false</code> otherwise
+     */
+    public boolean mapsOutbound() {
+        return (this.direction & Mapping.OUTBOUND) > 0;
+    }
+
+    /**
+     * Constructs a new mapping with the given mapping string and the direction
+     */
+    private Mapping(String from, String to, int dir) {
+        this.from = from;
+        this.to = to;
+        this.fromLength = from.length();
+        this.toLength = to.length();
+        this.direction = dir;
+    }
+
+    public static String[] split(String map) {
+
+        // standard case of mapping <path>[<:>]<path>
+        Matcher mapMatch = CONFIG_SPLITTER.matcher(map);
+        if (mapMatch.matches()) {
+            return new String[] { mapMatch.group(1), mapMatch.group(2),
+                mapMatch.group(3) };
+        }
+
+        // backwards compatibility using "-" instead of ":"
+        int dash = map.indexOf('-');
+        if (dash > 0) {
+            return new String[] { map.substring(0, dash),
+                map.substring(dash, dash + 1),
+                map.substring(dash + 1, map.length()) };
+        }
+
+        return new String[] { map, "-", map };
+    }
+
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RedirectResource.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RedirectResource.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RedirectResource.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RedirectResource.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.adapter.annotations.Adaptable;
+import org.apache.sling.adapter.annotations.Adapter;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+
+@Adaptable(adaptableClass = Resource.class, adapters = @Adapter(value = { Map.class, ValueMap.class }))
+public final class RedirectResource extends SyntheticResource {
+
+    static final String RT_SLING_REDIRECT = "sling:redirect";
+
+    static final String PROP_SLING_TARGET = "sling:target";
+
+    static final String PROP_SLING_STATUS = "sling:status";
+
+    private final Map<String, Object> values;
+
+    public RedirectResource(final ResourceResolver resolver, final String path,
+            final String target, final int status) {
+        super(resolver, path, RT_SLING_REDIRECT);
+
+        HashMap<String, Object> props = new HashMap<String, Object>();
+        props.put(PROP_SLING_TARGET, target);
+        props.put(PROP_SLING_STATUS, status);
+        this.values = Collections.unmodifiableMap(props);
+    }
+
+    /**
+     * @see org.apache.sling.api.adapter.Adaptable#adaptTo(java.lang.Class)
+     */
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type == ValueMap.class) {
+            return (AdapterType) new ValueMapDecorator(values);
+        } else if (type == Map.class) {
+            return (AdapterType) values;
+        }
+
+        return super.adaptTo(type);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + ", values=" + values;
+    }
+}
\ No newline at end of file

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceIterator.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceIterator.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceIterator.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceIterator.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,294 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ResourceIterator</code> implements the
+ * <code>Iterator&lt;Resource&gt;</code> returned from the
+ * <code>ResourceResolver.listChidlren(Resource)</code> method.
+ * <p>
+ * Note: This iterator is created by the
+ * <code>JcrResourceResolver.listChildren(Resource)</code> and is not intended
+ * for general use by any other code. This class uses internal API of the
+ * {@link ResourceProviderEntry} class.
+ */
+public class ResourceIterator implements Iterator<Resource> {
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    /**
+     * The resource whose children are listed
+     */
+    private final Resource parentResource;
+
+    /**
+     * The root {@link ResourceProviderEntry} used to walk down the resource
+     * tree to collect entries which might provide children for the
+     * {@link #parentResource}.
+     */
+    private final ResourceProviderEntry rootProviderEntry;
+
+    /**
+     * <code>ResourceProvider</code> objects registered as nodes above the
+     * {@link #parentResource} up to the root of the resource tree
+     */
+    private final Iterator<ResourceProvider> providers;
+
+    /**
+     * The child {@link ResourceProviderEntry} registered at the node of the
+     * {@link #parentResource} in the resource tree. This may be
+     * <code>null</code> if there is no provider entry registered at that
+     * location and will be set to <code>null</code> once all entries have been
+     * processed.
+     */
+    private Iterator<ResourceProviderEntry> baseEntryValues;
+
+    /**
+     * An iterator of child resources provided by the current provider entry of
+     * the {@link #providers} iterator.
+     */
+    private Iterator<Resource> resources;
+
+    /**
+     * The next resource to be returned from the {@link #next()} method. If this
+     * is <code>null</code> the {@link #hasNext()} returns <code>false</code>.
+     */
+    private Resource nextResource;
+
+    /**
+     * Map of synthetic resources returned from resource providers while
+     * scanning for children of the {@link #parentResource}. These delayed
+     * entries are returned after all non-synthetic resources have been
+     * returned. Any delayed entry whose path matches the path of a
+     * non-synthetic resource will not returned.
+     */
+    private Map<String, Resource> delayed;
+
+    /**
+     * Set of paths of resources already returned. This is used to prevent
+     * duplicate return of resources.
+     */
+    private Set<String> visited;
+
+    /**
+     * The absolute path prefix of the {@link #parentResource} resource with a
+     * trailing slash to build the absolute path of child resources.
+     */
+    private String iteratorPath;
+
+    /**
+     * Iterator on the map of {@link #delayed} synthetic resources
+     */
+    private Iterator<Resource> delayedIter;
+
+    public ResourceIterator(final Resource parentResource,
+            final ResourceProviderEntry rootProviderEntry) {
+        this.parentResource = parentResource;
+        this.rootProviderEntry = rootProviderEntry;
+
+        log.debug("Child Iterator for {}", parentResource.getPath());
+
+        String path = parentResource.getPath();
+        if (!path.endsWith("/")) {
+            path += "/";
+        }
+
+        // gather the providers in linked set, such that we keep
+        // the order of addition and make sure we only get one entry
+        // for each resource provider
+        Set<ResourceProvider> providersSet = new LinkedHashSet<ResourceProvider>();
+        ResourceProviderEntry atPath = getResourceProviders(path, providersSet);
+
+        if (log.isDebugEnabled()) {
+            log.debug(" Provider Set for path {} {} ", path,
+                Arrays.toString(providersSet.toArray(new ResourceProvider[0])));
+        }
+        this.iteratorPath = path;
+        providers = providersSet.iterator();
+        baseEntryValues = (atPath != null) ? atPath.values().iterator() : null;
+        delayed = new HashMap<String, Resource>();
+        visited = new HashSet<String>();
+        nextResource = seek();
+    }
+
+    public boolean hasNext() {
+        return nextResource != null;
+    }
+
+    public Resource next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        Resource result = nextResource;
+        nextResource = seek();
+        log.debug("  Child resource [{}] [{}] ", iteratorPath, result.getPath());
+        return result;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException("remove");
+    }
+
+    private Resource seek() {
+        while (delayedIter == null) {
+            while ((resources == null || !resources.hasNext())
+                && providers.hasNext()) {
+                ResourceProvider provider = providers.next();
+                resources = provider.listChildren(parentResource);
+                log.debug("     Checking Provider {} ", provider);
+            }
+
+            if (resources != null && resources.hasNext()) {
+                Resource res = resources.next();
+                String resPath = res.getPath();
+
+                if (visited.contains(resPath)) {
+
+                    // ignore a path, we have already visited and
+                    // ensure it will not be listed as a delayed
+                    // resource at the end
+                    delayed.remove(resPath);
+
+                } else if (res instanceof SyntheticResource) {
+
+                    // don't return synthetic resources right away,
+                    // since a concrete resource for the same path
+                    // may be provided later on
+                    delayed.put(resPath, res);
+
+                } else {
+
+                    // we use this concrete, unvisited resource but
+                    // mark it as visited and remove from delayed
+                    visited.add(resPath);
+                    delayed.remove(resPath);
+                    log.debug("      resource {} {}", resPath, res.getClass());
+                    return res;
+
+                }
+
+            } else if (baseEntryValues != null) {
+
+                while (baseEntryValues.hasNext()) {
+                    final ResourceProviderEntry rpw = baseEntryValues.next();
+                    final String resPath = iteratorPath + rpw.getPath();
+                    if (!visited.contains(resPath)) {
+                        final ResourceResolver rr = parentResource.getResourceResolver();
+                        final Resource res = rpw.getResourceFromProviders(rr,
+                            resPath);
+                        if (res == null) {
+                            if (!delayed.containsKey(resPath)) {
+                                delayed.put(resPath, new SyntheticResource(rr,
+                                    resPath,
+                                    ResourceProvider.RESOURCE_TYPE_SYNTHETIC));
+                            }
+                        } else {
+                            // return the real resource immediately, add
+                            // to the visited keys and ensure delayed
+                            // does not contain it
+                            delayed.remove(resPath);
+                            visited.add(resPath);
+                            log.debug("   B  resource {} {}", resPath,
+                                res.getClass());
+                            return res;
+                        }
+                    }
+                }
+
+                baseEntryValues = null;
+
+            } else {
+
+                // all resource providers and baseEntryValues have
+                // exhausted, so we should continue returning the
+                // delayed (synthetic resources)
+                delayedIter = delayed.values().iterator();
+            }
+        }
+
+        // we exhausted all resource providers with their concrete
+        // resources. now lets do the delayed (synthetic) resources
+        Resource res = delayedIter.hasNext() ? delayedIter.next() : null;
+        if (res != null) {
+            log.debug("   D  resource {} {}", res.getPath(), res.getClass());
+        }
+        return res;
+    }
+
+    /**
+     * Returns all resource providers which provider resources whose prefix is
+     * the given path.
+     *
+     * @param path The prefix path to match the resource provider roots against
+     * @param providers The set of already found resource providers to which any
+     *            additional resource providers are added.
+     * @return The ResourceProviderEntry at the node identified with the path or
+     *         <code>null</code> if there is no entry at the given location
+     */
+    private ResourceProviderEntry getResourceProviders(String path,
+            Set<ResourceProvider> providers) {
+
+        // collect providers along the ancestor path segements
+        String[] elements = ResourceProviderEntry.split(path, '/');
+        ResourceProviderEntry base = rootProviderEntry;
+        for (String element : elements) {
+            if (base.containsKey(element)) {
+                base = base.get(element);
+                if (log.isDebugEnabled()) {
+                    log.debug("Loading from {}  {} ", element,
+                        base.getResourceProviders().length);
+                }
+                for (ResourceProvider rp : base.getResourceProviders()) {
+                    log.debug("Adding {} for {} ", rp, path);
+                    providers.add(rp);
+                }
+            } else {
+                log.debug("No container for {} ", element);
+                base = null;
+                break;
+            }
+        }
+
+        // add in providers at this node in the tree, ie the root provider
+        for (ResourceProvider rp : rootProviderEntry.getResourceProviders()) {
+            log.debug("Loading All at {} ", path);
+            providers.add(rp);
+        }
+        return base;
+    }
+
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourcePathIterator.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourcePathIterator.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourcePathIterator.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourcePathIterator.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterate over the the HTTP request path by creating shorter segments of that
+ * path using "." as a separator.
+ * <p>
+ * For example, if path = /some/stuff.a4.html/xyz.ext the sequence is:
+ * <ol>
+ * <li> /some/stuff.a4.html/xyz.ext </li>
+ * <li> /some/stuff.a4.html/xyz </li>
+ * <li> /some/stuff.a4</li>
+ * <li> /some/stuff </li>
+ * </ol>
+ * <p>
+ * The root path (/) is never returned.
+ */
+public class ResourcePathIterator implements Iterator<String> {
+
+    // the next path to return, null if nothing more to return
+    private String nextPath;
+
+    /**
+     * Creates a new instance iterating over the given path
+     *
+     * @param path The path to iterate over. If this is empty or
+     *            <code>null</code> this iterator will not return anything.
+     */
+    public ResourcePathIterator(String path) {
+
+        if (path == null || path.length() == 0) {
+
+            // null or empty path, there is nothing to return
+            nextPath = null;
+
+        } else {
+
+            // find last non-slash character
+            int i = path.length() - 1;
+            while (i >= 0 && path.charAt(i) == '/') {
+                i--;
+            }
+
+            if (i < 0) {
+                // only slashes, assume root node
+                nextPath = "/";
+
+            } else if (i < path.length() - 1) {
+                // cut off slash
+                nextPath = path.substring(0, i + 1);
+
+            } else {
+                // no trailing slash
+                nextPath = path;
+            }
+        }
+    }
+
+    public boolean hasNext() {
+        return nextPath != null;
+    }
+
+    public String next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        final String result = nextPath;
+
+        // find next path
+        int lastDot = nextPath.lastIndexOf('.');
+        nextPath = (lastDot > 0) ? nextPath.substring(0, lastDot) : null;
+
+        return result;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException("remove");
+    }
+
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceProviderEntry.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceProviderEntry.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceProviderEntry.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/ResourceProviderEntry.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,464 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections.FastTreeMap;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ResourceProviderEntry</code> class represents a node in the tree of
+ * resource providers spanned by the root paths of the provider resources.
+ * <p>
+ * This class is comparable to itself to help keep the child entries list sorted
+ * by their prefix.
+ */
+public class ResourceProviderEntry implements
+        Comparable<ResourceProviderEntry> {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 7420631325909144862L;
+
+    private static Logger LOGGER = LoggerFactory.getLogger(ResourceProviderEntry.class);
+
+    // the path to resources provided by the resource provider of this
+    // entry. this path is relative to the path of the parent resource
+    // provider entry and has no trailing slash.
+    private final String path;
+
+    // the path to resources provided by the resource provider of this
+    // entry. this is the same path as the path field but with a trailing
+    // slash to be used as a prefix match resource paths to resolve
+    private final String prefix;
+
+    // the resource provider kept in this entry supporting resources at and
+    // below the path of this entry.
+    private WrappedResourceProvider[] providers = new WrappedResourceProvider[0];
+
+    private long ttime = 0L;
+
+    private long nmiss = 0L;
+
+    private long nsynthetic = 0L;
+
+    private long nreal = 0L;
+
+    private FastTreeMap storageMap = new FastTreeMap();
+
+    private Collection<ResourceProviderEntry> storageMapValues = new ArrayList<ResourceProviderEntry>();
+
+    /**
+     * Creates an instance of this class with the given path relative to the
+     * parent resource provider entry, encapsulating the given ResourceProvider,
+     * and a number of inital child entries.
+     *
+     * @param path
+     *            The relative path supported by the provider
+     * @param providerList
+     *            The resource provider to encapsulate by this entry.
+     */
+    public ResourceProviderEntry(String path, ResourceProvider[] providerList) {
+        if (path.endsWith("/")) {
+            this.path = path.substring(0, path.length() - 1);
+            this.prefix = path;
+        } else {
+            this.path = path;
+            this.prefix = path + "/";
+        }
+        if ( providerList != null ) {
+          providers = new WrappedResourceProvider[providerList.length];
+          for ( int i = 0; i < providerList.length; i++ ) {
+            if ( providerList[i] instanceof WrappedResourceProvider ) {
+              providers[i] = (WrappedResourceProvider) providerList[i];
+            } else {
+              providers[i] = new WrappedResourceProvider(providerList[i], null);
+            }
+          }
+        }
+
+        // this will consume slightly more memory but ensures read is fast.
+        storageMap.setFast(true);
+
+    }
+
+    String getPath() {
+        return path;
+    }
+
+    /**
+     * Returns the resource provider contained in this entry
+     */
+    public ResourceProvider[] getResourceProviders() {
+        return providers;
+    }
+
+    /**
+     * Returns the resource with the given path or <code>null</code> if neither
+     * the resource provider of this entry nor the resource provider of any of
+     * the child entries can provide the resource.
+     *
+     * @param path
+     *            The path to the resource to return.
+     * @return The resource for the path or <code>null</code> if no resource can
+     *         be found.
+     * @throws org.apache.sling.api.SlingException
+     *             if an error occurrs trying to access an existing resource.
+     */
+    public Resource getResource(ResourceResolver resourceResolver, String path) {
+        return getInternalResource(resourceResolver, path);
+    }
+
+    /**
+     * Adds the given resource provider into the tree for the given prefix.
+     *
+     * @return <code>true</code> if the provider could be entered into the
+     *         subtree below this entry. Otherwise <code>false</code> is
+     *         returned.
+     */
+    public boolean addResourceProvider(String prefix, ResourceProvider provider, Comparable<?> comparable) {
+        synchronized (this) {
+            String[] elements = split(prefix, '/');
+            List<ResourceProviderEntry> entryPath = new ArrayList<ResourceProviderEntry>();
+            entryPath.add(this); // add this the start so if the list is empty we have a position to add to
+            populateProviderPath(entryPath, elements);
+            for (int i = entryPath.size() - 1; i < elements.length; i++) {
+                String stubPrefix = elements[i];
+                ResourceProviderEntry rpe2 = new ResourceProviderEntry(
+                        stubPrefix, new ResourceProvider[0]);
+                entryPath.get(i).put(elements[i], rpe2);
+                entryPath.add(rpe2);
+            }
+            return entryPath.get(elements.length).addInternalProvider(new WrappedResourceProvider(provider, comparable));
+
+        }
+    }
+
+
+    //------------------ Map methods, here so that we can delegate 2 maps together
+    @SuppressWarnings("unchecked")
+    public void put(String key, ResourceProviderEntry value) {
+        storageMap.put(key,value);
+        // get a thread safe copy, the ArrayList constructor does a toArray which is thread safe.
+        storageMapValues = new ArrayList<ResourceProviderEntry>(storageMap.values());
+    }
+
+    public boolean containsKey(String key) {
+        return storageMap.containsKey(key);
+    }
+
+    public ResourceProviderEntry get(String key) {
+        return (ResourceProviderEntry) storageMap.get(key);
+    }
+
+    public Collection<ResourceProviderEntry> values() {
+        return storageMapValues;
+    }
+
+    public boolean removeResourceProvider(String prefix,
+            ResourceProvider resourceProvider, Comparable<?> comparable) {
+        synchronized (this) {
+            String[] elements = split(prefix, '/');
+            List<ResourceProviderEntry> entryPath = new ArrayList<ResourceProviderEntry>();
+            populateProviderPath(entryPath, elements);
+            if (entryPath.size() > 0 && entryPath.size() == elements.length) {
+                // the last element is a perfect match;
+                return entryPath.get(entryPath.size()-1).removeInternalProvider(new WrappedResourceProvider(resourceProvider, comparable));
+            }
+            return false;
+        }
+    }
+
+    // ---------- Comparable<ResourceProviderEntry> interface ------------------
+
+    public int compareTo(ResourceProviderEntry o) {
+        return prefix.compareTo(o.prefix);
+    }
+
+    // ---------- internal -----------------------------------------------------
+
+    /**
+     * Adds a list of providers to this entry.
+     *
+     * @param provider
+     */
+    private boolean addInternalProvider(WrappedResourceProvider provider) {
+        synchronized (providers) {
+            int before = providers.length;
+            Set<WrappedResourceProvider> set = new HashSet<WrappedResourceProvider>();
+            if (providers != null) {
+                set.addAll(Arrays.asList(providers));
+            }
+            LOGGER.debug("Adding provider {} at {} ",provider,path);
+            set.add(provider);
+            providers = conditionalSort(set);
+            return providers.length > before;
+        }
+
+    }
+
+    /**
+     * @param provider
+     * @return
+     */
+    private boolean removeInternalProvider(WrappedResourceProvider provider) {
+        synchronized (providers) {
+            int before = providers.length;
+            Set<WrappedResourceProvider> set = new HashSet<WrappedResourceProvider>();
+            if (providers != null) {
+                set.addAll(Arrays.asList(providers));
+            }
+            set.remove(provider);
+            providers = conditionalSort(set);
+            return providers.length < before;
+        }
+    }
+
+    /**
+     * @param set
+     * @return
+     */
+    private WrappedResourceProvider[] conditionalSort(Set<WrappedResourceProvider> set) {
+
+        List<WrappedResourceProvider> providerList = new ArrayList<WrappedResourceProvider>(
+                set);
+
+        Collections.sort(providerList, new Comparator<WrappedResourceProvider>() {
+
+            @SuppressWarnings("unchecked")
+            public int compare(WrappedResourceProvider o1, WrappedResourceProvider o2) {
+                Comparable c1 = o1.getComparable();
+                Comparable c2 = o2.getComparable();
+                if ( c1 == null && c2 == null ) {
+                  return 0;
+                }
+                if ( c1 == null ) {
+                  return -1;
+                }
+                if ( c2 == null ) {
+                  return 1;
+                }
+                return c1.compareTo(c2);
+            }
+        });
+
+        return set.toArray(new WrappedResourceProvider[set.size()]);
+    }
+
+    /**
+     * Get a of ResourceProvidersEntries leading to the fullPath in reverse
+     * order.
+     *
+     * @param fullPath
+     *            the full path
+     */
+    private void populateProviderPath(
+        List<ResourceProviderEntry> providerEntryPath, String[] elements) {
+        ResourceProviderEntry base = this;
+        if (elements != null) {
+            for (String element : elements) {
+                if (element != null) {
+                    if (base.containsKey(element)) {
+                        base = base.get(element);
+                        providerEntryPath.add(base);
+                    } else {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Resolve a resource from a path into a Resource
+     *
+     * @param resolver
+     *            the ResourceResolver.
+     * @param fullPath
+     *            the Full path
+     * @return null if no resource was found, a resource if one was found.
+     */
+    private Resource getInternalResource(ResourceResolver resourceResolver,
+            String fullPath) {
+        long start = System.currentTimeMillis();
+        try {
+
+            if (fullPath == null || fullPath.length() == 0
+                    || fullPath.charAt(0) != '/') {
+                nmiss++;
+                LOGGER.debug("Not absolute {} :{}",fullPath,(System.currentTimeMillis() - start));
+                return null; // fullpath must be absolute
+            }
+            String[] elements = split(fullPath, '/');
+
+            List<ResourceProviderEntry> list = new ArrayList<ResourceProviderEntry>();
+            populateProviderPath(list, elements);
+            // the path is in reverse order end first
+
+            for(int i = list.size()-1; i >= 0; i--) {
+                ResourceProvider[] rps = list.get(i).getResourceProviders();
+                for (ResourceProvider rp : rps) {
+
+                    Resource resource = rp.getResource(resourceResolver,
+                            fullPath);
+                    if (resource != null) {
+                        nreal++;
+                        LOGGER.debug("Resolved Full {} using {} from {} ",new Object[]{
+                                fullPath, rp, Arrays.toString(rps)});
+                        return resource;
+                    }
+                }
+            }
+
+            // resolve against this one
+            final Resource resource = getResourceFromProviders(
+                resourceResolver, fullPath);
+            if (resource != null) {
+                return resource;
+            }
+
+            // query: /libs/sling/servlet/default
+            // resource Provider: libs/sling/servlet/default/GET.servlet
+            // list will match libs, sling, servlet, default
+            // and there will be no resource provider at the end
+            if (list.size() > 0 && list.size() == elements.length ) {
+                if ( list.get(list.size()-1).getResourceProviders().length == 0 ) {
+                    nsynthetic++;
+                    LOGGER.debug("Resolved Synthetic {}", fullPath);
+                    return new SyntheticResource(resourceResolver,
+                            fullPath,
+                            ResourceProvider.RESOURCE_TYPE_SYNTHETIC);
+                }
+            }
+
+
+
+            LOGGER.debug("Resource null {} ", fullPath);
+            nmiss++;
+            return null;
+        } catch (Exception ex) {
+            LOGGER.debug("Failed! ",ex);
+            return null;
+        } finally {
+            ttime += System.currentTimeMillis() - start;
+        }
+    }
+
+    Resource getResourceFromProviders(final ResourceResolver resourceResolver,
+            final String fullPath) {
+        ResourceProvider[] rps = getResourceProviders();
+        for (ResourceProvider rp : rps) {
+            Resource resource = rp.getResource(resourceResolver, fullPath);
+            if (resource != null) {
+                nreal++;
+                LOGGER.debug("Resolved Base {} using {} ", fullPath, rp);
+                return resource;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param st
+     * @param sep
+     * @return an array of the strings between the separator
+     */
+    static String[] split(String st, char sep) {
+
+        if (st == null) {
+            return new String[0];
+        }
+        char[] pn = st.toCharArray();
+        if (pn.length == 0) {
+            return new String[0];
+        }
+        if (pn.length == 1 && pn[0] == sep) {
+            return new String[0];
+        }
+        int n = 1;
+        int start = 0;
+        int end = pn.length;
+        while (start < end && sep == pn[start])
+            start++;
+        while (start < end && sep == pn[end - 1])
+            end--;
+        for (int i = start; i < end; i++) {
+            if (sep == pn[i]) {
+                n++;
+            }
+        }
+        String[] e = new String[n];
+        int s = start;
+        int j = 0;
+        for (int i = start; i < end; i++) {
+            if (pn[i] == sep) {
+                e[j++] = new String(pn, s, i - s);
+                s = i + 1;
+            }
+        }
+        if (s < end) {
+            e[j++] = new String(pn, s, end - s);
+        }
+        return e;
+    }
+
+    public String getResolutionStats() {
+        long tot = nreal + nsynthetic + nmiss;
+        if (tot == 0) {
+            return null;
+        }
+        float n = tot;
+        float t = ttime;
+        float persec = 1000 * n / t;
+        float avgtime = t / n;
+
+        String stat = "Resolved: Real(" + nreal + ") Synthetic(" + nsynthetic
+                + ") Missing(" + nmiss + ") Total(" + tot + ") at " + persec
+                + " ops/sec avg " + avgtime + " ms";
+        ttime = nmiss = nsynthetic = nreal = 0L;
+        return stat;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @see java.util.AbstractMap#toString()
+     */
+    @Override
+    public String toString() {
+        return this.path;
+        //"{path:\"" + this.path + "\", providers:"+Arrays.toString(getResourceProviders())+", map:" + storageMap.toString() + "}";
+    }
+
+}

Added: sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RootResourceProviderEntry.java
URL: http://svn.apache.org/viewvc/sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RootResourceProviderEntry.java?rev=1239587&view=auto
==============================================================================
--- sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RootResourceProviderEntry.java (added)
+++ sling/whiteboard/resourceresolverfactory/jcr-resource/src/main/java/org/apache/sling/jcr/resource/internal/helper/RootResourceProviderEntry.java Thu Feb  2 12:46:58 2012
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.sling.jcr.resource.internal.helper;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.resource.ResourceProvider;
+import org.apache.sling.commons.osgi.OsgiUtil;
+import org.osgi.framework.Constants;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the root resource provider entry which keeps track
+ * of the resource providers.
+ */
+public class RootResourceProviderEntry extends ResourceProviderEntry {
+
+    /** default logger */
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    public RootResourceProviderEntry() {
+        super("/", null);
+    }
+
+    public void bindResourceProvider(final ResourceProvider provider,
+                                     final Map<String, Object> props,
+                                     final ServiceTracker eventAdminTracker) {
+
+        final String serviceName = getServiceName(provider, props);
+
+        logger.debug("bindResourceProvider: Binding {}", serviceName);
+
+        String[] roots = OsgiUtil.toStringArray(props.get(ResourceProvider.ROOTS));
+        if (roots != null && roots.length > 0) {
+            final EventAdmin localEA = (EventAdmin) ( eventAdminTracker != null ? eventAdminTracker.getService() : null);
+
+            for (String root : roots) {
+                // cut off trailing slash
+                if (root.endsWith("/") && root.length() > 1) {
+                    root = root.substring(0, root.length() - 1);
+                }
+
+                // synchronized insertion of new resource providers into
+                // the tree to not inadvertently loose an entry
+                synchronized (this) {
+
+                    this.addResourceProvider(root,
+                        provider, OsgiUtil.getComparableForServiceRanking(props));
+                }
+                logger.debug("bindResourceProvider: {}={} ({})",
+                    new Object[] { root, provider, serviceName });
+                if ( localEA != null ) {
+                    final Dictionary<String, Object> eventProps = new Hashtable<String, Object>();
+                    eventProps.put(SlingConstants.PROPERTY_PATH, root);
+                    localEA.postEvent(new Event(SlingConstants.TOPIC_RESOURCE_PROVIDER_ADDED,
+                            eventProps));
+                }
+            }
+        }
+
+        logger.debug("bindResourceProvider: Bound {}", serviceName);
+    }
+
+    public void unbindResourceProvider(final ResourceProvider provider,
+                                       final Map<String, Object> props,
+                                       final ServiceTracker eventAdminTracker) {
+
+        final String serviceName = getServiceName(provider, props);
+
+        logger.debug("unbindResourceProvider: Unbinding {}", serviceName);
+
+        String[] roots = OsgiUtil.toStringArray(props.get(ResourceProvider.ROOTS));
+        if (roots != null && roots.length > 0) {
+
+            final EventAdmin localEA = (EventAdmin) ( eventAdminTracker != null ? eventAdminTracker.getService() : null);
+
+            for (String root : roots) {
+                // cut off trailing slash
+                if (root.endsWith("/") && root.length() > 1) {
+                    root = root.substring(0, root.length() - 1);
+                }
+
+                // synchronized insertion of new resource providers into
+                // the tree to not inadvertently loose an entry
+                synchronized (this) {
+                    // TODO: Do not remove this path, if another resource
+                    // owns it. This may be the case if adding the provider
+                    // yielded an ResourceProviderEntryException
+                    this.removeResourceProvider(root, provider, OsgiUtil.getComparableForServiceRanking(props));
+                }
+                logger.debug("unbindResourceProvider: root={} ({})", root,
+                    serviceName);
+                if ( localEA != null ) {
+                    final Dictionary<String, Object> eventProps = new Hashtable<String, Object>();
+                    eventProps.put(SlingConstants.PROPERTY_PATH, root);
+                    localEA.postEvent(new Event(SlingConstants.TOPIC_RESOURCE_PROVIDER_REMOVED,
+                            eventProps));
+                }
+            }
+        }
+
+        logger.debug("unbindResourceProvider: Unbound {}", serviceName);
+    }
+
+    private String getServiceName(final ResourceProvider provider, final Map<String, Object> props) {
+        if (logger.isDebugEnabled()) {
+            StringBuilder snBuilder = new StringBuilder(64);
+            snBuilder.append('{');
+            snBuilder.append(provider.toString());
+            snBuilder.append('/');
+            snBuilder.append(props.get(Constants.SERVICE_ID));
+            snBuilder.append('}');
+            return snBuilder.toString();
+        }
+
+        return null;
+    }
+}
\ No newline at end of file