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}