de.xaniox.heavyspleef.core.flag.FlagRegistry.java Source code

Java tutorial

Introduction

Here is the source code for de.xaniox.heavyspleef.core.flag.FlagRegistry.java

Source

/*
 * This file is part of HeavySpleef.
 * Copyright (c) 2014-2016 Matthias Werning
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package de.xaniox.heavyspleef.core.flag;

import com.google.common.base.Predicate;
import com.google.common.collect.BiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import de.xaniox.heavyspleef.commands.base.CommandManager;
import de.xaniox.heavyspleef.core.HeavySpleef;
import de.xaniox.heavyspleef.core.Unregister;
import de.xaniox.heavyspleef.core.collection.DualKeyBiMap;
import de.xaniox.heavyspleef.core.collection.DualKeyHashBiMap;
import de.xaniox.heavyspleef.core.collection.DualKeyMap;
import de.xaniox.heavyspleef.core.collection.DualMaps;
import de.xaniox.heavyspleef.core.config.ConfigType;
import de.xaniox.heavyspleef.core.config.DefaultConfig;
import de.xaniox.heavyspleef.core.game.Game;
import de.xaniox.heavyspleef.core.hook.HookManager;
import de.xaniox.heavyspleef.core.hook.HookReference;
import de.xaniox.heavyspleef.core.i18n.I18N;
import de.xaniox.heavyspleef.core.i18n.I18NManager;
import org.apache.commons.lang.Validate;
import org.bukkit.plugin.PluginManager;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.lang.reflect.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;

public class FlagRegistry {

    public static final String FLAG_PATH_SEPERATOR = ":";

    private final HeavySpleef heavySpleef;
    private final I18NSupplier GLOBAL_SUPPLIER = new I18NSupplier() {

        @Override
        public I18N supply() {
            return I18NManager.getGlobal();
        }
    };
    private final DualMaps.Mapper<FlagClassHolder, Class<? extends AbstractFlag<?>>> valueMapper = new DualMaps.Mapper<FlagClassHolder, Class<? extends AbstractFlag<?>>>() {

        @Override
        public Class<? extends AbstractFlag<?>> map(FlagClassHolder from) {
            return from.flagClass;
        }
    };
    private DualKeyBiMap<String, Flag, FlagClassHolder> registeredFlagsMap;
    private Queue<Method> queuedInitMethods;
    private Set<Injector<AbstractFlag<?>>> staticInjectors;
    private Set<Injector<AbstractFlag<?>>> instanceInjectors;
    private InitializationPolicy initializationPolicy;

    public FlagRegistry(HeavySpleef heavySpleef) {
        this.heavySpleef = heavySpleef;
        this.registeredFlagsMap = new DualKeyHashBiMap<String, Flag, FlagClassHolder>(String.class, Flag.class);
        this.queuedInitMethods = Lists.newLinkedList();
        this.staticInjectors = Sets.newHashSet();
        this.instanceInjectors = Sets.newHashSet();
        this.initializationPolicy = InitializationPolicy.COMMIT;
    }

    public void registerFlag(Class<? extends AbstractFlag<?>> clazz) {
        registerFlag(clazz, null, null);
    }

    public void registerFlag(Class<? extends AbstractFlag<?>> clazz, I18NSupplier i18nSupplier, Object cookie) {
        Validate.notNull(clazz, "clazz cannot be null");
        Validate.isTrue(!isFlagPresent(clazz), "Cannot register flag twice: " + clazz.getName());

        if (i18nSupplier == null) {
            i18nSupplier = GLOBAL_SUPPLIER;
        }

        /* Check if the class provides the required Flag annotation */
        Validate.isTrue(clazz.isAnnotationPresent(Flag.class),
                "Flag-Class must be annotated with the @Flag annotation");

        Flag flagAnnotation = clazz.getAnnotation(Flag.class);
        String name = flagAnnotation.name();

        Validate.isTrue(!name.isEmpty(),
                "name() of annotation of flag for class " + clazz.getCanonicalName() + " cannot be empty");

        /* Generate a path */
        StringBuilder pathBuilder = new StringBuilder();
        Flag parentFlagData = flagAnnotation;

        do {
            pathBuilder.insert(0, parentFlagData.name());

            Class<? extends AbstractFlag<?>> parentFlagClass = parentFlagData.parent();
            parentFlagData = parentFlagClass.getAnnotation(Flag.class);

            if (parentFlagData != null && parentFlagClass != NullFlag.class) {
                pathBuilder.insert(0, FLAG_PATH_SEPERATOR);
            }
        } while (parentFlagData != null);

        String path = pathBuilder.toString();

        /* Check for name collides */
        for (String flagPath : registeredFlagsMap.primaryKeySet()) {
            if (flagPath.equalsIgnoreCase(path)) {
                throw new IllegalArgumentException("Flag " + clazz.getName() + " collides with "
                        + registeredFlagsMap.get(flagPath).flagClass.getName());
            }
        }

        /* Check if the class can be instantiated */
        try {
            Constructor<? extends AbstractFlag<?>> constructor = clazz.getDeclaredConstructor();

            //Make the constructor accessible for future uses
            constructor.setAccessible(true);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new IllegalArgumentException("Flag-Class must provide an empty constructor");
        }

        FlagClassHolder holder = new FlagClassHolder();
        holder.flagClass = clazz;
        holder.supplier = i18nSupplier;
        holder.cookie = cookie;

        Field[] instanceInjectableFields = null;

        if (checkHooks(flagAnnotation)) {
            inject(holder, null, null);
            instanceInjectableFields = getInjectableDeclaredFieldsByFilter(clazz,
                    new FieldFilter(FieldFilter.INSTANCE_MODE));

            if (initializationPolicy == InitializationPolicy.COMMIT) {
                Method[] initMethods = getInitMethods(holder);
                for (Method method : initMethods) {
                    queuedInitMethods.offer(method);
                }
            } else if (initializationPolicy == InitializationPolicy.REGISTER) {
                runInitMethods(holder);
            }

            if (flagAnnotation.hasCommands()) {
                CommandManager manager = heavySpleef.getCommandManager();
                manager.registerSpleefCommands(clazz);
            }

            holder.staticFieldsInjected = true;
            holder.staticMethodsInitialized = true;
        }

        holder.injectingFields = instanceInjectableFields;
        registeredFlagsMap.put(path, flagAnnotation, holder);

        if (heavySpleef.isGamesLoaded()) {
            for (Game game : heavySpleef.getGameManager().getGames()) {
                for (AbstractFlag<?> flag : game.getFlagManager().getFlags()) {
                    if (!(flag instanceof UnloadedFlag)) {
                        continue;
                    }

                    UnloadedFlag unloaded = (UnloadedFlag) flag;
                    if (!unloaded.getFlagName().equals(path)) {
                        continue;
                    }

                    game.removeFlag(path);

                    AbstractFlag<?> newFlag = newFlagInstance(path, AbstractFlag.class, game);
                    newFlag.unmarshal(unloaded.getXmlElement());

                    game.addFlag(newFlag);
                }
            }
        }
    }

    public void unregister(Class<? extends AbstractFlag<?>> flagClass) {
        String path = null;

        for (Entry<DualKeyMap.DualKeyPair<String, Flag>, FlagClassHolder> entry : registeredFlagsMap.entrySet()) {
            FlagClassHolder holder = entry.getValue();

            if (holder.flagClass != flagClass) {
                continue;
            }

            Flag annotation = entry.getKey().getSecondaryKey();
            if (annotation.hasCommands()) {
                CommandManager manager = heavySpleef.getCommandManager();
                manager.unregisterSpleefCommand(flagClass);
            }

            Iterator<Method> methodIterator = queuedInitMethods.iterator();
            while (methodIterator.hasNext()) {
                Method method = methodIterator.next();
                if (method.getDeclaringClass() == flagClass) {
                    methodIterator.remove();
                }
            }

            Unregister.Unregisterer.runUnregisterMethods(flagClass, heavySpleef, true, true);
            path = entry.getKey().getPrimaryKey();

            for (Game game : heavySpleef.getGameManager().getGames()) {
                if (!game.isFlagPresent(flagClass)) {
                    continue;
                }

                AbstractFlag<?> flag = game.getFlag(flagClass);
                game.removeFlag(flagClass);

                Element element = DocumentHelper.createElement("flag");
                element.addAttribute("name", path);
                flag.marshal(element);

                UnloadedFlag unloaded = new UnloadedFlag();
                unloaded.setXmlElement(element);
                game.addFlag(unloaded, false);
            }
            break;
        }

        if (path != null) {
            registeredFlagsMap.remove(path);
        }
    }

    public Flag getFlagData(Class<? extends AbstractFlag<?>> clazz) {
        for (Entry<FlagClassHolder, DualKeyMap.DualKeyPair<String, Flag>> entry : registeredFlagsMap.inverse()
                .entrySet()) {
            FlagClassHolder holder = entry.getKey();
            if (clazz != holder.flagClass) {
                continue;
            }

            return entry.getValue().getSecondaryKey();
        }

        throw new NoSuchFlagException(clazz.getName());
    }

    public boolean isFlagPresent(String flagPath) {
        return getFlagClass(flagPath) != null;
    }

    public boolean isFlagPresent(Class<? extends AbstractFlag<?>> flagClass) {
        for (FlagClassHolder holder : registeredFlagsMap.values()) {
            if (holder.flagClass == flagClass) {
                return true;
            }
        }

        return false;
    }

    /* Reverse path lookup */
    public Class<? extends AbstractFlag<?>> getFlagClass(String flagPath) {
        for (Entry<DualKeyMap.DualKeyPair<String, Flag>, FlagClassHolder> entry : registeredFlagsMap.entrySet()) {
            if (!entry.getKey().getPrimaryKey().equalsIgnoreCase(flagPath)) {
                continue;
            }

            return entry.getValue().flagClass;
        }

        return null;
    }

    public String getFlagPath(Class<? extends AbstractFlag<?>> flagClass) {
        for (Entry<DualKeyMap.DualKeyPair<String, Flag>, FlagClassHolder> entry : registeredFlagsMap.entrySet()) {
            if (entry.getValue().flagClass != flagClass) {
                continue;
            }

            return entry.getKey().getPrimaryKey();
        }

        return null;
    }

    public DualKeyBiMap<String, Flag, Class<? extends AbstractFlag<?>>> getAvailableFlags() {
        return DualMaps.valueMappedImmutableDualBiMap(registeredFlagsMap, valueMapper);
    }

    public <T extends AbstractFlag<?>> T newFlagInstance(String name, Class<T> expected) {
        return newFlagInstance(name, expected, null);
    }

    @SuppressWarnings("unchecked")
    public <T extends AbstractFlag<?>> T newFlagInstance(String name, Class<T> expected, Game game) {
        FlagClassHolder holder = registeredFlagsMap.get(name);
        if (holder == null) {
            throw new NoSuchFlagException(name);
        }

        if (expected == null) {
            throw new IllegalArgumentException("expected cannot be null");
        }

        if (!expected.isAssignableFrom(holder.flagClass)) {
            throw new NoSuchFlagException("Expected class " + expected.getName() + " is not compatible with "
                    + holder.flagClass.getName());
        }

        //Do a check on plugin dependencies
        Flag annotation = registeredFlagsMap.inverse().get(holder).getSecondaryKey();
        if (!checkHooks(annotation)) {
            throw new IllegalStateException(
                    "Cannot instantiate flag when its plugin dependencies are not available");
        }

        if (!holder.staticFieldsInjected) {
            inject(holder, null, null);
            holder.staticFieldsInjected = true;
        }

        if (!holder.staticMethodsInitialized) {
            runInitMethods(holder);
            holder.staticMethodsInitialized = true;
        }

        try {
            AbstractFlag<?> flag = holder.flagClass.newInstance();
            flag.setHeavySpleef(heavySpleef);
            flag.setI18N(holder.supplier.supply());

            inject(holder, flag, game);

            return (T) flag;
        } catch (InstantiationException | IllegalAccessException e) {
            //This should not happen as we made the constructor
            //accessible while the class was registered

            //But to be sure throw a RuntimeException
            throw new RuntimeException(e);
        }
    }

    private boolean checkHooks(Flag annotation) {
        HookReference[] refs = annotation.depend();
        String[] pluginDepends = annotation.pluginDepend();

        HookManager hookManager = heavySpleef.getHookManager();
        PluginManager pluginManager = heavySpleef.getPlugin().getServer().getPluginManager();
        boolean hooksPresent = true;

        for (HookReference ref : refs) {
            if (!hookManager.getHook(ref).isProvided()) {
                hooksPresent = false;
            }
        }

        for (String pluginDepend : pluginDepends) {
            if (!pluginManager.isPluginEnabled(pluginDepend)) {
                hooksPresent = false;
            }
        }

        return hooksPresent;
    }

    private void inject(FlagClassHolder holder, AbstractFlag<?> instance, final Game game) {
        Field[] injectingFields;
        Class<? extends AbstractFlag<?>> clazz = holder.flagClass;

        if (instance == null) {
            injectingFields = getInjectableDeclaredFieldsByFilter(clazz, new FieldFilter(FieldFilter.STATIC_MODE));
        } else {
            if (holder.injectingFields != null) {
                injectingFields = holder.injectingFields;
            } else {
                injectingFields = getInjectableDeclaredFieldsByFilter(clazz,
                        new FieldFilter(FieldFilter.INSTANCE_MODE));
            }
        }

        if (instance != null) {
            Injector<AbstractFlag<?>> baseInjector = new Injector<AbstractFlag<?>>() {

                @Override
                public void inject(AbstractFlag<?> instance, Field[] injectableFields, Object cookie)
                        throws IllegalArgumentException, IllegalAccessException {
                    DefaultConfig config = heavySpleef.getConfiguration(ConfigType.DEFAULT_CONFIG);

                    for (Field field : injectableFields) {
                        if (field.getType() == Game.class) {
                            field.set(instance, game);
                        } else if (field.getType() == DefaultConfig.class) {
                            field.set(instance, config);
                        }
                    }
                }
            };

            try {
                baseInjector.inject(instance, injectingFields, holder);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new RuntimeException("Failed to run basic injector", e);
            }
        }

        for (Injector<AbstractFlag<?>> injector : (instance == null ? staticInjectors : instanceInjectors)) {
            try {
                injector.inject(instance, injectingFields, holder);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new RuntimeException(
                        "Could not inject fields when running injector '" + injector.getClass().getName() + "'", e);
            }
        }
    }

    private Field[] getInjectableDeclaredFieldsByFilter(Class<?> clazz, Predicate<Field> predicate) {
        Set<Field> fieldList = Sets.newHashSet();
        Class<?> currentClass = clazz;

        do {
            Field[] fields = currentClass.getDeclaredFields();

            for (Field field : fields) {
                if (!field.isAnnotationPresent(Inject.class)) {
                    continue;
                }

                if (!predicate.apply(field)) {
                    continue;
                }

                field.setAccessible(true);
                fieldList.add(field);
            }

            currentClass = currentClass.getSuperclass();
        } while (AbstractFlag.class.isAssignableFrom(currentClass));

        return fieldList.toArray(new Field[fieldList.size()]);
    }

    public void flushAndExecuteInitMethods() {
        while (!queuedInitMethods.isEmpty()) {
            Method method = queuedInitMethods.poll();
            runInitMethod(method);
        }
    }

    private void runInitMethods(FlagClassHolder holder) {
        Method[] initMethods = getInitMethods(holder);
        for (Method method : initMethods) {
            runInitMethod(method);
        }
    }

    private void runInitMethod(Method method) {
        Class<?>[] parameters = method.getParameterTypes();
        Object[] args = new Object[parameters.length];

        for (int i = 0; i < parameters.length; i++) {
            Class<?> parameter = parameters[i];
            if (parameter == HeavySpleef.class) {
                args[i] = heavySpleef;
            }
        }

        try {
            method.invoke(null, args);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new IllegalArgumentException("Could not invoke flag initialization method " + method.getName()
                    + " of type " + method.getDeclaringClass().getCanonicalName() + ": ", e);
        }
    }

    private Method[] getInitMethods(FlagClassHolder holder) {
        Class<? extends AbstractFlag<?>> clazz = holder.flagClass;
        List<Method> methods = Lists.newArrayList();

        for (Method method : clazz.getDeclaredMethods()) {
            if (!method.isAnnotationPresent(FlagInit.class)) {
                continue;
            }

            if ((method.getModifiers() & Modifier.STATIC) == 0) {
                continue;
            }

            method.setAccessible(true);
            methods.add(method);
        }

        return methods.toArray(new Method[methods.size()]);
    }

    public List<Class<? extends AbstractFlag<?>>> getChildFlags(Class<? extends AbstractFlag<?>> flagClass) {
        List<Class<? extends AbstractFlag<?>>> childs = Lists.newArrayList();
        for (FlagClassHolder holder : registeredFlagsMap.values()) {
            if (!isChildFlag(flagClass, holder.flagClass)) {
                continue;
            }

            childs.add(holder.flagClass);
        }

        return childs;
    }

    public boolean isChildFlag(Class<? extends AbstractFlag<?>> parent,
            Class<? extends AbstractFlag<?>> childCandidate) {
        Validate.notNull(parent, "Parent cannot be null");
        Validate.notNull(childCandidate, "Child candidate cannot be null");

        BiMap<FlagClassHolder, DualKeyMap.DualKeyPair<String, Flag>> inverse = registeredFlagsMap.inverse();
        FlagClassHolder foundHolder = null;

        for (FlagClassHolder holder : inverse.keySet()) {
            if (holder.flagClass == childCandidate) {
                foundHolder = holder;
                break;
            }
        }

        Validate.isTrue(foundHolder != null,
                "Child candidate flag " + childCandidate.getName() + " has not been registered");

        Flag annotation = inverse.get(foundHolder).getSecondaryKey();

        Validate.isTrue(annotation != null, "ChildCandidate has not been registered");
        boolean directChild = annotation != null && annotation.parent() != NullFlag.class
                && annotation.parent() == parent;
        if (directChild) {
            return true;
        }

        //Do a search on the path
        Class<? extends AbstractFlag<?>> recentChildParent = annotation.parent();

        loop: do {
            if (recentChildParent == parent) {
                return true;
            }

            for (FlagClassHolder holder : inverse.keySet()) {
                if (holder.flagClass != recentChildParent) {
                    continue;
                }

                //Get the parent of the parent
                Flag parentData = holder.flagClass.getAnnotation(Flag.class);
                recentChildParent = parentData.parent();
                continue loop;
            }

            recentChildParent = null;
        } while (recentChildParent != null && recentChildParent != NullFlag.class);
        //NullFlag is the root parent as annotations require non-null values

        return false;
    }

    public void registerInjector(Injector<AbstractFlag<?>> injector) {
        staticInjectors.add(injector);
        instanceInjectors.add(injector);
    }

    public void registerInjector(Injector<AbstractFlag<?>> injector, boolean isStatic) {
        Set<Injector<AbstractFlag<?>>> injectorSet = isStatic ? staticInjectors : instanceInjectors;

        Validate.isTrue(!injectorSet.contains(injector), "Injector already registered");
        Validate.notNull(injector, "Injector cannot be null");

        injectorSet.add(injector);
    }

    public void unregisterInjector(Injector<AbstractFlag<?>> injector) {
        Validate.isTrue(instanceInjectors.contains(injector) || staticInjectors.contains(injector),
                "Injector has not been registered");

        instanceInjectors.remove(injector);
        staticInjectors.remove(injector);
    }

    public void setInitializationPolicy(InitializationPolicy policy) {
        this.initializationPolicy = policy;
    }

    public enum InitializationPolicy {

        REGISTER, COMMIT;

    }

    public static interface I18NSupplier {

        public I18N supply();

    }

    private class FieldFilter implements Predicate<Field> {

        public static final int INSTANCE_MODE = 0;
        public static final int STATIC_MODE = 1;

        private int mode;

        public FieldFilter(int mode) {
            this.mode = mode;
        }

        @Override
        public boolean apply(Field input) {
            int modifiers = input.getModifiers();
            int result = (modifiers & Modifier.STATIC);

            return mode == 0 ? result == 0 : mode != 1 || result != 0;
        }
    };

    public class FlagClassHolder {

        private Class<? extends AbstractFlag<?>> flagClass;
        private Field[] injectingFields;
        private I18NSupplier supplier;
        private Object cookie;
        private boolean staticFieldsInjected;
        private boolean staticMethodsInitialized;

        public Class<? extends AbstractFlag<?>> getFlagClass() {
            return flagClass;
        }

        public Field[] getInjectingFields() {
            return injectingFields;
        }

        public I18NSupplier getSupplier() {
            return supplier;
        }

        public Object getCookie() {
            return cookie;
        }

        public boolean isStaticFieldsInjected() {
            return staticFieldsInjected;
        }

        public boolean isStaticMethodsInitialized() {
            return staticMethodsInitialized;
        }

    }

}