SettingsManager.java

package org.egothor.methodatlas.gui.service;

import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import org.egothor.methodatlas.gui.model.AppSettings;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Loads and saves {@link AppSettings} to a JSON file in the user's
 * platform-specific configuration directory.
 *
 * <h2>File location</h2>
 * <p>The settings file path is resolved once at class-load time using the
 * following priority order:</p>
 * <ol>
 *   <li>{@code %APPDATA%\MethodAtlasGUI\settings.json} — Windows, when
 *       the {@code APPDATA} environment variable is set</li>
 *   <li>{@code $XDG_CONFIG_HOME/methodatlas-gui/settings.json} — Linux /
 *       macOS, when the {@code XDG_CONFIG_HOME} variable is set</li>
 *   <li>{@code ~/.methodatlas-gui/settings.json} — fallback on all
 *       platforms when neither variable is set</li>
 * </ol>
 * <p>The resolved path is exposed via {@link #getSettingsFile()} and is
 * constant for the lifetime of the JVM.</p>
 *
 * <h2>Error handling</h2>
 * <p>Both {@link #load()} and {@link #save(AppSettings)} log warnings and
 * continue rather than propagating {@link IOException}.  A failed load
 * returns factory-default settings; a failed save is silently dropped
 * after logging.</p>
 *
 * @see AppSettings
 */
public final class SettingsManager {

    private static final Logger LOG = Logger.getLogger(SettingsManager.class.getName());
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private static final Path SETTINGS_FILE = resolveSettingsPath();

    private SettingsManager() {}

    /**
     * Returns the path of the JSON file used by {@link #load()} and
     * {@link #save(AppSettings)}.
     *
     * <p>The path is resolved once at class-load time according to the
     * platform-specific rules described in the class-level documentation,
     * and does not change for the lifetime of the JVM.  The file may or may
     * not exist when this method is called.</p>
     *
     * @return absolute path to the settings file; never {@code null}
     */
    public static Path getSettingsFile() { return SETTINGS_FILE; }

    private static Path resolveSettingsPath() {
        String appData = System.getenv("APPDATA");
        if (appData != null && !appData.isBlank()) {
            return Path.of(appData, "MethodAtlasGUI", "settings.json");
        }
        String xdg = System.getenv("XDG_CONFIG_HOME");
        if (xdg != null && !xdg.isBlank()) {
            return Path.of(xdg, "methodatlas-gui", "settings.json");
        }
        return Path.of(System.getProperty("user.home"), ".methodatlas-gui", "settings.json");
    }

    /**
     * Loads application settings from the settings file.
     *
     * <p>If the file does not exist, or if it cannot be parsed, this method
     * logs a warning and returns a fresh {@link AppSettings} object
     * initialised to all default values.</p>
     *
     * @return application settings loaded from disk, or factory defaults on
     *         any read or parse error; never {@code null}
     */
    public static AppSettings load() {
        if (Files.exists(SETTINGS_FILE)) {
            try {
                return MAPPER.readValue(SETTINGS_FILE.toFile(), AppSettings.class);
            } catch (JacksonException e) {
                if (LOG.isLoggable(Level.WARNING)) {
                    LOG.log(Level.WARNING, "Cannot read settings from " + SETTINGS_FILE + "; using defaults", e);
                }
            }
        }
        return new AppSettings();
    }

    /**
     * Persists the given settings to the settings file.
     *
     * <p>The parent directory is created if it does not yet exist.  If the
     * write fails for any reason, a warning is logged and the method returns
     * normally without propagating the exception.</p>
     *
     * @param settings settings object to serialise; must not be {@code null}
     */
    public static void save(AppSettings settings) {
        try {
            Files.createDirectories(SETTINGS_FILE.getParent());
            MAPPER.writerWithDefaultPrettyPrinter().writeValue(SETTINGS_FILE.toFile(), settings);
        } catch (IOException | JacksonException e) {
            if (LOG.isLoggable(Level.WARNING)) {
                LOG.log(Level.WARNING, "Cannot save settings to " + SETTINGS_FILE, e);
            }
        }
    }
}