001/**
002 * Powerunit - A JDK1.8 test framework
003 * Copyright (C) 2014 Mathieu Boretti.
004 *
005 * This file is part of Powerunit
006 *
007 * Powerunit is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published by
009 * the Free Software Foundation, either version 3 of the License, or
010 * (at your option) any later version.
011 *
012 * Powerunit is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with Powerunit. If not, see <http://www.gnu.org/licenses/>.
019 */
020package ch.powerunit.helpers;
021
022import java.lang.reflect.Array;
023import java.lang.reflect.ParameterizedType;
024import java.lang.reflect.Type;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Set;
031import java.util.function.Function;
032import java.util.stream.Collectors;
033
034import ch.powerunit.Parameter;
035
036/**
037 * This is a class that provide a way to transform stream of String to Object
038 * based on the @Parameter field type.
039 * <p>
040 * Access to this class is available from the {@link ch.powerunit.TestSuite
041 * TestSuite} interface.
042 *
043 * @author borettim
044 * @param <T>
045 *            the input type
046 */
047public final class StreamParametersMapFunction<T> implements
048        Function<T[], Object[]> {
049
050    private StreamParametersMapFunction() {
051    }
052
053    /**
054     * This is the regex used to support the split in case the input is an
055     * array.
056     */
057    public static final String DEFAULT_REGEX_FOR_ARRAY = "\\s*,\\s*";
058
059    private final Map<Integer, Function<Object, Object>> mapper = new HashMap<>();
060
061    @Override
062    public Object[] apply(T[] input) {
063        Object output[] = new Object[input.length];
064        for (int i = 0; i < input.length; i++) {
065            Function<Object, Object> f = mapper.getOrDefault(i,
066                    Function.identity());
067            output[i] = f.apply(input[i]);
068        }
069        return output;
070    }
071
072    /**
073     * Start building a Parameter Mapper function, with an initial converter.
074     * <p>
075     * Not specified index are considered transformed by identity function.
076     * 
077     * @param idx
078     *            The parameter index
079     * @param mapFunction
080     *            the function to be applied
081     * @return the function on the parameter array
082     * @param <T>
083     *            The input type for the function
084     * @param <R>
085     *            the result type for the function
086     */
087    public static <T, R> StreamParametersMapFunction<T> map(int idx,
088            Function<T, R> mapFunction) {
089        if (idx < 0) {
090            throw new IllegalArgumentException("idx can't be negative");
091        }
092        return new StreamParametersMapFunction<T>().andMap(idx, mapFunction);
093    }
094
095    /**
096     * Start building a Parameter Mapper function, assuming that the input are
097     * String, and using the type of the {@link Parameter &#64;Parameter} field.
098     * <p>
099     * Fields not supported will not be mapped and must be handled manually,
100     * using {@link StreamParametersMapFunction#andMap(int, Function) andMap}
101     * method.
102     * 
103     * @param testClass
104     *            the testClass with the annotation
105     * @return the function on the parameter array
106     * @see <a href="./doc-files/convertedType.html">Supported automated
107     *      conversion</a>
108     */
109    public static StreamParametersMapFunction<String> stringToParameterMap(
110            Class<?> testClass) {
111        StreamParametersMapFunction<String> map = new StreamParametersMapFunction<>();
112        Arrays.stream(testClass.getDeclaredFields())
113                .filter(f -> f.isAnnotationPresent(Parameter.class))
114                .forEach(
115                        f -> {
116                            int pid = f.getAnnotation(Parameter.class).value();
117                            Function<String, ?> fct = null;
118                            if (f.getGenericType() instanceof Class) {
119                                Class<?> c = (Class<?>) f.getGenericType();
120                                fct = getEntryClassMapperFunction(c);
121
122                            } else if (f.getGenericType() instanceof ParameterizedType) {
123                                ParameterizedType p = (ParameterizedType) f
124                                        .getGenericType();
125                                fct = getEntryParameterizedTypeFunction(p);
126                            }
127                            if (fct != null) {
128                                map.andMap(pid, fct);
129                            }
130                        });
131        return map;
132    }
133
134    @SuppressWarnings({ "unchecked", "rawtypes" })
135    private static Function<String, ?> getEntryParameterizedTypeFunction(
136            ParameterizedType p) {
137        Type raw = p.getRawType();
138        if (Collection.class.equals(raw)
139                && p.getActualTypeArguments()[0] instanceof Class) {
140            Class<?> param = (Class<?>) p.getActualTypeArguments()[0];
141            return collectionMapper((Class) param,
142                    getEntryClassMapperFunction(param), DEFAULT_REGEX_FOR_ARRAY);
143        } else if (Set.class.equals(raw)
144                && p.getActualTypeArguments()[0] instanceof Class) {
145            Class<?> param = (Class<?>) p.getActualTypeArguments()[0];
146            return setMapper((Class) param, getEntryClassMapperFunction(param),
147                    DEFAULT_REGEX_FOR_ARRAY);
148        } else if (Class.class.equals(raw)) {
149            return getEntryClassMapperFunction((Class) raw);
150        }
151        return null;
152    }
153
154    @SuppressWarnings({ "unchecked", "rawtypes" })
155    private static Function<String, ?> getEntryClassMapperFunction(Class<?> c) {
156        if (c.isArray()) {
157            Function<String, ?> compound = getEntryClassMapperFunction(c
158                    .getComponentType());
159            if (compound != null) {
160                return arrayMapper((Class) c.getComponentType(), compound,
161                        DEFAULT_REGEX_FOR_ARRAY);
162            }
163        } else {
164            return getSingleEntryClassMapperFunction(c);
165        }
166        return null;
167    }
168
169    private static Function<String, ?> getSingleEntryClassMapperFunction(
170            Class<?> c) {
171        if (int.class.equals(c) || Integer.class.equals(c)) {
172            return Integer::valueOf;
173        } else if (float.class.equals(c) || Float.class.equals(c)) {
174            return Float::valueOf;
175        } else if (short.class.equals(c) || Short.class.equals(c)) {
176            return Short::valueOf;
177        } else if (double.class.equals(c) || Double.class.equals(c)) {
178            return Double::valueOf;
179        } else if (long.class.equals(c) || Long.class.equals(c)) {
180            return Long::valueOf;
181        } else if (char.class.equals(c) || Character.class.equals(c)) {
182            return s -> s.charAt(0);
183        } else if (String.class.equals(c)) {
184            return Function.identity();
185        } else if (boolean.class.equals(c) || Boolean.class.equals(c)) {
186            return Boolean::valueOf;
187        } else if (Class.class.equals(c))
188            return s -> {
189                try {
190                    return Class.forName(s);
191                } catch (ClassNotFoundException e) {
192                    throw new IllegalArgumentException("Unexpected error "
193                            + e.getMessage(), e);
194                }
195            };
196        return null;
197    }
198
199    @SuppressWarnings("unchecked")
200    private static <T> Function<String, T[]> arrayMapper(Class<T> clazz,
201            Function<String, T> singleElementMapper, String separator) {
202        return (s) -> {
203            if (s == null) {
204                return null;
205            }
206            return Arrays.stream(s.split(separator)).map(singleElementMapper)
207                    .toArray((i) -> (T[]) Array.newInstance(clazz, i));
208        };
209    }
210
211    private static <T> Function<String, Collection<T>> collectionMapper(
212            Class<T> clazz, Function<String, T> singleElementMapper,
213            String separator) {
214        return (s) -> {
215            if (s == null) {
216                return null;
217            }
218            return Arrays.stream(s.split(separator)).map(singleElementMapper)
219                    .collect(Collectors.toList());
220        };
221    }
222
223    private static <T> Function<String, Set<T>> setMapper(Class<T> clazz,
224            Function<String, T> singleElementMapper, String separator) {
225        return (s) -> {
226            if (s == null) {
227                return null;
228            }
229            return Arrays.stream(s.split(separator)).map(singleElementMapper)
230                    .collect(Collectors.toSet());
231        };
232    }
233
234    /**
235     * Defines an additional Parameter Mapper function.
236     * <p>
237     * Not specified index are considered transformed by identity function.
238     * 
239     * @param idx
240     *            The parameter index
241     * @param mapFunction
242     *            the function to be applied
243     * @return the function on the parameter array
244     * @param <R>
245     *            The return type for the function
246     */
247    @SuppressWarnings({ "rawtypes", "unchecked" })
248    public <R> StreamParametersMapFunction<T> andMap(int idx,
249            Function<T, R> mapFunction) {
250        if (idx < 0) {
251            throw new IllegalArgumentException("idx can't be negative");
252        }
253        Objects.requireNonNull(mapFunction);
254        mapper.put(idx, (Function) mapFunction);
255        return this;
256    }
257
258    /**
259     * Provide a way to add a field to head parameter line.
260     * 
261     * @param field
262     *            The field to be added.
263     * @return the function that can be used on the stream (
264     *         {@link java.util.stream.Stream#map(Function)}).
265     * @since 0.1.0
266     * @param <T>
267     *            The object type to be added.
268     */
269    public static <T> Function<Object[], Object[]> addFieldToEachEntry(T field) {
270        return i -> {
271            if (i == null) {
272                return new Object[] { field };
273            } else {
274                Object o[] = new Object[i.length + 1];
275                System.arraycopy(i, 0, o, 0, i.length);
276                o[i.length] = field;
277                return o;
278            }
279        };
280    }
281}