/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.collect.io;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.Unchecked;
/**
* Provides access to configuration files.
* <p>
* A standard approach to configuration is provided by this class.
* Any configuration information provided by this library can be overridden or added to by applications.
* <p>
* By default, there are three groups of recognized configuration directories:
* <ul>
* <li>base
* <li>library
* <li>application
* </ul>
* <p>
* Each group consists of ten directories using a numeric suffix:
* <ul>
* <li>{@code com/opengamma/strata/config/base}
* <li>{@code com/opengamma/strata/config/base1}
* <li>{@code com/opengamma/strata/config/base2}
* <li>...
* <li>{@code com/opengamma/strata/config/base9}
* <li>{@code com/opengamma/strata/config/library}
* <li>{@code com/opengamma/strata/config/library1}
* <li>...
* <li>{@code com/opengamma/strata/config/library9}
* <li>{@code com/opengamma/strata/config/application}
* <li>{@code com/opengamma/strata/config/application1}
* <li>...
* <li>{@code com/opengamma/strata/config/application9}
* </ul>
* These form a complete set of thirty directories that are searched for configuration.
* <p>
* The search strategy looks for the same file name in each of the thirty directories.
* All the files that are found are then merged, with directories lower down the list taking priorty.
* Thus, any configuration file in the 'application9' directory will override the same file
* in the 'appication1' directory, which will override the same file in the 'library' group,
* which will further override the same file in the 'base' group.
* <p>
* The 'base' group is reserved for Strata.
* The 'library' group is reserved for libraries built directly on Strata.
* <p>
* The set of configuration directories can be changed using the system property
* 'com.opengamma.strata.config.directories'.
* This must be a comma separated list, such as 'base,base1,base2,override,application'.
* <p>
* In general, the configuration managed by this class will be in INI format.
* The {@link #combinedIniFile(String)} method is the main entry point, returning a single
* INI file merged from all available configuration files.
*/
public final class ResourceConfig {
/**
* The logger.
*/
private static final Logger log = Logger.getLogger(ResourceConfig.class.getName());
/**
* The package/folder location for the configuration.
*/
private static final String CONFIG_PACKAGE = "com/opengamma/strata/config/";
/**
* The default set of directories to query configuration files in.
*/
private static final ImmutableList<String> DEFAULT_DIRS = ImmutableList.of(
"base",
"base1",
"base2",
"base3",
"base4",
"base5",
"base6",
"base7",
"base8",
"base9",
"library",
"library1",
"library2",
"library3",
"library4",
"library5",
"library6",
"library7",
"library8",
"library9",
"application",
"application1",
"application2",
"application3",
"application4",
"application5",
"application6",
"application7",
"application8",
"application9");
/**
* The system property defining the comma separated list of groups.
*/
public static final String RESOURCE_DIRS_PROPERTY = "com.opengamma.strata.config.directories";
/**
* The resource groups.
* Always falls back to the known set in case of error.
*/
private static final ImmutableList<String> RESOURCE_DIRS;
static {
List<String> dirs = DEFAULT_DIRS;
String property = null;
try {
property = System.getProperty(RESOURCE_DIRS_PROPERTY);
} catch (Exception ex) {
log.warning("Unable to access system property: " + ex.toString());
}
if (property != null && !property.isEmpty()) {
try {
dirs = Splitter.on(',').trimResults().splitToList(property);
} catch (Exception ex) {
log.warning("Invalid system property: " + property + ": " + ex.toString());
}
for (String dir : dirs) {
if (!dir.matches("[A-Za-z0-9-]+")) {
log.warning("Invalid system property directory, must match regex [A-Za-z0-9-]+: " + dir);
}
}
}
log.config("Using directories: " + dirs);
RESOURCE_DIRS = ImmutableList.copyOf(dirs);
}
/**
* INI section name used for chaining.
*/
private static final String CHAIN_SECTION = "chain";
/**
* INI property name used for chaining.
*/
private static final String CHAIN_NEXT = "chainNextFile";
/**
* INI property name used for removing sections.
*/
private static final String CHAIN_REMOVE = "chainRemoveSections";
//-------------------------------------------------------------------------
/**
* Returns a combined INI file formed by merging INI files with the specified name.
* <p>
* This finds the all files with the specified name in the configuration directories.
* Each file is loaded, with the result being formed by merging the files into one.
* See {@link #combinedIniFile(List)} for more details on the merge process.
*
* @param resourceName the resource name
* @return the resource locators
* @throws UncheckedIOException if an IO exception occurs
* @throws IllegalStateException if there is a configuration error
*/
public static IniFile combinedIniFile(String resourceName) {
ArgChecker.notNull(resourceName, "resourceName");
return ResourceConfig.combinedIniFile(ResourceConfig.orderedResources(resourceName));
}
/**
* Returns a combined INI file formed by merging the specified INI files.
* <p>
* The result of this method is formed by merging the specified files together.
* The files are combined in order forming a chain.
* The first file in the list has the lowest priority.
* The last file in the list has the highest priority.
* <p>
* The algorithm starts with all the sections and properties from the highest priority file.
* It then adds any sections or properties from subsequent files that are not already present.
* <p>
* The algorithm can be controlled by providing a '[chain]' section.
* Within the 'chain' section, if 'chainNextFile' is 'false', then processing stops,
* and lower priority files are ignored. If the 'chainRemoveSections' property is specified,
* the listed sections are ignored from the files lower in the chain.
*
* @param resources the INI file resources to read
* @return the combined chained INI file
* @throws UncheckedIOException if an IO error occurs
* @throws IllegalArgumentException if the configuration is invalid
*/
public static IniFile combinedIniFile(List<ResourceLocator> resources) {
ArgChecker.notNull(resources, "resources");
Map<String, PropertySet> sectionMap = new LinkedHashMap<>();
for (ResourceLocator resource : resources) {
IniFile file = IniFile.of(resource.getCharSource());
if (file.contains(CHAIN_SECTION)) {
PropertySet chainSection = file.section(CHAIN_SECTION);
// remove everything from lower priority files if not chaining
if (chainSection.contains(CHAIN_NEXT) && Boolean.parseBoolean(chainSection.value(CHAIN_NEXT)) == false) {
sectionMap.clear();
} else {
// remove sections from lower priority files
sectionMap.keySet().removeAll(chainSection.valueList(CHAIN_REMOVE));
}
}
// add entries, replacing existing data
for (String sectionName : file.asMap().keySet()) {
if (!sectionName.equals(CHAIN_SECTION)) {
sectionMap.merge(sectionName, file.section(sectionName), PropertySet::overrideWith);
}
}
}
return IniFile.of(sectionMap);
}
//-------------------------------------------------------------------------
/**
* Obtains an ordered list of resource locators.
* <p>
* This finds the all files with the specified name in the configuration directories.
* The result is ordered from the lowest priority (base) file to the highest priority (application) file.
* The result will always contain at least one file, but it may contain more than one.
*
* @param resourceName the resource name
* @return the resource locators
* @throws UncheckedIOException if an IO exception occurs
* @throws IllegalStateException if there is a configuration error
*/
public static List<ResourceLocator> orderedResources(String resourceName) {
ArgChecker.notNull(resourceName, "resourceName");
return Unchecked.wrap(() -> orderedResources0(resourceName));
}
// find the list of resources
private static List<ResourceLocator> orderedResources0(String classpathResourceName) throws IOException {
ClassLoader classLoader = ResourceLocator.classLoader();
List<String> names = new ArrayList<>();
List<ResourceLocator> result = new ArrayList<>();
for (String dir : RESOURCE_DIRS) {
String name = CONFIG_PACKAGE + dir + "/" + classpathResourceName;
names.add(name);
List<URL> urls = Collections.list(classLoader.getResources(name));
switch (urls.size()) {
case 0:
continue;
case 1:
result.add(ResourceLocator.ofClasspathUrl(urls.get(0)));
break;
default:
// handle case where Strata is on the classpath more than once
// only accept this if the data being read is the same in all URLs
ResourceLocator baseResource = ResourceLocator.ofClasspathUrl(urls.get(0));
for (int i = 1; i < urls.size(); i++) {
ResourceLocator otherResource = ResourceLocator.ofClasspathUrl(urls.get(i));
if (!baseResource.getByteSource().contentEquals(otherResource.getByteSource())) {
log.severe("More than one file found on the classpath: " + name + ": " + urls);
throw new IllegalStateException("More than one file found on the classpath: " + name + ": " + urls);
}
}
result.add(baseResource);
break;
}
}
if (result.isEmpty()) {
log.severe("No resource files found on the classpath: " + names);
throw new IllegalStateException("No files found on the classpath: " + names);
}
log.config(() -> "Resources found: " + result);
return result;
}
//-------------------------------------------------------------------------
private ResourceConfig() {
}
}