org.apache.sling.superimposing.impl.SuperimposingManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.superimposing.impl.SuperimposingManagerImpl.java

Source

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

import static org.apache.sling.superimposing.SuperimposingResourceProvider.MIXIN_SUPERIMPOSE;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_OVERLAYABLE;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_REGISTER_PARENT;
import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH;

import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
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.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.superimposing.SuperimposingManager;
import org.apache.sling.superimposing.SuperimposingResourceProvider;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manages the resource registrations for the {@link SuperimposingResourceProviderImpl}.
 * Provides read-only access to all registered providers.
 */
@Component(label = "Apache Sling Superimposing Resource Manager", description = "Manages the resource registrations for the Superimposing Resource Provider.", immediate = true, metatype = true)
@Service(SuperimposingManager.class)
public class SuperimposingManagerImpl implements SuperimposingManager, EventListener {

    @Property(label = "Enabled", description = "Enable/Disable the superimposing functionality.", boolValue = SuperimposingManagerImpl.ENABLED_DEFAULT)
    static final String ENABLED_PROPERTY = "enabled";
    static final boolean ENABLED_DEFAULT = false;
    private boolean enabled;

    @Property(label = "Find all Queries", description = "List of query expressions to find all existing superimposing registrations on service startup. "
            + "Query syntax is depending on underlying resource provdider implementation. Prepend the query with query syntax name separated by \"|\".", value = {
                    SuperimposingManagerImpl.FINDALLQUERIES_DEFAULT }, unbounded = PropertyUnbounded.ARRAY)
    static final String FINDALLQUERIES_PROPERTY = "findAllQueries";
    static final String FINDALLQUERIES_DEFAULT = "JCR-SQL2|SELECT * FROM [" + MIXIN_SUPERIMPOSE
            + "] WHERE ISDESCENDANTNODE('/content')";
    private String[] findAllQueries;

    @Property(label = "Obervation paths", description = "List of paths that should be monitored for resource events to detect superimposing content nodes.", value = {
            SuperimposingManagerImpl.OBSERVATION_PATHS_DEFAULT }, unbounded = PropertyUnbounded.ARRAY)
    static final String OBSERVATION_PATHS_PROPERTY = "obervationPaths";
    static final String OBSERVATION_PATHS_DEFAULT = "/content";
    private String[] obervationPaths;
    private EventListener[] observationEventListeners;

    /**
     * Map for holding the superimposing mappings, with the superimpose path as key and the providers as values
     */
    private ConcurrentMap<String, SuperimposingResourceProviderImpl> superimposingProviders = new ConcurrentHashMap<String, SuperimposingResourceProviderImpl>();

    @Reference
    private ResourceResolverFactory resolverFactory;

    /**
     * Administrative resource resolver (read only usage)
     */
    private ResourceResolver resolver;

    /**
     * A reference to the initialization task. Needed to check if
     * initialization has completed.
     */
    Future<?> initialization;

    /**
     * This bundle's context.
     */
    private BundleContext bundleContext;

    /**
     * The default logger
     */
    private static final Logger log = LoggerFactory.getLogger(SuperimposingManagerImpl.class);

    /**
     * Find all existing superimposing registrations using all query defined in service configuration.
     * @param resolver Resource resolver
     * @return All superimposing registrations
     */
    @SuppressWarnings("unchecked")
    private List<Resource> findSuperimposings(ResourceResolver resolver) {
        List<Resource> allResources = new ArrayList<Resource>();
        for (String queryString : this.findAllQueries) {
            if (!StringUtils.contains(queryString, "|")) {
                throw new IllegalArgumentException(
                        "Query string does not contain query syntax seperated by '|': " + queryString);
            }
            String queryLanguage = StringUtils.substringBefore(queryString, "|");
            String query = StringUtils.substringAfter(queryString, "|");
            allResources.addAll(IteratorUtils.toList(resolver.findResources(query, queryLanguage)));
        }
        return allResources;
    }

    private void registerAllSuperimposings() {
        log.debug("Start registering all superimposing trees...");
        final long start = System.currentTimeMillis();
        long countSuccess = 0;
        long countFailed = 0;

        final List<Resource> existingSuperimposings = findSuperimposings(resolver);
        for (Resource superimposingResource : existingSuperimposings) {
            boolean success = registerProvider(superimposingResource);
            if (success) {
                countSuccess++;
            } else {
                countFailed++;
            }
        }

        final long time = System.currentTimeMillis() - start;
        log.info("Registered {} SuperimposingResourceProvider(s) in {} ms, skipping {} invalid one(s).",
                new Object[] { countSuccess, time, countFailed });
    }

    /**
     * @param superimposingResource
     * @return true if registration was done, false if skipped (already registered)
     * @throws RepositoryException
     */
    private boolean registerProvider(Resource superimposingResource) {
        String superimposePath = superimposingResource.getPath();

        // use JCR API to get properties from superimposing resource to make sure superimposing does not delivery values from source node
        final String sourcePath = getJcrStringProperty(superimposePath, PROP_SUPERIMPOSE_SOURCE_PATH);
        final boolean registerParent = getJcrBooleanProperty(superimposePath, PROP_SUPERIMPOSE_REGISTER_PARENT);
        final boolean overlayable = getJcrBooleanProperty(superimposePath, PROP_SUPERIMPOSE_OVERLAYABLE);

        // check if superimposing definition is valid
        boolean valid = true;
        if (StringUtils.isBlank(sourcePath)) {
            valid = false;
        } else {
            // check whether the parent of the node should be registered as superimposing provider
            if (registerParent) {
                superimposePath = ResourceUtil.getParent(superimposePath);
            }
            // target path is not valid if it equals to a parent or child of the superimposing path, or to the superimposing path itself
            if (StringUtils.equals(sourcePath, superimposePath)
                    || StringUtils.startsWith(sourcePath, superimposePath + "/")
                    || StringUtils.startsWith(superimposePath, sourcePath + "/")) {
                valid = false;
            }
        }

        // register valid superimposing
        if (valid) {
            final SuperimposingResourceProviderImpl srp = new SuperimposingResourceProviderImpl(superimposePath,
                    sourcePath, overlayable);
            final SuperimposingResourceProviderImpl oldSrp = superimposingProviders.put(superimposePath, srp);

            // unregister in case there was a provider registered before
            if (!srp.equals(oldSrp)) {
                log.debug("(Re-)registering resource provider {}.", superimposePath);
                if (null != oldSrp) {
                    oldSrp.unregisterService();
                }
                srp.registerService(bundleContext);
                return true;
            } else {
                log.debug("Skipped re-registering resource provider {} because there were no relevant changes.",
                        superimposePath);
            }
        }

        // otherwise remove previous superimposing resource provider if new superimposing definition is not valid
        else {
            final SuperimposingResourceProviderImpl oldSrp = superimposingProviders.remove(superimposePath);
            if (null != oldSrp) {
                log.debug("Unregistering resource provider {}.", superimposePath);
                oldSrp.unregisterService();
            }
            log.warn("Superimposing definition '{}' pointing to '{}' is invalid.", superimposePath, sourcePath);
        }

        return false;
    }

    private String getJcrStringProperty(String pNodePath, String pPropertName) {
        String absolutePropertyPath = pNodePath + "/" + pPropertName;
        Session session = resolver.adaptTo(Session.class);
        try {
            if (!session.itemExists(absolutePropertyPath)) {
                return null;
            }
            return session.getProperty(absolutePropertyPath).getString();
        } catch (RepositoryException ex) {
            return null;
        }
    }

    private boolean getJcrBooleanProperty(String pNodePath, String pPropertName) {
        String absolutePropertyPath = pNodePath + "/" + pPropertName;
        Session session = resolver.adaptTo(Session.class);
        try {
            if (!session.itemExists(absolutePropertyPath)) {
                return false;
            }
            return session.getProperty(absolutePropertyPath).getBoolean();
        } catch (RepositoryException ex) {
            return false;
        }
    }

    private void registerProvider(String path) {
        final Resource provider = resolver.getResource(path);
        if (provider != null) {
            registerProvider(provider);
        }
    }

    private void unregisterProvider(String path) {
        final SuperimposingResourceProviderImpl srp = superimposingProviders.remove(path);
        if (null != srp) {
            srp.unregisterService();
        }
    }

    // ---------- SCR Integration

    @Activate
    protected synchronized void activate(final ComponentContext ctx) throws LoginException, RepositoryException {

        // check enabled state
        @SuppressWarnings("unchecked")
        final Dictionary<String, Object> props = ctx.getProperties();
        this.enabled = PropertiesUtil.toBoolean(props.get(ENABLED_PROPERTY), ENABLED_DEFAULT);
        log.debug("Config: " + "Enabled={} ", enabled);
        if (!isEnabled()) {
            return;
        }

        // get "find all" queries
        this.findAllQueries = PropertiesUtil.toStringArray(props.get(FINDALLQUERIES_PROPERTY),
                new String[] { FINDALLQUERIES_DEFAULT });
        this.obervationPaths = PropertiesUtil.toStringArray(props.get(OBSERVATION_PATHS_PROPERTY),
                new String[] { OBSERVATION_PATHS_DEFAULT });

        if (null == resolver) {
            bundleContext = ctx.getBundleContext();
            resolver = resolverFactory.getAdministrativeResourceResolver(null);

            // Watch for events on the root to register/deregister superimposings at runtime
            // For each observed path create an event listener object which redirects the event to the main class
            final Session session = resolver.adaptTo(Session.class);
            if (session != null) {
                this.observationEventListeners = new EventListener[this.obervationPaths.length];
                for (int i = 0; i < this.obervationPaths.length; i++) {
                    this.observationEventListeners[i] = new EventListener() {
                        public void onEvent(EventIterator events) {
                            SuperimposingManagerImpl.this.onEvent(events);
                        }
                    };
                    session.getWorkspace().getObservationManager().addEventListener(
                            this.observationEventListeners[i],
                            Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED
                                    | Event.PROPERTY_REMOVED,
                            this.obervationPaths[i], // absolute path
                            true, // isDeep
                            null, // uuids
                            null, // node types
                            true); // noLocal
                }
            }

            // register all superimposing definitions that already exist
            initialization = Executors.newSingleThreadExecutor().submit(new Runnable() {
                public void run() {
                    try {
                        registerAllSuperimposings();
                    } catch (Throwable ex) {
                        log.warn("Error registering existing superimposing resources on service startup.", ex);
                    }
                }
            });
        }
    }

    @Deactivate
    protected synchronized void deactivate(final ComponentContext ctx) throws RepositoryException {
        try {
            // make sure initialization has finished
            if (null != initialization && !initialization.isDone()) {
                initialization.cancel(/* myInterruptIfRunning */ true);
            }

            // de-register JCR observation
            if (resolver != null) {
                final Session session = resolver.adaptTo(Session.class);
                if (session != null && this.observationEventListeners != null) {
                    for (EventListener eventListener : this.observationEventListeners) {
                        session.getWorkspace().getObservationManager().removeEventListener(eventListener);
                    }
                }
            }

            // de-register all superimpsing resource providers
            for (final SuperimposingResourceProviderImpl srp : superimposingProviders.values()) {
                srp.unregisterService();
            }

        } finally {
            if (null != resolver) {
                resolver.close();
                resolver = null;
            }
            initialization = null;
            superimposingProviders.clear();
        }
    }

    /**
     * Handle resource events to add or remove superimposing registrations
     */
    public void onEvent(EventIterator events) {
        if (!isEnabled()) {
            return;
        }
        try {
            // collect all actions to be performed for this event
            final Map<String, Boolean> actions = new HashMap<String, Boolean>();
            boolean nodeAdded = false;
            boolean nodeRemoved = false;
            while (events.hasNext()) {
                final Event event = events.nextEvent();
                final String path = event.getPath();
                final String name = ResourceUtil.getName(path);
                if (event.getType() == Event.NODE_ADDED) {
                    nodeAdded = true;
                } else if (event.getType() == Event.NODE_REMOVED && superimposingProviders.containsKey(path)) {
                    nodeRemoved = true;
                    actions.put(path, false);
                } else if (StringUtils.equals(name, PROP_SUPERIMPOSE_SOURCE_PATH)
                        || StringUtils.equals(name, PROP_SUPERIMPOSE_REGISTER_PARENT)
                        || StringUtils.equals(name, PROP_SUPERIMPOSE_OVERLAYABLE)) {
                    final String nodePath = ResourceUtil.getParent(path);
                    actions.put(nodePath, true);
                }
            }

            // execute all collected actions (having this outside the above
            // loop prevents repeated registrations within one transaction
            // but allows for several superimposings to be added within a single
            // transaction)
            for (Map.Entry<String, Boolean> action : actions.entrySet()) {
                if (action.getValue()) {
                    registerProvider(action.getKey());
                } else {
                    unregisterProvider(action.getKey());
                }
            }

            if (nodeAdded && nodeRemoved) {
                // maybe a superimposing was moved, re-register all superimposings
                // (existing ones will be skipped)
                registerAllSuperimposings();
            }
        } catch (RepositoryException e) {
            log.error("Unexpected repository exception during event processing.");
        }
    }

    /**
     * @return true if superimposing mode is enabled
     */
    public boolean isEnabled() {
        return this.enabled;
    }

    /**
     * @return Iterator with all superimposing resource providers currently registered.
     *   Iterator is backed by a {@link java.util.concurrent.ConcurrentHashMap} and is safe to access
     *   even if superimposing resource providers are registered or unregistered at the same time.
     */
    @SuppressWarnings("unchecked")
    public Iterator<SuperimposingResourceProvider> getRegisteredProviders() {
        return IteratorUtils.unmodifiableIterator(superimposingProviders.values().iterator());
    }

    SuperimposingManagerImpl withResourceResolverFactory(ResourceResolverFactory resolverFactory) {
        this.resolverFactory = resolverFactory;
        return this;
    }

}