package uk.ac.uea.threadr.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Logger;
import uk.ac.uea.threadr.Threadr;
import uk.ac.uea.threadr.util.Operation.ConstructorOperation;
import uk.ac.uea.threadr.util.Operation.FieldOperation;
import uk.ac.uea.threadr.util.Operation.MethodOperation;
/**
* General operations that can be performed on Classes such as instantiation,
* retrieval of constructors, methods and fields of certain types.
*
* @author Jordan Woerner
* @version 1.2
*/
public class ClassOperations {
/** Logger for this class. */
private static Logger logger = Threadr.logger;
/** The maximum depth generating parameters can go before quitting. */
final static int MAX_LEVEL = 5;
/**
* Attempts to create an instance of the provided class type. Automatically
* fails if the maximum recursion depth is reached.
*
* @param classType The Class to try and instantiate.
* @return An Object of the Class type required, or null if Class cannot be
* instantiated.
* @throws InstantiationException Thrown if the Class cannot be
* instantiated due to it being abstract, synthetic, an interface or enum.
* Also thrown if recursion depth is too great.
* @since 1.0
*/
public static Object instantiateClass(Class<?> classType)
throws InstantiationException
{
try {
return instantiateClass(classType, 0);
} catch (InstantiationException ex) {
throw ex;
}
}
/**
* Attempts to create an instance of the provided class type. Automatically
* fails if the maximum recursion depth is reached.
*
* @param classType The Class to try and instantiate.
* @param level The current recursion depth.
* @return An Object of the Class type required, or null if Class cannot be
* instantiated.
* @throws InstantiationException Thrown if the class cannot be
* instantiated due to it being abstract, synthetic, an interface or enum.
* Also thrown if recursion depth is too great.
* @since 1.0
*/
static Object instantiateClass(Class<?> classType, int level)
throws InstantiationException {
/* Do not attempt to test abstract classes, interfaces, etc. */
if (Modifier.isAbstract(classType.getModifiers())
|| classType.isInterface()
|| classType.isEnum()
|| classType.isSynthetic()) {
throw new InstantiationException("Class impossible to instance.");
}
/* If we have recursed too deeply, just give up. */
if (level > MAX_LEVEL) {
throw new InstantiationException("Maximum instantiation depth reached!");
}
List<Operation> constructors = ClassOperations.getConstructingOperations(classType, classType);
/* If there are no constructing operations, we can't do anything. */
if (constructors.isEmpty()) return null;
Object clazz = null;
Random rand = new Random();
while (clazz == null && !constructors.isEmpty()) {
/* Pick and remove a random constructor from the list. */
Operation constr = constructors.remove(rand.nextInt(constructors.size()));
/* Log the constructor being tested. */
if (level == 0)
logger.fine(String.format("Trying '%s' to instantiate '%s'...", constr, classType.getCanonicalName()));
/* Get all the parameter types needed for this Constructor. */
Class<?>[] paramTypes = constr.getParamTypes();
/* If there are no parameters, this array needs to be null. */
Object[] params = null;
if (paramTypes != null && paramTypes.length > 0) {
/* If there are parameters needed, try and create them. */
params = new Object[paramTypes.length];
for (int j = 0; j < params.length; j++) {
if (paramTypes[j].isArray()) {
logger.finer(String.format("\t%s array",
paramTypes[j].getComponentType().getCanonicalName(), params[j]));
/* If the parameter is an array type, make an array. */
params[j] = ParameterGen.createParameter(paramTypes[j], level, 2);
} else {
/* Otherwise just make an Object. */
params[j] = ParameterGen.createParameter(paramTypes[j], level);
logger.finer(String.format("\t%s - %s", paramTypes[j], params[j]));
}
}
}
/* Try and create a new instance of the reference. */
try {
clazz = constr.execute(clazz, params); // Use the chosen constructor.
} catch (Exception ex) {
logger.warning(ex.getMessage());
clazz = null;
}
if (clazz == null) {
logger.fine(String.format("Could not instantiate class %s using %s",
classType.getCanonicalName(), constr));
} else {
logger.fine(String.format("Class '%s' instantiated using '%s'",
classType.getCanonicalName(), constr));
}
}
if (clazz == null) {
/* If after all that, the class still isn't instanced. Try the
* default constructor. This should call the default constructor
* for java.lang.Object should none exist in the class. */
try {
clazz = classType.newInstance();
} catch (IllegalAccessException iaEx) {
logger.warning(String.format("Class '%s' is inaccesible", classType.getCanonicalName()));
clazz = null;
} catch (InstantiationException instEx) {
logger.warning(String.format("Could not instantiate %s using default constructor", classType.getCanonicalName()));
clazz = null;
}
}
return clazz; // Return the constructed (or failed) object.
}
/**
* <p>Look for any classes in the provided package and return their
* {@link Class} objects in a {@link List}.</p>
* <p>Checks for classes in the provided package and any sub-packages found
* if requested.</p>
*
* @param pkg The Java package to search for Classes in.
* @param subPackages Set to true to search sub-packages of the specified
* package. False to ignore sub-packages.
* @return A List of Class objects.
* @since 1.0
*/
public static List<Class<?>> findClassesInPackage(String pkg,
boolean subPackages) {
ClassLoader classLoader =
Thread.currentThread().getContextClassLoader();
List<Class<?>> classes = new ArrayList<>();
if (classLoader != null) {
String fileSep = System.getProperty("file.separator");
String fixPkg = pkg.replace(".", fileSep);
try {
Enumeration<URL> resources = classLoader.getResources(fixPkg);
List<File> dirs = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.toURI()));
}
for (File dir : dirs) {
classes.addAll(findClasses(dir, pkg, subPackages));
}
} catch (IOException ioEx) {
logger.warning("IO Exception occured when loading classes from"
+ "package '" + pkg + "'!");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
logger.info(String.format("%d classes found in package %s!",
classes.size(), pkg));
return classes;
}
/**
* Find and return all Classes found in the provided package and all the
* sub-packages of the package.
*
* @param dir The directory to find classes in.
* @param pkg The package the classes belong to.
* @param subPackages Set to true to search for Classes in sub-packages.
* @return A {@link List} of {@link Class} objects.
* @since 1.0
*/
static List<Class<?>> findClasses(File dir, String pkg,
boolean subPackages) {
List<Class<?>> classes = new ArrayList<>();
if (dir.exists()) {
/* As long as the provided directory actually exists. */
File[] files = dir.listFiles(); // Grab all the files.
for (File file : files) {
/* If the current file is another directory, check it. */
if (subPackages
&&file.isDirectory()
&& !file.getName().contains(".")) {
String nPkg = String.format("%s.%s", pkg, file.getName());
classes.addAll(findClasses(file, nPkg, true));
} else if (file.getName().endsWith(".class")) {
/* If it's a class instead, try and load it. */
String fileSep = System.getProperty("file.separator");
/* Fix up the package and class name to be used safely. */
String classPath = String.format("%s%s%s", pkg, fileSep,
file.getName().substring(0, file.getName().length()
- ".class".length()));
classPath = classPath.replace(fileSep, ".");
try {
classes.add(Class.forName(classPath));
} catch (ClassNotFoundException e) {
logger.warning(String.format("Could not load class %s"
+ "\nReason: %s", classPath, e.getMessage()));
}
}
}
}
return classes; // Return the List of found classes.
}
/**
* Gets a lust of all the classes contained inside the specified JAR file.
* Classes in sub-packages will not be loaded.
*
* @param file The JAR file to read.
* @return Returns an List of Class objectes type to wildcard. If a
* file that isn't a JAR is provided, this will return null.
* @since 1.2
*/
public static List<Class<?>> getClasses(File file) {
return getClasses(file, null, false);
}
/**
* Gets a list of the classes contained inside the specified JAR file.
* Allows for classes to be filtered by a package name. Classes in the
* sub packages will not be loaded however.
*
* @param file The JAR file to read.
* @param pkg The package to filter classes out by as a String.
* @return Returns an List of Class objects typed to wildcard. If a
* file that isn't a JAR is provided, this will return null.
* @since 1.2
*/
public static List<Class<?>> getClasses(File file, String pkg) {
return getClasses(file, pkg, false);
}
/**
* Gets a list of the classes contained inside the specified JAR file.
* Allows for classes to be filtered by a package name. Classes in sub-
* packages can be loaded by specifying 'true' as the final parameter of
* this method.
*
* @param file The JAR file to read.
* @param pkg The package to filter classes out by as a String.
* @param subPkg Set to true to load classes found in the sub-packages
* of the specified package.
* @return Returns an List of Class objects typed to wildcard. If a
* file that isn't a JAR is provided, this will return null.
* @since 1.2
*/
public static List<Class<?>> getClasses(File file, String pkg, boolean subPkg) {
/* Stop now if the file provided is not a JAR file. */
if (!file.getName().matches("[.\\/\\d\\w\\s-]+.jar"))
return null;
/* If a package name was provided, remove the dots. */
if (pkg != null)
pkg = pkg.replace(".", "/");
List<Class<?>> classes = new ArrayList<>();
JarInputStream jarStream = null;
String className = null;
try {
/* Open up the JAR. */
jarStream = new JarInputStream(new FileInputStream(file));
JarEntry jarEntry = null;
/* Look at all the files in the JAR file. Keep checking until
nothing is left in the JarInputStream. */
while ((jarEntry = jarStream.getNextJarEntry()) != null) {
/* Add any classes found to the ArrayList. */
if (jarEntry.getName().endsWith(".class")) {
if (pkg == null || jarEntry.getName().startsWith(pkg)) {
className = jarEntry.getName();
/* If we aren't searching sub-packages, make sure we skip them. */
if (!subPkg && !className.matches(pkg + "/[\\d\\w\\s-]+.class"))
continue;
/* Strip the '.class' from the class name to load it. */
String strippedName = className.replace("/", ".") // Remove the dots
.substring(0, className.length() - 6);
/* Try and load the class. */
Class<?> clazz = Class.forName(strippedName);
classes.add(clazz); // Add it to the ArrayList.
}
}
}
} catch(IOException ioEx) {
/* Thrown if the JarInputStream fails. */
logger.severe("Could not read JAR file - " + file.getName());
} catch (ClassNotFoundException e) {
/* Thrown if a class cannot be loaded. */
logger.warning("Could not find class - " + className);
} finally {
/* Attempt to close the JarInputStream when we're done. */
try {
jarStream.close();
} catch (IOException e) {
logger.warning("Could not close jarStream.");
} finally {
jarStream = null;
}
}
return classes;
}
/**
* Gets a list of Operations that can construct the specified target class.
* Should an Operation require the target type in its parameters, it is
* excluded.
*
* @param clazz The Class to look for Operations in.
* @param target The Class that Operations must construct.
* @return An ArrayList of Operations that will construct the target Class.
* @since 1.0
*/
static List<Operation> getConstructingOperations(Class<?> clazz,
Class<?> target) {
List<Operation> operations = new ArrayList<>();
/* Get all the Constructors, Methods and Fields from the class. */
Constructor<?>[] constructors = clazz.getConstructors();
Method[] methods = clazz.getMethods();
Field[] fields = clazz.getFields();
/* Add the constructors if the target is the same class. */
if (clazz == target) {
for (Constructor<?> c : constructors) {
boolean cyclic = false;
Class<?>[] params = c.getParameterTypes();
for (Class<?> param : params) {
/* Make sure the parameters don't need the target type. */
if (param == target || param.getComponentType() == target)
cyclic = true;
}
/* If the constructor would cause more recursion, skip it. */
if (cyclic) continue;
/* Otherwise, add the constructor to the list. */
operations.add(new Operation.ConstructorOperation(clazz, c));
}
}
/* Add any useful methods. */
for (Method m : methods) {
if (Modifier.isPublic(m.getModifiers()) // Must be public.
&& !Modifier.isAbstract(m.getModifiers()) // Must not be abstract.
&& !m.isSynthetic() // Must not be synthetic.
&& m.getGenericReturnType() == target) // Must have the right return type.
{
boolean cyclic = false;
Type[] params = m.getGenericParameterTypes();
for (Type param : params) {
/* Make sure the parameters don't need the target type. */
if (param == target || param.getClass().getComponentType() == target)
cyclic = true;
}
/* If the method would cause more recursion, skip it. */
if (cyclic) continue;
/* Add the method to the list of operations. */
operations.add(new Operation.MethodOperation(clazz, m));
}
}
/* Add any relevant accessible fields. */
for (Field f : fields) {
if (Modifier.isPublic(f.getModifiers()) // Must be public.
&& !Modifier.isAbstract(f.getModifiers()) // Must not be abstract.
&& !f.isSynthetic() // Must not be synthetic.
&& f.getType() == target) // Must have the right return type.
{
operations.add(new Operation.FieldOperation(clazz, f));
}
}
return operations; // Return the list of Operations.
}
/**
* Gets all declared constructors in a {@link Class} (including protected,
* default and private visibility constructors) and converts them into
* {@link ConstructorOperation}s.
*
* @param clazz The Class to gather constructors from.
* @return An array of ConstructorOperations if any constructors exist in
* the Class, otherwise null.
* @since 1.1
*/
public static final ConstructorOperation[] getConstructors(Class<?> clazz) {
/* Get all the declared constructors in a Class. */
Constructor<?>[] reflectedConstructors =
clazz.getDeclaredConstructors();
/* If there are none, just return null. */
if (reflectedConstructors.length == 0) return new ConstructorOperation[0];
/* Otherwise, convert them to ConstructorOperations. */
ConstructorOperation[] constrs =
new ConstructorOperation[reflectedConstructors.length];
for (int i = 0; i < constrs.length; i++) {
constrs[i] = new ConstructorOperation(clazz,
reflectedConstructors[i]);
}
return constrs; // Return the ConstructorOperations.
}
/**
* Gets all declared methods in a {@link Class} (including protected,
* default and private visibility methods) and converts them into
* {@link MethodOperation}s.
*
* @param clazz The Class to gather methods from.
* @return An array of MethodOperations if any methods exist in the Class,
* otherwise null.
* @since 1.1
*/
public static final MethodOperation[] getMethods(Class<?> clazz) {
/* Get all the declared methods in the provided Class. */
Method[] declaredMethods = clazz.getDeclaredMethods();
if (declaredMethods.length == 0) return new MethodOperation[0];
/* If there are methods, convert them to MethodOperations. */
MethodOperation[] mthds = new MethodOperation[declaredMethods.length];
for (int i = 0; i < mthds.length; i++) {
mthds[i] = new MethodOperation(clazz, declaredMethods[i]);
}
return mthds; // Return the MethodOperations.
}
/**
* Gets all declared fields in a {@link Class} (including protected,
* default and private visibility fields) and converts them into
* {@link FieldOperation}s.
*
* @param clazz The Class to gather fields from.
* @return An array of FieldOperations if any fields exist in the Class,
* otherwise null.
* @since 1.1
*/
public static final FieldOperation[] getFields(Class<?> clazz) {
/* Get all the fields declared in the provided Class. */
Field[] declaredFields = clazz.getDeclaredFields();
if (declaredFields.length == 0) return new FieldOperation[0];
/* If there are fields, convert them to FieldOperations. */
FieldOperation[] fields = new FieldOperation[declaredFields.length];
for (int i = 0; i < fields.length; i++) {
fields[i] = new FieldOperation(clazz, declaredFields[i]);
}
return fields; // Return the FieldOperations.
}
/**
* Searches for the specified Annotation on the provided class.
*
* @param clazz The Class to search for the Annotation on.
* @param annotation The Annotation to look for as an instance of Class.
* @return Returns true if the Annotation if found on the specified class,
* false otherwise.
* @since 1.1
*/
public static boolean hasAnnotation(Class<?> clazz,
Class<? extends Annotation> annotation) {
/* Loop through all the annotations found on the specified class. */
for (Annotation a : clazz.getAnnotations()) {
if (a.annotationType() == annotation) {
/* If the current annotation matches the specified one. */
logger.finer(String.format("Field '%s' has "
+ "'ThreadSafe' annotation.", clazz.getName()));
return true; // The class must be marked.
}
}
return false; // The annotation wasn't found.
}
}