blob: f584d60329e74b8cf170cb44e2a1e5cd116d6662 [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.spi.observation;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.commons.json.JsopReader;
import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
import org.apache.jackrabbit.oak.commons.json.JsopWriter;
import org.jetbrains.annotations.Nullable;
/**
* A ChangeSet is a collection of items that have been changed as part of a
* commit. A ChangeSet is immutable and built by a ChangeSetBuilder.
* <p>
* Those items are parent paths, parent node names, parent node types and
* (child) properties. 'Changed' refers to any of add, remove, change (where
* applicable).
* <p>
* A ChangeSet is piggybacked on a CommitInfo in the CommitContext and can be
* used by (downstream) Observers for their convenience.
* <p>
* To limit memory usage, the ChangeSet has a limit on the number of items,
* each, that it collects. If one of those items reach the limit this is called
* an 'overflow' and the corresponding item type is marked as having
* 'overflown'. Downstream Observers should thus check if a particular item has
* overflown or not - this is indicated with null as the return value of the
* corresponding getters (while empty means: not overflown but nothing changed
* of that type).
* <p>
* Also, the ChangeSet carries a 'maxPathDepth' which is the depth of the path
* up until which paths have been collected. Thus any path that is longer than
* this 'maxPathDepth' will be cut off and only reported up to that max depth.
* Downstream Observers should thus inspect the 'maxPathDepth' and compare
* actual path depths with it in order to find out if any child paths have been
* cut off.
* <p>
* Naming: note that path, node name and node types all refer to the *parent* of
* a change. While properties naturally are leafs.
*/
public final class ChangeSet {
public static final String COMMIT_CONTEXT_OBSERVATION_CHANGESET = "oak.observation.changeSet";
private final int maxPathDepth;
private final Set<String> parentPaths;
private final Set<String> parentNodeNames;
private final Set<String> parentNodeTypes;
private final Set<String> propertyNames;
private final Set<String> allNodeTypes;
private final boolean hitsMaxPathDepth;
ChangeSet(int maxPathDepth, Set<String> parentPaths, Set<String> parentNodeNames, Set<String> parentNodeTypes,
Set<String> propertyNames, Set<String> allNodeTypes) {
this.maxPathDepth = maxPathDepth;
this.parentPaths = parentPaths == null ? null : ImmutableSet.copyOf(parentPaths);
this.parentNodeNames = parentNodeNames == null ? null : ImmutableSet.copyOf(parentNodeNames);
this.parentNodeTypes = parentNodeTypes == null ? null : ImmutableSet.copyOf(parentNodeTypes);
this.propertyNames = propertyNames == null ? null : ImmutableSet.copyOf(propertyNames);
this.allNodeTypes = allNodeTypes == null ? null : ImmutableSet.copyOf(allNodeTypes);
boolean hitsMaxPathDepth = false;
if (parentPaths != null) {
for (String aPath : parentPaths) {
if (PathUtils.getDepth(aPath) >= maxPathDepth) {
hitsMaxPathDepth = true;
break;
}
}
}
this.hitsMaxPathDepth = hitsMaxPathDepth;
}
@Override
public String toString() {
return "ChangeSet{paths[maxDepth:" + maxPathDepth + "]=" + parentPaths + ", propertyNames=" + propertyNames
+ ", parentNodeNames=" + parentNodeNames + ", parentNodeTypes=" + parentNodeTypes
+ ", allNodeTypes=" + allNodeTypes + ", any overflow: " + anyOverflow()
+ ", hits max path depth: " + hitsMaxPathDepth + "}";
}
public boolean doesHitMaxPathDepth() {
return hitsMaxPathDepth;
}
@Nullable
public Set<String> getParentPaths() {
return parentPaths;
}
@Nullable
public Set<String> getParentNodeNames() {
return parentNodeNames;
}
@Nullable
public Set<String> getParentNodeTypes() {
return parentNodeTypes;
}
@Nullable
public Set<String> getPropertyNames() {
return propertyNames;
}
public int getMaxPrefilterPathDepth() {
return maxPathDepth;
}
@Nullable
public Set<String> getAllNodeTypes() {
return allNodeTypes;
}
public boolean anyOverflow() {
return getAllNodeTypes() == null ||
getParentNodeNames() == null ||
getParentNodeTypes() == null ||
getParentPaths() == null ||
getPropertyNames() == null;
}
//~---------------------------------------------------< equals/hashcode >
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChangeSet changeSet = (ChangeSet) o;
if (maxPathDepth != changeSet.maxPathDepth) return false;
if (parentPaths != null ? !parentPaths.equals(changeSet.parentPaths) : changeSet.parentPaths != null)
return false;
if (parentNodeNames != null ? !parentNodeNames.equals(changeSet.parentNodeNames) : changeSet.parentNodeNames != null)
return false;
if (parentNodeTypes != null ? !parentNodeTypes.equals(changeSet.parentNodeTypes) : changeSet.parentNodeTypes != null)
return false;
if (propertyNames != null ? !propertyNames.equals(changeSet.propertyNames) : changeSet.propertyNames != null)
return false;
return allNodeTypes != null ? allNodeTypes.equals(changeSet.allNodeTypes) : changeSet.allNodeTypes == null;
}
@Override
public int hashCode() {
return 0;
}
//~----------------------------------------------------< json support >
public String asString(){
JsopWriter json = new JsopBuilder();
json.object();
json.key("maxPathDepth").value(maxPathDepth);
addToJson(json, "parentPaths", parentPaths);
addToJson(json, "parentNodeNames", parentNodeNames);
addToJson(json, "parentNodeTypes", parentNodeTypes);
addToJson(json, "propertyNames", propertyNames);
addToJson(json, "allNodeTypes", allNodeTypes);
json.endObject();
return json.toString();
}
public static ChangeSet fromString(String json) {
JsopReader reader = new JsopTokenizer(json);
int maxPathDepth = 0;
Set<String> parentPaths = null;
Set<String> parentNodeNames = null;
Set<String> parentNodeTypes = null;
Set<String> propertyNames = null;
Set<String> allNodeTypes = null;
reader.read('{');
if (!reader.matches('}')) {
do {
String name = reader.readString();
reader.read(':');
if ("maxPathDepth".equals(name)){
maxPathDepth = Integer.parseInt(reader.read(JsopReader.NUMBER));
} else {
Set<String> data = readArrayAsSet(reader);
if ("parentPaths".equals(name)){
parentPaths = data;
} else if ("parentNodeNames".equals(name)){
parentNodeNames = data;
} else if ("parentNodeTypes".equals(name)){
parentNodeTypes = data;
} else if ("propertyNames".equals(name)){
propertyNames = data;
} else if ("allNodeTypes".equals(name)){
allNodeTypes = data;
}
}
} while (reader.matches(','));
reader.read('}');
}
reader.read(JsopReader.END);
return new ChangeSet(maxPathDepth, parentPaths, parentNodeNames, parentNodeTypes, propertyNames, allNodeTypes);
}
private static Set<String> readArrayAsSet(JsopReader reader) {
Set<String> values = Sets.newHashSet();
reader.read('[');
for (boolean first = true; !reader.matches(']'); first = false) {
if (!first) {
reader.read(',');
}
values.add(reader.readString());
}
return values;
}
private static void addToJson(JsopWriter json, String name, Set<String> values){
if (values == null){
return;
}
json.key(name).array();
for (String v : values){
json.value(v);
}
json.endArray();
}
}