1package junitparams.internal;
2
3import java.lang.annotation.Annotation;
4import java.lang.reflect.Array;
5import java.math.BigDecimal;
6
7import org.junit.runners.model.FrameworkMethod;
8import org.junit.runners.model.Statement;
9
10import junitparams.converters.ConversionFailedException;
11import junitparams.converters.ConvertParam;
12import junitparams.converters.ParamAnnotation;
13import junitparams.converters.ParamConverter;
14
15/**
16 * JUnit invoker for parameterised test methods
17 *
18 * @author Pawel Lipinski
19 */
20public class InvokeParameterisedMethod extends Statement {
21
22    private final Object[] params;
23    private final FrameworkMethod testMethod;
24    private final Object testClass;
25
26    public InvokeParameterisedMethod(FrameworkMethod testMethod, Object testClass, Object params) {
27        this.testMethod = testMethod;
28        this.testClass = testClass;
29        try {
30            if (params instanceof String)
31                this.params = castParamsFromString((String) params);
32            else {
33                this.params = castParamsFromObjects(params);
34            }
35        } catch (ConversionFailedException e) {
36            throw new RuntimeException(e);
37        }
38    }
39
40    private Object[] castParamsFromString(String params) throws ConversionFailedException {
41        Object[] columns = null;
42        try {
43            columns = Utils.splitAtCommaOrPipe(params);
44            columns = castParamsUsingConverters(columns);
45        } catch (RuntimeException e) {
46            new IllegalArgumentException("Cannot parse parameters. Did you use ',' or '|' as column separator? "
47                    + params, e).printStackTrace();
48        }
49
50        return columns;
51    }
52
53    private Object[] castParamsFromObjects(Object params) throws ConversionFailedException {
54        Object[] paramset = Utils.safelyCastParamsToArray(params);
55
56        try {
57            return castParamsUsingConverters(paramset);
58        } catch (ConversionFailedException e) {
59            throw e;
60        } catch (Exception e) {
61            Class<?>[] typesOfParameters = createArrayOfTypesOf(paramset);
62            Object resultParam = createObjectOfExpectedTypeBasedOnParams(paramset, typesOfParameters);
63            return new Object[]{resultParam};
64        }
65    }
66
67    private Object createObjectOfExpectedTypeBasedOnParams(Object[] paramset, Class<?>[] typesOfParameters) {
68        Object resultParam;
69
70        try {
71            if (testMethod.getMethod().getParameterTypes()[0].isArray()) {
72                resultParam = Array.newInstance(typesOfParameters[0], paramset.length);
73                for (int i = 0; i < paramset.length; i++) {
74                    ((Object[]) resultParam)[i] = paramset[i];
75                }
76            } else {
77                resultParam = testMethod.getMethod().getParameterTypes()[0].getConstructor(typesOfParameters).newInstance(paramset);
78            }
79        } catch (Exception e) {
80            throw new IllegalStateException("While trying to create object of class " + testMethod.getMethod().getParameterTypes()[0]
81                    + " could not find constructor with arguments matching (type-wise) the ones given in parameters.", e);
82        }
83        return resultParam;
84    }
85
86    private Class<?>[] createArrayOfTypesOf(Object[] paramset) {
87        Class<?>[] parametersBasedOnValues = new Class<?>[paramset.length];
88        for (int i = 0; i < paramset.length; i++) {
89            parametersBasedOnValues[i] = paramset[i].getClass();
90        }
91        return parametersBasedOnValues;
92    }
93
94    private Object[] castParamsUsingConverters(Object[] columns) throws ConversionFailedException {
95        Class<?>[] expectedParameterTypes = testMethod.getMethod().getParameterTypes();
96
97        if (testMethodParamsHasVarargs(columns, expectedParameterTypes)) {
98            columns = columnsWithVarargs(columns, expectedParameterTypes);
99        }
100
101        Annotation[][] parameterAnnotations = testMethod.getMethod().getParameterAnnotations();
102        verifySameSizeOfArrays(columns, expectedParameterTypes);
103        columns = castAllParametersToProperTypes(columns, expectedParameterTypes, parameterAnnotations);
104        return columns;
105    }
106
107    private Object[] columnsWithVarargs(Object[] columns, Class<?>[] expectedParameterTypes) {
108        Object[] allParameters = standardParameters(columns, expectedParameterTypes);
109        allParameters[allParameters.length - 1] = varargsParameters(columns, expectedParameterTypes);
110        return allParameters;
111    }
112
113    private Object[] varargsParameters(Object[] columns, Class<?>[] expectedParameterTypes) {
114        Class<?> varArgType = expectedParameterTypes[expectedParameterTypes.length - 1].getComponentType();
115        Object[] varArgsParameters = (Object[]) Array.newInstance(varArgType, columns.length - expectedParameterTypes.length + 1);
116        for (int i = 0; i < varArgsParameters.length; i++) {
117            varArgsParameters[i] = columns[i + expectedParameterTypes.length - 1];
118        }
119        return varArgsParameters;
120    }
121
122    private Object[] standardParameters(Object[] columns, Class<?>[] expectedParameterTypes) {
123        Object[] standardParameters = new Object[expectedParameterTypes.length];
124        for (int i = 0; i < standardParameters.length - 1; i++) {
125            standardParameters[i] = columns[i];
126        }
127        return standardParameters;
128    }
129
130    private boolean testMethodParamsHasVarargs(Object[] columns, Class<?>[] expectedParameterTypes) {
131        int last = expectedParameterTypes.length - 1;
132        if (columns[last] == null) {
133            return false;
134        }
135        return expectedParameterTypes.length <= columns.length
136                && expectedParameterTypes[last].isArray()
137                && expectedParameterTypes[last].getComponentType().equals(columns[last].getClass());
138    }
139
140    private Object[] castAllParametersToProperTypes(Object[] columns, Class<?>[] expectedParameterTypes,
141                                                    Annotation[][] parameterAnnotations) throws ConversionFailedException {
142        Object[] result = new Object[columns.length];
143
144        for (int i = 0; i < columns.length; i++) {
145            if (parameterAnnotations[i].length == 0)
146                result[i] = castParameterDirectly(columns[i], expectedParameterTypes[i]);
147            else
148                result[i] = castParameterUsingConverter(columns[i], parameterAnnotations[i]);
149        }
150
151        return result;
152    }
153
154    private Object castParameterUsingConverter(Object param, Annotation[] annotations) throws ConversionFailedException {
155        for (Annotation annotation : annotations) {
156            if (ParamAnnotation.matches(annotation)) {
157                return ParamAnnotation.convert(annotation, param);
158            }
159            if (annotation.annotationType().isAssignableFrom(ConvertParam.class)) {
160                Class<? extends ParamConverter<?>> converterClass = ((ConvertParam) annotation).value();
161                String options = ((ConvertParam) annotation).options();
162                try {
163                    return converterClass.newInstance().convert(param, options);
164                } catch (ConversionFailedException e) {
165                    throw e;
166                } catch (Exception e) {
167                    throw new RuntimeException("Your ParamConverter class must have a public no-arg constructor!", e);
168                }
169            }
170        }
171        return param;
172    }
173
174    @SuppressWarnings("unchecked")
175    private Object castParameterDirectly(Object object, Class clazz) {
176        if (object == null || clazz.isInstance(object) || (!(object instanceof String) && clazz.isPrimitive()))
177            return object;
178        if (clazz.isEnum())
179            return (Enum.valueOf(clazz, (String) object));
180        if (clazz.isAssignableFrom(String.class))
181            return object.toString();
182        if (clazz.isAssignableFrom(Class.class))
183            try {
184                return Class.forName((String) object);
185            } catch (ClassNotFoundException e) {
186                throw new IllegalArgumentException("Parameter class (" + object + ") not found", e);
187            }
188        if (clazz.isAssignableFrom(Integer.TYPE) || clazz.isAssignableFrom(Integer.class))
189            return Integer.parseInt((String) object);
190        if (clazz.isAssignableFrom(Short.TYPE) || clazz.isAssignableFrom(Short.class))
191            return Short.parseShort((String) object);
192        if (clazz.isAssignableFrom(Long.TYPE) || clazz.isAssignableFrom(Long.class))
193            return Long.parseLong((String) object);
194        if (clazz.isAssignableFrom(Float.TYPE) || clazz.isAssignableFrom(Float.class))
195            return Float.parseFloat((String) object);
196        if (clazz.isAssignableFrom(Double.TYPE) || clazz.isAssignableFrom(Double.class))
197            return Double.parseDouble((String) object);
198        if (clazz.isAssignableFrom(Boolean.TYPE) || clazz.isAssignableFrom(Boolean.class))
199            return Boolean.parseBoolean((String) object);
200        if (clazz.isAssignableFrom(Character.TYPE) || clazz.isAssignableFrom(Character.class))
201            return object.toString().charAt(0);
202        if (clazz.isAssignableFrom(Byte.TYPE) || clazz.isAssignableFrom(Byte.class))
203            return Byte.parseByte((String) object);
204        if (clazz.isAssignableFrom(BigDecimal.class))
205            return new BigDecimal((String) object);
206        throw new IllegalArgumentException("Parameter type (" + clazz.getName() + ") cannot be handled!" +
207                " Only primitive types, BigDecimals and Strings can be used.");
208    }
209
210    private void verifySameSizeOfArrays(Object[] columns, Class<?>[] parameterTypes) {
211        if (parameterTypes.length != columns.length)
212            throw new IllegalArgumentException(
213                    "Number of parameters inside @Parameters annotation doesn't match the number of test method parameters.\nThere are "
214                            + columns.length + " parameters in annotation, while there's " + parameterTypes.length + " parameters in the "
215                            + testMethod.getName() + " method.");
216    }
217
218    @Override
219    public void evaluate() throws Throwable {
220        testMethod.invokeExplosively(testClass, params == null ? new Object[]{params} : params);
221    }
222
223}
224