/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.collect.named; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; 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.opengamma.collect.ArgChecker; import com.opengamma.collect.io.IniFile; import com.opengamma.collect.io.PropertiesFile; import com.opengamma.collect.io.PropertySet; import com.opengamma.collect.io.ResourceLocator; /** * 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> * A configuration file is used to define the set of named instances 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> * The configuration file is found in the classpath. It has the same package location as the enum type * and is a chained {@linkplain IniFile#ofChained(java.util.stream.Stream) INI file}. * <p> * A chained INI file allows multiple files to be on the classpath. * A 'chain' section includes a 'priority' value to specify the order to load the files. * The 'chainNextFile' and 'chainRemoveSections' keys provide fine grained control. * <p> * Two sections control the loading of extended enum providers - 'providers' and 'alternates'. * <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 either 'constants' or 'lookup'. * A 'constants' provider defines the extended enums are public static constants. * A 'lookup' provider implemented {@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> * 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> { /** * Section name used for providers. */ private static final String PROVIDERS_SECTION = "providers"; /** * Section name used for alternates. */ private static final String ALTERNATES_SECTION = "alternates"; /** * The enum type. */ private final Class<T> type; /** * The lookup functions. */ private final ImmutableList<NamedLookup<T>> lookups; /** * The map of alternate names. */ private final ImmutableMap<String, String> alternates; //------------------------------------------------------------------------- /** * Obtains an extended enum instance. * <p> * Calling this method loads configuration files to determine the extended enum values. * The configuration file has the same location as the specified type and is a * {@linkplain PropertiesFile properties file} with the suffix '.properties'. * 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) { ArgChecker.notNull(type, "type"); try { // load all matching files String name = type.getName().replace('.', '/') + ".ini"; IniFile config = IniFile.ofChained( ResourceLocator.streamOfClasspathResources(name).map(ResourceLocator::getCharSource)); // parse files ImmutableList<NamedLookup<R>> lookups = parseProviders(config, type); ImmutableMap<String, String> alternateNames = parseAlternates(config); return new ExtendedEnum<>(type, lookups, alternateNames); } catch (RuntimeException ex) { // logging used because this is loaded in a static variable Logger logger = Logger.getLogger(ExtendedEnum.class.getName()); logger.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()); } } // 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.getSection(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.getValue(key); if (value.equals("constants")) { // extract public static final constants builder.add(parseConstants(enumType, cls)); } else if (value.equals("lookup")) { if (!NamedLookup.class.isAssignableFrom(cls)) { throw new IllegalArgumentException("Enum provider class must implement NamedLookup " + cls.getName()); } // class is a named lookup try { Constructor<?> cons = cls.getDeclaredConstructor(); if (Modifier.isPublic(cls.getModifiers()) == false) { cons.setAccessible(true); } builder.add((NamedLookup<R>) cons.newInstance()); } catch (Exception ex) { throw new IllegalArgumentException("Invalid enum provider constructor: new " + cls.getName() + "()", 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); } 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(); } ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); PropertySet section = config.getSection(ALTERNATES_SECTION); for (String key : section.keys()) { builder.put(key, section.getValue(key)); } return builder.build(); } //------------------------------------------------------------------------- /** * Creates an instance. * * @param type the enum type * @param lookups the lookup functions to find instances * @param alternates the map of alternate name to standard name */ private ExtendedEnum(Class<T> type, ImmutableList<NamedLookup<T>> lookups, ImmutableMap<String, String> alternates) { ArgChecker.notNull(type, "type"); ArgChecker.notNull(alternates, "alternates"); ArgChecker.notNull(lookups, "lookups"); this.type = type; this.lookups = lookups; this.alternates = alternates; } //------------------------------------------------------------------------- /** * 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 */ public T lookup(String name) { ArgChecker.notNull(name, "name"); String standardName = alternates.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 */ 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, 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) { ImmutableMap<String, T> lookupMap = lookup.lookupAll(); for (Entry<String, T> entry : lookupMap.entrySet()) { map.putIfAbsent(entry.getKey(), entry.getValue()); } } return ImmutableMap.copyOf(map); } /** * 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 alternates; } //------------------------------------------------------------------------- @Override public String toString() { return "ExtendedEnum[" + type.getSimpleName() + "]"; } }