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            final String[] parts = parseListValues(plainValue);
184            final List<Object> list = new ArrayList<>(parts.length);
185            for (int i = 0; i < parts.length; i++) {
186                list.add(elementConverter.convert(elementType.rawType, elementType.genericType, parts[i].trim()));
187            }
188            return list;
189        }
190
191        private static <E extends Enum<E>> EnumSet<E> enumSet(final Class<E> enumType, final List<?> list) {
192            final EnumSet<E> set = EnumSet.noneOf(enumType);
193            list.forEach(v -> set.add(enumType.cast(v)));
194            return set;
195        }
196    }
197
198    /**
199     * Value converter for array target types including primitive arrays.
200     */
201    public static class ArrayConverter implements ValueConverter {
202        private final ValueConverter elementConverter;
203        private final CollectionConverter collectionConverter;
204        public ArrayConverter(final ValueConverter elementConverter) {
205            this.elementConverter = Objects.requireNonNull(elementConverter);
206            this.collectionConverter = new CollectionConverter(elementConverter);
207        }
208
209        @Override
210        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
211            if (!type.isArray()) {
212                throw new IllegalArgumentException("Type must be an array: " + type.getName());
213            }
214            final Class<?> componentType = type.getComponentType();
215            final Type genericComponentType;
216            if (genericType instanceof GenericArrayType) {
217                genericComponentType = ((GenericArrayType)genericType).getGenericComponentType();
218            } else {
219                genericComponentType = componentType;
220            }
221            final Type genericListType = genericListType(genericComponentType);
222            final List<?> list = collectionConverter.convert(List.class, genericListType, value);
223            final Object array = Array.newInstance(componentType, list.size());
224            for (int i = 0; i < list.size(); i++) {
225                final Object val = list.get(i);
226                Array.set(array, i, val);
227            }
228            return type.cast(array);
229        }
230    }
231
232    /**
233     * Value converter for target type {@link Map} or sub-interfaces and implementations of it.
234     */
235    public static class MapConverter implements ValueConverter {
236        private final ValueConverter elementConverter;
237        public MapConverter(final ValueConverter elementConverter) {
238            this.elementConverter = Objects.requireNonNull(elementConverter);
239        }
240
241        @Override
242        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
243            final ActualType keyType = actualTypeForTypeParam(genericType, 0, 2);
244            final ActualType valueType = actualTypeForTypeParam(genericType, 1, 2);
245            final Map<?,?> map = toMap(keyType, valueType, value);
246            if (type.isInstance(map)) {
247                return type.cast(map);
248            }
249            if (type.isAssignableFrom(LinkedHashMap.class)) {
250                return type.cast(new LinkedHashMap<>(map));
251            }
252            if (type.isAssignableFrom(TreeMap.class)) {
253                return type.cast(new TreeMap<>(map));
254            }
255            if (type.isAssignableFrom(HashMap.class)) {
256                return type.cast(new HashMap<>(map));
257            }
258            if (type.isAssignableFrom(Hashtable.class)) {
259                return type.cast(new Hashtable<>(map));
260            }
261            if (type.isAssignableFrom(EnumMap.class)) {
262                final EnumMap<?,?> enumMap = enumMap(keyType.rawType.asSubclass(Enum.class), map);
263                return type.cast(enumMap);
264            }
265            if (type.isAssignableFrom(Properties.class)) {
266                final Properties props = new Properties();
267                props.putAll(map);
268                return type.cast(props);
269            }
270            if (type.isAssignableFrom(ConcurrentHashMap.class)) {
271                return type.cast(new ConcurrentHashMap<>(map));
272            }
273            if (type.isAssignableFrom(ConcurrentSkipListMap.class)) {
274                return type.cast(new ConcurrentSkipListMap<>(map));
275            }
276            //unsupported map type
277            throw new IllegalArgumentException("Cannot convert value to " + type.getName() + ": " + value);
278        }
279
280        private Map<Object, Object> toMap(final ActualType keyType, final ActualType valueType, final String value) {
281            final String plainValue = Strings.removeStartAndEndChars(value, '{', '}');
282            if (plainValue.trim().isEmpty()) {
283                return Collections.emptyMap();
284            }
285            final String[] parts = parseListValues(plainValue);
286            final Map<Object, Object> map = new LinkedHashMap<>();
287            for (int i = 0; i < parts.length; i++) {
288                final String[] keyAndValue = parseKeyValue(parts[i].trim());
289                if (keyAndValue.length != 2) {
290                    throw new IllegalArgumentException("Invalid map key/value pair: " + parts[i]);
291                }
292                try {
293                    final Object key = elementConverter.convert(keyType.rawType, keyType.genericType, keyAndValue[0].trim());
294                    final Object val = elementConverter.convert(valueType.rawType, valueType.genericType, keyAndValue[1].trim());
295                    map.put(key, val);
296                } catch (final Exception e) {
297                    throw new IllegalArgumentException("Conversion to map key/value failed: " + parts[i], e);
298                }
299            }
300            return map;
301        }
302
303        private static <K extends Enum<K>, V> EnumMap<K, V> enumMap(final Class<K> enumType, final Map<?,V> map) {
304            final EnumMap<K,V> enumMap = new EnumMap(enumType);
305            map.forEach((k,v) -> enumMap.put(enumType.cast(k), v));
306            return enumMap;
307        }
308    }
309
310    /**
311     * Value converter for Java beans as target type. The beans either have getters and setters to access the fields
312     * or otherwise fields are accessed directly.
313     */
314    public static class BeanConverter implements ValueConverter {
315
316        private final ValueConverter elementConverter;
317        public BeanConverter(final ValueConverter elementConverter) {
318            this.elementConverter = Objects.requireNonNull(elementConverter);
319        }
320
321        @Override
322        public <T> T convert(final Class<T> type, final Type genericType, final String value) {
323            final T instance = newInstance(type, value);
324            final Map<String, Accessor> accessorByName = new LinkedHashMap<>();
325            if (hasAccessibleSetters(type)) {
326                inspectSetters(type, accessorByName);
327            } else {
328                inspectFields(type, accessorByName);
329            }
330            if (accessorByName.isEmpty()) {
331                throw new IllegalArgumentException(type.getName() + " is not a bean class, no accessible setters or fields found");
332            }
333            injectValues(instance, accessorByName, value);
334            return instance;
335        }
336
337        private <T> T newInstance(final Class<T> type, final String value) {
338            try {
339                final Constructor<T> constructor = type.getDeclaredConstructor();
340                constructor.setAccessible(true);
341                return constructor.newInstance();
342            } catch (final Exception e) {
343                throw new IllegalArgumentException("Could not instantiate bean " + type.getName(), e);
344            }
345        }
346
347        private void injectValues(final Object instance, final Map<String, Accessor> accessorByName, final String value) {
348            final String plainValue = Strings.removeStartAndEndChars(value, '{', '}');
349            final String[] parts = parseListValues(plainValue);
350            final Map<String, String> valueByName = new LinkedHashMap<>();
351            for (int i = 0; i < parts.length; i++) {
352                final String[] nameAndValue = parseKeyValue(parts[i].trim());
353                if (nameAndValue.length != 2) {
354                    throw new IllegalArgumentException("Invalid name/value pair: " + parts[i]);
355                }
356                final String name = normalizeFieldName(nameAndValue[0].trim());
357                final String val = nameAndValue[1].trim();
358                valueByName.put(name, val);
359            }
360            for (final Map.Entry<String, Accessor> e : accessorByName.entrySet()) {
361                final String val = valueByName.get(e.getKey());
362                if (val == null) {
363                    throw new IllegalArgumentException("No value found for bean property " + instance.getClass().getName() + "." + e.getKey());
364                }
365                try {
366                    final Class<?> type = e.getValue().type();
367                    final Type genericType = e.getValue().genericType();
368                    final Object convertedVal = elementConverter.convert(type, genericType, val);
369                    e.getValue().set(instance, convertedVal);
370                } catch (final Exception ex) {
371                    throw new IllegalArgumentException("Could not set bean property " + instance.getClass().getName() + "." + e.getKey() +
372                            " to value: " + val, ex);
373                }
374            }
375        }
376
377        public static boolean isBeanClass(final Class<?> clazz) {
378            return isInstantiatable(clazz) && (hasAccessibleFields(clazz) || hasAccessibleSetters(clazz));
379        }
380
381        private static boolean isInstantiatable(final Class<?> clazz) {
382            return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && hasDefaultConstructor(clazz);
383        }
384
385        private static boolean hasDefaultConstructor(final Class<?> clazz) {
386            try {
387                clazz.getDeclaredConstructor();
388                return true;
389            } catch (final Exception e) {
390                return false;
391            }
392        }
393
394        private interface Accessor {
395            void set(Object instance, Object value) throws Exception;
396            default Class<?> type() {
397                return Object.class;
398            }
399            default Type genericType() {
400                return Object.class;
401            }
402            static Accessor forType(final Class<?> type, final Type genericType, final Accessor accessor) {
403                return new Accessor() {
404                    @Override
405                    public void set(final Object instance, final Object value) throws Exception {
406                        accessor.set(instance, value);
407                    }
408
409                    @Override
410                    public Class<?> type() {
411                        return type;
412                    }
413
414                    @Override
415                    public Type genericType() {
416                        return genericType;
417                    }
418                };
419            }
420        }
421
422        private static boolean hasAccessibleSetters(final Class<?> clazz) {
423            return !inspectSetters(clazz, new HashMap<>()).isEmpty();
424        }
425        private static final Map<String, Accessor> inspectSetters(final Class<?> clazz, final Map<String, Accessor> accessorByName) {
426            if (clazz == null || Object.class.equals(clazz)) {
427                return accessorByName;
428            }
429            for (final Method method : clazz.getDeclaredMethods()) {
430                final int mod = method.getModifiers();
431                final String name = method.getName();
432                if (name.length() > 3 && name.startsWith("set") && method.getParameterCount() == 1 &&
433                        !method.isSynthetic() && !Modifier.isStatic(mod) && !Modifier.isPrivate(mod) && !Modifier.isProtected(mod)) {
434                    final String propertyName = normalizeFieldName(name.substring(3));
435                    accessorByName.put(propertyName, Accessor.forType(method.getParameterTypes()[0], method.getGenericParameterTypes()[0], method::invoke));
436                }
437            }
438            return inspectSetters(clazz.getSuperclass(), accessorByName);
439        }
440
441        private static boolean hasAccessibleFields(final Class<?> clazz) {
442            return !inspectFields(clazz, new HashMap<>()).isEmpty();
443        }
444        private static final Map<String, Accessor> inspectFields(final Class<?> clazz, final Map<String, Accessor> accessorByName) {
445            if (clazz == null || Object.class.equals(clazz)) {
446                return accessorByName;
447            }
448            for (final Field field : clazz.getDeclaredFields()) {
449                final int mod = field.getModifiers();
450                if (!field.isSynthetic() && !Modifier.isFinal(mod) && !Modifier.isStatic(mod) && !Modifier.isPrivate(mod) && !Modifier.isProtected(mod)) {
451                    accessorByName.put(field.getName(), Accessor.forType(field.getType(), field.getGenericType(), field::set));
452                }
453            }
454            return inspectFields(clazz.getSuperclass(), accessorByName);
455        }
456    }
457
458    private static String normalizeFieldName(final String name) {
459        if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) {
460            return Character.toLowerCase(name.charAt(0)) + name.substring(1);
461        }
462        return name;
463    }
464
465    private static final String[] parseKeyValue(final String pair) {
466        return Strings.split(pair, Strings.UNESCAPED_EQUAL, Strings.UNESCAPED_COLON);
467    }
468
469    private static final String[] parseListValues(final String pair) {
470        return Strings.split(pair, Strings.UNESCAPED_COMMA, Strings.UNESCAPED_SEMICOLON);
471    }
472
473    private static class ActualType {
474        final Class<?> rawType;
475        final Type genericType;
476        public ActualType(final Class<?> rawType, final Type genericType) {
477            this.rawType = Objects.requireNonNull(rawType);
478            this.genericType = Objects.requireNonNull(genericType);
479        }
480    }
481    private static ActualType actualTypeForTypeParam(final Type type, final int paramIndex, final int paramCount) {
482        if (type instanceof ParameterizedType) {
483            final Type[] actualTypeArgs = ((ParameterizedType) type).getActualTypeArguments();
484            if (actualTypeArgs.length == paramCount) {
485                Type actualType = actualTypeArgs[paramIndex];
486                if (actualType instanceof WildcardType) {
487                    final Type[] bounds = ((WildcardType) actualType).getUpperBounds();
488                    if (bounds.length == 1) {
489                        actualType = bounds[0];
490                    }
491                }
492                if (actualType instanceof Class) {
493                    return new ActualType((Class<?>) actualType, actualType);
494                }
495                if (actualType instanceof ParameterizedType) {
496                    final ParameterizedType parameterizedType = (ParameterizedType)actualType;
497                    if (parameterizedType.getRawType() instanceof Class) {
498                        return new ActualType((Class<?>)parameterizedType.getRawType(), parameterizedType);
499                    }
500                }
501            }
502        }
503        if (Properties.class.equals(type) && paramCount == 2) {
504            return new ActualType(String.class, String.class);
505        }
506        throw new IllegalArgumentException("Could not derive actual generic type [" + paramIndex + "] for " + type);
507    }
508
509    private static Type genericListType(final Type listElementType) {
510        return new ParameterizedType() {
511            @Override
512            public Type[] getActualTypeArguments() {
513                return new Type[]{listElementType};
514            }
515            @Override
516            public Type getRawType() {
517                return List.class;
518            }
519            @Override
520            public Type getOwnerType() {
521                return null;
522            }
523            @Override
524            public String toString() {
525                return List.class.getName() + "<" + listElementType + ">";
526            }
527        };
528    }
529
530    private Converters() {
531        throw new RuntimeException("No Converters for you!");
532    }
533}