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 @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}