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}