001package io.avaje.config.load; 002 003import io.avaje.config.Configuration; 004import org.slf4j.Logger; 005import org.slf4j.LoggerFactory; 006 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.util.Enumeration; 011import java.util.Properties; 012import java.util.regex.Pattern; 013 014import static io.avaje.config.load.Loader.Source.FILE; 015import static io.avaje.config.load.Loader.Source.RESOURCE; 016 017/** 018 * Loads the configuration from known/expected locations. 019 * <p> 020 * Defines the loading order of resources and files. 021 * </p> 022 */ 023public class Loader { 024 025 private static final Logger log = LoggerFactory.getLogger(Loader.class); 026 027 private static final Pattern SPLIT_PATHS = Pattern.compile("[\\s,;]+"); 028 029 /** 030 * Return the Expression evaluator using the given properties. 031 */ 032 public static Configuration.ExpressionEval evalFor(Properties properties) { 033 return new CoreExpressionEval(properties); 034 } 035 036 enum Source { 037 RESOURCE, 038 FILE 039 } 040 041 private final LoadContext loadContext = new LoadContext(); 042 043 private YamlLoader yamlLoader; 044 045 public Loader() { 046 initYamlLoader(); 047 } 048 049 /** 050 * Provides properties by reading known locations. 051 * <p> 052 * <h3>Main configuration</h3> 053 * <p> 054 * <p>Firstly loads from main resources</p> 055 * <pre> 056 * - application.properties 057 * - application.yaml 058 * </pre> 059 * <p> 060 * <p>Then loads from local files</p> 061 * <pre> 062 * - application.properties 063 * - application.yaml 064 * </pre> 065 * <p> 066 * <p>Then loads from environment variable <em>PROPS_FILE</em></p> 067 * <p>Then loads from system property <em>props.file</em></p> 068 * <p>Then loads from <em>load.properties</em></p> 069 * <p> 070 * <h3>Test configuration</h3> 071 * <p> 072 * Once the main configuration is read it will try to read common test configuration. 073 * This will only be successful if the test resources are available (i.e. running tests). 074 * </p> 075 * <p>Loads from test resources</p> 076 * <pre> 077 * - application-test.properties 078 * - application-test.yaml 079 * </pre> 080 */ 081 public Properties load() { 082 loadEnvironmentVars(); 083 loadLocalFiles(); 084 return eval(); 085 } 086 087 private void initYamlLoader() { 088 if (!"true".equals(System.getProperty("skipYaml"))) { 089 try { 090 Class<?> exists = Class.forName("org.yaml.snakeyaml.Yaml"); 091 if (exists != null) { 092 yamlLoader = new YamlLoader(loadContext); 093 } 094 } catch (ClassNotFoundException e) { 095 // ignored, no yaml loading 096 } 097 } 098 } 099 100 void loadEnvironmentVars() { 101 loadContext.loadEnvironmentVars(); 102 } 103 104 /** 105 * Load from local files and resources. 106 */ 107 void loadLocalFiles() { 108 109 loadMain(RESOURCE); 110 // external file configuration overrides the resources configuration 111 loadMain(FILE); 112 loadViaSystemProperty(); 113 loadViaIndirection(); 114 115 // test configuration (if found) overrides main configuration 116 // we should only find these resources when running tests 117 if (!loadTest()) { 118 loadLocalDev(); 119 loadViaCommandLineArgs(); 120 } 121 } 122 123 private void loadViaCommandLineArgs() { 124 final String rawArgs = System.getProperty("sun.java.command"); 125 if (rawArgs != null) { 126 loadViaCommandLine(rawArgs.split(" ")); 127 } 128 } 129 130 void loadViaCommandLine(String[] args) { 131 for (int i = 0; i < args.length; i++) { 132 String arg = args[i]; 133 if (arg.startsWith("-P") || arg.startsWith("-p")) { 134 if (arg.length() == 2 && i < args.length - 1) { 135 // next argument expected to be a properties file paths 136 i++; 137 loadCommandLineArg(args[i]); 138 } else { 139 // no space between -P and properties file paths 140 loadCommandLineArg(arg.substring(2)); 141 } 142 } 143 } 144 } 145 146 private void loadCommandLineArg(String arg) { 147 if (isValidExtension(arg)) { 148 loadViaPaths(arg); 149 } 150 } 151 152 private boolean isValidExtension(String arg) { 153 return arg.endsWith(".yaml") || arg.endsWith(".yml") || arg.endsWith(".properties"); 154 } 155 156 /** 157 * Provides a way to override properties when running via main() locally. 158 */ 159 private void loadLocalDev() { 160 File localDev = new File(System.getProperty("user.home"), ".localdev"); 161 if (localDev.exists()) { 162 final String appName = loadContext.getAppName(); 163 if (appName != null) { 164 final String prefix = localDev.getAbsolutePath() + File.separator + appName; 165 loadFileWithExtensionCheck(prefix + ".yaml"); 166 loadFileWithExtensionCheck(prefix + ".properties"); 167 } 168 } 169 } 170 171 /** 172 * Load test configuration. 173 * 174 * @return true if test properties have been loaded. 175 */ 176 private boolean loadTest() { 177 int before = loadContext.size(); 178 loadProperties("application-test.properties", RESOURCE); 179 loadYaml("application-test.yaml", RESOURCE); 180 if (loadYaml("application-test.yml", RESOURCE)) { 181 log.warn("Please rename application-test.yml to application-test.yaml - Using yml suffix (rather than yaml) is deprecated."); 182 } 183 if (loadProperties("test-ebean.properties", RESOURCE)) { 184 log.warn("Loading properties from test-ebean.properties is deprecated. Please migrate to application-test.yaml or application-test.properties instead."); 185 } 186 return loadContext.size() > before; 187 } 188 189 /** 190 * Load configuration defined by a <em>load.properties</em> entry in properties file. 191 */ 192 private void loadViaIndirection() { 193 194 String paths = loadContext.indirectLocation(); 195 if (paths != null) { 196 loadViaPaths(paths); 197 } 198 } 199 200 private void loadViaPaths(String paths) { 201 for (String path : splitPaths(paths)) { 202 loadFileWithExtensionCheck(loadContext.eval(path)); 203 } 204 } 205 206 int size() { 207 return loadContext.size(); 208 } 209 210 String[] splitPaths(String location) { 211 return SPLIT_PATHS.split(location); 212 } 213 214 /** 215 * Load the main configuration for the given source. 216 */ 217 private void loadMain(Source source) { 218 loadYaml("application.yaml", source); 219 if (loadYaml("application.yml", source)) { 220 log.warn("Please rename application.yml to application.yaml - Using yml suffix (rather than yaml) is deprecated."); 221 } 222 loadProperties("application.properties", source); 223 if (loadProperties("ebean.properties", source)) { 224 log.warn("Loading properties from ebean.properties is deprecated. Please migrate to use application.yaml or application.properties instead."); 225 } 226 } 227 228 private void loadViaSystemProperty() { 229 String fileName = System.getenv("PROPS_FILE"); 230 if (fileName == null) { 231 fileName = System.getProperty("props.file"); 232 if (fileName != null) { 233 loadFileWithExtensionCheck(fileName); 234 } 235 } 236 } 237 238 void loadFileWithExtensionCheck(String fileName) { 239 if (fileName.endsWith("yaml") || fileName.endsWith("yml")) { 240 loadYaml(fileName, FILE); 241 } else if (fileName.endsWith("properties")) { 242 loadProperties(fileName, FILE); 243 } else { 244 throw new IllegalArgumentException("Expecting only yaml or properties file but got [" + fileName + "]"); 245 } 246 } 247 248 /** 249 * Evaluate all the configuration entries and return as properties. 250 */ 251 Properties eval() { 252 return loadContext.evalAll(); 253 } 254 255 boolean loadYaml(String resourcePath, Source source) { 256 if (yamlLoader != null) { 257 try { 258 try (InputStream is = resource(resourcePath, source)) { 259 if (is != null) { 260 yamlLoader.load(is); 261 return true; 262 } 263 } 264 } catch (Exception e) { 265 throw new RuntimeException("Error loading yaml properties - " + resourcePath, e); 266 } 267 } 268 return false; 269 } 270 271 boolean loadProperties(String resourcePath, Source source) { 272 try { 273 try (InputStream is = resource(resourcePath, source)) { 274 if (is != null) { 275 loadProperties(is); 276 return true; 277 } 278 } 279 } catch (IOException e) { 280 throw new RuntimeException("Error loading properties - " + resourcePath, e); 281 } 282 return false; 283 } 284 285 private InputStream resource(String resourcePath, Source source) { 286 return loadContext.resource(resourcePath, source); 287 } 288 289 private void loadProperties(InputStream is) throws IOException { 290 Properties properties = new Properties(); 291 properties.load(is); 292 put(properties); 293 } 294 295 private void put(Properties properties) { 296 Enumeration<?> enumeration = properties.propertyNames(); 297 while (enumeration.hasMoreElements()) { 298 String key = (String) enumeration.nextElement(); 299 String property = properties.getProperty(key); 300 loadContext.put(key, property); 301 } 302 } 303 304}