DiscoverableEnums.java

package cn.home1.oss.lib.common;

import static com.google.common.collect.Maps.newLinkedHashMap;
import static java.util.Collections.unmodifiableMap;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;

import lombok.extern.slf4j.Slf4j;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.type.filter.TypeFilter;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Slf4j
public final class DiscoverableEnums<T extends Enum<T> & DiscoverableEnum<T>> {

  private final Map<Class<?>, Map<String, T>> type2TextConstantMap;
  private final Map<String, T> allTextConstantMap;

  private DiscoverableEnums(final String basePackage, final Class<T> type) {
    final Set<Class<T>> discoverableEnums = scanDiscoverableEnums(basePackage, type);
    final Map<Class<?>, Map<String, T>> typeToText2ConstantMap = newLinkedHashMap();
    final Map<String, T> text2ConstantAllInOneMap = newLinkedHashMap();
    final Map<String, Collection<String>> discoverableEnumTexts = newLinkedHashMap();
    discoverableEnums.forEach(discoverableEnum -> {
      final Map<String, T> text2ConstantMap = text2ConstantMap(discoverableEnum);
      typeToText2ConstantMap.put(discoverableEnum, text2ConstantMap);
      text2ConstantMap.entrySet().forEach(entry -> {
        final String text = entry.getKey();
        final T constant = entry.getValue();
        if (text2ConstantAllInOneMap.containsKey(text)) {
          throw new IllegalArgumentException("'" + text + "' of '" + constant + "' conflict with '"
            + text + "' of '" + text2ConstantAllInOneMap.get(text).getDeclaringClass() + "'");
        }
        text2ConstantAllInOneMap.put(text, constant);
      });
      discoverableEnumTexts.put(StringUtils.lowerCaseFirstChar(discoverableEnum.getSimpleName()),
        ImmutableList.copyOf(enumTexts(discoverableEnum)));
    });

    this.type2TextConstantMap = ImmutableMap.copyOf(typeToText2ConstantMap);
    this.allTextConstantMap = ImmutableMap.copyOf(text2ConstantAllInOneMap);
  }

  /**
   * for validator.
   *
   * @param type type
   * @param text text
   * @return contains
   */
  public boolean contains(final Class<?> type, final String text) {
    return this.type2TextConstantMap.get(type).get(text) != null; // may null pointer
  }

  /**
   * for String2DiscoverableEnumConverter.
   *
   * @param source text or name
   * @return enum instance
   */
  public T convert(final Object source) {
    final T result;
    if (source != null) {
      result = this.allTextConstantMap.get(source.toString());
    } else {
      result = null;
    }
    return result;
  }

  /**
   * for String2DiscoverableEnumConverter.
   *
   * @param source     text or name
   * @param targetType targetType
   * @return enum instance
   */
  @SuppressWarnings("unchecked")
  public Object convert(final Object source, final TypeDescriptor targetType) {
    final Object result;
    if (source != null) {
      result = this.parse((Class<T>) targetType.getType(), source.toString());
    } else {
      result = null;
    }
    return result;
  }

  /**
   * parse enum.
   *
   * @param targetType targetType
   * @param source     text or name
   * @return enum instance
   */
  public Optional<T> parse(final Class<T> targetType, final String source) {
    final String textOrName = source.trim().toUpperCase();
    final Optional<T> result = this.parseByText(targetType, textOrName);
    return result.isPresent() ? result : this.parseByName(targetType, textOrName);
  }

  Optional<T> parseByText(final Class<T> targetType, final String text) {
    return Optional.ofNullable(this.type2TextConstantMap.get(targetType).get(text)); // may be null
  }

  Optional<T> parseByName(final Class<T> targetType, final String name) {
    T result = null;
    try {
      final T found = Enum.valueOf(targetType, name);
      if (isNotBlank(found.getText())) {
        result = found;
      }
    } catch (final RuntimeException ignored) {
      // no-op
      log.info("error parseByName '{}' of type '{}'.", name, targetType, ignored);
    }
    return Optional.ofNullable(result);
  }

  static <T extends Enum<T> & DiscoverableEnum<?>> Map<String, T> text2ConstantMap(
    final Class<T> type) {
    final Map<String, T> map = newLinkedHashMap();
    for (final T constant : type.getEnumConstants()) {
      if (isNotBlank(constant.getText())) {
        map.put(constant.getText(), constant);
      }
    }
    return unmodifiableMap(map);
  }

  static <T extends Enum<T> & DiscoverableEnum<T>> Collection<String> enumTexts(
    final Class<T> type) {
    // Lambda error: Invalid receiver type class java.lang.Enum; not a subtype of implementation
    // type interface DiscoverableEnum
    final Set<String> result = Sets.newLinkedHashSet();
    // Note: must use for loop here!
    // lambda will trigger an 'Invalid receiver type class java.lang.Enum,
    // not a subtype of implementation type interface XXX' error;
    // see: https://github.com/orfjackal/retrolambda/issues/69
    for (final T tag : Arrays.asList(type.getEnumConstants())) {
      if (isNotBlank(tag.getText())) {
        result.add(tag.getText());
      }
    }
    return result;
  }

  static <T extends Enum<T> & DiscoverableEnum<T>> Set<Class<T>> scanDiscoverableEnums( //
    final String basePackage, final Class<T> type) {
    // InterfaceFilter(type, false) not working here.
    final TypeFilter includeFilter = new FileAndClasspathUtils.AssignableFilter(type, false, false);
    return FileAndClasspathUtils.scan(basePackage, includeFilter);
  }

  /**
   * scan.
   *
   * @param basePackage base package
   * @param type        type to find
   * @param <T>         enum type
   * @return found
   */
  @SuppressWarnings({"unchecked", "rawtypes"})
  public static <T extends Enum<T> & DiscoverableEnum<T>> DiscoverableEnums<T> discoverableEnums( //
    final String basePackage, final Class<?> type //
  ) {
    return new DiscoverableEnums(basePackage, type);
  }
}