ProtoEnum.java
/*
* Copyright 2016 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.proto;
import com.google.protobuf.Descriptors;
import com.google.protobuf.ProtocolMessageEnum;
import java.lang.reflect.Type;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
import static net.morimekta.collect.UnmodifiableList.toList;
import static net.morimekta.collect.UnmodifiableMap.toMap;
import static net.morimekta.collect.util.LazyCachedSupplier.lazyCache;
import static net.morimekta.proto.utils.ReflectionUtil.getInstanceClass;
import static net.morimekta.proto.utils.ReflectionUtil.getSingleton;
import static net.morimekta.strings.EscapeUtil.javaEscape;
/**
* The definition of a serializable enum.
*/
public class ProtoEnum<E extends Enum<E> & ProtocolMessageEnum> implements Type {
/**
* The name of the UNRECOGNIZED enum entry.
*/
public static final String UNRECOGNIZED = "UNRECOGNIZED";
private final Class<E> enumClass;
private final Descriptors.EnumDescriptor protoDescriptor;
private final Supplier<List<E>> values;
private final Supplier<Map<String, E>> nameMap;
private final Supplier<Map<Integer, E>> idMap;
private final Supplier<Map<Descriptors.EnumValueDescriptor, E>> valueMap;
private final Supplier<Optional<E>> defaultValue;
private final Supplier<Optional<E>> unrecognized;
/**
* @param enumClass The enum type.
* @param enumDescriptor The enum type descriptor.
*/
protected ProtoEnum(Class<E> enumClass, Descriptors.EnumDescriptor enumDescriptor) {
this.enumClass = requireNonNull(enumClass, "class == null");
this.protoDescriptor = requireNonNull(enumDescriptor, "descriptor == null");
// only v3 has unrecognized.
this.unrecognized = lazyCache(
() -> EnumSet.allOf(enumClass)
.stream()
.filter(val -> val.name().equals(UNRECOGNIZED))
.findFirst());
this.values = lazyCache(
() -> EnumSet.allOf(enumClass)
.stream()
.filter(val -> !val.name().equals(UNRECOGNIZED))
.collect(toList()));
this.idMap = lazyCache(
() -> allValues()
.stream()
.collect(toMap(ProtocolMessageEnum::getNumber)));
this.nameMap = lazyCache(
() -> enumDescriptor.getValues()
.stream()
.collect(toMap(Descriptors.EnumValueDescriptor::getName,
v -> valueForNumber(v.getNumber()))));
this.valueMap = lazyCache(
() -> enumDescriptor.getValues()
.stream()
.collect(toMap(Function.identity(),
v -> valueForNumber(v.getNumber()))));
this.defaultValue = lazyCache(() -> {
if (protoDescriptor.getFile().getSyntax() == Descriptors.FileDescriptor.Syntax.PROTO3) {
return Optional.ofNullable(idMap.get().getOrDefault(0, getUnrecognized()));
}
return Optional.empty();
});
}
/**
* @return The full type name.
*/
public String getTypeName() {
return protoDescriptor.getFullName();
}
/**
* @return The enum type class.
*/
public Class<E> getEnumClass() {
return enumClass;
}
/**
* @return The enum type descriptor.
*/
public Descriptors.EnumDescriptor getProtoDescriptor() {
return protoDescriptor;
}
/**
* @return The default value for the enum, or null if no default value.
*/
public Optional<E> getDefaultValue() {
return defaultValue.get();
}
/**
* @return The unrecognized value, or null if no such values exists.
*/
public E getUnrecognized() {
return unrecognized.get().orElse(null);
}
/**
* @return List of all values in declared order.
*/
public List<E> allValues() {
return values.get();
}
/**
* @param id Value to look up enum from.
* @return Enum if found, null otherwise.
*/
public E findByNumber(Integer id) {
if (id == null) {
return null;
}
return idMap.get().get(id);
}
/**
* @param name Name to look up enum from.
* @return Enum if found, null otherwise.
*/
public E findByName(String name) {
if (name == null) {
return null;
}
return nameMap.get().get(name);
}
/**
* @param enumValue The enum value descriptor.
* @return The enum value matching the descriptor.
*/
public E findByValue(Descriptors.EnumValueDescriptor enumValue) {
if (enumValue == null) {
return null;
}
return valueMap.get().get(enumValue);
}
/**
* @param id Value to look up enum from.
* @return The enum value.
* @throws IllegalArgumentException If value not found.
*/
public E valueForNumber(int id) {
return Optional.ofNullable(idMap.get().get(id))
.orElseThrow(() -> new IllegalArgumentException(
"No " + getTypeName() + " value for number " + id));
}
/**
* @param name Name to look up enum from.
* @return The enum value.
* @throws IllegalArgumentException If value not found.
*/
public E valueForName(String name) {
requireNonNull(name, "name == null");
return Optional.ofNullable(nameMap.get().get(name))
.orElseThrow(() -> new IllegalArgumentException(
"No " + getTypeName() + " value for name '" + javaEscape(name) + "'"));
}
/**
* @param value Value to look up enum from.
* @return The enum value.
* @throws IllegalArgumentException If value not found.
*/
public E valueFor(Descriptors.EnumValueDescriptor value) {
requireNonNull(value, "value == null");
return Optional.ofNullable(valueMap.get().get(value))
.orElseThrow(() -> new IllegalArgumentException(
"No " + getTypeName() + " value for " + value.getFullName()));
}
// --- Object ---
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ProtoEnum)) {
return false;
}
ProtoEnum<?> that = (ProtoEnum<?>) o;
return allValues().equals(that.allValues());
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), allValues(), getDefaultValue());
}
@Override
public String toString() {
return getTypeName() + "{" +
allValues().stream().map(e -> e.getValueDescriptor().getName() + "=" + e.getNumber())
.collect(Collectors.joining(",")) + "}";
}
// --- Static Utils ---
/**
* @param instance An enum value.
* @param <E> The enum value type.
* @return The instance case to the enum value type.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
E requireProtoEnum(Object instance) {
requireNonNull(instance, "instance == null");
if (instance instanceof Enum && instance instanceof ProtocolMessageEnum) {
return (E) instance;
}
throw new IllegalArgumentException("Not a proto enum " + instance);
}
/**
* @param type A java class.
* @param <E> The enum type.
* @return The class cast to the proto enum class.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
Class<E> requireProtoEnumClass(Class<?> type) {
requireNonNull(type, "type == null");
if (isProtoEnumClass(type)) {
return (Class<E>) type;
}
throw new IllegalArgumentException("Not a proto enum type: " +
type.getName().replaceAll("\\$", "."));
}
/**
* @param type A java class.
* @return True if the class is a java proto enum class.
*/
public static boolean isProtoEnumClass(Class<?> type) {
if (type == null) {
return false;
}
return Enum.class.isAssignableFrom(type) && ProtocolMessageEnum.class.isAssignableFrom(type);
}
/**
* @param value An enum value.
* @param <E> The enum value type.
* @return The enum descriptor helper.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
ProtoEnum<E> getEnumDescriptor(E value) {
requireNonNull(value, "value == null");
var enumType = value.getDeclaringClass();
return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
enumType, t -> {
ProtoEnum<?> descriptor = new ProtoEnum<>(enumType, value.getDescriptorForType());
classFromDescriptor.put(value.getDescriptorForType(), enumType);
return descriptor;
});
}
/**
* @param protoDescriptor An enum descriptor.
* @param <E> The enum value type.
* @return The enum descriptor helper.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
ProtoEnum<E> getEnumDescriptor(Descriptors.EnumDescriptor protoDescriptor) {
return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
classFromDescriptor.computeIfAbsent(protoDescriptor, a -> getInstanceClass(protoDescriptor)),
type -> new ProtoEnum<>((Class<E>) type, protoDescriptor));
}
/**
* @param enumType An enum java class.
* @param <E> The enum value type.
* @return The enum descriptor helper.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
ProtoEnum<E> getEnumDescriptorUnchecked(Class<?> enumType) {
return getEnumDescriptor((Class<E>) requireProtoEnumClass(enumType));
}
/**
* @param enumType An enum java class.
* @param <E> The enum value type.
* @return The enum descriptor helper.
*/
@SuppressWarnings("unchecked")
public static <E extends Enum<E> & ProtocolMessageEnum>
ProtoEnum<E> getEnumDescriptor(Class<E> enumType) {
if (isProtoEnumClass(enumType)) {
return (ProtoEnum<E>) enumTypeFromClass.computeIfAbsent(
enumType,
t -> {
Descriptors.EnumDescriptor protoDescriptor =
getSingleton(enumType, Descriptors.EnumDescriptor.class, "getDescriptor");
var descriptor = new ProtoEnum<>(enumType, protoDescriptor);
classFromDescriptor.put(protoDescriptor, enumType);
return descriptor;
});
}
throw new IllegalArgumentException("Not a proto enum type: " +
enumType.getName().replaceAll("\\$", "."));
}
private static final Map<Class<?>, ProtoEnum<?>> enumTypeFromClass
= new ConcurrentHashMap<>();
private static final Map<Descriptors.EnumDescriptor, Class<?>> classFromDescriptor
= new ConcurrentHashMap<>();
}