/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.Resource;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.ResourceUtils;
/**
* Loads configuration from the INI format file.
* <p>
* The format is line-based as follows:<br>
* <code>#</code> or <code>;</code> for comment lines (at the start of the line)<br>
* <code>${key}</code> is replaced by an earlier replacement declaration<br>
* <code>[group]</code> defines the start of a named group of configs<br>
* <code>key = value</code> defines a single config element within a group<br>
* <code>MANAGER.INCLUDE = resource</code> declares a resource to be included immediately<br>
* the "global" group is used to add keys to the set of properties used for replacement<br>
* Everything is trimmed as necessary.
* <p>
* The specified properties are used in two ways.
* Firstly, they are used to substitute sections that have defined the '<code>${key}</code>' notation.
* Secondly, they can directly override properties in an INI group, if the property has a
* key of the format '{@code [group].key}'.
*/
public class ComponentConfigIniLoader extends AbstractComponentConfigLoader {
/**
* The pattern to match [group].key
*/
private static final Pattern GROUP_OVERRIDE = Pattern.compile("\\[" + "([^\\]]+)" + "\\]" + "[.]" + "(.+)");
/**
* Creates an instance.
*
* @param logger the logger, not null
* @param properties the properties in use, not null
*/
public ComponentConfigIniLoader(ComponentLogger logger, ConcurrentMap<String, String> properties) {
super(logger, properties);
}
//-------------------------------------------------------------------------
/**
* Loads the INI file.
* <p>
* Loads the configuration defining components from the specified resource.
*
* @param resource the config resource to load, not null
* @param depth the depth of the properties file, used for logging
* @param config the config being loaded, not null
*/
public void load(final Resource resource, final int depth, final ComponentConfig config) {
ArgumentChecker.notNull(resource, "resource");
ArgumentChecker.notNull(config, "config");
try {
doLoad(resource, depth, config);
overrideProperties(config);
} catch (RuntimeException ex) {
throw new OpenGammaRuntimeException("Unable to load INI file: " + resource, ex);
}
}
/**
* Loads the INI file.
* <p>
* Loads the configuration defining components from the specified resource.
*
* @param resource the config resource to load, not null
* @param depth the depth of the properties file, used for logging
* @param config the config being loaded, not null
* @return the config, not null
*/
private void doLoad(final Resource resource, final int depth, final ComponentConfig config) {
List<String> lines = readLines(resource);
String group = null;
int lineNum = 0;
for (String line : lines) {
lineNum++;
line = line.trim();
if (line.length() == 0 || line.startsWith("#") || line.startsWith(";")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
group = line.substring(1, line.length() - 1);
} else if (group == null) {
throw new OpenGammaRuntimeException("Invalid format, properties must be specified within a [group], line " + lineNum);
} else {
int equalsPosition = line.indexOf('=');
if (equalsPosition < 0) {
throw new OpenGammaRuntimeException("Invalid format, line " + lineNum);
}
String key = line.substring(0, equalsPosition).trim();
String value = line.substring(equalsPosition + 1).trim();
if (key.length() == 0) {
throw new IllegalArgumentException("Invalid empty key, line " + lineNum);
}
if (config.contains(group, key)) {
throw new IllegalArgumentException("Invalid file, key '" + key + "' specified twice, line " + lineNum);
}
// resolve ${} references
value = resolveProperty(value, lineNum);
// handle includes
if (key.equals(ComponentManager.MANAGER_INCLUDE)) {
handleInclude(resource, value, depth, config);
} else {
// store property
config.put(group, key, value);
if (group.equals("global")) {
getProperties().put(key, value);
}
}
}
}
}
/**
* Handle the inclusion of another file.
*
* @param baseResource the base resource, not null
* @param includeFile the resource to include, not null
* @param depth the depth of the properties file, used for logging
* @param config the config being loaded, not null
*/
private void handleInclude(final Resource baseResource, String includeFile, final int depth, final ComponentConfig config) {
// find resource
Resource include;
try {
include = ResourceUtils.createResource(includeFile);
} catch (Exception ex) {
try {
include = baseResource.createRelative(includeFile);
} catch (Exception ex2) {
throw new OpenGammaRuntimeException(ex2.getMessage(), ex2);
}
}
// load and merge
getLogger().logInfo(StringUtils.repeat(" ", depth) + " Including item: " + ResourceUtils.getLocation(include));
try {
doLoad(include, depth + 1, config);
} catch (RuntimeException ex) {
throw new OpenGammaRuntimeException("Unable to load INI file: " + include, ex);
}
}
//-------------------------------------------------------------------------
/**
* Override config using the properties.
* <p>
* Any property that has a key of the form '[group].key' will replace the
* specified key within the group.
*
* @param config the config to update, not null
*/
private void overrideProperties(final ComponentConfig config) {
List<String[]> iniProperties = extractIniOverrideProperties();
for (String[] array : iniProperties) {
config.getGroup(array[0]); // validate group (but returns a copy of the inner map)
config.put(array[0], array[1], array[2]);
getLogger().logDebug(" Replacing group property: [" + array[0] + "]." + array[1] + "=" + array[2]);
}
}
/**
* Extracts any properties that match the group name style "[group].key".
* <p>
* These directly override any INI file settings.
*
* @return the extracted set of INI properties, not null
*/
private List<String[]> extractIniOverrideProperties() {
List<String[]> extracted = new ArrayList<String[]>();
for (String key : getProperties().keySet()) {
Matcher matcher = GROUP_OVERRIDE.matcher(key);
if (matcher.matches()) {
String group = matcher.group(1);
String propertyKey = matcher.group(2);
String[] array = {group, propertyKey, getProperties().get(key)};
extracted.add(array);
}
}
return extracted;
}
}