/*==========================================================================*\ | $Id: Types.java,v 1.2 2011/03/07 22:56:53 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2011 Virginia Tech | | This file is part of the Student-Library. | | The Student-Library is free software; you can redistribute it and/or | modify it under the terms of the GNU Lesser General Public License as | published by the Free Software Foundation; either version 3 of the | License, or (at your option) any later version. | | The Student-Library 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 Lesser General Public License for more details. | | You should have received a copy of the GNU Lesser General Public License | along with the Student-Library; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package student.testingsupport.reflection.internal; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.lang.ref.SoftReference; import java.lang.reflect.Modifier; import java.net.JarURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import student.testingsupport.reflection.ReflectionError; import student.web.internal.MRUMap; //------------------------------------------------------------------------- /** * A set of static utility methods to look up {@link Class} objects, where * the results are backed by an internal cache. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.2 $, $Date: 2011/03/07 22:56:53 $ */ public class Types { //~ Fields ................................................................ private static MRUMap<ClassLoader, Map<String, PackageContent>> classesForPackage = new MRUMap<ClassLoader, Map<String, PackageContent>>(10, 0); private static MRUMap<ClassLoader, PackageContent> allClasses = new MRUMap<ClassLoader, PackageContent>(10, 0); private static Set<String> searchLocations = null; private static boolean searchDirectoriesOnly = true; //~ Constructor ........................................................... // ---------------------------------------------------------- /** * This class contains only static helper methods, and so it should * never be instantiated. */ private Types() { // never called } // ---------------------------------------------------------- // The real initialization is static static { String searchPath = null; // TODO: do privileged ... searchPath = System.getProperty(Types.class.getName() + ".searchPath"); if (searchPath != null) { restrictSearchesTo(searchPath); } } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * Restrict type searches that look for all classes in a package, or * all classes visible to a class loader, to the specified list of * search locations (the default is the search path in the system * property * <code>student.testingsupport.reflection.internal.Types.searchPath</code>, * or the set of directories visible from the class loader where * this class was loaded). * This restriction affects all type searches when a fully qualified * class name is not provided (e.g., when using * {@link #classesInPackage(String, ClassLoader)} or * {@link #allClasses(ClassLoader)} or * {@link #allClassesWithSimpleName(String, ClassLoader)}. * @param searchPath The search path to use. A classpath-like string * denoting the classpath locations where searching should be performed. */ public static void restrictSearchesTo(String searchPath) { synchronized (allClasses) { synchronized (classesForPackage) { allClasses.clear(); classesForPackage.clear(); searchDirectoriesOnly = false; if (searchPath == null) { searchLocations = null; } else { searchLocations = new HashSet<String>(); String[] locations = searchPath.split( "[:;]|\\Q" + File.pathSeparator + "\\E"); for (String location : locations) { if (!location.isEmpty()) { if (location.toLowerCase().endsWith(".jar")) { searchLocations.add( "jar:file:" + location+ "!/"); } else { if (!location.endsWith("/")) { location = location + "/"; } searchLocations.add("file:" + location); } } } } } } } // ---------------------------------------------------------- /** * Indicate whether type searches that look for all classes in a package, * or all classes visible to a class loader, should look only in * directories, or in both directories and jar files. * @param choice If true, searches will only examine directories; if * false, searches will also examine jar files. */ public static void searchDirectoriesOnly(boolean choice) { synchronized (allClasses) { synchronized (classesForPackage) { allClasses.clear(); classesForPackage.clear(); searchDirectoriesOnly = choice; } } } // ---------------------------------------------------------- /** * Determine whether an entity's modifiers indicate it has "default" * visibility (that is, package-level visibility). * @param modifiers The modifiers to check. * @return True if the modifiers indicate default visibility (that is, * they do not include private, protected, or public). */ public static boolean isPackageVisible(int modifiers) { return (modifiers & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.PUBLIC)) == 0; } // ---------------------------------------------------------- /** * Determine whether a specified class has "default" visibility (that is, * package-level visibility). * @param clazz The class to check. * @return True if the class has default visibility (that is, it is not * private, protected, or public). */ public static boolean isPackageVisible(Class<?> clazz) { return isPackageVisible(clazz.getModifiers()); } // ---------------------------------------------------------- /** * Determine whether a class is declared in a given package. * @param clazz The class to check. * @param packageName The name of the package to check for. Null (or the * empty string) mean the "default" package. * @return True if clazz is declared in the given package. */ public static boolean isInPackage(Class<?> clazz, String packageName) { if (packageName == null || packageName.isEmpty()) { return clazz.getPackage() == null; } else { return clazz.getPackage() != null && packageName.equals(clazz.getPackage().getName()); } } // ---------------------------------------------------------- /** * Determine whether two classes are declared in the same package. * @param class1 The first class to check. * @param class2 The second class to check. * @return True if class1 is declared in the same package as class2. */ public static boolean areInSamePackage(Class<?> class1, Class<?> class2) { Package pkg = class1.getPackage(); String pkgName = (pkg == null) ? null : pkg.getName(); return isInPackage(class2, pkgName); } // ---------------------------------------------------------- /** * Look up the class for a given name. The lookup is performed using * the thread's context class loader, or if that is unsuccessful, the * class loader in which Types was loaded. * @param fqcn The fully qualified class name. * @return The class with the given name, or null if none can be found. */ public static Class<?> classForName(String fqcn) { Class<?> result = classForName( fqcn, Thread.currentThread().getContextClassLoader()); if (result == null) { result = classForName(fqcn, Types.class.getClassLoader()); } return result; } // ---------------------------------------------------------- /** * Look up the class for a given name in the given class loader. * @param fqcn The fully qualified class name. * @param loader The class loader to use. * @return The class with the given name, or null if none can be found. */ public static Class<?> classForName(String fqcn, ClassLoader loader) { if (loader == null) { // Use the default mechanism return classForName(fqcn); } Class<?> result = null; try { result = loader.loadClass(fqcn); } catch (NoClassDefFoundError e) { System.out.println( "Unable to load class " + fqcn + " because of " + e.getClass().getName() + ": " + e.getMessage()); // let null be returned } catch (ClassNotFoundException e) { // ignore this, and let null be returned } catch (OutOfMemoryError e) { if (e.getMessage() != null && e.getMessage().contains("PermGen")) { long currentMaxPermSize = maxPermSize(); System.out.println( "Insufficient PermGen space to load all classes in " + "method\n" + Types.class.getName() + ".allClasses().\nCurrent max PermGen size = " + currentMaxPermSize + ".\n" + "Increase availabe PermGen by adding this command " + "line parameter:\n-XX:MaxPermSize=" + recommendedNewMaxSize(currentMaxPermSize) ); } throw e; } return result; } // ---------------------------------------------------------- /** * Find all the classes declared in the given package and visible using * the given class loader. * @param packageName The package name. Null or an empty string * represent the "default" package. * @param loader The class loader to use. * @return All classes declared in the given package, visible in the * given class loader. */ public static List<Class<?>> classesInPackage( String packageName, ClassLoader loader) { if (packageName == null) { packageName = ""; } if (loader == null) { loader = Thread.currentThread().getContextClassLoader(); } List<Class<?>> result = null; synchronized (classesForPackage) { Map<String, PackageContent> forLoader = classesForPackage.get(loader); if (forLoader == null) { forLoader = new HashMap<String, PackageContent>(); classesForPackage.put(loader, forLoader); } PackageContent pkg = forLoader.get(packageName); if (pkg == null) { pkg = new PackageContent(); forLoader.put(packageName, pkg); Set<String> names = new TreeSet<String>(); scanClassLoaderForNames(loader, names, packageName, false); pkg.setNames(names); } result = pkg.classes(loader); } return result; } // ---------------------------------------------------------- /** * Find all the classes visible using the given class loader. * <b>Use with care!</b> * Note that this method may require extra PermGen space, since it will * force <b>all</b> classes to be loaded simultaneously. Add * <code>-XX:MaxPermSize=128m</code> (or some other size) as a command * line argument if you must use this method but it runs out of PermGen * space. If PermGen space is exhausted, a diagnostic message * suggesting a larger size is necessary. * @param loader The class loader to use. * @return All classes visible in the given class loader. */ public static List<Class<?>> allClasses(ClassLoader loader) { if (loader == null) { loader = Thread.currentThread().getContextClassLoader(); } List<Class<?>> result = null; synchronized (allClasses) { PackageContent all = allClasses.get(loader); if (all == null) { all = new PackageContent(); allClasses.put(loader, all); Set<String> names = new TreeSet<String>(); scanClassLoaderForNames(loader, names, "", true); all.setNames(names); } result = all.classes(loader); } return result; } // ---------------------------------------------------------- /** * Find all the classes with the given simple name that are visible using * the given class loader, in any package. * @param simpleName The simple name (without any package prefix) * to look for. * @param loader The class loader to use. * @return All classes visible in the given class loader. */ public static List<Class<?>> allClassesWithSimpleName( String simpleName, ClassLoader loader) { if (loader == null) { loader = Thread.currentThread().getContextClassLoader(); } List<Class<?>> result = null; synchronized (allClasses) { PackageContent all = allClasses.get(loader); if (all == null) { all = new PackageContent(); allClasses.put(loader, all); Set<String> names = new TreeSet<String>(); scanClassLoaderForNames(loader, names, "", true); all.setNames(names); } result = all.classesWithSimpleName(simpleName, loader); } return result; } //~ Private Methods ....................................................... // ---------------------------------------------------------- private static void scanClassLoaderForNames( ClassLoader loader, Set<String> accumulator, String packageName, boolean recurse) { String packageDir = ""; if (packageName == null) { packageName = ""; } else { packageDir = packageName.replace('.', '/'); } Set<URL> locations = new HashSet<URL>(); try { Enumeration<URL> urls = loader.getResources(packageDir); while (urls.hasMoreElements()) { locations.add(urls.nextElement()); } } catch (IOException e) { throw new ReflectionError( "IO exception scanning class loader for classes: " + e.getMessage()); } if (packageName.isEmpty()) { // Then it is the default package, and we need to grab the // jars too. try { Enumeration<URL> urls = loader.getResources("META-INF"); while (urls.hasMoreElements()) { URL location = urls.nextElement(); if ("jar".equals(location.getProtocol())) { String locString = location.toString(); location = new URL(locString.substring( 0, locString.length() - "META-INF".length())); locations.add(location); } } } catch (IOException e) { throw new ReflectionError( "IO exception scanning class loader for classes: " + e.getMessage()); } } // System.out.println( // "locations for package " + packageName + " = " + locations); for (URL location : locations) { scanClasspathLocationForNames( location, accumulator, packageName, recurse); } } // ---------------------------------------------------------- private static void scanClasspathLocationForNames( URL location, Set<String> accumulator, String packageName, boolean recurse) { // Check searchLocations to make sure this location is included if (searchLocations != null) { String urlBase = location.toString(); if (packageName != null && !packageName.isEmpty()) { urlBase = urlBase.substring( 0, urlBase.length() - packageName.length()); } if (!searchLocations.contains(urlBase)) { // System.out.println("[skipping url: " + location + "]"); return; } } // System.out.println("[scanning url: " + location + "]"); // Check to see if it is a plain directory in the file system File directory = new File(location.getFile()); if (directory.exists()) { if (directory.isDirectory()) { scanClasspathLocationForNames( directory, accumulator, packageName, recurse); } } else if (!searchDirectoriesOnly) { // If it isn't a directory, then it is a jar file URL try { JarURLConnection conn = (JarURLConnection)location.openConnection(); JarFile jarFile = conn.getJarFile(); scanClasspathLocationForNames( jarFile, accumulator, packageName, recurse); jarFile.close(); } catch (IOException e) { throw new ReflectionError( "IO exception scanning " + location + " for classes: " + e.getMessage()); } } } // ---------------------------------------------------------- private static void scanClasspathLocationForNames( File directory, Set<String> accumulator, String packageName, boolean recurse) { // System.out.println("[scanning directory: " + directory + "]"); for (String fileName : directory.list()) { // System.out.println(" [scanning file: " + fileName + "]"); // we are only interested in .class files if (fileName.endsWith(".class")) { // removes the .class extension String className = fileName.substring( 0, fileName.length() - ".class".length()); // Ignore inner classes if (className.indexOf('$') > 0) { continue; } // Compute fully qualified class name String fqcn = packageName; if (fqcn == null || fqcn.isEmpty()) { fqcn = className; } else { fqcn += '.' + className; } // System.out.println(" [adding class: " + fqcn + "]"); accumulator.add(fqcn); } else if (recurse) { File subPackage = new File(directory, fileName); if (subPackage.exists() && subPackage.isDirectory()) { String subPackageName = packageName; if (subPackageName == null || subPackageName.isEmpty()) { subPackageName = fileName; } else { subPackageName += '.' + fileName; } scanClasspathLocationForNames( subPackage, accumulator, subPackageName, recurse); } } } } // ---------------------------------------------------------- private static void scanClasspathLocationForNames( JarFile location, Set<String> accumulator, String packageName, boolean recurse) { // System.out.println("[scanning jar file: " + location + "]"); String packagePrefix = packageName; if (packagePrefix == null) { packagePrefix = ""; } else if (!packagePrefix.isEmpty()) { packagePrefix = packagePrefix.replace('.', '/'); if (!packagePrefix.endsWith("/")) { packagePrefix += '/'; } } Enumeration<JarEntry> entries = location.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); // System.out.println(" [scanning entry: " + name + "]"); if (name.startsWith(packagePrefix) && name.endsWith(".class")) { name = name.substring(0, name.length() - ".class".length()); // System.out.println(" [scanning class: " + name + "]"); if (recurse || name.lastIndexOf('/') == packagePrefix.length() - 1) { // Translate to fully qualified class name name = name.replace('/', '.'); // Ignore inner classes int pos = name.lastIndexOf('.'); if (pos < 0) { pos = 0; } if (name.lastIndexOf('$') < pos) { // System.out.println( // " [adding: " + name + "]"); accumulator.add(name); } } } } } // ---------------------------------------------------------- private static class PackageContent { private List<String> classNames = new ArrayList<String>(); private SoftReference<List<Class<?>>> classes = null; public void setNames(Collection<String> newNames) { classNames.clear(); classNames.addAll(newNames); classes = null; } @SuppressWarnings("unused") public List<String> classNames() { return classNames; } public List<Class<?>> classesWithSimpleName( String name, ClassLoader loader) { List<Class<?>> result = new ArrayList<Class<?>>(); for (String className : classNames) { int pos = className.lastIndexOf('.') + 1; if (name.equals(className.substring(pos))) { Class<?> c = classForName(className, loader); if (c != null) { result.add(c); } } } return result; } public List<Class<?>> classes(ClassLoader loader) { List<Class<?>> result = null; if (classes != null) { result = classes.get(); } if (result == null) { result = new ArrayList<Class<?>>(classNames.size()); for (String name : classNames) { Class<?> c = classForName(name, loader); if (c != null) { result.add(c); } } classes = new SoftReference<List<Class<?>>>(result); } return result; } } private static long maxPermSize() { for (MemoryPoolMXBean mx : ManagementFactory.getMemoryPoolMXBeans()) { if (mx.getName() != null && mx.getName().contains("Perm Gen")) { return mx.getUsage().getMax(); } } return 0; } private static String recommendedNewMaxSize(long oldMaxSize) { int recommended = 64; long rawRecommended = 64 * 1024 * 1024; while (rawRecommended <= oldMaxSize) { recommended *= 2; rawRecommended = recommended * 1024 * 1024; } return recommended + "m"; } }