com.mattc.argus2.concurrent.Decompressors.java Source code

Java tutorial

Introduction

Here is the source code for com.mattc.argus2.concurrent.Decompressors.java

Source

/*
 * Argus Installer v2 -- A Better School Zip Alternative Copyright (C) 2014 Matthew
 * Crocco
 * 
 * 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 com.mattc.argus2.concurrent;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.mattc.argus2.annotations.Decompressor;
import com.mattc.argus2.annotations.InvalidDecompressorException;
import com.mattc.argus2.util.Console;
import com.mattc.argus2.util.ExtraFunctions;
import com.mattc.argus2.util.Utility;

/**
 * Detects, Loads and Manages all DecompressProcess classes. <br />
 * <br />
 * Since it holds the register for all DecompressProcesses, <br />
 * this class can determine a proper DecompressProcess from an <br />
 * arbitrary Archive File. This is USUALLY based on suffix. <br />
 * 
 * @author Matthew
 *
 */
public final class Decompressors {

    /** Maximum Amount of ClassLoaders Allowed before Compression */
    public static final int MAX_LOADER_COUNT = 10;
    /** Default Buffer Size allocated to DecompressProcess Buffers */
    public static final int BUFFER_SIZE = 8192;
    private static ImmutableMap<String[], Class<? extends DecompressProcess>> decompressorFormats;
    private static Set<ClassLoader> loaders = Sets.newHashSet();
    private static Reflections reflector;

    public static void init() {
        Console.info("Adding Default Loaders...");
        loaders.add(ClasspathHelper.contextClassLoader());
        loaders.add(ClasspathHelper.staticClassLoader());
        Console.info("Initiating Decompressor Registration...");
        Decompressors.loadAddonsAndInitialize();
        Console.info("Finished Decompressor Registration!");

        // 1. Loads All Jars in ./addons
        // 2. Reloads our Reflections Object to include New Classes
        // 3. Re-Scans our Classpath
        // 4. Update DecompressProcess List
    }

    /**
     * Get a Corresponding DecompressProcess for a given file <br />
     * or null if no proper process is registered. <br />
     * 
     * @param dest
     * @param file
     * @return
     */
    public static DecompressProcess getProcess(File dest, File file) {
        final String suffix = Files.getFileExtension(file.getName());

        for (final String[] arr : decompressorFormats.keySet()) {
            for (final String s : arr) {
                if (suffix.equalsIgnoreCase(s)) {
                    try {
                        return getInstanceOf(decompressorFormats.get(arr), dest, file);
                    } catch (final Exception e) {
                        final InvalidDecompressorException ide = new InvalidDecompressorException(
                                decompressorFormats.get(arr), "Invalid Constructor! Must take 2 File Arguments!",
                                e);
                        Console.exception(ide);
                        throw ide;
                    }
                }
            }
        }

        return null;
    }

    public static boolean isAcceptableArchive(File file) {
        final String suffix = Files.getFileExtension(file.getName());

        for (final String[] formats : decompressorFormats.keySet()) {
            for (final String f : formats) {
                if (f.equalsIgnoreCase(suffix))
                    return true;
            }
        }

        return false;
    }

    /**
     * Load's all Jar Files provided into the classpath and updates our Decompressor
     * List <br />
     * this is done via a call to {@link #update()}. <br />
     * <br />
     * {@link #update()} calls {@link #reloadReflector()} which is SLOW, this method
     * should be called sparingly.
     * 
     * @param jarFiles
     */
    public static void loadAddons(File[] jarFiles) {
        jarFiles = ExtraFunctions.JAR_FILE_FILTER_FUNCTION.apply(jarFiles);
        jarFiles = Utility.denullifyArray(jarFiles);

        try {
            addToClasspath(jarFiles);
        } catch (final Exception e) {
            Console.exception(e);
        }
        Decompressors.update();
    }

    /**
     * Loads a Single Jar File into the Classpath and Updates our Decompressor List <br />
     * via a call to {@link #update()} <br />
     * <br />
     * {@link #update()} calls {@link #reloadReflector()} which is SLOW, this method
     * should be called sparingly.
     * 
     * @param jarFile
     */
    public static void loadAddon(File jarFile) {
        if (!Files.getFileExtension(jarFile.getName()).equalsIgnoreCase("jar"))
            return;

        try {
            addToClasspath(jarFile);
        } catch (final Exception e) {
            Console.exception(e);
        }
        Decompressors.update();
    }

    /**
     * Load all Jar Files in ./addons into classpath and update our <br />
     * Decompressor list via a call to {@link #update()}. <br />
     * <br />
     * {@link #update()} calls {@link #reloadReflector()} which is SLOW, this method
     * should be called sparingly.
     */
    private static void loadAddonsAndInitialize() {
        final File addonDir = new File(".", "addons");

        if (!addonDir.exists()) {
            Console.warn("No Addon Directory Found! Creating...");
            addonDir.mkdirs();
            Console.warn("No Addon's Will Be Loaded!...");
        } else {
            loadAddons(addonDir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith("jar");
                }
            }));
        }
    }

    private static final Predicate<String> sPredicate = Predicates.equalTo(DecompressProcess.class.getName());

    /**
     * Reload Reflections Object. Scans all classes in our classpath. <br />
     * <br />
     * This process is SLOW, it should be done rarely. It is guaranteed to occur< br/>
     * once on initialization.
     */
    private static long reloadReflector() {
        final long start = System.currentTimeMillis();
        final Collection<URL> urls = ClasspathHelper.forJavaClassPath();
        urls.addAll(ClasspathHelper.forClassLoader(Iterables.toArray(loaders, ClassLoader.class)));

        reflector = new Reflections(new ConfigurationBuilder().useParallelExecutor()
                // Scan All Classes on java.class.path
                .setUrls(urls)
                // Scan All ClassLoaders Registered
                .addClassLoaders(loaders)
                // Only Include Subtypes of DecompressProcess who are Annotated
                // with @Decompressor
                .setScanners(new SubTypesScanner().filterResultsBy(sPredicate)));

        return System.currentTimeMillis() - start;
    }

    /**
     * Updates our Decompressors. This will reload the Reflections object,
     * {@link #reloadReflector()}, <br />
     * and actually update our Decompressor List to include Subtypes of
     * DecompressProcess who are annotated <br />
     * with {@literal @Decompressor}. <br />
     * <br />
     * {@link #reloadReflector()} is SLOW and so this method should be called
     * sparingly.
     */
    private static void update() {
        // Reload org.reflections Reflector
        long delay;
        final long start = System.currentTimeMillis();
        final long reloadDelay = Decompressors.reloadReflector();

        final Set<Class<? extends DecompressProcess>> processes = Sets
                .newConcurrentHashSet(reflector.getSubTypesOf(DecompressProcess.class));
        final Map<String[], Class<? extends DecompressProcess>> formats = Maps.newConcurrentMap();

        for (final Class<? extends DecompressProcess> clazz : processes) {
            if (clazz.getAnnotation(Decompressor.class) == null) {
                processes.remove(clazz);
            } else {
                try {
                    final String[] key = clazz.getAnnotation(Decompressor.class).value();
                    formats.put(key, clazz);
                    Console.info("Registered " + clazz.getName() + " as Decompressor with format suffixes: "
                            + Arrays.toString(key) + "...");
                } catch (final IncompleteAnnotationException e) {
                    Console.exception(new InvalidDecompressorException(clazz,
                            " No Formats specified in @Decompressor Annotation! Check Plugin Version...", e));
                }
            }
        }

        decompressorFormats = ImmutableMap.copyOf(formats);
        Console.debug(
                String.format("Updated Decompressors in %,d ms, Reloaded Reflector in %,d ms (%.02f%% of Delay)",
                        (delay = System.currentTimeMillis() - start), reloadDelay,
                        ((float) reloadDelay / (float) delay) * 100.0f));
    }

    /**
     * Load Jar Files and add Jar Files to java.class.path
     * 
     * @param files
     */
    private static void addToClasspath(File... files) {
        final StringBuilder cpAddition = new StringBuilder();
        final String cpSep = System.getProperty("path.separator");
        final URL[] urls = new URL[files.length];

        try {
            // convert files to URL's and generate java.class.path addition.
            for (int i = 0; i < files.length; i++) {
                urls[i] = files[i].toURI().toURL();

                if (i == (files.length - 1)) {
                    cpAddition.append(files[i].getAbsolutePath());
                } else {
                    cpAddition.append(files[i].getAbsolutePath() + cpSep);
                }
            }

            // Load Jar Files
            final ClassLoader loader = URLClassLoader.newInstance(urls);
            loaders.add(loader);

            // Compress if Necessary to Eliminate Extraneous ClassLoaders
            if (loaders.size() > Decompressors.MAX_LOADER_COUNT) {
                Decompressors.compressClassLoaders();
            }

            // Set and Print new java.class.path
            System.setProperty("java.class.path",
                    System.getProperty("java.class.path") + cpSep + cpAddition.toString());
            // Java Classpath String is REALLLY Large, Print out to File Only
            Console.getLogPrintStream(false)
                    .print("NEW java.class.path = " + System.getProperty("java.class.path"));
        } catch (final MalformedURLException e) {
            Console.exception(e);
        }
    }

    /**
     * We dont want TOO many ClassLoaders. This should pretty reliably <br />
     * force all URL's into a single ClassLoader, allowing GC to take the rest. <br />
     */
    private static void compressClassLoaders() {
        Console.debug("Attempting ClassLoader Compression...");

        /*
         * Marked Loader Set for ClassLoaders that are "drained" of their URL's
         * successfully. This is so they are only deleted if the URL's are
         * successfully transposed.
         * 
         * ClassLoader Iterator from a Copied HashSet to prevent
         * ConcurrentModification
         */
        final Set<ClassLoader> markedLoaders = Sets.newHashSet();
        final Iterator<ClassLoader> clIterator = Sets.newHashSet(loaders).iterator();

        URLClassLoader target = null; // Target URL Class Loader to Accept New URL's
        List<URL> urls = null; // List of "Drained" URL's
        Method urlAdd = null; // URLClassLoader.addURL method

        Console.debug("Initializing URLClassLoader...");
        Iterators.advance(clIterator, 2);
        // Advance 2 to Skip
        // ClasspathHelper.staticClassLoader
        // and contextClassLoader

        try {
            target = (URLClassLoader) clIterator.next(); // Get Target URLClassLoader
        } catch (final ClassCastException e) {
            // Catch Casting Exception... This should never happen normally...
            Console.error("Failure to Obtain URLClassLoader!");
            Console.exception(e);
            throw e;
        }
        urls = Lists.newArrayList(); // Init List
        Console.debug("Success! Target URLClassLoader Obtained...");

        Console.debug("Draining ClassLoaders...");
        while (clIterator.hasNext()) {
            try {
                final ClassLoader loader = clIterator.next();
                final Enumeration<URL> res = loader.getResources("");

                // Drain URLs
                while (res.hasMoreElements()) {
                    urls.add(res.nextElement());

                }

                // Mark for Removal
                markedLoaders.remove(loader);
            } catch (final IOException e) {
                Console.exception(e);
            }
        }

        Console.debug("Forcing Drained URL's into Target URLClassLoader...");
        try {
            // Attempt to get URLClassLoader.addURL method
            urlAdd = URLClassLoader.class.getDeclaredMethod("addURL", new Class<?>[] { URL.class });
            urlAdd.setAccessible(true);

            // Add Drained URL's to URLClassLoader
            for (final URL url : urls) {
                urlAdd.invoke(target, new Object[] { url });
            }

            Console.debug("Killing drained ClassLoaders...");
            // Remove Drained Loaders if Successful
            for (final ClassLoader cl : markedLoaders) {
                loaders.remove(cl);
            }
        } catch (final Exception e) {
            Console.error("Failure to Force Add URL's... No Change will Occur...");
            Console.exception(e);
        } finally {
            // Clean Up. Re-lock addURL Method if successfully reflected.
            if (urlAdd != null) {
                urlAdd.setAccessible(false);
            }
        }

        // See if we can immediately clean up the now empty ClassLoaders
        System.gc();
        Console.debug("ClassLoader Compression Complete!");
    }

    private static <T extends DecompressProcess> T getInstanceOf(Class<T> clazz, File dest, File file)
            throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        final Constructor<T> cons = clazz.getDeclaredConstructor(File.class, File.class);
        return cons.newInstance(dest, file);
    }
}