Package com.addthis.codec.plugins

Source Code of com.addthis.codec.plugins.PluginMap

/*
* 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.Nullable;

import java.lang.reflect.Field;

import java.util.Iterator;
import java.util.Set;

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.ConfigValue;
import com.typesafe.config.ConfigValueType;

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

public class PluginMap {

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

    public static final PluginMap EMPTY = new PluginMap();

    private final BiMap<String, Class<?>> map;

    private final String category;
    private final String classField;

    @Nullable private final Class<?>       baseClass;
    @Nullable private final Class<?>       defaultSugar;
    @Nullable private final Class<?>       arraySugar;
    @Nullable private final String         arrayField;
    @Nullable private final PluginRegistry typoRegistry;

    public PluginMap(String category, Config config) {
        this(category, config, null);
    }

    public PluginMap(String category, Config config, @Nullable PluginRegistry registry) {
        this.category = 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());
        for (String label : labels) {
            if (label.charAt(0) == '_') {
                continue;
            }
            ConfigValue configValue = config.root().get(label);
            if (configValue.valueType() != ConfigValueType.STRING) {
                throw new ConfigException.WrongType(configValue.origin(), label,
                                                    "STRING", configValue.valueType().toString());
            }
            String className = (String) configValue.unwrapped();
            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);
        if (config.hasPath("_array")) {
            String arraySugarName = config.getString("_array");
            Class<?> configuredArraySugar = map.get(arraySugarName);
            arrayField = searchArraySugarFieldName(configuredArraySugar);
            if (arrayField != null) {
                arraySugar = configuredArraySugar;
            } else {
                arraySugar = null;
            }
        } else {
            arraySugar = null;
            arrayField = null;
        }
        if (config.hasPath("_default")) {
            String defaultName = config.getString("_default");
            defaultSugar = map.get(defaultName);
        } else {
            defaultSugar = null;
        }
        this.typoRegistry = registry;
    }

    private PluginMap() {
        map = ImmutableBiMap.of();
        classField = "class";
        category = "unknown";
        defaultSugar = null;
        baseClass = null;
        arraySugar = null;
        arrayField = null;
        typoRegistry = null;
    }

    /** A thread safe, immutable bi-map view of this plugin map. */
    public BiMap<String, Class<?>> asBiMap() {
        return map;
    }

    public String classField() {
        return classField;
    }

    public String category() {
        return category;
    }

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

    @Nullable public String arrayField() {
        return arrayField;
    }

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

    @Nullable public Class<?> baseClass() {
        return baseClass;
    }

    public String getClassName(Class<?> type) {
        String alt = map.inverse().get(type);
        if (alt != null) {
            return alt;
        } else {
            return type.getName();
        }
    }

    public Class<?> getClass(String type) throws ClassNotFoundException {
        Class<?> alt = map.get(type);
        if (alt != null) {
            return alt;
        }
        try {
            return findAndValidateClass(type);
        } catch (ClassNotFoundException ex) {
            throw classNameSuggestions(type, ex);
        }
    }

    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 ClassNotFoundException classNameSuggestions(String type, ClassNotFoundException ex) {
        Set<String> classNames = map.keySet();
        if (classNames.isEmpty()) {
            return ex;
        }
        StringBuilder builder = new StringBuilder();

        if (category != null) {
            builder.append("Could not instantiate an instance of the ");
            builder.append(category);
            builder.append(" category that you have specified");
        } else {
            builder.append("Could not instantiate something you have specified");
        }
        builder.append(" with \"");
        builder.append(type);
        builder.append("\".");
        if (typoRegistry != null) {
            for (PluginMap pluginMap : typoRegistry.asMap().values()) {
                if (pluginMap.asBiMap().containsKey(type)) {
                    builder.append("\nIt looks like you tried to instantiate a ");
                    builder.append(pluginMap.category());
                    builder.append(" and I am expecting a ");
                    builder.append(category);
                    builder.append('.');
                    return new ClassNotFoundException(builder.toString(), ex);
                }
            }
        }
        builder.append("\nPerhaps you intended one of the following: ");
        Iterator<String> iterator = classNames.iterator();
        while (iterator.hasNext()) {
            String name = iterator.next();

            if (!iterator.hasNext() && classNames.size() > 1) {
                builder.append("or ");
            }

            builder.append('"');
            builder.append(name);
            builder.append('"');
            builder.append(' ');
        }
        builder.append("?\n");
        return new ClassNotFoundException(builder.toString(), ex);
    }

    @Override public String toString() {
        return Objects.toStringHelper(this)
                      .add("category", category)
                      .add("baseClass", baseClass)
                      .add("classField", classField)
                      .add("defaultSugar", defaultSugar)
                      .add("arraySugar", arraySugar)
                      .add("arrayField", arrayField)
                      .add("typoRegistry", typoRegistry)
                      .add("map", map)
                      .toString();
    }

    private static String searchArraySugarFieldName(Class<?> arraySugar) {
        Class<?> clazzptr = arraySugar;
        while (clazzptr != null) {
            for (Field field : clazzptr.getDeclaredFields()) {
                Class<?> fieldType = field.getType();
                if (fieldType.isArray()
                    && fieldType.getComponentType().isAssignableFrom(arraySugar)) {
                    return field.getName();
                }
            }
            clazzptr = clazzptr.getSuperclass();
        }
        log.warn("failed to find an appropriate array field for class marked as array" +
                 "sugar: {}", arraySugar);
        return null;
    }
}
TOP

Related Classes of com.addthis.codec.plugins.PluginMap

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.