blob: 51809ae93b9ea2ce4a8b1a3bbb9b9fe73359d5da [file] [log] [blame]
/*
* 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.Collections;
import java.util.Dictionary;
import java.util.HashMap;
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 Immutable map with all superimposing resource providers currently registered
*/
public Map<String, SuperimposingResourceProvider> getRegisteredProviders() {
Map<String, SuperimposingResourceProvider> mapcopy = new HashMap<String, SuperimposingResourceProvider>(superimposingProviders);
return Collections.unmodifiableMap(mapcopy);
}
SuperimposingManagerImpl withResourceResolverFactory(ResourceResolverFactory resolverFactory) {
this.resolverFactory = resolverFactory;
return this;
}
}