blob: e1cb839e1b150fa321c21f93727bd778bf390e36 [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.index.property;
import static com.google.common.collect.ImmutableSet.of;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.NT_BASE;
import static org.apache.jackrabbit.JcrConstants.NT_FILE;
import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexUtils.createIndexDefinition;
import static org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditor.COUNT_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
import static org.apache.jackrabbit.oak.spi.filter.PathFilter.PROP_EXCLUDED_PATHS;
import static org.apache.jackrabbit.oak.spi.filter.PathFilter.PROP_INCLUDED_PATHS;
import static org.apache.jackrabbit.oak.spi.state.NodeStateUtils.getNode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.ContentMirrorStoreStrategy;
import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.QueryEngineSettings;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfo;
import org.apache.jackrabbit.oak.query.ast.NodeTypeInfoProvider;
import org.apache.jackrabbit.oak.query.ast.Operator;
import org.apache.jackrabbit.oak.query.ast.SelectorImpl;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.query.index.TraversingIndex;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.CompositeHook;
import org.apache.jackrabbit.oak.spi.commit.DefaultValidator;
import org.apache.jackrabbit.oak.spi.commit.EditorHook;
import org.apache.jackrabbit.oak.spi.commit.Validator;
import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider;
import org.apache.jackrabbit.oak.spi.mount.Mount;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.core.spi.FilterReply;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
/**
* Test the Property2 index mechanism.
*/
public class PropertyIndexTest {
private static final int MANY = 100;
private static final EditorHook HOOK = new EditorHook(
new IndexUpdateProvider(new PropertyIndexEditorProvider()));
@Test
public void costEstimation() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
// disable the estimation
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
for (int i = 0; i < MANY; i++) {
builder.child("n" + i).setProperty("foo", "x" + i % 20);
}
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
double cost;
cost = lookup.getCost(f, "foo", PropertyValues.newString("x1"));
assertTrue("cost: " + cost, cost >= 6.5 && cost <= 7.5);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2")));
assertTrue("cost: " + cost, cost >= 11.5 && cost <= 12.5);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2", "x3", "x4", "x5")));
assertTrue("cost: " + cost, cost >= 26.5 && cost <= 27.5);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x0")));
assertTrue("cost: " + cost, cost >= 51.5 && cost <= 52.5);
cost = lookup.getCost(f, "foo", null);
assertTrue("cost: " + cost, cost >= MANY);
}
/**
* This is essentially same test as {@link #costEstimation()} with one difference that it uses
* path constraint in query and creates similar trees under 2 branches {@code path1} and {@code path2}.
* The cost estimation is then verified to be same as that in {@code costEstimation} for query under {@code path1}
* @throws Exception
*/
@Test
public void pathBasedCostEstimation() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
// disable the estimation
index.setProperty("entryCount", -1);
builder.setProperty(COUNT_PROPERTY_NAME, (long) MANY * 2, Type.LONG);
NodeState before = builder.getNodeState();
NodeBuilder path1 = builder.child("path1");
NodeBuilder path2 = builder.child("path2");
// Add some content and process it through the property index hook
for (int i = 0; i < MANY; i++) {
path1.child("n" + i).setProperty("foo", "x" + i % 20);
path2.child("n" + i).setProperty("foo", "x" + i % 20);
}
path1.setProperty(COUNT_PROPERTY_NAME, (long) MANY, Type.LONG);
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
f.restrictPath("/path1", Filter.PathRestriction.ALL_CHILDREN);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
double cost;
cost = lookup.getCost(f, "foo", PropertyValues.newString("x1"));
assertTrue("cost: " + cost, cost >= 10 && cost <= 14);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2")));
assertTrue("cost: " + cost, cost >= 20 && cost <= 24);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2", "x3", "x4", "x5")));
assertTrue("cost: " + cost, cost >= 50 && cost <= 54);
cost = lookup.getCost(f, "foo", PropertyValues.newString(
Arrays.asList("x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x0")));
assertTrue("cost: " + cost, cost >= 120 && cost <= 124);
cost = lookup.getCost(f, "foo", null);
assertTrue("cost: " + cost, cost >= MANY);
}
@Test
public void costMaxEstimation() throws Exception {
NodeState root = EmptyNodeState.EMPTY_NODE;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
NodeState before = builder.getNodeState();
// 100 nodes in the index:
// with a single level /content cost is 121
// adding a second level /content/data cost is133
// 101 nodes in the index:
// with a single level /content cost is 121
// adding a second level /content/data cost is 133
// 100 nodes, 12 levels deep, cost is 345
// 101 nodes, 12 levels deep, cost is 345
// threshold for estimation (PropertyIndexLookup.MAX_COST) is at 100
int nodes = 101;
int levels = 12;
NodeBuilder data = builder;
for (int i = 0; i < levels; i++) {
data = data.child("l" + i);
}
for (int i = 0; i < nodes; i++) {
NodeBuilder c = data.child("c_" + i);
c.setProperty("foo", "azerty");
}
// add more nodes (to make traversal more expensive)
for (int i = 0; i < 10000; i++) {
data.child("cx_" + i);
}
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
double cost = lookup.getCost(f, "foo",
PropertyValues.newString("azerty"));
double traversal = new TraversingIndex().getCost(f, indexed);
assertTrue("Estimated cost for " + nodes
+ " nodes should not be higher than traversal (" + cost + " < " + traversal + ")",
cost < traversal);
}
@Test
public void testPropertyLookup() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", Arrays.asList("abc", "def"),
Type.STRINGS);
// plus lots of dummy content to highlight the benefit of indexing
for (int i = 0; i < MANY; i++) {
builder.child("n" + i).setProperty("foo", "xyz");
}
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("a", "b"), find(lookup, "foo", "abc", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "def", f));
assertEquals(ImmutableSet.of(), find(lookup, "foo", "ghi", f));
assertEquals(MANY, find(lookup, "foo", "xyz", f).size());
assertEquals(MANY + 2, find(lookup, "foo", null, f).size());
double cost;
cost = lookup.getCost(f, "foo", PropertyValues.newString("xyz"));
assertTrue("cost: " + cost, cost >= MANY);
cost = lookup.getCost(f, "foo", null);
assertTrue("cost: " + cost, cost >= MANY);
}
@Test
public void testPathAwarePropertyLookup() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
f.restrictPath("/a", Filter.PathRestriction.ALL_CHILDREN);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("a"), find(lookup, "foo", "abc", f));
}
private static Set<String> find(PropertyIndexLookup lookup, String name,
String value, Filter filter) {
return Sets.newHashSet(lookup.query(filter, name, value == null ? null
: PropertyValues.newString(value)));
}
@Test
public void testCustomConfigPropertyLookup() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, false, ImmutableSet.of("foo", "extrafoo"),
null);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a").setProperty("foo", "abc")
.setProperty("extrafoo", "pqr");
builder.child("b").setProperty("foo", Arrays.asList("abc", "def"),
Type.STRINGS);
// plus lots of dummy content to highlight the benefit of indexing
for (int i = 0; i < MANY; i++) {
builder.child("n" + i).setProperty("foo", "xyz");
}
NodeState after = builder.getNodeState();
// Add an index
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("a", "b"), find(lookup, "foo", "abc", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "def", f));
assertEquals(ImmutableSet.of(), find(lookup, "foo", "ghi", f));
assertEquals(MANY, find(lookup, "foo", "xyz", f).size());
assertEquals(ImmutableSet.of("a"), find(lookup, "extrafoo", "pqr", f));
try {
assertEquals(ImmutableSet.of(), find(lookup, "pqr", "foo", f));
fail();
} catch (IllegalArgumentException e) {
// expected: no index for "pqr"
}
}
/**
* @see <a href="https://issues.apache.org/jira/browse/OAK-666">OAK-666:
* Property2Index: node type is used when indexing, but ignored when
* querying</a>
*/
@Test
public void testCustomConfigNodeType() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definitions
NodeBuilder builder = root.builder();
NodeBuilder index = builder.child(INDEX_DEFINITIONS_NAME);
createIndexDefinition(index, "fooIndex", true, false,
ImmutableSet.of("foo", "extrafoo"),
ImmutableSet.of(NT_UNSTRUCTURED));
createIndexDefinition(index, "fooIndexFile", true, false,
ImmutableSet.of("foo"), ImmutableSet.of(NT_FILE));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "abc");
builder.child("b")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", Arrays.asList("abc", "def"), Type.STRINGS);
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_UNSTRUCTURED);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("a", "b"), find(lookup, "foo", "abc", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "def", f));
assertEquals(ImmutableSet.of(), find(lookup, "foo", "ghi", f));
try {
assertEquals(ImmutableSet.of(), find(lookup, "pqr", "foo", f));
fail();
} catch (IllegalArgumentException e) {
// expected: no index for "pqr"
}
}
private static FilterImpl createFilter(NodeState root, String nodeTypeName) {
NodeTypeInfoProvider nodeTypes = new NodeStateNodeTypeInfoProvider(root);
NodeTypeInfo type = nodeTypes.getNodeTypeInfo(nodeTypeName);
SelectorImpl selector = new SelectorImpl(type, nodeTypeName);
return new FilterImpl(selector, "SELECT * FROM [" + nodeTypeName + "]", new QueryEngineSettings());
}
/**
* @see <a href="https://issues.apache.org/jira/browse/OAK-666">OAK-666:
* Property2Index: node type is used when indexing, but ignored when
* querying</a>
*/
@Test
public void testCustomConfigNodeTypeFallback() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definitions
NodeBuilder builder = root.builder();
NodeBuilder index = builder.child(INDEX_DEFINITIONS_NAME);
createIndexDefinition(
index, "fooIndex", true, false,
ImmutableSet.of("foo", "extrafoo"), null);
createIndexDefinition(
index, "fooIndexFile", true, false,
ImmutableSet.of("foo"), ImmutableSet.of(NT_FILE));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "abc");
builder.child("b")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", Arrays.asList("abc", "def"), Type.STRINGS);
NodeState after = builder.getNodeState();
// Add an index
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(after, NT_UNSTRUCTURED);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("a", "b"), find(lookup, "foo", "abc", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "def", f));
assertEquals(ImmutableSet.of(), find(lookup, "foo", "ghi", f));
try {
assertEquals(ImmutableSet.of(), find(lookup, "pqr", "foo", f));
fail();
} catch (IllegalArgumentException e) {
// expected: no index for "pqr"
}
}
@Test
public void valuePattern() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definitions
NodeBuilder builder = root.builder();
NodeBuilder index = builder.child(INDEX_DEFINITIONS_NAME);
NodeBuilder indexDef = createIndexDefinition(
index, "fooIndex", true, false,
ImmutableSet.of("foo"), null);
indexDef.setProperty(IndexConstants.VALUE_PATTERN, "(a.*|b)");
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "a");
builder.child("a1")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "a1");
builder.child("b")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "b");
builder.child("c")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "c");
NodeState after = builder.getNodeState();
// Add an index
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(after, NT_UNSTRUCTURED);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
PropertyIndex pIndex = new PropertyIndex(Mounts.defaultMountInfoProvider());
assertEquals(ImmutableSet.of("a"), find(lookup, "foo", "a", f));
assertEquals(ImmutableSet.of("a1"), find(lookup, "foo", "a1", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "b", f));
// expected: no index for "is not null"
assertTrue(pIndex.getCost(f, indexed) == Double.POSITIVE_INFINITY);
ArrayList<PropertyValue> list = new ArrayList<PropertyValue>();
list.add(PropertyValues.newString("c"));
f.restrictPropertyAsList("foo", list);
// expected: no index for value c
assertTrue(pIndex.getCost(f, indexed) == Double.POSITIVE_INFINITY);
f = createFilter(after, NT_UNSTRUCTURED);
list = new ArrayList<PropertyValue>();
list.add(PropertyValues.newString("a"));
f.restrictPropertyAsList("foo", list);
// expected: no index for value a
assertTrue(pIndex.getCost(f, indexed) < Double.POSITIVE_INFINITY);
}
@Test
public void valuePatternExclude() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definitions
NodeBuilder builder = root.builder();
NodeBuilder index = builder.child(INDEX_DEFINITIONS_NAME);
NodeBuilder indexDef = createIndexDefinition(
index, "fooIndex", true, false,
ImmutableSet.of("foo"), null);
indexDef.setProperty(IndexConstants.VALUE_EXCLUDED_PREFIXES, "test");
valuePatternExclude0(builder);
}
@Test
public void valuePatternExclude2() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definitions
NodeBuilder builder = root.builder();
NodeBuilder index = builder.child(INDEX_DEFINITIONS_NAME);
NodeBuilder indexDef = createIndexDefinition(
index, "fooIndex", true, false,
ImmutableSet.of("foo"), null);
PropertyState ps = PropertyStates.createProperty(
IndexConstants.VALUE_EXCLUDED_PREFIXES,
Arrays.asList("test"),
Type.STRINGS);
indexDef.setProperty(ps);
valuePatternExclude0(builder);
}
private void valuePatternExclude0(NodeBuilder builder) throws CommitFailedException {
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "a");
builder.child("a1")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "a1");
builder.child("b")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "b");
builder.child("c")
.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)
.setProperty("foo", "c");
NodeState after = builder.getNodeState();
// Add an index
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(after, NT_UNSTRUCTURED);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
PropertyIndex pIndex = new PropertyIndex(Mounts.defaultMountInfoProvider());
assertEquals(ImmutableSet.of("a"), find(lookup, "foo", "a", f));
assertEquals(ImmutableSet.of("a1"), find(lookup, "foo", "a1", f));
assertEquals(ImmutableSet.of("b"), find(lookup, "foo", "b", f));
// expected: no index for "is not null", "= 'test'", "like 't%'"
assertTrue(pIndex.getCost(f, indexed) == Double.POSITIVE_INFINITY);
f.restrictProperty("foo", Operator.EQUAL, PropertyValues.newString("test"));
assertTrue(pIndex.getCost(f, indexed) == Double.POSITIVE_INFINITY);
f = createFilter(after, NT_UNSTRUCTURED);
f.restrictProperty("foo", Operator.LIKE, PropertyValues.newString("t%"));
assertTrue(pIndex.getCost(f, indexed) == Double.POSITIVE_INFINITY);
f = createFilter(after, NT_UNSTRUCTURED);
// expected: index for "like 'a%'"
f.restrictProperty("foo", Operator.GREATER_OR_EQUAL, PropertyValues.newString("a"));
f.restrictProperty("foo", Operator.LESS_OR_EQUAL, PropertyValues.newString("a0"));
assertTrue(pIndex.getCost(f, indexed) < Double.POSITIVE_INFINITY);
f = createFilter(after, NT_UNSTRUCTURED);
// expected: index for value c
ArrayList<PropertyValue> list = new ArrayList<PropertyValue>();
list.add(PropertyValues.newString("c"));
f.restrictPropertyAsList("foo", list);
assertTrue(pIndex.getCost(f, indexed) < Double.POSITIVE_INFINITY);
// expected: index for value a
f = createFilter(after, NT_UNSTRUCTURED);
list = new ArrayList<PropertyValue>();
list.add(PropertyValues.newString("a"));
f.restrictPropertyAsList("foo", list);
assertTrue(pIndex.getCost(f, indexed) < Double.POSITIVE_INFINITY);
}
@Test(expected = CommitFailedException.class)
public void testUnique() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(
builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, true, ImmutableSet.of("foo"), null);
NodeState before = builder.getNodeState();
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", Arrays.asList("abc", "def"),
Type.STRINGS);
NodeState after = builder.getNodeState();
// should throw
HOOK.processCommit(before, after, CommitInfo.EMPTY);
}
@Test
public void testUpdateUnique() throws Exception {
NodeState root = EMPTY_NODE;
NodeBuilder builder = root.builder();
createIndexDefinition(
builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, true, ImmutableSet.of("foo"), null);
NodeState before = builder.getNodeState();
builder.child("a").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState done = HOOK.processCommit(before, after, CommitInfo.EMPTY);
// remove, and then re-add the same node
builder = done.builder();
builder.child("a").setProperty("foo", "abc");
after = builder.getNodeState();
// apply the changes to the state before adding the entries
done = HOOK.processCommit(before, after, CommitInfo.EMPTY);
// re-apply the changes
done = HOOK.processCommit(done, after, CommitInfo.EMPTY);
}
@Test
public void testUniqueByTypeOK() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, true, ImmutableSet.of("foo"),
ImmutableSet.of("typeFoo"));
NodeState before = builder.getNodeState();
builder.child("a").setProperty(JCR_PRIMARYTYPE, "typeFoo", Type.NAME)
.setProperty("foo", "abc");
builder.child("b").setProperty(JCR_PRIMARYTYPE, "typeBar", Type.NAME)
.setProperty("foo", "abc");
NodeState after = builder.getNodeState();
HOOK.processCommit(before, after, CommitInfo.EMPTY); // should not throw
}
@Test(expected = CommitFailedException.class)
public void testUniqueByTypeKO() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, true, ImmutableSet.of("foo"),
ImmutableSet.of("typeFoo"));
NodeState before = builder.getNodeState();
builder.child("a").setProperty(JCR_PRIMARYTYPE, "typeFoo", Type.NAME)
.setProperty("foo", "abc");
builder.child("b").setProperty(JCR_PRIMARYTYPE, "typeFoo", Type.NAME)
.setProperty("foo", "abc");
NodeState after = builder.getNodeState();
HOOK.processCommit(before, after, CommitInfo.EMPTY); // should throw
}
@Test
public void testUniqueByTypeDelete() throws Exception {
NodeState root = EMPTY_NODE;
// Add index definition
NodeBuilder builder = root.builder();
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME),
"fooIndex", true, true, ImmutableSet.of("foo"),
ImmutableSet.of("typeFoo"));
builder.child("a").setProperty(JCR_PRIMARYTYPE, "typeFoo", Type.NAME)
.setProperty("foo", "abc");
builder.child("b").setProperty(JCR_PRIMARYTYPE, "typeBar", Type.NAME)
.setProperty("foo", "abc");
NodeState before = builder.getNodeState();
builder.getChildNode("b").remove();
NodeState after = builder.getNodeState();
HOOK.processCommit(before, after, CommitInfo.EMPTY); // should not throw
}
@Test
public void traversalWarning() throws Exception {
ListAppender<ILoggingEvent> appender = createAndRegisterAppender();
int testDataSize = ContentMirrorStoreStrategy.TRAVERSING_WARN;
NodeState indexed = createTestData(testDataSize);
assertEquals(testDataSize, getResultSize(indexed, "foo", "bar"));
assertFalse(appender.list.isEmpty());
appender.list.clear();
testDataSize = 100;
indexed = createTestData(100);
assertEquals(testDataSize, getResultSize(indexed, "foo", "bar"));
assertTrue("Warning should not be logged for traversing " + testDataSize,
appender.list.isEmpty());
deregisterAppender(appender);
}
@Test
public void testPathInclude() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty(createProperty(PROP_INCLUDED_PATHS, of("/test/a"), Type.STRINGS));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("test").child("a").setProperty("foo", "abc");
builder.child("test").child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("test/a"), find(lookup, "foo", "abc", f));
}
@Test
public void testPathExclude() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty(createProperty(PROP_EXCLUDED_PATHS, of("/test/a"), Type.STRINGS));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("test").child("a").setProperty("foo", "abc");
builder.child("test").child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
f.restrictProperty("foo", Operator.EQUAL, PropertyValues.newString("abc"));
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("test/b"), find(lookup, "foo", "abc", f));
//no path restriction, opt out
PropertyIndexPlan plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
//path restriction is not an ancestor of excluded path, index may be used
f.setPath("/test2");
plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY != plan.getCost());
//path restriction is an ancestor of excluded path, opt out
f.setPath("/test");
plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
}
@Test
public void testPathIncludeExclude() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty(createProperty(PROP_INCLUDED_PATHS, of("/test/a"), Type.STRINGS));
index.setProperty(createProperty(PROP_EXCLUDED_PATHS, of("/test/a/b"), Type.STRINGS));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("test").child("a").setProperty("foo", "abc");
builder.child("test").child("a").child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
f.restrictProperty("foo", Operator.EQUAL, PropertyValues.newString("abc"));
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
assertEquals(ImmutableSet.of("test/a"), find(lookup, "foo", "abc", f));
//no path restriction, opt out
PropertyIndexPlan plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
//path restriction is not an ancestor of excluded path, index may be used
f.setPath("/test/a/x");
plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY != plan.getCost());
//path restriction is an ancestor of excluded path but no included path, opt out
f.setPath("/test/a/b");
plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
//path restriction is an ancestor of excluded path, opt out
f.setPath("/test/a");
plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
}
@Test
public void testPathExcludeInclude() throws Exception{
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty(createProperty(PROP_INCLUDED_PATHS, of("/test/a/b"), Type.STRINGS));
index.setProperty(createProperty(PROP_EXCLUDED_PATHS, of("/test/a"), Type.STRINGS));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("test").child("a").setProperty("foo", "abc");
builder.child("test").child("a").child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
try {
HOOK.processCommit(before, after, CommitInfo.EMPTY);
assertTrue(false);
} catch (IllegalStateException expected) {}
}
@Test
public void testPathMismatch() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty(createProperty(PROP_INCLUDED_PATHS, of("/test/a"), Type.STRINGS));
index.setProperty(createProperty(PROP_EXCLUDED_PATHS, of("/test/a/b"), Type.STRINGS));
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("test").child("a").setProperty("foo", "abc");
builder.child("test").child("a").child("b").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
f.restrictPath("/test2", Filter.PathRestriction.ALL_CHILDREN);
PropertyIndexPlan plan = new PropertyIndexPlan("plan", root, index.getNodeState(), f);
assertTrue(Double.POSITIVE_INFINITY == plan.getCost());
}
@Test
public void singleMount() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("a").setProperty("foo", "abc");
builder.child("b").child("x").setProperty("foo", "abc");
builder.child("a").child("x").setProperty("foo", "abc");
builder.child("m").child("n").setProperty("foo", "abc");
builder.child("m").child("n").child("o").setProperty("foo", "abc");
builder.child("m").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
MountInfoProvider mip = Mounts.newBuilder()
.mount("foo", "/a", "/m/n")
.build();
Mount fooMount = mip.getMountByName("foo");
Mount defMount = mip.getDefaultMount();
EditorHook hook = new EditorHook(
new IndexUpdateProvider(new PropertyIndexEditorProvider().with(mip)));
NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed,mip);
assertEquals(ImmutableSet.of("a", "b/x", "a/x", "m", "m/n", "m/n/o"), find(lookup, "foo", "abc", f));
assertEquals(ImmutableSet.of(), find(lookup, "foo", "ghi", f));
assertTrue(getNode(indexed, "/oak:index/foo/:index").exists());
//Separate node for mount
assertTrue(getNode(indexed, "/oak:index/foo/"+ getNodeForMount(fooMount)).exists());
//Index entries for paths in foo mount should go to :oak:foo-index
assertTrue(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/a", "abc")).exists());
assertTrue(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/a/x", "abc")).exists());
assertTrue(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/m/n", "abc")).exists());
assertTrue(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/m/n/o", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/a", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/a/x", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/m/n", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/m/n/o", "abc")).exists());
//All other index entries should go to :index
assertTrue(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/b", "abc")).exists());
assertTrue(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/b/x", "abc")).exists());
assertTrue(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/m", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/b", "abc")).exists());
assertFalse(getNode(indexed, pathInIndex(fooMount, "/oak:index/foo", "/b/x", "abc")).exists());
//System.out.println(NodeStateUtils.toString(getNode(indexed, "/oak:index/foo")));
}
@Test
public void mountWithCommitInWritableMount() throws Exception{
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("content").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
MountInfoProvider mip = Mounts.newBuilder()
.readOnlyMount("foo", "/readOnly")
.build();
CompositeHook hook = new CompositeHook(
new EditorHook(new IndexUpdateProvider(new PropertyIndexEditorProvider().with(mip))),
new EditorHook(new ValidatorProvider(){
protected Validator getRootValidator(NodeState before, NodeState after, CommitInfo info) {
return new PrivateStoreValidator("/", mip);
}
})
);
NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
Mount defMount = mip.getDefaultMount();
assertTrue(getNode(indexed, pathInIndex(defMount, "/oak:index/foo", "/content", "abc")).exists());
}
@Test
public void mountWithCommitInWritableMountForUniqueIndex() throws Exception{
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, true, ImmutableSet.of("foo"), null);
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
builder.child("content").setProperty("foo", "abc");
NodeState after = builder.getNodeState();
MountInfoProvider mip = Mounts.newBuilder()
.readOnlyMount("foo", "/readOnly")
.build();
CompositeHook hook = new CompositeHook(
new EditorHook(new IndexUpdateProvider(new PropertyIndexEditorProvider().with(mip))),
new EditorHook(new ValidatorProvider(){
protected Validator getRootValidator(NodeState before, NodeState after, CommitInfo info) {
return new PrivateStoreValidator("/", mip);
}
})
);
NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
Mount defMount = mip.getDefaultMount();
NodeState indexedState = getNode(indexed, "/oak:index/foo/" + getNodeForMount(defMount) + "/abc");
assertTrue(indexedState.exists());
Iterable<String> values = indexedState.getStrings("entry");
assertEquals(1, Iterables.size(values));
assertEquals("/content", Iterables.getFirst(values, null));
Mount roMount = mip.getMountByName("foo");
assertFalse(getNode(indexed, "/oak:index/foo/" + getNodeForMount(roMount)).exists());
}
@Test(expected = CommitFailedException.class)
public void mountAndUniqueIndexes() throws Exception {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, true, ImmutableSet.of("foo"), null);
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
MountInfoProvider mip = Mounts.newBuilder()
.mount("foo", "/a")
.build();
builder.child("a").setProperty("foo", "abc");
builder.child("b").setProperty("foo", Arrays.asList("abc", "def"),
Type.STRINGS);
NodeState after = builder.getNodeState();
EditorHook hook = new EditorHook(
new IndexUpdateProvider(new PropertyIndexEditorProvider().with(mip)));
// should throw
hook.processCommit(before, after, CommitInfo.EMPTY);
}
private static String pathInIndex(Mount mount,
String indexPath, String indexedPath, String indexedValue){
return indexPath + "/" + getNodeForMount(mount) + "/" + indexedValue + indexedPath;
}
private static String getNodeForMount(Mount mount) {
return Multiplexers.getNodeForMount(mount, INDEX_CONTENT_NODE_NAME);
}
private int getResultSize(NodeState indexed, String name, String value){
FilterImpl f = createFilter(indexed, NT_BASE);
// Query the index
PropertyIndexLookup lookup = new PropertyIndexLookup(indexed);
Iterable<String> result = lookup.query(f, name, PropertyValues.newString(value));
return Iterables.size(result);
}
private NodeState createTestData(int entryCount) throws CommitFailedException {
NodeState root = INITIAL_CONTENT;
// Add index definition
NodeBuilder builder = root.builder();
NodeBuilder index = createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
true, false, ImmutableSet.of("foo"), null);
// disable the estimation
index.setProperty("entryCount", -1);
NodeState before = builder.getNodeState();
// Add some content and process it through the property index hook
int depth = ContentMirrorStoreStrategy.TRAVERSING_WARN / entryCount + 10;
for (int i = 0; i < entryCount; i++) {
NodeBuilder parentNode = builder.child("n" + i);
for (int j = 0; j < depth ; j++) {
parentNode = parentNode.child("c" + j);
}
parentNode.setProperty("foo", "bar");
}
NodeState after = builder.getNodeState();
return HOOK.processCommit(before, after, CommitInfo.EMPTY);
}
private ListAppender<ILoggingEvent> createAndRegisterAppender() {
TraversingWarningFilter filter = new TraversingWarningFilter();
filter.start();
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.setContext(getContext());
appender.setName("TestLogCollector");
appender.addFilter(filter);
appender.start();
rootLogger().addAppender(appender);
return appender;
}
private void deregisterAppender(Appender<ILoggingEvent> appender){
rootLogger().detachAppender(appender);
}
private static LoggerContext getContext(){
return (LoggerContext) LoggerFactory.getILoggerFactory();
}
private static ch.qos.logback.classic.Logger rootLogger() {
return getContext().getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
}
private static class TraversingWarningFilter extends ch.qos.logback.core.filter.Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
if (event.getLevel().isGreaterOrEqual(Level.WARN)
&& event.getMessage().contains("Traversed")) {
return FilterReply.ACCEPT;
} else {
return FilterReply.DENY;
}
}
}
private class PrivateStoreValidator extends DefaultValidator {
private final String path;
private final MountInfoProvider mountInfoProvider;
public PrivateStoreValidator(String path, MountInfoProvider mountInfoProvider) {
this.path = path;
this.mountInfoProvider = mountInfoProvider;
}
public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
return checkPrivateStoreCommit(getCommitPath(name));
}
public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException {
return checkPrivateStoreCommit(getCommitPath(name));
}
public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException {
return checkPrivateStoreCommit(getCommitPath(name));
}
private Validator checkPrivateStoreCommit(String commitPath) throws CommitFailedException {
Mount mountInfo = mountInfoProvider.getMountByPath(commitPath);
if (mountInfo.isReadOnly()) {
throw new CommitFailedException(CommitFailedException.UNSUPPORTED, 0,
"Unsupported commit to a read-only store "+ commitPath);
}
return new PrivateStoreValidator(commitPath, mountInfoProvider);
}
private String getCommitPath(String changeNodeName) {
return PathUtils.concat(path, changeNodeName);
}
}
}