com.opengamma.component.ComponentManager.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.component.ComponentManager.java

Source

/**
 * 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 ---");
    }

}