ProtoEnum.java

  1. /*
  2.  * Copyright 2016 Providence Authors
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.proto;

  22. import com.google.protobuf.Descriptors;
  23. import com.google.protobuf.ProtocolMessageEnum;

  24. import java.lang.reflect.Type;
  25. import java.util.EnumSet;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Objects;
  29. import java.util.Optional;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. import java.util.function.Function;
  32. import java.util.function.Supplier;
  33. import java.util.stream.Collectors;

  34. import static java.util.Objects.requireNonNull;
  35. import static net.morimekta.collect.UnmodifiableList.toList;
  36. import static net.morimekta.collect.UnmodifiableMap.toMap;
  37. import static net.morimekta.collect.util.LazyCachedSupplier.lazyCache;
  38. import static net.morimekta.proto.utils.ReflectionUtil.getInstanceClass;
  39. import static net.morimekta.proto.utils.ReflectionUtil.getSingleton;
  40. import static net.morimekta.strings.EscapeUtil.javaEscape;

  41. /**
  42.  * The definition of a serializable enum.
  43.  */
  44. public class ProtoEnum<E extends Enum<E> & ProtocolMessageEnum> implements Type {
  45.     /**
  46.      * The name of the UNRECOGNIZED enum entry.
  47.      */
  48.     public static final String UNRECOGNIZED = "UNRECOGNIZED";

  49.     private final Class<E>                                          enumClass;
  50.     private final Descriptors.EnumDescriptor                        protoDescriptor;
  51.     private final Supplier<List<E>>                                 values;
  52.     private final Supplier<Map<String, E>>                          nameMap;
  53.     private final Supplier<Map<Integer, E>>                         idMap;
  54.     private final Supplier<Map<Descriptors.EnumValueDescriptor, E>> valueMap;
  55.     private final Supplier<Optional<E>>                             defaultValue;
  56.     private final Supplier<Optional<E>>                             unrecognized;

  57.     /**
  58.      * @param enumClass      The enum type.
  59.      * @param enumDescriptor The enum type descriptor.
  60.      */
  61.     protected ProtoEnum(Class<E> enumClass, Descriptors.EnumDescriptor enumDescriptor) {
  62.         this.enumClass = requireNonNull(enumClass, "class == null");
  63.         this.protoDescriptor = requireNonNull(enumDescriptor, "descriptor == null");

  64.         // only v3 has unrecognized.
  65.         this.unrecognized = lazyCache(
  66.                 () -> EnumSet.allOf(enumClass)
  67.                              .stream()
  68.                              .filter(val -> val.name().equals(UNRECOGNIZED))
  69.                              .findFirst());
  70.         this.values = lazyCache(
  71.                 () -> EnumSet.allOf(enumClass)
  72.                              .stream()
  73.                              .filter(val -> !val.name().equals(UNRECOGNIZED))
  74.                              .collect(toList()));
  75.         this.idMap = lazyCache(
  76.                 () -> allValues()
  77.                         .stream()
  78.                         .collect(toMap(ProtocolMessageEnum::getNumber)));
  79.         this.nameMap = lazyCache(
  80.                 () -> enumDescriptor.getValues()
  81.                                     .stream()
  82.                                     .collect(toMap(Descriptors.EnumValueDescriptor::getName,
  83.                                                    v -> valueForNumber(v.getNumber()))));
  84.         this.valueMap = lazyCache(
  85.                 () -> enumDescriptor.getValues()
  86.                                     .stream()
  87.                                     .collect(toMap(Function.identity(),
  88.                                                    v -> valueForNumber(v.getNumber()))));
  89.         this.defaultValue = lazyCache(() -> {
  90.             if (protoDescriptor.getFile().getSyntax() == Descriptors.FileDescriptor.Syntax.PROTO3) {
  91.                 return Optional.ofNullable(idMap.get().getOrDefault(0, getUnrecognized()));
  92.             }
  93.             return Optional.empty();
  94.         });
  95.     }

  96.     /**
  97.      * @return The full type name.
  98.      */
  99.     public String getTypeName() {
  100.         return protoDescriptor.getFullName();
  101.     }

  102.     /**
  103.      * @return The enum type class.
  104.      */
  105.     public Class<E> getEnumClass() {
  106.         return enumClass;
  107.     }

  108.     /**
  109.      * @return The enum type descriptor.
  110.      */
  111.     public Descriptors.EnumDescriptor getProtoDescriptor() {
  112.         return protoDescriptor;
  113.     }

  114.     /**
  115.      * @return The default value for the enum, or null if no default value.
  116.      */
  117.     public Optional<E> getDefaultValue() {
  118.         return defaultValue.get();
  119.     }

  120.     /**
  121.      * @return The unrecognized value, or null if no such values exists.
  122.      */
  123.     public E getUnrecognized() {
  124.         return unrecognized.get().orElse(null);
  125.     }

  126.     /**
  127.      * @return List of all values in declared order.
  128.      */
  129.     public List<E> allValues() {
  130.         return values.get();
  131.     }

  132.     /**
  133.      * @param id Value to look up enum from.
  134.      * @return Enum if found, null otherwise.
  135.      */
  136.     public E findByNumber(Integer id) {
  137.         if (id == null) {
  138.             return null;
  139.         }
  140.         return idMap.get().get(id);
  141.     }

  142.     /**
  143.      * @param name Name to look up enum from.
  144.      * @return Enum if found, null otherwise.
  145.      */
  146.     public E findByName(String name) {
  147.         if (name == null) {
  148.             return null;
  149.         }
  150.         return nameMap.get().get(name);
  151.     }

  152.     /**
  153.      * @param enumValue The enum value descriptor.
  154.      * @return The enum value matching the descriptor.
  155.      */
  156.     public E findByValue(Descriptors.EnumValueDescriptor enumValue) {
  157.         if (enumValue == null) {
  158.             return null;
  159.         }
  160.         return valueMap.get().get(enumValue);
  161.     }

  162.     /**
  163.      * @param id Value to look up enum from.
  164.      * @return The enum value.
  165.      * @throws IllegalArgumentException If value not found.
  166.      */
  167.     public E valueForNumber(int id) {
  168.         return Optional.ofNullable(idMap.get().get(id))
  169.                        .orElseThrow(() -> new IllegalArgumentException(
  170.                                "No " + getTypeName() + " value for number " + id));
  171.     }

  172.     /**
  173.      * @param name Name to look up enum from.
  174.      * @return The enum value.
  175.      * @throws IllegalArgumentException If value not found.
  176.      */
  177.     public E valueForName(String name) {
  178.         requireNonNull(name, "name == null");
  179.         return Optional.ofNullable(nameMap.get().get(name))
  180.                        .orElseThrow(() -> new IllegalArgumentException(
  181.                                "No " + getTypeName() + " value for name '" + javaEscape(name) + "'"));
  182.     }

  183.     /**
  184.      * @param value Value to look up enum from.
  185.      * @return The enum value.
  186.      * @throws IllegalArgumentException If value not found.
  187.      */
  188.     public E valueFor(Descriptors.EnumValueDescriptor value) {
  189.         requireNonNull(value, "value == null");
  190.         return Optional.ofNullable(valueMap.get().get(value))
  191.                        .orElseThrow(() -> new IllegalArgumentException(
  192.                                "No " + getTypeName() + " value for " + value.getFullName()));
  193.     }

  194.     // --- Object ---

  195.     @Override
  196.     public boolean equals(Object o) {
  197.         if (o == this) {
  198.             return true;
  199.         }
  200.         if (!(o instanceof ProtoEnum)) {
  201.             return false;
  202.         }
  203.         ProtoEnum<?> that = (ProtoEnum<?>) o;
  204.         return allValues().equals(that.allValues());
  205.     }

  206.     @Override
  207.     public int hashCode() {
  208.         return Objects.hash(super.hashCode(), allValues(), getDefaultValue());
  209.     }

  210.     @Override
  211.     public String toString() {
  212.         return getTypeName() + "{" +
  213.                allValues().stream().map(e -> e.getValueDescriptor().getName() + "=" + e.getNumber())
  214.                           .collect(Collectors.joining(",")) + "}";
  215.     }

  216.     // --- Static Utils ---

  217.     /**
  218.      * @param instance An enum value.
  219.      * @param <E>      The enum value type.
  220.      * @return The instance case to the enum value type.
  221.      */
  222.     @SuppressWarnings("unchecked")
  223.     public static <E extends Enum<E> & ProtocolMessageEnum>
  224.     E requireProtoEnum(Object instance) {
  225.         requireNonNull(instance, "instance == null");
  226.         if (instance instanceof Enum && instance instanceof ProtocolMessageEnum) {
  227.             return (E) instance;
  228.         }
  229.         throw new IllegalArgumentException("Not a proto enum " + instance);
  230.     }

  231.     /**
  232.      * @param type A java class.
  233.      * @param <E>  The enum type.
  234.      * @return The class cast to the proto enum class.
  235.      */
  236.     @SuppressWarnings("unchecked")
  237.     public static <E extends Enum<E> & ProtocolMessageEnum>
  238.     Class<E> requireProtoEnumClass(Class<?> type) {
  239.         requireNonNull(type, "type == null");
  240.         if (isProtoEnumClass(type)) {
  241.             return (Class<E>) type;
  242.         }
  243.         throw new IllegalArgumentException("Not a proto enum type: " +
  244.                                            type.getName().replaceAll("\\$", "."));
  245.     }

  246.     /**
  247.      * @param type A java class.
  248.      * @return True if the class is a java proto enum class.
  249.      */
  250.     public static boolean isProtoEnumClass(Class<?> type) {
  251.         if (type == null) {
  252.             return false;
  253.         }
  254.         return Enum.class.isAssignableFrom(type) && ProtocolMessageEnum.class.isAssignableFrom(type);
  255.     }

  256.     /**
  257.      * @param value An enum value.
  258.      * @param <E>   The enum value type.
  259.      * @return The enum descriptor helper.
  260.      */
  261.     @SuppressWarnings("unchecked")
  262.     public static <E extends Enum<E> & ProtocolMessageEnum>
  263.     ProtoEnum<E> getEnumDescriptor(E value) {
  264.         requireNonNull(value, "value == null");
  265.         var enumType = value.getDeclaringClass();
  266.         return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
  267.                 enumType, t -> {
  268.                     ProtoEnum<?> descriptor = new ProtoEnum<>(enumType, value.getDescriptorForType());
  269.                     classFromDescriptor.put(value.getDescriptorForType(), enumType);
  270.                     return descriptor;
  271.                 });
  272.     }

  273.     /**
  274.      * @param protoDescriptor An enum descriptor.
  275.      * @param <E>             The enum value type.
  276.      * @return The enum descriptor helper.
  277.      */
  278.     @SuppressWarnings("unchecked")
  279.     public static <E extends Enum<E> & ProtocolMessageEnum>
  280.     ProtoEnum<E> getEnumDescriptor(Descriptors.EnumDescriptor protoDescriptor) {
  281.         return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
  282.                 classFromDescriptor.computeIfAbsent(protoDescriptor, a -> getInstanceClass(protoDescriptor)),
  283.                 type -> new ProtoEnum<>((Class<E>) type, protoDescriptor));
  284.     }


  285.     /**
  286.      * @param enumType An enum java class.
  287.      * @param <E>      The enum value type.
  288.      * @return The enum descriptor helper.
  289.      */
  290.     @SuppressWarnings("unchecked")
  291.     public static <E extends Enum<E> & ProtocolMessageEnum>
  292.     ProtoEnum<E> getEnumDescriptorUnchecked(Class<?> enumType) {
  293.         return getEnumDescriptor((Class<E>) requireProtoEnumClass(enumType));
  294.     }

  295.     /**
  296.      * @param enumType An enum java class.
  297.      * @param <E>      The enum value type.
  298.      * @return The enum descriptor helper.
  299.      */
  300.     @SuppressWarnings("unchecked")
  301.     public static <E extends Enum<E> & ProtocolMessageEnum>
  302.     ProtoEnum<E> getEnumDescriptor(Class<E> enumType) {
  303.         if (isProtoEnumClass(enumType)) {
  304.             return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
  305.                     enumType,
  306.                     t -> {
  307.                         Descriptors.EnumDescriptor protoDescriptor =
  308.                                 getSingleton(enumType, Descriptors.EnumDescriptor.class, "getDescriptor");
  309.                         var descriptor = new ProtoEnum<>(enumType, protoDescriptor);
  310.                         classFromDescriptor.put(protoDescriptor, enumType);
  311.                         return descriptor;
  312.                     });
  313.         }
  314.         throw new IllegalArgumentException("Not a proto enum type: " +
  315.                                            enumType.getName().replaceAll("\\$", "."));
  316.     }

  317.     private static final Map<Class<?>, ProtoEnum<?>>               enumTypeFromClass
  318.             = new ConcurrentHashMap<>();
  319.     private static final Map<Descriptors.EnumDescriptor, Class<?>> classFromDescriptor
  320.             = new ConcurrentHashMap<>();
  321. }