All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jqwik.engine.support.JqwikReflectionSupport Maven / Gradle / Ivy

There is a newer version: 1.9.1
Show newest version
package net.jqwik.engine.support;

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

import org.junit.platform.commons.support.*;

import net.jqwik.api.lifecycle.*;
import net.jqwik.api.providers.*;
import net.jqwik.engine.discovery.predicates.*;

import static java.util.stream.Collectors.*;

import static net.jqwik.engine.support.OverriddenMethodAnnotationSupport.*;

public class JqwikReflectionSupport {

	private final static IsTopLevelClass isTopLevelClass = new IsTopLevelClass();

	public static Stream streamInstancesFromInside(Object inner) {
		return addInstances(inner, new ArrayList<>()).stream();
	}

	private static List addInstances(Object inner, List instances) {
		instances.add(inner);
		Optional outer = getOuterInstance(inner);
		outer.ifPresent(o -> addInstances(o, instances));
		return instances;
	}

	private static Optional getOuterInstance(Object inner) {
		// This is risky since it depends on the name of the field which is nowhere guaranteed
		// but has been stable so far in all JDKs

		return Arrays
				   .stream(inner.getClass().getDeclaredFields())
				   .filter(field -> field.getName().startsWith("this$"))
				   .findFirst()
				   .map(field -> {
					   try {
						   return makeAccessible(field).get(inner);
					   } catch (SecurityException | IllegalArgumentException | IllegalAccessException ex) {
						   return Optional.empty();
					   }
				   });
	}

	private static  T makeAccessible(T object) {
		if (!object.isAccessible()) {
			object.setAccessible(true);
		}
		return object;
	}

	/**
	 * Create instance of a class that can potentially be a non static inner class
	 *
	 * @param    The type of the instance to create
	 * @param clazz The class to instantiate
	 * @return the newly created instance
	 */
	public static  T newInstanceWithDefaultConstructor(Class clazz) {
		if (isTopLevelClass.test(clazz) || ModifierSupport.isStatic(clazz))
			return ReflectionSupport.newInstance(clazz);
		Object parentInstance = newInstanceWithDefaultConstructor(clazz.getDeclaringClass());
		return ReflectionSupport.newInstance(clazz, parentInstance);
	}

	/**
	 * Create instance of a class that can potentially be a non static inner class
	 * and its outer instance might be {@code context}
	 *
	 * @param      The type of the instance to create
	 * @param clazz   The class to instantiate
	 * @param context The potential context instance
	 * @return the newly created instance
	 */
	public static  T newInstanceInTestContext(Class clazz, Object context) {
		if (isTopLevelClass.test(clazz) || ModifierSupport.isStatic(clazz))
			return ReflectionSupport.newInstance(clazz);
		Class outerClass = clazz.getDeclaringClass();
		Object parentInstance = outerClass.equals(context.getClass()) ?
									context : newInstanceWithDefaultConstructor(outerClass);
		return ReflectionSupport.newInstance(clazz, parentInstance);
	}

	/**
	 * Find all {@linkplain Method methods} as in ReflectionSupport.findMethods(..) but also use outer classes to look for
	 * methods.
	 *
	 * @param clazz         The class in which you start the search
	 * @param predicate     The condition to check for all candidate methods
	 * @param traversalMode Traverse hierarchy up or down. Determines the order in resulting list.
	 * @return List of found methods
	 */
	public static List findMethodsPotentiallyOuter(
		Class clazz, Predicate predicate,
		HierarchyTraversalMode traversalMode
	) {

		List foundMethods = new ArrayList<>();
		foundMethods.addAll(ReflectionSupport.findMethods(clazz, predicate, traversalMode));
		Class searchClass = clazz;
		while (searchClass.getDeclaringClass() != null) {
			searchClass = searchClass.getDeclaringClass();
			foundMethods.addAll(ReflectionSupport.findMethods(searchClass, predicate, traversalMode));
		}
		return foundMethods;
	}

	/**
	 * Invoke the supplied {@linkplain Method method} as in ReflectionSupport.invokeMethod(..) but potentially use the outer
	 * instance if the method belongs to the outer instance of an object.
	 *
	 * @param method The method to invoke
	 * @param target The object to invoke the method on
	 * @param args   The arguments of the method invocation
	 * @return Result of method invocation if there is one, otherwise null
	 */
	public static Object invokeMethodPotentiallyOuter(Method method, Object target, Object... args) {
		if (method.getDeclaringClass().isAssignableFrom(target.getClass())) {
			return ReflectionSupport.invokeMethod(method, target, args);
		} else {
			if (target.getClass().getDeclaringClass() != null) {
				Optional newTarget = getOuterInstance(target);
				if (newTarget.isPresent()) {
					return invokeMethodPotentiallyOuter(method, newTarget.get(), args);
				}
			}
			throw new IllegalArgumentException(String.format("Method [%s] cannot be invoked on target [%s].", method, target));
		}
	}

	public static Set getAllClasspathRootDirectories() {
		// TODO: This is quite a hack, since sometimes the classpath is quite different.
		// Especially under Java >=9's module system this will probably no longer work.
		String classpath = System.getProperty("java.class.path");
		return Arrays.stream(classpath.split(File.pathSeparator)) //
					 .map(Paths::get).filter(Files::isDirectory) //
					 .collect(toSet());
	}

	public static MethodParameter[] getMethodParameters(Method method, Class containerClass) {

		List list = new ArrayList<>();
		Parameter[] parameters = method.getParameters();
		GenericsClassContext containerClassContext = GenericsSupport.contextFor(containerClass);

		for (int i = 0; i < parameters.length; i++) {
			Parameter parameter = parameters[i];
			TypeResolution resolution = containerClassContext.resolveParameter(parameter);
			MethodParameter methodParameter = new MethodParameter(parameter, resolution);
			list.add(methodParameter);
		}
		return list.toArray(new MethodParameter[parameters.length]);
	}

	public static Optional findGeneratorMethod(
		String generatorToFind,
		Class containerClass,
		Class requiredGeneratorAnnotation,
		Function generatorNameSupplier,
		TypeUsage targetType
	) {
		List creators = findMethodsPotentiallyOuter(
			containerClass,
			isGeneratorMethod(targetType, requiredGeneratorAnnotation),
			HierarchyTraversalMode.BOTTOM_UP
		);
		return creators.stream().filter(generatorMethod -> {
			String generatorName = generatorNameSupplier.apply(generatorMethod);
			if (generatorName.isEmpty()) {
				generatorName = generatorMethod.getName();
			}
			return generatorName.equals(generatorToFind);
		}).findFirst();
	}

	public static Predicate isGeneratorMethod(TypeUsage targetType, Class requiredAnnotation) {
		return method -> {
			if (!findDeclaredOrInheritedAnnotation(method, requiredAnnotation).isPresent()) {
				return false;
			}
			TypeUsage generatorReturnType = TypeUsage.forType(method.getAnnotatedReturnType().getType());
			return generatorReturnType.canBeAssignedTo(targetType)
					   // for generic types in test hierarchies:
					   || targetType.canBeAssignedTo(generatorReturnType);
		};
	}

	public static boolean isInnerClass(Class hookClass) {
		return hookClass.isMemberClass() && !ModifierSupport.isStatic(hookClass);
	}

	/**
	 * Throw the supplied {@link Throwable}, masked as an
	 * unchecked exception.
	 *
	 * @param t   the Throwable to be wrapped
	 * @param  type of the value to return
	 * @return Fake return to make using the method a bit simpler
	 */
	public static  T throwAsUncheckedException(Throwable t) {
		JqwikReflectionSupport.throwAs(t);

		// Will never get here
		return null;
	}

	@SuppressWarnings("unchecked")
	private static  void throwAs(Throwable t) throws T {
		throw (T) t;
	}

}