blob: 81d32e3bd169622a675a53b943758cdd2c219bc3 [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;
import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.spi.commit.CommitContext;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EditorProvider;
import org.apache.jackrabbit.oak.spi.commit.Validator;
import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider;
import org.apache.jackrabbit.oak.spi.observation.ChangeSet;
import org.apache.jackrabbit.oak.spi.observation.ChangeSetBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A ChangeCollectorProvider can be hooked into Oak thus enabling the collection
* of ChangeSets of changed items of a commit, which downstream Observers can
* then use at their convenience.
* <p>
*
* @see ChangeSet for details on what is tracked and how that data should be
* interpreted
*/
@Component(
immediate = true,
service = {ValidatorProvider.class, EditorProvider.class},
property = "type=" + ChangeCollectorProvider.TYPE)
@Designate(ocd = ChangeCollectorProvider.Configuration.class)
public class ChangeCollectorProvider extends ValidatorProvider {
private static final int DEFAULT_MAX_ITEMS = 50;
private static final int DEFAULT_MAX_PATH_DEPTH = 9;
private static final boolean DEFAULT_ENABLED = true;
@ObjectClassDefinition(
name = "Apache Jackrabbit Oak Change Collector Service",
description = "It hooks into the commit and collects a ChangeSet of changed items of a commit which " +
"is then used to speed up observation processing"
)
@interface Configuration {
@AttributeDefinition(
name = "Maximum Number of Collected Items (per type)",
description = "Integer value indicating maximum number of individual items of changes - "
+ "such as property, nodeType, node name, path - to be collected. If there are "
+ "more changes, the collection is considered failed and marked as such. "
+ "Default is " + DEFAULT_MAX_ITEMS)
int maxItems() default DEFAULT_MAX_ITEMS;
@AttributeDefinition(
name = "Maximum depth of paths to collect",
description = "Integer value indicating maximum depth of paths to collect. "
+ "Paths deeper than this will not be individually reported, and instead "
+ "a path at this max depth will be added. Note that this doesn't affect "
+ "any other collected item such as property, nodeType - ie those will "
+ "all be collected irrespective of this config param." + "Default is " + DEFAULT_MAX_PATH_DEPTH)
int maxPathDepth() default DEFAULT_MAX_PATH_DEPTH;
@AttributeDefinition(
name = "enable/disable this validator",
description = "Whether this validator is enabled. If disabled no ChangeSet will be generated. Default is "
+ DEFAULT_ENABLED)
boolean enabled() default DEFAULT_ENABLED;
}
public static final String TYPE = "changeCollectorProvider";
private static final Logger LOG = LoggerFactory.getLogger(ChangeCollectorProvider.class);
public static final String COMMIT_CONTEXT_OBSERVATION_CHANGESET = "oak.observation.changeSet";
/**
* There is one CollectorSupport per validation process - it is shared
* between multiple instances of ChangeCollector (Validator) - however it
* can remain unsynchronized as validators are executed single-threaded.
*/
private static class CollectorSupport {
final CommitInfo info;
final int maxPathDepth;
final ChangeSetBuilder changeSetBuilder;
public CollectorSupport(@NotNull CommitInfo info, @NotNull ChangeSetBuilder changeSetBuilder,
int maxPathDepth) {
this.info = info;
this.changeSetBuilder = changeSetBuilder;
this.maxPathDepth = maxPathDepth;
}
@Override
public String toString() {
return "CollectorSupport with " + changeSetBuilder;
}
}
/**
* ChangeCollectors are the actual working-horse Validators that are created
* for each level thus as a whole propage through the entire change.
* <p>
* The actual data is collected via a per-commit CollectorSupport and its
* underlying ChangeSet (the latter is where the actual changes end up in).
* <p>
* When finished - ie in the last==root leave() - the resulting ChangeSet is
* marked immutable and set in the CommitContext.
*/
private static class ChangeCollector implements Validator {
private final CollectorSupport support;
private final boolean isRoot;
private final NodeState beforeParentNodeOrNull;
private final NodeState afterParentNodeOrNull;
private final String path;
private final String childName;
private final int level;
private boolean changed;
private static ChangeCollector newRootCollector(@NotNull CommitInfo info, int maxItems, int maxPathDepth) {
ChangeSetBuilder changeSetBuilder = new ChangeSetBuilder(maxItems, maxPathDepth);
CollectorSupport support = new CollectorSupport(info, changeSetBuilder, maxPathDepth);
return new ChangeCollector(support, true, null, null, "/", null, 0);
}
private ChangeCollector newChildCollector(@Nullable NodeState beforeParentNodeOrNull, @Nullable NodeState afterParentNodeOrNull, @NotNull String childName) {
return new ChangeCollector(support, false, beforeParentNodeOrNull, afterParentNodeOrNull, concat(path, childName), childName, level + 1);
}
private ChangeCollector(@NotNull CollectorSupport support, boolean isRoot, @Nullable NodeState beforeParentNodeOrNull,
@Nullable NodeState afterParentNodeOrNull, @NotNull String path, @Nullable String childNameOrNull, int level) {
this.support = support;
this.isRoot = isRoot;
this.beforeParentNodeOrNull = beforeParentNodeOrNull;
this.afterParentNodeOrNull = afterParentNodeOrNull;
this.path = path;
this.childName = childNameOrNull;
this.level = level;
}
@Override
public String toString() {
return "ChangeCollector[path=" + path + "]";
}
@Override
public void enter(NodeState before, NodeState after) throws CommitFailedException {
// nothing to be done here
}
@Override
public void leave(NodeState before, NodeState after) throws CommitFailedException {
// first check if we have to add anything to paths and/or nodeNames
if (changed && level <= support.maxPathDepth) {
support.changeSetBuilder.addParentPath(path);
}
if (changed && childName != null) {
support.changeSetBuilder.addParentNodeName(childName);
}
if (changed){
addParentNodeType(beforeParentNodeOrNull);
addParentNodeType(afterParentNodeOrNull);
}
// then if we're not at the root, we're done
if (!isRoot) {
return;
}
// but if we're at the root, then we add the ChangeSet to the
// CommitContext of the CommitInfo
CommitContext commitContext = (CommitContext) support.info.getInfo().get(CommitContext.NAME);
ChangeSet changeSet = support.changeSetBuilder.build();
commitContext.set(ChangeSet.COMMIT_CONTEXT_OBSERVATION_CHANGESET, changeSet);
LOG.debug("Collected changeSet for commit {} is {}", support.info, changeSet);
}
@Override
public void propertyAdded(PropertyState after) throws CommitFailedException {
addPropertyName(after);
}
@Override
public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException {
addPropertyName(before);
}
@Override
public void propertyDeleted(PropertyState before) throws CommitFailedException {
addPropertyName(before);
}
@Override
public Validator childNodeAdded(String childName, NodeState after) throws CommitFailedException {
changed = true;
addToAllNodeType(after);
return newChildCollector(null, after, childName);
}
@Override
public Validator childNodeChanged(String childName, NodeState before, NodeState after)
throws CommitFailedException {
if (level == support.maxPathDepth) {
// then we'll cut off further paths below.
// to compensate, add the current path at this level
support.changeSetBuilder.addParentPath(path);
// however, continue normally to handle names/types/properties
// below
}
// in theory the node type could be changed, so collecting both before and after
addToAllNodeType(before);
addToAllNodeType(after);
return newChildCollector(before, after, childName);
}
@Override
public Validator childNodeDeleted(String childName, NodeState before) throws CommitFailedException {
changed = true;
addToAllNodeType(before);
return newChildCollector(before, null, childName);
}
private void addToAllNodeType(NodeState state) {
String primaryType = state.getName(JcrConstants.JCR_PRIMARYTYPE);
if (primaryType != null) {
support.changeSetBuilder.addNodeType(primaryType);
}
support.changeSetBuilder.addNodeTypes(state.getNames(JcrConstants.JCR_MIXINTYPES));
}
private void addParentNodeType(@Nullable NodeState state) {
if (state == null){
return;
}
String primaryType = state.getName(JcrConstants.JCR_PRIMARYTYPE);
if (primaryType != null) {
support.changeSetBuilder.addParentNodeType(primaryType);
}
support.changeSetBuilder.addParentNodeTypes(state.getNames(JcrConstants.JCR_MIXINTYPES));
}
private void addPropertyName(PropertyState after) {
changed = true;
support.changeSetBuilder.addPropertyName(after.getName());
}
}
private int maxItems = DEFAULT_MAX_ITEMS;
private int maxPathDepth = DEFAULT_MAX_PATH_DEPTH;
private boolean enabled = DEFAULT_ENABLED;
@Activate
protected void activate(final Configuration config) {
reconfig(config);
LOG.info("activate: maxItems=" + maxItems + ", maxPathDepth=" + maxPathDepth + ", enabled=" + enabled);
}
@Modified
protected void modified(final Configuration config) {
reconfig(config);
LOG.info("modified: maxItems=" + maxItems + ", maxPathDepth=" + maxPathDepth + ", enabled=" + enabled);
}
private void reconfig(final Configuration config) {
maxItems = config.maxItems();
maxPathDepth = config.maxPathDepth();
enabled = config.enabled();
}
/** FOR TESTING-ONLY **/
protected void setMaxPathDepth(int maxPathDepth) {
this.maxPathDepth = maxPathDepth;
}
/** FOR TESTING-ONLY **/
protected int getMaxPathDepth() {
return this.maxPathDepth;
}
/** FOR TESTING-ONLY **/
protected void setMaxItems(int maxItems) {
this.maxItems = maxItems;
}
/** FOR TESTING-ONLY **/
protected int getMaxItems() {
return this.maxItems;
}
@Override
protected Validator getRootValidator(NodeState before, NodeState after, CommitInfo info) {
if (!enabled) {
return null;
}
if (info == null || !info.getInfo().containsKey(CommitContext.NAME)) {
// then we cannot do change-collecting, as we can't store
// it in the info
return null;
}
return ChangeCollector.newRootCollector(info, maxItems, maxPathDepth);
}
}