StreamParametersMapFunction.java
/**
* Powerunit - A JDK1.8 test framework
* Copyright (C) 2014 Mathieu Boretti.
*
* This file is part of Powerunit
*
* Powerunit is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Powerunit 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Powerunit. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.powerunit.helpers;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.powerunit.Parameter;
/**
* This is a class that provide a way to transform stream of String to Object
* based on the @Parameter field type.
* <p>
* Access to this class is available from the {@link ch.powerunit.TestSuite
* TestSuite} interface.
*
* @author borettim
* @param <T>
* the input type
*/
public final class StreamParametersMapFunction<T> implements
Function<T[], Object[]> {
private StreamParametersMapFunction() {
}
/**
* This is the regex used to support the split in case the input is an
* array.
*/
public static final String DEFAULT_REGEX_FOR_ARRAY = "\\s*,\\s*";
private final Map<Integer, Function<Object, Object>> mapper = new HashMap<>();
@Override
public Object[] apply(T[] input) {
Object output[] = new Object[input.length];
for (int i = 0; i < input.length; i++) {
Function<Object, Object> f = mapper.getOrDefault(i,
Function.identity());
output[i] = f.apply(input[i]);
}
return output;
}
/**
* Start building a Parameter Mapper function, with an initial converter.
* <p>
* Not specified index are considered transformed by identity function.
*
* @param idx
* The parameter index
* @param mapFunction
* the function to be applied
* @return the function on the parameter array
* @param <T>
* The input type for the function
* @param <R>
* the result type for the function
*/
public static <T, R> StreamParametersMapFunction<T> map(int idx,
Function<T, R> mapFunction) {
if (idx < 0) {
throw new IllegalArgumentException("idx can't be negative");
}
return new StreamParametersMapFunction<T>().andMap(idx, mapFunction);
}
/**
* Start building a Parameter Mapper function, assuming that the input are
* String, and using the type of the {@link Parameter @Parameter} field.
* <p>
* Fields not supported will not be mapped and must be handled manually,
* using {@link StreamParametersMapFunction#andMap(int, Function) andMap}
* method.
*
* @param testClass
* the testClass with the annotation
* @return the function on the parameter array
* @see <a href="./doc-files/convertedType.html">Supported automated
* conversion</a>
*/
public static StreamParametersMapFunction<String> stringToParameterMap(
Class<?> testClass) {
StreamParametersMapFunction<String> map = new StreamParametersMapFunction<>();
Arrays.stream(testClass.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Parameter.class))
.forEach(
f -> {
int pid = f.getAnnotation(Parameter.class).value();
Function<String, ?> fct = null;
if (f.getGenericType() instanceof Class) {
Class<?> c = (Class<?>) f.getGenericType();
fct = getEntryClassMapperFunction(c);
} else if (f.getGenericType() instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) f
.getGenericType();
fct = getEntryParameterizedTypeFunction(p);
}
if (fct != null) {
map.andMap(pid, fct);
}
});
return map;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Function<String, ?> getEntryParameterizedTypeFunction(
ParameterizedType p) {
Type raw = p.getRawType();
if (Collection.class.equals(raw)
&& p.getActualTypeArguments()[0] instanceof Class) {
Class<?> param = (Class<?>) p.getActualTypeArguments()[0];
return collectionMapper((Class) param,
getEntryClassMapperFunction(param), DEFAULT_REGEX_FOR_ARRAY);
} else if (Set.class.equals(raw)
&& p.getActualTypeArguments()[0] instanceof Class) {
Class<?> param = (Class<?>) p.getActualTypeArguments()[0];
return setMapper((Class) param, getEntryClassMapperFunction(param),
DEFAULT_REGEX_FOR_ARRAY);
} else if (Class.class.equals(raw)) {
return getEntryClassMapperFunction((Class) raw);
}
return null;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Function<String, ?> getEntryClassMapperFunction(Class<?> c) {
if (c.isArray()) {
Function<String, ?> compound = getEntryClassMapperFunction(c
.getComponentType());
if (compound != null) {
return arrayMapper((Class) c.getComponentType(), compound,
DEFAULT_REGEX_FOR_ARRAY);
}
} else {
return getSingleEntryClassMapperFunction(c);
}
return null;
}
private static Function<String, ?> getSingleEntryClassMapperFunction(
Class<?> c) {
if (int.class.equals(c) || Integer.class.equals(c)) {
return Integer::valueOf;
} else if (float.class.equals(c) || Float.class.equals(c)) {
return Float::valueOf;
} else if (short.class.equals(c) || Short.class.equals(c)) {
return Short::valueOf;
} else if (double.class.equals(c) || Double.class.equals(c)) {
return Double::valueOf;
} else if (long.class.equals(c) || Long.class.equals(c)) {
return Long::valueOf;
} else if (char.class.equals(c) || Character.class.equals(c)) {
return s -> s.charAt(0);
} else if (String.class.equals(c)) {
return Function.identity();
} else if (boolean.class.equals(c) || Boolean.class.equals(c)) {
return Boolean::valueOf;
} else if (Class.class.equals(c))
return s -> {
try {
return Class.forName(s);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Unexpected error "
+ e.getMessage(), e);
}
};
return null;
}
@SuppressWarnings("unchecked")
private static <T> Function<String, T[]> arrayMapper(Class<T> clazz,
Function<String, T> singleElementMapper, String separator) {
return (s) -> {
if (s == null) {
return null;
}
return Arrays.stream(s.split(separator)).map(singleElementMapper)
.toArray((i) -> (T[]) Array.newInstance(clazz, i));
};
}
private static <T> Function<String, Collection<T>> collectionMapper(
Class<T> clazz, Function<String, T> singleElementMapper,
String separator) {
return (s) -> {
if (s == null) {
return null;
}
return Arrays.stream(s.split(separator)).map(singleElementMapper)
.collect(Collectors.toList());
};
}
private static <T> Function<String, Set<T>> setMapper(Class<T> clazz,
Function<String, T> singleElementMapper, String separator) {
return (s) -> {
if (s == null) {
return null;
}
return Arrays.stream(s.split(separator)).map(singleElementMapper)
.collect(Collectors.toSet());
};
}
/**
* Defines an additional Parameter Mapper function.
* <p>
* Not specified index are considered transformed by identity function.
*
* @param idx
* The parameter index
* @param mapFunction
* the function to be applied
* @return the function on the parameter array
* @param <R>
* The return type for the function
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public <R> StreamParametersMapFunction<T> andMap(int idx,
Function<T, R> mapFunction) {
if (idx < 0) {
throw new IllegalArgumentException("idx can't be negative");
}
Objects.requireNonNull(mapFunction);
mapper.put(idx, (Function) mapFunction);
return this;
}
/**
* Provide a way to add a field to head parameter line.
*
* @param field
* The field to be added.
* @return the function that can be used on the stream (
* {@link java.util.stream.Stream#map(Function)}).
* @since 0.1.0
* @param <T>
* The object type to be added.
*/
public static <T> Function<Object[], Object[]> addFieldToEachEntry(T field) {
return i -> {
if (i == null) {
return new Object[] { field };
} else {
Object o[] = new Object[i.length + 1];
System.arraycopy(i, 0, o, 0, i.length);
o[i.length] = field;
return o;
}
};
}
}