blob: fe942791f75e8a846a9639caac67f4ff9074d01a [file] [log] [blame]
/*
* 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.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.google.gson.JsonElement;
import com.google.gson.JsonNull;
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.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import static com.google.gson.JsonParser.parseString;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.ignite.internal.configuration.json.JsonConverter.jsonSource;
import static org.apache.ignite.internal.configuration.json.JsonConverter.jsonVisitor;
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 JsonConverter}.
*/
public class JsonConverterTest {
/** */
@ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
public static class JsonRootConfigurationSchema {
/** */
@NamedConfigValue
public JsonArraysConfigurationSchema arraysList;
/** */
@NamedConfigValue
public JsonPrimitivesConfigurationSchema primitivesList;
}
/**
* Configuration schema for testing the support of arrays of primitives.
*/
@Config
public static class JsonArraysConfigurationSchema {
/** */
@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 JsonPrimitivesConfigurationSchema {
/** */
@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 ConfigurationRegistry registry;
/** */
private JsonRootConfiguration configuration;
/** */
@BeforeEach
public void before() {
registry = new ConfigurationRegistry(
Collections.singletonList(JsonRootConfiguration.KEY),
Collections.emptyMap(),
Collections.singletonList(new TestConfigurationStorage(ConfigurationType.LOCAL))
);
registry.start();
configuration = registry.getConfiguration(JsonRootConfiguration.KEY);
}
/** */
@AfterEach
public void after() {
registry.stop();
}
/** */
@Test
public void toJsonBasic() {
assertEquals(parseString("{'root':{'arraysList':{},'primitivesList':{}}}"), getJsonValue(List.of()));
assertEquals(parseString("{'arraysList':{},'primitivesList':{}}"), getJsonValue(List.of("root")));
assertEquals(parseString("{}"), getJsonValue(List.of("root", "arraysList")));
assertThrowsIllegalArgException(
() -> registry.represent(List.of("doot"), jsonVisitor()),
"Configuration 'doot' is not found"
);
assertThrowsIllegalArgException(
() -> registry.represent(List.of("root", "x"), jsonVisitor()),
"Configuration 'root.x' is not found"
);
assertEquals(JsonNull.INSTANCE, getJsonValue(List.of("root", "primitivesList", "foo")));
}
/**
* Tests that the {@code JsonConverter} supports serialization of Strings and primitives.
*/
@Test
public void testJsonPrimitivesSerialization() throws Exception {
configuration.change(cfg -> cfg
.changePrimitivesList(primitivesList -> primitivesList
.create("name", primitives -> {})
)
).get(1, SECONDS);
var basePath = List.of("root", "primitivesList", "name");
assertEquals(
parseString("{'booleanVal':false,'byteVal':0,'shortVal':0,'intVal':0,'longVal':0,'charVal':\u0000,'floatVal':0.0,'doubleVal':0.0,'stringVal':''}"),
getJsonValue(basePath)
);
assertEquals(parseString("false"), getJsonValue(basePath, "booleanVal"));
assertEquals(parseString("0"), getJsonValue(basePath, "byteVal"));
assertEquals(parseString("0"), getJsonValue(basePath, "shortVal"));
assertEquals(parseString("0"), getJsonValue(basePath, "intVal"));
assertEquals(parseString("0"), getJsonValue(basePath, "longVal"));
assertEquals(parseString("\u0000"), getJsonValue(basePath, "charVal"));
assertEquals(parseString("0.0"), getJsonValue(basePath, "floatVal"));
assertEquals(parseString("0.0"), getJsonValue(basePath, "doubleVal"));
assertEquals(parseString("''"), getJsonValue(basePath, "stringVal"));
}
/**
* Tests that the {@code JsonConverter} supports serialization of arrays of Strings and primitives.
*/
@Test
public void testJsonArraysSerialization() throws Exception {
configuration.change(cfg -> cfg
.changeArraysList(arraysList -> arraysList
.create("name", arrays -> {})
)
).get(1, SECONDS);
var basePath = List.of("root", "arraysList", "name");
assertEquals(
parseString("{'booleans':[false],'bytes':[0],'shorts':[0],'ints':[0],'longs':[0],'chars':[\u0000],'floats':[0.0],'doubles':[0.0],'strings':['']}"),
getJsonValue(basePath)
);
assertEquals(parseString("[false]"), getJsonValue(basePath, "booleans"));
assertEquals(parseString("[0]"), getJsonValue(basePath, "bytes"));
assertEquals(parseString("[0]"), getJsonValue(basePath, "shorts"));
assertEquals(parseString("[0]"), getJsonValue(basePath, "ints"));
assertEquals(parseString("[0]"), getJsonValue(basePath, "longs"));
assertEquals(parseString("[\u0000]"), getJsonValue(basePath, "chars"));
assertEquals(parseString("[0.0]"), getJsonValue(basePath, "floats"));
assertEquals(parseString("[0.0]"), getJsonValue(basePath, "doubles"));
assertEquals(parseString("['']"), getJsonValue(basePath, "strings"));
}
/**
* Retrieves the JSON element located at the given path.
*/
private JsonElement getJsonValue(List<String> basePath, String... path) {
List<String> fullPath = Stream.concat(basePath.stream(), Arrays.stream(path)).collect(Collectors.toList());
return registry.represent(fullPath, jsonVisitor());
}
/** */
@Test
public void fromJsonBasic() {
// 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"
);
}
/**
* Tests that the {@code JsonConverter} supports deserialization of Strings and primitives.
*/
@Test
public void testJsonPrimitivesDeserialization() throws Throwable {
change("{'root':{'primitivesList':{'name' : {}}}}");
JsonPrimitivesConfiguration 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 testInvalidJsonPrimitivesDeserialization() {
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 JsonConverter} supports deserialization of arrays of Strings and primitives.
*/
@Test
public void testJsonArraysDeserialization() throws Throwable {
change("{'root':{'arraysList':{'name' : {}}}}");
JsonArraysConfiguration 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 testInvalidJsonArraysDeserialization() {
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 JSON string.
*/
private void change(String json) throws Throwable {
try {
registry.change(jsonSource(parseString(json)), 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));
}
}