001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ninja.leaping.configurate.util;
018
019import com.google.common.cache.CacheBuilder;
020import com.google.common.cache.CacheLoader;
021import com.google.common.cache.LoadingCache;
022import com.google.common.collect.ImmutableMap;
023import org.checkerframework.checker.nullness.qual.NonNull;
024
025import java.util.HashMap;
026import java.util.Map;
027import java.util.Optional;
028import java.util.concurrent.ExecutionException;
029
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Utility class to cache more flexible enum lookup.
034 *
035 * <p>While normally case and punctuation have to match exactly, this method performs lookup that:</p>
036 *
037 * <ul>
038 *     <li>is case-insensitive</li>
039 *     <li>ignores underscores</li>
040 *     <li>caches mappings</li>
041 * </ul>
042 *
043 * <p>If the enum has two fields that are equal except for case and underscores, an exact match
044 * will return the appropriate value, and any fuzzy matches will map to the first value in the enum
045 * that is applicable.</p>
046 */
047public final class EnumLookup {
048    private EnumLookup() {}
049
050    private static final LoadingCache<Class<? extends Enum<?>>, Map<String, Enum<?>>> ENUM_FIELD_CACHE = CacheBuilder
051            .newBuilder()
052            .weakKeys()
053            .maximumSize(512)
054            .build(new CacheLoader<Class<? extends Enum<?>>, Map<String, Enum<?>>>() {
055                @Override
056                public Map<String, Enum<?>> load(@NonNull Class<? extends Enum<?>> key) {
057                    Map<String, Enum<?>> ret = new HashMap<>();
058                    for (Enum<?> field : key.getEnumConstants()) {
059                        ret.put(field.name(), field);
060                        ret.putIfAbsent(processKey(field.name()), field);
061                    }
062                    return ImmutableMap.copyOf(ret);
063                }
064            });
065
066    @NonNull
067    private static String processKey(@NonNull String key) {
068        // stick a flower at the front so processed keys are different from literal keys
069        return "🌸" + key.toLowerCase().replace("_", "");
070    }
071
072    @SuppressWarnings("unchecked")
073    @NonNull
074    public static <T extends Enum<T>> Optional<T> lookupEnum(@NonNull Class<T> clazz, @NonNull String key) {
075        try {
076            Map<String, Enum<?>> vals = ENUM_FIELD_CACHE.get(requireNonNull(clazz, "clazz"));
077            Enum<?> possibleRet = vals.get(requireNonNull(key, "key"));
078            if (possibleRet != null) {
079                return Optional.of((T) possibleRet);
080            }
081            return Optional.ofNullable((T) vals.get(processKey(key)));
082        } catch (ExecutionException e) {
083            return Optional.empty();
084        }
085    }
086}