com.codelanx.codelanxlib.util.Reflections.java Source code

Java tutorial

Introduction

Here is the source code for com.codelanx.codelanxlib.util.Reflections.java

Source

/*
 * Copyright (C) 2015 Codelanx, All Rights Reserved
 *
 * This work is licensed under a Creative Commons
 * Attribution-NonCommercial-NoDerivs 3.0 Unported License.
 *
 * This program is protected software: You are free to distrubute your
 * own use of this software under the terms of the Creative Commons BY-NC-ND
 * license as published by Creative Commons in the year 2015 or as published
 * by a later date. You may not provide the source files or provide a means
 * of running the software outside of those licensed to use it.
 *
 * 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.
 *
 * You should have received a copy of the Creative Commons BY-NC-ND license
 * long with this program. If not, see <https://creativecommons.org/licenses/>.
 */
package com.codelanx.codelanxlib.util;

import com.codelanx.codelanxlib.annotation.PluginClass;
import com.codelanx.codelanxlib.logging.Debugger;
import com.codelanx.codelanxlib.util.exception.Exceptions;
import com.google.common.primitives.Primitives;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import org.apache.commons.lang.Validate;
import org.bukkit.plugin.java.JavaPlugin;

/**
 * Represents utility functions that utilize either java's reflection api,
 * analysis of the current Stack in use, low-level operations, primitives, or
 * other methods that deal with operations outside the norm of Java or Bukkit's
 * own system
 *
 * @since 0.1.0
 * @author 1Rogue
 * @version 0.1.0
 */
public final class Reflections {

    private Reflections() {
    }

    /**
     * Returns the relevant {@link JavaPlugin} that is specified by a
     * class-level {@link PluginClass} annotation if it is loaded, otherwise
     * {@code null}
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param clazz The {@link Class} that holds the {@link Annotation}
     * @return The relevant {@link JavaPlugin}, or {@code null} if not found
     */
    public static JavaPlugin getPlugin(Class<?> clazz) {
        PluginClass pc = clazz.getAnnotation(PluginClass.class);
        if (pc == null) {
            return null;
        }
        return JavaPlugin.getPlugin(pc.value());
    }

    /**
     * Returns {@code true} if the specified target has the passed
     * {@link Annotation}
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param target The relevant element to check for an {@link Annotation}
     * @param check The {@link Annotation} class type to check for
     * @return {@code true} if the {@link Annotation} is present
     */
    public static boolean hasAnnotation(AnnotatedElement target, Class<? extends Annotation> check) {
        return target.getAnnotation(check) != null;
    }

    /**
     * Returns whether or not the current context was called from a class
     * (instance or otherwise) that is passed to this method. This method can
     * match a regex pattern for multiple classes. Note anonymous classes have
     * an empty name.
     *
     * @since 0.1.0
     * @version 0.1.0
     *
     * @param regex The regex to check against the calling class
     * @return {@code true} if accessed from a class that matches the regex
     */
    public static boolean accessedFrom(String regex) {
        return Reflections.getCaller(1).getClassName().matches(regex);
    }

    /**
     * Returns whether or not the current context was called from a class
     * (instance or otherwise) that is passed to this method. Note anonymous
     * classes have an empty name.
     *
     * @since 0.1.0
     * @version 0.1.0
     *
     * @param clazz The class to check
     * @return {@code true} if accessed from this class
     */
    public static boolean accessedFrom(Class<?> clazz) {
        return Reflections.getCaller(1).getClassName().equals(clazz.getName());
    }

    /**
     * Faade method for determining if Bukkit is the invoker of the method
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @return {@code true} if Bukkit is the direct invoker of the method
     */
    public static boolean accessedFromBukkit() {
        return Reflections.getCaller(1).getClassName().matches("org\\.bukkit\\..*");
    }

    /**
     * Returns the {@link JavaPlugin} that immediately called the method in the
     * current context. Useful for finding out which plugins accessed static API
     * methods
     *
     * @since 0.1.0
     * @version 0.1.0
     *
     * @param offset The number of additional methods to look back
     * @return The relevant {@link JavaPlugin}
     * @throws UnsupportedOperationException If not called from a
     * {@link JavaPlugin} class (either through an alternative ClassLoader,
     * executing code directly, or some voodoo magic)
     */
    public static JavaPlugin getCallingPlugin(int offset) {
        try {
            Class<?> cl = Class.forName(Reflections.getCaller(1 + offset).getClassName());
            JavaPlugin back = JavaPlugin.getProvidingPlugin(cl);
            if (back == null) {
                throw new UnsupportedOperationException("Must be called from a class loaded from a plugin");
            }
            return back;
        } catch (ClassNotFoundException ex) {
            //Potentially dangerous (Stackoverflow)
            Debugger.error(ex, "Error reflecting for plugin class");
        }
        return null;
    }

    /**
     * Returns the {@link JavaPlugin} that immediately called the method in the
     * current context. Useful for finding out which plugins accessed static API
     * methods. This method is equivalent to calling
     * {@code Reflections.getCallingPlugin(0)}
     *
     * @since 0.1.0
     * @version 0.1.0
     *
     * @see Reflections#getCallingPlugin(int)
     * @return The relevant {@link JavaPlugin}
     * @throws UnsupportedOperationException If not called from a
     * {@link JavaPlugin} class (either through an alternative ClassLoader,
     * executing code directly, or some voodoo magic)
     */
    public static JavaPlugin getCallingPlugin() {
        return Reflections.getCallingPlugin(1);
    }

    /**
     * Returns a {@link StackTraceElement} of the direct caller of the current
     * method's context.
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param offset The number of additional methods to look back
     * @return A {@link StackTraceElement} representing where the current
     *         context was called from
     */
    public static StackTraceElement getCaller(int offset) {
        Validate.isTrue(offset >= 0, "Offset must be a positive number");
        StackTraceElement[] elems = Thread.currentThread().getStackTrace();
        if (elems.length < 4 + offset) {
            //We shouldn't be able to get this high on the stack at theoritical offset 0
            throw new IndexOutOfBoundsException("Offset too large for current stack");
        }
        return elems[3 + offset];
    }

    /**
     * Returns a {@link StackTraceElement} of the direct caller of the current
     * method's context. This method is equivalent to calling
     * {@code Reflections.getCaller(0)}
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @return A {@link StackTraceElement} representing where the current
     *         context was called from
     */
    public static StackTraceElement getCaller() {
        return Reflections.getCaller(1);
    }

    /**
     * Checks whether or not there is a plugin on the server with the name of
     * the passed {@code name} paramater. This method achieves this by scanning
     * the plugins folder and reading the {@code plugin.yml} files of any
     * respective jarfiles in the directory.
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param name The name of the plugin as specified in the {@code plugin.yml}
     * @return The {@link File} for the plugin jarfile, or {@code null} if not
     *         found
     */
    public static File findPluginJarfile(String name) {
        File plugins = new File("plugins");
        Exceptions.illegalState(plugins.isDirectory(), "'plugins' isn't a directory! (wat)");
        for (File f : plugins.listFiles((File pathname) -> {
            return pathname.getPath().endsWith(".jar");
        })) {
            try (InputStream is = new FileInputStream(f); ZipInputStream zi = new ZipInputStream(is)) {
                ZipEntry ent = null;
                while ((ent = zi.getNextEntry()) != null) {
                    if (ent.getName().equalsIgnoreCase("plugin.yml")) {
                        break;
                    }
                }
                if (ent == null) {
                    continue; //no plugin.yml found
                }
                ZipFile z = new ZipFile(f);
                try (InputStream fis = z.getInputStream(ent);
                        InputStreamReader fisr = new InputStreamReader(fis);
                        BufferedReader scan = new BufferedReader(fisr)) {
                    String in;
                    while ((in = scan.readLine()) != null) {
                        if (in.startsWith("name: ")) {
                            if (in.substring(6).equalsIgnoreCase(name)) {
                                return f;
                            }
                        }
                    }
                }
            } catch (IOException ex) {
                Debugger.error(ex, "Error reading plugin jarfiles");
            }
        }
        return null;
    }

    /**
     * Returns a "default value" of -1 or {@code false} for a default type's
     * class or autoboxing class. Will return {@code null} if not relevant to
     * primitives
     *
     * @since 0.1.0
     * @version 0.1.0
     *
     * @param <T> The type of the primitive
     * @param c The primitive class
     * @return The default value, or {@code null} if not a primitive
     */
    public static <T> T defaultPrimitiveValue(Class<T> c) {
        if (c.isPrimitive() || Primitives.isWrapperType(c)) {
            c = Primitives.unwrap(c);
            T back;
            if (c == boolean.class) {
                back = c.cast(false);
            } else if (c == char.class) { //god help me
                back = c.cast((char) -1);
            } else if (c == float.class) {
                back = c.cast(-1F);
            } else if (c == long.class) {
                back = c.cast(-1L);
            } else if (c == double.class) {
                back = c.cast(-1D);
            } else if (c == int.class) {
                back = c.cast(-1); //ha
            } else if (c == short.class) {
                back = c.cast((short) -1);
            } else if (c == byte.class) {
                back = c.cast((byte) -1);
            }
        }
        return null;
    }

    /**
     * Returns a {@link Set} of keys that closest match the passed in string
     * value
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param map The {@link Map} with a string key to look through
     * @param search The string to use as a base to search for
     * @return A list of the closest matching keys, will be empty if no keys
     *         begin with the search phrase
     */
    public static Set<String> matchClosestKeys(Map<String, ?> map, String search) {
        return map.keySet().stream().filter(k -> k.startsWith(search)).collect(Collectors.toSet());
    }

    /**
     * Returns a {@link List} of values mapped from keys that closest match the
     * passed in string value
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param <T> The type of the values
     * @param map The {@link Map} to look through
     * @param search The string to use as a base to search for
     * @return A list of the closest matching values, will be empty if no keys
     *         begin with the search phrase
     */
    public static <T> List<T> matchClosestValues(Map<String, T> map, String search) {
        return Reflections.matchClosestKeys(map, search).stream().map(map::get).collect(Collectors.toList());
    }

    /**
     * Returns a new {@link ArrayList} of the passed parameters that is not
     * fixed-size, akin to what {@link Arrays#asList(Object...)} returns
     * 
     * @since 0.1.0
     * @version 0.1.0
     * 
     * @param <T> The type of the objects being passed in
     * @param items The parameters to add to a list
     * @return A new {@link List} of the items, or an empty list if no params
     */
    public static <T> List<T> nonFixedList(T... items) {
        return new ArrayList<>(Arrays.asList(items));
    }

}