package org.javalite.app_config;
import org.javalite.common.Convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* This class allows configuration of applications for different deployment environments, such as development, test, staging, production, etc.
* Configuration is done either with property files on the classpath, or on a file system.
*
* <h2>1. Classpath configuration</h2>
*
* Applications could have environment-specific files, whose names follow this pattern:
* <code>name.properties</code>, where <code>name</code> is a name of a deployment environment, such as "development",
* "staging", "production", etc.
* You can also provide a global file, properties from which will be loaded in all environments: <code>global.properties</code>.
*
* <p></p>
*
* In all cases the files need to be on the classpath in package <code>/app_config</code>.
* <p></p>
*
* Environment-specific file will have an "environment" part of the file name match to an environment variable called "ACTIVE_ENV".
* Such configuration is easy to achieve in Unix shell:
*
* <p></p>
*
* <code>
* export ACTIVE_ENV=test
* </code>
*
* <p></p>
*
* If environment variable <code>ACTIVE_ENV</code> is missing, it defaults to "development".
*
* <p>
* You can also provide an environment as a system property <code>active_env</code>. System property overrides environment
* variable <code>ACTIVE_ENV</code>
* </p>
* <h3>Example:</h3>
* If there are four files packed into a <code>/app_config</code> package:
*
* <ul>
* <li>global.properties</li>
* <li>development.properties</li>
* <li>staging.properties</li>
* <li>production.properties</li>
* </ul>
* And the <code>ACTIVE_ENV=staging</code>, then properties will be loaded from the following files:
* <ul>
* <li>global.properties</li>
* <li>staging.properties</li>
* </ul>
*
* <h2>2. File configuration</h2>
*
* In addition to properties on classpath, you can also specify a single file for properties to loaded from a file system.
* Use a system property with a full path to a file like:
*
* <pre>
* java -cp $CLASSPATH com.myproject.Main -Dapp_config.properties=/opt/directory1/myproject.properties
* </pre>
*
* <blockquote><strong>The file-based configuration overrides classpath one. If you have a property defined in both,
* the classpath configuration will be completely ignored and the file property will be used.</strong></blockquote>
*
* @author Igor Polevoy
*/
public class AppConfig implements Map<String, String> {
private static Logger LOGGER = LoggerFactory.getLogger(AppConfig.class);
private static HashMap<String, Property> props;
private static final String activeEnv;
static {
String env = System.getenv("ACTIVE_ENV");
if (env == null) {
LOGGER.warn("Environment variable 'ACTIVE_ENV' not found, defaulting to 'development'");
env = "development";
}
activeEnv = env;
init();
}
public static synchronized void init() {
if (!isInited()){
reload();
}
}
public static void reload(){
try {
props = new HashMap<>();
loadFromClasspath();
String propName = "app_config.properties";
if(System.getProperties().containsKey(propName)){
loadFromFileSystem(System.getProperty(propName));
}
} catch (ConfigInitException e) {
throw e;
}catch (Exception e){
throw new ConfigInitException(e);
}
}
private static void loadFromFileSystem(String filePath) throws MalformedURLException {
File f = new File(filePath);
if(!f.exists() || f.isDirectory()){
throw new ConfigInitException("failed to find file: " + filePath);
}
registerProperties(f.toURI().toURL());
}
private static void loadFromClasspath(){
URL globalUrl = AppConfig.class.getResource("/app_config/global.properties");
if (globalUrl != null) {
registerProperties(globalUrl);
}
//get env - specific file, first from a system property, than from env var.
String activeEnv = System.getProperty("active_env");
if (activeEnv == null) {
activeEnv = System.getenv("ACTIVE_ENV");
}
if (activeEnv == null) {
LOGGER.warn("Environment variable 'ACTIVE_ENV' not found, defaulting to 'development'");
activeEnv = "development";
}
String file = "/app_config/" + activeEnv + ".properties";
URL url = AppConfig.class.getResource(file);
if (url == null) {
LOGGER.warn("Property file not found: '" + file + "'");
} else {
registerProperties(url);
}
}
private static boolean isInited() {
return props != null;
}
private static void registerProperties(URL url) {
LOGGER.info("Registering properties from: " + url.getPath());
Properties temp = new Properties();
try {
temp.load(url.openStream());
} catch (IOException e) {
throw new ConfigInitException(e);
}
Enumeration keys = temp.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String value = temp.getProperty(key);
Property property = new Property(key, value, url.getPath());
Property previous = props.put(key, property);
if (previous != null) {
LOGGER.warn("\n************************************************************\n"
+ "Duplicate property defined. Property: '" + key + "' found in files: \n"
+ previous.getPropertyFile() + ", \n" + url.getPath()
+ "\nUsing value '" + property.getValue() + "' from:\n"
+ property.getPropertyFile()
+ "\n************************************************************");
}
}
}
/**
* Sets a property in memory. If property exists, it will be overwritten, if not, a new one will be created.
*
* @param name - name of property
* @param value - value of property
* @return old value
*/
public static String setProperty(String name, String value) {
String val = null;
if(props.containsKey(name)){
val = props.get(name).getValue();
}
props.put(name, new Property(name, value, "dynamically added"));
LOGGER.warn("Temporary overriding property: " + name + ". Old value: " + val + ". New value: " + value);
return val;
}
/**
* Returns property instance corresponding to key.
*
* @param key key for property.
* @return Property for this key.
*/
public static Property getAsProperty(String key) {
if (!isInited()) {
init();
}
return props.get(key);
}
/**
* Returns property value for a key.
*
* @param key key of property.
* @return value for this key, <code>null</code> if not found.
*/
public static String getProperty(String key) {
if (!isInited()) {
init();
}
Property p = props.get(key);
return p == null ? null : p.getValue();
}
/**
* Gets property, synonym for {@link #getProperty(String)}.
*
* @param key key of property
* @return property value
*/
public static String p(String key) {
return getProperty(key);
}
public static Map<String, String> getAllProperties() {
if (!isInited()) {
init();
}
HashMap<String, String> plainProps = new HashMap<>();
for (String name: props.keySet()) {
plainProps.put(name, props.get(name).getValue());
}
return plainProps;
}
/////////// Implementation of Map interface below ///////////////////
@Override
public int size() {
return props.size();
}
@Override
public boolean isEmpty() {
return props.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return props.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return props.containsValue(value);
}
@Override
public String get(Object key) {
return p(key.toString());
}
@Override
public String put(String key, String value) {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public String remove(Object key) {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public void clear() {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public Set<String> keySet() {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public Collection<String> values() {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
@Override
public Set<Entry<String, String>> entrySet() {
throw new UnsupportedOperationException("Operation not supported, not a real map");
}
/**
* Returns current environment name as defined by environment variable <code>ACTIVE_ENV</code>.
*
* @return current environment name as defined by environment variable <code>ACTIVE_ENV</code>.
*/
public static String activeEnv() {
return activeEnv;
}
/**
* Checks if running in a context of a test by checking of a presence of a class <code>org.junit.Test</code> on classpath.
*
* @return true if class <code>org.junit.Test</code> is on classpath, otherwise returns <code>false</code>
*/
public static boolean isInTestMode(){
return AppConfig.class.getResource("/org/junit/Test.class") != null;
}
/**
* @return true if environment name as defined by environment variable <code>ACTIVE_ENV</code> is "testenv".
*/
public static boolean isInTestEnv() {
return "testenv".equals(activeEnv());
}
/**
* @return true if environment name as defined by environment variable <code>ACTIVE_ENV</code> is "production".
*/
public static boolean isInProduction() {
return "production".equals(activeEnv());
}
/**
* @return true if environment name as defined by environment variable <code>ACTIVE_ENV</code> is "development".
*/
public static boolean isInDevelopment() {
return "development".equals(activeEnv());
}
/**
* @return true if environment name as defined by environment variable <code>ACTIVE_ENV</code> is "staging".
*/
public static boolean isInStaging() {
return "staging".equals(activeEnv());
}
/**
* Returns all keys that start with a prefix
*
* @param prefix prefix for properties.
*/
public static List<String> getKeys(String prefix) {
List<String> res = new ArrayList<>();
for(String key: props.keySet()){
if(key.startsWith(prefix)){
res.add(key);
}
}
return res;
}
/**
* Return all numbered properties with a prefix. For instance if there is a file:
* <pre>
* prop.1=one
* prop.2=two
* </pre>
*
* .. and this method is called:
* <pre>
* List<String> props = AppConfig.getProperties("prop");
* </pre>
* then the resulting list will have all properties starting from <code>prop</code>.
* This method presumes consecutive numbers in the suffix.
*
* @param prefix prefix of numbered properties.
* @return list of property values.
*/
public static List<String> getProperties(String prefix) {
List<String> res = new ArrayList<>();
prefix += ".";
for (int i = 1; ; i++) {
String prop = p(prefix + i);
if (prop == null)
return res;
res.add(prop);
}
}
/**
* Read property as <code>Integer</code>.
*
* @param propertyName name of property.
* @return property as <code>Integer</code>.
*/
public static Integer pInteger(String propertyName){
return Convert.toInteger(p(propertyName));
}
/**
* Read property as <code>Double</code>.
*
* @param propertyName name of property.
* @return property as <code>Double</code>.
*/
public static Double pDouble(String propertyName){
return Convert.toDouble(p(propertyName));
}
/**
* Read property as <code>Float</code>.
*
* @param propertyName name of property.
* @return property as <code>Float</code>.
*/
public static Float pFloat(String propertyName){
return Convert.toFloat(p(propertyName));
}
/**
* Read property as <code>Boolean</code>.
*
* @param propertyName name of property.
* @return property as <code>Boolean</code>.
*/
public static Boolean pBoolean(String propertyName){
return Convert.toBoolean(p(propertyName));
}
}