From d70eddeec5dced1821d003f7d6ff28f800608180 Mon Sep 17 00:00:00 2001 From: Vankka Date: Sat, 29 Jun 2024 14:06:17 +0300 Subject: [PATCH] Get field type from its value --- .../FieldValueDiscovererProxy.java | 203 ++++++++++++++++++ .../abstraction/ConfigurateConfigManager.java | 3 +- .../common/config/main/MainConfig.java | 2 +- .../config/main/PresenceUpdaterConfig.java | 2 +- .../config/main/TimedUpdaterConfig.java | 6 +- 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 common/src/main/java/com/discordsrv/common/config/configurate/fielddiscoverer/FieldValueDiscovererProxy.java diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/fielddiscoverer/FieldValueDiscovererProxy.java b/common/src/main/java/com/discordsrv/common/config/configurate/fielddiscoverer/FieldValueDiscovererProxy.java new file mode 100644 index 00000000..86df8b84 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/configurate/fielddiscoverer/FieldValueDiscovererProxy.java @@ -0,0 +1,203 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.configurate.fielddiscoverer; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.FieldDiscoverer; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedFunction; +import org.spongepowered.configurate.util.Types; + +import java.lang.reflect.*; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import static io.leangen.geantyref.GenericTypeReflector.*; + +/* + * https://github.com/SpongePowered/Configurate/blob/c3d2105e0c03a0f6e0ae20ad74dbb4df9a83df36/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java + * Identical except uses the type of the value instead of the fields type. + * + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed 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. + */ +public class FieldValueDiscovererProxy implements FieldDiscoverer> { + + public static final FieldValueDiscovererProxy EMPTY_CONSTRUCTOR_INSTANCE = new FieldValueDiscovererProxy(type -> { + try { + final Constructor constructor; + constructor = erase(type.getType()).getDeclaredConstructor(); + constructor.setAccessible(true); + return () -> { + try { + return constructor.newInstance(); + } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }; + } catch (final NoSuchMethodException e) { + return null; + } + }, "Objects must have a zero-argument constructor to be able to create new instances", false); + + private final CheckedFunction, SerializationException> instanceFactory; + private final String instanceUnavailableErrorMessage; + private final boolean requiresInstanceCreation; + + FieldValueDiscovererProxy( + final CheckedFunction, SerializationException> instanceFactory, + final @Nullable String instanceUnavailableErrorMessage, + final boolean requiresInstanceCreation + ) { + this.instanceFactory = instanceFactory; + if (instanceUnavailableErrorMessage == null) { + this.instanceUnavailableErrorMessage = "Unable to create instances for this type!"; + } else { + this.instanceUnavailableErrorMessage = instanceUnavailableErrorMessage; + } + this.requiresInstanceCreation = requiresInstanceCreation; + } + + @Override + public @Nullable InstanceFactory> discover(final AnnotatedType target, + final FieldCollector, V> collector) throws SerializationException { + final Class clazz = erase(target.getType()); + if (clazz.isInterface()) { + throw new SerializationException(target.getType(), "ObjectMapper can only work with concrete types"); + } + + final @Nullable Supplier maker = this.instanceFactory.apply(target); + if (maker == null && this.requiresInstanceCreation) { + return null; + } + + Object instanceForCollection = maker != null ? maker.get() : null; + + AnnotatedType collectType = target; + Class collectClass = clazz; + while (true) { + collectFields(collectType, collector, instanceForCollection); + collectClass = collectClass.getSuperclass(); + if (collectClass.equals(Object.class)) { + break; + } + collectType = getExactSuperType(collectType, collectClass); + } + + return new MutableInstanceFactory>() { + + @Override + public Map begin() { + return new HashMap<>(); + } + + @Override + public void complete(final Object instance, final Map intermediate) throws SerializationException { + for (final Map.Entry entry : intermediate.entrySet()) { + try { + // Handle implicit field initialization by detecting any existing information in the object + if (entry.getValue() instanceof ImplicitProvider) { + final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get(); + if (implicit != null) { + if (entry.getKey().get(instance) == null) { + entry.getKey().set(instance, implicit); + } + } + } else { + entry.getKey().set(instance, entry.getValue()); + } + } catch (final IllegalAccessException e) { + throw new SerializationException(target.getType(), e); + } + } + } + + @Override + public Object complete(final Map intermediate) throws SerializationException { + final Object instance = maker == null ? null : maker.get(); + if (instance == null) { + throw new SerializationException(target.getType(), instanceUnavailableErrorMessage); + } + complete(instance, intermediate); + return instance; + } + + @Override + public boolean canCreateInstances() { + return maker != null; + } + + }; + } + + private void collectFields(final AnnotatedType clazz, final FieldCollector, ?> fieldMaker, Object instanceForCollection) { + for (final Field field : erase(clazz.getType()).getDeclaredFields()) { + if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) { + continue; + } + + field.setAccessible(true); + + AnnotatedType fieldType = getFieldType(field, clazz); + try { + if (instanceForCollection != null) { + Object instance = field.get(instanceForCollection); + Class type = instance != null ? instance.getClass() : null; + if (type != null && type.isAnnotationPresent(ConfigSerializable.class)) { + fieldType = annotate(type); + } + } + } catch (IllegalAccessException ignored) {} + + fieldMaker.accept(field.getName(), fieldType, Types.combinedAnnotations(fieldType, field), + (intermediate, val, implicitProvider) -> { + if (val != null) { + intermediate.put(field, val); + } else { + intermediate.put(field, new ImplicitProvider(implicitProvider)); + } + }, field::get); + } + } + + static class ImplicitProvider { + + final Supplier provider; + + ImplicitProvider(final Supplier provider) { + this.provider = provider; + } + + } +} + diff --git a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java index 07dad746..c43bd9e4 100644 --- a/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java +++ b/common/src/main/java/com/discordsrv/common/config/configurate/manager/abstraction/ConfigurateConfigManager.java @@ -25,6 +25,7 @@ import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.configurate.annotation.Constants; import com.discordsrv.common.config.configurate.annotation.DefaultOnly; import com.discordsrv.common.config.configurate.annotation.Order; +import com.discordsrv.common.config.configurate.fielddiscoverer.FieldValueDiscovererProxy; import com.discordsrv.common.config.configurate.fielddiscoverer.OrderedFieldDiscovererProxy; import com.discordsrv.common.config.configurate.manager.loader.ConfigLoaderProvider; import com.discordsrv.common.config.configurate.serializer.*; @@ -209,7 +210,7 @@ public abstract class ConfigurateConfigManager((FieldDiscoverer) FieldDiscoverer.emptyConstructorObject(), fieldOrder)) + .addDiscoverer(new OrderedFieldDiscovererProxy<>((FieldDiscoverer) (Object) FieldValueDiscovererProxy.EMPTY_CONSTRUCTOR_INSTANCE, fieldOrder)) .addDiscoverer(new OrderedFieldDiscovererProxy<>((FieldDiscoverer) FieldDiscoverer.record(), fieldOrder)) .addProcessor(Constants.Comment.class, (data, fieldType) -> (value, destination) -> { // This needs to go before comment processing. diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index 55c6f3a6..f06668b5 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -84,7 +84,7 @@ public abstract class MainConfig implements Config { public PresenceUpdaterConfig presenceUpdater = defaultPresenceUpdater(); protected PresenceUpdaterConfig defaultPresenceUpdater() { - return new PresenceUpdaterConfig(); // TODO: fix overriding this not effecting the final config + return new PresenceUpdaterConfig(); } public TimedUpdaterConfig timedUpdater = new TimedUpdaterConfig(); diff --git a/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java b/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java index a9a16073..da7f56a7 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java @@ -34,7 +34,7 @@ import java.util.Locale; public class PresenceUpdaterConfig { @Comment("The amount of seconds between presence updates\n" - + "Minimum value: %1s") + + "Minimum value: %1") @Constants.Comment("30") public int updaterRateInSeconds = 90; diff --git a/common/src/main/java/com/discordsrv/common/config/main/TimedUpdaterConfig.java b/common/src/main/java/com/discordsrv/common/config/main/TimedUpdaterConfig.java index f0d3fd51..b1b88642 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/TimedUpdaterConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/TimedUpdaterConfig.java @@ -59,7 +59,8 @@ public class TimedUpdaterConfig { @Comment("The format for the channel name(s), placeholders are supported.") public String nameFormat = ""; - @Comment("The time between updates in minutes. The minimum time is %1 minutes.") + @Comment("The time between updates in minutes.\n" + + "Minimum value: %1") @Constants.Comment(intValue = MINIMUM_MINUTES) public int timeMinutes = MINIMUM_MINUTES; @@ -95,7 +96,8 @@ public class TimedUpdaterConfig { + "If this is blank, the topic will not be updated") public String topicFormat = ""; - @Comment("The time between updates in minutes. The minimum time is %1 minutes.") + @Comment("The time between updates in minutes.\n" + + "Minimum value: %1") @Constants.Comment(intValue = MINIMUM_MINUTES) public int timeMinutes = MINIMUM_MINUTES;