com.addthis.codec.plugins.PluginMap.java Source code

Java tutorial

Introduction

Here is the source code for com.addthis.codec.plugins.PluginMap.java

Source

/*
 * Licensed 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 com.addthis.codec.plugins;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.Maps;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkNotNull;

@Beta
public class PluginMap {

    private static final Logger log = LoggerFactory.getLogger(PluginMap.class);

    public static final PluginMap EMPTY = new PluginMap();

    @Nonnull
    private final Config config;
    @Nonnull
    private final BiMap<String, Class<?>> map;
    @Nonnull
    private final Map<String, String> aliases;
    @Nonnull
    private final Set<String> inlinedAliases;

    @Nonnull
    private final String category;
    @Nonnull
    private final String classField;

    @Nullable
    private final Class<?> baseClass;

    public PluginMap(@Nonnull String category, @Nonnull Config config) {
        this.config = config;
        this.category = checkNotNull(category);
        classField = config.getString("_field");
        boolean errorMissing = config.getBoolean("_strict");
        if (config.hasPath("_class")) {
            String baseClassName = config.getString("_class");
            try {
                baseClass = Class.forName(baseClassName);
            } catch (ClassNotFoundException e) {
                log.error("could not find specified base class {} for category {}", baseClassName, category);
                throw new RuntimeException(e);
            }
        } else {
            baseClass = null;
        }
        Set<String> labels = config.root().keySet();
        BiMap<String, Class<?>> mutableMap = HashBiMap.create(labels.size());
        Map<String, String> mutableAliasMap = new HashMap<>();
        Set<String> mutableInlinedAliasSet = new HashSet<>();
        for (String label : labels) {
            if (!((label.charAt(0) != '_') || "_array".equals(label) || "_default".equals(label))) {
                continue;
            }
            ConfigValue configValue = config.root().get(label);
            String className;
            if (configValue.valueType() == ConfigValueType.STRING) {
                className = (String) configValue.unwrapped();
            } else if (configValue.valueType() == ConfigValueType.OBJECT) {
                ConfigObject configObject = (ConfigObject) configValue;
                className = configObject.toConfig().getString("_class");
                if (configObject.toConfig().hasPath("_inline") && configObject.toConfig().getBoolean("_inline")) {
                    mutableInlinedAliasSet.add(label);
                }
            } else if (configValue.valueType() == ConfigValueType.NULL) {
                continue;
            } else {
                throw new ConfigException.WrongType(configValue.origin(), label, "STRING OR OBJECT",
                        configValue.valueType().toString());
            }
            if (labels.contains(className)) {
                // points to another alias
                mutableAliasMap.put(label, className);
            } else {
                try {
                    Class<?> foundClass = findAndValidateClass(className);
                    mutableMap.put(label, foundClass);
                } catch (ClassNotFoundException maybeSwallowed) {
                    if (errorMissing) {
                        throw new RuntimeException(maybeSwallowed);
                    } else {
                        log.warn("plugin category {} with alias {} is pointing to missing class {}", category,
                                label, className);
                    }
                }
            }
        }
        map = Maps.unmodifiableBiMap(mutableMap);
        aliases = Collections.unmodifiableMap(mutableAliasMap);
        checkAliasesForCycles();
        inlinedAliases = Collections.unmodifiableSet(mutableInlinedAliasSet);
    }

    private PluginMap() {
        config = ConfigFactory.empty();
        map = ImmutableBiMap.of();
        aliases = Collections.emptyMap();
        inlinedAliases = Collections.emptySet();
        classField = "class";
        category = "unknown";
        baseClass = null;
    }

    @Nonnull
    public BiMap<String, Class<?>> asBiMap() {
        return map;
    }

    @Nonnull
    public Config config() {
        return config;
    }

    @Nonnull
    public ConfigObject aliasDefaults(String alias) {
        ConfigValue configValue = config.root().get(alias);
        ConfigObject defaults;
        if ((configValue != null) && (configValue.valueType() == ConfigValueType.OBJECT)) {
            defaults = (ConfigObject) configValue;
        } else {
            defaults = ConfigFactory.empty().root();
        }
        String aliasTarget = aliases.get(alias);
        if (aliasTarget != null) {
            defaults = defaults.withFallback(aliasDefaults(aliasTarget));
        }
        return defaults;
    }

    @Nonnull
    public String classField() {
        return classField;
    }

    @Nonnull
    public String category() {
        return category;
    }

    @Nonnull
    public Set<String> inlinedAliases() {
        return inlinedAliases;
    }

    @Nullable
    public Class<?> arraySugar() {
        return getClassIfConfigured("_array");
    }

    @Nullable
    public Class<?> defaultSugar() {
        return getClassIfConfigured("_default");
    }

    /** The base class, if any, that all configured plugins must be assignable to. */
    @Nullable
    public Class<?> baseClass() {
        return baseClass;
    }

    /** Reverse look-up on the bi-map to get the root alias for this type; if none, then returns the class name */
    @Nonnull
    public String getClassName(Class<?> type) {
        String alt = map.inverse().get(type);
        if (alt != null) {
            return alt;
        } else {
            return type.getName();
        }
    }

    /** Resolves aliases that point to other aliases until pointing to a real class; then return the current label. */
    @Nullable
    public String getLastAlias(String alias) {
        String aliasTarget = aliases.get(alias);
        if (aliasTarget != null) {
            return getLastAlias(aliasTarget);
        } else {
            if (asBiMap().containsKey(alias)) {
                return alias;
            } else {
                return null;
            }
        }
    }

    /**
     * Resolves aliases until the root class for that alias chain is found or if no such alias exsits, then
     * try find and validate the given string as if it were the class name of an anonymous alias. This means
     * that it if a base _class is defined for this category that all package prefixes will be tried and that
     * any found class must be assignable to the base _class type.
     */
    @Nonnull
    public Class<?> getClass(String type) throws ClassNotFoundException {
        Class<?> alt = map.get(type);
        if (alt != null) {
            return alt;
        } else {
            String aliasTarget = aliases.get(type);
            if (aliasTarget != null) {
                return getClass(aliasTarget);
            }
        }
        return findAndValidateClass(type);
    }

    /** Like {@link #getClass(String)}, but will return null rather than try to locate a new class. */
    @Nullable
    public Class<?> getClassIfConfigured(String type) {
        Class<?> alt = map.get(type);
        if (alt != null) {
            return alt;
        } else {
            String aliasTarget = aliases.get(type);
            if (aliasTarget != null) {
                return getClassIfConfigured(aliasTarget);
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("category", category).add("baseClass", baseClass)
                .add("classField", classField).add("map", map).add("aliases", aliases)
                .add("inlined-aliases", inlinedAliases).toString();
    }

    @Nonnull
    private Class<?> findAndValidateClass(String className) throws ClassNotFoundException {
        Class<?> classValue = null;
        // if baseClass is defined, support shared parent package omission
        if (baseClass != null) {
            @Nullable
            String packageName = baseClass.getPackage().getName();
            while ((packageName != null) && (classValue == null)) {
                String packageSugaredName = packageName + '.' + className;
                try {
                    classValue = Class.forName(packageSugaredName);
                } catch (ClassNotFoundException ignored) {
                    int lastDotIndex = packageName.lastIndexOf('.');
                    if (lastDotIndex >= 0) {
                        packageName = packageName.substring(0, lastDotIndex);
                    } else {
                        packageName = null;
                    }
                }
            }
        }
        if (classValue == null) {
            classValue = Class.forName(className);
        }
        // if baseClass is defined, validate all aliased classes as being valid subtypes
        if ((baseClass != null) && !baseClass.isAssignableFrom(classValue)) {
            throw new ClassCastException(
                    String.format("plugin %s specified a base class %s and '%s: %s', is not a valid subtype",
                            category, baseClass.getName(), classField, classValue.getName()));
        }
        return classValue;
    }

    private void checkAliasesForCycles() {
        for (String key : aliases.keySet()) {
            Set<String> visited = new HashSet<>(aliases.size());
            checkAliasesForCyclesHelper(key, visited);
        }
    }

    private void checkAliasesForCyclesHelper(String key, Set<String> visited) {
        visited.add(key);
        String nextKey = aliases.get(key);
        if (nextKey == null) {
            // should mean it is present in the bimap
            return;
        }
        if (visited.contains(nextKey)) {
            throw new ConfigException.BadValue(config.root().get(key).origin(), key, "cyclical aliases detected");
        } else {
            checkAliasesForCyclesHelper(nextKey, visited);
        }
    }
}