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;