blob: c8e359df80955ae84e036676572862cba81a5d34 [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.ignite.internal.configuration.util;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.ignite.internal.configuration.SuperRoot;
import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.apache.ignite.internal.configuration.tree.NamedListNode;
/** Utility class that has {@link ConfigurationFlattener#createFlattenedUpdatesMap(SuperRoot, SuperRoot)} method. */
public class ConfigurationFlattener {
/**
* Convert a traversable tree to a map of qualified keys to values.
*
* @param curRoot Current root tree.
* @param updates Tree with updates.
* @return Map of changes.
*/
public static Map<String, Serializable> createFlattenedUpdatesMap(SuperRoot curRoot, SuperRoot updates) {
// Resulting map.
Map<String, Serializable> resMap = new HashMap<>();
// This method traverses two trees at the same time. In order to reuse the visitor object it's decided to
// use an explicit stack for nodes of the "old" tree. We need to reuse the visitor object because it accumulates
// "full" keys in dot-separated notation. Please refer to KeysTrackingConfigurationVisitor for details..
Deque<InnerNode> oldInnerNodesStack = new ArrayDeque<>();
oldInnerNodesStack.push(curRoot);
// Explicit access to the children of super root guarantees that "oldInnerNodesStack" is never empty and thus
// we don't need null-checks when calling Deque#peek().
updates.traverseChildren(new FlattenerVisitor(oldInnerNodesStack, resMap), true);
assert oldInnerNodesStack.peek() == curRoot : oldInnerNodesStack;
return resMap;
}
/**
* @param node Named list node.
* @return Map that contains same keys and their positions as values.
*/
private static Map<String, Integer> keysToOrderIdx(NamedListNode<?> node) {
Map<String, Integer> res = new HashMap<>();
int idx = 0;
for (String key : node.namedListKeys()) {
if (node.get(key) != null)
res.put(key, idx++);
}
return res;
}
/**
* Visitor that collects diff between "old" and "new" trees into a flat map.
*/
private static class FlattenerVisitor extends KeysTrackingConfigurationVisitor<Object> {
/** Old nodes stack for recursion. */
private final Deque<InnerNode> oldInnerNodesStack;
/** Map with the result. */
private final Map<String, Serializable> resMap;
/** Flag indicates that "old" and "new" trees are literally the same at the moment. */
private boolean singleTreeTraversal;
/**
* Makes sense only if {@link #singleTreeTraversal} is {@code true}. Helps distinguishing creation from
* deletion. Always {@code false} if {@link #singleTreeTraversal} is {@code false}.
*/
private boolean deletion;
FlattenerVisitor(Deque<InnerNode> oldInnerNodesStack, Map<String, Serializable> resMap) {
this.oldInnerNodesStack = oldInnerNodesStack;
this.resMap = resMap;
}
/** {@inheritDoc} */
@Override public Void doVisitLeafNode(String key, Serializable newVal) {
// Read same value from old tree.
Serializable oldVal = oldInnerNodesStack.peek().traverseChild(key, ConfigurationUtil.leafNodeVisitor(), true);
// Do not put duplicates into the resulting map.
if (singleTreeTraversal || !Objects.deepEquals(oldVal, newVal))
resMap.put(currentKey(), deletion ? null : newVal);
return null;
}
/** {@inheritDoc} */
@Override public Void doVisitInnerNode(String key, InnerNode newNode) {
// Read same node from old tree.
InnerNode oldNode = oldInnerNodesStack.peek().traverseChild(key, ConfigurationUtil.innerNodeVisitor(), true);
// Skip subtree that has not changed.
if (oldNode == newNode && !singleTreeTraversal)
return null;
if (oldNode == null)
visitAsymmetricInnerNode(newNode, false);
else {
oldInnerNodesStack.push(oldNode);
newNode.traverseChildren(this, true);
oldInnerNodesStack.pop();
}
return null;
}
/** {@inheritDoc} */
@Override public <N extends InnerNode> Void doVisitNamedListNode(String key, NamedListNode<N> newNode) {
// Read same named list node from old tree.
NamedListNode<?> oldNode = oldInnerNodesStack.peek().traverseChild(key, ConfigurationUtil.namedListNodeVisitor(), true);
// Skip subtree that has not changed.
if (oldNode == newNode && !singleTreeTraversal)
return null;
// Old keys ordering can be ignored if we either create or delete everything.
Map<String, Integer> oldKeysToOrderIdxMap = singleTreeTraversal ? null
: keysToOrderIdx(oldNode);
// New keys ordering can be ignored if we delete everything.
Map<String, Integer> newKeysToOrderIdxMap = deletion ? null
: keysToOrderIdx(newNode);
for (String newNodeKey : newNode.namedListKeys()) {
String newNodeInternalId = newNode.internalId(newNodeKey);
withTracking(newNodeInternalId, false, false, () -> {
InnerNode newNamedElement = newNode.get(newNodeKey);
String oldNodeKey = oldNode.keyByInternalId(newNodeInternalId);
InnerNode oldNamedElement = oldNode.get(oldNodeKey);
// Deletion of nonexistent element.
if (oldNamedElement == null && newNamedElement == null)
return null;
// Skip element that has not changed.
// Its index can be different though, so we don't "continue" straight away.
if (singleTreeTraversal || oldNamedElement != newNamedElement) {
if (newNamedElement == null)
visitAsymmetricInnerNode(oldNamedElement, true);
else if (oldNamedElement == null)
visitAsymmetricInnerNode(newNamedElement, false);
else {
oldInnerNodesStack.push(oldNamedElement);
newNamedElement.traverseChildren(this, true);
oldInnerNodesStack.pop();
}
}
Integer newIdx = newKeysToOrderIdxMap == null ? null : newKeysToOrderIdxMap.get(newNodeKey);
Integer oldIdx = oldKeysToOrderIdxMap == null ? null : oldKeysToOrderIdxMap.get(newNodeKey);
// We should "persist" changed indexes only.
if (newIdx != oldIdx || singleTreeTraversal || newNamedElement == null) {
String orderKey = currentKey() + NamedListNode.ORDER_IDX;
resMap.put(orderKey, deletion || newNamedElement == null ? null : newIdx);
}
// If it's creation / deletion / rename.
if (singleTreeTraversal || oldNamedElement == null || newNamedElement == null
|| !oldNodeKey.equals(newNodeKey)
) {
String idKey = currentKey() + NamedListNode.NAME;
resMap.put(idKey, deletion || newNamedElement == null ? null : newNodeKey);
}
return null;
});
}
return null;
}
/**
* Here we must list all joined keys belonging to deleted or created element. The only way to do it is to
* traverse the entire configuration tree unconditionally.
*/
private void visitAsymmetricInnerNode(InnerNode node, boolean delete) {
assert !singleTreeTraversal;
assert node != null;
oldInnerNodesStack.push(node);
singleTreeTraversal = true;
deletion = delete;
node.traverseChildren(this, true);
deletion = false;
singleTreeTraversal = false;
oldInnerNodesStack.pop();
}
}
}