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 &#64;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;
            }
        };
    }
}