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

net.morimekta.config.ConfigSupplier Maven / Gradle / Ivy

/*
 * Copyright 2023 Morimekta Utils Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import net.morimekta.config.ConfigEventListener.Status;
import net.morimekta.file.FileEvent;
import net.morimekta.file.FileEventListener;
import net.morimekta.file.FileUtil;
import net.morimekta.file.FileWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;
import static net.morimekta.config.ConfigChangeType.CREATED;
import static net.morimekta.config.ConfigChangeType.MODIFIED;

/**
 * A wrapper around a config file to handle loading and parsing during
 * application setup.
 *
 * 
{@code
 * class MyApplication extends TinyApplication {
 *     private var config = ConfigSupplier.yamlConfig(MyConfig.class)
 *     {@literal@}Override
 *     public void initialize(ArgParser argParser, TinyApplicationContext.Builder context) {
 *         argParser.add(Option
 *             .optionLong("--config", "A config file", ValueParser.path(config::loadFromFile))
 *             .required())
 *     }
 *
 *     public void onStart(TinyApplicationContext context) {
 *         var myConfig = config.get();
 *         // you have a config!
 *     }
 * }
 * }
*

* See net.morimekta.tiny.server:tiny-server for * details on the microservice server starter. * * @param The config type. */ public class ConfigSupplier implements Supplier, Closeable { /** * Create a config supplier. * * @param loader The config loader to use for the supplier. */ public ConfigSupplier(ConfigReader loader) { this(loader, ConfigWatcher.FILE_WATCHER); } /** * Create a config supplier. * * @param loader The config loader to use for the supplier. * @param fileWatcherSupplier Supplier of file watcher. Mainly for testing. */ public ConfigSupplier(ConfigReader loader, Supplier fileWatcherSupplier) { this.reference = new AtomicReference<>(); this.changeListeners = new ArrayList<>(); this.eventListeners = new ArrayList<>(); this.loader = loader; this.fileWatcherSupplier = fileWatcherSupplier; this.fileEventListener = this::onFileEvent; } public ConfigSupplier addChangeListener(ConfigChangeListener listener) { synchronized (mutex) { changeListeners.removeIf(it -> it == listener); changeListeners.add(listener); } return this; } public ConfigSupplier addEventListener(ConfigEventListener listener) { synchronized (mutex) { eventListeners.removeIf(it -> it == listener); eventListeners.add(listener); } return this; } /** * Load config from file and store the result as the supplied config. * * @param filePath The file to load. * @throws IOException If unable to read the file. * @throws ConfigException If unable to parse the file. */ public void load(Path filePath) throws IOException, ConfigException { synchronized (mutex) { checkBeforeLoad(filePath); reference.set(requireNonNull(loader.readConfig(filePath), "loaded config == null")); } } /** * Load config from file, store the result as the supplied config and start * monitoring the actual file for updates. Updates will then cause config * listeners to be triggered. * * @param filePath The file to load. * @throws IOException If unable to read the file. * @throws ConfigException If unable to parse the file. */ public void loadAndMonitor(Path filePath) throws ConfigException, IOException { synchronized (mutex) { checkBeforeLoad(filePath); fileWatcherSupplier.get().weakAddWatcher(filePath, fileEventListener); ConfigType config; try { config = requireNonNull(loader.readConfig(filePath), "loaded config == null"); onConfigFileRead(FileEvent.CREATED, filePath, Status.OK); } catch (ConfigException e) { onConfigFileRead(FileEvent.CREATED, filePath, Status.PARSE_FAILED); throw e; } catch (IOException e) { onConfigFileRead(FileEvent.CREATED, filePath, Status.READ_FAILED); throw e; } var old = reference.getAndSet(config); if (!config.equals(old)) { onConfigChange(CREATED, filePath, config); } } } /** * Load config from file and store the result as the supplied config. Protect * the caller from checked exceptions by wrapping them as matching unchecked * variants. * * @param filePath The file to load. */ public void loadUnchecked(Path filePath) { try { load(filePath); } catch (ConfigException e) { throw e.asUncheckedException(); } catch (IOException e) { throw new UncheckedIOException(e.getMessage(), e); } } /** * Load config from file, store the result as the supplied config and start * monitoring the actual file for updates. Updates will then cause config * listeners to be triggered. Protect the caller from checked exceptions by * wrapping them as matching unchecked variants. * * @param filePath The file to load. */ public void loadAndMonitorUnchecked(Path filePath) { try { loadAndMonitor(filePath); } catch (ConfigException e) { throw e.asUncheckedException(); } catch (IOException e) { throw new UncheckedIOException(e.getMessage(), e); } } // --- Supplier --- /** * Get the current config content. * * @return The last loaded config. * @throws NullPointerException If no config has been loaded. */ @Override public ConfigType get() { return Optional.ofNullable(reference.get()) .orElseThrow(() -> new NullPointerException("Config not loaded.")); } // --- Closeable --- @Override public void close() { synchronized (mutex) { changeListeners.clear(); eventListeners.clear(); fileWatcherSupplier.get().removeWatcher(fileEventListener); } } // --- Object --- @Override public String toString() { return "ConfigSupplier{config=" + reference.get() + "}"; } // --- Static --- /** * Load config as YAML, just using available jackson modules. * * @param type The config entry class. * @param The config entry type. * @return The config supplier. */ public static ConfigSupplier yamlConfig(Class type) { return yamlConfig(type, ObjectMapper::findAndRegisterModules); } /** * Load config as YAML. * * @param type The config entry class. * @param initMapper Initializer for the ObjectMapper to handle the config. * @param The config entry type. * @return The config supplier. */ public static ConfigSupplier yamlConfig(Class type, Consumer initMapper) { return new ConfigSupplier<>(new ConfigReader.YamlConfigReader<>(type, initMapper)); } // --- Private --- private static final Logger LOGGER = LoggerFactory.getLogger(ConfigSupplier.class); private final Object mutex = new Object(); private final AtomicReference reference; private final ArrayList> changeListeners; private final ArrayList eventListeners; private final ConfigReader loader; private final Supplier fileWatcherSupplier; private final FileEventListener fileEventListener; private void checkBeforeLoad(Path filePath) throws IOException { if (reference.get() != null) { throw new IllegalStateException("Config already loaded."); } var canonical = FileUtil.readCanonicalPath(filePath); if (!Files.exists(canonical)) { throw new FileNotFoundException("No such config file " + filePath); } if (!Files.isRegularFile(canonical)) { throw new IOException("Config path " + filePath + " is not a regular file."); } } private void onFileEvent(Path file, FileEvent event) { if (event == FileEvent.DELETED) { if (Files.exists(file)) { // Ignore, same special case as with ConfigWatcher#onFileEventInternal return; } synchronized (mutex) { fileWatcherSupplier.get().removeWatcher(fileEventListener); onConfigFileRead(FileEvent.DELETED, file, Status.OK); onConfigChange(ConfigChangeType.DELETED, file, reference.get()); } // NOTE: Keeping last loaded config, just stop listening. } else { if (!Files.exists(file)) { // Ignore, same special case as with ConfigWatcher#onFileEventInternal return; } synchronized (mutex) { ConfigType config; try { config = requireNonNull(loader.readConfig(file), "loaded config == null"); onConfigFileRead(FileEvent.MODIFIED, file, Status.OK); } catch (ConfigException e) { LOGGER.error("Exception parsing config: {}", e.getMessage(), e); onConfigFileRead(FileEvent.MODIFIED, file, Status.PARSE_FAILED); return; } catch (IOException e) { LOGGER.error("Exception reading config: {}", e.getMessage(), e); onConfigFileRead(FileEvent.MODIFIED, file, Status.READ_FAILED); return; } var old = reference.getAndSet(config); if (!config.equals(old)) { onConfigChange(MODIFIED, file, config); } } } } private void onConfigFileRead(FileEvent type, Path file, Status status) { for (var el : getEventListeners()) { try { el.onConfigFileRead(type, file, status); } catch (Exception e) { LOGGER.error("Exception notifying on config read.", e); } } } private void onConfigFileUpdate(ConfigChangeType type, Path file, Status status) { for (var el : getEventListeners()) { try { el.onConfigFileUpdate(type, file, file.getFileName().toString(), status); } catch (Exception e) { LOGGER.error("Exception notifying on config update.", e); } } } private void onConfigChange(ConfigChangeType type, Path file, ConfigType config) { boolean error = false; for (var cl : getChangeListeners()) { try { cl.onConfigChange(type, config); } catch (Exception e) { error = true; LOGGER.error("Exception notifying updated config.", e); } } onConfigFileUpdate(type, file, error ? Status.ERROR : Status.OK); } private List> getChangeListeners() { synchronized (mutex) { return List.copyOf(changeListeners); } } private List getEventListeners() { synchronized (mutex) { return List.copyOf(eventListeners); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy