Package org.apache.sling.resourceresolver.impl.mapping

Source Code of org.apache.sling.resourceresolver.impl.mapping.MapEntries

/*
* 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.resourceresolver.impl.mapping;

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.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

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.resourceresolver.impl.ResourceResolverFactoryImpl;
import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider.VanityPathConfig;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MapEntries implements EventHandler {

    public static final MapEntries EMPTY = new MapEntries();

    private static final String PROP_REG_EXP = "sling:match";

    public static final String PROP_REDIRECT_EXTERNAL = "sling:redirect";

    public static final String PROP_REDIRECT_EXTERNAL_STATUS = "sling:status";

    public static final String PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS = "sling:redirectStatus";
   
    public static final String PROP_VANITY_PATH = "sling:vanityPath";
   
    public static final String PROP_VANITY_ORDER = "sling:vanityOrder";

    /** Key for the global list. */
    private static final String GLOBAL_LIST_KEY = "*";

    public static final String DEFAULT_MAP_ROOT = "/etc/map";

    public static final int DEFAULT_DEFAULT_VANITY_PATH_REDIRECT_STATUS = HttpServletResponse.SC_FOUND;

    private static final String JCR_SYSTEM_PREFIX = "/jcr:system/";

    static final String ANY_SCHEME_HOST = "[^/]+/[^/]+";

    /** default log */
    private final Logger log = LoggerFactory.getLogger(getClass());

    private MapConfigurationProvider factory;

    private volatile ResourceResolver resolver;

    private final String mapRoot;

    private Map<String, List<MapEntry>> resolveMapsMap;

    private Collection<MapEntry> mapMaps;

    private Map <String,List <String>> vanityTargets;

    private Map<String, Map<String, String>> aliasMap;

    private ServiceRegistration registration;

    private EventAdmin eventAdmin;

    private final ReentrantLock initializing = new ReentrantLock();

    private final boolean enabledVanityPaths;

    private final boolean enableOptimizeAliasResolution;
   
    private final boolean vanityPathPrecedence;

    private final List<VanityPathConfig> vanityPathConfig;

    @SuppressWarnings("unchecked")
    private MapEntries() {
        this.factory = null;
        this.resolver = null;
        this.mapRoot = DEFAULT_MAP_ROOT;

        this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
        this.mapMaps = Collections.<MapEntry> emptyList();
        this.vanityTargets = Collections.<String,List <String>>emptyMap();
        this.aliasMap = Collections.<String, Map<String, String>>emptyMap();
        this.registration = null;
        this.eventAdmin = null;
        this.enabledVanityPaths = true;
        this.enableOptimizeAliasResolution = true;
        this.vanityPathConfig = null;
        this.vanityPathPrecedence = false;
    }

    @SuppressWarnings("unchecked")
    public MapEntries(final MapConfigurationProvider factory, final BundleContext bundleContext, final EventAdmin eventAdmin)
                    throws LoginException {
        this.resolver = factory.getAdministrativeResourceResolver(null);
        this.factory = factory;
        this.mapRoot = factory.getMapRoot();
        this.enabledVanityPaths = factory.isVanityPathEnabled();
        this.vanityPathConfig = factory.getVanityPathConfig();
        this.enableOptimizeAliasResolution = factory.isOptimizeAliasResolutionEnabled();
        this.vanityPathPrecedence = factory.hasVanityPathPrecedence();
        this.eventAdmin = eventAdmin;

        this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
        this.mapMaps = Collections.<MapEntry> emptyList();
        this.vanityTargets = Collections.<String,List <String>>emptyMap();
        this.aliasMap = Collections.<String, Map<String, String>>emptyMap();

        doInit();

        final Dictionary<String, String> props = new Hashtable<String, String>();
        props.put(EventConstants.EVENT_TOPIC, "org/apache/sling/api/resource/*");
        props.put(EventConstants.EVENT_FILTER, createFilter(this.enabledVanityPaths));
        props.put(Constants.SERVICE_DESCRIPTION, "Map Entries Observation");
        props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
        this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props);
    }

    /**
     * Actual initializer. Guards itself against concurrent use by using a
     * ReentrantLock. Does nothing if the resource resolver has already been
     * null-ed.
     */
    protected void doInit() {

        this.initializing.lock();
        try {
            final ResourceResolver resolver = this.resolver;
            final MapConfigurationProvider factory = this.factory;
            if (resolver == null || factory == null) {
                return;
            }

            final Map<String, List<MapEntry>> newResolveMapsMap = new ConcurrentHashMap<String, List<MapEntry>>()
            final Map<String,List<String>> vanityTargets = (this.enabledVanityPaths ? this.loadVanityPaths(resolver, newResolveMapsMap) : Collections.<String,List <String>>emptyMap());
           
            //optimization made in SLING-2521
            if (enableOptimizeAliasResolution){
                final Map<String, Map<String, String>> aliasMap = this.loadAliases(resolver);
                this.aliasMap = aliasMap;
            }

            this.vanityTargets = vanityTargets;
            this.resolveMapsMap = newResolveMapsMap;

            doUpdateConfiguration();

            sendChangeEvent();
        } catch (final Exception e) {

            log.warn("doInit: Unexpected problem during initialization", e);

        } finally {

            this.initializing.unlock();

        }
    }

    private boolean doNodeAdded(String path, boolean refreshed) {
        this.initializing.lock();
        boolean newRefreshed = refreshed;
        if (!newRefreshed) {
            resolver.refresh();
            newRefreshed = true;
        }
        try {
            Resource resource = resolver.getResource(path);
            final ValueMap props = resource.adaptTo(ValueMap.class);
            if (props.containsKey(PROP_VANITY_PATH)) {
                doAddVanity(path);
            }
            if (props.containsKey(ResourceResolverImpl.PROP_ALIAS)) {
                doAddAlias(path);
            }
            if (path.startsWith(this.mapRoot)) {
                doUpdateConfiguration();
            }
            sendChangeEvent();
        } finally {
            this.initializing.unlock();
        }
        return newRefreshed;
    }
   
    private boolean doAddAttributes(String path, String[] addedAttributes, boolean refreshed) {
        this.initializing.lock();
        boolean newRefreshed = refreshed;
        if (!newRefreshed) {
            resolver.refresh();
            newRefreshed = true;
        }
        try {
            for (String changedAttribute:addedAttributes){
                if (PROP_VANITY_PATH.equals(changedAttribute)) {
                    doAddVanity(path);
                } else if (PROP_VANITY_ORDER.equals(changedAttribute)) {
                    doUpdateVanityOrder(path, false);
                } else if (PROP_REDIRECT_EXTERNAL.equals(changedAttribute)
                        || PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS.equals(changedAttribute)) {
                    doUpdateRedirectStatus(path);
                } else if (ResourceResolverImpl.PROP_ALIAS.equals(changedAttribute)) {
                    if (enableOptimizeAliasResolution) {
                       doAddAlias(path);
                    }
                }
            }
            if (path.startsWith(this.mapRoot)) {
                doUpdateConfiguration();
            }
            sendChangeEvent();
        } finally {
            this.initializing.unlock();
        }
        return newRefreshed;
    }

    private boolean doUpdateAttributes(String path, String[] changedAttributes, boolean refreshed) {
        this.initializing.lock();
        boolean newRefreshed = refreshed;
        if (!newRefreshed) {
            resolver.refresh();
            newRefreshed = true;
        }
        try {
            for (String changedAttribute:changedAttributes){
                if (PROP_VANITY_PATH.equals(changedAttribute)) {
                    doUpdateVanity(path);
                } else if (PROP_VANITY_ORDER.equals(changedAttribute)) {
                    doUpdateVanityOrder(path, false);
                } else if (PROP_REDIRECT_EXTERNAL.equals(changedAttribute)
                        || PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS.equals(changedAttribute)) {
                    doUpdateRedirectStatus(path);
                } else if (ResourceResolverImpl.PROP_ALIAS.equals(changedAttribute)) {
                    if (enableOptimizeAliasResolution) {
                        doRemoveAlias(path, false);
                        doAddAlias(path);
                        doUpdateAlias(path, false);
                     }                   
                }
            }
            if (path.startsWith(this.mapRoot)) {
                doUpdateConfiguration();
            }
            sendChangeEvent();
        } finally {
            this.initializing.unlock();
        }
        return newRefreshed;
    }

    private boolean doRemoveAttributes(String path, String[] removedAttributes, boolean nodeDeletion, boolean refreshed) {
        this.initializing.lock();
        boolean newRefreshed = refreshed;
        if (!newRefreshed) {
            resolver.refresh();
            newRefreshed = true;
        }
        try {
            for (String changedAttribute:removedAttributes){
                if (PROP_VANITY_PATH.equals(changedAttribute)){
                    doRemoveVanity(path);
                } else if (PROP_VANITY_ORDER.equals(changedAttribute)) {
                    doUpdateVanityOrder(path, true);
                } else if (PROP_REDIRECT_EXTERNAL.equals(changedAttribute)
                        || PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS.equals(changedAttribute)) {
                    doUpdateRedirectStatus(path);
                } else if (ResourceResolverImpl.PROP_ALIAS.equals(changedAttribute)) {
                    if (enableOptimizeAliasResolution) {
                        doRemoveAlias(path, nodeDeletion);
                        doUpdateAlias(path, nodeDeletion);                       
                    }
                }
            }
            if (path.startsWith(this.mapRoot)) {
                doUpdateConfiguration();
            }
            sendChangeEvent();
        } finally {
            this.initializing.unlock();
        }
        return newRefreshed;
    }
   
    private boolean doUpdateConfiguration(boolean refreshed){
        this.initializing.lock();
        boolean newRefreshed = refreshed;
        if (!newRefreshed) {
            resolver.refresh();
            newRefreshed = true;
        }
        try {
            doUpdateConfiguration();
            sendChangeEvent();
        } finally {
            this.initializing.unlock();
        }
        return newRefreshed;
    }

    private void doUpdateConfiguration(){
        final List<MapEntry> globalResolveMap = new ArrayList<MapEntry>();
        final SortedMap<String, MapEntry> newMapMaps = new TreeMap<String, MapEntry>();
        // load the /etc/map entries into the maps
        loadResolverMap(resolver, globalResolveMap, newMapMaps);
        // load the configuration into the resolver map
        loadConfiguration(factory, globalResolveMap);
        // load the configuration into the mapper map
        loadMapConfiguration(factory, newMapMaps);
        // sort global list and add to map
        Collections.sort(globalResolveMap);
        resolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
        this.mapMaps = Collections.unmodifiableSet(new TreeSet<MapEntry>(newMapMaps.values()));
    }

    private void doAddVanity(String path) {
        Resource resource = resolver.getResource(path);
        loadVanityPath(resource, resolveMapsMap, vanityTargets);
    }

    private void doUpdateVanity(String path) {
         doRemoveVanity(path);
         doAddVanity(path);
    }

    private void doRemoveVanity(String path) {
        String actualContentPath = getActualContentPath(path);
        List <String> l = vanityTargets.get(actualContentPath);
        if (l != null){
            for (String s : l){
                List<MapEntry> entries = this.resolveMapsMap.get(s);
                if (entries!= null) {
                    for (Iterator<MapEntry> iterator =entries.iterator(); iterator.hasNext(); ) {
                        MapEntry entry = iterator.next();
                        String redirect = getMapEntryRedirect(entry);
                        if (redirect != null && redirect.equals(actualContentPath)) {
                            iterator.remove();
                        }
                    }
                }
                if (entries!= null && entries.isEmpty()) {
                    this.resolveMapsMap.remove(s);
                }    
            }
        }
        vanityTargets.remove(actualContentPath);
    }

    private void doUpdateVanityOrder(String path, boolean deletion) {
        Resource resource = resolver.getResource(path);
        final ValueMap props = resource.adaptTo(ValueMap.class);

        long vanityOrder;
        if (deletion) {
            vanityOrder = 0;
        } else {
            vanityOrder = props.get(PROP_VANITY_ORDER, Long.class);
        }

        String actualContentPath = getActualContentPath(path);
        List<String> vanityPaths = vanityTargets.get(actualContentPath);
        if (vanityPaths != null) {
            boolean updatedOrder = false;
            for (String vanityTarget : vanityPaths) {
                List<MapEntry> entries = this.resolveMapsMap.get(vanityTarget);
                for (MapEntry entry : entries) {
                    String redirect = getMapEntryRedirect(entry);
                    if (redirect != null && redirect.equals(actualContentPath)) {
                        entry.setOrder(vanityOrder);
                        updatedOrder = true;
                    }
                }
                if (updatedOrder) {
                    Collections.sort(entries);
                }
            }
        }
    }
   
    private void doUpdateRedirectStatus(String path) {
        String actualContentPath = getActualContentPath(path);
        List<String> vanityPaths = vanityTargets.get(actualContentPath);
        if (vanityPaths != null) {
            doUpdateVanity(path);
        }
    }

    private void doAddAlias(String path) {
        Resource resource = resolver.getResource(path);
        loadAlias(resource, this.aliasMap);
    }

    private void doUpdateAlias(String path, boolean nodeDeletion) {
        if (nodeDeletion){
            if (path.endsWith("/jcr:content")) {
                path =  path.substring(0, path.length() - "/jcr:content".length());
                final Resource resource = resolver.getResource(path)
                if (resource != null) {
                    path =  resource.getPath();           
                    final ValueMap props = resource.adaptTo(ValueMap.class);
                    if (props.get(ResourceResolverImpl.PROP_ALIAS, String[].class) != null) {
                        doAddAlias(path);
                    }
                }
            } 
        } else {
            final Resource resource = resolver.getResource(path)
            if (resource != null) {
                if (resource.getName().equals("jcr:content")) { 
                    final Resource parent = resource.getParent();
                    path =  parent.getPath();           
                    final ValueMap props = parent.adaptTo(ValueMap.class);
                    if (props.get(ResourceResolverImpl.PROP_ALIAS, String[].class) != null) {
                        doAddAlias(path);
                    }
                } else if (resource.getChild("jcr:content") != null) {
                    Resource jcrContent = resource.getChild("jcr:content");
                    path =  jcrContent.getPath();        
                    final ValueMap props = jcrContent.adaptTo(ValueMap.class);
                    if (props.get(ResourceResolverImpl.PROP_ALIAS, String[].class) != null) {
                        doAddAlias(path);
                    }
                }
            }
        }
    }

    private void doRemoveAlias(String path, boolean nodeDeletion) {
        String resourceName = null;
        if (nodeDeletion) {
            if (!"/".equals(path)){
                if (path.endsWith("/jcr:content")) {
                    path =  path.substring(0, path.length() - "/jcr:content".length());
               
                resourceName = path.substring(path.lastIndexOf("/")+1);
                path = path.substring(0, path.lastIndexOf("/"));
            } else {
                resourceName = "";
            }
        } else {
            final Resource resource = resolver.getResource(path);
            if (resource.getName().equals("jcr:content")) {
                final Resource containingResource = resource.getParent();
                path = containingResource.getParent().getPath();  
                resourceName = containingResource.getName();
            } else {
                path =  resource.getParent().getPath();
                resourceName = resource.getName();
            }           
        }
        Map<String, String> aliasMapEntry = aliasMap.get(path);
        if (aliasMapEntry != null) {
            for (Iterator<String> iterator =aliasMapEntry.keySet().iterator(); iterator.hasNext(); ) {
                String key = iterator.next();
                if (resourceName.equals(aliasMapEntry.get(key))){
                    iterator.remove();
                }
            }
        }
        if (aliasMapEntry != null && aliasMapEntry.isEmpty()) {
            this.aliasMap.remove(path);
        }
    }

    public boolean isOptimizeAliasResolutionEnabled() {
        return this.enableOptimizeAliasResolution;
    }

    /**
     * Cleans up this class.
     */
    public void dispose() {
        if (this.registration != null) {
            this.registration.unregister();
            this.registration = null;
        }

        /*
         * Cooperation with doInit: The same lock as used by doInit is acquired
         * thus preventing doInit from running and waiting for a concurrent
         * doInit to terminate. Once the lock has been acquired, the resource
         * resolver is null-ed (thus causing the init to terminate when
         * triggered the right after and prevent the doInit method from doing
         * any thing).
         */

        // wait at most 10 seconds for a notifcation during initialization
        boolean initLocked;
        try {
            initLocked = this.initializing.tryLock(10, TimeUnit.SECONDS);
        } catch (final InterruptedException ie) {
            initLocked = false;
        }

        try {
            if (!initLocked) {
                log.warn("dispose: Could not acquire initialization lock within 10 seconds; ongoing intialization may fail");
            }

            // immediately set the resolver field to null to indicate
            // that we have been disposed (this also signals to the
            // event handler to stop working
            final ResourceResolver oldResolver = this.resolver;
            this.resolver = null;

            if (oldResolver != null) {
                oldResolver.close();
            } else {
                log.warn("dispose: ResourceResolver has already been cleared before; duplicate call to dispose ?");
            }
        } finally {
            if (initLocked) {
                this.initializing.unlock();
            }
        }

        // clear the rest of the fields
        this.factory = null;
        this.eventAdmin = null;
    }

    /**
     * This is for the web console plugin
     */
    public List<MapEntry> getResolveMaps() {
        final List<MapEntry> entries = new ArrayList<MapEntry>();
        for (final List<MapEntry> list : this.resolveMapsMap.values()) {
            entries.addAll(list);
        }
        Collections.sort(entries);
        return Collections.unmodifiableList(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, vanityPathPrecedence);
    }

    public Collection<MapEntry> getMapMaps() {
        return mapMaps;
    }

    public Map<String, String> getAliasMap(final String parentPath) {
        return aliasMap.get(parentPath);
    }

    // ---------- EventListener interface

    /**
     * Handles the change to any of the node properties relevant for vanity URL
     * mappings. The {@link #MapEntries(ResourceResolverFactoryImpl, BundleContext, EventAdmin)}
     * constructor makes sure the event listener is registered to only get
     * appropriate events.
     */
    public void handleEvent(final Event event) {

        // check for path (used for some tests below
        final Object p = event.getProperty(SlingConstants.PROPERTY_PATH);
        final String path;
        if (p instanceof String) {
            path = (String) p;
            log.debug("handleEvent, topic={}, path={}", event.getTopic(), path);
        } else {
            log.debug("handleEvent, topic={}, no path provided, event ignored", event.getTopic());
            return;
        }

        // don't care for system area
        if (path.startsWith(JCR_SYSTEM_PREFIX)) {
            return;
        }

        boolean wasResolverRefreshed = false;

        //removal of a node is handled differently
        if (SlingConstants.TOPIC_RESOURCE_REMOVED.equals(event.getTopic())) {
            final String actualContentPath = getActualContentPath(path);
            for (final String target : this.vanityTargets.keySet()) {
                if (target.startsWith(actualContentPath)) {
                    wasResolverRefreshed = doRemoveAttributes(path, new String [] {PROP_VANITY_PATH}, true, wasResolverRefreshed);
                }
            }
            for (final String target : this.aliasMap.keySet()) {
                if (actualContentPath.startsWith(target)) {
                    wasResolverRefreshed = doRemoveAttributes(path, new String [] {ResourceResolverImpl.PROP_ALIAS}, true, wasResolverRefreshed);
                }
            }
            if (path.startsWith(this.mapRoot)) {
                //need to update the configuration
                wasResolverRefreshed = doUpdateConfiguration(wasResolverRefreshed);
            }
        //session.move() is handled differently see also SLING-3713 and   
        } else if (SlingConstants.TOPIC_RESOURCE_ADDED.equals(event.getTopic()) && event.getProperty(SlingConstants.PROPERTY_ADDED_ATTRIBUTES) == null) {
            wasResolverRefreshed = doNodeAdded(path, wasResolverRefreshed);
        } else {
            String [] addedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_ADDED_ATTRIBUTES);
            if (addedAttributes != null) {
                if (log.isDebugEnabled()) {
                    log.debug("found added attributes {}", addedAttributes);
                }
                wasResolverRefreshed = doAddAttributes(path, addedAttributes, wasResolverRefreshed);
            }

            String [] changedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_CHANGED_ATTRIBUTES);
            if (changedAttributes != null) {
                if (log.isDebugEnabled()) {
                    log.debug("found changed attributes {}", changedAttributes);
                }
                wasResolverRefreshed = doUpdateAttributes(path, changedAttributes, wasResolverRefreshed);
            }

            String [] removedAttributes = (String []) event.getProperty(SlingConstants.PROPERTY_REMOVED_ATTRIBUTES);
            if (removedAttributes != null) {
                if (log.isDebugEnabled()) {
                    log.debug("found removed attributes {}", removedAttributes);
                }
                wasResolverRefreshed = doRemoveAttributes(path, removedAttributes, false, wasResolverRefreshed);
            }
        }
    }

    // ---------- internal
   
    private boolean isValidVanityPath(Resource resource){
        // ignore system tree
        if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
            log.debug("isValidVanityPath: not valid {}", resource);
            return false;
        }

        // check white list
        if ( this.vanityPathConfig != null ) {
            boolean allowed = false;
            for(final VanityPathConfig config : this.vanityPathConfig) {
                if ( resource.getPath().startsWith(config.prefix) ) {
                    allowed = !config.isExclude;
                    break;
                }
            }
            if ( !allowed ) {
                log.debug("isValidVanityPath: not valid as not in white list {}", resource);
                return false;
            }
        }
        // require properties
        final ValueMap props = resource.adaptTo(ValueMap.class);
        if (props == null) {
            log.debug("isValidVanityPath: not valid {} without properties", resource);
            return false;
        }
        return true;
    }

    private String getActualContentPath(String path){
        final String checkPath;
        if ( path.endsWith("/jcr:content") ) {
            checkPath = path.substring(0, path.length() - "/jcr:content".length());
        } else {
            checkPath = path;
        }
        return checkPath;
    }

    private String getMapEntryRedirect(MapEntry mapEntry) {
        String[] redirect = mapEntry.getRedirect();
        if (redirect.length > 1) {
            log.warn("something went wrong, please restart the bundle");
            return null;
        }

        String path = redirect[0];
        if (path.endsWith("$1")) {
            path = path.substring(0, path.length() - "$1".length());
        } else if (path.endsWith(".html")) {
            path = path.substring(0, path.length() - ".html".length());
        }

        return path;
    }

    /**
     * Send an OSGi event
     */
    private void sendChangeEvent() {
        if (this.eventAdmin != null) {
            final Event event = new Event(SlingConstants.TOPIC_RESOURCE_RESOLVER_MAPPING_CHANGED,
                            (Dictionary<?, ?>) null);
            this.eventAdmin.postEvent(event);
        }
    }

    private void loadResolverMap(final ResourceResolver resolver, final List<MapEntry> entries, final Map<String, MapEntry> mapEntries) {
        // the standard map configuration
        final Resource res = resolver.getResource(mapRoot);
        if (res != null) {
            gather(resolver, entries, mapEntries, res, "");
        }
    }

    private void gather(final ResourceResolver resolver, final List<MapEntry> entries, final Map<String, MapEntry> mapEntries,
                    final Resource parent, final String parentPath) {
        // scheme list
        final Iterator<Resource> children = parent.listChildren();
        while (children.hasNext()) {
            final Resource child = children.next();
            final ValueMap vm = ResourceUtil.getValueMap(child);

            String name = vm.get(PROP_REG_EXP, String.class);
            boolean trailingSlash = false;
            if (name == null) {
                name = child.getName().concat("/");
                trailingSlash = true;
            }

            final 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, entries, mapEntries, child, childParent);
            }

            // add resolution entries for this node
            MapEntry childResolveEntry = null;
            try{
                childResolveEntry=MapEntry.createResolveEntry(childPath, child, trailingSlash);
            }catch (IllegalArgumentException iae){
                //ignore this entry
                log.debug("ignored entry due exception ",iae);
            }
            if (childResolveEntry != null) {
                entries.add(childResolveEntry);
            }

            // add map entries for this node
            final List<MapEntry> childMapEntries = MapEntry.createMapEntry(childPath, child, trailingSlash);
            if (childMapEntries != null) {
                for (final MapEntry mapEntry : childMapEntries) {
                    addMapEntry(mapEntries, mapEntry.getPattern(), mapEntry.getRedirect()[0], mapEntry.getStatus());
                }
            }

        }
    }

    /**
     * Add an entry to the resolve map.
     */
    private boolean addEntry(final Map<String, List<MapEntry>> entryMap, final String key, final MapEntry entry) {
        if (entry==null){
            return false;
        }
        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);
        return true;
    }

    /**
     * Load aliases Search for all nodes inheriting the sling:alias
     * property
     */
    private Map<String, Map<String, String>> loadAliases(final ResourceResolver resolver) {
        final Map<String, Map<String, String>> map = new ConcurrentHashMap<String, Map<String, String>>();
        final String queryString = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL";
        final Iterator<Resource> i = resolver.findResources(queryString, "sql");
        while (i.hasNext()) {
            final Resource resource = i.next();        
            loadAlias(resource, map);
        }
        return map;
    }
   
    /**
     * Load alias given a resource
     */
    private void loadAlias(final Resource resource, Map<String, Map<String, String>> map) {
        // ignore system tree
        if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
            log.debug("loadAliases: Ignoring {}", resource);
            return;
        }

        // require properties
        final ValueMap props = resource.adaptTo(ValueMap.class);
        if (props == null) {
            log.debug("loadAliases: Ignoring {} without properties", resource);
            return;
        }

        final String resourceName;
        final String parentPath;
        if (resource.getName().equals("jcr:content")) {
            final Resource containingResource = resource.getParent();
            parentPath = containingResource.getParent().getPath();
            resourceName = containingResource.getName();
        } else {
            parentPath = resource.getParent().getPath();
            resourceName = resource.getName();
        }
        Map<String, String> parentMap = map.get(parentPath);
        for (final String alias : props.get(ResourceResolverImpl.PROP_ALIAS, String[].class)) {
            if (parentMap != null && parentMap.containsKey(alias)) {
                log.warn("Encountered duplicate alias {} under parent path {}. Refusing to replace current target {} with {}.", new Object[] {
                        alias,
                        parentPath,
                        parentMap.get(alias),
                        resourceName
                });
            } else {
                // check alias
                boolean invalid = alias.equals("..") || alias.equals(".");
                if ( !invalid ) {
                    for(final char c : alias.toCharArray()) {
                        // invalid if / or # or a ?
                        if ( c == '/' || c == '#' || c == '?' ) {
                            invalid = true;
                            break;
                        }
                    }
                }
                if ( invalid ) {
                    log.warn("Encountered invalid alias {} under parent path {}. Refusing to use it.",
                            alias, parentPath);
                } else {
                    if (parentMap == null) {
                        parentMap = new HashMap<String, String>();
                        map.put(parentPath, parentMap);
                    }
                    parentMap.put(alias, resourceName);
                }
            }
        }
    }

    /**
     * Load vanity paths Search for all nodes inheriting the sling:VanityPath
     * mixin
     */
    private Map <String, List<String>> loadVanityPaths(final ResourceResolver resolver, final Map<String, List<MapEntry>> entryMap) {
        // sling:VanityPath (uppercase V) is the mixin name
        // sling:vanityPath (lowercase) is the property name
        final Map <String, List<String>> targetPaths = new ConcurrentHashMap <String, List<String>>();
        final String queryString = "SELECT sling:vanityPath, sling:redirect, sling:redirectStatus FROM sling:VanityPath WHERE sling:vanityPath IS NOT NULL";
        final Iterator<Resource> i = resolver.findResources(queryString, "sql");

        while (i.hasNext()) {
            final Resource resource = i.next();
            loadVanityPath(resource, entryMap, targetPaths);
        }

        return targetPaths;
    }

    /**
     * Load vanity path given a resource
     */
    private void loadVanityPath(final Resource resource, final Map<String, List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths) {
       
        if (!isValidVanityPath(resource)) {           
            return;
        }
       
        final ValueMap props = resource.adaptTo(ValueMap.class);
        long vanityOrder = 0;
        if (props.containsKey(PROP_VANITY_ORDER)) {
            vanityOrder = props.get(PROP_VANITY_ORDER, Long.class);
        }  

        // url is ignoring scheme and host.port and the path is
        // what is stored in the sling:vanityPath property
        final String[] pVanityPaths = props.get(PROP_VANITY_PATH, 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)
                final Resource redirectTarget;
                if (resource.getName().equals("jcr:content")) {
                    redirectTarget = resource.getParent();
                } else {
                    redirectTarget = resource;
                }
                final String redirect = redirectTarget.getPath();
                final String redirectName = redirectTarget.getName();

                // whether the target is attained by a external redirect or
                // by an internal redirect is defined by the sling:redirect
                // property
                final int status = props.get(PROP_REDIRECT_EXTERNAL, false) ? props.get(
                        PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, factory.getDefaultVanityPathRedirectStatus())
                        : -1;

                final String checkPath = result[1];

                boolean addedEntry;
                if (redirectName.indexOf('.') > -1) {
                    // 1. entry with exact match
                    this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect));

                    final int idx = redirectName.lastIndexOf('.');
                    final String extension = redirectName.substring(idx + 1);

                    // 2. entry with extension
                    addedEntry = this.addEntry(entryMap, checkPath, getMapEntry(url + "\\." + extension, status, false, vanityOrder, redirect));
                } else {
                    // 1. entry with exact match
                    this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect + ".html"));

                    // 2. entry with match supporting selectors and extension
                    addedEntry = this.addEntry(entryMap, checkPath, getMapEntry(url + "(\\..*)", status, false, vanityOrder, redirect + "$1"));
                }
                if (addedEntry) {
                    // 3. keep the path to return
                    this.updateTargetPaths(targetPaths, redirect, checkPath);
                }
            }
        }
    }
   
    private void updateTargetPaths(final Map<String, List<String>> targetPaths, final String key, final String entry) {
        if (entry == null) {
           return;
        }
        List<String> entries = targetPaths.get(key);
        if (entries == null) {
            entries = new ArrayList<String>();
            targetPaths.put(key, entries);
        }
        entries.add(entry);
    }
   
    /**
     * Create the vanity path definition. String array containing:
     * {protocol}/{host}[.port] {absolute path}
     */
    private String[] getVanityPathDefinition(final String pVanityPath) {
        String[] result = null;
        if (pVanityPath != null) {
            final String info = pVanityPath.trim();
            if (info.length() > 0) {
                String prefix = null;
                String path = null;
                // check for url
                if (info.indexOf(":/") > -1) {
                    try {
                        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);
                    }
                } else {
                    prefix = "^" + ANY_SCHEME_HOST;
                    if (!info.startsWith("/")) {
                        path = "/" + info;
                    } else {
                        path = info;
                    }
                }

                // remove extension
                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 = new String[] { prefix, path };
                }
            }
        }
        return result;
    }

    private void loadConfiguration(final MapConfigurationProvider factory, final List<MapEntry> entries) {
        // virtual uris
        final Map<?, ?> virtuals = factory.getVirtualURLMap();
        if (virtuals != null) {
            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 !!
                    final String url = "^" + ANY_SCHEME_HOST + extPath + "$";
                    final String redirect = intPath;
                    MapEntry mapEntry = getMapEntry(url, -1, false, redirect);
                    if (mapEntry!=null){
                        entries.add(mapEntry);
                    }
                }
            }
        }

        // URL Mappings
        final Mapping[] mappings = factory.getMappings();
        if (mappings != null) {
            final Map<String, List<String>> map = new HashMap<String, List<String>>();
            for (final Mapping mapping : mappings) {
                if (mapping.mapsInbound()) {
                    final String url = mapping.getTo();
                    final 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 (final Entry<String, List<String>> entry : map.entrySet()) {
                MapEntry mapEntry = getMapEntry(ANY_SCHEME_HOST + entry.getKey(), -1, false, entry.getValue().toArray(new String[0]));
                if (mapEntry!=null){
                    entries.add(mapEntry);
                }
            }
        }
    }

    private void loadMapConfiguration(final MapConfigurationProvider factory, final Map<String, MapEntry> entries) {
        // URL Mappings
        final Mapping[] mappings = factory.getMappings();
        if (mappings != null) {
            for (int i = mappings.length - 1; i >= 0; i--) {
                final Mapping mapping = mappings[i];
                if (mapping.mapsOutbound()) {
                    final String url = mapping.getTo();
                    final String alias = mapping.getFrom();
                    if (!url.equals(alias)) {
                        addMapEntry(entries, alias, url, -1);
                    }
                }
            }
        }

        // virtual uris
        final Map<?, ?> virtuals = factory.getVirtualURLMap();
        if (virtuals != null) {
            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 !!
                    final String path = "^" + intPath + "$";
                    final String url = extPath;
                    addMapEntry(entries, path, url, -1);
                }
            }
        }
    }

    private void addMapEntry(final Map<String, MapEntry> entries, final String path, final String url, final int status) {
        MapEntry entry = entries.get(path);
        if (entry == null) {
            entry = getMapEntry(path, status, false, url);
        } else {
            final String[] redir = entry.getRedirect();
            final String[] newRedir = new String[redir.length + 1];
            System.arraycopy(redir, 0, newRedir, 0, redir.length);
            newRedir[redir.length] = url;
            entry = getMapEntry(entry.getPattern(), entry.getStatus(), false, newRedir);
        }
        if (entry!=null){
            entries.put(path, entry);
        }
    }

    /**
     * Returns a filter which matches if any of the nodeProps (JCR properties
     * modified) is listed in any of the eventProps (event properties listing
     * modified JCR properties) this allows to only get events interesting for
     * updating the internal structure
     */
    private static String createFilter(final boolean vanityPathEnabled) {
        final String[] nodeProps = {
                        PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, PROP_REDIRECT_EXTERNAL,
                        ResourceResolverImpl.PROP_REDIRECT_INTERNAL, PROP_REDIRECT_EXTERNAL_STATUS,
                        PROP_REG_EXP, ResourceResolverImpl.PROP_ALIAS };
        final String[] eventProps = { SlingConstants.PROPERTY_ADDED_ATTRIBUTES, SlingConstants.PROPERTY_CHANGED_ATTRIBUTES, SlingConstants.PROPERTY_REMOVED_ATTRIBUTES };
        final StringBuilder filter = new StringBuilder();
        filter.append("(|");
        for (final String eventProp : eventProps) {
            filter.append("(|");
            if vanityPathEnabled ) {
                filter.append('(').append(eventProp).append('=').append(PROP_VANITY_PATH).append(')');
                filter.append('(').append(eventProp).append('=').append(PROP_VANITY_ORDER).append(')');
            }
            for (final String nodeProp : nodeProps) {
                filter.append('(').append(eventProp).append('=').append(nodeProp).append(')');
            }
            filter.append(")");
        }
        filter.append("(").append(EventConstants.EVENT_TOPIC).append("=").append(SlingConstants.TOPIC_RESOURCE_REMOVED).append(")");
        filter.append("(").append(EventConstants.EVENT_TOPIC).append("=").append(SlingConstants.TOPIC_RESOURCE_ADDED).append(")");
        filter.append(")");

        return filter.toString();
    }

    private static final class MapEntryIterator implements Iterator<MapEntry> {

        private final Map<String, List<MapEntry>> resolveMapsMap;

        private String key;

        private MapEntry next;

        private final Iterator<MapEntry> globalListIterator;
        private MapEntry nextGlobal;

        private Iterator<MapEntry> specialIterator;
        private MapEntry nextSpecial;
       
        private boolean vanityPathPrecedence;

        public MapEntryIterator(final String startKey, final Map<String, List<MapEntry>> resolveMapsMap, final boolean vanityPathPrecedence) {
            this.key = startKey;
            this.resolveMapsMap = resolveMapsMap;
            this.globalListIterator = this.resolveMapsMap.get(GLOBAL_LIST_KEY).iterator();
            this.vanityPathPrecedence = vanityPathPrecedence;
            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.vanityPathPrecedence){
                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;
                }               
            } else {
                this.next = this.nextSpecial;
                this.nextSpecial = null;
            }
        }
    };

    private MapEntry getMapEntry(String url, final int status, final boolean trailingSlash,
            final String... redirect){

        MapEntry mapEntry = null;
        try{
            mapEntry = new MapEntry(url, status, trailingSlash, 0, redirect);
        }catch (IllegalArgumentException iae){
            //ignore this entry
            log.debug("ignored entry due exception ",iae);
        }
        return mapEntry;
    }
   
    private MapEntry getMapEntry(String url, final int status, final boolean trailingSlash, long order,
            final String... redirect){

        MapEntry mapEntry = null;
        try{
            mapEntry = new MapEntry(url, status, trailingSlash, order, redirect);
        }catch (IllegalArgumentException iae){
            //ignore this entry
            log.debug("ignored entry due exception ",iae);
        }
        return mapEntry;
    }

}
TOP

Related Classes of org.apache.sling.resourceresolver.impl.mapping.MapEntries

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.