001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.conversion;
018
019import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.lang.reflect.*;
024import java.math.*;
025import java.nio.charset.*;
026import java.util.*;
027import java.util.concurrent.atomic.*;
028
029import org.apache.juneau.commons.reflect.*;
030
031/**
032 * A concrete {@link CachingConverter} that supports common type conversions without
033 * requiring a {@code MarshallingContext} or {@code MarshallingSession}.
034 *
035 * <p>
036 * The following conversions are supported:
037 *
038 * <table class='styled'>
039 *    <tr><th>Input type</th><th>Output type</th><th>Notes</th></tr>
040 *    <tr>
041 *       <td>Any type</td>
042 *       <td>Same or supertype</td>
043 *       <td>Identity / widening (no-op cast)</td>
044 *    </tr>
045 *    <tr>
046 *       <td>{@link Number}</td>
047 *       <td>
048 *          {@link Integer}, {@link Long}, {@link Short}, {@link Float}, {@link Double}, {@link Byte},
049 *          {@link AtomicInteger}, {@link AtomicLong}, {@link BigDecimal}, {@link BigInteger},
050 *          and primitive equivalents
051 *       </td>
052 *       <td>Narrowing/widening numeric conversion</td>
053 *    </tr>
054 *    <tr>
055 *       <td>{@link Boolean}</td>
056 *       <td>{@link Number} types (including {@link BigDecimal}, {@link BigInteger})</td>
057 *       <td><c>true=1, false=0</c></td>
058 *    </tr>
059 *    <tr>
060 *       <td>{@link CharSequence}</td>
061 *       <td>{@link Number} types</td>
062 *       <td>Parsed via {@link org.apache.juneau.commons.utils.StringUtils#parseNumber(String, Class)};
063 *          empty string or {@code "null"} returns <jk>null</jk></td>
064 *    </tr>
065 *    <tr>
066 *       <td>{@link Number}</td>
067 *       <td>{@link Boolean}</td>
068 *       <td><c>intValue() != 0</c></td>
069 *    </tr>
070 *    <tr>
071 *       <td>{@link CharSequence}</td>
072 *       <td>{@link Boolean}</td>
073 *       <td>{@link Boolean#valueOf(String)}</td>
074 *    </tr>
075 *    <tr>
076 *       <td>{@link CharSequence} (length 1)</td>
077 *       <td>{@link Character}</td>
078 *       <td><c>charAt(0)</c></td>
079 *    </tr>
080 *    <tr>
081 *       <td>{@link Number}</td>
082 *       <td>{@link Character}</td>
083 *       <td>First character of {@code toString()} (e.g. {@code 65} → {@code '6'})</td>
084 *    </tr>
085 *    <tr>
086 *       <td>Any</td>
087 *       <td>{@link String}</td>
088 *       <td>{@link Object#toString()}, with array support via {@link Arrays#toString(Object[])}</td>
089 *    </tr>
090 *    <tr>
091 *       <td>{@link CharSequence}</td>
092 *       <td>{@code byte[]}</td>
093 *       <td>UTF-8 encoding via {@link String#getBytes(Charset)}</td>
094 *    </tr>
095 *    <tr>
096 *       <td>{@link CharSequence}</td>
097 *       <td>Any {@link Enum}</td>
098 *       <td>{@link Enum#valueOf(Class, String)}</td>
099 *    </tr>
100 *    <tr>
101 *       <td>{@link Collection} or array</td>
102 *       <td>{@link Collection} subtype</td>
103 *       <td>Copies elements; element type from <c>args[0]</c></td>
104 *    </tr>
105 *    <tr>
106 *       <td>{@link Map}</td>
107 *       <td>{@link Map} subtype</td>
108 *       <td>Copies entries; key type from <c>args[0]</c>, value type from <c>args[1]</c></td>
109 *    </tr>
110 *    <tr>
111 *       <td>{@link Collection} or array</td>
112 *       <td>Array type</td>
113 *       <td>Element type from <c>outType.getComponentType()</c></td>
114 *    </tr>
115 *    <tr>
116 *       <td>{@link String}</td>
117 *       <td>{@link TimeZone}</td>
118 *       <td>{@link TimeZone#getTimeZone(String)}</td>
119 *    </tr>
120 *    <tr>
121 *       <td>{@link TimeZone}</td>
122 *       <td>{@link String}</td>
123 *       <td>{@link TimeZone#getID()}</td>
124 *    </tr>
125 *    <tr>
126 *       <td>{@link String}</td>
127 *       <td>{@link Locale}</td>
128 *       <td>{@link Locale#forLanguageTag(String)} (underscores converted to hyphens)</td>
129 *    </tr>
130 *    <tr>
131 *       <td>Any</td>
132 *       <td>Any with <c>static T fromString/valueOf/of/from/parse/create/forName/fromValue/builder(X)</c>
133 *          or dynamic <c>fromX/forX/parseX</c> where X is the input class name</td>
134 *       <td>Reflection-based static factory lookup</td>
135 *    </tr>
136 *    <tr>
137 *       <td>Any</td>
138 *       <td>Any with <c>public T(X)</c> constructor</td>
139 *       <td>Reflection-based constructor lookup</td>
140 *    </tr>
141 *    <tr>
142 *       <td>Any with <c>toX()</c> instance method</td>
143 *       <td>Any type X</td>
144 *       <td>Instance method where name matches <c>to</c> + output class name (e.g. <c>toInteger()</c>)</td>
145 *    </tr>
146 * </table>
147 *
148 * <h5 class='section'>Thread Safety:</h5>
149 * <p>
150 * This class is thread-safe. The singleton instance can be safely shared across multiple threads.
151 */
152@SuppressWarnings({
153   "java:S1067", // Complex boolean expressions in conversion checks reflect the natural type hierarchy
154   "java:S1192", // "parse" literal is used in multiple independent Sets/arrays; extracting it would reduce clarity
155   "java:S3776", // Cognitive complexity of conversion dispatch methods is inherent to the number of supported type pairs
156   "rawtypes", // Raw types necessary for generic conversion dispatch
157   "unchecked" // Type erasure requires unchecked casts throughout conversion logic
158})
159public class BasicConverter extends CachingConverter {
160
161   /**
162    * Singleton instance.
163    */
164   public static final BasicConverter INSTANCE = new BasicConverter();
165
166   private static final Set<String> FACTORY_METHOD_NAMES = Set.of(
167      "fromString", "valueOf", "of", "from", "parse", "create", "forName", "fromValue", "builder"
168   );
169
170   private static final Map<Class<?>,Class<?>> PRIMITIVE_TO_WRAPPER = Map.of(
171      boolean.class, Boolean.class,
172      byte.class, Byte.class,
173      char.class, Character.class,
174      double.class, Double.class,
175      float.class, Float.class,
176      int.class, Integer.class,
177      long.class, Long.class,
178      short.class, Short.class
179   );
180
181   /**
182    * Constructor.
183    */
184   protected BasicConverter() {}
185
186   @Override
187   protected <I,O> Conversion<I,O> findConversion(Class<I> inType, Class<O> outType) {
188      var out = outType.isPrimitive() ? (Class<O>) PRIMITIVE_TO_WRAPPER.get(outType) : outType;
189
190      Conversion<I,O> c;
191
192      if ((c = findSpecialConversion(inType, out)) != null) return c;
193
194      if (out.isAssignableFrom(inType) && !Collection.class.isAssignableFrom(out) && !Map.class.isAssignableFrom(out))
195         return (in, memberOf, session, args) -> (O) in;
196
197      if (Number.class.isAssignableFrom(out) && (c = findNumberConversion(inType, out)) != null) return c;
198
199      if (out == Boolean.class && (c = findBooleanConversion(inType)) != null) return c;
200
201      if (out == Character.class && (c = findCharacterConversion(inType)) != null) return c;
202
203      if (out == String.class)
204         return (Conversion<I,O>) findToStringConversion(inType);
205
206      if (out.isEnum() && (c = findEnumConversion(inType, out)) != null) return c;
207
208      if (out == Optional.class && (c = findOptionalConversion()) != null) return c;
209
210      if ((out == OptionalInt.class || out == OptionalLong.class || out == OptionalDouble.class)
211            && (c = findPrimitiveOptionalConversion(out)) != null) return c;
212
213      if (Collection.class.isAssignableFrom(out) && (c = findCollectionConversion(inType, out)) != null) return c;
214
215      if (Map.class.isAssignableFrom(out) && (c = findMapConversion(inType, out)) != null) return c;
216
217      if (out.isArray() && (c = findArrayConversion(inType, out)) != null) return c;
218
219      if ((c = findToXMethod(inType, out)) != null) return c;
220
221      if ((c = findStaticFactory(inType, out)) != null) return c;
222
223      if ((c = findConstructorConversion(inType, out)) != null) return c;
224
225      return null;
226   }
227
228   //-----------------------------------------------------------------------------------------------------------------
229   // Number conversions
230   //-----------------------------------------------------------------------------------------------------------------
231
232   private <I,O> Conversion<I,O> findNumberConversion(Class<I> inType, Class<O> outType) {
233      if (Number.class.isAssignableFrom(inType))
234         return findNumberFromNumber(outType);
235      if (inType == Boolean.class)
236         return (Conversion<I,O>) findNumberFromBoolean(outType);
237      if (CharSequence.class.isAssignableFrom(inType))
238         return (Conversion<I,O>) findNumberFromString(outType);
239      return null;
240   }
241
242   private <I,O> Conversion<I,O> findNumberFromNumber(Class<O> outType) {
243      if (outType == Integer.class) return (in, memberOf, session, args) -> (O) Integer.valueOf(((Number) in).intValue());
244      if (outType == Long.class) return (in, memberOf, session, args) -> (O) Long.valueOf(((Number) in).longValue());
245      if (outType == Short.class) return (in, memberOf, session, args) -> (O) Short.valueOf(((Number) in).shortValue());
246      if (outType == Float.class) return (in, memberOf, session, args) -> (O) Float.valueOf(((Number) in).floatValue());
247      if (outType == Double.class) return (in, memberOf, session, args) -> (O) Double.valueOf(((Number) in).doubleValue());
248      if (outType == Byte.class) return (in, memberOf, session, args) -> (O) Byte.valueOf(((Number) in).byteValue());
249      if (outType == AtomicInteger.class) return (in, memberOf, session, args) -> (O) new AtomicInteger(((Number) in).intValue());
250      if (outType == AtomicLong.class) return (in, memberOf, session, args) -> (O) new AtomicLong(((Number) in).longValue());
251      if (outType == BigDecimal.class) return (in, memberOf, session, args) -> (O) new BigDecimal(in.toString());
252      if (outType == BigInteger.class) return (in, memberOf, session, args) -> {
253         var n = (Number) in;
254         if (n instanceof BigDecimal n2) return (O) n2.toBigInteger();
255         if (n instanceof BigInteger n2) return (O) n2;
256         return (O) BigInteger.valueOf(n.longValue());
257      };
258      return null;
259   }
260
261   private <O> Conversion<Boolean,O> findNumberFromBoolean(Class<O> outType) {
262      if (outType == Integer.class) return (in, memberOf, session, args) -> (O) Integer.valueOf(in.booleanValue() ? 1 : 0);
263      if (outType == Long.class) return (in, memberOf, session, args) -> (O) Long.valueOf(in.booleanValue() ? 1L : 0L);
264      if (outType == Short.class) return (in, memberOf, session, args) -> (O) Short.valueOf(in.booleanValue() ? (short) 1 : (short) 0);
265      if (outType == Float.class) return (in, memberOf, session, args) -> (O) Float.valueOf(in.booleanValue() ? 1f : 0f);
266      if (outType == Double.class) return (in, memberOf, session, args) -> (O) Double.valueOf(in.booleanValue() ? 1d : 0d);
267      if (outType == Byte.class) return (in, memberOf, session, args) -> (O) Byte.valueOf(in.booleanValue() ? (byte) 1 : (byte) 0);
268      if (outType == AtomicInteger.class) return (in, memberOf, session, args) -> (O) new AtomicInteger(in.booleanValue() ? 1 : 0);
269      if (outType == AtomicLong.class) return (in, memberOf, session, args) -> (O) new AtomicLong(in.booleanValue() ? 1L : 0L);
270      if (outType == BigDecimal.class) return (in, memberOf, session, args) -> (O) BigDecimal.valueOf(in.booleanValue() ? 1L : 0L);
271      if (outType == BigInteger.class) return (in, memberOf, session, args) -> (O) BigInteger.valueOf(in.booleanValue() ? 1L : 0L);
272      return null;
273   }
274
275   private <O> Conversion<CharSequence,O> findNumberFromString(Class<O> outType) {
276      return (in, memberOf, session, args) -> {
277         var s = in.toString();
278         if (s.isEmpty() || eq(s, "null"))
279            return null;
280         return (O) parseNumber(s, (Class<? extends Number>) outType);
281      };
282   }
283
284   //-----------------------------------------------------------------------------------------------------------------
285   // Boolean conversions
286   //-----------------------------------------------------------------------------------------------------------------
287
288   private <I,O> Conversion<I,O> findBooleanConversion(Class<I> inType) {
289      if (Number.class.isAssignableFrom(inType))
290         return (in, memberOf, session, args) -> (O) Boolean.valueOf(((Number) in).intValue() != 0);
291      return null;
292   }
293
294   //-----------------------------------------------------------------------------------------------------------------
295   // Character conversions
296   //-----------------------------------------------------------------------------------------------------------------
297
298   private <I,O> Conversion<I,O> findCharacterConversion(Class<I> inType) {
299      if (CharSequence.class.isAssignableFrom(inType))
300         return (in, memberOf, session, args) -> {
301            var s = in.toString();
302            if (s.length() != 1)
303               throw iaex("Cannot convert string of length %s to char: '%s'", s.length(), s);
304            return (O) Character.valueOf(s.charAt(0));
305         };
306      if (Number.class.isAssignableFrom(inType))
307         return (in, memberOf, session, args) -> {
308            var s = in.toString();
309            return s.isEmpty() ? null : (O) Character.valueOf(s.charAt(0));
310         };
311      return null;
312   }
313
314   //-----------------------------------------------------------------------------------------------------------------
315   // String conversions
316   //-----------------------------------------------------------------------------------------------------------------
317
318   private <I> Conversion<I,String> findToStringConversion(Class<I> inType) {
319      if (inType.isArray()) {
320         if (inType == int[].class) return (in, memberOf, session, args) -> Arrays.toString((int[]) in);
321         if (inType == long[].class) return (in, memberOf, session, args) -> Arrays.toString((long[]) in);
322         if (inType == double[].class) return (in, memberOf, session, args) -> Arrays.toString((double[]) in);
323         if (inType == float[].class) return (in, memberOf, session, args) -> Arrays.toString((float[]) in);
324         if (inType == boolean[].class) return (in, memberOf, session, args) -> Arrays.toString((boolean[]) in);
325         if (inType == byte[].class) return (in, memberOf, session, args) -> Arrays.toString((byte[]) in);
326         if (inType == short[].class) return (in, memberOf, session, args) -> Arrays.toString((short[]) in);
327         if (inType == char[].class) return (in, memberOf, session, args) -> Arrays.toString((char[]) in);
328         return (in, memberOf, session, args) -> Arrays.deepToString((Object[]) in);
329      }
330      return (in, memberOf, session, args) -> in.toString();
331   }
332
333   //-----------------------------------------------------------------------------------------------------------------
334   // Enum conversions
335   //-----------------------------------------------------------------------------------------------------------------
336
337   private <I,O> Conversion<I,O> findEnumConversion(Class<I> inType, Class<O> outType) {
338      if (CharSequence.class.isAssignableFrom(inType))
339         return (in, memberOf, session, args) -> (O) Enum.valueOf((Class<Enum>) outType, in.toString());
340      return null;
341   }
342
343   //-----------------------------------------------------------------------------------------------------------------
344   // Optional conversions
345   //-----------------------------------------------------------------------------------------------------------------
346
347   private <I,O> Conversion<I,O> findOptionalConversion() {
348      return (Conversion<I,O>) (Conversion<Object,Optional<?>>) (in, memberOf, session, args) -> {
349         if (in instanceof Optional<?> in2) {
350            if (args.length == 0)
351               return in2;
352            return in2.map(x -> to(x, memberOf, session, args[0]));
353         }
354         if (args.length == 0)
355            return o(in);
356         return o(to(in, memberOf, session, args[0]));
357      };
358   }
359
360   /**
361    * Conversion to a primitive {@link OptionalInt}/{@link OptionalLong}/{@link OptionalDouble}.
362    *
363    * <p>
364    * Mirrors the {@link Optional} contract: a {@code null} input (or an empty optional of any flavor)
365    * yields the matching empty primitive optional; any other value is unwrapped (if it is itself an
366    * optional) and coerced to the matching boxed number before wrapping.
367    *
368    * @param out The target primitive-optional class.
369    * @return The conversion, never <jk>null</jk>.
370    */
371   private <I,O> Conversion<I,O> findPrimitiveOptionalConversion(Class<O> out) {
372      final Class<? extends Number> elementType;
373      if (out == OptionalInt.class)
374         elementType = Integer.class;
375      else if (out == OptionalLong.class)
376         elementType = Long.class;
377      else
378         elementType = Double.class;
379      return (Conversion<I,O>) (Conversion<Object,Object>) (in, memberOf, session, args) -> {
380         Object v = in;
381         if (v instanceof Optional<?> v2)
382            v = v2.orElse(null);
383         else if (v instanceof OptionalInt v3)
384            v = v3.isPresent() ? Integer.valueOf(v3.getAsInt()) : null;
385         else if (v instanceof OptionalLong v4)
386            v = v4.isPresent() ? Long.valueOf(v4.getAsLong()) : null;
387         else if (v instanceof OptionalDouble v5)
388            v = v5.isPresent() ? Double.valueOf(v5.getAsDouble()) : null;
389         if (v == null)
390            return emptyPrimitiveOptional(out);
391         var n = (Number) to(v, memberOf, session, elementType);
392         if (n == null)
393            return emptyPrimitiveOptional(out);
394         if (out == OptionalInt.class)
395            return OptionalInt.of(n.intValue());
396         if (out == OptionalLong.class)
397            return OptionalLong.of(n.longValue());
398         return OptionalDouble.of(n.doubleValue());
399      };
400   }
401
402   private static Object emptyPrimitiveOptional(Class<?> out) {
403      if (out == OptionalInt.class)
404         return OptionalInt.empty();
405      if (out == OptionalLong.class)
406         return OptionalLong.empty();
407      return OptionalDouble.empty();
408   }
409
410   //-----------------------------------------------------------------------------------------------------------------
411   // Collection conversions
412   //-----------------------------------------------------------------------------------------------------------------
413
414   private <I,O> Conversion<I,O> findCollectionConversion(Class<I> inType, Class<O> outType) {
415      if (Collection.class.isAssignableFrom(inType) || inType.isArray()) {
416         return (in, memberOf, session, args) -> {
417            // Treat Object.class as "no element type" since it means untyped/raw and needs no element conversion.
418            var elemType = args.length > 0 && args[0] != Object.class ? args[0] : null;
419            if (elemType == null && !inType.isArray() && outType.isAssignableFrom(inType))
420               return (O) in;
421            var result = newCollection(outType);
422            if (Collection.class.isAssignableFrom(inType)) {
423               for (var elem : (Collection<?>) in)
424                  result.add(elemType != null ? to(elem, elemType) : elem);
425            } else {
426               var len = Array.getLength(in);
427               for (var i = 0; i < len; i++) {
428                  var elem = Array.get(in, i);
429                  result.add(elemType != null ? to(elem, elemType) : elem);
430               }
431            }
432            return (O) result;
433         };
434      }
435      return null;
436   }
437
438   private Collection<Object> newCollection(Class<?> outType) {
439      if (outType == List.class || outType == Collection.class || outType == Iterable.class /* HTT: Iterable is a supertype of Collection so findCollectionConversion never passes Iterable as outType */ || outType == AbstractList.class)
440         return l();
441      if (outType == Set.class || outType == LinkedHashSet.class || outType == AbstractSet.class)
442         return st();
443      if (outType == SortedSet.class || outType == NavigableSet.class || outType == TreeSet.class)
444         return new TreeSet<>();
445      if (outType == Queue.class || outType == Deque.class || outType == LinkedList.class)
446         return new LinkedList<>();
447      return newInstanceOrDefault(outType, ArrayList::new);
448   }
449
450   //-----------------------------------------------------------------------------------------------------------------
451   // Map conversions
452   //-----------------------------------------------------------------------------------------------------------------
453
454   private <I,O> Conversion<I,O> findMapConversion(Class<I> inType, Class<O> outType) {
455      if (Map.class.isAssignableFrom(inType)) {
456         return (in, memberOf, session, args) -> {
457            // Treat Object.class as "no key/value type" since it means untyped/raw and needs no element conversion.
458            var keyType = args.length > 0 && args[0] != Object.class ? args[0] : null;
459            var valType = args.length > 1 && args[1] != Object.class ? args[1] : null;
460            if (keyType == null && outType.isAssignableFrom(inType))
461               return (O) in;
462            var result = newMap(outType);
463            ((Map<?,?>) in).forEach((k, v) -> result.put(
464               keyType != null ? to(k, keyType) : k,
465               valType != null ? to(v, valType) : v
466            ));
467            return (O) result;
468         };
469      }
470      return null;
471   }
472
473   private Map<Object,Object> newMap(Class<?> outType) {
474      if (outType == Map.class || outType == LinkedHashMap.class || outType == AbstractMap.class)
475         return m();
476      if (outType == SortedMap.class || outType == NavigableMap.class || outType == TreeMap.class)
477         return new TreeMap<>();
478      if (outType == HashMap.class)
479         return new HashMap<>();
480      return newInstanceOrDefault(outType, LinkedHashMap::new);
481   }
482
483   //-----------------------------------------------------------------------------------------------------------------
484   // Array conversions
485   //-----------------------------------------------------------------------------------------------------------------
486
487   private <I,O> Conversion<I,O> findArrayConversion(Class<I> inType, Class<O> outType) {
488      if (Collection.class.isAssignableFrom(inType) || inType.isArray()) {
489         var componentType = outType.getComponentType();
490         // For array→array, validate that element-level conversion is possible.
491         // Skip the pre-check for Object[] — runtime elements may be of a more specific type.
492         // Collections use runtime element types so we skip the pre-check there too.
493         if (inType.isArray()) {
494            var inComponentType = inType.getComponentType();
495            // Primitive component types are auto-boxed by Array.get(), so skip the canConvert check.
496            // Otherwise verify that element-level conversion is possible before returning a lambda.
497            if (!inComponentType.isPrimitive()
498                  && inComponentType != componentType
499                  && inComponentType != Object.class
500                  && !canConvert(inComponentType, componentType))
501               return null;
502         }
503         return (in, memberOf, session, args) -> {
504            if (Collection.class.isAssignableFrom(inType)) {
505               var list = (Collection<?>) in;
506               var arr = Array.newInstance(componentType, list.size());
507               var i = 0;
508               for (var elem : list)
509                  Array.set(arr, i++, to(elem, memberOf, session, componentType));
510               return (O) arr;
511            }
512            var len = Array.getLength(in);
513            var arr = Array.newInstance(componentType, len);
514            for (var i = 0; i < len; i++)
515               Array.set(arr, i, to(Array.get(in, i), memberOf, session, componentType));
516            return (O) arr;
517         };
518      }
519      return null;
520   }
521
522   //-----------------------------------------------------------------------------------------------------------------
523   // Special-case conversions
524   //-----------------------------------------------------------------------------------------------------------------
525
526   private <I,O> Conversion<I,O> findSpecialConversion(Class<I> inType, Class<O> outType) {
527      if (inType == String.class && outType == TimeZone.class)
528         return (Conversion<I,O>) (Conversion<String,TimeZone>) (in, memberOf, session, args) -> TimeZone.getTimeZone(in);
529      if (TimeZone.class.isAssignableFrom(inType) && outType == String.class)
530         return (Conversion<I,O>) (Conversion<TimeZone,String>) (in, memberOf, session, args) -> in.getID();
531      if (inType == String.class && outType == Locale.class)
532         return (Conversion<I,O>) (Conversion<String,Locale>) (in, memberOf, session, args) -> Locale.forLanguageTag(in.replace('_', '-'));
533      if (CharSequence.class.isAssignableFrom(inType) && outType == Boolean.class)
534         return (in, memberOf, session, args) -> {
535            var s = in.toString();
536            if (s.isEmpty() || eq(s, "null"))
537               return null;
538            return (O) Boolean.valueOf(s);
539         };
540      if (CharSequence.class.isAssignableFrom(inType) && outType == byte[].class)
541         return (in, memberOf, session, args) -> (O) in.toString().getBytes(StandardCharsets.UTF_8);
542      return null;
543   }
544
545   //-----------------------------------------------------------------------------------------------------------------
546   // Reflection: static factory methods
547   //-----------------------------------------------------------------------------------------------------------------
548
549   private <I,O> Conversion<I,O> findStaticFactory(Class<I> inType, Class<O> outType) {
550      var ci = info(outType);
551
552      for (var name : FACTORY_METHOD_NAMES) {
553         var opt = findStaticMethod(ci, name, inType, outType);
554         if (opt.isPresent())
555            return (in, memberOf, session, args) -> opt.get().invoke(null, in);
556      }
557
558      // Walk the type hierarchy (superclasses then interfaces) so that e.g. InputStreamReader
559      // can be passed to a method declared as fromReader(Reader r).
560      for (Class<?> c = inType; c != null && c != Object.class; c = c.getSuperclass()) {
561         var inName = c.getSimpleName();
562         for (var prefix : new String[]{"from", "for", "parse"}) {
563            var opt = findStaticMethod(ci, prefix + inName, inType, outType);
564            if (opt.isPresent())
565               return (in, memberOf, session, args) -> opt.get().invoke(null, in);
566         }
567      }
568      for (var iface : allInterfaces(inType)) {
569         var inName = iface.getSimpleName();
570         for (var prefix : new String[]{"from", "for", "parse"}) {
571            var opt = findStaticMethod(ci, prefix + inName, inType, outType);
572            if (opt.isPresent())
573               return (in, memberOf, session, args) -> opt.get().invoke(null, in);
574         }
575      }
576
577      return null;
578   }
579
580   private static List<Class<?>> allInterfaces(Class<?> c) {
581      var result = new ArrayList<Class<?>>();
582      for (var x = c; x != null; x = x.getSuperclass())
583         for (var iface : x.getInterfaces())
584            collectInterfaces(iface, result);
585      return result;
586   }
587
588   private static void collectInterfaces(Class<?> iface, List<Class<?>> result) {
589      if (!result.contains(iface)) {
590         result.add(iface);
591         for (var parent : iface.getInterfaces())
592            collectInterfaces(parent, result);
593      }
594   }
595
596   private Optional<MethodInfo> findStaticMethod(ClassInfo ci, String name, Class<?> inType, Class<?> outType) {
597      return ci.getPublicMethod(m ->
598         m.isStatic()
599         && m.isNotDeprecated()
600         && m.hasName(name)
601         && m.getParameterCount() == 1
602         && m.getParameterTypes().get(0).isAssignableFrom(inType)
603         && m.hasReturnTypeParent(outType)
604         // Exclude generic Object-param factory methods (e.g. List.of(E)) when input is not Object,
605         // since type erasure makes E appear as Object at runtime, causing false matches.
606         && (inType == Object.class || m.getParameterTypes().get(0).inner() != Object.class)
607      );
608   }
609
610   //-----------------------------------------------------------------------------------------------------------------
611   // Reflection: public constructors
612   //-----------------------------------------------------------------------------------------------------------------
613
614   private <I,O> Conversion<I,O> findConstructorConversion(Class<I> inType, Class<O> outType) {
615      if (outType.getEnclosingClass() != null && !Modifier.isStatic(outType.getModifiers())) {
616         var enclosing = outType.getEnclosingClass();
617         var opt = info(outType).getPublicConstructor(c ->
618            c.getParameterCount() == 2
619            && c.isNotDeprecated()
620            && c.getParameterTypes().get(0).inner() == enclosing
621            && c.getParameterTypes().get(1).isAssignableFrom(inType)
622         );
623         if (opt.isPresent()) {
624            var ctor = opt.get();
625            return (in, memberOf, session, args) -> ctor.newInstance(memberOf, in);
626         }
627      }
628
629      var opt = info(outType).getPublicConstructor(c ->
630         c.getParameterCount() == 1
631         && c.isNotDeprecated()
632         && c.getParameterTypes().get(0).isAssignableFrom(inType)
633      );
634      if (opt.isPresent()) {
635         var ctor = opt.get();
636         return (in, memberOf, session, args) -> ctor.newInstance(in);
637      }
638      return null;
639   }
640
641   //-----------------------------------------------------------------------------------------------------------------
642   // Reflection: toX() instance methods
643   //-----------------------------------------------------------------------------------------------------------------
644
645   private <I,O> Conversion<I,O> findToXMethod(Class<I> inType, Class<O> outType) {
646      // Use getNameReadable() so that array types use "Array" suffix (e.g. "toStringArray" for String[]).
647      // Use equalsIgnoreCase to match Mutaters' behavior (e.g. "toByteArray" matches "tobyteArray" from byte[]).
648      var methodName = "to" + info(outType).getNameReadable();
649      var opt = info(inType).getPublicMethod(m ->
650         m.isNotStatic()
651         && m.isNotDeprecated()
652         && m.getParameterCount() == 0
653         && eqic(m.getNameSimple(), methodName)
654         && m.hasReturnTypeParent(outType)
655      );
656      if (opt.isPresent()) {
657         var method = opt.get();
658         return (in, memberOf, session, args) -> method.invoke(in);
659      }
660      return null;
661   }
662
663   //-----------------------------------------------------------------------------------------------------------------
664   // Helpers
665   //-----------------------------------------------------------------------------------------------------------------
666
667   private <T> T newInstanceOrDefault(Class<?> type, java.util.function.Supplier<T> defaultSupplier) {
668      var opt = info(type).getPublicConstructor(c -> c.getParameterCount() == 0);
669      if (opt.isPresent()) {
670         var ctor = opt.get();
671         try {
672            return ctor.newInstance();
673         } catch (@SuppressWarnings("unused") Exception e) { // HTT: constructor would have to throw to reach here
674            return defaultSupplier.get(); // HTT
675         }
676      }
677      return defaultSupplier.get();
678   }
679}