ConfigurationManager.java

package de.dlr.bt.stc.config;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.FileBasedBuilderParameters;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.greenrobot.eventbus.EventBus;

import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent;
import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent.ConfigurationModificationType;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public final class ConfigurationManager {
	private final List<Path> configurationPaths = new ArrayList<>();

	private final Map<String, ACfg> configurations = new HashMap<>();

	@Getter
	private final EventBus instanceEventBus;
	@Getter
	private final EventBus managementEventBus;

	public ConfigurationManager(EventBus instanceEventBus, EventBus managementEventBus) {
		this.instanceEventBus = Objects.requireNonNull(instanceEventBus);
		this.managementEventBus = Objects.requireNonNull(managementEventBus);
	}

	/**
	 * Add a {@link Path} (either designating a single configuration file or a
	 * directory containing .yml files) to the configuration list.
	 * 
	 * @param path A {@link Path} designating either a single file or a folder
	 *             containing configuration files. The folder is not read
	 *             recursively.
	 */
	public void addConfigurationPath(Path path) {
		configurationPaths.add(path);
	}

	/**
	 * Load all configuration files previously added using
	 * {@link #addConfigurationPath(Path)}.
	 * 
	 */
	public void loadConfigurations() {
		final List<Path> configurationFiles = new ArrayList<>();
		findConfigurations(configurationPaths, configurationFiles);
		loadConfiguration(configurationFiles);
	}

	private void findConfigurations(List<Path> paths, List<Path> results) {
		for (var p : paths) {
			findConfigurations(p, results);
		}
	}

	private void findConfigurations(Path path, List<Path> results) {
		if (Files.isRegularFile(path)) {
			results.add(path);
		} else if (Files.isDirectory(path)) {
			try (var ds = Files.list(path)) {
				var files = ds.filter(ConfigurationManager::isYAMLFile).sorted().toList();

				findConfigurations(files, results);
			} catch (IOException ioe) {
				log.error("Failed to list files in directory {}: {}", path, ioe);
			}
		} else {
			log.warn("Path supplied to ConfigurationManager which is neither regular nor directory: {}", path);
		}
	}

	/**
	 * Check whether given path designates a YAML configuration file (regular file
	 * ending with .yml or .yaml)
	 * 
	 * @param path The path to check
	 * @return Whether given path is a configuration file
	 */
	private static boolean isYAMLFile(Path path) {
		if (!Files.isRegularFile(path))
			return false;

		var lowerCase = path.getFileName().toString().toLowerCase();

		return lowerCase.endsWith(".yml") || lowerCase.endsWith(".yaml");
	}

	/**
	 * Load a single configuration file
	 * 
	 * @param parameters {@link FileBasedBuilderParameters} specifying the
	 *                   configuration file
	 * @throws ConfigurationException
	 */
	private void loadConfiguration(List<Path> configurationFiles) {
		List<HierarchicalConfiguration<?>> ymls = new ArrayList<>();
		for (var file : configurationFiles) {

			FileBasedConfigurationBuilder<YAMLConfigurationSTC> configBuilder = new FileBasedConfigurationBuilder<>(
					YAMLConfigurationSTC.class);

			configBuilder.configure(new Parameters().fileBased().setFile(file.toFile()));

			try {
				ymls.add(configBuilder.getConfiguration());
			} catch (ConfigurationException ce) {
				log.error("Failed to parse configuration file {}: {}", file, ce);
			}
		}

		var loadedConfigurations = ConfigHandler.separateAndResolveConfigs(ymls);

		for (var entry : loadedConfigurations.entrySet()) {
			try {
				ACfg cfg = CfgFactory.getInstance().create(entry.getValue().getString("type"), entry.getValue());
				configurations.put(entry.getKey(), cfg);
				managementEventBus
						.post(new ConfigurationModifiedEvent(entry.getKey(), cfg, ConfigurationModificationType.ADD));
			} catch (ConfigurationException ce) {
				log.error("Failed to parse configuration item {}: {}", entry.getKey(), ce);
			}
		}
	}

	/**
	 * Unload all configuration files, but keep information about the files/folders
	 * (to allow reloading configuration)
	 */
	public void unloadConfiguration() {
		for (var cfg : configurations.entrySet())
			managementEventBus.post(
					new ConfigurationModifiedEvent(cfg.getKey(), cfg.getValue(), ConfigurationModificationType.REMOVE));

		configurations.clear();
	}

	/**
	 * Clear all information about configuration files, including the files/folders.
	 */
	public void clearConfiguration() {
		unloadConfiguration();

		configurationPaths.clear();
	}

	public Map<String, ACfg> getConfigurations() {
		return Collections.unmodifiableMap(configurations);
	}

}