com.opengamma.collect.named.ExtendedEnum.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.collect.named.ExtendedEnum.java

Source

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

}