All Downloads are FREE. Search and download functionalities are using the official Maven repository.

host.anzo.commons.config.ConfigLoader Maven / Gradle / Ivy

There is a newer version: 1.29
Show newest version
package host.anzo.commons.config;

import com.google.common.collect.ImmutableList;
import com.sun.jna.platform.FileUtils;
import host.anzo.classindex.ClassIndex;
import host.anzo.commons.config.annotation.ConfigAfterLoad;
import host.anzo.commons.config.annotation.ConfigComments;
import host.anzo.commons.config.annotation.ConfigFile;
import host.anzo.commons.config.annotation.ConfigProperty;
import host.anzo.core.startup.IReloadable;
import host.anzo.core.startup.Reloadable;
import host.anzo.core.startup.StartupComponent;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.TextStringBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.stream.Stream;

/**
 * @author ANZO
 * @since 02.04.2017
 */
@Slf4j
@Reloadable(name = "all", group = "config")
@StartupComponent("Configure")
public final class ConfigLoader implements IReloadable {
	@Getter(lazy = true)
	private static final ConfigLoader instance = new ConfigLoader();

	private static final Set VAR_NAMES_CACHE = new HashSet<>();

	public ConfigLoader() {
		loadConfigs();
		cleanupConfigs();
	}

	private void loadConfigs() {
		for (Class clazz : ClassIndex.getAnnotated(ConfigFile.class)) {
			boolean loadConfig = false;
			final ConfigFile annotation = clazz.getAnnotation(ConfigFile.class);
			if (annotation.loadForPackages().length > 0) {
				for(String classPath : annotation.loadForPackages()) {
					if (!StringUtils.isEmpty(classPath)) {
						final URL url = getClass().getClassLoader().getResource(classPath.replace(".", "/"));
						if (url != null) {
							loadConfig = true;
							break;
						}
					}
				}
			}
			else {
				loadConfig = true;
			}

			if (!loadConfig) {
				continue;
			}

			final File file = getConfigFilePath(annotation.name()).toFile();

			if (!file.exists() && file.isDirectory()) {
				file.mkdirs();
			}

			if (!file.exists()) {
				buildConfig(clazz);
			}
			else {
				updateConfig(clazz);
			}

			loadConfig(clazz);
		}
	}

	/**
	 * Cleanup config file from unused properties
	 */
	private void cleanupConfigs() {
		final List> configClasses = ImmutableList.copyOf(ClassIndex.getAnnotated(ConfigFile.class).iterator());
		try (Stream walk = Files.walk(Paths.get(getConfigFolder(), "config"))) {
			final List configFiles = walk.filter(Files::isRegularFile).filter(x -> x.toString().endsWith(".properties"))
					.toList();
			for (Path configFilePath : configFiles) {
				if (configClasses.stream().noneMatch(clazz -> {
					final ConfigFile annotation = clazz.getAnnotation(ConfigFile.class);
					final Path filePath = getConfigFilePath(annotation.name());
					try {
						return Files.isSameFile(configFilePath, filePath);
					} catch (IOException e) {
						throw new RuntimeException("Error checking equality for config file " + configFilePath, e);
					}
				})) {
					if (FileUtils.getInstance().hasTrash()) {
						FileUtils.getInstance().moveToTrash(configFilePath.toFile());
						log.warn("Moved to trash [{}] config file due config loader didn't exist anymore.", configFilePath);
					}
					else {
						Files.delete(configFilePath);
						log.warn("Removed [{}] config file due config loader didn't exist anymore.", configFilePath);
					}
				}
			}
		}
		catch (Exception e) {
			log.error("Error while cleanupConfigs()", e);
		}
	}

	private void updateConfig(@NotNull Class clazz) {
		final Properties properties = new Properties();
		final Path filePath = getConfigFilePath(clazz.getAnnotation(ConfigFile.class).name());
		try(InputStream input = Files.newInputStream(filePath)) {
			properties.load(input);
		}
		catch (IOException ex) {
			log.error("Error while calling loadConfig", ex);
		}

		final TextStringBuilder newPropertiesText = new TextStringBuilder();
		newPropertiesText.appendln("");
		boolean isNewPropertyExists = false;
		for (Field field : clazz.getDeclaredFields()) {
			final ConfigProperty annotation = field.getAnnotation(ConfigProperty.class);
			if (!annotation.isLoadFromFile()) {
				continue;
			}
			final String propertyValue = properties.getProperty(annotation.name());
			if (propertyValue == null && !annotation.isMap()) {
				isNewPropertyExists = true;
				newPropertiesText.appendNewLine();
				newPropertiesText.appendln(generateFieldConfig(clazz, field).trim());
				log.warn("Updated '{}' config with new field '{}'", filePath, annotation.name());
			}
		}
		if (isNewPropertyExists) {
			try {
				Files.write(filePath, newPropertiesText.toString().getBytes(), StandardOpenOption.APPEND);
			}
			catch (Exception e) {
				log.error("Error while writing config update", e);
			}
		}
	}

	private void buildConfig(@NotNull Class clazz) {
		final Path filePath = getConfigFilePath(clazz.getAnnotation(ConfigFile.class).name());
		log.info("Generated '{}'", filePath);
		try {
			Files.deleteIfExists(filePath);
			Files.createDirectories(filePath.getParent());
		} catch (IOException ex) {
			log.error("Error while buildConfig()", ex);
			return;
		}

		final TextStringBuilder out = new TextStringBuilder();

		for (Field field : clazz.getDeclaredFields()) {
			final String configField = generateFieldConfig(clazz, field);
			if (StringUtils.isNotEmpty(configField)) {
				out.appendln(configField);
			}
		}
		if (out.trim().isEmpty()) {
			return;
		}
		try {
			Files.write(filePath, out.toString().getBytes(), StandardOpenOption.CREATE);
		} catch (IOException ex) {
			log.error("Error while writing config file: {}", filePath, ex);
		}
	}

	private @Nullable String generateFieldConfig(@NotNull Class clazz, @NotNull Field field) {
		final ConfigComments configComments = field.getAnnotation(ConfigComments.class);
		final ConfigProperty configProperty = field.getAnnotation(ConfigProperty.class);

		if (configProperty == null) {
			throw new RuntimeException("ConfigProperty annotation not found on field: " + field.getName());
		}

		if (!configProperty.isLoadFromFile()) {
			return null;
		}

		final TextStringBuilder out = new TextStringBuilder();
		if (configComments != null) {
			for (String txt : configComments.comment()) {
				out.appendln("# " + txt);
			}
		}

		final String varCacheName = clazz.getSimpleName() + "." + configProperty.name();
		if (VAR_NAMES_CACHE.contains(varCacheName)) {
			log.warn("Config property name [{}] already defined in class [{}]!", configProperty.name(), clazz.getSimpleName());
		}
		else {
			VAR_NAMES_CACHE.add(varCacheName);
		}

		if (configProperty.isMap()) {
			for (String value : configProperty.values()) {
				out.appendln(value);
			}
		}
		else {
			out.appendln(configProperty.name() + " = " + configProperty.value());
		}

		return out.toString();
	}

	private void loadConfig(@NotNull Class clazz) {
		final Properties properties = new Properties();

		final Path filePath = getConfigFilePath(clazz.getAnnotation(ConfigFile.class).name());
		log.info("Loading config file: {}", filePath);
		try(InputStream input = Files.newInputStream(filePath)) {
			properties.load(input);
		}
		catch (IOException ex) {
			log.error("Error while calling loadConfig", ex);
		}

		try {
			final Object configObject = clazz.getDeclaredConstructor().newInstance();
			for (Field field : clazz.getFields()) {
				final ConfigProperty configProperty = field.getAnnotation(ConfigProperty.class);
				if (configProperty == null) {
					continue;
				}

				if (!configProperty.isLoadFromFile()) {
					continue;
				}

				if (!Modifier.isStatic(field.getModifiers())
				    || Modifier.isFinal(field.getModifiers())) {
					log.warn("Invalid modifiers for {} (must be static and final)", field);
					continue;
				}
				setConfigValue(configObject, field, properties, configProperty);
			}

			for (Method method : clazz.getDeclaredMethods()) {
				if (method.isAnnotationPresent(ConfigAfterLoad.class)) {
					if (method.trySetAccessible()) {
						method.invoke(configObject);
					}
				}
			}
		} catch (Exception e) {
			log.error("Error while initializing config object", e);
		}
	}

	@SuppressWarnings("unchecked")
	private void setConfigValue(Object object, @NotNull Field field, @NotNull Properties properties, @NotNull ConfigProperty annotation) {
		final String propertyValue = properties.getProperty(annotation.name(), annotation.value());
		try {
			if (!field.canAccess(null)) {
				field.setAccessible(true);
			}

			if (field.getType().isAssignableFrom(Map.class)
					|| field.getType().isAssignableFrom(EnumMap.class)) {
				final Map map = ((Map) field.get(object));
				map.clear();

				final Class keyType = (Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[0];
				final Class valueType = (Class)((ParameterizedType)field.getGenericType()).getActualTypeArguments()[1];
				final String mapPrefix = annotation.name();
				for(Map.Entry entry : properties.entrySet()) {
					final String name = (String)entry.getKey();
					if (name.startsWith(mapPrefix)) {
						final Object key = ConfigTypeCaster.cast(keyType, name.replace(mapPrefix+".", ""));
						final Object value = ConfigTypeCaster.cast(valueType, ((String)entry.getValue()).trim());
						map.put(key, value);
					}
				}
			}
			else {
				try {
					ConfigTypeCaster.cast(object, field, propertyValue, annotation.splitter(), annotation.minValue(), annotation.maxValue());
				}
				catch (Exception e) {
					log.error("Error while casting property value \"{}\" to field {}", propertyValue, field, e);
				}
			}
		} catch (IllegalAccessException e) {
			log.error("Invalid modifiers for field {}", field);
		}
	}

	private @NotNull Path getConfigFilePath(String fileName) {
		final String configFolder = getConfigFolder();
		if (StringUtils.isNotEmpty(configFolder)) {
			return Paths.get(configFolder, fileName);
		}
		return Paths.get(fileName);
	}

	private String getConfigFolder() {
		return System.getProperty("configFolder", "");
	}

	@Override
	public void reload() {
		loadConfigs();
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy