org.junit.jupiter.params.converter.FallbackStringToObjectConverter Maven / Gradle / Ivy
Show all versions of junit-jupiter-params Show documentation
/*
* Copyright 2015-2024 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 v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/
package org.junit.jupiter.params.converter;
import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP;
import static org.junit.platform.commons.util.ReflectionUtils.findConstructors;
import static org.junit.platform.commons.util.ReflectionUtils.findMethods;
import static org.junit.platform.commons.util.ReflectionUtils.invokeMethod;
import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate;
import static org.junit.platform.commons.util.ReflectionUtils.isNotStatic;
import static org.junit.platform.commons.util.ReflectionUtils.newInstance;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import org.junit.platform.commons.util.Preconditions;
/**
* {@code FallbackStringToObjectConverter} is a {@link StringToObjectConverter}
* that provides a fallback conversion strategy for converting from a
* {@link String} to a given target type by invoking a static factory method
* or factory constructor defined in the target type.
*
* Search Algorithm
*
*
* - Search for a single, non-private static factory method in the target
* type that converts from a String to the target type. Use the factory method
* if present.
* - Search for a single, non-private constructor in the target type that
* accepts a String. Use the constructor if present.
*
*
* If multiple suitable factory methods are discovered they will be ignored.
* If neither a single factory method nor a single constructor is found, this
* converter acts as a no-op.
*
* @since 5.1
* @see DefaultArgumentConverter
*/
class FallbackStringToObjectConverter implements StringToObjectConverter {
/**
* Implementation of the NULL Object Pattern.
*/
private static final Function NULL_EXECUTABLE = source -> source;
/**
* Cache for factory methods and factory constructors.
*
* Searches that do not find a factory method or constructor are tracked
* by the presence of a {@link #NULL_EXECUTABLE} object stored in the map.
* This prevents the framework from repeatedly searching for things which
* are already known not to exist.
*/
private static final ConcurrentHashMap, Function> factoryExecutableCache //
= new ConcurrentHashMap<>(64);
@Override
public boolean canConvert(Class> targetType) {
return findFactoryExecutable(targetType) != NULL_EXECUTABLE;
}
@Override
public Object convert(String source, Class> targetType) throws Exception {
Function executable = findFactoryExecutable(targetType);
Preconditions.condition(executable != NULL_EXECUTABLE,
"Illegal state: convert() must not be called if canConvert() returned false");
return executable.apply(source);
}
private static Function findFactoryExecutable(Class> targetType) {
return factoryExecutableCache.computeIfAbsent(targetType, type -> {
Method factoryMethod = findFactoryMethod(type);
if (factoryMethod != null) {
return source -> invokeMethod(factoryMethod, null, source);
}
Constructor> constructor = findFactoryConstructor(type);
if (constructor != null) {
return source -> newInstance(constructor, source);
}
return NULL_EXECUTABLE;
});
}
private static Method findFactoryMethod(Class> targetType) {
List factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType), BOTTOM_UP);
if (factoryMethods.size() == 1) {
return factoryMethods.get(0);
}
return null;
}
private static Constructor> findFactoryConstructor(Class> targetType) {
List> constructors = findConstructors(targetType, new IsFactoryConstructor(targetType));
if (constructors.size() == 1) {
return constructors.get(0);
}
return null;
}
/**
* {@link Predicate} that determines if the {@link Method} supplied to
* {@link #test(Method)} is a non-private static factory method for the
* supplied {@link #targetType}.
*/
static class IsFactoryMethod implements Predicate {
private final Class> targetType;
IsFactoryMethod(Class> targetType) {
this.targetType = targetType;
}
@Override
public boolean test(Method method) {
// Please do not collapse the following into a single statement.
if (!method.getReturnType().equals(this.targetType)) {
return false;
}
if (isNotStatic(method)) {
return false;
}
return isNotPrivateAndAcceptsSingleStringArgument(method);
}
}
/**
* {@link Predicate} that determines if the {@link Constructor} supplied to
* {@link #test(Constructor)} is a non-private factory constructor for the
* supplied {@link #targetType}.
*/
static class IsFactoryConstructor implements Predicate> {
private final Class> targetType;
IsFactoryConstructor(Class> targetType) {
this.targetType = targetType;
}
@Override
public boolean test(Constructor> constructor) {
// Please do not collapse the following into a single statement.
if (!constructor.getDeclaringClass().equals(this.targetType)) {
return false;
}
return isNotPrivateAndAcceptsSingleStringArgument(constructor);
}
}
private static boolean isNotPrivateAndAcceptsSingleStringArgument(Executable executable) {
return isNotPrivate(executable) //
&& (executable.getParameterCount() == 1) //
&& (executable.getParameterTypes()[0] == String.class);
}
}