/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.collect.named;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.logging.Logger;
import org.joda.convert.RenameHandler;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.Messages;
import com.opengamma.strata.collect.io.IniFile;
import com.opengamma.strata.collect.io.PropertySet;
import com.opengamma.strata.collect.io.ResourceConfig;
/**
* Manager for extended enums controlled by code or configuration.
* <p>
* The standard Java {@code Enum} is a fixed set of constants defined at compile time.
* In many scenarios this can be too limiting and this class provides an alternative.
* <p>
* An INI configuration file is used to define the set of named instances.
* For more information on the process of loading the configuration file, see {@link ResourceConfig}.
* <p>
* The named instances are loaded via provider classes.
* A provider class is either an implementation of {@link NamedLookup} or a class
* providing {@code public static final} enum constants.
* <p>
* The configuration file also supports the notion of alternate names (aliases).
* This allows many different names to be used to lookup the same instance.
* <p>
* Three sections control the loading of additional information.
* <p>
* The 'providers' section contains a number of properties, one for each provider.
* The key is the full class name of the provider.
* The value is 'constants', 'lookup' or 'instance', and is used to obtain a {@link NamedLookup} instance.
* A 'constants' provider must contain public static constants of the correct type,
* which will be reflectively located and wrapped in a {@code NamedLookup}.
* A 'lookup' provider must implement {@link NamedLookup} and have a no-args constructor.
* An 'instance' provider must have a static variable named "INSTANCE" of type {@link NamedLookup}.
* <p>
* The 'alternates' section contains a number of properties, one for each alternate name.
* The key is the alternate name, the value is the standard name.
* Alternate names are used when looking up an extended enum.
* <p>
* The 'externals' sections contains a number of properties intended to allow external enum names to be mapped.
* Unlike 'alternates', which are always included, 'externals' are only included when requested.
* There may be multiple external <i>groups</i> to handle different external providers of data.
* For example, the mapping used by FpML may differ from that used by Bloomberg.
* <p>
* Each 'externals' section has a name of the form 'externals.Foo', where 'Foo' is the name of the group.
* Each property line in the section is of the same format as the 'alternates' section.
* It maps the external name to the standard name.
* <p>
* It is intended that this class is used as a helper class to load the configuration
* and manage the map of names to instances. It should be created and used by the author
* of the main abstract extended enum class, and not be application developers.
*
* @param <T> the type of the enum
*/
public final class ExtendedEnum<T extends Named> {
/**
* The logger.
*/
private static final Logger log = Logger.getLogger(ExtendedEnum.class.getName());
/**
* Section name used for providers.
*/
private static final String PROVIDERS_SECTION = "providers";
/**
* Section name used for alternates.
*/
private static final String ALTERNATES_SECTION = "alternates";
/**
* Section name used for externals.
*/
private static final String EXTERNALS_SECTION = "externals.";
/**
* The enum type.
*/
private final Class<T> type;
/**
* The lookup functions defining the standard names.
*/
private final ImmutableList<NamedLookup<T>> lookups;
/**
* The map of alternate names.
*/
private final ImmutableMap<String, String> alternateNames;
/**
* The map of external names, keyed by the group name.
* The first map holds groups of external names.
* The inner map holds the mapping from external name to our name.
*/
private final ImmutableMap<String, ImmutableMap<String, String>> externalNames;
//-------------------------------------------------------------------------
/**
* Obtains an extended enum instance.
* <p>
* Calling this method loads configuration files to determine the extended enum values.
* The configuration file has the same simple name as the specified type and is a
* {@linkplain IniFile INI file} with the suffix '.ini'.
* See class-level documentation for more information.
*
* @param <R> the type of the enum
* @param type the type to load
* @return the extended enum
*/
public static <R extends Named> ExtendedEnum<R> of(Class<R> type) {
try {
// load all matching files
String name = type.getSimpleName() + ".ini";
IniFile config = ResourceConfig.combinedIniFile(name);
// parse files
ImmutableList<NamedLookup<R>> lookups = parseProviders(config, type);
ImmutableMap<String, String> alternateNames = parseAlternates(config);
ImmutableMap<String, ImmutableMap<String, String>> externalNames = parseExternals(config);
log.fine(() -> "Loaded extended enum: " + name + ", providers: " + lookups);
return new ExtendedEnum<>(type, lookups, alternateNames, externalNames);
} catch (RuntimeException ex) {
// logging used because this is loaded in a static variable
log.severe("Failed to load ExtendedEnum for " + type + ": " + Throwables.getStackTraceAsString(ex));
// return an empty instance to avoid ExceptionInInitializerError
return new ExtendedEnum<>(type, ImmutableList.of(), ImmutableMap.of(), ImmutableMap.of());
}
}
// parses the alternate names
@SuppressWarnings("unchecked")
private static <R extends Named> ImmutableList<NamedLookup<R>> parseProviders(
IniFile config,
Class<R> enumType) {
if (!config.contains(PROVIDERS_SECTION)) {
return ImmutableList.of();
}
PropertySet section = config.section(PROVIDERS_SECTION);
ImmutableList.Builder<NamedLookup<R>> builder = ImmutableList.builder();
for (String key : section.keys()) {
Class<?> cls;
try {
cls = RenameHandler.INSTANCE.lookupType(key);
} catch (Exception ex) {
throw new IllegalArgumentException("Unable to find enum provider class: " + key, ex);
}
String value = section.value(key);
if (value.equals("constants")) {
// extract public static final constants
builder.add(parseConstants(enumType, cls));
} else if (value.equals("lookup")) {
// class is a named lookup
if (!NamedLookup.class.isAssignableFrom(cls)) {
throw new IllegalArgumentException("Enum provider class must implement NamedLookup " + cls.getName());
}
try {
Constructor<?> cons = cls.getDeclaredConstructor();
if (!Modifier.isPublic(cls.getModifiers())) {
cons.setAccessible(true);
}
builder.add((NamedLookup<R>) cons.newInstance());
} catch (Exception ex) {
throw new IllegalArgumentException("Invalid enum provider constructor: new " + cls.getName() + "()", ex);
}
} else if (value.equals("instance")) {
// class has a named lookup INSTANCE static field
try {
Field field = cls.getDeclaredField("INSTANCE");
if (!Modifier.isStatic(field.getModifiers()) || !NamedLookup.class.isAssignableFrom(field.getType())) {
throw new IllegalArgumentException("Invalid enum provider instance: " + cls.getName() + ".INSTANCE");
}
if (!Modifier.isPublic(cls.getModifiers()) || !Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
builder.add((NamedLookup<R>) field.get(null));
} catch (Exception ex) {
throw new IllegalArgumentException("Invalid enum provider instance: " + cls.getName() + ".INSTANCE", ex);
}
} else {
throw new IllegalArgumentException("Provider value must be either 'constants' or 'lookup'");
}
}
return builder.build();
}
// parses the public static final constants
private static <R extends Named> NamedLookup<R> parseConstants(Class<R> enumType, Class<?> constantsType) {
Field[] fields = constantsType.getDeclaredFields();
Map<String, R> instances = new HashMap<>();
for (Field field : fields) {
if (Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers()) &&
Modifier.isFinal(field.getModifiers()) && enumType.isAssignableFrom(field.getType())) {
if (Modifier.isPublic(constantsType.getModifiers()) == false) {
field.setAccessible(true);
}
try {
R instance = enumType.cast(field.get(null));
instances.putIfAbsent(instance.getName(), instance);
instances.putIfAbsent(instance.getName().toUpperCase(Locale.ENGLISH), instance);
} catch (Exception ex) {
throw new IllegalArgumentException("Unable to query field: " + field, ex);
}
}
}
ImmutableMap<String, R> constants = ImmutableMap.copyOf(instances);
return new NamedLookup<R>() {
@Override
public ImmutableMap<String, R> lookupAll() {
return constants;
}
};
}
// parses the alternate names.
private static ImmutableMap<String, String> parseAlternates(IniFile config) {
if (!config.contains(ALTERNATES_SECTION)) {
return ImmutableMap.of();
}
Map<String, String> alternates = new HashMap<>();
for (Entry<String, String> entry : config.section(ALTERNATES_SECTION).asMap().entrySet()) {
alternates.put(entry.getKey(), entry.getValue());
alternates.putIfAbsent(entry.getKey().toUpperCase(Locale.ENGLISH), entry.getValue());
}
return ImmutableMap.copyOf(alternates);
}
// parses the external names.
private static ImmutableMap<String, ImmutableMap<String, String>> parseExternals(IniFile config) {
ImmutableMap.Builder<String, ImmutableMap<String, String>> builder = ImmutableMap.builder();
for (String sectionName : config.sections()) {
if (sectionName.startsWith(EXTERNALS_SECTION)) {
String group = sectionName.substring(EXTERNALS_SECTION.length());
builder.put(group, config.section(sectionName).asMap());
}
}
return builder.build();
}
//-------------------------------------------------------------------------
/**
* Creates an instance.
*
* @param type the enum type
* @param lookups the lookup functions to find instances
* @param alternateNames the map of alternate name to standard name
* @param externalNames the map of external name groups
*/
private ExtendedEnum(
Class<T> type,
ImmutableList<NamedLookup<T>> lookups,
ImmutableMap<String, String> alternateNames,
ImmutableMap<String, ImmutableMap<String, String>> externalNames) {
this.type = ArgChecker.notNull(type, "type");
this.lookups = ArgChecker.notNull(lookups, "lookups");
this.alternateNames = ArgChecker.notNull(alternateNames, "alternateNames");
this.externalNames = ArgChecker.notNull(externalNames, "externalNames");
}
//-------------------------------------------------------------------------
/**
* Finds an instance by name.
* <p>
* This finds the instance matching the specified name.
* Instances may have alternate names (aliases), thus the returned instance
* may have a name other than that requested.
*
* @param name the enum name to return
* @return the named enum
*/
public Optional<T> find(String name) {
String standardName = alternateNames.getOrDefault(name, name);
for (NamedLookup<T> lookup : lookups) {
T instance = lookup.lookup(standardName);
if (instance != null) {
return Optional.of(instance);
}
}
return Optional.empty();
}
/**
* Looks up an instance by name.
* <p>
* This finds the instance matching the specified name.
* Instances may have alternate names (aliases), thus the returned instance
* may have a name other than that requested.
*
* @param name the enum name to return
* @return the named enum
* @throws IllegalArgumentException if the name is not found
*/
public T lookup(String name) {
String standardName = alternateNames.getOrDefault(name, name);
for (NamedLookup<T> lookup : lookups) {
T instance = lookup.lookup(standardName);
if (instance != null) {
return instance;
}
}
throw new IllegalArgumentException(type.getSimpleName() + " name not found: " + name);
}
/**
* Looks up an instance by name and type.
* <p>
* This finds the instance matching the specified name, ensuring it is of the specified type.
* Instances may have alternate names (aliases), thus the returned instance
* may have a name other than that requested.
*
* @param <S> the enum subtype
* @param subtype the enum subtype to match
* @param name the enum name to return
* @return the named enum
* @throws IllegalArgumentException if the name is not found or has the wrong type
*/
public <S extends T> S lookup(String name, Class<S> subtype) {
T result = lookup(name);
if (!subtype.isInstance(result)) {
throw new IllegalArgumentException(type.getSimpleName() + " name found but did not match expected type: " + name);
}
return subtype.cast(result);
}
//-------------------------------------------------------------------------
/**
* Returns the map of known instances by name.
* <p>
* This method returns all known instances.
* It is permitted for an enum provider implementation to return an empty map,
* thus the map may not be complete.
* The map may include instances keyed under an alternate name, such as names
* in upper case, however it will not include the base set of
* {@linkplain #alternateNames() alternate names}.
*
* @return the map of enum instance by name
*/
public ImmutableMap<String, T> lookupAll() {
Map<String, T> map = new HashMap<>();
for (NamedLookup<T> lookup : lookups) {
Map<String, T> lookupMap = lookup.lookupAll();
for (Entry<String, T> entry : lookupMap.entrySet()) {
map.putIfAbsent(entry.getKey(), entry.getValue());
}
}
return ImmutableMap.copyOf(map);
}
/**
* Returns the map of known instances by normalized name.
* <p>
* This method returns all known instances, keyed by the normalized name.
* This is equivalent to the result of {@link #lookupAll()} adjusted such
* that each entry is keyed by the result of {@link Named#getName()}.
*
* @return the map of enum instance by name
*/
public ImmutableMap<String, T> lookupAllNormalized() {
// add values that are keyed under the normalized name
// keep values keyed under a non-normalized name
Map<String, T> result = new HashMap<>();
Map<String, T> others = new HashMap<>();
for (Entry<String, T> entry : lookupAll().entrySet()) {
String normalizedName = entry.getValue().getName();
if (entry.getKey().equals(normalizedName)) {
result.put(normalizedName, entry.getValue());
} else {
others.put(normalizedName, entry.getValue());
}
}
// include any values that are only keyed under a non-normalized name
others.values().forEach(v -> result.putIfAbsent(v.getName(), v));
return ImmutableMap.copyOf(result);
}
/**
* Returns the complete map of alternate name to standard name.
* <p>
* The map is keyed by the alternate name.
*
* @return the map of alternate names
*/
public ImmutableMap<String, String> alternateNames() {
return alternateNames;
}
//-------------------------------------------------------------------------
/**
* Returns the set of groups that have external names defined.
* <p>
* External names are used to map names used by external systems to the standard name used here.
* There can be multiple groups of mappings to external systems,
* For example, the mapping used by FpML may differ from that used by Bloomberg.
*
* @return the set of groups that have external names
*/
public ImmutableSet<String> externalNameGroups() {
return externalNames.keySet();
}
/**
* Returns the mapping of external names to standard names for a group.
* <p>
* External names are used to map names used by external systems to the standard name used here.
* There can be multiple groups of mappings to external systems,
* For example, the mapping used by FpML may differ from that used by Bloomberg.
* <p>
* The result provides mapping between the external name and the standard name.
*
* @param group the group name to find external names for
* @return the map of external names for the group
* @throws IllegalArgumentException if the group is not found
*/
public ExternalEnumNames<T> externalNames(String group) {
ImmutableMap<String, String> externals = externalNames.get(group);
if (externals == null) {
throw new IllegalArgumentException(type.getSimpleName() + " group not found: " + group);
}
return new ExternalEnumNames<>(this, group, externals);
}
//-------------------------------------------------------------------------
@Override
public String toString() {
return "ExtendedEnum[" + type.getSimpleName() + "]";
}
//-------------------------------------------------------------------------
/**
* Maps names used by external systems to the standard name used here.
* <p>
* A frequent problem in parsing external file formats is converting enum values.
* This class provides a suitable mapping, allowing multiple external names to map to one standard name.
* <p>
* A single instance represents the mapping for a single external group.
* This allows the mapping for different groups to differ.
* For example, the mapping used by FpML may differ from that used by Bloomberg.
* <p>
* Instances of this class are configured via INI files and provided via {@link ExtendedEnum}.
*
* @param <T> the type of the enum
*/
public static final class ExternalEnumNames<T extends Named> {
private ExtendedEnum<T> extendedEnum;
private String group;
private ImmutableMap<String, String> externalNames;
private ExternalEnumNames(ExtendedEnum<T> extendedEnum, String group, ImmutableMap<String, String> externalNames) {
this.extendedEnum = extendedEnum;
this.group = group;
this.externalNames = externalNames;
}
/**
* Looks up an instance by name.
* <p>
* This finds the instance matching the specified name.
* Instances may have alternate names (aliases), thus the returned instance
* may have a name other than that requested.
*
* @param name the enum name to return
* @return the named enum
* @throws IllegalArgumentException if the name is not found
*/
public T lookup(String name) {
String standardName = externalNames.getOrDefault(name, name);
try {
return extendedEnum.lookup(standardName);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(Messages.format(
"{}:{} unable to find external name: {}", extendedEnum.type.getSimpleName(), group, name));
}
}
/**
* Looks up an instance by name and type.
* <p>
* This finds the instance matching the specified name, ensuring it is of the specified type.
* Instances may have alternate names (aliases), thus the returned instance
* may have a name other than that requested.
*
* @param <S> the enum subtype
* @param subtype the enum subtype to match
* @param name the enum name to return
* @return the named enum
* @throws IllegalArgumentException if the name is not found or has the wrong type
*/
public <S extends T> S lookup(String name, Class<S> subtype) {
T result = lookup(name);
if (!subtype.isInstance(result)) {
throw new IllegalArgumentException(Messages.format(
"{}:{} external name found but did not match expected type: {}", extendedEnum.type.getSimpleName(), group, name));
}
return subtype.cast(result);
}
/**
* Returns the complete map of external name to standard name.
* <p>
* The map is keyed by the external name.
*
* @return the map of external names
*/
public ImmutableMap<String, String> externalNames() {
return externalNames;
}
/**
* Looks up the external name given a standard enum instance.
* <p>
* This searches the map of external names and returns the first matching entry
* that maps to the given standard name.
*
* @param namedEnum the named enum to find an external name for
* @return the external name
* @throws IllegalArgumentException if there is no external name
*/
public String reverseLookup(T namedEnum) {
String name = namedEnum.getName();
for (Entry<String, String> entry : externalNames.entrySet()) {
if (entry.getValue().equals(name)) {
return entry.getKey();
}
}
throw new IllegalArgumentException(Messages.format(
"{}:{} external name not found for standard name: {}", extendedEnum.type.getSimpleName(), group, name));
}
//-------------------------------------------------------------------------
@Override
public String toString() {
return "ExternalEnumNames[" + extendedEnum.type.getSimpleName() + ":" + group + "]";
}
}
}