blob: 40f7ec82b7382fb72dad62cf5cd6c437b249065c [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.jackrabbit.oak.plugins.observation.filter;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static javax.jcr.observation.Event.NODE_ADDED;
import static javax.jcr.observation.Event.NODE_MOVED;
import static javax.jcr.observation.Event.NODE_REMOVED;
import static javax.jcr.observation.Event.PERSIST;
import static javax.jcr.observation.Event.PROPERTY_ADDED;
import static javax.jcr.observation.Event.PROPERTY_CHANGED;
import static javax.jcr.observation.Event.PROPERTY_REMOVED;
import static org.apache.jackrabbit.oak.commons.PathUtils.isAncestor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
import org.apache.jackrabbit.oak.plugins.observation.filter.UniversalFilter.Selector;
import org.apache.jackrabbit.oak.plugins.tree.factories.RootFactory;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.observation.ChangeSet;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Builder for {@link FilterProvider} instances.
*/
public final class FilterBuilder {
private static final int ALL_EVENTS = NODE_ADDED | NODE_REMOVED | NODE_MOVED | PROPERTY_ADDED |
PROPERTY_REMOVED | PROPERTY_CHANGED | PERSIST;
private boolean includeSessionLocal;
private boolean includeClusterExternal;
private boolean includeClusterLocal = true;
private final List<String> subTrees = newArrayList();
private final Set<String> pathsForMBean = newHashSet();
private Condition condition = includeAll();
private ChangeSetFilter changeSetFilter = new ChangeSetFilter() {
@Override
public boolean excludes(ChangeSet changeSet) {
return false;
}
};
private EventAggregator aggregator;
public interface Condition {
@NotNull
EventFilter createFilter(@NotNull NodeState before, @NotNull NodeState after);
}
@NotNull
public FilterBuilder setChangeSetFilter(@NotNull ChangeSetFilter changeSetFilter) {
this.changeSetFilter = changeSetFilter;
return this;
}
/**
* Adds a path to the set of paths whose subtrees include all events of
* this filter. Does nothing if the paths is already covered by an
* path added earlier.
* <p>
* This is used for optimisation in order to restrict traversal to
* these sub trees.
*
* @param absPath absolute path
* @return this instance
*/
@NotNull
public FilterBuilder addSubTree(@NotNull String absPath) {
if (absPath.endsWith("/")) {
absPath = absPath.substring(0, absPath.length() - 1);
}
for (String path : subTrees) {
if (path.equals(absPath) || isAncestor(path, absPath)) {
return this;
}
}
subTrees.add(checkNotNull(absPath));
return this;
}
/**
* Adds paths to the FilterConfigMBean's getPaths set
* @param paths
* @return
*/
public FilterBuilder addPathsForMBean(@NotNull Set<String> paths) {
pathsForMBean.addAll(paths);
return this;
}
/**
* A set of paths whose subtrees include all events of this filter.
* @return list of paths
* @see #addSubTree(String)
*/
@NotNull
private Iterable<String> getSubTrees() {
return subTrees.isEmpty() ? ImmutableList.of("/") : subTrees;
}
public FilterBuilder aggregator(EventAggregator aggregator) {
this.aggregator = aggregator;
return this;
}
/**
* Whether to include session local changes. Defaults to {@code false}.
* @param include if {@code true} session local changes are included,
* otherwise session local changes are not included.
* @return this instance
*/
@NotNull
public FilterBuilder includeSessionLocal(boolean include) {
this.includeSessionLocal = include;
return this;
}
/**
* Whether to include cluster external changes. Defaults to {@code false}.
* @param include if {@code true} cluster external changes are included,
* otherwise cluster external changes are not included.
* @return this instance
*/
@NotNull
public FilterBuilder includeClusterExternal(boolean include) {
this.includeClusterExternal = include;
return this;
}
/**
* Whether to include cluster local changes. Defaults to {@code true}.
* @param include if {@code true} cluster local changes are included,
* otherwise cluster local changes are not included.
* @return this instance
*/
@NotNull
public FilterBuilder includeClusterLocal(boolean include) {
this.includeClusterLocal = include;
return this;
}
/**
* Set the condition of this filter. Conditions are obtained from
* the various methods on this instance that return a {@code Condition}
* instance.
*
* @param condition the conditions to apply
* @return this instance
*/
@NotNull
public FilterBuilder condition(@NotNull Condition condition) {
this.condition = checkNotNull(condition);
return this;
}
//------------------------------------------------------------< initial conditions >---
/**
* A condition the always holds
* @return true condition
*/
@NotNull
public Condition includeAll() {
return ConstantCondition.INCLUDE_ALL;
}
/**
* A condition that never holds
* @return false condition
*/
@NotNull
public Condition excludeAll() {
return ConstantCondition.EXCLUDE_ALL;
}
/**
* A condition that hold for accessible items as determined by the passed permission
* provider.
*
* @param permissionProviderFactory permission provider for checking whether an item is accessible.
* @return access control condition
* @see ACFilter
*/
@NotNull
public Condition accessControl(@NotNull PermissionProviderFactory permissionProviderFactory) {
return new ACCondition(checkNotNull(permissionProviderFactory));
}
/**
* A condition that holds on the paths matching a certain pattern.
* @param pathPattern
* @return path condition
* @see GlobbingPathFilter
*/
@NotNull
public Condition path(@NotNull String pathPattern) {
return new PathCondition(checkNotNull(pathPattern));
}
/**
* A condition that holds for matching event types.
* @param eventTypes
* @return event type condition
* @see EventTypeFilter
*/
@NotNull
public Condition eventType(int eventTypes) {
if ((ALL_EVENTS & eventTypes) == 0) {
return excludeAll();
} else if ((ALL_EVENTS & eventTypes) != ALL_EVENTS) {
return new EventTypeCondition(eventTypes);
} else {
return includeAll();
}
}
/**
* A condition that holds for matching node types.
* @param selector selector selecting the node to check the condition on
* @param ntNames node type names to match. This conditions never matches if {@code null} and
* always matches if empty.
* @return node type condition
*/
@NotNull
public Condition nodeType(@NotNull Selector selector, @Nullable String[] ntNames) {
if (ntNames == null) {
return includeAll();
} else if (ntNames.length == 0) {
return excludeAll();
} else {
return new NodeTypeCondition(checkNotNull(selector), ntNames);
}
}
/**
* A condition that holds for matching uuids.
* @param selector selector selecting the node to check the condition on
* @param uuids uuids to match. This conditions never matches if {@code null} and
* always matches if empty.
* @return node type condition
*/
@NotNull
public Condition uuid(@NotNull Selector selector, @Nullable String[] uuids) {
if (uuids == null) {
return includeAll();
} else if (uuids.length == 0) {
return excludeAll();
} else {
return new UniversalCondition(checkNotNull(selector), new UuidPredicate(uuids));
}
}
/**
* A condition that holds when the property predicate matches.
* @param selector selector selecting the node to check the condition on
* @param name the name of the property to check the predicate on
* @param predicate the predicate to check on the named property
* @return property condition
*/
@NotNull
public Condition property(@NotNull Selector selector, @NotNull String name,
@NotNull Predicate<PropertyState> predicate) {
return new UniversalCondition(
checkNotNull(selector),
new PropertyPredicate(checkNotNull(name), checkNotNull(predicate)));
}
/**
* A condition that holds when the predicate matches.
* @param selector selector selecting the node to check the condition on
* @param predicate the predicate to check on the selected node
* @return universal condition
*/
@NotNull
public Condition universal(@NotNull Selector selector, @NotNull Predicate<NodeState> predicate) {
return new UniversalCondition(checkNotNull(selector), checkNotNull(predicate));
}
/**
* @return a condition that holds for children of added nodes.
*/
@NotNull
public Condition addSubtree() {
return new AddSubtreeTreeCondition();
}
/**
* @return a condition that holds for children of deleted nodes.
*/
@NotNull
public Condition deleteSubtree() {
return new DeleteSubtreeTreeCondition();
}
/**
* @return a condition that holds for children of the target of a moved node
*/
@NotNull
public Condition moveSubtree() {
return new MoveCondition();
}
//------------------------------------------------------------< Compound conditions >---
/**
* A compound condition that holds when any of its constituents hold.
* @param conditions conditions of which any must hold in order for this condition to hold
* @return any condition
*/
@NotNull
public Condition any(@NotNull Condition... conditions) {
return new AnyCondition(newArrayList(checkNotNull(conditions)));
}
/**
* A compound condition that holds when all of its constituents hold.
* @param conditions conditions of which all must hold in order for this condition to hold
* @return any condition
*/
@NotNull
public Condition all(@NotNull Condition... conditions) {
return new AllCondition(newArrayList(checkNotNull(conditions)));
}
/**
* A compound condition that holds when all of its constituents hold.
* @param conditions conditions of which all must hold in order for this condition to hold
* @return any condition
*/
@NotNull
public Condition all(@NotNull List<Condition> conditions) {
return new AllCondition(checkNotNull(conditions));
}
/**
* A compound condition that holds when its constituent does not hold.
* @param condition condition which must not hold in order for this condition to hold
* @return not condition
*/
@NotNull
public Condition not(@NotNull Condition condition) {
return new NotCondition(checkNotNull(condition));
}
/**
* A compound condition that holds when any of its constituents hold.
* @param conditions conditions of which any must hold in order for this condition to hold
* @return any condition
*/
@NotNull
public Condition any(@NotNull Iterable<Condition> conditions) {
return new AnyCondition(checkNotNull(conditions));
}
/**
* A compound condition that holds when all of its constituents hold.
* @param conditions conditions of which all must hold in order for this condition to hold
* @return any condition
*/
@NotNull
public Condition all(@NotNull Iterable<Condition> conditions) {
return new AllCondition(checkNotNull(conditions));
}
/**
* Create a {@code FilterProvider} reflecting the current state of this builder.
* @return filter provider of the current state of this builder
*/
@NotNull
public FilterProvider build() {
return new FilterProvider() {
final boolean includeSessionLocal = FilterBuilder.this.includeSessionLocal;
final boolean includeClusterExternal = FilterBuilder.this.includeClusterExternal;
final boolean includeClusterLocal = FilterBuilder.this.includeClusterLocal;
final EventAggregator aggregator = FilterBuilder.this.aggregator;
final Iterable<String> subTrees = FilterBuilder.this.getSubTrees();
final Condition condition = FilterBuilder.this.condition;
final ChangeSetFilter changeSetFilter = FilterBuilder.this.changeSetFilter;
@Override
public String toString() {
return super.toString() + " [changeSetFilter="+changeSetFilter+"]";
}
@Override
public boolean includeCommit(@NotNull String sessionId, @Nullable CommitInfo info) {
return (includeSessionLocal || !isLocal(checkNotNull(sessionId), info))
&& (includeClusterExternal || !isExternal(info))
&& (includeClusterLocal || isExternal(info));
}
@NotNull
@Override
public EventFilter getFilter(@NotNull NodeState before, @NotNull NodeState after) {
return condition.createFilter(checkNotNull(before), checkNotNull(after));
}
@NotNull
@Override
public Iterable<String> getSubTrees() {
return subTrees;
}
@Override
public FilterConfigMBean getConfigMBean() {
return FilterBuilder.this.getConfigMBean();
}
private boolean isLocal(String sessionId, CommitInfo info) {
return info != null && Objects.equal(info.getSessionId(), sessionId);
}
private boolean isExternal(CommitInfo info) {
return info.isExternal();
}
@Override
public EventAggregator getEventAggregator() {
return aggregator;
}
@Override
public boolean excludes(ChangeSet changeSet) {
return changeSetFilter.excludes(changeSet);
}
};
}
@NotNull
private FilterConfigMBean getConfigMBean(){
return new FilterConfigMBean() {
@Override
public String[] getPaths() {
return Iterables.toArray(pathsForMBean, String.class);
}
@Override
public boolean isIncludeClusterLocal() {
return FilterBuilder.this.includeClusterLocal;
}
@Override
public boolean isIncludeClusterExternal() {
return FilterBuilder.this.includeClusterExternal;
}
};
}
//------------------------------------------------------------< Conditions >---
private static class ConstantCondition implements Condition {
public static final ConstantCondition INCLUDE_ALL = new ConstantCondition(true);
public static final ConstantCondition EXCLUDE_ALL = new ConstantCondition(false);
private final boolean value;
public ConstantCondition(boolean value) {
this.value = value;
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
return value ? Filters.includeAll() : Filters.excludeAll();
}
}
private static class ACCondition implements Condition {
private final PermissionProviderFactory permissionProviderFactory;
public ACCondition(PermissionProviderFactory permissionProviderFactory) {
this.permissionProviderFactory = permissionProviderFactory;
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
return new ACFilter(before, after,
permissionProviderFactory.create(RootFactory.createReadOnlyRoot(after)));
}
}
private static class PathCondition implements Condition {
private final String pathGlob;
private final Map<String,Pattern> patternMap = new HashMap<String,Pattern>();
public PathCondition(String pathGlob) {
this.pathGlob = pathGlob;
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
return new GlobbingPathFilter(pathGlob, patternMap);
}
}
private static class EventTypeCondition implements Condition {
private final int eventTypes;
public EventTypeCondition(int eventTypes) {
this.eventTypes = eventTypes;
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
return new EventTypeFilter(eventTypes);
}
}
private static class NodeTypeCondition implements Condition {
private final Selector selector;
private final String[] ntNames;
public NodeTypeCondition(Selector selector, String[] ntNames) {
this.selector = selector;
this.ntNames = ntNames;
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
TypePredicate predicate = new TypePredicate(
after.exists() ? after : before, ntNames);
return new UniversalFilter(before, after, selector, predicate);
}
}
private static class UniversalCondition implements Condition {
private final Selector selector;
private final Predicate<NodeState> predicate;
public UniversalCondition(Selector selector, Predicate<NodeState> predicate) {
this.selector = selector;
this.predicate = predicate;
}
@NotNull
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
return new UniversalFilter(before, after, selector, predicate);
}
}
protected static class AddSubtreeTreeCondition implements Condition {
@NotNull
@Override
public EventFilter createFilter(@NotNull NodeState before, @NotNull NodeState after) {
return AddSubtreeFilter.getInstance();
}
}
protected static class DeleteSubtreeTreeCondition implements Condition {
@NotNull
@Override
public EventFilter createFilter(@NotNull NodeState before, @NotNull NodeState after) {
return DeleteSubtreeFilter.getInstance();
}
}
protected static class MoveCondition implements Condition {
@NotNull
@Override
public EventFilter createFilter(@NotNull NodeState before, @NotNull NodeState after) {
return new MoveFilter();
}
}
private static class AnyCondition implements Condition {
private final Iterable<Condition> conditions;
public AnyCondition(Iterable<Condition> conditions) {
this.conditions = conditions;
}
public AnyCondition(Condition... conditions) {
this(newArrayList(conditions));
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
List<EventFilter> filters = newArrayList();
for (Condition condition : conditions) {
if (condition == ConstantCondition.INCLUDE_ALL) {
return ConstantFilter.INCLUDE_ALL;
} else if (condition != ConstantCondition.EXCLUDE_ALL) {
filters.add(condition.createFilter(before, after));
}
}
return filters.isEmpty()
? ConstantFilter.EXCLUDE_ALL
: Filters.any(filters);
}
}
private static class AllCondition implements Condition {
private final Iterable<Condition> conditions;
public AllCondition(Iterable<Condition> conditions) {
this.conditions = conditions;
}
public AllCondition(Condition... conditions) {
this(newArrayList(conditions));
}
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
List<EventFilter> filters = newArrayList();
for (Condition condition : conditions) {
if (condition == ConstantCondition.EXCLUDE_ALL) {
return ConstantFilter.EXCLUDE_ALL;
} else if (condition != ConstantCondition.INCLUDE_ALL) {
filters.add(condition.createFilter(before, after));
}
}
return filters.isEmpty()
? ConstantFilter.INCLUDE_ALL
: Filters.all(filters);
}
}
private static class NotCondition implements Condition {
private final Condition condition;
public NotCondition(Condition condition) {
this.condition = condition;
}
@NotNull
@Override
public EventFilter createFilter(NodeState before, NodeState after) {
if (condition == ConstantCondition.EXCLUDE_ALL) {
return ConstantFilter.INCLUDE_ALL;
} else if (condition == ConstantCondition.INCLUDE_ALL) {
return ConstantFilter.EXCLUDE_ALL;
} else {
return Filters.not(condition.createFilter(before, after));
}
}
}
}