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}