/* * Copyright 2015-2017 the original author or authors. * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution and is available at * * http://www.eclipse.org/legal/epl-v10.html */ package org.junit.jupiter.engine.execution; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.junit.platform.commons.meta.API.Usage.Internal; import static org.junit.platform.commons.util.ReflectionUtils.isAssignableTo; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.meta.API; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; /** * {@code ExecutableInvoker} encapsulates the invocation of a * {@link java.lang.reflect.Executable} (i.e., method or constructor), * including support for dynamic resolution of method parameters via * {@link ParameterResolver ParameterResolvers}. * * @since 5.0 */ @API(Internal) public class ExecutableInvoker { private static final Logger LOG = Logger.getLogger(ExecutableInvoker.class.getName()); /** * Invoke the supplied constructor with dynamic parameter resolution. * * @param constructor the constructor to invoke and resolve parameters for * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from */ public <T> T invoke(Constructor<T> constructor, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { return ReflectionUtils.newInstance(constructor, resolveParameters(constructor, Optional.empty(), extensionContext, extensionRegistry)); } /** * Invoke the supplied constructor with the supplied outer instance and * dynamic parameter resolution. * * <p>This method should only be used to invoke the constructor for * an inner class. * * @param constructor the constructor to invoke and resolve parameters for * @param outerInstance the outer instance to supply as the first argument * to the constructor * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from */ public <T> T invoke(Constructor<T> constructor, Object outerInstance, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { return ReflectionUtils.newInstance(constructor, resolveParameters(constructor, Optional.empty(), outerInstance, extensionContext, extensionRegistry)); } /** * Invoke the supplied {@code static} method with dynamic parameter resolution. * * @param method the method to invoke and resolve parameters for * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from */ public Object invoke(Method method, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { return ReflectionUtils.invokeMethod(method, null, resolveParameters(method, Optional.empty(), extensionContext, extensionRegistry)); } /** * Invoke the supplied method on the supplied target object with dynamic parameter * resolution. * * @param method the method to invoke and resolve parameters for * @param target the object on which the method will be invoked; should be * {@code null} for static methods * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from */ public Object invoke(Method method, Object target, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { @SuppressWarnings("unchecked") Optional<Object> optionalTarget = (target instanceof Optional ? (Optional<Object>) target : Optional.ofNullable(target)); return ReflectionUtils.invokeMethod(method, target, resolveParameters(method, optionalTarget, extensionContext, extensionRegistry)); } /** * Resolve the array of parameters for the supplied executable and target. * * @param executable the executable for which to resolve parameters * @param target an {@code Optional} containing the target on which the * executable will be invoked; never {@code null} but should be empty for * static methods and constructors * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from * @return the array of Objects to be used as parameters in the executable * invocation; never {@code null} though potentially empty */ private Object[] resolveParameters(Executable executable, Optional<Object> target, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { return resolveParameters(executable, target, null, extensionContext, extensionRegistry); } /** * Resolve the array of parameters for the supplied executable, target, and * outer instance. * * @param executable the executable for which to resolve parameters * @param target an {@code Optional} containing the target on which the * executable will be invoked; never {@code null} but should be empty for * static methods and constructors * @param outerInstance the outer instance that will be supplied as the * first argument to a constructor for an inner class; should be {@code null} * for methods and constructors for top-level or static classes * @param extensionContext the current {@code ExtensionContext} * @param extensionRegistry the {@code ExtensionRegistry} to retrieve * {@code ParameterResolvers} from * @return the array of Objects to be used as parameters in the executable * invocation; never {@code null} though potentially empty */ private Object[] resolveParameters(Executable executable, Optional<Object> target, Object outerInstance, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { Preconditions.notNull(target, "target must not be null"); Parameter[] parameters = executable.getParameters(); Object[] values = new Object[parameters.length]; int start = 0; // Ensure that the outer instance is resolved as the first parameter if // the executable is a constructor for an inner class. if (outerInstance != null) { values[0] = outerInstance; start = 1; } // Resolve remaining parameters dynamically for (int i = start; i < parameters.length; i++) { ParameterContext parameterContext = new DefaultParameterContext(parameters[i], i, target); values[i] = resolveParameter(parameterContext, executable, extensionContext, extensionRegistry); } return values; } private Object resolveParameter(ParameterContext parameterContext, Executable executable, ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { try { // @formatter:off List<ParameterResolver> matchingResolvers = extensionRegistry.stream(ParameterResolver.class) .filter(resolver -> resolver.supportsParameter(parameterContext, extensionContext)) .collect(toList()); // @formatter:on if (matchingResolvers.isEmpty()) { throw new ParameterResolutionException( String.format("No ParameterResolver registered for parameter [%s] in executable [%s].", parameterContext.getParameter(), executable.toGenericString())); } if (matchingResolvers.size() > 1) { // @formatter:off String resolverNames = matchingResolvers.stream() .map(resolver -> resolver.getClass().getName()) .collect(joining(", ")); // @formatter:on throw new ParameterResolutionException(String.format( "Discovered multiple competing ParameterResolvers for parameter [%s] in executable [%s]: %s", parameterContext.getParameter(), executable.toGenericString(), resolverNames)); } ParameterResolver resolver = matchingResolvers.get(0); Object value = resolver.resolveParameter(parameterContext, extensionContext); validateResolvedType(parameterContext.getParameter(), value, executable, resolver); LOG.finer(() -> String.format( "ParameterResolver [%s] resolved a value of type [%s] for parameter [%s] in executable [%s].", resolver.getClass().getName(), (value != null ? value.getClass().getName() : null), parameterContext.getParameter(), executable.toGenericString())); return value; } catch (ParameterResolutionException ex) { throw ex; } catch (Throwable ex) { throw new ParameterResolutionException(String.format("Failed to resolve parameter [%s] in executable [%s]", parameterContext.getParameter(), executable.toGenericString()), ex); } } private void validateResolvedType(Parameter parameter, Object value, Executable executable, ParameterResolver resolver) { Class<?> type = parameter.getType(); // Note: null is permissible as a resolved value but only for non-primitive types. if (!isAssignableTo(value, type)) { String message; if (value == null && type.isPrimitive()) { message = String.format( "ParameterResolver [%s] resolved a null value for parameter [%s] " + "in executable [%s], but a primitive of type [%s] is required.", resolver.getClass().getName(), parameter, executable.toGenericString(), type.getName()); } else { message = String.format( "ParameterResolver [%s] resolved a value of type [%s] for parameter [%s] " + "in executable [%s], but a value assignment compatible with [%s] is required.", resolver.getClass().getName(), (value != null ? value.getClass().getName() : null), parameter, executable.toGenericString(), type.getName()); } throw new ParameterResolutionException(message); } } }