package org.rapidoid.config; import org.rapidoid.RapidoidThing; import org.rapidoid.annotation.Authors; import org.rapidoid.annotation.Since; import org.rapidoid.beany.Beany; import org.rapidoid.cls.Cls; import org.rapidoid.collection.Coll; import org.rapidoid.commons.Arr; import org.rapidoid.env.Env; import org.rapidoid.env.RapidoidEnv; import org.rapidoid.lambda.Operation; import org.rapidoid.log.Log; import org.rapidoid.u.U; import org.rapidoid.util.Msc; import org.rapidoid.value.Value; import org.rapidoid.value.Values; import java.io.File; import java.util.*; /* * #%L * rapidoid-commons * %% * Copyright (C) 2014 - 2017 Nikolche Mihajlovski and contributors * %% * Licensed 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. * #L% */ @Authors("Nikolche Mihajlovski") @Since("4.1.0") public class ConfigImpl extends RapidoidThing implements Config { private final List<String> baseKeys; private final ConfigImpl root; private final ConfigBase base; private final boolean isRoot; public ConfigImpl() { this(null, false); } public ConfigImpl(Map<String, Object> entries) { this(); update(entries); } public ConfigImpl(String defaultFilenameBase) { this(defaultFilenameBase, false); } public ConfigImpl(String defaultFilenameBase, boolean useBuiltInDefaults) { this.base = new ConfigBase(defaultFilenameBase, useBuiltInDefaults); this.root = this; this.baseKeys = U.list(); this.isRoot = true; } private ConfigImpl(ConfigBase base, List<String> baseKeys, ConfigImpl root) { this.base = base; this.root = root; this.baseKeys = Collections.unmodifiableList(U.list(baseKeys)); this.isRoot = false; } @Override public synchronized void reset() { clear(); base.reset(); } @Override public void invalidate() { clear(); base.invalidate(); } @Override public Value<Object> entry(String key) { return Values.wrap(new ConfigValueStore<Object>(this, key)); } private List<String> keyChain(Iterator<String> keys) { List<String> keyChain = U.list(this.baseKeys); while (keys.hasNext()) { String key = keys.next(); U.notNull(key, "config key"); Collections.addAll(keyChain, key.split("\\.")); } return keyChain; } @Override @SuppressWarnings("unchecked") public Config sub(String... keys) { U.must(U.notEmpty(keys), "Keys must be specified!"); return new ConfigImpl(base, keyChain(U.iterator(keys)), root()); } @Override public Config sub(List<String> keys) { U.must(U.notEmpty(keys), "Keys must be specified!"); return new ConfigImpl(base, keyChain(keys.iterator()), root()); } @Override public Object get(String key) { makeSureIsInitialized(); Object value; synchronized (base.properties) { value = asMap().get(key); } // if it's in the config, it's already overriden by the Env. If not, check manually: if (value == null) { value = globalOrArgConfigByRelativeKey(key); } return value; } private Object globalOrArgConfigByRelativeKey(String relKey) { String key = fullKey(relKey, "."); return globalOrArgConfig(key); } private String fullKey(String key, String separator) { return U.join(separator, Arr.concat(U.array(baseKeys), key)); } @Override public boolean has(String key) { makeSureIsInitialized(); return get(key) != null; } @Override public boolean is(String key) { makeSureIsInitialized(); Object value = get(key); try { return Boolean.TRUE.equals(Cls.convert(value, Boolean.class)); } catch (Exception e) { return false; } } @Override public Map<String, Object> toMap() { makeSureIsInitialized(); return Collections.unmodifiableMap(asMap()); } @Override public <T> Map<String, T> toMap(Class<T> type) { return Coll.toBeanMap(toMap(), type); } private Map<String, Object> asMap() { makeSureIsInitialized(); if (isRoot) { return base.properties; } else { synchronized (base.properties) { Map<String, Object> props = base.properties; for (String key : baseKeys) { Object value = props.get(key); if (value == null) { value = Coll.synchronizedMap(); props.put(key, value); } if (value instanceof Map<?, ?>) { props = (Map<String, Object>) value; } else { throw U.rte("Expected a Map for configuration section '%s', but found value of type: %s", sectionTo(key), value.getClass().getSimpleName()); } } return props; } } } private String sectionTo(String toKey) { String section = ""; for (String key : baseKeys) { if (!section.isEmpty()) { section += "."; } section += key; if (key.equals(toKey)) { break; } } return section; } @Override public void clear() { if (isRoot) { base.properties.clear(); } else { synchronized (base.properties) { parent().remove(lastBaseKey()); } } } private String lastBaseKey() { return baseKeys.get(baseKeys.size() - 1); } @Override public void remove(String key) { makeSureIsInitialized(); synchronized (base.properties) { asMap().remove(key); } } @Override public void assign(Map<String, Object> entries) { makeSureIsInitialized(); synchronized (base.properties) { clear(); update(entries); } } @Override public boolean isEmpty() { makeSureIsInitialized(); synchronized (base.properties) { return asMap().isEmpty() && !Env.properties().hasPrefix(U.join("_", baseKeys) + "_"); } } @Override public void update(Map<String, ?> entries) { makeSureIsInitialized(); update(entries, true); } @Override @SuppressWarnings("unchecked") public void update(Map<String, ?> entries, boolean overridenByEnv) { makeSureIsInitialized(); synchronized (base.properties) { for (Map.Entry<String, ?> e : entries.entrySet()) { String name = e.getKey(); Object value = e.getValue(); if (value instanceof Map<?, ?>) { sub(name).update((Map<String, ?>) value, overridenByEnv); } else { set(name, value, overridenByEnv); } } } } @Override public void set(String key, Object value) { set(key, value, true); } @Override public void set(String key, Object value, boolean overridenByEnv) { makeSureIsInitialized(); String[] keys = key.split("\\."); if (keys.length > 1) { Config cfg = sub(Arr.sub(keys, 0, -1)); cfg.set(U.last(keys), value, overridenByEnv); return; } if (overridenByEnv) { value = U.or(globalOrArgConfigByRelativeKey(key), value); } synchronized (base.properties) { asMap().put(key, value); } } @Override public String toString() { makeSureIsInitialized(); synchronized (base.properties) { return asMap().toString(); } } @Override @Deprecated public void args(List<String> args) { mustBeRoot(); base.initial.putAll(Msc.parseArgs(args)); } private Object globalOrArgConfig(String key) { return U.or(base.initial.get(key), Env.properties().get(key)); } private void mustBeRoot() { RapidoidEnv.touch(); U.must(isRoot, "Must be Config's root!"); } @Override public ConfigImpl root() { return root; } @Override public Config parent() { if (isRoot) return null; List<String> parentsKeys = baseKeys.subList(0, baseKeys.size() - 1); return parentsKeys.isEmpty() ? root : root.sub(parentsKeys); } @Override public List<String> keys() { return baseKeys; } @Override public Map<String, String> toFlatMap() { makeSureIsInitialized(); Map<String, String> flatMap = U.map(); Map<String, Object> map = toMap(); traverseToFlat(map, U.list(keys()), flatMap); return flatMap; } private static void traverseToFlat(Map<String, Object> map, List<String> keys, Map<String, String> flatMap) { for (Map.Entry<String, Object> e : map.entrySet()) { String key = e.getKey(); Object val = e.getValue(); if (val instanceof Map<?, ?>) { Map<String, Object> mapVal = (Map<String, Object>) val; List<String> keys2 = U.list(keys); keys2.add(key); traverseToFlat(mapVal, keys2, flatMap); } else { flatMap.put(U.join(".", keys) + "." + key, String.valueOf(val)); } } } @Override public Properties toProperties() { makeSureIsInitialized(); Properties props = new Properties(); props.putAll(toFlatMap()); return props; } @Override public ConfigAlternatives or(BasicConfig alternative) { return new ConfigAlternatives(this, alternative); } @Override public String getFilenameBase() { return base.getFilenameBase(); } @Override public Config setFilenameBase(String filenameBase) { mustBeRoot(); if (base.setFilenameBase(filenameBase)) { invalidate(); // clear to apply changes } return this; } @Override public String getPath() { return base.getPath(); } @Override public Config setPath(String path) { mustBeRoot(); if (base.setPath(path)) { invalidate(); // clear to apply changes } if (new File(path).isAbsolute()) { Msc.watchForChanges(path, new Operation<String>() { @Override public void execute(String filename) throws Exception { onFileSystemChange(filename); } }); } return this; } @Override public void applyTo(Object target) { makeSureIsInitialized(); Beany.update(target, toMap()); } private void makeSureIsInitialized() { if (base.initializing) { return; } if (!base.initialized) { synchronized (this) { if (!base.initialized) { base.initializing = true; root.initialize(); base.initialized = true; } } } } protected synchronized void initialize() { mustBeRoot(); List<String> loaded = U.list(); base.initial.putAll(Env.argsAsMap()); overrideByEnv(); if (useBuiltInDefaults()) { ConfigLoaderUtil.loadBuiltInConfig(this, loaded); } boolean useConfigFiles = U.notEmpty(getFilenameBase()); if (useConfigFiles) { ConfigLoaderUtil.loadConfig(this, loaded); } overrideByEnv(); Conf.applyConfig(this); if (!loaded.isEmpty()) { Log.info("Loaded configuration", "namespace", getFilenameBase(), "!files", loaded); } else { if (useConfigFiles) { Log.warn("Didn't find any configuration files", "path", getPath()); } } } private void overrideByEnv() { base.applyInitialConfig(this); } @Override public boolean useBuiltInDefaults() { return base.useBuiltInDefaults(); } @Override public boolean isInitialized() { RapidoidEnv.touch(); return base.initialized; } @Override public ConfigChanges getChangesSince(Config previousConfig) { Map<String, Object> prevMap; boolean initial = previousConfig == null; if (!initial) { prevMap = isRoot ? previousConfig.toMap() : previousConfig.sub(keys()).toMap(); } else { prevMap = U.map(); } return ConfigChanges.from(keys(), prevMap, toMap(), initial); } @Override public synchronized void addChangeListener(Operation<ConfigChanges> configChangeListener) { ConfigChangeListener listener = new ConfigChangeListener(keys(), configChangeListener); base.configChangesListeners.add(listener); // for the first time, call the listener with the current configuration as initial root().notifyChangeListener(listener, null); } @Override public synchronized void removeChangeListener(Operation<ConfigChanges> configChangeListener) { base.configChangesListeners.remove(new ConfigChangeListener(keys(), configChangeListener)); } private synchronized void onFileSystemChange(String filename) { mustBeRoot(); if (filename.endsWith(".yaml") || filename.endsWith(".yml") || filename.endsWith(".json")) { Log.info("Detected configuration file changes", "file", filename); reloadAndProcessChanges(); } } private void reloadAndProcessChanges() { mustBeRoot(); Set<ConfigChangeListener> listeners = U.set(base.configChangesListeners); if (listeners.isEmpty()) return; ConfigImpl previousConfig = new ConfigImpl(Coll.deepCopyOf(toMap())); // reload the configuration invalidate(); makeSureIsInitialized(); // notify listeners for (ConfigChangeListener listener : listeners) { notifyChangeListener(listener, previousConfig); } } private void notifyChangeListener(ConfigChangeListener listener, Config previousConfig) { mustBeRoot(); Config cfg = U.notEmpty(listener.keys) ? sub(listener.keys) : this; ConfigChanges changes = cfg.getChangesSince(previousConfig); if (changes.count() > 0) { try { listener.operation.execute(changes); } catch (Exception e) { Log.error("Error occurred in a configuration changes listener!", e); } } } @Override public Config defaultOrCustom(String name) { U.must(U.notEmpty(name), "The configuration name cannot be empty! Use name 'default' for the default configuration."); if (name.equalsIgnoreCase("default")) return this; String configKey = U.last(keys()) + "-" + name; return parent().sub(configKey); } }