IGNITE-14924 HOCON configuration source and representation implemented. (#228)
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java
new file mode 100644
index 0000000..e0a8fe6
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java
@@ -0,0 +1,567 @@
+/*
+ * 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.hocon;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.internal.configuration.ConfigurationRegistry;
+import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.ignite.internal.configuration.hocon.HoconConverter.hoconSource;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+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.assertThrows;
+
+/**
+ * Tests for the {@link HoconConverter}.
+ */
+public class HoconConverterTest {
+ /** */
+ @ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
+ public static class HoconRootConfigurationSchema {
+ /** */
+ @NamedConfigValue(syntheticKeyName = "a")
+ public HoconArraysConfigurationSchema arraysList;
+
+ /** */
+ @NamedConfigValue(syntheticKeyName = "p")
+ public HoconPrimitivesConfigurationSchema primitivesList;
+ }
+
+ /**
+ * Configuration schema for testing the support of arrays of primitives.
+ */
+ @Config
+ public static class HoconArraysConfigurationSchema {
+ /** */
+ @Value(hasDefault = true)
+ public boolean[] booleans = {false};
+
+ /** */
+ @Value(hasDefault = true)
+ public byte[] bytes = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public short[] shorts = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public int[] ints = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public long[] longs = {0L};
+
+ /** */
+ @Value(hasDefault = true)
+ public char[] chars = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public float[] floats = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public double[] doubles = {0};
+
+ /** */
+ @Value(hasDefault = true)
+ public String[] strings = {""};
+ }
+
+ /**
+ * Configuration schema for testing the support of primitives.
+ */
+ @Config
+ public static class HoconPrimitivesConfigurationSchema {
+ /** */
+ @Value(hasDefault = true)
+ public boolean booleanVal = false;
+
+ /** */
+ @Value(hasDefault = true)
+ public byte byteVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public short shortVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public int intVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public long longVal = 0L;
+
+ /** */
+ @Value(hasDefault = true)
+ public char charVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public float floatVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public double doubleVal = 0;
+
+ /** */
+ @Value(hasDefault = true)
+ public String stringVal = "";
+ }
+
+ /** */
+ private static ConfigurationRegistry registry;
+
+ /** */
+ private static HoconRootConfiguration configuration;
+
+ /** */
+ @BeforeAll
+ public static void beforeAll() {
+ registry = new ConfigurationRegistry(
+ Collections.singletonList(HoconRootConfiguration.KEY),
+ Collections.emptyMap(),
+ Collections.singletonList(new TestConfigurationStorage())
+ );
+
+ configuration = registry.getConfiguration(HoconRootConfiguration.KEY);
+ }
+
+ /** */
+ @AfterAll
+ public static void after() {
+ registry.stop();
+
+ registry = null;
+
+ configuration = null;
+ }
+
+ /** */
+ @BeforeEach
+ public void before() throws Exception {
+ configuration.change(cfg -> cfg
+ .changePrimitivesList(list -> list.namedListKeys().forEach(list::delete))
+ .changeArraysList(list -> list.namedListKeys().forEach(list::delete))
+ ).get(1, SECONDS);
+ }
+
+ /** */
+ @Test
+ public void toHoconBasic() {
+ assertEquals("root{arraysList=[],primitivesList=[]}", asHoconStr(List.of()));
+
+ assertEquals("arraysList=[],primitivesList=[]", asHoconStr(List.of("root")));
+
+ assertEquals("[]", asHoconStr(List.of("root", "arraysList")));
+
+ assertThrowsIllegalArgException(
+ () -> HoconConverter.represent(registry, List.of("doot")),
+ "Configuration 'doot' is not found"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> HoconConverter.represent(registry, List.of("root", "x")),
+ "Configuration 'root.x' is not found"
+ );
+
+ assertEquals("null", asHoconStr(List.of("root", "primitivesList", "foo")));
+ }
+
+ /**
+ * Tests that the {@code HoconConverter} supports serialization of Strings and primitives.
+ */
+ @Test
+ public void testHoconPrimitivesSerialization() throws Exception {
+ configuration.change(cfg -> cfg
+ .changePrimitivesList(primitivesList -> primitivesList
+ .create("name", primitives -> {})
+ )
+ ).get(1, SECONDS);
+
+ var basePath = List.of("root", "primitivesList", "name");
+
+ assertEquals(
+ "booleanVal=false,byteVal=0,charVal=\"\\u0000\",doubleVal=0.0,floatVal=0,intVal=0,longVal=0,shortVal=0,stringVal=\"\"",
+ asHoconStr(basePath)
+ );
+
+ assertEquals("false", asHoconStr(basePath, "booleanVal"));
+ assertEquals("0", asHoconStr(basePath, "byteVal"));
+ assertEquals("0", asHoconStr(basePath, "shortVal"));
+ assertEquals("0", asHoconStr(basePath, "intVal"));
+ assertEquals("0", asHoconStr(basePath, "longVal"));
+ assertEquals("\"\\u0000\"", asHoconStr(basePath, "charVal"));
+ assertEquals("0", asHoconStr(basePath, "floatVal"));
+ assertEquals("0.0", asHoconStr(basePath, "doubleVal"));
+ assertEquals("\"\"", asHoconStr(basePath, "stringVal"));
+ }
+
+ /**
+ * Tests that the {@code HoconConverter} supports serialization of arrays of Strings and primitives.
+ */
+ @Test
+ public void testHoconArraysSerialization() throws Exception {
+ configuration.change(cfg -> cfg
+ .changeArraysList(arraysList -> arraysList
+ .create("name", arrays -> {})
+ )
+ ).get(1, SECONDS);
+
+ var basePath = List.of("root", "arraysList", "name");
+
+ assertEquals(
+ "booleans=[false],bytes=[0],chars=[\"\\u0000\"],doubles=[0.0],floats=[0],ints=[0],longs=[0],shorts=[0],strings=[\"\"]",
+ asHoconStr(basePath)
+ );
+
+ assertEquals("[false]", asHoconStr(basePath, "booleans"));
+ assertEquals("[0]", asHoconStr(basePath, "bytes"));
+ assertEquals("[0]", asHoconStr(basePath, "shorts"));
+ assertEquals("[0]", asHoconStr(basePath, "ints"));
+ assertEquals("[0]", asHoconStr(basePath, "longs"));
+ assertEquals("[\"\\u0000\"]", asHoconStr(basePath, "chars"));
+ assertEquals("[0]", asHoconStr(basePath, "floats"));
+ assertEquals("[0.0]", asHoconStr(basePath, "doubles"));
+ assertEquals("[\"\"]", asHoconStr(basePath, "strings"));
+ }
+
+ /**
+ * Retrieves the HOCON configuration located at the given path.
+ */
+ private String asHoconStr(List<String> basePath, String... path) {
+ List<String> fullPath = Stream.concat(basePath.stream(), Arrays.stream(path)).collect(Collectors.toList());
+
+ ConfigValue hoconCfg = HoconConverter.represent(registry, fullPath);
+
+ return hoconCfg.render(ConfigRenderOptions.concise().setJson(false));
+ }
+
+ /** */
+ @Test
+ public void fromHoconBasic() {
+ // Wrong names:
+ assertThrowsIllegalArgException(
+ () -> change("doot : {}"),
+ "'doot' configuration root doesn't exist"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.foo : {}"),
+ "'root' configuration doesn't have the 'foo' sub-configuration"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.x = 1"),
+ "'root.arraysList.name' configuration doesn't have the 'x' sub-configuration"
+ );
+
+ // Wrong node types:
+ assertThrowsIllegalArgException(
+ () -> change("root = foo"),
+ "'root' is expected to be a composite configuration node, not a single value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList = foo"),
+ "'root.arraysList' is expected to be a composite configuration node, not a single value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name = foo"),
+ "'root.arraysList.name' is expected to be a composite configuration node, not a single value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.ints = {}"),
+ "'int[]' is expected as a type for the 'root.arraysList.name.ints' configuration value"
+ );
+
+ // Wrong ordered named list syntax:
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList = [1]"),
+ "'root.arraysList[0]' is expected to be a composite configuration node, not a single value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList = [{}]"),
+ "'root.arraysList[0].a' configuration value is mandatory and must be a String"
+ );
+ }
+
+ /**
+ * Tests that the {@code HoconConverter} supports deserialization of Strings and primitives.
+ */
+ @Test
+ public void testHoconPrimitivesDeserialization() throws Throwable {
+ change("root.primitivesList = [{p = name}]");
+
+ HoconPrimitivesConfiguration primitives = configuration.primitivesList().get("name");
+ assertNotNull(primitives);
+
+ change("root.primitivesList.name.booleanVal = true");
+ assertThat(primitives.booleanVal().value(), is(true));
+
+ change("root.primitivesList.name.byteVal = 123");
+ assertThat(primitives.byteVal().value(), is((byte)123));
+
+ change("root.primitivesList.name.shortVal = 12345");
+ assertThat(primitives.shortVal().value(), is((short)12345));
+
+ change("root.primitivesList.name.intVal = 12345");
+ assertThat(primitives.intVal().value(), is(12345));
+
+ change("root.primitivesList.name.longVal = 12345678900");
+ assertThat(primitives.longVal().value(), is(12345678900L));
+
+ change("root.primitivesList.name.charVal = p");
+ assertThat(primitives.charVal().value(), is('p'));
+
+ change("root.primitivesList.name.floatVal = 2.5");
+ assertThat(primitives.floatVal().value(), is(2.5f));
+
+ change("root.primitivesList.name.doubleVal = 2.5");
+ assertThat(primitives.doubleVal().value(), is(2.5d));
+
+ change("root.primitivesList.name.stringVal = foo");
+ assertThat(primitives.stringVal().value(), is("foo"));
+ }
+
+ /**
+ * Tests deserialization errors that can happen during the deserialization of primitives.
+ */
+ @Test
+ public void testInvalidHoconPrimitivesDeserialization() {
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.booleanVal = \"true\""),
+ "'boolean' is expected as a type for the 'root.primitivesList.name.booleanVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.byteVal = 290"),
+ "Value '290' of 'root.primitivesList.name.byteVal' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.byteVal = false"),
+ "'byte' is expected as a type for the 'root.primitivesList.name.byteVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.shortVal = 12345678900"),
+ "Value '12345678900' of 'root.primitivesList.name.shortVal' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.shortVal = false"),
+ "'short' is expected as a type for the 'root.primitivesList.name.shortVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.intVal = 12345678900"),
+ "Value '12345678900' of 'root.primitivesList.name.intVal' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.intVal = false"),
+ "'int' is expected as a type for the 'root.primitivesList.name.intVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.longVal = false"),
+ "'long' is expected as a type for the 'root.primitivesList.name.longVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.charVal = 10"),
+ "'char' is expected as a type for the 'root.primitivesList.name.charVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.floatVal = false"),
+ "'float' is expected as a type for the 'root.primitivesList.name.floatVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.doubleVal = []"),
+ "'double' is expected as a type for the 'root.primitivesList.name.doubleVal' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.primitivesList.name.stringVal = 10"),
+ "'String' is expected as a type for the 'root.primitivesList.name.stringVal' configuration value"
+ );
+ }
+
+ /**
+ * Tests that the {@code HoconConverter} supports deserialization of arrays of Strings and primitives.
+ */
+ @Test
+ public void testHoconArraysDeserialization() throws Throwable {
+ change("root.arraysList = [{a = name}]");
+
+ HoconArraysConfiguration arrays = configuration.arraysList().get("name");
+ assertNotNull(arrays);
+
+ change("root.arraysList.name.booleans = [true]");
+ assertThat(arrays.booleans().value(), is(new boolean[] {true}));
+
+ change("root.arraysList.name.bytes = [123]");
+ assertThat(arrays.bytes().value(), is(new byte[] {123}));
+
+ change("root.arraysList.name.shorts = [123]");
+ assertThat(arrays.shorts().value(), is(new short[] {123}));
+
+ change("root.arraysList.name.ints = [12345]");
+ assertThat(arrays.ints().value(), is(new int[] {12345}));
+
+ change("root.arraysList.name.longs = [12345678900]");
+ assertThat(arrays.longs().value(), is(new long[] {12345678900L}));
+
+ change("root.arraysList.name.chars = [p]");
+ assertThat(arrays.chars().value(), is(new char[] {'p'}));
+
+ change("root.arraysList.name.floats = [2.5]");
+ assertThat(arrays.floats().value(), is(new float[] {2.5f}));
+
+ change("root.arraysList.name.doubles = [2.5]");
+ assertThat(arrays.doubles().value(), is(new double[] {2.5d}));
+
+ change("root.arraysList.name.strings = [foo]");
+ assertThat(arrays.strings().value(), is(new String[] {"foo"}));
+ }
+
+ /**
+ * Tests deserialization errors that can happen during the deserialization of arrays of primitives.
+ */
+ @Test
+ public void testInvalidHoconArraysDeserialization() {
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.booleans = true"),
+ "'boolean[]' is expected as a type for the 'root.arraysList.name.booleans' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.booleans = [{}]"),
+ "'boolean' is expected as a type for the 'root.arraysList.name.booleans[0]' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.bytes = [123, 290]"),
+ "Value '290' of 'root.arraysList.name.bytes[1]' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.bytes = false"),
+ "'byte[]' is expected as a type for the 'root.arraysList.name.bytes' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.shorts = [12345678900]"),
+ "Value '12345678900' of 'root.arraysList.name.shorts[0]' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.shorts = [123, false]"),
+ "'short' is expected as a type for the 'root.arraysList.name.shorts[1]' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.ints = [5, 12345678900]"),
+ "Value '12345678900' of 'root.arraysList.name.ints[1]' is out of its declared bounds"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.ints = false"),
+ "'int[]' is expected as a type for the 'root.arraysList.name.ints' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.longs = [foo]"),
+ "'long' is expected as a type for the 'root.arraysList.name.longs[0]' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.chars = 10"),
+ "'char[]' is expected as a type for the 'root.arraysList.name.chars' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.chars = [abc]"),
+ "'char' is expected as a type for the 'root.arraysList.name.chars[0]' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.floats = [1.2, foo]"),
+ "'float' is expected as a type for the 'root.arraysList.name.floats[1]' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.doubles = foo"),
+ "'double[]' is expected as a type for the 'root.arraysList.name.doubles' configuration value"
+ );
+
+ assertThrowsIllegalArgException(
+ () -> change("root.arraysList.name.strings = 10"),
+ "'String[]' is expected as a type for the 'root.arraysList.name.strings' configuration value"
+ );
+ }
+
+ /**
+ * Updates the configuration using the provided HOCON string.
+ */
+ private void change(String hocon) throws Throwable {
+ try {
+ registry.change(hoconSource(ConfigFactory.parseString(hocon).root()), null).get(1, SECONDS);
+ }
+ catch (ExecutionException e) {
+ throw e.getCause();
+ }
+ }
+
+ /** */
+ private static void assertThrowsIllegalArgException(Executable executable, String msg) {
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, executable);
+
+ assertThat(e.getMessage(), containsString(msg));
+ }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/JsonConverterTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/JsonConverterTest.java
index 51ee93a..deeb07c 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/JsonConverterTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/JsonConverterTest.java
@@ -31,6 +31,7 @@
import org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.internal.configuration.ConfigurationRegistry;
+import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/TestConfigurationStorage.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/TestConfigurationStorage.java
deleted file mode 100644
index 4b916d6..0000000
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/json/TestConfigurationStorage.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.json;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
-import org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
-import org.apache.ignite.internal.configuration.storage.ConfigurationStorageListener;
-import org.apache.ignite.internal.configuration.storage.Data;
-import org.apache.ignite.internal.configuration.storage.StorageException;
-
-/** */
-class TestConfigurationStorage implements ConfigurationStorage {
- /** */
- private final Set<ConfigurationStorageListener> listeners = new HashSet<>();
-
- /** {@inheritDoc} */
- @Override public Data readAll() throws StorageException {
- return new Data(Collections.emptyMap(), 0);
- }
-
- /** {@inheritDoc} */
- @Override public CompletableFuture<Boolean> write(Map<String, Serializable> newValues, long version) {
- for (ConfigurationStorageListener listener : listeners)
- listener.onEntriesChanged(new Data(newValues, version + 1));
-
- return CompletableFuture.completedFuture(true);
- }
-
- /** {@inheritDoc} */
- @Override public void registerConfigurationListener(ConfigurationStorageListener listener) {
- listeners.add(listener);
- }
-
- /** {@inheritDoc} */
- @Override public void notifyApplied(long storageRevision) {
- }
-
- /** {@inheritDoc} */
- @Override public ConfigurationType type() {
- return ConfigurationType.LOCAL;
- }
-}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
index 1894f6a..31a42a7 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
@@ -41,6 +41,7 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** */
@@ -197,8 +198,14 @@
assertNotNull(elementNode);
+ assertSame(elementNode, elementsNode.get(0));
+
assertNull(elementNode.strCfg());
+ assertThrows(IndexOutOfBoundsException.class, () -> elementsNode.get(-1));
+
+ assertThrows(IndexOutOfBoundsException.class, () -> elementsNode.get(1));
+
elementsNode.createOrUpdate("keyPut", element -> element.changeStrCfg("val"));
// Assert that consecutive put methods create new object every time.
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
index 3371295..ba42132 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
@@ -88,7 +88,8 @@
* @return {@code this} for chaining.
*
* @throws NullPointerException If key is null.
- * @throws IllegalArgumentException If {@link #createOrUpdate(String, Consumer)} has been invoked with the same key previously.
+ * @throws IllegalArgumentException If {@link #createOrUpdate(String, Consumer)} has been invoked with the same key
+ * previously.
*/
NamedListChange<Change> delete(String key);
}
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
index 60d8e9c..c422cb3 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
@@ -41,6 +41,15 @@
View get(String key);
/**
+ * Returns value located at the specified index.
+ *
+ * @param index Value index.
+ * @return Requested value.
+ * @throws IndexOutOfBoundsException If index is out of bounds.
+ */
+ View get(int index) throws IndexOutOfBoundsException;
+
+ /**
* Returns the number of elements in this list.
*
* @return Number of elements.
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
index b5752b2..1746135 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
@@ -41,8 +41,30 @@
* }
* </code></pre>
*/
-@Target({ FIELD })
+@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface NamedConfigValue {
+ /**
+ * Key that can be used in HOCON configuration syntax to declare named list with fixed order.
+ * <pre><code>
+ * {
+ * root : {
+ * namedList : [
+ * {
+ * syntheticKey : Element1,
+ * someValue = Value1
+ * },
+ * {
+ * syntheticKey : Element2,
+ * someValue = Value2
+ * }
+ * ]
+ * }
+ * }
+ * </code></pre>
+ *
+ * @return Name for the synthetic key.
+ */
+ String syntheticKeyName() default "name";
}
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Max.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Max.java
index 8a4f19a..70a8e47 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Max.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Max.java
@@ -25,7 +25,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
- * Signifies that this value has upper limit (exclusive).
+ * Signifies that this value has upper limit (inclusive).
*/
@Target(FIELD)
@Retention(RUNTIME)
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Min.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Min.java
index 54b1ac4..e5e82dc 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Min.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/Min.java
@@ -25,7 +25,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
- * Signifies that this value has lower limit (exclusive).
+ * Signifies that this value has lower limit (inclusive).
*/
@Target(FIELD)
@Retention(RUNTIME)
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
index 1cb81e4..9885e4a 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
@@ -24,14 +24,12 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import com.typesafe.config.ConfigFactory;
-import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigObject;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.configuration.annotation.ConfigurationType;
import org.apache.ignite.configuration.validation.Validator;
-import org.apache.ignite.internal.configuration.json.JsonConverter;
+import org.apache.ignite.internal.configuration.hocon.HoconConverter;
import org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
/**
@@ -84,18 +82,16 @@
/**
* Bootstrap configuration manager with customer user cfg.
+ *
* @param hoconStr Customer configuration in hocon format.
* @param type Configuration type.
* @throws InterruptedException If thread is interrupted during bootstrap.
* @throws ExecutionException If configuration update failed for some reason.
*/
public void bootstrap(String hoconStr, ConfigurationType type) throws InterruptedException, ExecutionException {
- // TODO https://issues.apache.org/jira/browse/IGNITE-14924 Implement HoconConfigurationSource
- String jsonStr = ConfigFactory.parseString(hoconStr).root().render(ConfigRenderOptions.concise());
+ ConfigObject hoconCfg = ConfigFactory.parseString(hoconStr).root();
- JsonObject jsonCfg = JsonParser.parseString(jsonStr).getAsJsonObject();
-
- confRegistry.change(JsonConverter.jsonSource(jsonCfg), configurationStorages.get(type)).get();
+ confRegistry.change(HoconConverter.hoconSource(hoconCfg), configurationStorages.get(type)).get();
}
/**
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
index 37351a6..9848272 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/asm/ConfigurationAsmGenerator.java
@@ -450,12 +450,18 @@
if (!isNamedConfigValue(schemaField))
continue;
+ NamedConfigValue namedCfgAnnotation = schemaField.getAnnotation(NamedConfigValue.class);
+
SchemaClassesInfo fieldClassNames = new SchemaClassesInfo(schemaField.getType());
- // this.values = new NamedListNode<>(ValueNode::new);
+ // this.values = new NamedListNode<>(key, ValueNode::new);
ctor.getBody().append(ctor.getThis().setField(
fieldDefs.get(schemaField.getName()),
- newInstance(NamedListNode.class, newNamedListElementLambda(fieldClassNames.nodeClassName))
+ newInstance(
+ NamedListNode.class,
+ constantString(namedCfgAnnotation.syntheticKeyName()),
+ newNamedListElementLambda(fieldClassNames.nodeClassName)
+ )
));
}
@@ -720,15 +726,21 @@
)
);
}
- // this.field = src == null ? new NamedListNode(ValueNode::new) : src.descend(field = field.copy()));
+ // this.field = src == null ? new NamedListNode<>(key, ValueNode::new) : src.descend(field = field.copy()));
else {
+ NamedConfigValue namedCfgAnnotation = schemaField.getAnnotation(NamedConfigValue.class);
+
String fieldNodeClassName = schemasInfo.get(schemaField.getType()).nodeClassName;
caseClause.append(new IfStatement()
.condition(isNull(srcVar))
.ifTrue(constructMtd.getThis().setField(
fieldDef,
- newInstance(NamedListNode.class, newNamedListElementLambda(fieldNodeClassName))
+ newInstance(
+ NamedListNode.class,
+ constantString(namedCfgAnnotation.syntheticKeyName()),
+ newNamedListElementLambda(fieldNodeClassName)
+ )
))
.ifFalse(new BytecodeBlock()
.append(constructMtd.getThis().setField(
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConfigurationVisitor.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConfigurationVisitor.java
new file mode 100644
index 0000000..7485792
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConfigurationVisitor.java
@@ -0,0 +1,122 @@
+/*
+ * 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.hocon;
+
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+import org.apache.ignite.internal.configuration.tree.NamedListNode;
+
+/**
+ * {@link ConfigurationVisitor} implementation that converts a configuration tree to a combination of {@link Map} and
+ * {@link List} objects to convert the result into HOCON {@link ConfigValue} instance.
+ */
+class HoconConfigurationVisitor implements ConfigurationVisitor<Object> {
+ /** Stack with intermediate results. Used to store values during recursive calls. */
+ private Deque<Object> deque = new ArrayDeque<>();
+
+ /** {@inheritDoc} */
+ @Override public Object visitLeafNode(String key, Serializable val) {
+ Object valObj = val;
+
+ if (val instanceof Character)
+ valObj = val.toString();
+ else if (val != null && val.getClass().isArray())
+ valObj = toListOfObjects(val);
+
+ addToParent(key, valObj);
+
+ return valObj;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Object visitInnerNode(String key, InnerNode node) {
+ Map<String, Object> innerMap = new HashMap<>();
+
+ deque.push(innerMap);
+
+ node.traverseChildren(this);
+
+ deque.pop();
+
+ addToParent(key, innerMap);
+
+ return innerMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> node) {
+ List<Object> list = new ArrayList<>(node.size());
+
+ deque.push(list);
+
+ for (String subkey : node.namedListKeys()) {
+ node.get(subkey).accept(subkey, this);
+
+ ((Map<String, Object>)list.get(list.size() - 1)).put(node.syntheticKeyName(), subkey);
+ }
+
+ deque.pop();
+
+ addToParent(key, list);
+
+ return list;
+ }
+
+ /**
+ * Adds a sub-element to the parent object if it exists.
+ *
+ * @param key Key for the passed element
+ * @param val Value to add to the parent
+ */
+ private void addToParent(String key, Object val) {
+ Object parent = deque.peek();
+
+ if (parent instanceof Map)
+ ((Map<String, Object>)parent).put(key, val);
+ else if (parent instanceof List)
+ ((Collection<Object>)parent).add(val);
+ }
+
+ /**
+ * Converts array into a list of objects. Boxes array elements if they are primitive values.
+ *
+ * @param val Array of primitives or array of {@link String}s
+ * @return List of objects corresponding to the passed array.
+ */
+ private List<?> toListOfObjects(Serializable val) {
+ Stream<?> stream = IntStream.range(0, Array.getLength(val)).mapToObj(i -> Array.get(val, i));
+
+ if (val.getClass().getComponentType() == char.class)
+ stream = stream.map(Object::toString);
+
+ return stream.collect(Collectors.toList());
+ }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
new file mode 100644
index 0000000..5ce3bd2
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
@@ -0,0 +1,53 @@
+/*
+ * 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.hocon;
+
+import java.util.List;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.impl.ConfigImpl;
+import org.apache.ignite.internal.configuration.ConfigurationRegistry;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.jetbrains.annotations.NotNull;
+
+public class HoconConverter {
+ /**
+ * Converts configuration subtree to a HOCON {@link ConfigValue} instance.
+ *
+ * @param registry Configuration registry instance.
+ * @param path Path to the configuration subtree. Can be empty, can't be {@code null}.
+ * @return {@link ConfigValue} instance that represents configuration subtree.
+ * @throws IllegalArgumentException If {@code path} is not found in current configuration.
+ */
+ public static ConfigValue represent(
+ ConfigurationRegistry registry,
+ @NotNull List<String> path
+ ) throws IllegalArgumentException {
+ Object res = registry.represent(path, new HoconConfigurationVisitor());
+
+ return ConfigImpl.fromAnyRef(res, null);
+ }
+
+ /**
+ * @param hoconCfg HOCON that has to be converted to the configuration source.
+ * @return HOCON-based configuration source.
+ */
+ public static ConfigurationSource hoconSource(ConfigObject hoconCfg) {
+ return new HoconObjectConfigurationSource(null, List.of(), hoconCfg);
+ }
+}
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
new file mode 100644
index 0000000..0deb215
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconListConfigurationSource.java
@@ -0,0 +1,149 @@
+/*
+ * 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.hocon;
+
+import java.lang.reflect.Array;
+import java.util.Iterator;
+import java.util.List;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import org.apache.ignite.internal.configuration.TypeUtils;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
+import org.apache.ignite.internal.configuration.tree.NamedListNode;
+
+import static java.lang.String.format;
+import static org.apache.ignite.internal.configuration.hocon.HoconPrimitiveConfigurationSource.formatArrayPath;
+import static org.apache.ignite.internal.configuration.hocon.HoconPrimitiveConfigurationSource.unwrapPrimitive;
+import static org.apache.ignite.internal.configuration.hocon.HoconPrimitiveConfigurationSource.wrongTypeException;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.appendKey;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.join;
+
+/**
+ * {@link ConfigurationSource} created from a HOCON list.
+ */
+class HoconListConfigurationSource implements ConfigurationSource {
+ /**
+ * Current path inside the top-level HOCON object.
+ */
+ private final List<String> path;
+
+ /**
+ * HOCON list that this source has been created from.
+ */
+ private final ConfigList hoconCfgList;
+
+ /**
+ * Creates a {@link ConfigurationSource} from the given HOCON list.
+ *
+ * @param path Current path inside the top-level HOCON object. Can be empty if the given {@code hoconCfgList}
+ * is the top-level object.
+ * @param hoconCfgList HOCON list.
+ */
+ HoconListConfigurationSource(List<String> path, ConfigList hoconCfgList) {
+ this.path = path;
+ this.hoconCfgList = hoconCfgList;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T> T unwrap(Class<T> clazz) {
+ if (!clazz.isArray())
+ throw wrongTypeException(clazz, path, -1);
+
+ int size = hoconCfgList.size();
+
+ Class<?> componentType = clazz.getComponentType();
+ Class<?> boxedComponentType = box(componentType);
+
+ Object resArray = Array.newInstance(componentType, size);
+
+ int idx = 0;
+ for (Iterator<ConfigValue> iterator = hoconCfgList.iterator(); iterator.hasNext(); idx++) {
+ ConfigValue hoconCfgListElement = iterator.next();
+
+ switch (hoconCfgListElement.valueType()) {
+ case OBJECT:
+ case LIST:
+ throw wrongTypeException(boxedComponentType, path, idx);
+
+ default:
+ Array.set(resArray, idx, unwrapPrimitive(hoconCfgListElement, boxedComponentType, path, idx));
+ }
+ }
+
+ return (T)resArray;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void descend(ConstructableTreeNode node) {
+ if (!(node instanceof NamedListNode)) {
+ throw new IllegalArgumentException(
+ format("'%s' configuration is expected to be a composite configuration node, not a list", join(path))
+ );
+ }
+
+ String syntheticKeyName = ((NamedListNode<?>)node).syntheticKeyName();
+
+ int idx = 0;
+ for (Iterator<ConfigValue> iterator = hoconCfgList.iterator(); iterator.hasNext(); idx++) {
+ ConfigValue next = iterator.next();
+
+ 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)
+ )
+ );
+ }
+
+ ConfigObject hoconCfg = (ConfigObject)next;
+
+ ConfigValue keyValue = hoconCfg.get(syntheticKeyName);
+
+ if (keyValue == null || keyValue.valueType() != ConfigValueType.STRING) {
+ throw new IllegalArgumentException(
+ format(
+ "'%s' configuration value is mandatory and must be a String",
+ formatArrayPath(path, idx) + "." + syntheticKeyName
+ )
+ );
+ }
+
+ String key = (String)keyValue.unwrapped();
+
+ List<String> path = appendKey(this.path, syntheticKeyName);
+
+ node.construct(key, new HoconObjectConfigurationSource(syntheticKeyName, path, hoconCfg));
+ }
+ }
+
+ /**
+ * Non-null wrapper over {@link TypeUtils#boxed}.
+ *
+ * @param clazz Class, either primitive or not.
+ * @return Boxed version of passed class.
+ */
+ public static Class<?> box(Class<?> clazz) {
+ Class<?> boxed = TypeUtils.boxed(clazz);
+
+ return boxed == null ? clazz : boxed;
+ }
+}
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
new file mode 100644
index 0000000..ce6f14c
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconObjectConfigurationSource.java
@@ -0,0 +1,127 @@
+/*
+ * 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.hocon;
+
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
+import org.jetbrains.annotations.Nullable;
+
+import static java.lang.String.format;
+import static org.apache.ignite.internal.configuration.hocon.HoconPrimitiveConfigurationSource.wrongTypeException;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.appendKey;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.join;
+
+/**
+ * {@link ConfigurationSource} created from a HOCON object.
+ */
+class HoconObjectConfigurationSource implements ConfigurationSource {
+ /**
+ * Key that needs to be ignored by the source. Can be {@code null}.
+ */
+ private final String ignoredKey;
+
+ /**
+ * Current path inside the top-level HOCON object.
+ */
+ private final List<String> path;
+
+ /**
+ * HOCON object that this source has been created from.
+ */
+ private final ConfigObject hoconCfgObject;
+
+ /**
+ * Creates a {@link ConfigurationSource} from the given HOCON object.
+ *
+ * @param ignoredKey Key that needs to be ignored by the source. Can be {@code null}.
+ * @param path Current path inside the top-level HOCON object. Can be empty if the given {@code hoconCfgObject}
+ * is the top-level object.
+ * @param hoconCfgObject HOCON object.
+ */
+ HoconObjectConfigurationSource(@Nullable String ignoredKey, List<String> path, ConfigObject hoconCfgObject) {
+ this.ignoredKey = ignoredKey;
+ this.path = path;
+ this.hoconCfgObject = hoconCfgObject;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T> T unwrap(Class<T> clazz) {
+ throw wrongTypeException(clazz, path, -1);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void descend(ConstructableTreeNode node) {
+ for (Map.Entry<String, ConfigValue> entry : hoconCfgObject.entrySet()) {
+ String key = entry.getKey();
+
+ if (key.equals(ignoredKey))
+ continue;
+
+ ConfigValue hoconCfgValue = entry.getValue();
+
+ try {
+ switch (hoconCfgValue.valueType()) {
+ case NULL:
+ node.construct(key, null);
+
+ break;
+
+ case OBJECT: {
+ List<String> path = appendKey(this.path, key);
+
+ node.construct(key, new HoconObjectConfigurationSource(null, path, (ConfigObject)hoconCfgValue));
+
+ break;
+ }
+
+ case LIST: {
+ List<String> path = appendKey(this.path, key);
+
+ node.construct(key, new HoconListConfigurationSource(path, (ConfigList)hoconCfgValue));
+
+ break;
+ }
+
+ default: {
+ List<String> path = appendKey(this.path, key);
+
+ node.construct(key, new HoconPrimitiveConfigurationSource(path, hoconCfgValue));
+ }
+ }
+ }
+ 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
+ );
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..6362861
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconPrimitiveConfigurationSource.java
@@ -0,0 +1,199 @@
+/*
+ * 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.hocon;
+
+import java.util.List;
+import com.typesafe.config.ConfigValue;
+import org.apache.ignite.internal.configuration.TypeUtils;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
+
+import static com.typesafe.config.ConfigValueType.BOOLEAN;
+import static com.typesafe.config.ConfigValueType.NUMBER;
+import static com.typesafe.config.ConfigValueType.STRING;
+import static java.lang.String.format;
+import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.join;
+
+/**
+ * {@link ConfigurationSource} created from a HOCON element representing a primitive type.
+ */
+class HoconPrimitiveConfigurationSource implements ConfigurationSource {
+ /**
+ * Current path inside the top-level HOCON object.
+ */
+ private final List<String> path;
+
+ /**
+ * HOCON object that this source has been created from.
+ */
+ private final ConfigValue hoconCfgValue;
+
+ /**
+ * Creates a {@link ConfigurationSource} from the given HOCON object representing a primitive type.
+ *
+ * @param path current path inside the top-level HOCON object. Can be empty if the given {@code hoconCfgValue}
+ * is the top-level object
+ * @param hoconCfgValue HOCON object
+ */
+ HoconPrimitiveConfigurationSource(List<String> path, ConfigValue hoconCfgValue) {
+ assert !path.isEmpty();
+
+ this.path = path;
+ this.hoconCfgValue = hoconCfgValue;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T> T unwrap(Class<T> clazz) {
+ if (clazz.isArray())
+ throw wrongTypeException(clazz, path, -1);
+
+ 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))
+ );
+ }
+
+ /**
+ * Returns exception with the message that a value is expected to be of a specific type.
+ *
+ * @param clazz Expected type of the value.
+ * @param path Path to the value.
+ * @param idx Index in the array if the value is an array element. {@code -1} if it's not.
+ * @return New {@link IllegalArgumentException} instance.
+ */
+ public static IllegalArgumentException wrongTypeException(Class<?> clazz, List<String> path, int idx) {
+ return new IllegalArgumentException(format(
+ "'%s' is expected as a type for the '%s' configuration value",
+ unbox(clazz).getSimpleName(), formatArrayPath(path, idx)
+ ));
+ }
+
+ /**
+ * Non-null wrapper over {@link TypeUtils#unboxed}.
+ *
+ * @param clazz Class for non-primitive objects.
+ * @return Unboxed class fox boxed classes, same object otherwise.
+ */
+ private static Class<?> unbox(Class<?> clazz) {
+ assert !clazz.isPrimitive();
+
+ Class<?> unboxed = TypeUtils.unboxed(clazz);
+
+ return unboxed == null ? clazz : unboxed;
+ }
+
+ /**
+ * Extracts the value from the given {@link ConfigValue} based on the expected type.
+ *
+ * @param hoconCfgValue Configuration value to unwrap.
+ * @param clazz Class that signifies resulting type of the value. Boxed primitive or String.
+ * @param path Path to the value, used for error messages.
+ * @param idx Index in the array if the value is an array element. {@code -1} if it's not.
+ * @param <T> Type of the resulting unwrapped object.
+ * @return Unwrapped object.
+ * @throws IllegalArgumentException In case of type mismatch or numeric overflow.
+ */
+ public static <T> T unwrapPrimitive(ConfigValue hoconCfgValue, Class<T> clazz, List<String> path, int idx) {
+ assert !clazz.isArray();
+ assert !clazz.isPrimitive();
+
+ if (clazz == String.class) {
+ if (hoconCfgValue.valueType() != STRING)
+ throw wrongTypeException(clazz, path, idx);
+
+ return clazz.cast(hoconCfgValue.unwrapped());
+ }
+ else if (clazz == Boolean.class) {
+ if (hoconCfgValue.valueType() != BOOLEAN)
+ throw wrongTypeException(clazz, path, idx);
+
+ return clazz.cast(hoconCfgValue.unwrapped());
+ }
+ else if (clazz == Character.class) {
+ if (hoconCfgValue.valueType() != STRING || hoconCfgValue.unwrapped().toString().length() != 1)
+ throw wrongTypeException(clazz, path, idx);
+
+ return clazz.cast(hoconCfgValue.unwrapped().toString().charAt(0));
+ }
+ else if (Number.class.isAssignableFrom(clazz)) {
+ if (hoconCfgValue.valueType() != NUMBER)
+ throw wrongTypeException(clazz, path, idx);
+
+ Number numberValue = (Number)hoconCfgValue.unwrapped();
+
+ if (clazz == Byte.class) {
+ checkBounds(numberValue, Byte.MIN_VALUE, Byte.MAX_VALUE, path, idx);
+
+ return clazz.cast(numberValue.byteValue());
+ }
+ else if (clazz == Short.class) {
+ checkBounds(numberValue, Short.MIN_VALUE, Short.MAX_VALUE, path, idx);
+
+ return clazz.cast(numberValue.shortValue());
+ }
+ else if (clazz == Integer.class) {
+ checkBounds(numberValue, Integer.MIN_VALUE, Integer.MAX_VALUE, path, idx);
+
+ return clazz.cast(numberValue.intValue());
+ }
+ else if (clazz == Long.class)
+ return clazz.cast(numberValue.longValue());
+ else if (clazz == Float.class)
+ return clazz.cast(numberValue.floatValue());
+ else if (clazz == Double.class)
+ return clazz.cast(numberValue.doubleValue());
+ }
+
+ throw new IllegalArgumentException("Unsupported type: " + clazz);
+ }
+
+ /**
+ * Checks that a number fits into the given bounds.
+ *
+ * @param numberValue Number value to validate.
+ * @param lower Lower bound, inclusive.
+ * @param upper Upper bound, inclusive.
+ * @param path Path to the value, used for error messages.
+ * @param idx Index in the array if the value is an array element. {@code -1} if it's not.
+ */
+ private static void checkBounds(Number numberValue, long lower, long upper, List<String> path, int idx) {
+ long longValue = numberValue.longValue();
+
+ if (longValue < lower || longValue > upper) {
+ throw new IllegalArgumentException(format(
+ "Value '%d' of '%s' is out of its declared bounds: [%d : %d]",
+ longValue, formatArrayPath(path, idx), lower, upper
+ ));
+ }
+ }
+
+ /**
+ * Creates a string representation of the current HOCON path inside of an array.
+ *
+ * @param path Path to the value.
+ * @param idx Index in the array if the value is an array element. {@code -1} if it's not.
+ * @return Path in a proper format.
+ */
+ public static String formatArrayPath(List<String> path, int idx) {
+ return join(path) + (idx == -1 ? "" : ("[" + idx + "]"));
+ }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
index 0cd3874..6a90b15 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
@@ -23,6 +23,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.ignite.configuration.NamedListChange;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
/**
* Configuration node implementation for the collection of named {@link InnerNode}s. Unlike implementations of
@@ -34,6 +35,9 @@
/** Name of a synthetic configuration property that describes the order of elements in a named list. */
public static final String ORDER_IDX = "<idx>";
+ /** Configuration name for the synthetic key. */
+ private final String syntheticKeyName;
+
/** Supplier of new node objects when new list element node has to be created. */
private final Supplier<N> valSupplier;
@@ -43,9 +47,12 @@
/**
* Default constructor.
*
+ * @param syntheticKeyName Name of the synthetic configuration value that will represent keys in a specially ordered
+ * representation syntax.
* @param valSupplier Closure to instantiate values.
*/
- public NamedListNode(Supplier<N> valSupplier) {
+ public NamedListNode(String syntheticKeyName, Supplier<N> valSupplier) {
+ this.syntheticKeyName = syntheticKeyName;
this.valSupplier = valSupplier;
map = new OrderedMap<>();
}
@@ -56,6 +63,7 @@
* @param node Other node.
*/
private NamedListNode(NamedListNode<N> node) {
+ syntheticKeyName = node.syntheticKeyName;
valSupplier = node.valSupplier;
map = new OrderedMap<>(node.map);
}
@@ -76,6 +84,11 @@
}
/** {@inheritDoc} */
+ @Override public N get(int index) throws IndexOutOfBoundsException {
+ return map.get(index);
+ }
+
+ /** {@inheritDoc} */
@Override public int size() {
return map.size();
}
@@ -180,6 +193,15 @@
}
/**
+ * @return Configuration name for the synthetic key.
+ *
+ * @see NamedConfigValue#syntheticKeyName()
+ */
+ public String syntheticKeyName() {
+ return syntheticKeyName;
+ }
+
+ /**
* Deletes named list element.
*
* @param key Element's key.
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
index d67d3f3..024963a 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
@@ -72,6 +72,17 @@
}
/**
+ * Returns value located at the specified index.
+ *
+ * @param index Value index.
+ * @return Requested value.
+ * @throws IndexOutOfBoundsException If index is out of bounds.
+ */
+ public V get(int index) {
+ return map.get(orderedKeys.get(index));
+ }
+
+ /**
* Same as {@link Map#remove(Object)}.
*
* @param key Key to remove.
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
index 580d181..8863d89 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
@@ -23,6 +23,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
/** Test with basic {@link OrderedMap} invariants. */
public class OrderedMapTest {
@@ -38,10 +39,13 @@
assertEquals(1, map.size());
assertEquals("value", map.get("key"));
+ assertEquals("value", map.get(0));
map.remove("key");
assertNull(map.get("key1"));
+
+ assertThrows(IndexOutOfBoundsException.class, () -> map.get(0));
}
/** Tests that {@link OrderedMap#put(String, Object)} preserves order. */