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/01 12:26:46 UTC

[ignite-3] branch main updated: IGNITE-14087 Code generation for traversable configuration tree structure. (#38)

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 7267afc  IGNITE-14087 Code generation for traversable configuration tree structure. (#38)
7267afc is described below

commit 7267afcbdada0b580e88f00062cda512cc34e5eb
Author: ibessonov <be...@gmail.com>
AuthorDate: Mon Feb 1 15:26:25 2021 +0300

    IGNITE-14087 Code generation for traversable configuration tree structure. (#38)
    
    Signed-off-by: ibessonov <be...@gmail.com>
---
 .../processor/internal/Processor.java              | 363 +++++++++++++++++++-
 .../processor/internal/ProcessorTest.java          |  22 +-
 .../configuration/sample/TraversableNodesTest.java | 369 +++++++++++++++++++++
 .../internal/TestConfigurationSchema.java          |   6 -
 .../ignite/configuration/annotation/Value.java     |  10 +-
 .../configuration/tree/ConfigurationVisitor.java   |   9 +-
 .../ignite/configuration/tree/InnerNode.java       |   8 +-
 .../ignite/configuration/tree/NamedListNode.java   |   2 +-
 8 files changed, 750 insertions(+), 39 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 b86a3c5..cc85bc9 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
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.configuration.processor.internal;
 
+import com.squareup.javapoet.AnnotationSpec;
 import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
 import com.squareup.javapoet.FieldSpec;
@@ -38,7 +39,9 @@ import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import javax.annotation.processing.AbstractProcessor;
@@ -46,6 +49,7 @@ import javax.annotation.processing.Filer;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
 import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.PackageElement;
@@ -70,6 +74,11 @@ 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.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListChange;
+import org.apache.ignite.configuration.tree.NamedListNode;
+import org.apache.ignite.configuration.tree.NamedListView;
 
 import static javax.lang.model.element.Modifier.ABSTRACT;
 import static javax.lang.model.element.Modifier.FINAL;
@@ -81,6 +90,9 @@ import static javax.lang.model.element.Modifier.STATIC;
  * Annotation processor that produces configuration classes.
  */
 public class Processor extends AbstractProcessor {
+    /** Java file padding. */
+    private static final String INDENT = "    ";
+
     /** Wildcard (?) TypeName. */
     private static final TypeName WILDCARD = WildcardTypeName.subtypeOf(Object.class);
 
@@ -188,6 +200,8 @@ public class Processor extends AbstractProcessor {
             CodeBlock.Builder copyConstructorBodyBuilder = CodeBlock.builder();
 
             for (VariableElement field : fields) {
+                Element fieldTypeElement = processingEnv.getTypeUtils().asElement(field.asType());
+
                 // Get original field type (must be another configuration schema or "primitive" like String or long)
                 final TypeName baseType = TypeName.get(field.asType());
 
@@ -203,6 +217,13 @@ public class Processor extends AbstractProcessor {
 
                 final ConfigValue confAnnotation = field.getAnnotation(ConfigValue.class);
                 if (confAnnotation != null) {
+                    if (fieldTypeElement.getAnnotation(Config.class) == null) {
+                        throw new ProcessorException(
+                            "Class for @ConfigValue field must be defined as @Config: " +
+                                clazz.getQualifiedName() + "." + field.getSimpleName()
+                        );
+                    }
+
                     // Create DynamicConfiguration (descendant) field
                     final FieldSpec nestedConfigField =
                         FieldSpec
@@ -220,6 +241,13 @@ public class Processor extends AbstractProcessor {
 
                 final NamedConfigValue namedConfigAnnotation = field.getAnnotation(NamedConfigValue.class);
                 if (namedConfigAnnotation != null) {
+                    if (fieldTypeElement.getAnnotation(Config.class) == null) {
+                        throw new ProcessorException(
+                            "Class for @NamedConfigValue field must be defined as @Config: " +
+                                clazz.getQualifiedName() + "." + field.getSimpleName()
+                        );
+                    }
+
                     ClassName fieldType = Utils.getConfigurationName((ClassName) baseType);
 
                     // Create NamedListConfiguration<> field
@@ -247,6 +275,21 @@ public class Processor extends AbstractProcessor {
 
                 final Value valueAnnotation = field.getAnnotation(Value.class);
                 if (valueAnnotation != null) {
+                    switch (baseType.toString()) {
+                        case "boolean":
+                        case "int":
+                        case "long":
+                        case "double":
+                        case "java.lang.String":
+                            break;
+
+                        default:
+                            throw new ProcessorException(
+                                "@Value " + clazz.getQualifiedName() + "." + field.getSimpleName() + " field must" +
+                                    " have one of the following types: boolean, int, long, double, String."
+                            );
+                    }
+
                     // Create value (DynamicProperty<>) field
                     final FieldSpec generatedField = FieldSpec.builder(getMethodType, fieldName, Modifier.PRIVATE, FINAL).build();
 
@@ -275,7 +318,7 @@ public class Processor extends AbstractProcessor {
             createPojoBindings(packageName, fields, schemaClassName, configurationClassBuilder, configurationInterfaceBuilder);
 
             if (isRoot)
-                createRootKeyField(configInterface, configurationClassBuilder, configDesc);
+                createRootKeyField(configInterface, configurationInterfaceBuilder, configDesc);
 
             // Create constructors for configuration class
             createConstructors(configClass, configName, configurationClassBuilder, CONFIGURATOR_TYPE, constructorBodyBuilder, copyConstructorBodyBuilder);
@@ -335,8 +378,7 @@ public class Processor extends AbstractProcessor {
                 .build()).build();
 
         FieldSpec keyField = FieldSpec.builder(
-            fieldTypeName, "KEY", PUBLIC, STATIC
-            )
+            fieldTypeName, "KEY", PUBLIC, STATIC, FINAL)
             .initializer("$L", anonymousClass)
             .build();
 
@@ -560,10 +602,10 @@ public class Processor extends AbstractProcessor {
         configurationClassBuilder.addMethod(copyConstructor);
 
         final MethodSpec emptyConstructor = MethodSpec.constructorBuilder()
-                .addModifiers(PUBLIC)
-                .addParameter(configuratorClassName, "configurator")
-                .addStatement("this($S, $S, false, configurator, null)", "", configName)
-                .build();
+            .addModifiers(PUBLIC)
+            .addParameter(configuratorClassName, "configurator")
+            .addStatement("this($S, $S, false, configurator, null)", "", configName)
+            .build();
 
         configurationClassBuilder.addMethod(emptyConstructor);
     }
@@ -776,6 +818,311 @@ public class Processor extends AbstractProcessor {
         catch (IOException e) {
             throw new ProcessorException("Failed to write class " + initClassName.toString(), e);
         }
+
+        // This code will be refactored in the future. Right now I don't want to entangle it with existing code
+        // generation. It has only a few considerable problems - hardcode and a lack of proper arrays handling.
+        // Clone method should be used to guarantee data integrity.
+        ClassName viewClsName = ClassName.get(
+            schemaClassName.packageName(),
+            schemaClassName.simpleName().replace("ConfigurationSchema", "View")
+        );
+
+        ClassName changeClsName = ClassName.get(
+            schemaClassName.packageName(),
+            schemaClassName.simpleName().replace("ConfigurationSchema", "Change")
+        );
+
+        ClassName initClsName = ClassName.get(
+            schemaClassName.packageName(),
+            schemaClassName.simpleName().replace("ConfigurationSchema", "Init")
+        );
+
+        ClassName nodeClsName = ClassName.get(
+            schemaClassName.packageName() + ".impl",
+            schemaClassName.simpleName().replace("ConfigurationSchema", "Node")
+        );
+
+        TypeSpec.Builder viewClsBuilder = TypeSpec.interfaceBuilder(viewClsName)
+            .addModifiers(PUBLIC);
+
+        TypeSpec.Builder changeClsBuilder = TypeSpec.interfaceBuilder(changeClsName)
+            .addModifiers(PUBLIC);
+
+        TypeSpec.Builder initClsBuilder = TypeSpec.interfaceBuilder(initClsName)
+            .addModifiers(PUBLIC);
+
+        TypeSpec.Builder nodeClsBuilder = TypeSpec.classBuilder(nodeClsName)
+            .addModifiers(PUBLIC, FINAL)
+            .superclass(ClassName.get(InnerNode.class))
+            .addSuperinterface(viewClsName)
+            .addSuperinterface(changeClsName)
+            .addSuperinterface(initClsName);
+
+        MethodSpec.Builder traverseChildrenBuilder = MethodSpec.methodBuilder("traverseChildren")
+            .addAnnotation(Override.class)
+            .addJavadoc("{@inheritDoc}")
+            .addModifiers(PUBLIC)
+            .returns(TypeName.VOID)
+            .addParameter(ClassName.get(ConfigurationVisitor.class), "visitor");
+
+        MethodSpec.Builder traverseChildBuilder = MethodSpec.methodBuilder("traverseChild")
+            .addAnnotation(Override.class)
+            .addJavadoc("{@inheritDoc}")
+            .addModifiers(PUBLIC)
+            .returns(TypeName.VOID)
+            .addException(NoSuchElementException.class)
+            .addParameter(ClassName.get(String.class), "key")
+            .addParameter(ClassName.get(ConfigurationVisitor.class), "visitor")
+            .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();
+
+            String fieldName = field.getSimpleName().toString();
+            TypeName schemaFieldType = TypeName.get(field.asType());
+
+            boolean leafField = schemaFieldType.isPrimitive() || !((ClassName)schemaFieldType).simpleName().contains("ConfigurationSchema");
+            boolean namedListField = field.getAnnotation(NamedConfigValue.class) != null;
+
+            TypeName viewFieldType = schemaFieldType.isPrimitive() ? schemaFieldType : ClassName.get(
+                ((ClassName)schemaFieldType).packageName(),
+                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "View")
+            );
+
+            TypeName changeFieldType = schemaFieldType.isPrimitive() ? schemaFieldType : ClassName.get(
+                ((ClassName)schemaFieldType).packageName(),
+                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Change")
+            );
+
+            TypeName initFieldType = schemaFieldType.isPrimitive() ? schemaFieldType : ClassName.get(
+                ((ClassName)schemaFieldType).packageName(),
+                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Init")
+            );
+
+            TypeName nodeFieldType = schemaFieldType.isPrimitive() ? schemaFieldType.box() : ClassName.get(
+                ((ClassName)schemaFieldType).packageName() + (leafField ? "" : ".impl"),
+                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Node")
+            );
+
+            if (namedListField) {
+                viewFieldType = ParameterizedTypeName.get(ClassName.get(NamedListView.class), WildcardTypeName.subtypeOf(viewFieldType));
+
+                changeFieldType = ParameterizedTypeName.get(ClassName.get(NamedListChange.class), changeFieldType);
+
+                initFieldType = ParameterizedTypeName.get(ClassName.get(NamedListChange.class), initFieldType);
+
+                nodeFieldType = ParameterizedTypeName.get(ClassName.get(NamedListNode.class), nodeFieldType);
+            }
+
+            {
+                FieldSpec.Builder nodeFieldBuilder = FieldSpec.builder(nodeFieldType, fieldName, PRIVATE);
+
+                if (namedListField)
+                    nodeFieldBuilder.initializer("new $T<>($T::new)", NamedListNode.class, ((ParameterizedTypeName)nodeFieldType).typeArguments.get(0));
+
+                nodeClsBuilder.addField(nodeFieldBuilder.build());
+            }
+
+            {
+                {
+                    MethodSpec.Builder getMtdBuilder = MethodSpec.methodBuilder(fieldName)
+                        .addModifiers(PUBLIC, ABSTRACT)
+                        .returns(viewFieldType);
+
+                    viewClsBuilder.addMethod(getMtdBuilder.build());
+                }
+
+                {
+                    MethodSpec.Builder nodeGetMtdBuilder = MethodSpec.methodBuilder(fieldName)
+                        .addAnnotation(Override.class)
+                        .addModifiers(PUBLIC)
+                        .returns(leafField ? viewFieldType : nodeFieldType)
+                        .addStatement("return $L", fieldName); //TODO Explicit null check?
+
+                    nodeClsBuilder.addMethod(nodeGetMtdBuilder.build());
+                }
+            }
+
+            if (!immutable) {
+                String changeMtdName = "change" + capitalize(fieldName);
+
+                {
+                    MethodSpec.Builder changeMtdBuilder = MethodSpec.methodBuilder(changeMtdName)
+                        .addModifiers(PUBLIC, ABSTRACT)
+                        .returns(changeClsName);
+
+                    if (valAnnotation != null)
+                        changeMtdBuilder.addParameter(changeFieldType, fieldName);
+                    else
+                        changeMtdBuilder.addParameter(ParameterizedTypeName.get(consumerClsName, changeFieldType), fieldName);
+
+                    changeClsBuilder.addMethod(changeMtdBuilder.build());
+                }
+
+                {
+                    MethodSpec.Builder nodeChangeMtdBuilder = MethodSpec.methodBuilder(changeMtdName)
+                        .addAnnotation(Override.class)
+                        .addModifiers(PUBLIC)
+                        .returns(changeClsName);
+
+                    if (valAnnotation != null) {
+                        nodeChangeMtdBuilder
+                            .addParameter(changeFieldType, fieldName)
+                            .addStatement("this.$L = $L", fieldName, fieldName);
+                    }
+                    else {
+                        String paramName = fieldName + "Consumer";
+                        nodeChangeMtdBuilder.addParameter(ParameterizedTypeName.get(consumerClsName, changeFieldType), paramName);
+
+                        if (!namedListField) {
+                            nodeChangeMtdBuilder.addStatement(
+                                "$L = $L == null ? new $T() : $L",
+                                fieldName,
+                                fieldName,
+                                nodeFieldType,
+                                fieldName
+                            );
+                            nodeChangeMtdBuilder.addStatement("$L.accept($L)", paramName, fieldName);
+                        }
+                        else {
+                            nodeChangeMtdBuilder.addAnnotation(
+                                AnnotationSpec.builder(SuppressWarnings.class)
+                                    .addMember("value", "$S", "unchecked")
+                                    .build()
+                            );
+
+                            nodeChangeMtdBuilder.addStatement("$L.accept((NamedListChange)$L)", paramName, fieldName);
+                        }
+                    }
+
+                    nodeChangeMtdBuilder.addStatement("return this");
+
+                    nodeClsBuilder.addMethod(nodeChangeMtdBuilder.build());
+                }
+            }
+
+            {
+                String initMtdName = "init" + capitalize(fieldName);
+
+                {
+                    MethodSpec.Builder initMtdBuilder = MethodSpec.methodBuilder(initMtdName)
+                        .addModifiers(PUBLIC, ABSTRACT)
+                        .returns(initClsName);
+
+                    if (valAnnotation != null)
+                        initMtdBuilder.addParameter(changeFieldType, fieldName);
+                    else
+                        initMtdBuilder.addParameter(ParameterizedTypeName.get(consumerClsName, initFieldType), fieldName);
+
+                    initClsBuilder.addMethod(initMtdBuilder.build());
+                }
+
+                {
+                    MethodSpec.Builder nodeInitMtdBuilder = MethodSpec.methodBuilder(initMtdName)
+                        .addAnnotation(Override.class)
+                        .addModifiers(PUBLIC)
+                        .returns(initClsName);
+
+                    if (valAnnotation != null) {
+                        nodeInitMtdBuilder
+                            .addParameter(initFieldType, fieldName)
+                            .addStatement("this.$L = $L", fieldName, fieldName);
+                    }
+                    else {
+                        String paramName = fieldName + "Consumer";
+                        nodeInitMtdBuilder.addParameter(ParameterizedTypeName.get(consumerClsName, initFieldType), paramName);
+
+                        if (!namedListField) {
+                            nodeInitMtdBuilder.addStatement(
+                                "$L = $L == null ? new $T() : $L",
+                                fieldName,
+                                fieldName,
+                                nodeFieldType,
+                                fieldName
+                            );
+
+                            nodeInitMtdBuilder.addStatement("$L.accept($L)", paramName, fieldName);
+                        }
+                        else {
+                            nodeInitMtdBuilder.addAnnotation(
+                                AnnotationSpec.builder(SuppressWarnings.class)
+                                    .addMember("value", "$S", "unchecked")
+                                    .build()
+                            );
+
+                            nodeInitMtdBuilder.addStatement("$L.accept((NamedListChange)$L)", paramName, fieldName);
+                        }
+                    }
+
+                    nodeInitMtdBuilder.addStatement("return this");
+
+                    nodeClsBuilder.addMethod(nodeInitMtdBuilder.build());
+                }
+            }
+
+            {
+                if (leafField) {
+                    traverseChildrenBuilder.addStatement("visitor.visitLeafNode($S, $L)", fieldName, fieldName);
+
+                    traverseChildBuilder
+                        .addStatement("case $S: visitor.visitLeafNode($S, $L)", fieldName, fieldName, fieldName)
+                        .addStatement(INDENT + "break");
+                }
+                else if (namedListField) {
+                    traverseChildrenBuilder.addStatement("visitor.visitNamedListNode($S, $L)", fieldName, fieldName);
+
+                    traverseChildBuilder
+                        .addStatement("case $S: visitor.visitNamedListNode($S, $L)", fieldName, fieldName, fieldName)
+                        .addStatement(INDENT + "break");
+                }
+                else {
+                    traverseChildrenBuilder.addStatement("visitor.visitInnerNode($S, $L)", fieldName, fieldName);
+
+                    traverseChildBuilder
+                        .addStatement("case $S: visitor.visitInnerNode($S, $L)", fieldName, fieldName, fieldName)
+                        .addStatement(INDENT + "break");
+                }
+            }
+        }
+
+        traverseChildBuilder
+            .addStatement("default: throw new $T(key)", NoSuchElementException.class)
+            .endControlFlow();
+
+        nodeClsBuilder
+            .addMethod(traverseChildrenBuilder.build())
+            .addMethod(traverseChildBuilder.build());
+
+        TypeSpec viewCls = viewClsBuilder.build();
+        TypeSpec changeCls = changeClsBuilder.build();
+        TypeSpec initCls = initClsBuilder.build();
+        TypeSpec nodeCls = nodeClsBuilder.build();
+
+        try {
+            buildClass(viewClsName, viewCls);
+            buildClass(changeClsName, changeCls);
+            buildClass(initClsName, initCls);
+            buildClass(nodeClsName, nodeCls);
+        }
+        catch (IOException e) {
+            throw new ProcessorException("Failed to generate classes", e);
+        }
+    }
+
+    /** */
+    private void buildClass(ClassName viewClsName, TypeSpec viewCls) throws IOException {
+        JavaFile.builder(viewClsName.packageName(), viewCls)
+            .indent(INDENT)
+            .build()
+            .writeTo(filer);
+    }
+
+    /** */
+    private static String capitalize(String name) {
+        return name.substring(0, 1).toUpperCase() + name.substring(1);
     }
 
     /**
@@ -933,6 +1280,6 @@ public class Processor extends AbstractProcessor {
 
     /** {@inheritDoc} */
     @Override public SourceVersion getSupportedSourceVersion() {
-        return SourceVersion.RELEASE_8;
+        return SourceVersion.RELEASE_11;
     }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/processor/internal/ProcessorTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/processor/internal/ProcessorTest.java
index 68b19c6..f227e7a 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/processor/internal/ProcessorTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/processor/internal/ProcessorTest.java
@@ -44,9 +44,9 @@ public class ProcessorTest extends AbstractProcessorTest {
 
         final Compilation status = batch.getCompilationStatus();
 
-        assertNotEquals(Compilation.Status.FAILURE, status);
+        assertNotEquals(Compilation.Status.FAILURE, status.status());
 
-        assertEquals(7, batch.generated().size());
+        assertEquals(11, batch.generated().size());
 
         final ConfigSet classSet = batch.getBySchema(testConfigurationSchema);
 
@@ -57,9 +57,7 @@ public class ProcessorTest extends AbstractProcessorTest {
             hasFields(
                 "value1", Types.STRING,
                 "primitiveLong", Types.LONG,
-                "boxedLong", Types.LONG,
-                "primitiveInt", Types.INT,
-                "boxedInt", Types.INT
+                "primitiveInt", Types.INT
             )
         );
 
@@ -68,9 +66,7 @@ public class ProcessorTest extends AbstractProcessorTest {
             hasMethods(
                 "value1()", Types.STRING,
                 "primitiveLong()", Types.LONG,
-                "boxedLong()", Types.LONG,
-                "primitiveInt()", Types.INT,
-                "boxedInt()", Types.INT
+                "primitiveInt()", Types.INT
             )
         );
 
@@ -79,9 +75,7 @@ public class ProcessorTest extends AbstractProcessorTest {
             hasFields(
                 "value1", Types.STRING,
                 "primitiveLong", Types.LONG,
-                "boxedLong", Types.LONG,
-                "primitiveInt", Types.INT,
-                "boxedInt", Types.INT
+                "primitiveInt", Types.INT
             )
         );
 
@@ -92,14 +86,10 @@ public class ProcessorTest extends AbstractProcessorTest {
             hasMethods(
                 "value1()", Types.STRING,
                 "primitiveLong()", Types.LONG,
-                "boxedLong()", Types.LONG,
                 "primitiveInt()", Types.INT,
-                "boxedInt()", Types.INT,
                 "withValue1(java.lang.String)", initTypeName,
                 "withPrimitiveLong(java.lang.Long)", initTypeName,
-                "withBoxedLong(java.lang.Long)", initTypeName,
-                "withPrimitiveInt(java.lang.Integer)", initTypeName,
-                "withBoxedInt(java.lang.Integer)", initTypeName
+                "withPrimitiveInt(java.lang.Integer)", initTypeName
             )
         );
     }
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/TraversableNodesTest.java
new file mode 100644
index 0000000..2646711
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableNodesTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+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.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+import org.junit.jupiter.api.Test;
+
+import static java.util.Collections.emptySet;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** */
+public class TraversableNodesTest {
+    /** */
+    @Config
+    public static class ParentConfigurationSchema {
+        /** */
+        @ConfigValue
+        private ChildConfigurationSchema child;
+
+        /** */
+        @NamedConfigValue
+        private NamedElementConfigurationSchema elements;
+    }
+
+    /** */
+    @Config
+    public static class ChildConfigurationSchema {
+        /** */
+        @Value(immutable = true)
+        private int intCfg;
+
+        /** */
+        @Value
+        private String strCfg;
+    }
+
+    /** */
+    @Config
+    public static class NamedElementConfigurationSchema {
+        /** */
+        @Value
+        private String strCfg;
+    }
+
+    /** */
+    private static class VisitException extends RuntimeException {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+    }
+
+    /**
+     * Test that generated node classes implement generated VIEW, CHANGE and INIT interfaces.
+     */
+    @Test
+    public void nodeClassesImplementRequiredInterfaces() {
+        var parentNode = new ParentNode();
+
+        assertThat(parentNode, instanceOf(ParentView.class));
+        assertThat(parentNode, instanceOf(ParentChange.class));
+        assertThat(parentNode, instanceOf(ParentInit.class));
+
+        var namedElementNode = new NamedElementNode();
+
+        assertThat(namedElementNode, instanceOf(NamedElementView.class));
+        assertThat(namedElementNode, instanceOf(NamedElementChange.class));
+        assertThat(namedElementNode, instanceOf(NamedElementInit.class));
+
+        var childNode = new ChildNode();
+
+        assertThat(childNode, instanceOf(ChildView.class));
+        assertThat(childNode, instanceOf(ChildChange.class));
+        assertThat(childNode, instanceOf(ChildInit.class));
+    }
+
+    /**
+     * Test for signature and implementation of "change" method on leaves.
+     */
+    @Test
+    public void changeLeaf() {
+        var childNode = new ChildNode();
+
+        assertNull(childNode.strCfg());
+
+        childNode.changeStrCfg("value");
+
+        assertEquals("value", childNode.strCfg());
+    }
+
+    /**
+     * Test for signature and implementation of "change" method on inner nodes.
+     */
+    @Test
+    public void changeInnerChild() {
+        var parentNode = new ParentNode();
+
+        assertNull(parentNode.child());
+
+        parentNode.changeChild(child -> {});
+
+        ChildNode childNode = parentNode.child();
+
+        assertNotNull(childNode);
+
+        parentNode.changeChild(child -> child.changeStrCfg("value"));
+
+        // Assert that change method applied its closure to the same object instead of creating a new one.
+        assertSame(childNode, parentNode.child());
+    }
+
+    /**
+     * Test for signature and implementation of "change" method on named list nodes.
+     */
+    @Test
+    public void changeNamedChild() {
+        var parentNode = new ParentNode();
+
+        NamedListNode<NamedElementNode> elementsNode = parentNode.elements();
+
+        // Named list node must always be instantiated.
+        assertNotNull(elementsNode);
+
+        parentNode.changeElements(elements -> elements.put("key", element -> {}));
+
+        // Assert that change method applied its closure to the same object instead of creating a new one.
+        assertSame(elementsNode, parentNode.elements());
+    }
+
+    /**
+     * Test for signature and implementation of "init" method on leaves.
+     */
+    @Test
+    public void initLeaf() {
+        var childNode = new ChildNode();
+
+        childNode.initStrCfg("value");
+
+        assertEquals("value", childNode.strCfg());
+    }
+
+    /**
+     * Test for signature and implementation of "init" method on inner nodes.
+     */
+    @Test
+    public void initInnerChild() {
+        var parentNode = new ParentNode();
+
+        parentNode.initChild(child -> {});
+
+        ChildNode childNode = parentNode.child();
+
+        parentNode.initChild(child -> child.initStrCfg("value"));
+
+        // Assert that init method applied its closure to the same object instead of creating a new one.
+        assertSame(childNode, parentNode.child());
+    }
+
+    /**
+     * Test for signature and implementation of "init" method on named list nodes.
+     */
+    @Test
+    public void initNamedChild() {
+        var parentNode = new ParentNode();
+
+        NamedListNode<NamedElementNode> elementsNode = parentNode.elements();
+
+        parentNode.initElements(elements -> elements.put("key", element -> {}));
+
+        // Assert that change method applied its closure to the same object instead of creating a new one.
+        assertSame(elementsNode, parentNode.elements());
+    }
+
+    /**
+     * Test for signature and implementation of "put" and "remove" methods on elements of named list nodes.
+     */
+    @Test
+    public void putRemoveNamedConfiguration() {
+        var elementsNode = new NamedListNode<>(NamedElementNode::new);
+
+        assertEquals(emptySet(), elementsNode.namedListKeys());
+
+        elementsNode.put("keyPut", element -> {});
+
+        assertThat(elementsNode.namedListKeys(), hasItem("keyPut"));
+
+        NamedElementNode elementNode = elementsNode.get("keyPut");
+
+        assertNotNull(elementNode);
+
+        assertNull(elementNode.strCfg());
+
+        elementsNode.put("keyPut", element -> element.changeStrCfg("val"));
+
+        // Assert that consecutive put methods don't create new object every time.
+        assertSame(elementNode, elementsNode.get("keyPut"));
+
+        assertEquals("val", elementNode.strCfg());
+
+        // Assert that once you put something into list, removing it makes no sense and hence prohibited.
+        assertThrows(IllegalStateException.class, () -> elementsNode.remove("keyPut"));
+
+        elementsNode.remove("keyRemove");
+
+        // Assert that "remove" method creates null element inside of the node.
+        assertThat(elementsNode.namedListKeys(), hasItem("keyRemove"));
+
+        assertNull(elementsNode.get("keyRemove"));
+
+        // Assert that once you remove something from list, you can't put it back again with different set of fields.
+        assertThrows(IllegalStateException.class, () -> elementsNode.put("keyRemove", element -> {}));
+    }
+
+    /**
+     * Test that inner nodes properly implement visitor interface.
+     */
+    @Test
+    public void innerNodeAcceptVisitor() {
+        var parentNode = new ParentNode();
+
+        assertThrows(VisitException.class, () ->
+            parentNode.accept("root", new ConfigurationVisitor() {
+                @Override public void visitInnerNode(String key, InnerNode node) {
+                    throw new VisitException();
+                }
+            })
+        );
+    }
+
+    /**
+     * Test that named list nodes properly implement visitor interface.
+     */
+    @Test
+    public void namedListNodeAcceptVisitor() {
+        var elementsNode = new NamedListNode<>(NamedElementNode::new);
+
+        assertThrows(VisitException.class, () ->
+            elementsNode.accept("root", new ConfigurationVisitor() {
+                @Override public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                    throw new VisitException();
+                }
+            })
+        );
+    }
+
+    /**
+     * Test for "traverseChildren" method implementation on generated inner nodes classes.
+     */
+    @Test
+    public void traverseChildren() {
+        var parentNode = new ParentNode();
+
+        List<String> keys = new ArrayList<>(2);
+
+        parentNode.traverseChildren(new ConfigurationVisitor() {
+            @Override public void visitInnerNode(String key, InnerNode node) {
+                assertNull(node);
+
+                assertEquals("child", key);
+
+                keys.add(key);
+            }
+
+            @Override public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                assertEquals("elements", key);
+
+                keys.add(key);
+            }
+        });
+
+        // Assert that updates happened in the same order as fields declaration in schema.
+        assertEquals(List.of("child", "elements"), keys);
+
+        keys.clear();
+
+        ChildNode childNode = new ChildNode();
+
+        childNode.traverseChildren(new ConfigurationVisitor() {
+            @Override public void visitLeafNode(String key, Serializable val) {
+                keys.add(key);
+            }
+        });
+
+        // Assert that updates happened in the same order as fields declaration in schema.
+        assertEquals(List.of("intCfg", "strCfg"), keys);
+    }
+
+    /**
+     * Test for "traverseChild" method implementation on generated inner nodes classes.
+     */
+    @Test
+    public void traverseSingleChild() {
+        var parentNode = new ParentNode();
+
+        // Assert that proper method has been invoked.
+        assertThrows(VisitException.class, () ->
+            parentNode.traverseChild("child", new ConfigurationVisitor() {
+                @Override public void visitInnerNode(String key, InnerNode node) {
+                    assertEquals("child", key);
+
+                    throw new VisitException();
+                }
+            })
+        );
+
+        // Assert that proper method has been invoked.
+        assertThrows(VisitException.class, () ->
+            parentNode.traverseChild("elements", new ConfigurationVisitor() {
+                @Override
+                public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                    assertEquals("elements", key);
+
+                    throw new VisitException();
+                }
+            })
+        );
+
+        var childNode = new ChildNode();
+
+        // Assert that proper method has been invoked.
+        assertThrows(VisitException.class, () ->
+            childNode.traverseChild("intCfg", new ConfigurationVisitor() {
+                @Override public void visitLeafNode(String key, Serializable val) {
+                    assertEquals("intCfg", key);
+
+                    throw new VisitException();
+                }
+            })
+        );
+
+        // Assert that traversing inexistent field leads to exception.
+        assertThrows(NoSuchElementException.class, () ->
+            childNode.traverseChild("foo", new ConfigurationVisitor() {})
+        );
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/resources/org/apache/ignite/configuration/processor/internal/TestConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/resources/org/apache/ignite/configuration/processor/internal/TestConfigurationSchema.java
index 7b59384..682f952 100644
--- a/modules/configuration-annotation-processor/src/test/resources/org/apache/ignite/configuration/processor/internal/TestConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/resources/org/apache/ignite/configuration/processor/internal/TestConfigurationSchema.java
@@ -29,11 +29,5 @@ public class TestConfigurationSchema {
     private long primitiveLong;
 
     @Value
-    private Long boxedLong;
-
-    @Value
     private int primitiveInt;
-
-    @Value
-    private Integer boxedInt;
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
index eeda647..4459890 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
@@ -28,8 +28,16 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
 /**
  * This annotation marks configuration schema field as a configuration tree leaf.
  * Every field annotated with this annotation will produce a {@link DynamicProperty} field in generated configuration class.
+ * <br/> Type must be one of the following:
+ * <ul>
+ *     <li>boolean</li>
+ *     <li>int</li>
+ *     <li>long</li>
+ *     <li>double</li>
+ *     <li>String</li>
+ * </ul>
  */
-@Target({ FIELD })
+@Target(FIELD)
 @Retention(SOURCE)
 @Documented
 public @interface Value {
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationVisitor.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationVisitor.java
index 98db039..b9c70c8 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationVisitor.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConfigurationVisitor.java
@@ -29,7 +29,8 @@ public interface ConfigurationVisitor {
      * @param key Name of the serializable value retrieved from its holder object.
      * @param val Configuration value.
      */
-    void visitLeafNode(String key, Serializable val);
+    default void visitLeafNode(String key, Serializable val) {
+    }
 
     /**
      * Invoked on visiting regular inner node.
@@ -37,7 +38,8 @@ public interface ConfigurationVisitor {
      * @param key Name of the node retrieved from its holder object.
      * @param node Inner configuration node.
      */
-    void visitInnerNode(String key, InnerNode node);
+    default void visitInnerNode(String key, InnerNode node) {
+    }
 
     /**
      * Invoked on visiting named list nodes.
@@ -45,5 +47,6 @@ public interface ConfigurationVisitor {
      * @param key Name of the node retrieved from its holder object.
      * @param node Named list inner configuration node.
      */
-    <N extends TraversableTreeNode> void visitNamedListNode(String key, NamedListNode<N> node);
+    default <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+    }
 }
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 1da138b..8a14b1a 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
@@ -30,9 +30,9 @@ public abstract class InnerNode implements TraversableTreeNode, Cloneable {
      * Method with auto-generated implementation. Must look like this:
      * <pre>{@code
      * @Override public void traverseChildren(ConfigurationVisitor visitor) {
-     *     this.pojoField1.accept("pojoField1", visitor);
+     *     visitor.visitInnerNode("pojoField1", this.pojoField1);
      *
-     *     this.pojoField2.accept("pojoField2", visitor);
+     *     visitor.visitNamedListNode("pojoField2", this.pojoField2);
      *
      *     visitor.visitLeafNode("primitiveField1", this.primitiveField1);
      *
@@ -52,11 +52,11 @@ public abstract class InnerNode implements TraversableTreeNode, Cloneable {
      * @Override public void traverseChild(String key, ConfigurationVisitor visitor) throws NoSuchElementException {
      *     switch (key) {
      *         case "pojoField1":
-     *             this.pojoField1.accept("pojoField1", visitor);
+     *             visitor.visitInnerNode("pojoField1", this.pojoField1);
      *             break;
      *
      *         case "pojoField2":
-     *             this.pojoField2.accept("pojoField2", visitor);
+     *             visitor.visitNamedListNode("pojoField2", this.pojoField2);
      *             break;
      *
      *         case "primitiveField1":
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 c4d6ab4..38ce004 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 TraversableTreeNode> implements NamedListView<N>, NamedListChange<N>, TraversableTreeNode, Cloneable {
+public final class NamedListNode<N extends InnerNode> implements NamedListView<N>, NamedListChange<N>, TraversableTreeNode, Cloneable {
     /** */
     private final Supplier<N> valSupplier;