blob: f8f41b80604869a54e4ba0e95265c7ee20f405d1 [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.resourceresolver.impl.providers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.api.resource.path.PathSet;
import org.apache.sling.api.resource.runtime.dto.AuthType;
import org.apache.sling.api.resource.runtime.dto.FailureReason;
import org.apache.sling.api.resource.runtime.dto.ResourceProviderDTO;
import org.apache.sling.api.resource.runtime.dto.ResourceProviderFailureDTO;
import org.apache.sling.api.resource.runtime.dto.RuntimeDTO;
import org.apache.sling.resourceresolver.impl.legacy.LegacyResourceProviderWhiteboard;
import org.apache.sling.spi.resource.provider.ObservationReporter;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service keeps track of all resource providers.
*/
public class ResourceProviderTracker implements ResourceProviderStorageProvider {
public interface ObservationReporterGenerator {
ObservationReporter create(final Path path, final PathSet excludes);
ObservationReporter createProviderReporter();
}
public interface ChangeListener {
void providerAdded();
void providerRemoved(String name, String pid, boolean stateful, boolean used);
}
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@SuppressWarnings("rawtypes")
private final Map<ServiceReference<ResourceProvider>, ResourceProviderInfo> infos = new ConcurrentHashMap<>();
private volatile BundleContext bundleContext;
@SuppressWarnings("rawtypes")
private volatile ServiceTracker<ResourceProvider, ServiceReference<ResourceProvider>> tracker;
private final Map<String, List<ResourceProviderHandler>> handlers = new HashMap<>();
private final Map<ResourceProviderInfo, FailureReason> invalidProviders = new ConcurrentHashMap<>();
private volatile EventAdmin eventAdmin;
private volatile ObservationReporterGenerator reporterGenerator;
private volatile ResourceProviderStorage storage;
private volatile ObservationReporter providerReporter;
private volatile ChangeListener listener;
@SuppressWarnings("rawtypes")
public void activate(final BundleContext bundleContext, final EventAdmin eventAdmin, final ChangeListener listener) {
this.bundleContext = bundleContext;
this.eventAdmin = eventAdmin;
this.listener = listener;
this.tracker = new ServiceTracker<>(bundleContext,
ResourceProvider.class,
new ServiceTrackerCustomizer<ResourceProvider, ServiceReference<ResourceProvider>>() {
@Override
public void removedService(final ServiceReference<ResourceProvider> reference, final ServiceReference<ResourceProvider> ref) {
final ResourceProviderInfo info = infos.remove(ref);
if ( info != null ) {
Object pid = ref.getProperty(Constants.SERVICE_PID);
if ( pid != null && !(pid instanceof String) ) {
pid = null;
}
unregister(info, (String)pid);
}
}
@Override
public void modifiedService(final ServiceReference<ResourceProvider> reference, final ServiceReference<ResourceProvider> service) {
removedService(reference, service);
addingService(reference);
}
@Override
public ServiceReference<ResourceProvider> addingService(final ServiceReference<ResourceProvider> reference) {
final ResourceProviderInfo info = new ResourceProviderInfo(reference);
infos.put(reference, info);
register(info);
return reference;
}
});
this.tracker.open();
}
public void deactivate() {
this.listener = null;
this.eventAdmin = null;
this.providerReporter = null;
if ( this.tracker != null ) {
this.tracker.close();
this.tracker = null;
}
this.infos.clear();
this.handlers.clear();
this.invalidProviders.clear();
}
public void setObservationReporterGenerator(final ObservationReporterGenerator generator) {
this.providerReporter = generator.createProviderReporter();
synchronized ( this.handlers ) {
this.reporterGenerator = generator;
this.updateHandlers();
}
}
/**
* Try to register a new resource provider.
* @param info The resource provider info.
*/
private void register(final ResourceProviderInfo info) {
if ( info.isValid() ) {
logger.debug("Registering new resource provider {}", info);
final List<ProviderEvent> events = new ArrayList<>();
boolean providerAdded = false;
ResourceProviderHandler deactivateHandler = null;
ResourceProviderHandler activate = null;
synchronized ( this.handlers ) {
List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers == null ) {
matchingHandlers = new ArrayList<>();
this.handlers.put(info.getPath(), matchingHandlers);
}
final ResourceProviderHandler handler = new ResourceProviderHandler(bundleContext, info);
matchingHandlers.add(handler);
Collections.sort(matchingHandlers);
if ( matchingHandlers.get(0) == handler ) {
activate = handler;
}
}
if ( activate != null ) {
if ( this.activate(activate) ) {
providerAdded = true;
events.add(new ProviderEvent(true, info));
synchronized ( this.handlers ) {
storage = null;
final List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers != null && matchingHandlers.size() > 1 ) {
deactivateHandler = matchingHandlers.get(1);
}
}
} else {
synchronized ( this.handlers ) {
final List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers != null && !matchingHandlers.isEmpty() && matchingHandlers.remove(activate) ) {
storage = null;
if ( matchingHandlers.isEmpty() ) {
this.handlers.remove(info.getPath());
}
}
}
}
}
final ChangeListener cl = this.listener;
if ( providerAdded && cl != null ) {
cl.providerAdded();
}
// we have to check for deactivated handlers
if ( deactivateHandler != null ) {
final ResourceProviderInfo handlerInfo = deactivateHandler.getInfo();
if ( cl != null ) {
Object pid = handlerInfo.getServiceReference().getProperty(Constants.SERVICE_PID);
if ( pid != null && !(pid instanceof String) ) {
pid = null;
}
cl.providerRemoved(handlerInfo.getName(), (String)pid,
handlerInfo.getAuthType() != AuthType.no,
deactivateHandler.isUsed());
}
this.deactivate(deactivateHandler);
synchronized ( this.handlers ) {
storage = null;
}
events.add(new ProviderEvent(false, handlerInfo));
}
this.postEvents(events);
} else {
logger.warn("Ignoring invalid resource provider {}", info);
this.invalidProviders.put(info, FailureReason.invalid);
}
}
/**
* Unregister a resource provider.
* @param info The resource provider info.
*/
private void unregister(final ResourceProviderInfo info, final String pid) {
final boolean isInvalid;
synchronized ( this.invalidProviders ) {
isInvalid = this.invalidProviders.remove(info) != null;
}
if ( !isInvalid ) {
logger.debug("Unregistering resource provider {}", info);
// remove provider from handlers and if the provider is active (first handler)
// keep the reference for deactivation
ResourceProviderHandler deactivateHandler = null;
synchronized (this.handlers) {
final List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers != null ) {
final Iterator<ResourceProviderHandler> it = matchingHandlers.iterator();
boolean first = true;
while (it.hasNext()) {
final ResourceProviderHandler h = it.next();
if (h.getInfo() == info) {
it.remove();
if ( first ) {
deactivateHandler = h;
} else {
h.dispose();
}
if (matchingHandlers.isEmpty()) {
this.handlers.remove(info.getPath());
}
storage = null;
break;
}
first = false;
}
}
}
if ( deactivateHandler != null ) {
final List<ProviderEvent> events = new ArrayList<>();
final ChangeListener cl = this.listener;
if ( cl != null ) {
cl.providerRemoved(info.getName(), pid,
info.getAuthType() != AuthType.no,
deactivateHandler.isUsed());
}
this.deactivate(deactivateHandler);
deactivateHandler.dispose();
// check if we can activate another handler
ResourceProviderHandler addingProvider = null;
synchronized ( this.handlers ) {
final List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers != null ) {
if ( matchingHandlers.isEmpty() ) {
this.handlers.remove(info.getPath());
} else {
addingProvider = matchingHandlers.get(0);
}
}
}
boolean providerAdded = false;
while ( addingProvider != null ) {
if (this.activate(addingProvider)) {
events.add(new ProviderEvent(true, addingProvider.getInfo()));
providerAdded = true;
addingProvider = null;
synchronized ( this.handlers ) {
this.storage = null;
}
} else {
synchronized ( this.handlers ) {
final List<ResourceProviderHandler> matchingHandlers = this.handlers.get(info.getPath());
if ( matchingHandlers != null && !matchingHandlers.isEmpty() ) {
if ( matchingHandlers.get(0) == addingProvider ) {
this.storage = null;
matchingHandlers.remove(0);
addingProvider.dispose();
if ( matchingHandlers.isEmpty() ) {
this.handlers.remove(info.getPath());
addingProvider = null;
} else {
addingProvider = matchingHandlers.get(0);
}
}
}
}
}
}
if ( providerAdded && cl != null ) {
if ( cl != null ) {
cl.providerAdded();
}
}
events.add(new ProviderEvent(false,info));
this.postEvents(events);
}
} else {
logger.debug("Unregistering invalid resource provider {}", info);
}
}
/**
* Activate a resource provider
* @param handler The provider handler
*/
private boolean activate(final ResourceProviderHandler handler) {
synchronized (this.handlers) {
updateHandlers();
}
if ( !handler.activate() ) {
logger.warn("Activating resource provider {} failed", handler.getInfo());
this.invalidProviders.put(handler.getInfo(), FailureReason.service_not_gettable);
return false;
}
logger.debug("Activated resource provider {}", handler.getInfo());
return true;
}
/**
* Deactivate a resource provider
* @param handler The provider handler
*/
private void deactivate(final ResourceProviderHandler handler) {
handler.deactivate();
synchronized (this.handlers) {
updateHandlers();
}
logger.debug("Deactivated resource provider {}", handler.getInfo());
}
/**
* Post a change event through the event admin
* @param event
*/
private void postOSGiEvent(final ProviderEvent event) {
final EventAdmin ea = this.eventAdmin;
if ( ea != null ) {
final Dictionary<String, Object> eventProps = new Hashtable<>();
eventProps.put(SlingConstants.PROPERTY_PATH, event.path);
if (event.pid != null) {
eventProps.put(Constants.SERVICE_PID, event.pid);
}
ea.postEvent(new Event(event.isAdd ? SlingConstants.TOPIC_RESOURCE_PROVIDER_ADDED : SlingConstants.TOPIC_RESOURCE_PROVIDER_REMOVED,
eventProps));
}
}
/**
* Post a change event for a resource provider change
* @param type The change type
* @param info The resource provider
*/
private void postResourceProviderChange(final ProviderEvent event) {
final ObservationReporter or = this.providerReporter;
if ( or != null ) {
final ResourceChange change = new ResourceChange(event.isAdd ? ChangeType.PROVIDER_ADDED : ChangeType.PROVIDER_REMOVED,
event.path, false);
or.reportChanges(Collections.singletonList(change), false);
}
}
public void fill(final RuntimeDTO dto) {
final List<ResourceProviderDTO> dtos = new ArrayList<>();
final List<ResourceProviderFailureDTO> failures = new ArrayList<>();
synchronized ( this.handlers ) {
for(final List<ResourceProviderHandler> handlers : this.handlers.values()) {
boolean isFirst = true;
for(final ResourceProviderHandler h : handlers) {
final ResourceProviderDTO d;
if ( isFirst ) {
d = new ResourceProviderDTO();
dtos.add(d);
isFirst = false;
} else {
d = new ResourceProviderFailureDTO();
((ResourceProviderFailureDTO)d).reason = FailureReason.shadowed;
failures.add((ResourceProviderFailureDTO)d);
}
fill(d, h);
}
}
}
synchronized ( this.invalidProviders ) {
for(final Map.Entry<ResourceProviderInfo, FailureReason> entry : this.invalidProviders.entrySet()) {
final ResourceProviderFailureDTO d = new ResourceProviderFailureDTO();
fill(d, entry.getKey());
d.reason = entry.getValue();
failures.add(d);
}
}
dto.providers = dtos.toArray(new ResourceProviderDTO[dtos.size()]);
dto.failedProviders = failures.toArray(new ResourceProviderFailureDTO[failures.size()]);
}
@Override
public ResourceProviderStorage getResourceProviderStorage() {
ResourceProviderStorage result = storage;
if (result == null) {
synchronized (this.handlers) {
if (storage == null) {
final List<ResourceProviderHandler> handlerList = new ArrayList<>();
for (List<ResourceProviderHandler> list : handlers.values()) {
if ( !list.isEmpty() ) {
final ResourceProviderHandler h = list.get(0);
if (h != null && h.getResourceProvider() != null ) {
handlerList.add(h);
}
}
}
storage = new ResourceProviderStorage(handlerList);
}
result = storage;
}
}
return result;
}
private void fill(final ResourceProviderDTO d, final ResourceProviderInfo info) {
d.adaptable = info.isAdaptable();
d.attributable = info.isAttributable();
d.authType = info.getAuthType();
d.modifiable = info.isModifiable();
d.name = info.getName();
d.path = info.getPath();
d.refreshable = info.isRefreshable();
d.serviceId = (Long)info.getServiceReference().getProperty(Constants.SERVICE_ID);
d.supportsQueryLanguage = false;
d.useResourceAccessSecurity = info.getUseResourceAccessSecurity();
}
private void fill(final ResourceProviderDTO d, final ResourceProviderHandler handler) {
fill(d, handler.getInfo());
final ResourceProvider<?> provider = handler.getResourceProvider();
if ( provider != null ) {
d.supportsQueryLanguage = provider.getQueryLanguageProvider() != null;
}
}
private void updateHandlers() {
for (List<ResourceProviderHandler> list : handlers.values()) {
if ( !list.isEmpty() ) {
final ResourceProviderHandler h = list.get(0);
if (h != null) {
updateProviderContext(h);
h.update();
}
}
}
}
private void updateProviderContext(final ResourceProviderHandler handler) {
final Set<String> excludedPaths = new HashSet<>();
final Path handlerPath = new Path(handler.getPath());
for(final String otherPath : handlers.keySet()) {
if ( !handler.getPath().equals(otherPath) && handlerPath.matches(otherPath) ) {
excludedPaths.add(otherPath);
}
}
final PathSet excludedPathSet = PathSet.fromStringCollection(excludedPaths);
handler.getProviderContext().update(
reporterGenerator.create(handlerPath, excludedPathSet),
excludedPathSet);
}
private void postEvents(final List<ProviderEvent> events) {
if ( events.isEmpty() ) {
return;
}
if ( this.listener == null && this.providerReporter == null ) {
return;
}
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(final ProviderEvent e : events) {
postOSGiEvent(e);
postResourceProviderChange(e);
}
}
});
t.setName("Apache Sling Resource Provider Change Notifier");
t.setDaemon(true);
t.start();
}
private static final class ProviderEvent {
public final boolean isAdd;
public final Object pid;
public final String path;
public ProviderEvent(final boolean isAdd, final ResourceProviderInfo info) {
this.isAdd = isAdd;
this.path = info.getPath();
Object pid = info.getServiceReference().getProperty(Constants.SERVICE_PID);
if (pid == null) {
pid = info.getServiceReference().getProperty(LegacyResourceProviderWhiteboard.ORIGINAL_SERVICE_PID);
}
this.pid = pid;
}
}
}