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 <[email protected]>
AuthorDate: Mon Feb 1 15:26:25 2021 +0300
IGNITE-14087 Code generation for traversable configuration tree structure.
(#38)
Signed-off-by: ibessonov <[email protected]>
---
.../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;