Java tutorial
/* * 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); } }