package org.yamcs;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.LogManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.utils.TimeEncoding;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;
/**
* This class loads yamcs configurations. There are a number of "subsystems",
* each using a corresponding subsystem.yaml file
*
* There are three places where a configuration file is looked up in order:
* - in the prefix/file.yaml via the classpath if the prefix is set in the setup method (used in the unittests)
* - in the userConfigDirectory .yamcs/etc/file.yaml
* - in the file.yaml via the classpath.
*
* @author nm
*/
@SuppressWarnings("rawtypes")
public class YConfiguration {
Map<String, Object> root;
static String userConfigDirectory; //This is used by the users to overwrite
static YConfigurationResolver resolver = new YConfigurationResolver();
private static Map<String, YConfiguration> configurations=new HashMap<>();
static Logger log=LoggerFactory.getLogger(YConfiguration.class.getName());
static String prefix=null;
//keeps track of the configuration path so meaningful error messages can be printed
//the path is someting like filename->key1->subkey2[3]->...
static private IdentityHashMap<Object, String> confPath=new IdentityHashMap<>();
@SuppressWarnings("unchecked")
private YConfiguration(String subsystem) throws IOException, ConfigurationException {
Yaml yaml=new Yaml();
String filename=subsystem+".yaml";
try {
Object o=yaml.load(resolver.getConfigurationStream("/"+filename));
if(o==null) {
o=new HashMap<String, Object>(); //config file is empty, not an error
} else if(!(o instanceof Map<?, ?>)) {
throw new ConfigurationException(filename, "top level structure must be a map and not a "+o);
}
root=(Map<String, Object>)o;
confPath.put(root, filename);
} catch (YAMLException e) {
throw new ConfigurationException(filename, e.toString(), e);
}
}
/**
* If configPrefix is not null, sets up the configuration to search the classpath for files like "configPrefix/xyz.properties"
*
* Also sets up the TimeEncoding configuration
*
* @param configPrefix
* @throws ConfigurationException
*/
public synchronized static void setup(String configPrefix) throws ConfigurationException {
prefix=configPrefix;
configurations.clear();//forget any known config (useful in the maven unit tests called in the same VM)
if(System.getenv("YAMCS_DAEMON")==null) {
userConfigDirectory=System.getProperty("user.home")+File.separatorChar+".yamcs";
File logDir = new File(userConfigDirectory+File.separatorChar+"log");
if (!logDir.exists()) {
if (logDir.mkdirs()) {
System.err.println("Created directory: "+logDir);
} else {
System.err.println("Cannot create directory: "+logDir);
}
}
System.getProperties().put("cacheDirectory", userConfigDirectory+File.separatorChar);
} else {
String yamcsDirectory=System.getProperty("user.home");
System.getProperties().put("cacheDirectory", yamcsDirectory+File.separatorChar+"cache"+File.separatorChar);
userConfigDirectory=yamcsDirectory+File.separatorChar+"etc";
}
if(System.getProperty("java.util.logging.config.file")==null) {
try {
LogManager.getLogManager().readConfiguration(resolver.getConfigurationStream("/logging.properties"));
} catch (Exception e) {
//do nothing, the default java builtin logging is used
}
}
TimeEncoding.setUp();
}
/**
* calls setup(null)
*
* @throws ConfigurationException
*/
public synchronized static void setup() throws ConfigurationException {
setup(null);
}
/**
* Loads (if not already loaded) and returns a configuration corresponding to a file <subsystem>.yaml
*
* This method does not reload the configuration file if it has changed.
*
* @param subsystem
* @return the loaded configuration
* @throws ConfigurationException if the configuration file could not be found or not loaded (e.g. error in yaml formatting)
*/
public synchronized static YConfiguration getConfiguration(String subsystem) throws ConfigurationException {
if(subsystem.contains("..") || subsystem.contains("/")) throw new ConfigurationException("Invalid subsystem '"+subsystem+"'");
YConfiguration c = configurations.get(subsystem);
if(c==null) {
try {
c=new YConfiguration(subsystem);
} catch (IOException e){
throw new ConfigurationException("Cannot load configuration for subsystem "+subsystem+": "+e);
}
configurations.put(subsystem, c);
}
return c;
}
/**
* Loads and returns a configuration corresponding to a file <subsystem>.yaml
*
* This method reloads the configuration file always.
*
* @param subsystem
* @param reload
* @return the loaded configuration
* @throws ConfigurationException if the configuration file could not be found or not loaded (e.g. error in yaml formatting)
*/
public synchronized static YConfiguration getConfiguration(String subsystem, boolean reload) throws ConfigurationException {
if(reload) {
YConfiguration c = configurations.get(subsystem);
if (c != null) {
configurations.remove(subsystem);
}
}
return getConfiguration(subsystem);
}
public static boolean isDefined(String subsystem) throws ConfigurationException {
try {
getConfiguration(subsystem);
return true;
} catch (ConfigurationNotFoundException e) {
return false;
}
}
public static String getGlobalProperty(String key) {
return System.getProperty(key);
}
static private void checkKey(Map m, String key) throws ConfigurationException {
if(!m.containsKey(key)) throw new ConfigurationException(confPath.get(m), "cannot find a mapping for key '"+key+"'");
else if(m.get(key)==null) throw new ConfigurationException(confPath.get(m), key+" exists but is null");
}
public boolean containsKey(String key) {
return root.containsKey(key);
}
public boolean containsKey(String key, String key1) throws ConfigurationException {
if(!root.containsKey(key)) return false;
Map<String, Object> m = getMap(key);
return m.containsKey(key1);
}
/**
* returns the first entry in the config file if it's a map. Otherwise throws an error
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getFirstMap() throws ConfigurationException {
Object o=root.values().iterator().next();
if(o instanceof Map) {
return (Map<String, Object>) o;
} else {
throw new ConfigurationException("the first entry in the config is of type "+o.getClass()+" and not Map");
}
}
/**
* returns the first entry(key) in the config file.
* @return
*/
public String getFirstEntry() throws ConfigurationException {
return root.keySet().iterator().next();
}
public Set<String> getKeys() {
return root.keySet();
}
private static String getUnqualfiedClassName(Object o) {
String name=o.getClass().getName();
if (name.lastIndexOf('.') > 0) {
name = name.substring(name.lastIndexOf('.')+1); // Map$Entry
}
// The $ can be converted to a .
name = name.replace('$', '.'); // Map.Entry
return name;
}
/****************************** Map configs*/
@SuppressWarnings("unchecked")
static public Map<String, Object> getMap(Map<String, Object> m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof Map) {
Map<String, Object> m1=(Map)o;
if(confPath.containsKey(m1)) {
confPath.put(m1, confPath.get(m)+"->"+key);
}
return m1;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+o.getClass().getCanonicalName()+" and not Map");
}
}
public Map<String, Object> getMap(String key) throws ConfigurationException {
return getMap(root, key);
}
public Map<String, Object> getMap(String key, String key1) throws ConfigurationException {
Map<String, Object> m=getMap(key);
return getMap(m, key1);
}
/***************************String configs*/
/**
* Returns m.get(key) if it exists and is of type string, otherwise throws an exception
* @param m
* @param key
* @return
* @throws ConfigurationException
*/
static public String getString(Map m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof String) {
return (String)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not String");
}
}
static public String getString(Map m, String key, String defaultValue) throws ConfigurationException {
if(m.containsKey(key)) {
return getString(m, key);
} else {
return defaultValue;
}
}
public String getString(String key) throws ConfigurationException {
return getString(root, key);
}
/*
* The key has to point to a map that contains the subkey that points to a string
*/
public String getString(String key, String subkey) throws ConfigurationException {
Map<String, Object> m=getMap(key);
return getString(m, subkey);
}
public String getString(String key, String key1, String key2) throws ConfigurationException {
Map<String, Object> m=getMap(key,key1);
return getString(m, key2);
}
@SuppressWarnings("unchecked")
public <T> List<T> getList(String key) throws ConfigurationException {
return (List<T>) getList(root, key);
}
/*****************List configs*/
/*
* The key has to point to a list
*/
@SuppressWarnings("unchecked")
static public <T> List<T> getList(Map<String, Object> m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof List) {
List l=(List) o;
String parentPath=confPath.get(m);
for(int i=0; i<l.size();i++) {
Object o1=l.get(i);
if(!confPath.containsKey(o1)) {
confPath.put(o1, parentPath+"->"+key+"["+i+"]");
}
}
return l;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not List");
}
}
public <T> List<T> getList(String key, String key1, String key2) throws ConfigurationException {
Map<String, Object> m=getMap(key,key1);
return getList(m, key2);
}
public <T> List<T> getList(String key, String key1) throws ConfigurationException {
Map<String, Object> m=getMap(key);
return getList(m, key1);
}
/**********************Boolean configs*/
/**
* Returns m.get(key) if it exists and is of type boolean,
* if m.get(key) exists and is not boolean, throw an exception.
* if m.get(key) does not exist, return the default value.
* @param m
* @param key
* @param defaultValue - the default value to return if m.get(key) does not exist.
* @return the boolean config value
* @throws ConfigurationException
*/
static public boolean getBoolean(Map<String, Object> m, String key, boolean defaultValue) throws ConfigurationException {
Object o=m.get(key);
if(o!=null){
if (o instanceof Boolean) {
return (Boolean)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Boolean (use true or false without quotes)");
}
} else {
return defaultValue;
}
}
static public boolean getBoolean(Map<String, Object> m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof Boolean) {
return (Boolean)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Boolean (use true or false without quotes)");
}
}
public boolean getBoolean(String key) throws ConfigurationException {
return getBoolean(root,key);
}
public boolean getBoolean(String key, String key1) throws ConfigurationException {
Map<String, Object> m=getMap(key);
return getBoolean(m, key1);
}
public boolean getBoolean(String key, String key1, String key2) throws ConfigurationException {
Map<String, Object> m=getMap(key,key1);
return getBoolean(m, key2);
}
public boolean getBoolean(String key, boolean defaultValue) {
return getBoolean(root, key, defaultValue);
}
/********************** int configs */
static public int getInt(Map<String, Object> m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof Integer) {
return (Integer)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Integer");
}
}
/**
* return the m.get(key) as an int if it's present or v if it is not.
*
* If the key is present but the value is not an integer, a ConfigurationException is thrown.
* @param m
* @param key
* @param defaultValue
* @return the value from the map or the passed value if the map does not contain the key
* @throws ConfigurationException if the key is present but it's not an int
*/
static public int getInt(Map<String, Object> m, String key, int defaultValue) throws ConfigurationException {
if(!m.containsKey(key)) return defaultValue;
Object o=m.get(key);
if(o instanceof Integer) {
return (Integer)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Integer");
}
}
static public long getLong(Map<String, Object> m, String key) throws ConfigurationException {
checkKey(m, key);
Object o=m.get(key);
if(o instanceof Integer) {
return (Integer)o;
} else if(o instanceof Long) {
return (Long)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Integer or Long");
}
}
/**
* return the m.get(key) as an long if it's present or v if it is not.
*
* @param m
* @param key
* @param v
* @return the value from the map or the passed value if the map does not contain the key
* @throws ConfigurationException if the key is present but it's not an long
*/
static public long getLong(Map<String, Object> m, String key, long v) throws ConfigurationException {
if(!m.containsKey(key)) return v;
Object o=m.get(key);
if(o instanceof Integer) {
return (Integer)o;
} else if(o instanceof Long) {
return (Long)o;
} else {
throw new ConfigurationException(confPath.get(m), "mapping for key '"+key+"' is of type "+getUnqualfiedClassName(o)+" and not Integer or Long");
}
}
public int getInt(String key) throws ConfigurationException {
return getInt(root,key);
}
public int getInt(String key, String key1) throws ConfigurationException {
Map<String, Object> m=getMap(key);
return getInt(m, key1);
}
public int getInt(String key, String key1, int defaultValue) throws ConfigurationException {
if(!root.containsKey(key)) return defaultValue;
Map<String, Object> m = getMap(key);
return getInt(m, key1, defaultValue);
}
public boolean isList(String key) {
return isList(root, key);
}
public static boolean isList(Map m, String key) {
checkKey(m, key);
Object o = m.get(key);
return (o instanceof List);
}
public static void setResolver(YConfigurationResolver resolver) {
YConfiguration.resolver = resolver;
}
/**
* Default config file resolver
*/
public static class YConfigurationResolver {
public InputStream getConfigurationStream(String name) throws ConfigurationException {
InputStream is;
if(prefix!=null) {
if((is=YConfiguration.class.getResourceAsStream("/"+prefix+name))!=null) {
log.debug("Reading "+new File(YConfiguration.class.getResource("/"+prefix+name).getFile()).getAbsolutePath());
return is;
}
}
//see if the users has an own version of the file
File f=new File(userConfigDirectory+name);
if(f.exists()) {
try {
is=new FileInputStream(f);
log.debug("Reading "+f.getAbsolutePath());
return is;
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
if((is=YConfiguration.class.getResourceAsStream(name))==null) {
throw(new ConfigurationNotFoundException("Cannot find resource "+name));
}
log.debug("Reading "+new File(YConfiguration.class.getResource(name).getFile()).getAbsolutePath());
return is;
}
}
/**
* Introduced to be able to detect when a configuration file was not
* specified (as opposed to when there's a validation error inside). The
* current default behaviour of Yamcs is to throw an error when
* getConfiguration(String subystem) is called and the resource does not
* exist.
*/
public static class ConfigurationNotFoundException extends ConfigurationException {
private static final long serialVersionUID = 1L;
public ConfigurationNotFoundException(String message) {
super(message);
}
}
}