| /* |
| * 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; |
| for (List<ResourceProviderHandler> list : handlers.values()) { |
| final ResourceProviderHandler h = list.get(0); |
| if (h != null) { |
| updateProviderContext(h); |
| h.update(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| } |
| } |
| |
| 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) { |
| updateProviderContext(handler); |
| } |
| 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(); |
| 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 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; |
| } |
| } |
| } |