001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.settings;
019
020import com.moandjiezana.toml.Toml;
021import io.stallion.Context;
022import io.stallion.boot.CommandOptionsBase;
023import io.stallion.reflection.PropertyUtils;
024import io.stallion.secrets.SecretsVault;
025import io.stallion.services.Log;
026import io.stallion.settings.childSections.SecretsSettings;
027import io.stallion.settings.childSections.SettingsSection;
028
029import java.io.File;
030import java.lang.reflect.Field;
031import java.util.Collections;
032import java.util.List;
033import java.util.Map;
034
035import static io.stallion.utils.Literals.empty;
036import static io.stallion.utils.Literals.or;
037
038
039public class SettingsLoader  {
040
041    private Map<String, String> secrets;
042
043    public <T extends ISettings> T loadSettings(String baseName, Class<T> settingsClass) {
044        String env = Context.settings().getEnv();
045        return loadSettings(env, Context.settings().getTargetFolder(), baseName, settingsClass);
046    }
047
048    public <T extends ISettings> T loadSettings(String env, String targetFolder, String baseName, Class<T> settingsClass)  {
049        return loadSettings(env, targetFolder, baseName, settingsClass, null);
050    }
051
052    public <T extends ISettings> T loadSettings(String env, String targetFolder, String baseName, Class<T> settingsClass, CommandOptionsBase options)  {
053        String basePath = targetFolder + "/conf/" + baseName;
054
055        // Load the default settings
056        Log.fine("Loading default toml settings.");
057        String userName = System.getProperty("user.name");
058        if (userName == null) {
059            userName = "emptyuser";
060        }
061
062
063        // Settings load order, later settings override earlier settings
064        // 1. stallion.toml
065        // 2. stallion.user-(username).toml
066        // 3. stallion.env.toml
067        // 4. stallion.env.user-(username).toml
068        File baseFile = new File(basePath + ".toml");
069        File userFile = new File(basePath + ".user-" + userName + ".toml");
070        File envFile = new File(basePath + "." + env + ".toml");
071        File envUserFile = new File(basePath + "." + env + ".user-" + userName + ".toml");
072
073        T settings = null;
074        try {
075            settings = (T)settingsClass.newInstance();
076        } catch (Exception e) {
077            throw new RuntimeException(e);
078        }
079
080
081
082
083        Toml toml = new Toml();
084        Toml tomlOrg = null;
085        if (baseFile.exists()) {
086            Log.info("Loading base settings file: {0}", baseFile.getPath());
087            toml = new Toml().read(baseFile);
088            tomlOrg = toml;
089        }
090
091
092
093        if (envFile.exists()) {
094            Log.finer("Env file exists {0}, merging", envFile.getPath());
095            toml = new Toml(toml).read(envFile);
096        }
097
098
099        settings = toml.to(settingsClass);
100
101        // Hack because the toml library can only do one level of merging
102        // toml files. Thus, need to manually merge this third file.
103        if (envUserFile.exists()) {
104            Toml localToml = new Toml().read(envUserFile);
105            T localSettings = localToml.to(settingsClass);
106            for(Map.Entry<String, Object> entry: localToml.entrySet()) {
107                PropertyUtils.setProperty(
108                        settings, entry.getKey(),
109                        PropertyUtils.getPropertyOrMappedValue(localSettings, entry.getKey()));
110            }
111        }
112
113
114        if (settings instanceof Settings) {
115            ((Settings)settings).setEnv(env);
116            ((Settings)settings).setTargetFolder(targetFolder);
117            SecretsSettings secretsSettings = or(((Settings) settings).getSecrets(), new SecretsSettings());
118            if (empty(secretsSettings.getPassPhraseFile())) {
119                secretsSettings.setPassPhraseFile("/usr/local/etc/stallion-secrets-passphrase");
120            }
121            SecretsVault.init(targetFolder, secretsSettings);
122        }
123
124        if (options != null && settings instanceof  Settings) {
125            options.hydrateSettings((Settings)settings);
126        }
127
128
129
130
131        settings.assignDefaults();
132
133        try {
134            assignDefaultsFromAnnotations(settings);
135        } catch (Exception e) {
136            throw new RuntimeException(e);
137        }
138
139        Log.finer("Settings Loaded. {0}", settings);
140
141        return settings;
142    }
143
144    public void assignDefaultsFromAnnotations(Object settings) throws IllegalAccessException, InstantiationException {
145        Class cls = settings.getClass();
146        for(Field field: cls.getDeclaredFields()) {
147            boolean isAccessible = field.isAccessible();
148            field.setAccessible(true);
149            if (SettingsSection.class.isAssignableFrom(field.getType())) {
150                Object value = field.get(settings);
151                if (value == null) {
152                    value = field.getType().newInstance();
153                    field.set(settings, value);
154                }
155                assignDefaultsFromAnnotations(value);
156                field.setAccessible(isAccessible);
157                ((SettingsSection)value).postLoad();
158                continue;
159            }
160            SettingMeta[] metas = field.getAnnotationsByType(SettingMeta.class);
161            if (metas.length == 0) {
162                field.setAccessible(isAccessible);
163                continue;
164            }
165            SettingMeta meta = metas[0];
166            Object value = field.get(settings);
167            Class type = field.getType();
168
169            if (value instanceof String && value != null && ((String) value).startsWith("secret:::")) {
170                String secretName = ((String) value).substring(9);
171                Log.info("Load secret {0} ", secretName);
172                value = SecretsVault.getAppSecrets().getOrDefault(secretName, null);
173                field.set(settings, value);
174            }
175
176
177            if (value == null) {
178                if (!empty(meta.useField())) {
179                    field.set(settings, PropertyUtils.getProperty(settings, meta.useField()));
180                } else if (type == String.class) {
181                    field.set(settings, meta.val());
182                } else if (type == Integer.class) {
183                    field.set(settings, meta.valInt());
184                } else if (type == Long.class) {
185                    field.set(settings, meta.valLong());
186                } else if (type == Boolean.class) {
187                    field.set(settings, meta.valBoolean());
188                } else if (meta.cls() != null) {
189                    Object o = meta.cls().newInstance();
190                    field.set(settings, o);
191                } else {
192                    Log.warn("Field " + field.getName() + " on settings class " + settings.getClass() + " is null and has no matching class initializer.");
193                }
194            }
195            field.setAccessible(isAccessible);
196        }
197    }
198
199    /**
200     * Merges all values defined in as not-null in overrides, into defaults
201     * Operates recursively
202     * @param existing
203     * @param overrides
204     */
205    public void mergeObjects(Object existing, Object overrides) {
206        if (existing instanceof Map && overrides instanceof Map) {
207            Map<String, Object> prevMap = (Map<String, Object>)existing;
208            Map<String, Object> newMap = (Map<String, Object>)overrides;
209            for (Map.Entry<String, Object> item: newMap.entrySet()) {
210                prevMap.put(item.getKey(), item.getValue());
211            }
212            return;
213        } else {
214            for (Map.Entry<String, Object> entry : PropertyUtils.getProperties(overrides).entrySet()) {
215                // If the override was not defined, we continue
216                Object overrideValue = entry.getValue();
217                if (overrideValue == null) {
218                    continue;
219                }
220                if (!(overrideValue instanceof SettingsSection)) {
221                    PropertyUtils.setProperty(existing, entry.getKey(), entry.getValue());
222                    continue;
223                }
224
225                Object existingValue = PropertyUtils.getProperty(existing, entry.getKey());
226                // The existingValue is either null or the default
227                if (existingValue == null) {
228                    PropertyUtils.setProperty(existing, entry.getKey(), entry.getValue());
229                    continue;
230                }
231                // If the default never defined the section, we set it, otherwise we merge
232                if (entry.getValue() instanceof Map) {
233                    Map<String, Object> vMap = (Map<String, Object>) entry.getValue();
234                    for (Map.Entry<String, Object> subEntry : vMap.entrySet()) {
235                        Map currentMap = (Map) existingValue;
236                        currentMap.put(subEntry.getKey(), subEntry.getValue());
237                    }
238                    continue;
239                }
240                mergeObjects(existingValue, overrideValue);
241
242            }
243        }
244    }
245}