From 1b40916046020410940e77f19123fcffd37a098e Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 23 Nov 2018 21:01:45 +0800 Subject: [PATCH] Rewrite config properties --- .../org/jackhuang/hmcl/setting/Config.java | 29 +-- .../hmcl/util/javafx/PropertyUtils.java | 167 ++++++++++++++++++ 2 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index f56290b1c..3d41274da 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -40,13 +40,12 @@ import org.jackhuang.hmcl.util.gson.FileTypeAdapter; import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; import org.jackhuang.hmcl.util.javafx.ObservableHelper; +import org.jackhuang.hmcl.util.javafx.PropertyUtils; import java.io.File; -import java.lang.reflect.Modifier; import java.net.Proxy; import java.util.Map; import java.util.TreeMap; -import java.util.stream.Stream; public final class Config implements Cloneable, Observable { @@ -68,10 +67,9 @@ public final class Config implements Cloneable, Observable { .create(); public static Config fromJson(String json) throws JsonParseException { - Config instance = CONFIG_GSON.fromJson(json, Config.class); - // Gson will replace the property fields (even they are final!) - // So we have to add the listeners again after deserialization - instance.addListenerToProperties(); + Config loaded = CONFIG_GSON.fromJson(json, Config.class); + Config instance = new Config(); + PropertyUtils.copyProperties(loaded, instance); return instance; } @@ -166,24 +164,7 @@ public final class Config implements Cloneable, Observable { private transient ObservableHelper helper = new ObservableHelper(this); public Config() { - addListenerToProperties(); - } - - private void addListenerToProperties() { - Stream.of(getClass().getDeclaredFields()) - .filter(it -> { - int modifiers = it.getModifiers(); - return !Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers); - }) - .filter(it -> Observable.class.isAssignableFrom(it.getType())) - .map(it -> { - try { - return (Observable) it.get(this); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to get my own properties"); - } - }) - .forEach(helper::receiveUpdatesFrom); + PropertyUtils.attachListener(this, helper); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java new file mode 100644 index 000000000..26ee2af7d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/PropertyUtils.java @@ -0,0 +1,167 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.util.javafx; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.Property; +import javafx.beans.value.WritableValue; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; + +public final class PropertyUtils { + private PropertyUtils() { + } + + public static class PropertyHandle { + public final WritableValue accessor; + public final Observable observable; + + public PropertyHandle(WritableValue accessor, Observable observable) { + this.accessor = accessor; + this.observable = observable; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static Map> getPropertyHandleFactories(Class type) { + Map collectionGetMethods = new LinkedHashMap<>(); + Map propertyMethods = new LinkedHashMap<>(); + for (Method method : type.getMethods()) { + Class returnType = method.getReturnType(); + if (method.getParameterCount() == 0 + && !returnType.equals(void.class)) { + String name = method.getName(); + if (name.endsWith("Property")) { + String propertyName = name.substring(0, name.length() - "Property".length()); + if (!propertyName.isEmpty() && Property.class.isAssignableFrom(returnType)) { + propertyMethods.put(propertyName, method); + } + } else if (name.startsWith("get")) { + String propertyName = name.substring("get".length()); + if (!propertyName.isEmpty() && + (ObservableList.class.isAssignableFrom(returnType) + || ObservableSet.class.isAssignableFrom(returnType) + || ObservableMap.class.isAssignableFrom(returnType))) { + propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1); + collectionGetMethods.put(propertyName, method); + } + } + } + } + propertyMethods.keySet().forEach(collectionGetMethods::remove); + + Map> result = new LinkedHashMap<>(); + propertyMethods.forEach((propertyName, method) -> { + result.put(propertyName, instance -> { + Property returnValue; + try { + returnValue = (Property) method.invoke(instance); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + return new PropertyHandle(returnValue, returnValue); + }); + }); + + collectionGetMethods.forEach((propertyName, method) -> { + result.put(propertyName, instance -> { + Object returnValue; + try { + returnValue = method.invoke(instance); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + WritableValue accessor; + if (returnValue instanceof ObservableList) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ((ObservableList) returnValue).setAll((List) value); + } + }; + } else if (returnValue instanceof ObservableSet) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ObservableSet target = (ObservableSet) returnValue; + target.clear(); + target.addAll((Set) value); + } + }; + } else if (returnValue instanceof ObservableMap) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ObservableMap target = (ObservableMap) returnValue; + target.clear(); + target.putAll((Map) value); + } + }; + } else { + throw new IllegalStateException(); + } + return new PropertyHandle(accessor, (Observable) returnValue); + }); + }); + return result; + } + + public static void copyProperties(Object from, Object to) { + Class type = from.getClass(); + while (!type.isInstance(to)) + type = type.getSuperclass(); + + getPropertyHandleFactories(type) + .forEach((name, factory) -> { + PropertyHandle src = factory.apply(from); + PropertyHandle target = factory.apply(to); + target.accessor.setValue(src.accessor.getValue()); + }); + } + + public static void attachListener(Object instance, InvalidationListener listener) { + getPropertyHandleFactories(instance.getClass()) + .forEach((name, factory) -> { + factory.apply(instance).observable.addListener(listener); + }); + } +}