From 7eb75ec7088ac3bcdb9e9cc8e8b1aea5b632612d Mon Sep 17 00:00:00 2001 From: Alexander Polovtcev Date: Fri, 10 Jan 2025 17:24:32 +0200 Subject: [PATCH] IGNITE-24014 Introduce a way to have key-value entries in configuration (#4997) --- examples/config/ignite-config.conf | 4 +- .../topology/ItLogicalTopologyTest.java | 2 +- .../NodeAttributeConfigurationSchema.java | 4 +- .../ItConfigurationProcessorTest.java | 60 ++++++ ...ipleInjectedValuesConfigurationSchema.java | 30 +++ ...supportedFieldTypeConfigurationSchema.java | 27 +++ .../ValidConfigurationSchema.java | 31 +++ ...ueAndInjectedValueConfigurationSchema.java | 34 +++ .../processor/ConfigurationProcessor.java | 71 ++++--- .../ConfigurationProcessorUtils.java | 22 +- .../validators/InjectedValueValidator.java | 101 +++++++++ .../annotation/InjectedValue.java | 79 +++++++ .../SystemPropertyConfigurationSchema.java | 4 +- ...ibutedConfigurationPropertyHolderTest.java | 3 +- .../asm/ConfigurationImplAsmGenerator.java | 3 +- .../asm/InnerNodeAsmGenerator.java | 26 ++- .../hocon/HoconListConfigurationSource.java | 44 ++-- .../hocon/HoconObjectConfigurationSource.java | 92 +++++---- .../HoconPrimitiveConfigurationSource.java | 14 +- .../tree/ConstructableTreeNode.java | 9 + .../tree/ConverterToMapVisitor.java | 71 +++++-- .../configuration/util/ConfigurationUtil.java | 23 ++- .../InjectedValueConfigurationTest.java | 194 ++++++++++++++++++ .../hocon/HoconConverterTest.java | 2 +- .../ItDistributionZonesFiltersTest.java | 20 +- .../ItRebalanceTriggersRecoveryTest.java | 4 +- .../BaseDistributionZoneManagerTest.java | 4 +- .../distribution-zones/tech-notes/filters.md | 15 +- .../metastorage/TestMetasStorageUtils.java | 4 +- ...ageCompactionTriggerConfigurationTest.java | 4 +- .../replicator/ItReplicaLifecycleTest.java | 6 +- .../runner/app/ItIgniteNodeRestartTest.java | 4 +- .../runner/app/ItReplicaStateManagerTest.java | 6 +- .../sql/engine/ItUnstableTopologyTest.java | 2 +- ...ePartitionsRecoveryByFilterUpdateTest.java | 6 +- 35 files changed, 843 insertions(+), 182 deletions(-) create mode 100644 modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/MultipleInjectedValuesConfigurationSchema.java create mode 100644 modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/UnsupportedFieldTypeConfigurationSchema.java create mode 100644 modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValidConfigurationSchema.java create mode 100644 modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValueAndInjectedValueConfigurationSchema.java create mode 100644 modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/validators/InjectedValueValidator.java create mode 100644 modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/InjectedValue.java create mode 100644 modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java diff --git a/examples/config/ignite-config.conf b/examples/config/ignite-config.conf index adc047156ab..6f95edcdcb6 100644 --- a/examples/config/ignite-config.conf +++ b/examples/config/ignite-config.conf @@ -24,7 +24,7 @@ ignite { ] } nodeAttributes.nodeAttributes { - region.attribute = US - storage.attribute = SSD + region = US + storage = SSD } } diff --git a/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/topology/ItLogicalTopologyTest.java b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/topology/ItLogicalTopologyTest.java index 3a7593d302d..97e3816272a 100644 --- a/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/topology/ItLogicalTopologyTest.java +++ b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/topology/ItLogicalTopologyTest.java @@ -67,7 +67,7 @@ class ItLogicalTopologyTest extends ClusterPerTestIntegrationTest { + " port: {},\n" + " nodeFinder.netClusterNodes: [ {} ]\n" + " },\n" - + " nodeAttributes.nodeAttributes: {region.attribute = US, storage.attribute = SSD},\n" + + " nodeAttributes.nodeAttributes: {region = US, storage = SSD},\n" + " storage.profiles: {lru_rocks.engine = rocksdb, segmented_aipersist.engine = aipersist},\n" + " clientConnector.port: {},\n" + " rest.port: {},\n" diff --git a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/configuration/NodeAttributeConfigurationSchema.java b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/configuration/NodeAttributeConfigurationSchema.java index cb52ebf3a2e..3dc822dc4a4 100644 --- a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/configuration/NodeAttributeConfigurationSchema.java +++ b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/configuration/NodeAttributeConfigurationSchema.java @@ -19,7 +19,7 @@ import org.apache.ignite.configuration.annotation.Config; import org.apache.ignite.configuration.annotation.InjectedName; -import org.apache.ignite.configuration.annotation.Value; +import org.apache.ignite.configuration.annotation.InjectedValue; /** * Node's attribute configuration schema. User can specify any number of pairs (key, attribute) for a node through the local configuration @@ -35,6 +35,6 @@ public class NodeAttributeConfigurationSchema { public String name; /** Node attribute field. */ - @Value(hasDefault = true) + @InjectedValue(hasDefault = true) public String attribute = ""; } diff --git a/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java b/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java index b47f017e453..ce2f61768cc 100644 --- a/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java +++ b/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java @@ -615,6 +615,66 @@ void testSuccessfulCodeGenerationAbstractConfigurationAndItsDescendants() { configRootConfigurationInterfaceContent.contains("extends " + getConfigurationInterfaceName(abstractRootConfigSchema).simpleName()); } + @Test + void testSuccessInjectedValueFieldCodeGeneration() { + String packageName = "org.apache.ignite.internal.configuration.processor.injectedvalue"; + + ClassName cls0 = ClassName.get(packageName, "ValidConfigurationSchema"); + + BatchCompilation batchCompile = batchCompile(cls0); + + assertThat(batchCompile.getCompilationStatus()).succeededWithoutWarnings(); + + assertEquals(3, batchCompile.generated().size()); + + assertTrue(batchCompile.getBySchema(cls0).allGenerated()); + } + + @Test + void testMultipleInjectedValuesUnsuccessfulGeneration() { + String packageName = "org.apache.ignite.internal.configuration.processor.injectedvalue"; + + ClassName cls0 = ClassName.get(packageName, "MultipleInjectedValuesConfigurationSchema"); + + assertThrowsEx( + IllegalStateException.class, + () -> batchCompile(cls0), + "Field marked as @InjectedValue must be the only \"value\" field in the schema " + + "org.apache.ignite.internal.configuration.processor.injectedvalue.MultipleInjectedValuesConfigurationSchema, " + + "found: [firstValue, secondValue]" + ); + } + + @Test + void testValuesAndInjectedValueUnsuccessfulGeneration() { + String packageName = "org.apache.ignite.internal.configuration.processor.injectedvalue"; + + ClassName cls0 = ClassName.get(packageName, "ValueAndInjectedValueConfigurationSchema"); + + assertThrowsEx( + IllegalStateException.class, + () -> batchCompile(cls0), + "Field marked as @InjectedValue must be the only \"value\" field in the schema " + + "org.apache.ignite.internal.configuration.processor.injectedvalue.ValueAndInjectedValueConfigurationSchema, " + + "found: [secondValue, firstValue, thirdValue]" + ); + } + + @Test + void testUnsupportedFieldTypeUnsuccessfulGeneration() { + String packageName = "org.apache.ignite.internal.configuration.processor.injectedvalue"; + + ClassName cls0 = ClassName.get(packageName, "UnsupportedFieldTypeConfigurationSchema"); + + assertThrowsEx( + IllegalStateException.class, + () -> batchCompile(cls0), + "org.apache.ignite.internal.configuration.processor.injectedvalue.UnsupportedFieldTypeConfigurationSchema.firstValue " + + "field must have one of the following types: boolean, int, long, double, String, UUID " + + "or an array of aforementioned type." + ); + } + /** * Compile set of classes. * diff --git a/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/MultipleInjectedValuesConfigurationSchema.java b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/MultipleInjectedValuesConfigurationSchema.java new file mode 100644 index 00000000000..02d7994e83a --- /dev/null +++ b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/MultipleInjectedValuesConfigurationSchema.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration.processor.injectedvalue; + +import org.apache.ignite.configuration.annotation.Config; +import org.apache.ignite.configuration.annotation.InjectedValue; + +@Config +public class MultipleInjectedValuesConfigurationSchema { + @InjectedValue + public String firstValue; + + @InjectedValue + public String secondValue; +} diff --git a/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/UnsupportedFieldTypeConfigurationSchema.java b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/UnsupportedFieldTypeConfigurationSchema.java new file mode 100644 index 00000000000..e869d08f031 --- /dev/null +++ b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/UnsupportedFieldTypeConfigurationSchema.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration.processor.injectedvalue; + +import org.apache.ignite.configuration.annotation.Config; +import org.apache.ignite.configuration.annotation.InjectedValue; + +@Config +public class UnsupportedFieldTypeConfigurationSchema { + @InjectedValue + public Object firstValue; +} diff --git a/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValidConfigurationSchema.java b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValidConfigurationSchema.java new file mode 100644 index 00000000000..0698a0768a9 --- /dev/null +++ b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValidConfigurationSchema.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration.processor.injectedvalue; + +import org.apache.ignite.configuration.annotation.Config; +import org.apache.ignite.configuration.annotation.InjectedName; +import org.apache.ignite.configuration.annotation.InjectedValue; + +@Config +public class ValidConfigurationSchema { + @InjectedName + public String name; + + @InjectedValue + public String someValue; +} diff --git a/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValueAndInjectedValueConfigurationSchema.java b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValueAndInjectedValueConfigurationSchema.java new file mode 100644 index 00000000000..f1e42d03563 --- /dev/null +++ b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/injectedvalue/ValueAndInjectedValueConfigurationSchema.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration.processor.injectedvalue; + +import org.apache.ignite.configuration.annotation.Config; +import org.apache.ignite.configuration.annotation.InjectedValue; +import org.apache.ignite.configuration.annotation.Value; + +@Config +public class ValueAndInjectedValueConfigurationSchema { + @Value + public String firstValue; + + @InjectedValue + public String secondValue; + + @Value + public String thirdValue; +} diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java index 76e630a6b75..3dc7f7007a2 100644 --- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java +++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java @@ -24,6 +24,7 @@ import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.collectFieldsWithAnnotation; +import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.containsAnyAnnotation; import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.findFirstPresentAnnotation; import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.getChangeName; import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.getConfigurationInterfaceName; @@ -80,6 +81,7 @@ import org.apache.ignite.configuration.annotation.ConfigurationExtension; import org.apache.ignite.configuration.annotation.ConfigurationRoot; import org.apache.ignite.configuration.annotation.InjectedName; +import org.apache.ignite.configuration.annotation.InjectedValue; import org.apache.ignite.configuration.annotation.InternalId; import org.apache.ignite.configuration.annotation.NamedConfigValue; import org.apache.ignite.configuration.annotation.PolymorphicConfig; @@ -87,6 +89,7 @@ import org.apache.ignite.configuration.annotation.PolymorphicId; import org.apache.ignite.configuration.annotation.Secret; import org.apache.ignite.configuration.annotation.Value; +import org.apache.ignite.internal.configuration.processor.validators.InjectedValueValidator; import org.jetbrains.annotations.Nullable; /** @@ -153,12 +156,16 @@ private boolean process0(RoundEnvironment roundEnvironment) { return false; } + var injectedValueValidator = new InjectedValueValidator(processingEnv); + for (TypeElement clazz : annotatedConfigs) { // Find all the fields of the schema. List fields = fields(clazz); validateConfigurationSchemaClass(clazz, fields); + injectedValueValidator.validate(clazz, fields); + // Get package name of the schema class String packageName = elementUtils.getPackageOf(clazz).getQualifiedName().toString(); @@ -175,10 +182,7 @@ private boolean process0(RoundEnvironment roundEnvironment) { throw new ConfigurationProcessorException("Field " + clazz.getQualifiedName() + "." + field + " must be public"); } - final String fieldName = field.getSimpleName().toString(); - - // Get configuration types (VIEW, CHANGE and so on) - final TypeName interfaceGetMethodType = getInterfaceGetMethodType(field); + String fieldName = field.getSimpleName().toString(); if (field.getAnnotation(ConfigValue.class) != null) { checkConfigField(field, ConfigValue.class); @@ -190,8 +194,7 @@ private boolean process0(RoundEnvironment roundEnvironment) { checkConfigField(field, NamedConfigValue.class); } - Value valueAnnotation = field.getAnnotation(Value.class); - if (valueAnnotation != null) { + if (field.getAnnotation(Value.class) != null) { // Must be a primitive or an array of the primitives (including java.lang.String, java.util.UUID). if (!isValidValueAnnotationFieldType(field.asType())) { throw new ConfigurationProcessorException(String.format( @@ -204,8 +207,7 @@ private boolean process0(RoundEnvironment roundEnvironment) { } } - PolymorphicId polymorphicId = field.getAnnotation(PolymorphicId.class); - if (polymorphicId != null) { + if (field.getAnnotation(PolymorphicId.class) != null) { if (!isClass(field.asType(), String.class)) { throw new ConfigurationProcessorException(String.format( FIELD_MUST_BE_SPECIFIC_CLASS_ERROR_FORMAT, @@ -237,6 +239,9 @@ private boolean process0(RoundEnvironment roundEnvironment) { } } + // Get configuration types (VIEW, CHANGE and so on) + TypeName interfaceGetMethodType = getInterfaceGetMethodType(field); + createGetters(configurationInterfaceBuilder, fieldName, interfaceGetMethodType); } @@ -355,13 +360,11 @@ private static void createGetters( * @return Bundle with all types for configuration */ private static TypeName getInterfaceGetMethodType(VariableElement field) { - TypeName interfaceGetMethodType = null; - TypeName baseType = TypeName.get(field.asType()); ConfigValue confAnnotation = field.getAnnotation(ConfigValue.class); if (confAnnotation != null) { - interfaceGetMethodType = getConfigurationInterfaceName((ClassName) baseType); + return getConfigurationInterfaceName((ClassName) baseType); } NamedConfigValue namedConfigAnnotation = field.getAnnotation(NamedConfigValue.class); @@ -371,7 +374,7 @@ private static TypeName getInterfaceGetMethodType(VariableElement field) { TypeName viewClassType = getViewName((ClassName) baseType); TypeName changeClassType = getChangeName((ClassName) baseType); - interfaceGetMethodType = ParameterizedTypeName.get( + return ParameterizedTypeName.get( ClassName.get(NamedConfigurationTree.class), interfaceGetType, viewClassType, @@ -379,13 +382,16 @@ private static TypeName getInterfaceGetMethodType(VariableElement field) { ); } - Value valueAnnotation = field.getAnnotation(Value.class); - PolymorphicId polymorphicIdAnnotation = field.getAnnotation(PolymorphicId.class); - InjectedName injectedNameAnnotation = field.getAnnotation(InjectedName.class); - InternalId internalIdAnnotation = field.getAnnotation(InternalId.class); + boolean containsAnnotation = containsAnyAnnotation( + field, + Value.class, + PolymorphicId.class, + InjectedName.class, + InternalId.class, + InjectedValue.class + ); - if (valueAnnotation != null || polymorphicIdAnnotation != null || injectedNameAnnotation != null - || internalIdAnnotation != null) { + if (containsAnnotation) { // It is necessary to use class names without loading classes so that we won't // accidentally get NoClassDefFoundError ClassName confValueClass = ClassName.get("org.apache.ignite.configuration", "ConfigurationValue"); @@ -396,10 +402,10 @@ private static TypeName getInterfaceGetMethodType(VariableElement field) { genericType = genericType.box(); } - interfaceGetMethodType = ParameterizedTypeName.get(confValueClass, genericType); + return ParameterizedTypeName.get(confValueClass, genericType); } - return interfaceGetMethodType; + throw new IllegalArgumentException(String.format("Field \"%s\" does not contain any supported annotations", field)); } /** @@ -503,8 +509,6 @@ private void createPojoBindings( ClassName consumerClsName = ClassName.get(Consumer.class); for (VariableElement field : fields) { - Value valAnnotation = field.getAnnotation(Value.class); - String fieldName = field.getSimpleName().toString(); TypeMirror schemaFieldType = field.asType(); TypeName schemaFieldTypeName = TypeName.get(schemaFieldType); @@ -512,15 +516,13 @@ private void createPojoBindings( boolean leafField = isValidValueAnnotationFieldType(schemaFieldType) || !((ClassName) schemaFieldTypeName).simpleName().contains(CONFIGURATION_SCHEMA_POSTFIX); - boolean namedListField = field.getAnnotation(NamedConfigValue.class) != null; - TypeName viewFieldType = leafField ? schemaFieldTypeName : getViewName((ClassName) schemaFieldTypeName); TypeName changeFieldType = leafField ? schemaFieldTypeName : getChangeName((ClassName) schemaFieldTypeName); - if (namedListField) { + if (field.getAnnotation(NamedConfigValue.class) != null) { changeFieldType = ParameterizedTypeName.get( ClassName.get(NamedListChange.class), viewFieldType, @@ -540,8 +542,7 @@ private void createPojoBindings( viewClsBuilder.addMethod(getMtdBuilder.build()); // Read only. - if (field.getAnnotation(PolymorphicId.class) != null || field.getAnnotation(InjectedName.class) != null - || field.getAnnotation(InternalId.class) != null) { + if (containsAnyAnnotation(field, PolymorphicId.class, InjectedName.class, InternalId.class)) { continue; } @@ -551,7 +552,7 @@ private void createPojoBindings( .addModifiers(PUBLIC, ABSTRACT) .returns(changeClsName); - if (valAnnotation != null) { + if (containsAnyAnnotation(field, Value.class, InjectedValue.class)) { if (schemaFieldType.getKind() == TypeKind.ARRAY) { changeMtdBuilder.varargs(true); } @@ -559,18 +560,16 @@ private void createPojoBindings( changeMtdBuilder.addParameter(changeFieldType, fieldName); } else { changeMtdBuilder.addParameter(ParameterizedTypeName.get(consumerClsName, changeFieldType), fieldName); - } - changeClsBuilder.addMethod(changeMtdBuilder.build()); - - // Create "FooChange changeFoo()" method with no parameters, if it's a config value or named list value. - if (valAnnotation == null) { + // Create "FooChange changeFoo()" method with no parameters, if it's a config value or named list value. MethodSpec.Builder shortChangeMtdBuilder = MethodSpec.methodBuilder(changeMtdName) .addModifiers(PUBLIC, ABSTRACT) .returns(changeFieldType); changeClsBuilder.addMethod(shortChangeMtdBuilder.build()); } + + changeClsBuilder.addMethod(changeMtdBuilder.build()); } if (isPolymorphicConfig) { @@ -1112,8 +1111,8 @@ private void validateNameFields(TypeElement clazz, List fields) * Checks for missing {@link org.apache.ignite.configuration.annotation.Name} for nested schema with {@link InjectedName}. * * @param field Class field. - * @throws ConfigurationProcessorException If there is no {@link org.apache.ignite.configuration.annotation.Name} for the nested schema - * with {@link InjectedName}. + * @throws ConfigurationProcessorException If there is no {@link org.apache.ignite.configuration.annotation.Name} for the nested + * schema with {@link InjectedName}. */ private void checkMissingNameForInjectedName(VariableElement field) { TypeElement fieldType = (TypeElement) processingEnv.getTypeUtils().asElement(field.asType()); @@ -1123,7 +1122,7 @@ private void checkMissingNameForInjectedName(VariableElement field) { List fields; if (!isClass(superClassFieldType.asType(), Object.class) - && findFirstPresentAnnotation(superClassFieldType, AbstractConfiguration.class).isPresent()) { + && superClassFieldType.getAnnotation(AbstractConfiguration.class) != null) { fields = concat( collectFieldsWithAnnotation(fields(fieldType), InjectedName.class), collectFieldsWithAnnotation(fields(superClassFieldType), InjectedName.class) diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessorUtils.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessorUtils.java index 5b3a2e70974..5061557ed15 100644 --- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessorUtils.java +++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessorUtils.java @@ -27,13 +27,13 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; -import javax.lang.model.element.TypeElement; +import javax.lang.model.element.Element; import javax.lang.model.element.VariableElement; /** * Annotation processing utilities. */ -class ConfigurationProcessorUtils { +public class ConfigurationProcessorUtils { /** * Returns {@link ClassName} for configuration class public interface. * @@ -91,17 +91,25 @@ public static String joinSimpleName(String delimiter, Class findFirstPresentAnnotation( - TypeElement clazz, + Element element, Class... annotationClasses ) { - return Stream.of(annotationClasses).map(clazz::getAnnotation).filter(Objects::nonNull).findFirst(); + return Stream.of(annotationClasses).map(element::getAnnotation).filter(Objects::nonNull).findFirst(); + } + + /** + * Returns {@code true} if any of the given annotations are present on the given element. + */ + @SafeVarargs + public static boolean containsAnyAnnotation(Element element, Class... annotationClasses) { + return findFirstPresentAnnotation(element, annotationClasses).isPresent(); } /** diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/validators/InjectedValueValidator.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/validators/InjectedValueValidator.java new file mode 100644 index 00000000000..7963ff81242 --- /dev/null +++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/validators/InjectedValueValidator.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration.processor.validators; + +import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.collectFieldsWithAnnotation; +import static org.apache.ignite.internal.configuration.processor.ConfigurationProcessorUtils.simpleName; +import static org.apache.ignite.internal.util.CollectionUtils.concat; + +import java.util.List; +import java.util.UUID; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.apache.ignite.configuration.annotation.InjectedValue; +import org.apache.ignite.configuration.annotation.Value; +import org.apache.ignite.internal.configuration.processor.ConfigurationProcessorException; + +/** + * Validator class for the {@link InjectedValue} annotation. + */ +public class InjectedValueValidator { + private final ProcessingEnvironment processingEnv; + + public InjectedValueValidator(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + /** + * Validates invariants of the {@link InjectedValue} annotation. This includes: + * + *
    + *
  1. Type of InjectedValue field is either a primitive, or a String, or a UUID;
  2. + *
  3. There is only a single InjectedValue field in the schema (including {@link Value} fields).
  4. + *
+ */ + public void validate(TypeElement clazz, List fields) { + List injectedValueFields = collectFieldsWithAnnotation(fields, InjectedValue.class); + + if (injectedValueFields.isEmpty()) { + return; + } + + List valueFields = collectFieldsWithAnnotation(fields, Value.class); + + if (injectedValueFields.size() > 1 || !valueFields.isEmpty()) { + throw new ConfigurationProcessorException(String.format( + "Field marked as %s must be the only \"value\" field in the schema %s, found: %s", + simpleName(InjectedValue.class), + clazz.getQualifiedName(), + concat(injectedValueFields, valueFields) + )); + } + + VariableElement injectedValueField = injectedValueFields.get(0); + + // Must be a primitive or an array of the primitives (including java.lang.String, java.util.UUID). + if (!isValidValueAnnotationFieldType(injectedValueField.asType())) { + throw new ConfigurationProcessorException(String.format( + "%s.%s field must have one of the following types: " + + "boolean, int, long, double, String, UUID or an array of aforementioned type.", + clazz.getQualifiedName(), + injectedValueField.getSimpleName() + )); + } + } + + private boolean isValidValueAnnotationFieldType(TypeMirror type) { + if (type.getKind() == TypeKind.ARRAY) { + type = ((ArrayType) type).getComponentType(); + } + + return type.getKind().isPrimitive() || isClass(type, String.class) || isClass(type, UUID.class); + } + + private boolean isClass(TypeMirror type, Class clazz) { + TypeMirror classType = processingEnv + .getElementUtils() + .getTypeElement(clazz.getCanonicalName()) + .asType(); + + return classType.equals(type); + } +} diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/InjectedValue.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/InjectedValue.java new file mode 100644 index 00000000000..403f100a8e8 --- /dev/null +++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/InjectedValue.java @@ -0,0 +1,79 @@ +/* + * 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.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotations is intended to be placed on {@link NamedConfigValue} elements to emulate key-value pairs in configuration schemas. + * + *

Therefore, this annotation induces the following constraints: + * + *

    + *
  1. Must be placed on a field of a configuration schema that represents a Named List element;
  2. + *
  3. Must be the only field in the configuration schema, apart from the one marked with {@link InjectedName}.
  4. + *
+ * + *

In all other aspects it behaves exactly like the {@link Value} annotation. + * + *

For example, this annotation can be used to declare a configuration schema with arbitrary {@code String} properties: + * + *

{@code
+ *     @Config
+ *     class PropertyConfigurationSchema {
+ *         @NamedConfigValue
+ *         public PropertyEntryConfigurationSchema properties;
+ *     }
+ *
+ *     @Config
+ *     class PropertyEntryConfigurationSchema {
+ *         @InjectedName
+ *         public String propertyName;
+ *
+ *         @InjectedValue
+ *         public String propertyValue;
+ *     }
+ * }
+ * + *

This will allow to use the following HOCON to represent this configuration: + * + *

{@code
+ *     root.properties {
+ *         property1: "value1",
+ *         property2: "value2",
+ *         property3: "value3"
+ *     }
+ * }
+ */ +@Target(FIELD) +@Retention(RUNTIME) +@Documented +public @interface InjectedValue { + /** + * Indicates that the current configuration value has a default value. Value itself is derived from the instantiated object of a + * corresponding schema type. This means that the default is not necessarily a constant value. + * + * @return {@code hasDefault} flag value. + */ + boolean hasDefault() default false; +} diff --git a/modules/configuration-system/src/main/java/org/apache/ignite/internal/configuration/SystemPropertyConfigurationSchema.java b/modules/configuration-system/src/main/java/org/apache/ignite/internal/configuration/SystemPropertyConfigurationSchema.java index acbe11a4e2b..ad7a6c08c94 100644 --- a/modules/configuration-system/src/main/java/org/apache/ignite/internal/configuration/SystemPropertyConfigurationSchema.java +++ b/modules/configuration-system/src/main/java/org/apache/ignite/internal/configuration/SystemPropertyConfigurationSchema.java @@ -20,7 +20,7 @@ import org.apache.ignite.configuration.ConfigurationModule; import org.apache.ignite.configuration.annotation.Config; import org.apache.ignite.configuration.annotation.InjectedName; -import org.apache.ignite.configuration.annotation.Value; +import org.apache.ignite.configuration.annotation.InjectedValue; import org.apache.ignite.configuration.validation.CamelCaseKeys; import org.apache.ignite.internal.configuration.validation.LongNumberSystemPropertyValueValidator; @@ -41,6 +41,6 @@ public class SystemPropertyConfigurationSchema { @InjectedName public String name; - @Value + @InjectedValue public String propertyValue; } diff --git a/modules/configuration-system/src/test/java/org/apache/ignite/internal/configuration/utils/SystemDistributedConfigurationPropertyHolderTest.java b/modules/configuration-system/src/test/java/org/apache/ignite/internal/configuration/utils/SystemDistributedConfigurationPropertyHolderTest.java index b9a9b600a05..96d99299077 100644 --- a/modules/configuration-system/src/test/java/org/apache/ignite/internal/configuration/utils/SystemDistributedConfigurationPropertyHolderTest.java +++ b/modules/configuration-system/src/test/java/org/apache/ignite/internal/configuration/utils/SystemDistributedConfigurationPropertyHolderTest.java @@ -60,8 +60,7 @@ void testEmptySystemProperties(@InjectConfiguration SystemDistributedConfigurati @Test void testValidSystemPropertiesOnStart( - @InjectConfiguration("mock.properties = {" - + PROPERTY_NAME + ".propertyValue = \"newValue\"}") + @InjectConfiguration("mock.properties." + PROPERTY_NAME + " = newValue") SystemDistributedConfiguration systemConfig ) { var config = noopConfigHolder(systemConfig); diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationImplAsmGenerator.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationImplAsmGenerator.java index a4a2d2fab42..8c0d45f8ef8 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationImplAsmGenerator.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationImplAsmGenerator.java @@ -54,6 +54,7 @@ import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicConfig; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicConfigInstance; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicId; +import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isReadOnly; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isValue; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.polymorphicInstanceId; import static org.apache.ignite.internal.util.ArrayUtils.nullOrEmpty; @@ -385,7 +386,7 @@ private void addConfigurationImplConstructor( rootKeyVar, changerVar, listenOnlyVar, - constantBoolean(isPolymorphicId(schemaField) || isInjectedName(schemaField) || isInternalId(schemaField)) + constantBoolean(isReadOnly(schemaField)) ); } else { SchemaClassesInfo fieldInfo = cgen.schemaInfo(schemaField.getType()); diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/InnerNodeAsmGenerator.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/InnerNodeAsmGenerator.java index 506a34b602e..d1841df1a53 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/InnerNodeAsmGenerator.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/InnerNodeAsmGenerator.java @@ -59,10 +59,12 @@ import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.hasDefault; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isConfigValue; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isInjectedName; +import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isInjectedValue; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isNamedConfigValue; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicConfig; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicConfigInstance; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isPolymorphicId; +import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isReadOnly; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isValue; import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.polymorphicInstanceId; import static org.apache.ignite.internal.util.CollectionUtils.concat; @@ -177,6 +179,9 @@ class InnerNodeAsmGenerator extends AbstractAsmGenerator { /** {@link ConstructableTreeNode#construct(String, ConfigurationSource, boolean)} method name. */ private static final String CONSTRUCT_MTD_NAME = "construct"; + /** {@link ConstructableTreeNode#injectedValueFieldName}. */ + private static final String INJECTED_VALUE_FIELD_NAME_MTD_NAME = "injectedValueFieldName"; + /** Mapping for each configuration {@link Field} to a static constant with this {@link Field} as value. */ private final Map fieldToFieldDefinitionMap = new HashMap<>(); @@ -309,6 +314,9 @@ private ClassDefinition createNodeClass() { // Field with @InjectedName. FieldDefinition injectedNameFieldDef = null; + // Field with @InjectedValue. + Field injectedValueField = null; + for (Field schemaField : concat(schemaFields, publicExtensionFields, internalExtensionFields, polymorphicFields)) { FieldDefinition fieldDef = addInnerNodeField(schemaField); @@ -318,6 +326,8 @@ private ClassDefinition createNodeClass() { polymorphicTypeIdFieldDef = fieldDef; } else if (isInjectedName(schemaField)) { injectedNameFieldDef = fieldDef; + } else if (isInjectedValue(schemaField)) { + injectedValueField = schemaField; } } @@ -444,6 +454,10 @@ private ClassDefinition createNodeClass() { addInjectedNameFieldMethods(injectedNameFieldDef); } + if (injectedValueField != null) { + implementInjectedValueFieldNameMethod(injectedValueField); + } + if (polymorphicTypeIdFieldDef != null) { addIsPolymorphicMethod(); } @@ -1578,6 +1592,16 @@ private void addInjectedNameFieldMethods(FieldDefinition injectedNameFieldDef) { )).ret(); } + private void implementInjectedValueFieldNameMethod(Field injectedValueField) { + MethodDefinition method = innerNodeClassDef.declareMethod( + EnumSet.of(PUBLIC), + INJECTED_VALUE_FIELD_NAME_MTD_NAME, + type(String.class) + ); + + method.getBody().append(constantString(publicName(injectedValueField))).retObject(); + } + /** * Adds an override for the {@link InnerNode#isPolymorphic} method that returns {@code true}. */ @@ -1698,7 +1722,7 @@ private ClassDefinition createPolymorphicExtensionNodeClass( ); // Read only. - if (isPolymorphicId(schemaField) || isInjectedName(schemaField)) { + if (isReadOnly(schemaField)) { continue; } diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconListConfigurationSource.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconListConfigurationSource.java index bcae565b7ce..e1c1486e9f5 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconListConfigurationSource.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconListConfigurationSource.java @@ -105,35 +105,39 @@ public void descend(ConstructableTreeNode node) { String syntheticKeyName = ((NamedListNode) node).syntheticKeyName(); - int idx = 0; - for (Iterator iterator = hoconCfgList.iterator(); iterator.hasNext(); idx++) { - ConfigValue next = iterator.next(); + for (int idx = 0; idx < hoconCfgList.size(); idx++) { + ConfigValue next = hoconCfgList.get(idx); if (next.valueType() != ConfigValueType.OBJECT) { - throw new IllegalArgumentException( - format( - "'%s' is expected to be a composite configuration node, not a single value", - formatArrayPath(path, idx) - ) - ); + throw new IllegalArgumentException(format( + "'%s' is expected to be a composite configuration node, not a single value", + formatArrayPath(path, idx) + )); } ConfigObject hoconCfg = (ConfigObject) next; - ConfigValue keyValue = hoconCfg.get(syntheticKeyName); + String key; - if (keyValue == null || keyValue.valueType() != ConfigValueType.STRING) { - throw new IllegalArgumentException( - format( - "'%s' configuration value is mandatory and must be a String", - formatArrayPath(path, idx) + KEY_SEPARATOR + syntheticKeyName - ) - ); - } + List path; - String key = (String) keyValue.unwrapped(); + ConfigValue keyValue = hoconCfg.get(syntheticKeyName); - List path = appendKey(this.path, key); + if (keyValue != null && keyValue.valueType() == ConfigValueType.STRING) { + // If the synthetic key is present, check that it has the correct type and use it as the key. + key = (String) keyValue.unwrapped(); + path = appendKey(this.path, key); + } else if (keyValue == null && hoconCfg.size() == 1) { + // If the synthetic key is not present explicitly, we need to handle the case when a configuration uses InjectedValue. + // This means that this object must only have one key. + key = hoconCfg.entrySet().iterator().next().getKey(); + path = this.path; + } else { + throw new IllegalArgumentException(format( + "'%s' configuration value is mandatory and must be a String", + formatArrayPath(this.path, idx) + KEY_SEPARATOR + syntheticKeyName + )); + } node.construct(key, new HoconObjectConfigurationSource(syntheticKeyName, path, hoconCfg), false); } diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconObjectConfigurationSource.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconObjectConfigurationSource.java index b6e97ec7858..b6a39978609 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconObjectConfigurationSource.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconObjectConfigurationSource.java @@ -27,7 +27,6 @@ import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import org.apache.ignite.configuration.ConfigurationWrongPolymorphicTypeIdException; import org.apache.ignite.internal.configuration.tree.ConfigurationSource; @@ -67,76 +66,81 @@ class HoconObjectConfigurationSource implements ConfigurationSource { this.hoconCfgObject = hoconCfgObject; } - /** {@inheritDoc} */ @Override public T unwrap(Class clazz) { throw wrongTypeException(clazz, path, -1); } - /** {@inheritDoc} */ @Override public void descend(ConstructableTreeNode node) { - for (Map.Entry entry : hoconCfgObject.entrySet()) { - String key = entry.getKey(); + String injectedValueFieldName = node.injectedValueFieldName(); - if (key.equals(ignoredKey)) { - continue; - } + if (injectedValueFieldName == null) { + hoconCfgObject.forEach((key, value) -> parseConfigEntry(key, value, node)); + } else { + assert hoconCfgObject.size() == 1; // User-friendly check must have been performed outside this method. - ConfigValue hoconCfgValue = entry.getValue(); + ConfigValue value = hoconCfgObject.values().iterator().next(); - try { - switch (hoconCfgValue.valueType()) { - case NULL: - node.construct(key, null, false); + parseConfigEntry(injectedValueFieldName, value, node); + } + } - break; + private void parseConfigEntry(String key, ConfigValue hoconCfgValue, ConstructableTreeNode node) { + if (key.equals(ignoredKey)) { + return; + } - case OBJECT: { - List path = appendKey(this.path, key); + try { + switch (hoconCfgValue.valueType()) { + case NULL: + node.construct(key, null, false); - node.construct( - key, - new HoconObjectConfigurationSource(null, path, (ConfigObject) hoconCfgValue), - false - ); + break; - break; - } + case OBJECT: { + List path = appendKey(this.path, key); - case LIST: { - List path = appendKey(this.path, key); + node.construct( + key, + new HoconObjectConfigurationSource(null, path, (ConfigObject) hoconCfgValue), + false + ); - node.construct(key, new HoconListConfigurationSource(path, (ConfigList) hoconCfgValue), false); + break; + } - break; - } + case LIST: { + List path = appendKey(this.path, key); - default: { - List path = appendKey(this.path, key); + node.construct(key, new HoconListConfigurationSource(path, (ConfigList) hoconCfgValue), false); - node.construct(key, new HoconPrimitiveConfigurationSource(path, hoconCfgValue), false); - } + break; } - } catch (NoSuchElementException e) { - if (path.isEmpty()) { - throw new IllegalArgumentException( - format("'%s' configuration root doesn't exist", key), e - ); - } else { - throw new IllegalArgumentException( - format("'%s' configuration doesn't have the '%s' sub-configuration", join(path), key), e - ); + + default: { + List path = appendKey(this.path, key); + + node.construct(key, new HoconPrimitiveConfigurationSource(path, hoconCfgValue), false); } - } catch (ConfigurationWrongPolymorphicTypeIdException e) { + } + } catch (NoSuchElementException e) { + if (path.isEmpty()) { throw new IllegalArgumentException( - "Polymorphic configuration type is not correct: " + e.getMessage() + format("'%s' configuration root doesn't exist", key), e + ); + } else { + throw new IllegalArgumentException( + format("'%s' configuration doesn't have the '%s' sub-configuration", join(path), key), e ); } + } catch (ConfigurationWrongPolymorphicTypeIdException e) { + throw new IllegalArgumentException( + "Polymorphic configuration type is not correct: " + e.getMessage() + ); } } - /** {@inheritDoc} */ @Override public @Nullable String polymorphicTypeId(String fieldName) { ConfigValue typeId = hoconCfgObject.get(fieldName); diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconPrimitiveConfigurationSource.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconPrimitiveConfigurationSource.java index 830b2567e77..4c50b42b27b 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconPrimitiveConfigurationSource.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconPrimitiveConfigurationSource.java @@ -58,7 +58,6 @@ class HoconPrimitiveConfigurationSource implements ConfigurationSource { this.hoconCfgValue = hoconCfgValue; } - /** {@inheritDoc} */ @Override public T unwrap(Class clazz) { if (clazz.isArray()) { @@ -68,12 +67,17 @@ public T unwrap(Class clazz) { return unwrapPrimitive(hoconCfgValue, clazz, path, -1); } - /** {@inheritDoc} */ @Override public void descend(ConstructableTreeNode node) { - throw new IllegalArgumentException( - format("'%s' is expected to be a composite configuration node, not a single value", join(path)) - ); + String fieldName = node.injectedValueFieldName(); + + if (fieldName == null) { + throw new IllegalArgumentException( + format("'%s' is expected to be a composite configuration node, not a single value", join(path)) + ); + } + + node.construct(fieldName, this, false); } /** diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNode.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNode.java index a82d45fa469..edd2c18343f 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNode.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNode.java @@ -18,6 +18,8 @@ package org.apache.ignite.internal.configuration.tree; import java.util.NoSuchElementException; +import org.apache.ignite.configuration.annotation.InjectedValue; +import org.jetbrains.annotations.Nullable; /** * Interface for filling the configuration node. @@ -48,4 +50,11 @@ public interface ConstructableTreeNode { * @return {@code true} if node became immutable, {@code false} if it has already been immutable before. */ boolean makeImmutable(); + + /** + * Returns the name of the field annotated with {@link InjectedValue} or {@code null} if no such field exists. + */ + default @Nullable String injectedValueFieldName() { + return null; + } } diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java index 76a396e6f73..4e90427b4f0 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java @@ -17,6 +17,8 @@ package org.apache.ignite.internal.configuration.tree; +import static org.apache.ignite.internal.util.IgniteUtils.newHashMap; + import java.io.Serializable; import java.lang.reflect.Array; import java.lang.reflect.Field; @@ -82,7 +84,7 @@ public Object visitLeafNode(Field field, String key, Serializable val) { if (val instanceof Character || val instanceof UUID) { valObj = val.toString(); } else if (val != null && val.getClass().isArray()) { - valObj = toListOfObjects(field, val); + valObj = toListOfObjects(val); } else if (val instanceof String) { valObj = maskIfNeeded(field, (String) val); } @@ -92,7 +94,6 @@ public Object visitLeafNode(Field field, String key, Serializable val) { return valObj; } - /** {@inheritDoc} */ @Override public Object visitInnerNode(Field field, String key, InnerNode node) { if (skipEmptyValues && node == null) { @@ -107,33 +108,74 @@ public Object visitInnerNode(Field field, String key, InnerNode node) { deque.pop(); - addToParent(key, innerMap); + String injectedValueFieldName = node.injectedValueFieldName(); + + if (injectedValueFieldName != null) { + // If configuration contains an injected value, the rendered named list will be a map, where every injected value is represented + // as a separate key-value pair. + Object injectedValue = innerMap.get(injectedValueFieldName); + + addToParent(key, injectedValue); + + return injectedValue; + } else { + // Otherwise, the rendered named list will be a list of maps. + addToParent(key, innerMap); - return innerMap; + return innerMap; + } } - /** {@inheritDoc} */ @Override public Object visitNamedListNode(Field field, String key, NamedListNode node) { - if (skipEmptyValues && node.size() == 0) { + if (skipEmptyValues && node.isEmpty()) { return null; } - List list = new ArrayList<>(node.size()); + Object renderedList; + + boolean hasInjectedValues = !node.isEmpty() && getFirstNode(node).injectedValueFieldName() != null; + + // See the comment inside "visitInnerNode" why named lists are rendered differently for injected values. + if (hasInjectedValues) { + Map map = newHashMap(node.size()); + + deque.push(map); - deque.push(list); + for (String subkey : node.namedListKeys()) { + InnerNode innerNode = node.getInnerNode(subkey); - for (String subkey : node.namedListKeys()) { - node.getInnerNode(subkey).accept(field, subkey, this); + innerNode.accept(field, subkey, this); + } + + renderedList = map; + } else { + List list = new ArrayList<>(node.size()); + + deque.push(list); + + for (String subkey : node.namedListKeys()) { + InnerNode innerNode = node.getInnerNode(subkey); + + innerNode.accept(field, subkey, this); - ((Map) list.get(list.size() - 1)).put(node.syntheticKeyName(), subkey); + ((Map) list.get(list.size() - 1)).put(node.syntheticKeyName(), subkey); + } + + renderedList = list; } deque.pop(); - addToParent(key, list); + addToParent(key, renderedList); + + return renderedList; + } + + private static InnerNode getFirstNode(NamedListNode namedListNode) { + String firstKey = namedListNode.namedListKeys().get(0); - return list; + return namedListNode.getInnerNode(firstKey); } /** @@ -173,11 +215,10 @@ private void addToParent(String key, Object val) { /** * Converts array into a list of objects. Boxes array elements if they are primitive values. * - * @param field Field of the array * @param val Array of primitives or array of {@link String}s * @return List of objects corresponding to the passed array. */ - private List toListOfObjects(Field field, Serializable val) { + private static List toListOfObjects(Serializable val) { Stream stream = IntStream.range(0, Array.getLength(val)).mapToObj(i -> Array.get(val, i)); if (val.getClass().getComponentType() == char.class || val.getClass().getComponentType() == UUID.class) { diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java index 7ff2b2eae5f..a0e1a28b967 100644 --- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java +++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java @@ -56,6 +56,7 @@ import org.apache.ignite.configuration.annotation.ConfigurationExtension; import org.apache.ignite.configuration.annotation.ConfigurationRoot; import org.apache.ignite.configuration.annotation.InjectedName; +import org.apache.ignite.configuration.annotation.InjectedValue; import org.apache.ignite.configuration.annotation.InternalId; import org.apache.ignite.configuration.annotation.Name; import org.apache.ignite.configuration.annotation.NamedConfigValue; @@ -443,7 +444,7 @@ public static void checkConfigurationType(Collection> rootKeys, Co * @return {@code true} if field represents primitive configuration. */ public static boolean isValue(Field schemaField) { - return schemaField.isAnnotationPresent(Value.class); + return schemaField.isAnnotationPresent(Value.class) || schemaField.isAnnotationPresent(InjectedValue.class); } /** @@ -498,6 +499,12 @@ public static String syntheticKeyName(Field field) { public static boolean hasDefault(Field field) { assert isValue(field) : field; + InjectedValue injectedValue = field.getAnnotation(InjectedValue.class); + + if (injectedValue != null) { + return injectedValue.hasDefault(); + } + return field.getAnnotation(Value.class).hasDefault(); } @@ -1129,6 +1136,20 @@ public static boolean isInjectedName(Field schemaField) { return schemaField.isAnnotationPresent(InjectedName.class); } + /** + * Returns {@code true} if the given schema field contains the {@link InjectedValue} annotation. + */ + public static boolean isInjectedValue(Field schemaField) { + return schemaField.isAnnotationPresent(InjectedValue.class); + } + + /** + * Returns {@code true} if the given schema field is read-only. + */ + public static boolean isReadOnly(Field schemaField) { + return isPolymorphicId(schemaField) || isInjectedName(schemaField) || isInternalId(schemaField); + } + /** * Checks whether configuration schema field contains {@link Name}. * diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java new file mode 100644 index 00000000000..3d929c30558 --- /dev/null +++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.configuration; + +import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL; +import static org.apache.ignite.internal.configuration.hocon.HoconConverter.hoconSource; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigRenderOptions; +import com.typesafe.config.ConfigValue; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.ignite.configuration.RootKey; +import org.apache.ignite.configuration.annotation.Config; +import org.apache.ignite.configuration.annotation.ConfigurationRoot; +import org.apache.ignite.configuration.annotation.InjectedName; +import org.apache.ignite.configuration.annotation.InjectedValue; +import org.apache.ignite.configuration.annotation.NamedConfigValue; +import org.apache.ignite.internal.configuration.hocon.HoconConverter; +import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage; +import org.apache.ignite.internal.configuration.validation.TestConfigurationValidator; +import org.apache.ignite.internal.manager.ComponentContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Tests for schemas with {@link InjectedValue}s. + */ +public class InjectedValueConfigurationTest { + /** Root schema. */ + @ConfigurationRoot(rootName = "rootInjectedValue", type = LOCAL) + public static class HoconInjectedValueRootConfigurationSchema { + @NamedConfigValue + public HoconInjectedValueConfigurationSchema nestedNamed; + } + + /** Named list element schema. */ + @Config + public static class HoconInjectedValueConfigurationSchema { + @InjectedName + public String someName; + + @InjectedValue(hasDefault = true) + public String someValue = "default"; + } + + private ConfigurationRegistry registry; + + @BeforeEach + void setUp() { + List> roots = List.of( + HoconInjectedValueRootConfiguration.KEY + ); + + registry = new ConfigurationRegistry( + roots, + new TestConfigurationStorage(LOCAL), + new ConfigurationTreeGenerator(roots, List.of(), List.of()), + new TestConfigurationValidator() + ); + + assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully()); + } + + @AfterEach + void tearDown() { + assertThat(registry.stopAsync(new ComponentContext()), willCompleteSuccessfully()); + } + + @Nested + class HoconConverterTest { + @Test + void testEmpty() { + assertEquals("nestedNamed=[]", asHoconStr(List.of("rootInjectedValue"))); + } + + @Test + void testAssignValuesUsingObjectNotation() { + change("rootInjectedValue.nestedNamed = {foo: bar}"); + + assertEquals("nestedNamed{foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + change("rootInjectedValue.nestedNamed = {foo: bar, baz: quux}"); + + assertEquals("nestedNamed{baz=quux,foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + change("rootInjectedValue.nestedNamed = {baz: anotherQuux}"); + + assertEquals("nestedNamed{baz=anotherQuux,foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + change("rootInjectedValue.nestedNamed = {baz: null}"); + + assertEquals("nestedNamed{foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + } + + @Test + void testAssignValuesUsingListNotation() { + change("rootInjectedValue.nestedNamed = [{foo=bar}]"); + + assertEquals("nestedNamed{foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + change("rootInjectedValue.nestedNamed = [{foo=bar},{baz=quux}]"); + + assertEquals("nestedNamed{baz=quux,foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + change("rootInjectedValue.nestedNamed = [{baz=anotherQuux}]"); + + assertEquals("nestedNamed{baz=anotherQuux,foo=bar}", asHoconStr(List.of("rootInjectedValue"))); + + // Removing a value does not work in this notation. + } + } + + @Nested + class JavaApiTest { + @Test + void testEmpty() { + HoconInjectedValueRootConfiguration cfg = registry.getConfiguration(HoconInjectedValueRootConfiguration.KEY); + + assertThat(cfg.nestedNamed().value().size(), is(0)); + } + + @Test + void testDefaults() { + HoconInjectedValueRootConfiguration cfg = registry.getConfiguration(HoconInjectedValueRootConfiguration.KEY); + + CompletableFuture changeFuture = cfg.change(rootChange -> rootChange + .changeNestedNamed(nestedChange -> nestedChange + .create("foo", valueChange -> {}))); + + assertThat(changeFuture, willCompleteSuccessfully()); + + assertThat(cfg.value().nestedNamed().get("foo").someValue(), is("default")); + assertThat(cfg.nestedNamed().value().get("foo").someValue(), is("default")); + assertThat(cfg.nestedNamed().get("foo").value().someValue(), is("default")); + assertThat(cfg.nestedNamed().get("foo").someValue().value(), is("default")); + } + + @Test + void testAssignValues() { + HoconInjectedValueRootConfiguration cfg = registry.getConfiguration(HoconInjectedValueRootConfiguration.KEY); + + CompletableFuture changeFuture = cfg.change(rootChange -> rootChange + .changeNestedNamed(nestedChange -> nestedChange + .create("foo", valueChange -> valueChange.changeSomeValue("bar")))); + + assertThat(changeFuture, willCompleteSuccessfully()); + + assertThat(cfg.value().nestedNamed().get("foo").someValue(), is("bar")); + assertThat(cfg.nestedNamed().value().get("foo").someValue(), is("bar")); + assertThat(cfg.nestedNamed().get("foo").value().someValue(), is("bar")); + assertThat(cfg.nestedNamed().get("foo").someValue().value(), is("bar")); + } + } + + private void change(String hocon) { + assertThat( + registry.change(hoconSource(ConfigFactory.parseString(hocon).root())), + willCompleteSuccessfully() + ); + } + + private String asHoconStr(List basePath, String... path) { + List fullPath = Stream.concat(basePath.stream(), Arrays.stream(path)).collect(Collectors.toList()); + + ConfigValue hoconCfg = HoconConverter.represent(registry.superRoot(), fullPath); + + return hoconCfg.render(ConfigRenderOptions.concise().setJson(false)); + } +} diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java index f403cb9312c..7909d9cd439 100644 --- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java +++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java @@ -731,7 +731,7 @@ void testInjectedName() throws Throwable { // Let's check that the NamedConfigValue#syntheticKeyName key will not work. assertThrowsIllegalArgException( () -> change("rootInjectedName.nestedNamed = [{superName = foo}]"), - "'rootInjectedName.nestedNamed[0].someName' configuration value is mandatory and must be a String" + "'rootInjectedName.nestedNamed' configuration doesn't have the 'superName' sub-configuration" ); } diff --git a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItDistributionZonesFiltersTest.java b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItDistributionZonesFiltersTest.java index 8bfd18470fa..14786209b36 100644 --- a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItDistributionZonesFiltersTest.java +++ b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItDistributionZonesFiltersTest.java @@ -63,7 +63,7 @@ public class ItDistributionZonesFiltersTest extends ClusterPerTestIntegrationTes private static final String TABLE_NAME = "table1"; @Language("HOCON") - private static final String NODE_ATTRIBUTES = "{region.attribute = US, storage.attribute = SSD}"; + private static final String NODE_ATTRIBUTES = "{region = US, storage = SSD}"; private static final String STORAGE_PROFILES = String.format("'%s, %s'", DEFAULT_ROCKSDB_PROFILE_NAME, DEFAULT_AIPERSIST_PROFILE_NAME); @@ -115,7 +115,7 @@ void testFilteredDataNodesPropagatedToStable() throws Exception { String filter = "$[?(@.region == \"US\" && @.storage == \"SSD\")]"; // This node do not pass the filter - @Language("HOCON") String firstNodeAttributes = "{region:{attribute:\"EU\"},storage:{attribute:\"SSD\"}}"; + @Language("HOCON") String firstNodeAttributes = "{region: EU, storage: SSD}"; Ignite node = startNode(1, createStartConfig(firstNodeAttributes, STORAGE_PROFILES_CONFIGS)); @@ -123,10 +123,10 @@ void testFilteredDataNodesPropagatedToStable() throws Exception { node.sql().execute(null, createTableSql()); - MetaStorageManager metaStorageManager = (MetaStorageManager) IgniteTestUtils + MetaStorageManager metaStorageManager = IgniteTestUtils .getFieldValue(node, IgniteImpl.class, "metaStorageMgr"); - TableManager tableManager = (TableManager) IgniteTestUtils.getFieldValue(node, IgniteImpl.class, "distributedTblMgr"); + TableManager tableManager = IgniteTestUtils.getFieldValue(node, IgniteImpl.class, "distributedTblMgr"); TableViewInternal table = (TableViewInternal) tableManager.table(TABLE_NAME); @@ -140,7 +140,7 @@ void testFilteredDataNodesPropagatedToStable() throws Exception { TIMEOUT_MILLIS ); - @Language("HOCON") String secondNodeAttributes = "{region:{attribute:\"US\"},storage:{attribute:\"SSD\"}}"; + @Language("HOCON") String secondNodeAttributes = "{region: US, storage: SSD}"; // This node pass the filter but storage profiles of a node do not match zone's storage profiles. // TODO: https://issues.apache.org/jira/browse/IGNITE-21387 recovery of this node is failing, @@ -210,7 +210,7 @@ void testAlteringFiltersPropagatedDataNodesToStableImmediately() throws Exceptio TIMEOUT_MILLIS ); - @Language("HOCON") String firstNodeAttributes = "{region:{attribute:\"US\"},storage:{attribute:\"SSD\"}}"; + @Language("HOCON") String firstNodeAttributes = "{region: US, storage: SSD}"; // This node pass the filter startNode(1, createStartConfig(firstNodeAttributes, STORAGE_PROFILES_CONFIGS)); @@ -265,7 +265,7 @@ void testEmptyDataNodesDoNotPropagatedToStableAfterAlteringFilter() throws Excep TIMEOUT_MILLIS ); - @Language("HOCON") String firstNodeAttributes = "{region:{attribute:\"US\"},storage:{attribute:\"SSD\"}}"; + @Language("HOCON") String firstNodeAttributes = "{region: US, storage: SSD}"; // This node pass the filter startNode(1, createStartConfig(firstNodeAttributes, STORAGE_PROFILES_CONFIGS)); @@ -306,7 +306,7 @@ void testFilteredEmptyDataNodesDoNotTriggerRebalance() throws Exception { Ignite node0 = unwrapIgniteImpl(node(0)); // This node passes the filter - @Language("HOCON") String firstNodeAttributes = "{region:{attribute:\"EU\"},storage:{attribute:\"HDD\"}}"; + @Language("HOCON") String firstNodeAttributes = "{region: EU, storage: HDD}"; Ignite node1 = startNode(1, createStartConfig(firstNodeAttributes, STORAGE_PROFILES_CONFIGS)); @@ -324,7 +324,7 @@ void testFilteredEmptyDataNodesDoNotTriggerRebalance() throws Exception { node1.sql().execute(null, createTableSql()); - TableManager tableManager = (TableManager) IgniteTestUtils.getFieldValue(node0, IgniteImpl.class, "distributedTblMgr"); + TableManager tableManager = IgniteTestUtils.getFieldValue(node0, IgniteImpl.class, "distributedTblMgr"); TableViewInternal table = (TableViewInternal) tableManager.table(TABLE_NAME); @@ -350,7 +350,7 @@ void testFilteredEmptyDataNodesDoNotTriggerRebalanceOnReplicaUpdate() throws Exc IgniteImpl node0 = unwrapIgniteImpl(node(0)); // This node passes the filter - @Language("HOCON") String firstNodeAttributes = "{region:{attribute:\"EU\"},storage:{attribute:\"HDD\"}}"; + @Language("HOCON") String firstNodeAttributes = "{region: EU, storage: HDD}"; startNode(1, createStartConfig(firstNodeAttributes, STORAGE_PROFILES_CONFIGS)); diff --git a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceTriggersRecoveryTest.java b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceTriggersRecoveryTest.java index ac954febcd8..65729316584 100644 --- a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceTriggersRecoveryTest.java +++ b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceTriggersRecoveryTest.java @@ -62,7 +62,7 @@ public class ItRebalanceTriggersRecoveryTest extends ClusterPerTestIntegrationTe + " },\n" + " clientConnector: { port:{} },\n" + " nodeAttributes: {\n" - + " nodeAttributes: {region: {attribute: \"US\"}, zone: {attribute: \"global\"}}\n" + + " nodeAttributes: {region: US, zone: global}\n" + " },\n" + " rest.port: {},\n" + " failureHandler.dumpThreadsOnFailure: false\n" @@ -77,7 +77,7 @@ public class ItRebalanceTriggersRecoveryTest extends ClusterPerTestIntegrationTe + " },\n" + " clientConnector: { port:{} },\n" + " nodeAttributes: {\n" - + " nodeAttributes: {zone: {attribute: \"global\"}}\n" + + " nodeAttributes: {zone: global}\n" + " },\n" + " rest.port: {},\n" + " failureHandler.dumpThreadsOnFailure: false\n" diff --git a/modules/distribution-zones/src/test/java/org/apache/ignite/internal/distributionzones/BaseDistributionZoneManagerTest.java b/modules/distribution-zones/src/test/java/org/apache/ignite/internal/distributionzones/BaseDistributionZoneManagerTest.java index 251e3bbd603..26909c6b2ac 100644 --- a/modules/distribution-zones/src/test/java/org/apache/ignite/internal/distributionzones/BaseDistributionZoneManagerTest.java +++ b/modules/distribution-zones/src/test/java/org/apache/ignite/internal/distributionzones/BaseDistributionZoneManagerTest.java @@ -89,9 +89,7 @@ public abstract class BaseDistributionZoneManagerTest extends BaseIgniteAbstract private final List components = new ArrayList<>(); - @InjectConfiguration("mock.properties = {" - + PARTITION_DISTRIBUTION_RESET_TIMEOUT + ".propertyValue = \"" + IMMEDIATE_TIMER_VALUE + "\", " - + "}") + @InjectConfiguration("mock.properties." + PARTITION_DISTRIBUTION_RESET_TIMEOUT + " = \"" + IMMEDIATE_TIMER_VALUE + "\"") SystemDistributedConfiguration systemDistributedConfiguration; @BeforeEach diff --git a/modules/distribution-zones/tech-notes/filters.md b/modules/distribution-zones/tech-notes/filters.md index 0cad552679f..22269fdf695 100644 --- a/modules/distribution-zones/tech-notes/filters.md +++ b/modules/distribution-zones/tech-notes/filters.md @@ -16,8 +16,8 @@ In the `ignite-config.conf` it looks like this: ``` nodeAttributes.nodeAttributes { - region.attribute = "US" - storage.attribute = "SSD" + region = "US" + storage = "SSD" } ``` @@ -27,12 +27,8 @@ or like this in a `ignite-config.json`: { "nodeAttributes":{ "nodeAttributes":{ - "region":{ - "attribute":"US" - }, - "storage":{ - "attribute":"SSD" - } + "region": "US", + "storage": "SSD" } } } @@ -65,6 +61,3 @@ To check all capabilities of JSONPath, see https://github.com/json-path/JsonPath Note that as a default value for filter '$..*' filter is used, meaning that all nodes match the filter. Also it is important, that if there are no specified attributes for a node, it means that a node match only the default filter. - - - diff --git a/modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/TestMetasStorageUtils.java b/modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/TestMetasStorageUtils.java index 67deee87073..40a4482d30b 100644 --- a/modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/TestMetasStorageUtils.java +++ b/modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/TestMetasStorageUtils.java @@ -81,8 +81,8 @@ public static boolean equals(Entry act, Entry exp) { public static String createClusterConfigWithCompactionProperties(long interval, long dataAvailabilityTime) { return String.format( "ignite.system.properties: {" - + "%s.propertyValue= \"%s\", " - + "%s.propertyValue= \"%s\"" + + "%s = \"%s\", " + + "%s = \"%s\"" + "}", INTERVAL_SYSTEM_PROPERTY_NAME, interval, DATA_AVAILABILITY_TIME_SYSTEM_PROPERTY_NAME, dataAvailabilityTime ); diff --git a/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/impl/MetaStorageCompactionTriggerConfigurationTest.java b/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/impl/MetaStorageCompactionTriggerConfigurationTest.java index 296fed8b936..17364d6ee4f 100644 --- a/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/impl/MetaStorageCompactionTriggerConfigurationTest.java +++ b/modules/metastorage/src/test/java/org/apache/ignite/internal/metastorage/impl/MetaStorageCompactionTriggerConfigurationTest.java @@ -52,8 +52,8 @@ void testEmptySystemProperties(@InjectConfiguration SystemDistributedConfigurati @Test void testValidSystemPropertiesOnStart( @InjectConfiguration("mock.properties = {" - + INTERVAL_SYSTEM_PROPERTY_NAME + ".propertyValue = \"100\", " - + DATA_AVAILABILITY_TIME_SYSTEM_PROPERTY_NAME + ".propertyValue = \"500\"" + + INTERVAL_SYSTEM_PROPERTY_NAME + " = \"100\", " + + DATA_AVAILABILITY_TIME_SYSTEM_PROPERTY_NAME + " = \"500\"" + "}") SystemDistributedConfiguration systemConfig ) { diff --git a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java index 749194de65a..6280a5e0f6c 100644 --- a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java +++ b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/ItReplicaLifecycleTest.java @@ -256,13 +256,13 @@ public class ItReplicaLifecycleTest extends BaseIgniteAbstractTest { @InjectConfiguration private static SystemLocalConfiguration systemConfiguration; - @InjectConfiguration("mock.nodeAttributes: {region.attribute = US, storage.attribute = SSD}") + @InjectConfiguration("mock.nodeAttributes: {region = US, storage = SSD}") private static NodeAttributesConfiguration nodeAttributes1; - @InjectConfiguration("mock.nodeAttributes: {region.attribute = EU, storage.attribute = SSD}") + @InjectConfiguration("mock.nodeAttributes: {region = EU, storage = SSD}") private static NodeAttributesConfiguration nodeAttributes2; - @InjectConfiguration("mock.nodeAttributes: {region.attribute = UK, storage.attribute = SSD}") + @InjectConfiguration("mock.nodeAttributes: {region = UK, storage = SSD}") private static NodeAttributesConfiguration nodeAttributes3; @InjectConfiguration diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java index e9efcc2d8c9..3e86b870d6a 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java @@ -1028,8 +1028,8 @@ public void changeNodeAttributesConfigurationOnStartTest() { stopNode(0); String newAttributesCfg = "{\n" - + " region.attribute = \"US\"\n" - + " storage.attribute = \"SSD\"\n" + + " region = US\n" + + " storage = SSD\n" + "}"; Map newAttributesMap = Map.of("region", "US", "storage", "SSD"); diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItReplicaStateManagerTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItReplicaStateManagerTest.java index 6f2cd40ea40..615a03b0bce 100644 --- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItReplicaStateManagerTest.java +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItReplicaStateManagerTest.java @@ -52,9 +52,9 @@ */ public class ItReplicaStateManagerTest extends BaseIgniteRestartTest { private static final String[] ATTRIBUTES = { - "{region:{attribute:\"REG0\"}}", - "{region:{attribute:\"REG1\"}}", - "{region:{attribute:\"REG2\"}}" + "{ region: REG0 }", + "{ region: REG1 }", + "{ region: REG2 }" }; private static final String ZONE_NAME = "TEST_ZONE"; diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUnstableTopologyTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUnstableTopologyTest.java index 6c703266b78..3c716423b98 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUnstableTopologyTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItUnstableTopologyTest.java @@ -37,7 +37,7 @@ public class ItUnstableTopologyTest extends BaseSqlIntegrationTest { + " },\n" + " clientConnector: { port:{} },\n" + " nodeAttributes: {\n" - + " nodeAttributes: {role: {attribute: \"data\"}}\n" + + " nodeAttributes: { role: data }\n" + " },\n" + " rest.port: {},\n" + " failureHandler.dumpThreadsOnFailure: false\n" diff --git a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java index 08dc440b6a8..7456704f5f9 100644 --- a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java +++ b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java @@ -24,11 +24,11 @@ /** Test suite for the cases with a recovery of the group replication factor after reset by zone filter update. */ public class ItHighAvailablePartitionsRecoveryByFilterUpdateTest extends AbstractHighAvailablePartitionsRecoveryTest { - private static final String GLOBAL_EU_NODES_CONFIG = nodeConfig("{region.attribute = EU, zone.attribute = global}"); + private static final String GLOBAL_EU_NODES_CONFIG = nodeConfig("{region = EU, zone = global}"); - private static final String EU_ONLY_NODES_CONFIG = nodeConfig("{region.attribute = EU}"); + private static final String EU_ONLY_NODES_CONFIG = nodeConfig("{region = EU}"); - private static final String GLOBAL_NODES_CONFIG = nodeConfig("{zone.attribute = global}"); + private static final String GLOBAL_NODES_CONFIG = nodeConfig("{zone = global}"); @Override protected int initialNodes() {