| /* |
| * 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); |
| } |
| |
| } |