You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ib...@apache.org on 2021/02/09 10:48:28 UTC

[ignite-3] branch main updated: IGNITE-14121 Ability to generate configuration trees from arbitrary sources (#44)

This is an automated email from the ASF dual-hosted git repository.

ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new bca081e  IGNITE-14121 Ability to generate configuration trees from arbitrary sources (#44)
bca081e is described below

commit bca081e2d05a23bef524e56f1c0ea5f47c298c1a
Author: ibessonov <be...@gmail.com>
AuthorDate: Tue Feb 9 13:48:22 2021 +0300

    IGNITE-14121 Ability to generate configuration trees from arbitrary sources (#44)
---
 .../processor/internal/Processor.java              |  72 +++++-
 .../sample/ConstructableTreeNodeTest.java          | 147 ++++++++++++
 ...NodesTest.java => TraversableTreeNodeTest.java} |  10 +-
 .../configuration/util/ConfigurationUtilTest.java  |  97 +++++++-
 .../configuration/tree/ConfigurationSource.java    |  40 ++++
 .../configuration/tree/ConstructableTreeNode.java  |  41 ++++
 .../ignite/configuration/tree/InnerNode.java       |  37 ++-
 .../ignite/configuration/tree/NamedListNode.java   |  20 +-
 .../configuration/util/ConfigurationUtil.java      | 251 ++++++++++++++++++++-
 9 files changed, 682 insertions(+), 33 deletions(-)

diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
index f8fe5ef..ef64d72 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
@@ -74,6 +74,7 @@ import org.apache.ignite.configuration.processor.internal.pojo.ChangeClassGenera
 import org.apache.ignite.configuration.processor.internal.pojo.InitClassGenerator;
 import org.apache.ignite.configuration.processor.internal.pojo.ViewClassGenerator;
 import org.apache.ignite.configuration.processor.internal.validation.ValidationGenerator;
+import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.tree.NamedListChange;
@@ -875,11 +876,20 @@ public class Processor extends AbstractProcessor {
             .addParameter(ClassName.get(ConfigurationVisitor.class), "visitor")
             .beginControlFlow("switch (key)");
 
+        MethodSpec.Builder constructBuilder = MethodSpec.methodBuilder("construct")
+            .addAnnotation(Override.class)
+            .addJavadoc("{@inheritDoc}")
+            .addModifiers(PUBLIC)
+            .returns(TypeName.VOID)
+            .addParameter(ClassName.get(String.class), "key")
+            .addParameter(ClassName.get(ConfigurationSource.class), "src")
+            .beginControlFlow("switch (key)");
+
         ClassName consumerClsName = ClassName.get(Consumer.class);
 
         for (VariableElement field : fields) {
             Value valAnnotation = field.getAnnotation(Value.class);
-            boolean immutable = valAnnotation != null && valAnnotation.immutable();
+            boolean mutable = valAnnotation == null || !valAnnotation.immutable();
 
             String fieldName = field.getSimpleName().toString();
             TypeName schemaFieldType = TypeName.get(field.asType());
@@ -907,6 +917,8 @@ public class Processor extends AbstractProcessor {
                 ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Node")
             );
 
+            TypeName namedListParamType = nodeFieldType;
+
             if (namedListField) {
                 viewFieldType = ParameterizedTypeName.get(ClassName.get(NamedListView.class), WildcardTypeName.subtypeOf(viewFieldType));
 
@@ -921,7 +933,7 @@ public class Processor extends AbstractProcessor {
                 FieldSpec.Builder nodeFieldBuilder = FieldSpec.builder(nodeFieldType, fieldName, PRIVATE);
 
                 if (namedListField)
-                    nodeFieldBuilder.initializer("new $T<>($T::new)", NamedListNode.class, ((ParameterizedTypeName)nodeFieldType).typeArguments.get(0));
+                    nodeFieldBuilder.initializer("new $T<>($T::new)", NamedListNode.class, namedListParamType);
 
                 nodeClsBuilder.addField(nodeFieldBuilder.build());
             }
@@ -940,13 +952,13 @@ public class Processor extends AbstractProcessor {
                         .addAnnotation(Override.class)
                         .addModifiers(PUBLIC)
                         .returns(leafField ? viewFieldType : nodeFieldType)
-                        .addStatement("return $L", fieldName); //TODO Explicit null check?
+                        .addStatement("return $L", fieldName);
 
                     nodeClsBuilder.addMethod(nodeGetMtdBuilder.build());
                 }
             }
 
-            if (!immutable) {
+            if (mutable) {
                 String changeMtdName = "change" + capitalize(fieldName);
 
                 {
@@ -1086,15 +1098,65 @@ public class Processor extends AbstractProcessor {
                         .addStatement(INDENT + "break");
                 }
             }
+
+            {
+                if (leafField) {
+                    constructBuilder.addStatement(
+                        "case $S: $L = src == null ? null : src.unwrap($T.class)",
+                        fieldName,
+                        fieldName,
+                        schemaFieldType.box()
+                    )
+                    .addStatement(INDENT + "break");
+                }
+                else if (namedListField) {
+                    constructBuilder
+                        .addStatement(
+                            "case $S: if (src == null) $L = new $T<>($T::new)",
+                            fieldName,
+                            fieldName,
+                            NamedListNode.class,
+                            namedListParamType
+                        )
+                        .addStatement(
+                            INDENT + "else src.descend($L = $L.copy())",
+                            fieldName,
+                            fieldName
+                        )
+                        .addStatement(INDENT + "break");
+                }
+                else {
+                    constructBuilder
+                        .addStatement(
+                            "case $S: if (src == null) $L = null",
+                            fieldName,
+                            fieldName
+                        )
+                        .addStatement(
+                            INDENT + "else src.descend($L = ($L == null ? new $T() : ($T)$L.copy()))",
+                            fieldName,
+                            fieldName,
+                            nodeFieldType,
+                            nodeFieldType,
+                            fieldName
+                        )
+                        .addStatement(INDENT + "break");
+                }
+            }
         }
 
         traverseChildBuilder
             .addStatement("default: throw new $T(key)", NoSuchElementException.class)
             .endControlFlow();
 
+        constructBuilder
+            .addStatement("default: throw new $T(key)", NoSuchElementException.class)
+            .endControlFlow();
+
         nodeClsBuilder
             .addMethod(traverseChildrenBuilder.build())
-            .addMethod(traverseChildBuilder.build());
+            .addMethod(traverseChildBuilder.build())
+            .addMethod(constructBuilder.build());
 
         TypeSpec viewCls = viewClsBuilder.build();
         TypeSpec changeCls = changeClsBuilder.build();
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java
new file mode 100644
index 0000000..9791637
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.configuration.sample;
+
+import java.util.Collections;
+import java.util.NoSuchElementException;
+import org.apache.ignite.configuration.sample.impl.ChildNode;
+import org.apache.ignite.configuration.sample.impl.NamedElementNode;
+import org.apache.ignite.configuration.sample.impl.ParentNode;
+import org.apache.ignite.configuration.tree.ConfigurationSource;
+import org.apache.ignite.configuration.tree.ConstructableTreeNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** */
+public class ConstructableTreeNodeTest {
+    /** */
+    @Test
+    public void noKey() {
+        var childNode = new ChildNode();
+
+        assertThrows(NoSuchElementException.class, () -> childNode.construct("foo", null));
+    }
+
+    /** */
+    @Test
+    public void nullSource() {
+        var parentNode = new ParentNode().initChild(child ->
+            child.initStrCfg("value")
+        )
+        .initElements(elements ->
+            elements.put("name", element -> {})
+        );
+
+        // Named list node.
+        NamedListNode<NamedElementNode> elements = parentNode.elements();
+
+        parentNode.construct("elements", null);
+
+        assertNotNull(parentNode.elements());
+        assertNotSame(elements, parentNode.elements());
+        assertEquals(Collections.emptySet(), parentNode.elements().namedListKeys());
+
+        // Inner node.
+        NamedElementNode element = elements.get("name");
+
+        elements.construct("name", null);
+
+        assertNull(elements.get("name"));
+
+        // Leaf.
+        element.construct("strCfg", null);
+
+        assertNull(element.strCfg());
+    }
+
+    /** */
+    private static class ConstantConfigurationSource implements ConfigurationSource {
+        /** */
+        private final Object constant;
+
+        /**
+         * @param constant Constant.
+         */
+        private ConstantConfigurationSource(Object constant) {
+            this.constant = constant;
+        }
+
+        /** {@inheritDoc} */
+        @Override public <T> T unwrap(Class<T> clazz) {
+            return (T)constant;
+        }
+    }
+
+    /** */
+    @Test
+    public void unwrap() {
+        var childNode = new ChildNode();
+
+        childNode.construct("strCfg", new ConstantConfigurationSource("value"));
+
+        assertEquals("value", childNode.strCfg());
+
+        childNode.construct("intCfg", new ConstantConfigurationSource(255));
+
+        assertEquals(255, childNode.intCfg());
+
+        assertThrows(ClassCastException.class, () ->
+            childNode.construct("intCfg", new ConstantConfigurationSource(new Object()))
+        );
+    }
+
+    /** */
+    @Test
+    public void descend() {
+        // Inner node.
+        var parentNode = new ParentNode();
+
+        parentNode.construct("child", new ConfigurationSource() {
+            @Override public <T> T unwrap(Class<T> clazz) {
+                throw new UnsupportedOperationException("unwrap");
+            }
+
+            @Override public void descend(ConstructableTreeNode node) {
+                node.construct("strCfg", new ConstantConfigurationSource("value"));
+            }
+        });
+
+        assertEquals("value", parentNode.child().strCfg());
+
+        // Named list node.
+        NamedListNode<NamedElementNode> elementsNode = parentNode.elements();
+
+        elementsNode.construct("name", new ConfigurationSource() {
+            @Override public <T> T unwrap(Class<T> clazz) {
+                throw new UnsupportedOperationException("unwrap");
+            }
+
+            @Override public void descend(ConstructableTreeNode node) {
+                node.construct("strCfg", new ConstantConfigurationSource("value"));
+            }
+        });
+
+        assertEquals("value", elementsNode.get("name").strCfg());
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableNodesTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
similarity index 98%
rename from modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableNodesTest.java
rename to modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
index 2646711..393f17c 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableNodesTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
@@ -44,7 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /** */
-public class TraversableNodesTest {
+public class TraversableTreeNodeTest {
     /** */
     @Config
     public static class ParentConfigurationSchema {
@@ -165,9 +165,7 @@ public class TraversableNodesTest {
      */
     @Test
     public void initLeaf() {
-        var childNode = new ChildNode();
-
-        childNode.initStrCfg("value");
+        var childNode = new ChildNode().initStrCfg("value");
 
         assertEquals("value", childNode.strCfg());
     }
@@ -177,9 +175,7 @@ public class TraversableNodesTest {
      */
     @Test
     public void initInnerChild() {
-        var parentNode = new ParentNode();
-
-        parentNode.initChild(child -> {});
+        var parentNode = new ParentNode().initChild(child -> {});
 
         ChildNode childNode = parentNode.child();
 
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/util/ConfigurationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/util/ConfigurationUtilTest.java
index 45cc921..6b12e28 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/util/ConfigurationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/util/ConfigurationUtilTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.configuration.util;
 
 import java.util.List;
+import java.util.Map;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
@@ -25,7 +26,10 @@ import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.util.impl.ParentNode;
 import org.junit.jupiter.api.Test;
 
+import static java.util.Collections.singletonMap;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -95,9 +99,7 @@ public class ConfigurationUtilTest {
     /** */
     @Test
     public void findSuccessfully() {
-        var parent = new ParentNode();
-
-        parent.changeElements(elements ->
+        var parent = new ParentNode().changeElements(elements ->
             elements.put("name", element ->
                 element.changeChild(child ->
                     child.changeStr("value")
@@ -171,4 +173,93 @@ public class ConfigurationUtilTest {
             () -> ConfigurationUtil.find(List.of("elements", "name", "child", "str", "foo"), parent)
         );
     }
+
+    /** */
+    @Test
+    public void fillFromSuffixMapSuccessfully() {
+        var parentNode = new ParentNode();
+
+        ConfigurationUtil.fillFromSuffixMap(parentNode, Map.of(
+            "elements", Map.of(
+                "name1", Map.of(
+                    "child", Map.of("str", "value1")
+                ),
+                "name2", Map.of(
+                    "child", Map.of("str", "value2")
+                )
+            )
+        ));
+
+        assertEquals("value1", parentNode.elements().get("name1").child().str());
+        assertEquals("value2", parentNode.elements().get("name2").child().str());
+    }
+
+    /** */
+    @Test
+    public void fillFromSuffixMapSuccessfullyWithRemove() {
+        var parentNode = new ParentNode().changeElements(elements ->
+            elements.put("name", element ->
+                element.changeChild(child -> {})
+            )
+        );
+
+        ConfigurationUtil.fillFromSuffixMap(parentNode, Map.of(
+            "elements", singletonMap("name", null)
+        ));
+
+        assertNull(parentNode.elements().get("node"));
+    }
+
+    /** */
+    @Test
+    public void patch() {
+        var originalRoot = new ParentNode().initElements(elements ->
+            elements.put("name1", element ->
+                element.initChild(child -> child.initStr("value1"))
+            )
+        );
+
+        // Updating config.
+        ParentNode updatedRoot = ConfigurationUtil.patch(originalRoot, new ParentNode().changeElements(elements ->
+            elements.put("name1", element ->
+                element.changeChild(child -> child.changeStr("value2"))
+            )
+        ));
+
+        assertNotSame(originalRoot, updatedRoot);
+        assertNotSame(originalRoot.elements(), updatedRoot.elements());
+        assertNotSame(originalRoot.elements().get("name1"), updatedRoot.elements().get("name1"));
+        assertNotSame(originalRoot.elements().get("name1").child(), updatedRoot.elements().get("name1").child());
+
+        assertEquals("value1", originalRoot.elements().get("name1").child().str());
+        assertEquals("value2", updatedRoot.elements().get("name1").child().str());
+
+        // Expanding config.
+        ParentNode expandedRoot = ConfigurationUtil.patch(originalRoot, new ParentNode().changeElements(elements ->
+            elements.put("name2", element ->
+                element.changeChild(child -> child.changeStr("value2"))
+            )
+        ));
+
+        assertNotSame(originalRoot, expandedRoot);
+        assertNotSame(originalRoot.elements(), expandedRoot.elements());
+
+        assertSame(originalRoot.elements().get("name1"), expandedRoot.elements().get("name1"));
+        assertNull(originalRoot.elements().get("name2"));
+        assertNotNull(expandedRoot.elements().get("name2"));
+
+        assertEquals("value2", expandedRoot.elements().get("name2").child().str());
+
+        // Shrinking config.
+        ParentNode shrinkedRoot = ConfigurationUtil.patch(expandedRoot, new ParentNode().changeElements(elements ->
+            elements.remove("name1")
+        ));
+
+        assertNotSame(expandedRoot, shrinkedRoot);
+        assertNotSame(expandedRoot.elements(), shrinkedRoot.elements());
+
+        assertNotNull(expandedRoot.elements().get("name1"));
+        assertNull(shrinkedRoot.elements().get("name1"));
+        assertNotNull(shrinkedRoot.elements().get("name2"));
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationSource.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationSource.java
new file mode 100644
index 0000000..b1d6e30
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationSource.java
@@ -0,0 +1,40 @@
+/*
+ * 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.configuration.tree;
+
+/** */
+public interface ConfigurationSource {
+    /**
+     * Treat current configuration source as a leaf value and try to convert it to the specific class.
+     * Failing behaviour is not specified and depends on the implementation.
+     *
+     * @param <T> Type of the object for type safety during compilation.
+     * @param clazz Class instance of type to convert to.
+     * @return Converted leaf object.
+     */
+    <T> T unwrap(Class<T> clazz);
+
+    /**
+     * Treats current configuration source as an inner node. Tries to construct the content of {@code node} using
+     * available data from the source.
+     *
+     * @param node Constructable node which content will be modified by the configuration source.
+     */
+    default void descend(ConstructableTreeNode node) {
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java
new file mode 100644
index 0000000..5cad648
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java
@@ -0,0 +1,41 @@
+/*
+ * 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.configuration.tree;
+
+import java.util.NoSuchElementException;
+
+/** */
+public interface ConstructableTreeNode {
+    /**
+     * Initializes {@code key} element of the object with the content from the source.
+     * Please refer to implementation to find out exact details.
+     *
+     * @param key Field / named list element name to be constructed.
+     * @param src Source that provides data for construction.
+     * @throws NoSuchElementException If {@code key} cannot be constructed.
+     */
+    void construct(String key, ConfigurationSource src) throws NoSuchElementException;
+
+    /**
+     * Public equivalent of {@link Object#clone()} method. Creates a copy with effectively the same content.
+     * Helps to preserve trees immutability after construction is completed.
+     *
+     * @return Copy of the object.
+     */
+    ConstructableTreeNode copy();
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
index 8a14b1a..84e440c 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
@@ -20,7 +20,7 @@ package org.apache.ignite.configuration.tree;
 import java.util.NoSuchElementException;
 
 /** */
-public abstract class InnerNode implements TraversableTreeNode, Cloneable {
+public abstract class InnerNode implements TraversableTreeNode, ConstructableTreeNode, Cloneable {
     /** {@inheritDoc} */
     @Override public final void accept(String key, ConfigurationVisitor visitor) {
         visitor.visitInnerNode(key, this);
@@ -79,10 +79,41 @@ public abstract class InnerNode implements TraversableTreeNode, Cloneable {
      */
     public abstract void traverseChild(String key, ConfigurationVisitor visitor) throws NoSuchElementException;
 
+    /**
+     * Method with auto-generated implementation. Must look like this:
+     * <pre>{@code
+     * @Override public abstract void construct(String key, ConfigurationSource src) throws NoSuchElementException {
+     *     switch (key) {
+     *         case "namedList":
+     *             if (src == null)
+     *                 namedList = new NamedListNode<>(Foo::new);
+     *             else
+     *                 src.descend(namedList = namedList.copy());
+     *             break;
+     *
+     *         case "innerNode":
+     *             if (src == null)
+     *                 innerNode = null;
+     *             else
+     *                 src.descend(innerNode = (innerNode == null ? new Bar() : (Bar)innerNode.copy()));
+     *             break;
+     *
+     *         case "leaf":
+     *             leaf = src == null ? null : src.unwrap(Integer.class);
+     *             break;
+     *
+     *         default: throw new NoSuchElementException(key);
+     *     }
+     * }
+     * }</pre>
+     * {@inheritDoc}
+     */
+    @Override public abstract void construct(String key, ConfigurationSource src) throws NoSuchElementException;
+
     /** {@inheritDoc} */
-    @Override protected Object clone() {
+    @Override public InnerNode copy() {
         try {
-            return super.clone();
+            return (InnerNode)clone();
         }
         catch (CloneNotSupportedException e) {
             throw new IllegalStateException(e);
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
index 38ce004..648e2cb 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
@@ -26,7 +26,7 @@ import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /** */
-public final class NamedListNode<N extends InnerNode> implements NamedListView<N>, NamedListChange<N>, TraversableTreeNode, Cloneable {
+public final class NamedListNode<N extends InnerNode> implements NamedListView<N>, NamedListChange<N>, TraversableTreeNode, ConstructableTreeNode {
     /** */
     private final Supplier<N> valSupplier;
 
@@ -95,13 +95,23 @@ public final class NamedListNode<N extends InnerNode> implements NamedListView<N
         return this;
     }
 
-    /** */
-    public void delete(String key) {
-        map.remove(key);
+    /** {@inheritDoc} */
+    @Override public void construct(String key, ConfigurationSource src) {
+        if (src == null)
+            map.remove(key);
+        else {
+            N val = map.get(key);
+
+            val = val == null ? valSupplier.get() : (N)val.copy();
+
+            map.put(key, val);
+
+            src.descend(val);
+        }
     }
 
     /** {@inheritDoc} */
-    @Override public Object clone() {
+    @Override public NamedListNode<N> copy() {
         return new NamedListNode<>(this);
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/util/ConfigurationUtil.java
index c752fc8..1fb8a73 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/util/ConfigurationUtil.java
@@ -20,28 +20,48 @@ package org.apache.ignite.configuration.util;
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.RandomAccess;
 import java.util.stream.Collectors;
+import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.ConstructableTreeNode;
 import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.tree.NamedListNode;
 import org.apache.ignite.configuration.tree.TraversableTreeNode;
 
 /** */
-public interface ConfigurationUtil {
-    /** */
-    static String escape(String key) {
+public class ConfigurationUtil {
+    /**
+     * Replaces all {@code .} and {@code \} characters with {@code \.} and {@code \\} respectively.
+     *
+     * @param key Unescaped string.
+     * @return Escaped string.
+     */
+    public static String escape(String key) {
         return key.replaceAll("([.\\\\])", "\\\\$1");
     }
 
-    /** */
-    static String unescape(String key) {
+    /**
+     * Replaces all {@code \.} and {@code \\} with {@code .} and {@code \} respectively.
+     *
+     * @param key Escaped string.
+     * @return Unescaped string.
+     */
+    public static String unescape(String key) {
         return key.replaceAll("\\\\([.\\\\])", "$1");
     }
 
-    /** */
-    static List<String> split(String keys) {
+    /**
+     * Splits string using unescaped {@code .} character as a separator.
+     *
+     * @param keys Qualified key where escaped subkeys are joined with dots.
+     * @return List of unescaped subkeys.
+     * @see #unescape(String)
+     * @see #join(List)
+     */
+    public static List<String> split(String keys) {
         String[] split = keys.split("(?<!\\\\)[.]", -1);
 
         for (int i = 0; i < split.length; i++)
@@ -50,8 +70,15 @@ public interface ConfigurationUtil {
         return Arrays.asList(split);
     }
 
-    /** */
-    static String join(List<String> keys) {
+    /**
+     * Joins list of keys with {@code .} character as a separator. All keys are preemptively escaped.
+     *
+     * @param keys List of unescaped keys.
+     * @return Escaped keys joined with dots.
+     * @see #escape(String)
+     * @see #split(String)
+     */
+    public static String join(List<String> keys) {
         return keys.stream().map(ConfigurationUtil::escape).collect(Collectors.joining("."));
     }
 
@@ -63,7 +90,7 @@ public interface ConfigurationUtil {
      * @return Either {@link TraversableTreeNode} or {@link Serializable} depending on the keys and schema.
      * @throws KeyNotFoundException If node is not found.
      */
-    static Object find(List<String> keys, TraversableTreeNode node) throws KeyNotFoundException {
+    public static Object find(List<String> keys, TraversableTreeNode node) throws KeyNotFoundException {
         assert keys instanceof RandomAccess : keys.getClass();
 
         var visitor = new ConfigurationVisitor() {
@@ -110,4 +137,208 @@ public interface ConfigurationUtil {
 
         return visitor.res;
     }
+
+    /**
+     * Convert Map tree to configuration tree. No error handling here.
+     *
+     * @param node Node to fill. Not necessarily empty.
+     * @param prefixMap Map of {@link Serializable} values or other prefix maps (recursive structure).
+     *      Every key is unescaped.
+     * @throws UnsupportedOperationException if prefix map structure doesn't correspond to actual tree structure.
+     *      This will be fixed when method is actually used in configuration storage intergration.
+     */
+    public static void fillFromSuffixMap(ConstructableTreeNode node, Map<String, ?> prefixMap) {
+        assert node instanceof InnerNode;
+
+        /** */
+        class LeafConfigurationSource implements ConfigurationSource {
+            /** */
+            private final Serializable val;
+
+            /**
+             * @param val Value.
+             */
+            private LeafConfigurationSource(Serializable val) {
+                this.val = val;
+            }
+
+            /** {@inheritDoc} */
+            @Override public <T> T unwrap(Class<T> clazz) {
+                assert val == null || clazz.isInstance(val);
+
+                return clazz.cast(val);
+            }
+
+            /** {@inheritDoc} */
+            @Override public void descend(ConstructableTreeNode node) {
+                throw new UnsupportedOperationException("descend");
+            }
+        }
+
+        /** */
+        class InnerConfigurationSource implements ConfigurationSource {
+            /** */
+            private final Map<String, ?> map;
+
+            /**
+             * @param map Prefix map.
+             */
+            private InnerConfigurationSource(Map<String, ?> map) {
+                this.map = map;
+            }
+
+            /** {@inheritDoc} */
+            @Override public <T> T unwrap(Class<T> clazz) {
+                throw new UnsupportedOperationException("unwrap");
+            }
+
+            /** {@inheritDoc} */
+            @Override public void descend(ConstructableTreeNode node) {
+                for (Map.Entry<String, ?> entry : map.entrySet()) {
+                    String key = entry.getKey();
+                    Object val = entry.getValue();
+
+                    assert val == null || val instanceof Map || val instanceof Serializable;
+
+                    if (val == null)
+                        node.construct(key, null);
+                    else if (val instanceof Map)
+                        node.construct(key, new InnerConfigurationSource((Map<String, ?>)val));
+                    else
+                        node.construct(key, new LeafConfigurationSource((Serializable)val));
+                }
+            }
+        }
+
+        var src = new InnerConfigurationSource(prefixMap);
+
+        src.descend(node);
+    }
+
+    /**
+     * Apply changes on top of existing node. Creates completely new object while reusing parts of the original tree
+     * that weren't modified.
+     *
+     * @param root Immutable configuration node.
+     * @param changes Change or Init object to be applied.
+     */
+    public static <C extends ConstructableTreeNode> C patch(C root, TraversableTreeNode changes) {
+        assert root.getClass() == changes.getClass(); // Yes.
+
+        var scrHolder = new ConfigurationVisitor() {
+            ConfigurationSource src;
+
+            @Override public void visitInnerNode(String key, InnerNode node) {
+                src = new PatchInnerConfigurationSource(node);
+            }
+
+            @Override public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                src = new PatchNamedListConfigurationSource(node);
+            }
+        };
+
+        changes.accept(null, scrHolder);
+
+        assert scrHolder.src != null;
+
+        C copy = (C)root.copy();
+
+        scrHolder.src.descend(copy);
+
+        return copy;
+    }
+
+    /** */
+    private static class PatchLeafConfigurationSource implements ConfigurationSource {
+        /** */
+        private final Serializable val;
+
+        /**
+         * @param val Value.
+         */
+        PatchLeafConfigurationSource(Serializable val) {
+            this.val = val;
+        }
+
+        /** {@inheritDoc} */
+        @Override public <T> T unwrap(Class<T> clazz) {
+            assert clazz.isInstance(val);
+
+            return clazz.cast(val);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void descend(ConstructableTreeNode node) {
+            throw new UnsupportedOperationException("descend");
+        }
+    }
+
+    /** */
+    private static class PatchInnerConfigurationSource implements ConfigurationSource {
+        /** */
+        private final InnerNode srcNode;
+
+        /**
+         * @param srcNode Inner node.
+         */
+        PatchInnerConfigurationSource(InnerNode srcNode) {
+            this.srcNode = srcNode;
+        }
+
+        /** {@inheritDoc} */
+        @Override public <T> T unwrap(Class<T> clazz) {
+            throw new UnsupportedOperationException("unwrap");
+        }
+
+        /** {@inheritDoc} */
+        @Override public void descend(ConstructableTreeNode dstNode) {
+            assert srcNode.getClass() == dstNode.getClass();
+
+            srcNode.traverseChildren(new ConfigurationVisitor() {
+                @Override public void visitLeafNode(String key, Serializable val) {
+                    if (val != null)
+                        dstNode.construct(key, new PatchLeafConfigurationSource(val));
+                }
+
+                @Override public void visitInnerNode(String key, InnerNode node) {
+                    if (node != null)
+                        dstNode.construct(key, new PatchInnerConfigurationSource(node));
+                }
+
+                @Override public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                    if (node != null)
+                        dstNode.construct(key, new PatchNamedListConfigurationSource(node));
+                }
+            });
+        }
+    }
+
+    /** */
+    private static class PatchNamedListConfigurationSource implements ConfigurationSource {
+        /** */
+        private final NamedListNode<?> srcNode;
+
+        /**
+         * @param srcNode Named list node.
+         */
+        PatchNamedListConfigurationSource(NamedListNode<?> srcNode) {
+            this.srcNode = srcNode;
+        }
+
+        /** {@inheritDoc} */
+        @Override public <T> T unwrap(Class<T> clazz) {
+            throw new UnsupportedOperationException("unwrap");
+        }
+
+        /** {@inheritDoc} */
+        @Override public void descend(ConstructableTreeNode dstNode) {
+            assert srcNode.getClass() == dstNode.getClass();
+
+            for (String key : srcNode.namedListKeys()) {
+                InnerNode node = srcNode.get(key);
+
+                dstNode.construct(key, node == null ? null : new PatchInnerConfigurationSource(node));
+            }
+        }
+    }
 }