ru.vyarus.yaml.updater.UpdateConfig Maven / Gradle / Ivy
Show all versions of yaml-config-updater Show documentation
package ru.vyarus.yaml.updater;
import ru.vyarus.yaml.updater.listen.UpdateListener;
import ru.vyarus.yaml.updater.listen.UpdateListenerAdapter;
import ru.vyarus.yaml.updater.report.UpdateReport;
import ru.vyarus.yaml.updater.util.FileUtils;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Update configuration. Required current config and updating file. Optionally, environment variables could be
* provided (to replace in updating file). To override existing values (or to remove outdated paths) specify yaml
* paths to remove in old file before update (note that yaml path elements split with '/' because yaml property
* could contain dots).
*
* @author Vyacheslav Rusakov
* @since 14.04.2021
*/
@SuppressWarnings("PMD.MissingStaticMethodInNonInstantiatableClass")
public final class UpdateConfig {
private File current;
private boolean backup;
private File backupDir;
// not File type to allow loading from classpath (jar) or any other location
private String update;
private final List deleteProps = new ArrayList<>();
// variables to apply to fresh config placeholders (adopt config to exact environment)
private final Map env = new HashMap<>();
private boolean validateResult = true;
private UpdateListener listener;
private boolean dryRun;
/**
* Instance created through the {@link ru.vyarus.yaml.updater.UpdateConfig.Configurator} instance.
*/
private UpdateConfig() {
}
/**
* @return current config that must be updated
*/
public File getCurrent() {
return current;
}
/**
* @return true to save current config backup after update
*/
public boolean isBackup() {
return backup;
}
/**
* @return directory to store backups in or null to store near original config
*/
public File getBackupDir() {
return backupDir;
}
/**
* Configuration file may contain placeholders ({@link #getEnv()}).
* Only properties not found in the current config would be added (for properties remove
* see {@link #getDeleteProps()}).
*
* @return new configuration to update from
*/
public String getUpdate() {
return update;
}
/**
* NOTE: '/' used for property separation because in yaml property name could contain dot.
*
* @return properties to remove in current file (would be replaced if provided in new file)
*/
public List getDeleteProps() {
return deleteProps;
}
/**
* Variables for replacing placeholders in update file {@code #{var}}.
* Variables searched without counting yaml semantics.
* Used to adopt configuration to the target environment (especially useful for the first installation).
*
* @return variables to replace in updating file
*/
public Map getEnv() {
return env;
}
/**
* @return true to validate result against old and new file trees (to make sure all old values preserved and new
* values added)
*/
public boolean isValidateResult() {
return validateResult;
}
/**
* @return configured listener (might be dummy adapter if nothing configured)
*/
public UpdateListener getListener() {
return listener;
}
/**
* @return true if configuration should not be modified (test run), false for normal execution
*/
public boolean isDryRun() {
return dryRun;
}
/**
* Updater configurator. Class might be extended to extend functionality
* (see {@link ru.vyarus.yaml.updater.profile.TestConfigurator} as example).
*
* @param actual builder type (could be extended)
*/
public abstract static class Configurator> {
protected final UpdateConfig config = new UpdateConfig();
/**
* Yaml updater configurator.
*
* @param current current configuration file
* @param update update file stream
*/
public Configurator(final File current, final InputStream update) {
if (current == null) {
throw new IllegalArgumentException("Current config file not specified");
}
config.current = current;
if (update == null) {
throw new IllegalArgumentException("New config file not specified");
}
final String text = FileUtils.read(update);
if (text.isEmpty()) {
throw new IllegalArgumentException("New config file is empty");
}
config.update = text;
}
/**
* @param backup true to do backup of configuration before update
* @return builder instance for chained calls
*/
public T backup(final boolean backup) {
config.backup = backup;
return self();
}
/**
* By default, backup is created in the same directory with configuration.
*
* @param dir directory to store backup into
* @return builder instance for chained calls
*/
public T backupDir(final File dir) {
config.backupDir = dir;
return self();
}
/**
* IMPORTANT: yaml property names could contain '.' and so '/' used as property separator. But, as it would
* be a common point of confusion merger will try both: property as is and with replaced dots (fallback).
*
* Yaml path may include list values with syntax: prop/sublist[0]/foo. It would match first item of list
* prop/sublist and select foo item property.
*
* prop:
* sublist:
* - foo: 1
* bar: 2
*
*
* Method may be called multiple times.
*
* @param deleteProps yaml paths to delete in the old file (would be replaced with props from new file;
* null ignored)
* @return builder instance for chained calls
*/
public T deleteProps(final String... deleteProps) {
return deleteProps != null ? deleteProps(Arrays.asList(deleteProps)) : self();
}
/**
* Properties paths to delete in current file. Method may be called multiple times.
*
* @param deleteProps yaml paths to delete in the old file (would be replaced with props from new file;
* null ignored)
* @return builder instance for chained calls
* @see #deleteProps(String...)
*/
public T deleteProps(final List deleteProps) {
if (deleteProps != null) {
config.deleteProps.addAll(deleteProps);
}
return self();
}
/**
* Merged file validation (checks that all old values remains and new values added). Disabling might be
* only useful in case of bugs in validation logic (comparing yaml trees). When validation is disabled, merged
* file is still parsed with snakeyaml to make sure it's readable.
*
* @param validate true to enable validation
* @return builder instance for chained calls
*/
public T validateResult(final boolean validate) {
config.validateResult = validate;
return self();
}
/**
* Variables use special syntax {@code #{name}} because with it yaml file still remains valid (variable treated
* as comment). Method may be called multiple times.
*
* @param env variables to replace in updating file (null ignored)
* @return builder instance for chained calls
* @deprecated use {@link #vars(java.util.Map)} instead
*/
@Deprecated
public T envVars(final Map env) {
return vars(env);
}
/**
* Variables use special syntax {@code #{name}} because with it yaml file still remains valid (variable treated
* as comment). Method may be called multiple times.
*
* @param env variables to replace in updating file (null ignored)
* @return builder instance for chained calls
* @see ru.vyarus.yaml.updater.util.FileUtils#loadProperties(String) for loading
*/
public T vars(final Map env) {
if (env != null) {
config.env.putAll(env);
}
return self();
}
/**
* Load variables (see {@link #vars(java.util.Map)}) from properties file.
* May be called multiple times.
*
* @param path fs file path, classpath or file url
* @param failIfNotFound true to fail when file not found, false to bypass
* @return builder instance for chained calls
*/
public T varsFile(final String path, final boolean failIfNotFound) {
if (!FileUtils.loadProperties(path, config.env) && failIfNotFound) {
throw new IllegalArgumentException("Variables file not found: " + path);
}
return self();
}
/**
* Adds variable for source config substitution. May be called multiple times.
*
* @param name variable name
* @param value variable value
* @return builder instance for chained calls
* @see #vars(java.util.Map)
*/
@SuppressWarnings("checkstyle:IllegalIdentifierName")
public T var(final String name, final String value) {
if (name != null) {
config.env.put(name, value);
}
return self();
}
/**
* Register listener for accessing internal files model during merge process. Mainly used for testing.
*
* Only one listener allowed.
*
* @param listener merge process stages listener (null ignored)
* @return builder instance for chained calls
*/
public T listen(final UpdateListener listener) {
if (listener != null) {
config.listener = listener;
}
return self();
}
/**
* Test execution - performs complete update, but did not override existing file. Useful for validations
* (in tests or with CLI to make sure upgrade would be successful).
*
* @param dryRun true to not perform any modifications (test run)
* @return builder instance for chained calls
*/
public T dryRun(final boolean dryRun) {
config.dryRun = dryRun;
return self();
}
/**
* Performs configuration migration.
*
* @return update report
* @see ru.vyarus.yaml.updater.report.ReportPrinter for default report formatter
*/
public UpdateReport update() {
if (config.listener == null) {
// to avoid null checks everywhere
config.listener = new UpdateListenerAdapter();
}
config.listener.configured(config);
return new YamlUpdater(config).execute();
}
@SuppressWarnings("unchecked")
private T self() {
return (T) this;
}
}
}