Forráskód Böngészése

[WIP] config manager restructure

Lortseam 4 éve
szülő
commit
2aaea3eb0c

+ 24 - 0
src/main/java/me/lortseam/completeconfig/Config.java

@@ -0,0 +1,24 @@
+package me.lortseam.completeconfig;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import me.lortseam.completeconfig.api.ConfigCategory;
+import me.lortseam.completeconfig.collection.CollectionMap;
+import me.lortseam.completeconfig.serialization.CollectionMapDeserializer;
+
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+public class Config extends CollectionMap {
+
+    private final JsonElement json;
+
+    void registerTopLevelCategory(ConfigCategory category) {
+        fill(category);
+        new GsonBuilder()
+                .registerTypeAdapter(CollectionMapDeserializer.TYPE, new CollectionMapDeserializer(this, category.getConfigCategoryID()))
+                .create()
+                .fromJson(json, CollectionMapDeserializer.TYPE);
+    }
+
+}

+ 10 - 210
src/main/java/me/lortseam/completeconfig/ConfigManager.java

@@ -1,6 +1,5 @@
 package me.lortseam.completeconfig;
 
-import com.google.common.base.CaseFormat;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonElement;
@@ -8,19 +7,11 @@ import com.google.gson.JsonNull;
 import lombok.AccessLevel;
 import lombok.Getter;
 import me.lortseam.completeconfig.api.ConfigCategory;
-import me.lortseam.completeconfig.api.ConfigEntry;
 import me.lortseam.completeconfig.api.ConfigEntryContainer;
-import me.lortseam.completeconfig.api.ConfigEntryListener;
 import me.lortseam.completeconfig.collection.Collection;
 import me.lortseam.completeconfig.entry.Entry;
-import me.lortseam.completeconfig.exception.IllegalModifierException;
 import me.lortseam.completeconfig.gui.GuiRegistry;
-import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
-import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
-import me.lortseam.completeconfig.exception.IllegalReturnValueException;
-import me.lortseam.completeconfig.listener.Listener;
 import me.lortseam.completeconfig.serialization.CollectionSerializer;
-import me.lortseam.completeconfig.serialization.CollectionsDeserializer;
 import me.lortseam.completeconfig.serialization.EntrySerializer;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.api.ConfigBuilder;
@@ -32,11 +23,9 @@ import net.minecraft.client.resource.language.I18n;
 import net.minecraft.text.Text;
 import net.minecraft.text.TranslatableText;
 import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
 
 import java.io.*;
 import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -46,20 +35,22 @@ import java.util.stream.Collectors;
 
 public class ConfigManager {
 
+    private static String joinIDs(String... ids) {
+        return String.join(".", ids);
+    }
+
     @Getter(AccessLevel.PACKAGE)
     private final String modID;
     private final Path jsonPath;
-    private final LinkedHashMap<String, Collection> config = new LinkedHashMap<>();
-    private final JsonElement json;
-    private final Set<Listener> pendingListeners = new HashSet<>();
+    private final Config config;
     @Getter
     private final GuiRegistry guiRegistry = new GuiRegistry();
     private Supplier<ConfigBuilder> guiBuilder = ConfigBuilder::create;
 
     ConfigManager(String modID) {
         this.modID = modID;
-        jsonPath = Paths.get(FabricLoader.getInstance().getConfigDirectory().toPath().toString(), modID + ".json");
-        json = load();
+        jsonPath = Paths.get(FabricLoader.getInstance().getConfigDir().toString(), modID + ".json");
+        config = new Config(load());
     }
 
     private JsonElement load() {
@@ -73,128 +64,6 @@ public class ConfigManager {
         return JsonNull.INSTANCE;
     }
 
-    private LinkedHashMap<String, Entry> getContainerEntries(ConfigEntryContainer container) {
-        LinkedHashMap<String, Entry> entries = new LinkedHashMap<>();
-        Class clazz = container.getClass();
-        while (clazz != null) {
-            List<Listener> listeners = new ArrayList<>();
-            Iterator<Listener> iter = pendingListeners.iterator();
-            while (iter.hasNext()) {
-                Listener listener = iter.next();
-                if (listener.getFieldClass() == clazz) {
-                    listeners.add(listener);
-                    iter.remove();
-                }
-            }
-            Arrays.stream(clazz.getDeclaredMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()) && method.isAnnotationPresent(ConfigEntryListener.class)).forEach(method -> {
-                ConfigEntryListener listener = method.getDeclaredAnnotation(ConfigEntryListener.class);
-                String fieldName = listener.value();
-                if (fieldName.equals("")) {
-                    if (!method.getName().startsWith("set") || method.getName().equals("set")) {
-                        throw new IllegalAnnotationParameterException("Could not detect field name for listener method " + method + ", please provide it inside the annotation");
-                    }
-                    fieldName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, method.getName().replaceFirst("set", ""));
-                }
-                Class<? extends ConfigEntryContainer> fieldClass = listener.container();
-                if (fieldClass == ConfigEntryContainer.class) {
-                    listeners.add(new Listener(method, container, fieldName));
-                } else {
-                    Map<String, Entry> fieldClassEntries = findEntries(config, fieldClass);
-                    if (fieldClassEntries.isEmpty()) {
-                        pendingListeners.add(new Listener(method, container, fieldName, fieldClass));
-                    } else {
-                        Entry entry = fieldClassEntries.get(fieldName);
-                        if (entry == null) {
-                            throw new IllegalAnnotationParameterException("Could not find field " + fieldName + " in " + fieldClass + " requested by listener method " + method);
-                        }
-                        addListenerToEntry(entry, method, container);
-                    }
-                }
-            });
-            LinkedHashMap<String, Entry> clazzEntries = new LinkedHashMap<>();
-            Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
-                if (Modifier.isStatic(field.getModifiers())) {
-                    return false;
-                }
-                if (container.isConfigPOJO()) {
-                    return !ConfigEntryContainer.class.isAssignableFrom(field.getType()) && !field.isAnnotationPresent(ConfigEntry.Ignore.class);
-                }
-                return field.isAnnotationPresent(ConfigEntry.class);
-            }).forEach(field -> {
-                if (Modifier.isFinal(field.getModifiers())) {
-                    throw new IllegalModifierException("Entry field " + field + " must not be final");
-                }
-                if (!field.isAccessible()) {
-                    field.setAccessible(true);
-                }
-                Entry.Builder builder = Entry.Builder.create(field, container);
-                if (field.isAnnotationPresent(ConfigEntry.class)) {
-                    ConfigEntry entryAnnotation = field.getDeclaredAnnotation(ConfigEntry.class);
-                    String customTranslationKey = entryAnnotation.customTranslationKey();
-                    if (!StringUtils.isBlank(customTranslationKey)) {
-                        builder.setCustomTranslationKey(customTranslationKey);
-                    }
-                    String[] customTooltipKeys = entryAnnotation.customTooltipKeys();
-                    if (customTooltipKeys.length > 0) {
-                        for (String key : customTooltipKeys) {
-                            if (StringUtils.isBlank(key)) {
-                                throw new IllegalAnnotationParameterException("Tooltip key(s) of entry field " + field + " must not be blank");
-                            }
-                        }
-                        builder.setCustomTooltipKeys(customTooltipKeys);
-                    }
-                    builder.setForceUpdate(entryAnnotation.forceUpdate());
-                }
-                if (field.isAnnotationPresent(ConfigEntry.Bounded.Integer.class)) {
-                    if (field.getType() != int.class && field.getType() != Integer.class) {
-                        throw new IllegalAnnotationTargetException("Cannot apply Integer bound to non Integer field " + field);
-                    }
-                    ConfigEntry.Bounded.Integer bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Integer.class);
-                    builder.setBounds(bounds.min(), bounds.max());
-                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Long.class)) {
-                    if (field.getType() != long.class && field.getType() != Long.class) {
-                        throw new IllegalAnnotationTargetException("Cannot apply Long bound to non Long field " + field);
-                    }
-                    ConfigEntry.Bounded.Long bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Long.class);
-                    builder.setBounds(bounds.min(), bounds.max());
-                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Float.class)) {
-                    if (field.getType() != float.class && field.getType() != Float.class) {
-                        throw new IllegalAnnotationTargetException("Cannot apply Float bound to non Float field " + field);
-                    }
-                    ConfigEntry.Bounded.Float bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Float.class);
-                    builder.setBounds(bounds.min(), bounds.max());
-                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Double.class)) {
-                    if (field.getType() != double.class && field.getType() != Double.class) {
-                        throw new IllegalAnnotationTargetException("Cannot apply Double bound to non Double field " + field);
-                    }
-                    ConfigEntry.Bounded.Double bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Double.class);
-                    builder.setBounds(bounds.min(), bounds.max());
-                }
-                Entry<?> entry = builder.build();
-                if (guiRegistry.getProvider(entry) == null) {
-                    throw new UnsupportedOperationException("Could not find gui provider for field type " + entry.getType());
-                }
-                String fieldName = field.getName();
-                listeners.removeIf(listener -> {
-                    if (!listener.getFieldName().equals(fieldName)) {
-                        return false;
-                    }
-                    addListenerToEntry(entry, listener.getMethod(), listener.getParentObject());
-                    return true;
-                });
-                clazzEntries.put(fieldName, entry);
-            });
-            if (!listeners.isEmpty()) {
-                Listener listener = listeners.iterator().next();
-                throw new IllegalAnnotationParameterException("Could not find field " + listener.getFieldName() + " in " + clazz + " requested by listener method " + listener.getMethod());
-            }
-            clazzEntries.putAll(entries);
-            entries = clazzEntries;
-            clazz = clazz.getSuperclass();
-        }
-        return entries;
-    }
-
     //TODO: Create own class for validations like these
     private void addListenerToEntry(Entry<?> entry, Method method, ConfigEntryContainer container) {
         //TODO: Add void return type check
@@ -208,6 +77,7 @@ public class ConfigManager {
         entry.addListener(method, container);
     }
 
+    //TODO: Löschen
     private Map<String, Entry> findEntries(LinkedHashMap<String, Collection> collections, Class<? extends ConfigEntryContainer> parentClass) {
         Map<String, Entry> entries = new HashMap<>();
         for (Collection collection : collections.values()) {
@@ -219,81 +89,10 @@ public class ConfigManager {
 
     public void register(ConfigCategory... categories) {
         for (ConfigCategory category : categories) {
-            registerCategory(config, category, true);
-        }
-    }
-
-    private void registerCategory(LinkedHashMap<String, Collection> configMap, ConfigCategory category, boolean applyJson) {
-        String categoryID = category.getConfigCategoryID();
-        if (StringUtils.isBlank(categoryID)) {
-            throw new IllegalReturnValueException("Category ID of " + category.getClass() + " must not be null or blank");
-        }
-        if (configMap.containsKey(categoryID)) {
-            throw new IllegalStateException("Duplicate category ID found: " + categoryID);
-        }
-        Collection collection = new Collection();
-        configMap.put(categoryID, collection);
-        registerContainer(collection, category);
-        if (collection.getEntries().isEmpty() && collection.getCollections().isEmpty()) {
-            configMap.remove(categoryID);
-            return;
-        }
-        if (applyJson) {
-            new GsonBuilder()
-                    .registerTypeAdapter(CollectionsDeserializer.TYPE, new CollectionsDeserializer(configMap, categoryID))
-                    .create()
-                    .fromJson(json, CollectionsDeserializer.TYPE);
+            config.registerTopLevelCategory(category);
         }
     }
 
-    private void registerContainer(Collection collection, ConfigEntryContainer container) {
-        if (!findEntries(config, container.getClass()).isEmpty()) {
-            throw new UnsupportedOperationException("An instance of " + container.getClass() + " is already registered");
-        }
-        collection.getEntries().putAll(getContainerEntries(container));
-        List<ConfigEntryContainer> containers = new ArrayList<>();
-        Class clazz = container.getClass();
-        while (clazz != null) {
-            containers.addAll(Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
-                if (Modifier.isStatic(field.getModifiers())) {
-                    return false;
-                }
-                if (container.isConfigPOJO()) {
-                    return ConfigEntryContainer.class.isAssignableFrom(field.getType());
-                }
-                if (field.isAnnotationPresent(ConfigEntryContainer.Transitive.class)) {
-                    if (!ConfigEntryContainer.class.isAssignableFrom(field.getType())) {
-                        throw new IllegalAnnotationTargetException("Transitive entry " + field + " must implement ConfigEntryContainer");
-                    }
-                    return true;
-                }
-                return false;
-            }).map(field -> {
-                if (!field.isAccessible()) {
-                    field.setAccessible(true);
-                }
-                try {
-                    return (ConfigEntryContainer) field.get(container);
-                } catch (IllegalAccessException e) {
-                    throw new RuntimeException(e);
-                }
-            }).collect(Collectors.toList()));
-            clazz = clazz.getSuperclass();
-        }
-        containers.addAll(Arrays.asList(container.getTransitiveConfigEntryContainers()));
-        for (ConfigEntryContainer c : containers) {
-            if (c instanceof ConfigCategory) {
-                registerCategory(collection.getCollections(), (ConfigCategory) c, false);
-            } else {
-                registerContainer(collection, c);
-                collection.getEntries().putAll(getContainerEntries(c));
-            }
-        }
-    }
-
-    private String joinIDs(String... ids) {
-        return String.join(".", ids);
-    }
 
     private String buildTranslationKey(String... ids) {
         return joinIDs("config", modID, joinIDs(ids));
@@ -303,6 +102,7 @@ public class ConfigManager {
         this.guiBuilder = guiBuilder;
     }
 
+    //TODO: Rename to buildScreen(Screen parent)
     public Screen getConfigScreen(Screen parentScreen) {
         ConfigBuilder builder = guiBuilder.get();
         builder.setParentScreen(parentScreen)

+ 7 - 0
src/main/java/me/lortseam/completeconfig/ConfigMap.java

@@ -0,0 +1,7 @@
+package me.lortseam.completeconfig;
+
+import java.util.LinkedHashMap;
+
+public abstract class ConfigMap<T> extends LinkedHashMap<String, T> {
+
+}

+ 18 - 0
src/main/java/me/lortseam/completeconfig/api/ConfigEntryContainer.java

@@ -1,9 +1,13 @@
 package me.lortseam.completeconfig.api;
 
+import com.google.common.collect.ImmutableList;
+
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
 
 public interface ConfigEntryContainer {
 
@@ -20,5 +24,19 @@ public interface ConfigEntryContainer {
     @interface Transitive {
 
     }
+
+    default List<Class<? extends ConfigEntryContainer>> getClasses() {
+        List<Class<? extends ConfigEntryContainer>> classes = new ArrayList<>();
+        Class<? extends ConfigEntryContainer> clazz = getClass();
+        while (clazz != null) {
+            classes.add(clazz);
+            //TODO: Testen: Entries aus Superklassen die nicht ConfigEntryContainer implementieren sollten nun nicht mehr beachtet werden
+            if (!ConfigEntryContainer.class.isAssignableFrom(clazz.getSuperclass())) {
+                break;
+            }
+            clazz = (Class<? extends ConfigEntryContainer>) clazz.getSuperclass();
+        }
+        return ImmutableList.copyOf(classes);
+    }
     
 }

+ 53 - 8
src/main/java/me/lortseam/completeconfig/collection/Collection.java

@@ -1,21 +1,66 @@
 package me.lortseam.completeconfig.collection;
 
-import lombok.AllArgsConstructor;
 import lombok.Getter;
-import me.lortseam.completeconfig.entry.Entry;
+import me.lortseam.completeconfig.api.ConfigCategory;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
 
-import java.util.LinkedHashMap;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.stream.Collectors;
 
-@AllArgsConstructor
 public class Collection {
 
     @Getter
-    private final LinkedHashMap<String, Entry> entries;
+    private final EntryMap entries = new EntryMap();
     @Getter
-    private final LinkedHashMap<String, Collection> collections;
+    private final CollectionMap collections = new CollectionMap();
 
-    public Collection() {
-        this(new LinkedHashMap<>(), new LinkedHashMap<>());
+    public Collection(ConfigEntryContainer container) {
+        fill(container);
+    }
+
+    private void fill(ConfigEntryContainer container) {
+        //TODO
+        /*if (!findEntries(config, container.getClass()).isEmpty()) {
+            throw new UnsupportedOperationException("An instance of " + container.getClass() + " is already registered");
+        }*/
+        entries.fill(container);
+        List<ConfigEntryContainer> containers = new ArrayList<>();
+        for (Class<? extends ConfigEntryContainer> clazz : container.getClasses()) {
+            containers.addAll(Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
+                if (Modifier.isStatic(field.getModifiers())) {
+                    return false;
+                }
+                if (container.isConfigPOJO()) {
+                    return ConfigEntryContainer.class.isAssignableFrom(field.getType());
+                }
+                if (field.isAnnotationPresent(ConfigEntryContainer.Transitive.class)) {
+                    if (!ConfigEntryContainer.class.isAssignableFrom(field.getType())) {
+                        throw new IllegalAnnotationTargetException("Transitive entry " + field + " must implement ConfigEntryContainer");
+                    }
+                    return true;
+                }
+                return false;
+            }).map(field -> {
+                if (!field.isAccessible()) {
+                    field.setAccessible(true);
+                }
+                try {
+                    return (ConfigEntryContainer) field.get(container);
+                } catch (IllegalAccessException e) {
+                    throw new RuntimeException(e);
+                }
+            }).collect(Collectors.toList()));
+        }
+        containers.addAll(Arrays.asList(container.getTransitiveConfigEntryContainers()));
+        for (ConfigEntryContainer c : containers) {
+            if (c instanceof ConfigCategory) {
+                collections.fill((ConfigCategory) c);
+            } else {
+                fill(c);
+            }
+        }
     }
 
 }

+ 27 - 0
src/main/java/me/lortseam/completeconfig/collection/CollectionMap.java

@@ -0,0 +1,27 @@
+package me.lortseam.completeconfig.collection;
+
+import me.lortseam.completeconfig.ConfigMap;
+import me.lortseam.completeconfig.api.ConfigCategory;
+import me.lortseam.completeconfig.exception.IllegalReturnValueException;
+import org.apache.commons.lang3.StringUtils;
+
+public class CollectionMap extends ConfigMap<Collection> {
+
+    protected void fill(ConfigCategory category) {
+        String categoryID = category.getConfigCategoryID();
+        if (StringUtils.isBlank(categoryID)) {
+            throw new IllegalReturnValueException("Category ID of " + category.getClass() + " must not be null or blank");
+        }
+        if (containsKey(categoryID)) {
+            throw new IllegalStateException("Duplicate category ID found: " + categoryID);
+        }
+        Collection collection = new Collection(category);
+        if (collection.getEntries().isEmpty() && collection.getCollections().isEmpty()) {
+            //TODO
+            //LOGGER.warn()
+            return;
+        }
+        put(categoryID, collection);
+    }
+
+}

+ 140 - 0
src/main/java/me/lortseam/completeconfig/collection/EntryMap.java

@@ -0,0 +1,140 @@
+package me.lortseam.completeconfig.collection;
+
+import com.google.common.base.CaseFormat;
+import me.lortseam.completeconfig.ConfigMap;
+import me.lortseam.completeconfig.api.ConfigEntry;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+import me.lortseam.completeconfig.api.ConfigEntryListener;
+import me.lortseam.completeconfig.entry.Entry;
+import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
+import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
+import me.lortseam.completeconfig.exception.IllegalModifierException;
+import me.lortseam.completeconfig.listener.Listener;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Modifier;
+import java.util.*;
+
+public class EntryMap extends ConfigMap<Entry> {
+
+    void fill(ConfigEntryContainer container) {
+        LinkedHashMap<String, Entry> containerEntries = new LinkedHashMap<>();
+        for (Class<? extends ConfigEntryContainer> clazz : container.getClasses()) {
+            List<Listener> listeners = new ArrayList<>();
+            Iterator<Listener> iter = pendingListeners.iterator();
+            while (iter.hasNext()) {
+                Listener listener = iter.next();
+                if (listener.getFieldClass() == clazz) {
+                    listeners.add(listener);
+                    iter.remove();
+                }
+            }
+            Arrays.stream(clazz.getDeclaredMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()) && method.isAnnotationPresent(ConfigEntryListener.class)).forEach(method -> {
+                ConfigEntryListener listener = method.getDeclaredAnnotation(ConfigEntryListener.class);
+                String fieldName = listener.value();
+                if (fieldName.equals("")) {
+                    if (!method.getName().startsWith("set") || method.getName().equals("set")) {
+                        throw new IllegalAnnotationParameterException("Could not detect field name for listener method " + method + ", please provide it inside the annotation");
+                    }
+                    fieldName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, method.getName().replaceFirst("set", ""));
+                }
+                Class<? extends ConfigEntryContainer> fieldClass = listener.container();
+                if (fieldClass == ConfigEntryContainer.class) {
+                    listeners.add(new Listener(method, container, fieldName));
+                } else {
+                    Map<String, Entry> fieldClassEntries = findEntries(config, fieldClass);
+                    if (fieldClassEntries.isEmpty()) {
+                        pendingListeners.add(new Listener(method, container, fieldName, fieldClass));
+                    } else {
+                        Entry entry = fieldClassEntries.get(fieldName);
+                        if (entry == null) {
+                            throw new IllegalAnnotationParameterException("Could not find field " + fieldName + " in " + fieldClass + " requested by listener method " + method);
+                        }
+                        addListenerToEntry(entry, method, container);
+                    }
+                }
+            });
+            LinkedHashMap<String, Entry> clazzEntries = new LinkedHashMap<>();
+            Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
+                if (Modifier.isStatic(field.getModifiers())) {
+                    return false;
+                }
+                if (container.isConfigPOJO()) {
+                    return !ConfigEntryContainer.class.isAssignableFrom(field.getType()) && !field.isAnnotationPresent(ConfigEntry.Ignore.class);
+                }
+                return field.isAnnotationPresent(ConfigEntry.class);
+            }).forEach(field -> {
+                if (Modifier.isFinal(field.getModifiers())) {
+                    throw new IllegalModifierException("Entry field " + field + " must not be final");
+                }
+                if (!field.isAccessible()) {
+                    field.setAccessible(true);
+                }
+                Entry.Builder builder = new Entry.Builder(field, container);
+                if (field.isAnnotationPresent(ConfigEntry.class)) {
+                    ConfigEntry entryAnnotation = field.getDeclaredAnnotation(ConfigEntry.class);
+                    String customTranslationKey = entryAnnotation.customTranslationKey();
+                    if (!StringUtils.isBlank(customTranslationKey)) {
+                        builder.setCustomTranslationKey(customTranslationKey);
+                    }
+                    String[] customTooltipKeys = entryAnnotation.customTooltipKeys();
+                    if (customTooltipKeys.length > 0) {
+                        for (String key : customTooltipKeys) {
+                            if (StringUtils.isBlank(key)) {
+                                throw new IllegalAnnotationParameterException("Tooltip key(s) of entry field " + field + " must not be blank");
+                            }
+                        }
+                        builder.setCustomTooltipKeys(customTooltipKeys);
+                    }
+                    builder.setForceUpdate(entryAnnotation.forceUpdate());
+                }
+                if (field.isAnnotationPresent(ConfigEntry.Bounded.Integer.class)) {
+                    if (field.getType() != int.class && field.getType() != Integer.class) {
+                        throw new IllegalAnnotationTargetException("Cannot apply Integer bound to non Integer field " + field);
+                    }
+                    ConfigEntry.Bounded.Integer bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Integer.class);
+                    builder.setBounds(bounds.min(), bounds.max());
+                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Long.class)) {
+                    if (field.getType() != long.class && field.getType() != Long.class) {
+                        throw new IllegalAnnotationTargetException("Cannot apply Long bound to non Long field " + field);
+                    }
+                    ConfigEntry.Bounded.Long bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Long.class);
+                    builder.setBounds(bounds.min(), bounds.max());
+                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Float.class)) {
+                    if (field.getType() != float.class && field.getType() != Float.class) {
+                        throw new IllegalAnnotationTargetException("Cannot apply Float bound to non Float field " + field);
+                    }
+                    ConfigEntry.Bounded.Float bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Float.class);
+                    builder.setBounds(bounds.min(), bounds.max());
+                } else if (field.isAnnotationPresent(ConfigEntry.Bounded.Double.class)) {
+                    if (field.getType() != double.class && field.getType() != Double.class) {
+                        throw new IllegalAnnotationTargetException("Cannot apply Double bound to non Double field " + field);
+                    }
+                    ConfigEntry.Bounded.Double bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Double.class);
+                    builder.setBounds(bounds.min(), bounds.max());
+                }
+                Entry<?> entry = builder.build();
+                if (guiRegistry.getProvider(entry) == null) {
+                    throw new UnsupportedOperationException("Could not find gui provider for field type " + entry.getType());
+                }
+                String fieldName = field.getName();
+                listeners.removeIf(listener -> {
+                    if (!listener.getFieldName().equals(fieldName)) {
+                        return false;
+                    }
+                    addListenerToEntry(entry, listener.getMethod(), listener.getParentObject());
+                    return true;
+                });
+                clazzEntries.put(fieldName, entry);
+            });
+            if (!listeners.isEmpty()) {
+                Listener listener = listeners.iterator().next();
+                throw new IllegalAnnotationParameterException("Could not find field " + listener.getFieldName() + " in " + clazz + " requested by listener method " + listener.getMethod());
+            }
+            //TODO: Quite hacky solution to sort the entries (superclasses first)
+            clazzEntries.putAll(containerEntries);
+            containerEntries = clazzEntries;
+        }
+    }
+
+}

+ 25 - 39
src/main/java/me/lortseam/completeconfig/entry/Entry.java

@@ -1,7 +1,10 @@
 package me.lortseam.completeconfig.entry;
 
-import lombok.*;
-import lombok.experimental.Accessors;
+import com.google.common.collect.MoreCollectors;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
 import me.lortseam.completeconfig.api.ConfigEntryContainer;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -12,28 +15,40 @@ import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.util.*;
 
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
 public class Entry<T> {
 
     private static final Logger LOGGER = LogManager.getLogger();
+    private static final Set<Entry> entries = new HashSet<>();
+
+    public static Entry<?> of(String fieldName, Class<? extends ConfigEntryContainer> parentClass) {
+        try {
+            Field field = parentClass.getField(fieldName);
+            return entries.stream().filter(entry -> entry.parentObject.getClass() == parentClass && entry.field == field).collect(MoreCollectors.toOptional()).orElse(new Entry<>(field, parentClass));
+        } catch (NoSuchFieldException e) {
+            //TODO
+            throw new RuntimeException(e);
+        }
+    }
 
     @Getter
     private final Field field;
     @Getter
     private final Class<T> type;
     @Getter
-    private final ConfigEntryContainer parentObject;
+    private ConfigEntryContainer parentObject;
     @Getter
-    private final T defaultValue;
+    private T defaultValue;
     @Getter
-    private final String customTranslationKey;
+    private String customTranslationKey;
     @Getter
-    private final String[] customTooltipKeys;
+    private String[] customTooltipKeys;
     @Getter
-    private final Extras<T> extras;
+    private Extras<T> extras;
     private final List<Listener> listeners = new ArrayList<>();
-    private final boolean forceUpdate;
+    private boolean forceUpdate;
 
-    private Entry(Field field, Class<T> type, ConfigEntryContainer parentObject, String customTranslationKey, String[] customTooltipKeys, Extras<T> extras, boolean forceUpdate) {
+    /*private Entry(Field field, Class<T> type, ConfigEntryContainer parentObject, String customTranslationKey, String[] customTooltipKeys, Extras<T> extras, boolean forceUpdate) {
         this.field = field;
         this.type = type;
         this.parentObject = parentObject;
@@ -42,7 +57,7 @@ public class Entry<T> {
         this.extras = extras;
         this.forceUpdate = forceUpdate;
         defaultValue = getValue();
-    }
+    }*/
 
     public T getValue() {
         if (updateValueIfNecessary()) {
@@ -107,35 +122,6 @@ public class Entry<T> {
         listeners.add(new Listener(method, parentObject));
     }
 
-    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-    @Accessors(chain = true)
-    public static class Builder {
-
-        public static Builder create(Field field, ConfigEntryContainer parentObject) {
-            return new Builder(field, parentObject);
-        }
-
-        private final Field field;
-        private final ConfigEntryContainer parentObject;
-        @Setter
-        private String customTranslationKey;
-        @Setter
-        private String[] customTooltipKeys;
-        @Setter
-        private boolean forceUpdate;
-        private Bounds bounds;
-
-        public <N extends Number> Builder setBounds(N min, N max) {
-            bounds = new Bounds<>(min, max);
-            return this;
-        }
-
-        public Entry<?> build() {
-            return new Entry<>(field, field.getType(), parentObject, customTranslationKey, customTooltipKeys, new Extras<>(bounds), forceUpdate);
-        }
-
-    }
-
     @AllArgsConstructor
     private static class Listener {
 

+ 10 - 8
src/main/java/me/lortseam/completeconfig/serialization/CollectionsDeserializer.java → src/main/java/me/lortseam/completeconfig/serialization/CollectionMapDeserializer.java

@@ -3,28 +3,30 @@ package me.lortseam.completeconfig.serialization;
 import com.google.gson.*;
 import com.google.gson.reflect.TypeToken;
 import me.lortseam.completeconfig.collection.Collection;
+import me.lortseam.completeconfig.collection.CollectionMap;
 
 import java.lang.reflect.Type;
 import java.util.LinkedHashMap;
+import java.util.Map;
 
-public class CollectionsDeserializer implements JsonDeserializer<LinkedHashMap<String, Collection>> {
+public class CollectionMapDeserializer implements JsonDeserializer<CollectionMap> {
 
-    public static final Type TYPE = new TypeToken<LinkedHashMap<String, Collection>>() {}.getType();
+    public static final Type TYPE = new TypeToken<CollectionMap>() {}.getType();
 
-    private final LinkedHashMap<String, Collection> configMap;
+    private final Map<String, Collection> configMap;
     private final String collectionID;
 
-    public CollectionsDeserializer(LinkedHashMap<String, Collection> configMap, String collectionID) {
+    public CollectionMapDeserializer(Map<String, Collection> configMap, String collectionID) {
         this.configMap = configMap;
         this.collectionID = collectionID;
     }
 
-    private CollectionsDeserializer(LinkedHashMap<String, Collection> configMap) {
+    private CollectionMapDeserializer(Map<String, Collection> configMap) {
         this(configMap, null);
     }
 
     @Override
-    public LinkedHashMap<String, Collection> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    public CollectionMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
         LinkedHashMap<String, JsonElement> map = new Gson().fromJson(json, new TypeToken<LinkedHashMap<String, JsonElement>>() {}.getType());
         if (collectionID == null) {
             map.forEach(this::deserialize);
@@ -40,9 +42,9 @@ public class CollectionsDeserializer implements JsonDeserializer<LinkedHashMap<S
             return;
         }
         new GsonBuilder()
-                .registerTypeAdapter(CollectionsDeserializer.TYPE, new CollectionsDeserializer(collection.getCollections()))
                 .registerTypeAdapter(CollectionDeserializer.TYPE, new CollectionDeserializer())
-                .registerTypeAdapter(EntriesDeserializer.TYPE, new EntriesDeserializer(collection.getEntries()))
+                .registerTypeAdapter(CollectionMapDeserializer.TYPE, new CollectionMapDeserializer(collection.getCollections()))
+                .registerTypeAdapter(EntryMapDeserializer.TYPE, new EntryMapDeserializer(collection.getEntries()))
                 .create()
                 .fromJson(element, Collection.class);
     }

+ 7 - 8
src/main/java/me/lortseam/completeconfig/serialization/EntriesDeserializer.java → src/main/java/me/lortseam/completeconfig/serialization/EntryMapDeserializer.java

@@ -2,23 +2,22 @@ package me.lortseam.completeconfig.serialization;
 
 import com.google.gson.*;
 import com.google.gson.reflect.TypeToken;
+import lombok.RequiredArgsConstructor;
+import me.lortseam.completeconfig.collection.EntryMap;
 import me.lortseam.completeconfig.entry.Entry;
 
 import java.lang.reflect.Type;
 import java.util.LinkedHashMap;
 
-public class EntriesDeserializer implements JsonDeserializer<LinkedHashMap<String, Entry>> {
+@RequiredArgsConstructor
+public class EntryMapDeserializer implements JsonDeserializer<EntryMap> {
 
-    public static final Type TYPE = new TypeToken<LinkedHashMap<String, Entry>>() {}.getType();
+    public static final Type TYPE = new TypeToken<EntryMap>() {}.getType();
 
-    private final LinkedHashMap<String, Entry> configMap;
-
-    public EntriesDeserializer(LinkedHashMap<String, Entry> configMap) {
-        this.configMap = configMap;
-    }
+    private final EntryMap configMap;
 
     @Override
-    public LinkedHashMap<String, Entry> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    public EntryMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
         LinkedHashMap<String, JsonElement> map = new Gson().fromJson(json, new TypeToken<LinkedHashMap<String, JsonElement>>() {}.getType());
         map.forEach((entryID, element) -> {
             Entry<?> entry = configMap.get(entryID);