/* * Copyright (c) 2015 NOVA, All rights reserved. * This library is free software, licensed under GNU Lesser General Public License version 3 * * This file is part of NOVA. * * NOVA 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. * * NOVA 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 NOVA. If not, see <http://www.gnu.org/licenses/>. */ package nova.core.config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigRenderOptions; import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueFactory; import nova.core.util.ReflectionUtil; import nova.core.util.collection.Tuple2; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class Configuration { private Configuration() {} private static final ConfigRenderOptions renderOpts = ConfigRenderOptions.defaults() .setOriginComments(false) // If true, adds `# hardcoded value` everywhere .setJson(false); // If true, generates valid JSON /* * Hard reflection to add comments for HOCON rendering */ private static Field originField = null; private static Method appendComments = null; private static Class<?> abstractConfigValueClass; static { try { //And classes are not public sadly. Package name `impl` says for itself abstractConfigValueClass = Class.forName("com.typesafe.config.impl.AbstractConfigValue"); originField = abstractConfigValueClass.getDeclaredField("origin"); originField.setAccessible(true); appendComments = Class.forName("com.typesafe.config.impl.SimpleConfigOrigin").getDeclaredMethod("appendComments", List.class); appendComments.setAccessible(true); //Origins field is final, but we could use superhacks to get around that Field modifiers = Field.class.getDeclaredField("modifiers"); modifiers.setAccessible(true); modifiers.setInt(originField, originField.getModifiers() ^ Modifier.FINAL); } catch (ReflectiveOperationException e) { e.printStackTrace(); } } private static void addComment(ConfigValue val, String comment) { if (abstractConfigValueClass.isInstance(val) && originField != null && appendComments != null) { try { Object newOrigin = appendComments.invoke(val.origin(), Collections.singletonList(comment)); originField.set(val, newOrigin); } catch (InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } } } private static com.typesafe.config.Config handle(com.typesafe.config.Config config, Object holder, String pathOffset) { Map<String, Tuple2<Object, String>> defaults = new HashMap<>(); if (!holder.getClass().isAnnotationPresent(ConfigHolder.class)) { throw new nova.core.config.ConfigException("No @ConfigHolder annotation on your config class!"); } boolean scanHolders = holder.getClass().getAnnotation(ConfigHolder.class).value(); Map<Field, Config> fields; if (holder.getClass().getAnnotation(ConfigHolder.class).useAll()) { fields = Arrays.stream(holder.getClass().getDeclaredFields()) .filter(f -> !f.isSynthetic()) .map(f -> new Tuple2<>(f, f.isAnnotationPresent(Config.class) ? f.getAnnotation(Config.class) : Config.DEFAULT)) .collect(Collectors.toMap(t -> t._1, t -> t._2)); } else { fields = ReflectionUtil.getAnnotatedFields(Config.class, holder.getClass()); } for (Map.Entry<Field, Config> entry : fields.entrySet()) { Field field = entry.getKey(); Config ann = entry.getValue(); String path = pathOffset + ("".equals(ann.value()) ? "" : ann.value() + ".") + field.getName(); field.setAccessible(true); if (field.getType().isAnnotationPresent(ConfigHolder.class)) { if (scanHolders) { try { config = config.withFallback(handle(config, field.get(holder), path + ".")); } catch (IllegalAccessException e) { e.printStackTrace(); } } else { throw new nova.core.config.ConfigException("Scanning inner-objects is disabled for `%s`", path); } } else { Object def = null; Object value = null; boolean failed = false; try { def = field.get(holder); value = config.getAnyRef(path); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ConfigException e) { failed = true; } if (failed) { if (def != null) { defaults.put(path, new Tuple2<>(def, ann.comment())); } } else { try { field.set(holder, value); } catch (IllegalArgumentException e) { throw new nova.core.config.ConfigException("Field `%s` is of the wrong type!", field.getName()); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } if (!defaults.isEmpty()) { com.typesafe.config.Config merged = config; for (Map.Entry<String, Tuple2<Object, String>> entry : defaults.entrySet()) { ConfigValue val = ConfigValueFactory.fromAnyRef(entry.getValue()._1); String comment = entry.getValue()._2; if (!"".equals(comment)) { //TODO: Maybe `comment = Game.language().translate(comment);` ? if (!val.origin().comments().contains(comment)) { addComment(val, comment); } } merged = merged.withValue(entry.getKey(), val); } config = merged; } if (holder instanceof ConfigHandler) { Optional<com.typesafe.config.Config> handled = ((ConfigHandler) holder).handle(config); if (handled.isPresent()) config = handled.get(); } return config; } /** * Loads config data from HOCON string. * * @param configData Valid HOCON config. * @param holder Object with {@code @ConfigHolder} annotation and {@code @Config}'s in it. * @return Full config with added default data, represented as string. */ public static String load(String configData, Object holder) { com.typesafe.config.Config parsed = ConfigFactory.parseString(configData); com.typesafe.config.Config config = handle(parsed, holder, ""); if (!parsed.equals(config)) { return config.root().render(renderOpts); } return configData; } /** * Loads config data from HOCON. If any values wasn't there, * then writes defaults for them back to the file. * If file do not exist - creates it and writes all defaults. * * @param configFile File to load config from. * @param holder Object with {@code @ConfigHolder} annotation and {@code @Config}'s in it. */ // mkdirs and createNewFile public static void load(File configFile, Object holder) { if (!configFile.exists()) { try { configFile.getParentFile().mkdirs(); configFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } com.typesafe.config.Config parsed = ConfigFactory.parseFile(configFile); com.typesafe.config.Config config = handle(parsed, holder, ""); if (!parsed.equals(config)) { String hocon = config.root().render(renderOpts); try (FileWriter writer = new FileWriter(configFile)) { writer.write(hocon); } catch (IOException e) { e.printStackTrace(); } } } /** * Loads config data from InputStream, any values missing are just ignored(so your defaults stay at place). * * @param stream Input stream to load data from. * @param holder Object with {@code @ConfigHolder} annotation and {@code @Config}'s in it. */ public static void load(InputStream stream, Object holder) { handle(ConfigFactory.parseReader(new InputStreamReader(stream)), holder, ""); } /** * Shortcut to load configs from URL. * * @param url Given URL. * @param holder Object with {@code @ConfigHolder} annotation and {@code @Config}'s in it. */ public static void load(URL url, Object holder) { try { load(url.openStream(), holder); } catch (IOException e) { e.printStackTrace(); } } /** * Shortcut to load configs from URI. Just uses URI#toURL, be warned. * * @param uri Given URI. * @param holder Object with {@code @ConfigHolder} annotation and {@code @Config}'s in it. */ public static void load(URI uri, Object holder) { try { load(uri.toURL(), holder); } catch (MalformedURLException e) { e.printStackTrace(); } } }