Enabling the usage of records for configuration files.

Signed-off-by: gael@lhopital.org <gael@lhopital.org>
pull/4508/head
gael@lhopital.org 2024-12-20 17:28:27 +01:00
parent e078a5cd98
commit fdba11ebd9
2 changed files with 88 additions and 40 deletions

View File

@ -12,6 +12,7 @@
*/
package org.openhab.core.config.core;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
@ -21,6 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -77,67 +79,82 @@ public final class ConfigParser {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T> @Nullable T configurationAs(Map<String, @Nullable Object> properties,
Class<T> configurationClass) {
T configuration;
Constructor<T> constructor;
T configuration = null;
try {
configuration = configurationClass.getConstructor().newInstance();
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
if (!configurationClass.isRecord()) {
constructor = configurationClass.getConstructor();
configuration = constructor.newInstance();
} else {
Constructor<?>[] constructors = configurationClass.getConstructors();
constructor = (Constructor<T>) constructors[0];
}
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException
| InvocationTargetException e) {
return null;
}
List<Field> fields = getAllFields(configurationClass);
for (Field field : fields) {
// Don't try to write to final fields and ignore transient fields
if (Modifier.isFinal(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
Map<Field, Object> initArgs = new LinkedHashMap<>();
for (Field field : getAllFields(configurationClass)) {
// Don't try to write to final fields and ignore transient fields when it's a class
if (!configurationClass.isRecord()
&& (Modifier.isFinal(field.getModifiers()) || Modifier.isTransient(field.getModifiers()))) {
continue;
}
String fieldName = field.getName();
Class<?> type = field.getType();
String fieldName = field.getName();
Object value = properties.get(fieldName);
// Consider RequiredField annotations
if (value == null) {
LOGGER.trace("Skipping field '{}', because config has no entry for {}", fieldName, fieldName);
LOGGER.trace("Skipping field '{}', because config has no entry for it", fieldName);
continue;
}
Class<?> type = field.getType();
// Allows to have List<int>, List<Double>, List<String> etc (and the corresponding Set<?>)
if (value instanceof Collection collection1) {
Class<?> innerClass = (Class<?>) ((ParameterizedType) field.getGenericType())
.getActualTypeArguments()[0];
Collection collection;
if (List.class.isAssignableFrom(type)) {
collection = new ArrayList<>();
} else if (Set.class.isAssignableFrom(type)) {
collection = new HashSet<>();
if (value instanceof Collection valueCollection) {
Collection collection = List.class.isAssignableFrom(type) ? new ArrayList<>()
: Set.class.isAssignableFrom(type) ? new HashSet<>() : //
null;
if (collection != null) {
Class<?> innerClass = (Class<?>) ((ParameterizedType) field.getGenericType())
.getActualTypeArguments()[0];
valueCollection.stream().map(it -> valueAs(it, innerClass)).filter(Object.class::isInstance)
.forEach(collection::add);
initArgs.put(field, collection);
} else {
LOGGER.warn("Skipping field '{}', only List and Set is supported as target Collection", fieldName);
continue;
}
for (final Object it : collection1) {
final Object normalized = valueAs(it, innerClass);
if (normalized == null) {
continue;
}
collection.add(normalized);
}
value = collection;
}
try {
} else {
value = valueAs(value, type);
if (value == null) {
if (value != null) {
initArgs.put(field, value);
} else {
LOGGER.warn(
"Could not set value for field '{}' because conversion failed. Check your configuration value.",
fieldName);
continue;
}
LOGGER.trace("Setting value ({}) {} to field '{}' in configuration class {}", type.getSimpleName(),
value, fieldName, configurationClass.getName());
field.setAccessible(true);
field.set(configuration, value);
} catch (SecurityException | IllegalArgumentException | IllegalAccessException ex) {
LOGGER.warn("Could not set field value for field '{}': {}", fieldName, ex.getMessage(), ex);
}
}
if (configuration instanceof T localConfiguration) {
initArgs.forEach((field, value) -> {
try {
LOGGER.trace("Setting value ({}) {} to field '{}' in configuration class {}",
field.getType().getSimpleName(), value, field.getName(), configurationClass.getName());
field.setAccessible(true);
field.set(localConfiguration, value);
} catch (IllegalAccessException e) {
LOGGER.warn("Could not set field value for field '{}': {}", field.getName(), e.getMessage(), e);
}
});
} else {
try {
configuration = constructor.newInstance(initArgs.values().toArray());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
LOGGER.warn("Could invoke default record constructor '{}'", e.getMessage(), e);
}
}
return configuration;
@ -224,7 +241,11 @@ public final class ConfigParser {
result = Boolean.valueOf(strValue);
} else if (type.isEnum()) {
final Class<? extends Enum> enumType = (Class<? extends Enum>) typeClass;
result = Enum.valueOf(enumType, value.toString());
try {
result = Enum.valueOf(enumType, value.toString());
} catch (IllegalArgumentException e) {
result = null;
}
} else if (Set.class.isAssignableFrom(typeClass)) {
result = Set.of(value);
} else if (Collection.class.isAssignableFrom(typeClass)) {

View File

@ -58,6 +58,12 @@ public class ConfigurationTest {
public @NonNullByDefault({}) String listField;
}
public static record ConfigRecord(int intField, String stringField, boolean booleanField, List<String> listField,
Set<String> setField, org.openhab.core.config.core.ConfigurationTest.ConfigClass.MyEnum enumField) {
@SuppressWarnings("unused")
private static final String CONSTANT = "SOME_CONSTANT";
}
@Test
public void assertGetConfigAsWorks() {
Configuration configuration = new Configuration();
@ -79,6 +85,27 @@ public class ConfigurationTest {
assertThat(configClass.setField, is(hasItems("one", "two", "three")));
}
@Test
public void assertGetConfigAsWorksForRecord() {
Configuration configuration = new Configuration();
configuration.put("intField", 1);
configuration.put("booleanField", false);
configuration.put("stringField", "test");
configuration.put("enumField", "ON");
configuration.put("listField", List.of("one", "two", "three"));
configuration.put("setField", List.of("one", "two", "three"));
configuration.put("notExisitingProperty", true);
ConfigRecord configClass = configuration.as(ConfigRecord.class);
assertThat(configClass.intField, is(equalTo(1)));
assertThat(configClass.booleanField, is(false));
assertThat(configClass.stringField, is("test"));
assertThat(configClass.enumField, is(ConfigClass.MyEnum.ON));
assertThat(configClass.listField, is(hasItems("one", "two", "three")));
assertThat(configClass.setField, is(hasItems("one", "two", "three")));
}
@Test
public void assertGetConfigAsWorksWithSuperclass() {
Configuration configuration = new Configuration();