/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.component;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.lang.StringUtils;
import org.joda.beans.Bean;
import org.joda.beans.MetaProperty;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.threeten.bp.ZoneId;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.OpenGammaClock;
import com.opengamma.util.PlatformConfigUtils;
import com.opengamma.util.ResourceUtils;
/**
* Manages the process of loading and starting OpenGamma components.
* <p>
* The OpenGamma logical architecture consists of a set of components.
* This class loads and starts the components based on configuration.
* The end result is a populated {@link ComponentRepository}.
* <p>
* Two types of config file format are recognized - properties and INI.
* The INI file is the primary file for loading the components, see {@link ComponentConfigIniLoader}.
* The behavior of an INI file can be controlled using properties.
* <p>
* The properties can either be specified manually before {@link #start(Resource))}
* is called or loaded by specifying a properties file instead of an INI file.
* The properties file must contain the key "MANAGER.NEXT.FILE" which is used to load the next file.
* The next file is normally the INI file, but could be another properties file.
* As such, the properties files can be chained.
* <p>
* Properties are never overwritten, thus manual properties have priority over file-based, and
* earlier file-based have priority over later file-based.
* <p>
* It is not intended that the manager is retained for the lifetime of
* the application, the repository is intended for that purpose.
*/
public class ComponentManager {
/**
* The server name property.
*/
private static final String OPENGAMMA_SERVER_NAME = "og.server.name";
/**
* The key identifying the next config file in a properties file.
*/
static final String MANAGER_NEXT_FILE = "MANAGER.NEXT.FILE";
/**
* The key identifying the entire combined set of active properties.
*/
static final String MANAGER_PROPERTIES = "MANAGER.PROPERTIES";
/**
* The key identifying the the inclusion of another file.
*/
static final String MANAGER_INCLUDE = "MANAGER.INCLUDE";
/**
* The component repository.
*/
private final ComponentRepository _repo;
/**
* The component logger.
*/
private final ComponentLogger _logger;
/**
* The component properties, updated as properties are discovered.
*/
private final ConcurrentMap<String, String> _properties = new ConcurrentHashMap<String, String>();
/**
* The component INI, updated as configuration is discovered.
*/
private ComponentConfig _configIni = new ComponentConfig();
/**
* Creates an instance that does not log.
*
* @param serverName the server name, not null
*/
public ComponentManager(String serverName) {
this(serverName, ComponentLogger.Sink.INSTANCE);
}
/**
* Creates an instance.
*
* @param serverName the server name, not null
* @param logger the logger, not null
*/
public ComponentManager(String serverName, ComponentLogger logger) {
this(serverName, new ComponentRepository(logger));
}
/**
* Creates an instance.
*
* @param serverName the server name, not null
* @param repo the repository to use, not null
*/
protected ComponentManager(String serverName, ComponentRepository repo) {
ArgumentChecker.notNull(serverName, "serverName");
ArgumentChecker.notNull(repo, "repo");
_repo = repo;
_logger = repo.getLogger();
setServerName(serverName);
}
//-------------------------------------------------------------------------
/**
* Gets the repository of components.
*
* @return the repository, not null
*/
public ComponentRepository getRepository() {
return _repo;
}
/**
* Gets the properties used while loading the manager.
* <p>
* This may be populated before calling {@link #start()} if desired.
* This is an alternative to using a separate properties file.
*
* @return the map of key-value properties which may be directly edited, not null
*/
public ConcurrentMap<String, String> getProperties() {
return _properties;
}
/**
* Gets the component INI.
*
* @return the component INI, not null
*/
public ComponentConfig getConfigIni() {
return _configIni;
}
//-------------------------------------------------------------------------
/**
* Sets the server name property.
* <p>
* This can be used as a general purpose name for the server.
*
* @return the server name, null if name not set
*/
public String getServerName() {
return getProperties().get(OPENGAMMA_SERVER_NAME);
}
/**
* Sets the server name property.
* <p>
* This can be used as a general purpose name for the server.
*
* @param serverName the server name, not null
*/
public void setServerName(String serverName) {
getProperties().put(OPENGAMMA_SERVER_NAME, serverName);
System.setProperty(OPENGAMMA_SERVER_NAME, serverName);
}
//-------------------------------------------------------------------------
/**
* Loads, initializes and starts the components based on the specified resource.
* <p>
* See {@link #createResource(String)} for the valid resource location formats.
* <p>
* Calls {@link #start(Resource)}.
*
* @param resourceLocation the configuration resource location, not null
* @return the created repository, not null
*/
public ComponentRepository start(String resourceLocation) {
Resource resource = ResourceUtils.createResource(resourceLocation);
return start(resource);
}
/**
* Loads, initializes and starts the components based on the specified resource.
* <p>
* Calls {@link #load(Resource)}, {@link #init()} and {@link #start()}.
*
* @param resource the configuration resource to load, not null
* @return the created repository, not null
*/
public ComponentRepository start(Resource resource) {
load(resource);
init();
start();
return getRepository();
}
//-------------------------------------------------------------------------
/**
* Loads the component configuration based on the specified resource.
* <p>
* See {@link #createResource(String)} for the valid resource location formats.
* <p>
* Calls {@link #load(Resource)}.
*
* @param resourceLocation the configuration resource location, not null
* @return this manager, for chaining, not null
*/
public ComponentManager load(String resourceLocation) {
Resource resource = ResourceUtils.createResource(resourceLocation);
return load(resource);
}
/**
* Loads the component configuration based on the specified resource.
*
* @param resource the configuration resource to load, not null
* @return this manager, for chaining, not null
*/
public ComponentManager load(Resource resource) {
_logger.logInfo(" Using item: " + ResourceUtils.getLocation(resource));
if (resource.getFilename().endsWith(".properties")) {
String nextConfig = loadProperties(resource);
if (nextConfig == null) {
throw new OpenGammaRuntimeException("The properties file must contain the key '" + MANAGER_NEXT_FILE + "' to specify the next file to load: " + resource);
}
return load(nextConfig);
}
if (resource.getFilename().endsWith(".ini")) {
loadIni(resource);
return this;
}
throw new OpenGammaRuntimeException("Unknown file format: " + resource);
}
//-------------------------------------------------------------------------
/**
* Loads a properties file into the replacements map.
* <p>
* The properties file must be in the standard format defined by {@link Properties}.
* The file must contain a key "component.ini"
*
* @param resource the properties resource location, not null
* @return the next configuration file to load, null if not specified
*/
protected String loadProperties(Resource resource) {
ComponentConfigPropertiesLoader loader = new ComponentConfigPropertiesLoader(_logger, getProperties());
return loader.load(resource, 0);
}
/**
* Loads the INI file and initializes the components based on the contents.
*
* @param resource the INI resource location, not null
*/
protected void loadIni(Resource resource) {
ComponentConfigIniLoader loader = new ComponentConfigIniLoader(_logger, getProperties());
loader.load(resource, 0, _configIni);
logProperties();
}
/**
* Logs the properties to be used.
*/
protected void logProperties() {
_logger.logDebug("--- Using merged properties ---");
Map<String, String> properties = new TreeMap<String, String>(getProperties());
for (String key : properties.keySet()) {
if (key.contains("password")) {
_logger.logDebug(" " + key + " = " + StringUtils.repeat("*", properties.get(key).length()));
} else {
_logger.logDebug(" " + key + " = " + properties.get(key));
}
}
}
//-------------------------------------------------------------------------
/**
* Initializes the repository from the configuration that has been loaded.
* <p>
* Call {@code load(...)} before this method.
*
* @return this manager, for chaining, not null
*/
public ComponentManager init() {
getRepository().pushThreadLocal();
initGlobal();
initComponents();
return this;
}
/**
* Initializes the global definitions from the config.
*/
protected void initGlobal() {
LinkedHashMap<String, String> global = _configIni.getGroup("global");
if (global != null) {
PlatformConfigUtils.configureSystemProperties();
String zoneId = global.get("time.zone");
if (zoneId != null) {
OpenGammaClock.setZone(ZoneId.of(zoneId));
}
}
}
/**
* Initializes the component definitions from the config.
*/
protected void initComponents() {
for (String groupName : _configIni.getGroups()) {
LinkedHashMap<String, String> groupData = _configIni.getGroup(groupName);
if (groupData.containsKey("factory")) {
initComponent(groupName, groupData);
}
}
}
//-------------------------------------------------------------------------
/**
* Initialize the component.
*
* @param groupName the group name, not null
* @param groupConfig the config data, not null
*/
protected void initComponent(String groupName, LinkedHashMap<String, String> groupConfig) {
_logger.logInfo("--- Initializing " + groupName + " ---");
long startInstant = System.nanoTime();
LinkedHashMap<String, String> remainingConfig = new LinkedHashMap<String, String>(groupConfig);
String typeStr = remainingConfig.remove("factory");
_logger.logDebug(" Initializing factory '" + typeStr);
_logger.logDebug(" Using properties " + remainingConfig);
// load factory
ComponentFactory factory = loadFactory(typeStr);
// set properties
try {
setFactoryProperties(factory, remainingConfig);
} catch (Exception ex) {
throw new OpenGammaRuntimeException("Failed to set component factory properties: '" + groupName + "' with " + groupConfig, ex);
}
// init
try {
initFactory(factory, remainingConfig);
} catch (Exception ex) {
throw new OpenGammaRuntimeException("Failed to init component factory: '" + groupName + "' with " + groupConfig, ex);
}
long endInstant = System.nanoTime();
_logger.logInfo("--- Initialized " + groupName + " in " + ((endInstant - startInstant) / 1000000L) + "ms ---");
}
//-------------------------------------------------------------------------
/**
* Loads the factory.
* A factory should perform minimal work in the constructor.
*
* @param typeStr the factory type class name, not null
* @return the factory, not null
*/
protected ComponentFactory loadFactory(String typeStr) {
ComponentFactory factory;
try {
Class<? extends ComponentFactory> cls = getClass().getClassLoader().loadClass(typeStr).asSubclass(ComponentFactory.class);
factory = cls.newInstance();
} catch (ClassNotFoundException ex) {
throw new OpenGammaRuntimeException("Unknown component factory: " + typeStr, ex);
} catch (InstantiationException ex) {
throw new OpenGammaRuntimeException("Unable to create component factory: " + typeStr, ex);
} catch (IllegalAccessException ex) {
throw new OpenGammaRuntimeException("Unable to access component factory: " + typeStr, ex);
}
return factory;
}
//-------------------------------------------------------------------------
/**
* Sets the properties on the factory.
*
* @param factory the factory, not null
* @param remainingConfig the config data, not null
* @throws Exception allowing throwing of a checked exception
*/
protected void setFactoryProperties(ComponentFactory factory, LinkedHashMap<String, String> remainingConfig) throws Exception {
if (factory instanceof Bean) {
Bean bean = (Bean) factory;
for (MetaProperty<?> mp : bean.metaBean().metaPropertyIterable()) {
String value = remainingConfig.remove(mp.name());
setProperty(bean, mp, value);
}
}
}
/**
* Sets an individual property.
* <p>
* This method handles the main special case formats of the value.
*
* @param bean the bean, not null
* @param mp the property, not null
* @param value the configured value, not null
* @throws Exception allowing throwing of a checked exception
*/
protected void setProperty(Bean bean, MetaProperty<?> mp, String value) throws Exception {
if (ComponentRepository.class.equals(mp.propertyType())) {
// set the repo
mp.set(bean, getRepository());
} else if (value == null) {
// set to ensure validated by factory
mp.set(bean, mp.get(bean));
} else if ("null".equals(value)) {
// forcibly set to null
mp.set(bean, null);
} else if (value.contains("::")) {
// double colon used for component references
setPropertyComponentRef(bean, mp, value);
} else if (MANAGER_PROPERTIES.equals(value) && Resource.class.equals(mp.propertyType())) {
// set to the combined set of properties
setPropertyMergedProperties(bean, mp);
} else {
// set value
setPropertyInferType(bean, mp, value);
}
}
/**
* Intelligently sets the property to the merged set of properties.
* <p>
* The key "MANAGER.PROPERTIES" can be used in a properties file to refer to
* the entire set of merged properties. This is normally what you want to pass
* into other systems (such as Spring) that need a set of properties.
*
* @param bean the bean, not null
* @param mp the property, not null
* @throws Exception allowing throwing of a checked exception
*/
protected void setPropertyMergedProperties(Bean bean, MetaProperty<?> mp) throws Exception {
final String desc = MANAGER_PROPERTIES + " for " + mp;
final ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
Properties props = new Properties();
props.putAll(getProperties());
props.store(out, desc);
out.close();
Resource resource = new AbstractResource() {
@Override
public String getDescription() {
return MANAGER_PROPERTIES;
}
@Override
public String getFilename() throws IllegalStateException {
return MANAGER_PROPERTIES + ".properties";
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(out.toByteArray());
}
@Override
public String toString() {
return desc;
}
};
mp.set(bean, resource);
}
/**
* Intelligently sets the property which is a component reference.
* <p>
* The double colon is used in the format {@code Type::Classifier}.
* If the type is omitted, this method will try to infer it.
*
* @param bean the bean, not null
* @param mp the property, not null
* @param value the configured value containing double colon, not null
*/
protected void setPropertyComponentRef(Bean bean, MetaProperty<?> mp, String value) {
Class<?> propertyType = mp.propertyType();
String type = StringUtils.substringBefore(value, "::");
String classifier = StringUtils.substringAfter(value, "::");
if (type.length() == 0) {
try {
// infer type
mp.set(bean, getRepository().getInstance(propertyType, classifier));
return;
} catch (RuntimeException ex) {
throw new OpenGammaRuntimeException("Unable to set property " + mp + " of type " + propertyType.getName(), ex);
}
}
ComponentInfo info = getRepository().findInfo(type, classifier);
if (info == null) {
throw new OpenGammaRuntimeException("Unable to find component reference '" + value + "' while setting property " + mp);
}
if (ComponentInfo.class.isAssignableFrom(propertyType)) {
mp.set(bean, info);
} else {
mp.set(bean, getRepository().getInstance(info));
}
}
/**
* Intelligently sets the property.
* <p>
* This uses the repository to link properties declared with classifiers to the instance.
*
* @param bean the bean, not null
* @param mp the property, not null
* @param value the configured value, not null
*/
protected void setPropertyInferType(Bean bean, MetaProperty<?> mp, String value) {
Class<?> propertyType = mp.propertyType();
if (propertyType == Resource.class) {
mp.set(bean, ResourceUtils.createResource(value));
} else {
// set property by value type conversion from String
try {
mp.setString(bean, value);
} catch (RuntimeException ex) {
throw new OpenGammaRuntimeException("Unable to set property " + mp, ex);
}
}
}
//-------------------------------------------------------------------------
/**
* Initializes the factory.
* <p>
* The real work of creating the component and registering it should be done here.
* The factory may also publish a RESTful view and/or a life-cycle method.
*
* @param factory the factory to initialize, not null
* @param remainingConfig the remaining configuration data, not null
* @throws Exception to allow components to throw checked exceptions
*/
protected void initFactory(ComponentFactory factory, LinkedHashMap<String, String> remainingConfig) throws Exception {
factory.init(getRepository(), remainingConfig);
if (remainingConfig.size() > 0) {
throw new IllegalStateException("Configuration was specified but not used: " + remainingConfig);
}
}
//-------------------------------------------------------------------------
/**
* Starts the initialized components.
* <p>
* Call {@code load(...)} and {@code init()} before this method.
*/
public void start() {
_logger.logInfo("--- Starting Lifecycle ---");
long startInstant = System.nanoTime();
getRepository().start();
long endInstant = System.nanoTime();
_logger.logInfo("--- Started Lifecycle in " + ((endInstant - startInstant) / 1000000L) + "ms ---");
}
}