| /* |
| * 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.observation; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.sling.api.resource.observation.ResourceChange; |
| import org.apache.sling.api.resource.observation.ResourceChangeListener; |
| import org.apache.sling.api.resource.path.Path; |
| import org.apache.sling.api.resource.path.PathSet; |
| import org.apache.sling.spi.resource.provider.ObservationReporter; |
| import org.apache.sling.spi.resource.provider.ObserverConfiguration; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Implementation of the observation reporter. |
| * Each resource provider gets its on instance. |
| */ |
| public class BasicObservationReporter implements ObservationReporter { |
| |
| private Logger logger = LoggerFactory.getLogger(this.getClass()); |
| |
| /** List of observer configurations for the provider. */ |
| private final List<ObserverConfiguration> configs; |
| |
| /** The search path. */ |
| private final List<String> searchPath; |
| |
| /** |
| * Create a reporter listening for resource provider changes |
| * |
| * @param searchPath The search path |
| * @param infos The listeners map |
| */ |
| public BasicObservationReporter( |
| final List<String> searchPath, |
| final Collection<ResourceChangeListenerInfo> infos) { |
| this.searchPath = searchPath; |
| final Set<String> paths = new HashSet<>(); |
| final List<ResourceChangeListenerInfo> result = new ArrayList<>(); |
| for(final ResourceChangeListenerInfo info : infos) { |
| if ( !info.getProviderChangeTypes().isEmpty() ) { |
| for(final Path p : info.getPaths()) { |
| paths.add(p.getPath()); |
| } |
| result.add(info); |
| } |
| } |
| final BasicObserverConfiguration cfg = new BasicObserverConfiguration(PathSet.fromStringCollection(paths)); |
| for(final ResourceChangeListenerInfo i : infos) { |
| cfg.addListener(i); |
| } |
| this.configs = Collections.singletonList((ObserverConfiguration)cfg); |
| } |
| |
| /** |
| * Create a reporter listening for a provider |
| * |
| * @param searchPath The search paths |
| * @param infos The listeners map |
| * @param providerPath The mount point of the provider |
| * @param excludePaths Excluded paths for that provider |
| */ |
| public BasicObservationReporter( |
| final List<String> searchPath, |
| final Collection<ResourceChangeListenerInfo> infos, |
| final Path providerPath, |
| final PathSet excludePaths) { |
| this.searchPath = searchPath; |
| |
| final List<ObserverConfiguration> observerConfigs = new ArrayList<>(); |
| for(final ResourceChangeListenerInfo info : infos) { |
| if ( !info.getResourceChangeTypes().isEmpty() ) { |
| // find the set of paths that match the provider |
| final Set<Path> paths = new HashSet<>(); |
| for(final Path p : info.getPaths()) { |
| // add when there is an intersection between provider path and resource change listener path |
| boolean add = providerPath.matches(p.getPath()) || (!p.isPattern() && p.matches(providerPath.getPath())); |
| if ( add ) { |
| if ( p.isPattern() ) { |
| for(final Path exclude : excludePaths) { |
| if ( p.getPath().startsWith(Path.GLOB_PREFIX + exclude.getPath() + "/")) { |
| logger.debug("ResourceChangeListener {} is shadowed by {}", info, exclude); |
| add = false; |
| break; |
| } |
| } |
| } else { |
| final Path exclude = excludePaths.matches(p.getPath()); |
| if ( exclude != null ) { |
| logger.debug("ResourceChangeListener {} is shadowed by {}", info, exclude); |
| add = false; |
| } |
| } |
| } |
| if ( add ) { |
| paths.add(p); |
| } |
| } |
| if ( !paths.isEmpty() ) { |
| final PathSet pathSet = PathSet.fromPathCollection(paths); |
| // search for an existing configuration with the same paths and hints |
| BasicObserverConfiguration found = null; |
| for(final ObserverConfiguration c : observerConfigs) { |
| if ( c.getPaths().equals(pathSet) |
| && (( c.getPropertyNamesHint() == null && info.getPropertyNamesHint() == null) |
| || c.getPropertyNamesHint() != null && c.getPropertyNamesHint().equals(info.getPropertyNamesHint()))) { |
| found = (BasicObserverConfiguration)c; |
| break; |
| } |
| } |
| final BasicObserverConfiguration config; |
| if ( found != null ) { |
| // check external and types |
| boolean createNew = false; |
| if ( !found.includeExternal() && info.isExternal() ) { |
| createNew = true; |
| } |
| if ( !found.getChangeTypes().equals(info.getResourceChangeTypes()) ) { |
| createNew = true; |
| } |
| if ( createNew ) { |
| // create new/updated config |
| observerConfigs.remove(found); |
| final Set<ResourceChange.ChangeType> types = new HashSet<>(); |
| types.addAll(found.getChangeTypes()); |
| types.addAll(info.getResourceChangeTypes()); |
| config = new BasicObserverConfiguration(pathSet, |
| types, |
| info.isExternal() || found.includeExternal(), |
| found.getExcludedPaths(), |
| found.getPropertyNamesHint()); |
| observerConfigs.add(config); |
| for(final ResourceChangeListenerInfo i : found.getListeners()) { |
| config.addListener(i); |
| } |
| |
| } else { |
| config = found; |
| } |
| } else { |
| // create new config |
| config = new BasicObserverConfiguration(pathSet, |
| info.getResourceChangeTypes(), |
| info.isExternal(), |
| excludePaths.getSubset(pathSet), |
| info.getPropertyNamesHint()); |
| observerConfigs.add(config); |
| } |
| config.addListener(info); |
| } |
| } |
| } |
| this.configs = Collections.unmodifiableList(observerConfigs); |
| } |
| |
| @Override |
| public List<ObserverConfiguration> getObserverConfigurations() { |
| return configs; |
| } |
| |
| @Override |
| public void reportChanges(final Iterable<ResourceChange> changes, final boolean distribute) { |
| for(final ObserverConfiguration cfg : this.configs) { |
| final List<ResourceChange> filteredChanges = filterChanges(changes, cfg); |
| if (!filteredChanges.isEmpty() ) { |
| this.reportChanges(cfg, filteredChanges, distribute); |
| } |
| } |
| } |
| |
| @Override |
| public void reportChanges(final ObserverConfiguration config, |
| final Iterable<ResourceChange> changes, |
| final boolean distribute) { |
| if ( config != null && config instanceof BasicObserverConfiguration ) { |
| final BasicObserverConfiguration observerConfig = (BasicObserverConfiguration)config; |
| |
| ResourceChangeListenerInfo previousInfo = null; |
| List<ResourceChange> filteredChanges = null; |
| for(final ResourceChangeListenerInfo info : observerConfig.getListeners()) { |
| if ( previousInfo == null || !equals(previousInfo, info) ) { |
| filteredChanges = filterChanges(changes, info); |
| previousInfo = info; |
| } |
| if ( !filteredChanges.isEmpty() ) { |
| final ResourceChangeListener listener = info.getListener(); |
| if ( listener != null ) { |
| listener.onChange(filteredChanges); |
| } |
| } |
| } |
| // TODO implement distribute |
| if ( distribute ) { |
| logger.error("Distrubte flag is send for observation events, however distribute is currently not implemented!"); |
| } |
| } |
| } |
| |
| /** |
| * Test if two resource change listener infos are equal wrt external and change types |
| * @param infoA First info |
| * @param infoB Second info |
| * @return {@code true} if external and change types are equally configured |
| */ |
| private boolean equals(final ResourceChangeListenerInfo infoA, final ResourceChangeListenerInfo infoB) { |
| if ( infoA.isExternal() && !infoB.isExternal() ) { |
| return false; |
| } |
| if ( !infoA.isExternal() && infoB.isExternal() ) { |
| return false; |
| } |
| return infoA.getResourceChangeTypes().equals(infoB.getResourceChangeTypes()) |
| && infoA.getProviderChangeTypes().equals(infoB.getProviderChangeTypes()); |
| } |
| |
| /** |
| * Filter the change list based on the configuration |
| * @param changes The list of changes |
| * @param config The configuration |
| * @return The filtered list. |
| */ |
| private List<ResourceChange> filterChanges(final Iterable<ResourceChange> changes, final ObserverConfiguration config) { |
| final ResourceChangeListImpl filtered = new ResourceChangeListImpl(this.searchPath); |
| for (final ResourceChange c : changes) { |
| if (matches(c, config)) { |
| filtered.add(c); |
| } |
| } |
| filtered.lock(); |
| return filtered; |
| } |
| |
| /** |
| * Filter the change list based on the resource change listener, only type and external needs to be checkd. |
| * @param changes The list of changes |
| * @param config The resource change listener info |
| * @return The filtered list. |
| */ |
| private List<ResourceChange> filterChanges(final Iterable<ResourceChange> changes, final ResourceChangeListenerInfo config) { |
| final ResourceChangeListImpl filtered = new ResourceChangeListImpl(this.searchPath); |
| for (final ResourceChange c : changes) { |
| if (matches(c, config)) { |
| filtered.add(c); |
| } |
| } |
| filtered.lock(); |
| return filtered; |
| } |
| |
| /** |
| * Match a change against the configuration |
| * @param change The change |
| * @param config The configuration |
| * @return {@code true} whether it matches |
| */ |
| private boolean matches(final ResourceChange change, final ObserverConfiguration config) { |
| if (!config.getChangeTypes().contains(change.getType())) { |
| return false; |
| } |
| if (!config.includeExternal() && change.isExternal()) { |
| return false; |
| } |
| if (config.getPaths().matches(change.getPath()) == null ) { |
| return false; |
| } |
| if ( config.getExcludedPaths().matches(change.getPath()) != null ) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Match a change against the configuration |
| * @param change The change |
| * @param config The configuration |
| * @return {@code true} whether it matches |
| */ |
| private boolean matches(final ResourceChange change, final ResourceChangeListenerInfo config) { |
| if (!config.getResourceChangeTypes().contains(change.getType()) && !config.getProviderChangeTypes().contains(change.getType())) { |
| return false; |
| } |
| if (!config.isExternal() && change.isExternal()) { |
| return false; |
| } |
| return true; |
| } |
| } |