001/**
002 * The MIT License (MIT)
003 *
004 * Copyright (c) 2017 tools4j.org (Marco Terzer)
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024package org.tools4j.spockito;
025
026import java.lang.reflect.*;
027import java.math.BigDecimal;
028import java.math.BigInteger;
029import java.sql.Time;
030import java.sql.Timestamp;
031import java.time.*;
032import java.util.*;
033import java.util.concurrent.*;
034import java.util.function.Function;
035
036/**
037 * Contains conversion functions and value converters used by {@link SpockitoValueConverter}.
038 */
039public final class Converters {
040
041    public static final Function<? super String, Object> OBJECT_CONVERTER = Function.identity();
042    public static final Function<? super String, Long> LONG_CONVERTER = Long::valueOf;
043    public static final Function<? super String, Integer> INTEGER_CONVERTER = Integer::valueOf;
044    public static final Function<? super String, Short> SHORT_CONVERTER = Short::valueOf;
045    public static final Function<? super String, Byte> BYTE_CONVERTER = Byte::valueOf;
046    public static final Function<? super String, Double> DOUBLE_CONVERTER = Double::valueOf;
047    public static final Function<? super String, Float> FLOAT_CONVERTER = Float::valueOf;
048    public static final Function<? super String, Boolean> BOOLEAN_CONVERTER = Boolean::valueOf;
049
050    public static final Function<? super String, BigInteger> BIG_INTEGER_CONVERTER = BigInteger::new;
051    public static final Function<? super String, BigDecimal> BIG_DECIMAL_CONVERTER = BigDecimal::new;
052    public static final Function<? super String, LocalDate> LOCAL_DATE_CONVERTER = LocalDate::parse;
053    public static final Function<? super String, LocalTime> LOCAL_TIME_CONVERTER = LocalTime::parse;
054    public static final Function<? super String, LocalDateTime> LOCAL_DATE_TIME_CONVERTER = LocalDateTime::parse;
055    public static final Function<? super String, ZonedDateTime> ZONED_DATE_TIME_CONVERTER = ZonedDateTime::parse;
056    public static final Function<? super String, OffsetDateTime> OFFSET_DATE_TIME_CONVERTER = OffsetDateTime::parse;
057    public static final Function<? super String, Instant> INSTANT_CONVERTER = Instant::parse;
058    public static final Function<? super String, Date> DATE_CONVERTER = s -> Date.from(Timestamp.valueOf(s).toInstant());
059    public static final Function<? super String, java.sql.Date> SQL_DATE_CONVERTER = java.sql.Date::valueOf;
060    public static final Function<? super String, Time> SQL_TIME_CONVERTER = Time::valueOf;
061    public static final Function<? super String, Timestamp> SQL_TIMESTAMP_CONVERTER = Timestamp::valueOf;
062    public static final Function<? super String, String> STRING_CONVERTER = s -> Strings.removeStartAndEndChars(s, '\'', '\'');
063    public static final Function<? super String, StringBuilder> STRING_BUILDER_CONVERTER = s -> new StringBuilder(STRING_CONVERTER.apply(s));
064    public static final Function<? super String, StringBuffer> STRING_BUFFER_CONVERTER = s -> new StringBuffer(STRING_CONVERTER.apply(s));
065    public static final Function<? super String, Character> CHAR_CONVERTER = s -> {
066        if (s.length() == 1) {
067            return s.charAt(0);
068        }
069        if (s.length() == 3 && s.charAt(0) == '\'' && s.charAt(2) == '\'') {
070            return s.charAt(1);
071        }
072        throw new IllegalArgumentException("Cannot convert string to char: " + s);
073    };
074    public static final ValueConverter CLASS_CONVERTER = new ValueConverter() {
075        @Override
076        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
077            try {
078                final Class<?> clazz = Class.forName(value);
079                return type.cast(clazz);
080            } catch (final Exception e) {
081                throw new IllegalArgumentException("Cannot convert string to " + type.getName() + ": " + value, e);
082            }
083        }
084    };
085    public static final ValueConverter ENUM_CONVERTER = new ValueConverter() {
086        @Override
087        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
088            final Enum<?> e = Enum.valueOf(type.asSubclass(Enum.class), value);
089            return type.cast(e);
090        }
091    };
092
093    /**
094     * Value converter for target type {@link Optional}.
095     */
096    public static class OptionalConverter implements ValueConverter {
097        private final ValueConverter elementConverter;
098
099        public OptionalConverter(final ValueConverter elementConverter) {
100            this.elementConverter = Objects.requireNonNull(elementConverter);
101        }
102
103        @Override
104        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
105            if (!Optional.class.equals(type)) {
106                throw new IllegalArgumentException("Type must be Optional: " + type.getName());
107            }
108            final String trimmed = value.trim();
109            final Object elementValue;
110            if ("empty".equals(trimmed) || trimmed.isEmpty()) {
111                elementValue = null;
112            } else {
113                final ActualType elementType = actualTypeForTypeParam(genericType, 0, 1);
114                elementValue = elementConverter.convert(elementType.rawType, elementType.genericType, trimmed);
115            }
116            return type.cast(Optional.ofNullable(elementValue));
117        }
118
119    }
120
121    /**
122     * Value converter for target type {@link Collection} or sub-interfaces and implementations of it.
123     */
124    public static class CollectionConverter implements ValueConverter {
125        private final ValueConverter elementConverter;
126        public CollectionConverter(final ValueConverter elementConverter) {
127            this.elementConverter = Objects.requireNonNull(elementConverter);
128        }
129
130        @Override
131        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
132            if (!Collection.class.isAssignableFrom(type)) {
133                throw new IllegalArgumentException("Type must be a collection: " + type.getName());
134            }
135            final ActualType elementType = actualTypeForTypeParam(genericType, 0, 1);
136            final List<?> list = toList(elementType, value);
137            if (type.isInstance(list)) {
138                return type.cast(list);
139            }
140            if (type.isAssignableFrom(ArrayList.class)) {
141                return type.cast(new ArrayList<>(list));
142            }
143            if (type.isAssignableFrom(Vector.class)) {
144                return type.cast(new Vector<>(list));
145            }
146            if (type.isAssignableFrom(LinkedList.class)) {
147                return type.cast(new LinkedList<>(list));
148            }
149            if (type.isAssignableFrom(ArrayDeque.class)) {
150                return type.cast(new ArrayDeque<>(list));
151            }
152            if (type.isAssignableFrom(LinkedHashSet.class)) {
153                return type.cast(new LinkedHashSet<>(list));
154            }
155            if (type.isAssignableFrom(TreeSet.class)) {
156                return type.cast(new TreeSet<>(list));
157            }
158            if (type.isAssignableFrom(HashSet.class)) {
159                return type.cast(new HashSet<>(list));
160            }
161            if (type.isAssignableFrom(EnumSet.class)) {
162                final EnumSet<?> enumSet = enumSet(elementType.rawType.asSubclass(Enum.class), list);
163                return type.cast(enumSet);
164            }
165            if (type.isAssignableFrom(ConcurrentLinkedQueue.class)) {
166                return type.cast(new ConcurrentLinkedQueue<>(list));
167            }
168            if (type.isAssignableFrom(ConcurrentLinkedDeque.class)) {
169                return type.cast(new ConcurrentLinkedDeque<>(list));
170            }
171            if (type.isAssignableFrom(ConcurrentSkipListSet.class)) {
172                return type.cast(new ConcurrentSkipListSet<>(list));
173            }
174            //unsupported collection type
175            throw new IllegalArgumentException("Cannot convert value to " + type.getName() + ": " + value);
176        }
177
178        private List<Object> toList(final ActualType elementType, final String value) {
179            final String plainValue = Strings.removeStartAndEndChars(value, '[', ']');
180            if (plainValue.trim().isEmpty()) {
181                return Collections.emptyList();
182            }
183            String[] parts = Strings.UNESCAPED_COMMA.split(plainValue);
184            if (parts.length == 1) {
185                parts = Strings.UNESCAPED_SEMICOLON.split(plainValue);
186            }
187            final List<Object> list = new ArrayList<>(parts.length);
188            for (int i = 0; i < parts.length; i++) {
189                list.add(elementConverter.convert(elementType.rawType, elementType.genericType, parts[i].trim()));
190            }
191            return list;
192        }
193
194        private static <E extends Enum<E>> EnumSet<E> enumSet(final Class<E> enumType, final List<?> list) {
195            final EnumSet<E> set = EnumSet.noneOf(enumType);
196            list.forEach(v -> set.add(enumType.cast(v)));
197            return set;
198        }
199    }
200
201    /**
202     * Value converter for array target types including primitive arrays.
203     */
204    public static class ArrayConverter implements ValueConverter {
205        private final ValueConverter elementConverter;
206        private final CollectionConverter collectionConverter;
207        public ArrayConverter(final ValueConverter elementConverter) {
208            this.elementConverter = Objects.requireNonNull(elementConverter);
209            this.collectionConverter = new CollectionConverter(elementConverter);
210        }
211
212        @Override
213        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
214            if (!type.isArray()) {
215                throw new IllegalArgumentException("Type must be an array: " + type.getName());
216            }
217            final Class<?> componentType = type.getComponentType();
218            final Type genericComponentType;
219            if (genericType instanceof GenericArrayType) {
220                genericComponentType = ((GenericArrayType)genericType).getGenericComponentType();
221            } else {
222                genericComponentType = componentType;
223            }
224            final Type genericListType = genericListType(genericComponentType);
225            final List<?> list = collectionConverter.convert(List.class, genericListType, value);
226            final Object array = Array.newInstance(componentType, list.size());
227            for (int i = 0; i < list.size(); i++) {
228                final Object val = list.get(i);
229                Array.set(array, i, val);
230            }
231            return type.cast(array);
232        }
233    }
234
235    /**
236     * Value converter for target type {@link Map} or sub-interfaces and implementations of it.
237     */
238    public static class MapConverter implements ValueConverter {
239        private final ValueConverter elementConverter;
240        public MapConverter(final ValueConverter elementConverter) {
241            this.elementConverter = Objects.requireNonNull(elementConverter);
242        }
243
244        @Override
245        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
246            final ActualType keyType = actualTypeForTypeParam(genericType, 0, 2);
247            final ActualType valueType = actualTypeForTypeParam(genericType, 1, 2);
248            final Map<?,?> map = toMap(keyType, valueType, value);
249            if (type.isInstance(map)) {
250                return type.cast(map);
251            }
252            if (type.isAssignableFrom(LinkedHashMap.class)) {
253                return type.cast(new LinkedHashMap<>(map));
254            }
255            if (type.isAssignableFrom(TreeMap.class)) {
256                return type.cast(new TreeMap<>(map));
257            }
258            if (type.isAssignableFrom(HashMap.class)) {
259                return type.cast(new HashMap<>(map));
260            }
261            if (type.isAssignableFrom(Hashtable.class)) {
262                return type.cast(new Hashtable<>(map));
263            }
264            if (type.isAssignableFrom(EnumMap.class)) {
265                final EnumMap<?,?> enumMap = enumMap(keyType.rawType.asSubclass(Enum.class), map);
266                return type.cast(enumMap);
267            }
268            if (type.isAssignableFrom(Properties.class)) {
269                final Properties props = new Properties();
270                props.putAll(map);
271                return type.cast(props);
272            }
273            if (type.isAssignableFrom(ConcurrentHashMap.class)) {
274                return type.cast(new ConcurrentHashMap<>(map));
275            }
276            if (type.isAssignableFrom(ConcurrentSkipListMap.class)) {
277                return type.cast(new ConcurrentSkipListMap<>(map));
278            }
279            //unsupported map type
280            throw new IllegalArgumentException("Cannot convert value to " + type.getName() + ": " + value);
281        }
282
283        private Map<Object, Object> toMap(final ActualType keyType, final ActualType valueType, final String value) {
284            final String plainValue = Strings.removeStartAndEndChars(value, '{', '}');
285            if (plainValue.trim().isEmpty()) {
286                return Collections.emptyMap();
287            }
288            String[] parts = Strings.UNESCAPED_COMMA.split(plainValue);
289            if (parts.length == 1) {
290                parts = Strings.UNESCAPED_SEMICOLON.split(plainValue);
291            }
292            final Map<Object, Object> map = new LinkedHashMap<>();
293            for (int i = 0; i < parts.length; i++) {
294                final String[] keyAndValue = parseKeyValue(parts[i].trim());
295                if (keyAndValue.length != 2) {
296                    throw new IllegalArgumentException("Invalid map key/value pair: " + parts[i]);
297                }
298                try {
299                    final Object key = elementConverter.convert(keyType.rawType, keyType.genericType, keyAndValue[0].trim());
300                    final Object val = elementConverter.convert(valueType.rawType, valueType.genericType, keyAndValue[1].trim());
301                    map.put(key, val);
302                } catch (final Exception e) {
303                    throw new IllegalArgumentException("Conversion to map key/value failed: " + parts[i], e);
304                }
305            }
306            return map;
307        }
308
309        private static <K extends Enum<K>, V> EnumMap<K, V> enumMap(final Class<K> enumType, final Map<?,V> map) {
310            final EnumMap<K,V> enumMap = new EnumMap(enumType);
311            map.forEach((k,v) -> enumMap.put(enumType.cast(k), v));
312            return enumMap;
313        }
314    }
315
316    /**
317     * Value converter for Java beans as target type. The beans either have getters and setters to access the fields
318     * or otherwise fields are accessed directly.
319     */
320    public static class BeanConverter implements ValueConverter {
321
322        private final ValueConverter elementConverter;
323        public BeanConverter(final ValueConverter elementConverter) {
324            this.elementConverter = Objects.requireNonNull(elementConverter);
325        }
326
327        @Override
328        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
329            final T instance = newInstance(type, value);
330            final Map<String, Accessor> accessorByName = new LinkedHashMap<>();
331            if (hasAccessibleSetters(type)) {
332                inspectSetters(type, accessorByName);
333            } else {
334                inspectFields(type, accessorByName);
335            }
336            if (accessorByName.isEmpty()) {
337                throw new IllegalArgumentException(type.getName() + " is not a bean class, no accessible setters or fields found");
338            }
339            injectValues(instance, accessorByName, value);
340            return instance;
341        }
342
343        private <T> T newInstance(final Class<T> type, final String value) {
344            try {
345                final Constructor<T> constructor = type.getDeclaredConstructor();
346                constructor.setAccessible(true);
347                return constructor.newInstance();
348            } catch (final Exception e) {
349                throw new IllegalArgumentException("Could not instantiate bean " + type.getName(), e);
350            }
351        }
352
353        private void injectValues(final Object instance, final Map<String, Accessor> accessorByName, final String value) {
354            final String plainValue = Strings.removeStartAndEndChars(value, '{', '}');
355            String[] parts = Strings.UNESCAPED_COMMA.split(plainValue);
356            if (parts.length == 1) {
357                parts = Strings.UNESCAPED_SEMICOLON.split(plainValue);
358            }
359            final Map<String, String> valueByName = new LinkedHashMap<>();
360            for (int i = 0; i < parts.length; i++) {
361                final String[] nameAndValue = parseKeyValue(parts[i].trim());
362                if (nameAndValue.length != 2) {
363                    throw new IllegalArgumentException("Invalid name/value pair: " + parts[i]);
364                }
365                final String name = normalizeFieldName(nameAndValue[0].trim());
366                final String val = nameAndValue[1].trim();
367                valueByName.put(name, val);
368            }
369            for (final Map.Entry<String, Accessor> e : accessorByName.entrySet()) {
370                final String val = valueByName.get(e.getKey());
371                if (val == null) {
372                    throw new IllegalArgumentException("No value found for bean property " + instance.getClass().getName() + "." + e.getKey());
373                }
374                try {
375                    final Class<?> type = e.getValue().type();
376                    final Type genericType = e.getValue().genericType();
377                    final Object convertedVal = elementConverter.convert(type, genericType, val);
378                    e.getValue().set(instance, convertedVal);
379                } catch (final Exception ex) {
380                    throw new IllegalArgumentException("Could not set bean property " + instance.getClass().getName() + "." + e.getKey() +
381                            " to value: " + val, ex);
382                }
383            }
384        }
385
386        public static boolean isBeanClass(final Class<?> clazz) {
387            return isInstantiatable(clazz) && (hasAccessibleFields(clazz) || hasAccessibleSetters(clazz));
388        }
389
390        private static boolean isInstantiatable(final Class<?> clazz) {
391            return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && hasDefaultConstructor(clazz);
392        }
393
394        private static boolean hasDefaultConstructor(final Class<?> clazz) {
395            try {
396                clazz.getDeclaredConstructor();
397                return true;
398            } catch (final Exception e) {
399                return false;
400            }
401        }
402
403        private interface Accessor {
404            void set(Object instance, Object value) throws Exception;
405            default Class<?> type() {
406                return Object.class;
407            }
408            default Type genericType() {
409                return Object.class;
410            }
411            static Accessor forType(final Class<?> type, final Type genericType, final Accessor accessor) {
412                return new Accessor() {
413                    @Override
414                    public void set(final Object instance, final Object value) throws Exception {
415                        accessor.set(instance, value);
416                    }
417
418                    @Override
419                    public Class<?> type() {
420                        return type;
421                    }
422
423                    @Override
424                    public Type genericType() {
425                        return genericType;
426                    }
427                };
428            }
429        }
430
431        private static boolean hasAccessibleSetters(final Class<?> clazz) {
432            return !inspectSetters(clazz, new HashMap<>()).isEmpty();
433        }
434        private static final Map<String, Accessor> inspectSetters(final Class<?> clazz, final Map<String, Accessor> accessorByName) {
435            if (clazz == null || Object.class.equals(clazz)) {
436                return accessorByName;
437            }
438            for (final Method method : clazz.getDeclaredMethods()) {
439                final int mod = method.getModifiers();
440                final String name = method.getName();
441                if (name.length() > 3 && name.startsWith("set") && method.getParameterCount() == 1 &&
442                        !method.isSynthetic() && !Modifier.isStatic(mod) && !Modifier.isPrivate(mod) && !Modifier.isProtected(mod)) {
443                    final String propertyName = normalizeFieldName(name.substring(3));
444                    accessorByName.put(propertyName, Accessor.forType(method.getParameterTypes()[0], method.getGenericParameterTypes()[0], method::invoke));
445                }
446            }
447            return inspectSetters(clazz.getSuperclass(), accessorByName);
448        }
449
450        private static boolean hasAccessibleFields(final Class<?> clazz) {
451            return !inspectFields(clazz, new HashMap<>()).isEmpty();
452        }
453        private static final Map<String, Accessor> inspectFields(final Class<?> clazz, final Map<String, Accessor> accessorByName) {
454            if (clazz == null || Object.class.equals(clazz)) {
455                return accessorByName;
456            }
457            for (final Field field : clazz.getDeclaredFields()) {
458                final int mod = field.getModifiers();
459                if (!field.isSynthetic() && !Modifier.isFinal(mod) && !Modifier.isStatic(mod) && !Modifier.isPrivate(mod) && !Modifier.isProtected(mod)) {
460                    accessorByName.put(field.getName(), Accessor.forType(field.getType(), field.getGenericType(), field::set));
461                }
462            }
463            return inspectFields(clazz.getSuperclass(), accessorByName);
464        }
465    }
466
467    private static String normalizeFieldName(final String name) {
468        if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) {
469            return Character.toLowerCase(name.charAt(0)) + name.substring(1);
470        }
471        return name;
472    }
473
474    private static final String[] parseKeyValue(final String pair) {
475        final String[] split = Strings.UNESCAPED_EQUAL.split(pair);
476        if (split.length == 1) {
477            return Strings.UNESCAPED_COLON.split(pair);
478        }
479        return split;
480    }
481
482    private static class ActualType {
483        final Class<?> rawType;
484        final Type genericType;
485        public ActualType(final Class<?> rawType, final Type genericType) {
486            this.rawType = Objects.requireNonNull(rawType);
487            this.genericType = Objects.requireNonNull(genericType);
488        }
489    }
490    private static ActualType actualTypeForTypeParam(final Type type, final int paramIndex, final int paramCount) {
491        if (type instanceof ParameterizedType) {
492            final Type[] actualTypeArgs = ((ParameterizedType) type).getActualTypeArguments();
493            if (actualTypeArgs.length == paramCount) {
494                Type actualType = actualTypeArgs[paramIndex];
495                if (actualType instanceof WildcardType) {
496                    final Type[] bounds = ((WildcardType) actualType).getUpperBounds();
497                    if (bounds.length == 1) {
498                        actualType = bounds[0];
499                    }
500                }
501                if (actualType instanceof Class) {
502                    return new ActualType((Class<?>) actualType, actualType);
503                }
504                if (actualType instanceof ParameterizedType) {
505                    final ParameterizedType parameterizedType = (ParameterizedType)actualType;
506                    if (parameterizedType.getRawType() instanceof Class) {
507                        return new ActualType((Class<?>)parameterizedType.getRawType(), parameterizedType);
508                    }
509                }
510            }
511        }
512        if (Properties.class.equals(type) && paramCount == 2) {
513            return new ActualType(String.class, String.class);
514        }
515        throw new IllegalArgumentException("Could not derive actual generic type [" + paramIndex + "] for " + type);
516    }
517
518    private static Type genericListType(final Type listElementType) {
519        return new ParameterizedType() {
520            @Override
521            public Type[] getActualTypeArguments() {
522                return new Type[]{listElementType};
523            }
524            @Override
525            public Type getRawType() {
526                return List.class;
527            }
528            @Override
529            public Type getOwnerType() {
530                return null;
531            }
532            @Override
533            public String toString() {
534                return List.class.getName() + "<" + listElementType + ">";
535            }
536        };
537    }
538
539    private Converters() {
540        throw new RuntimeException("No Converters for you!");
541    }
542}