From f7923c5f5e395a5f055f787a5dc79e0608cf8a9a Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 10 Jan 2024 11:14:31 +0100 Subject: [PATCH] [jOOQ/jOOQ#16043] DATE column in Oracle is null when fetched through multiset This includes: - [jOOQ/jOOQ#16044] org.jooq.tools.Convert should delegate to org.jooq.impl.Convert, instead of duplicating implementations --- jOOQ/src/main/java/org/jooq/impl/Convert.java | 40 +- .../src/main/java/org/jooq/impl/Internal.java | 91 ++ .../src/main/java/org/jooq/tools/Convert.java | 1248 +---------------- 3 files changed, 138 insertions(+), 1241 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/Convert.java b/jOOQ/src/main/java/org/jooq/impl/Convert.java index 6c0eaba81e..bdd3fc4a1e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Convert.java +++ b/jOOQ/src/main/java/org/jooq/impl/Convert.java @@ -114,7 +114,6 @@ import org.jooq.exception.DataTypeException; import org.jooq.tools.Ints; import org.jooq.tools.JooqLogger; import org.jooq.tools.Longs; -import org.jooq.tools.StringUtils; import org.jooq.tools.jdbc.MockArray; import org.jooq.tools.jdbc.MockResultSet; import org.jooq.tools.json.ContainerFactory; @@ -1014,7 +1013,7 @@ final class Convert { // [#1501] Strings can be converted to java.sql.Date else if (fromClass == String.class && toClass == java.sql.Date.class) { try { - return (U) java.sql.Date.valueOf((String) from); + return (U) java.sql.Date.valueOf(patchIso8601Date((String) from)); } catch (IllegalArgumentException e) { return null; @@ -1041,14 +1040,15 @@ final class Convert { } } else if (fromClass == String.class && toClass == LocalDate.class) { + String s = patchIso8601Date((String) from); // Try "lenient" ISO date formats first try { - return (U) java.sql.Date.valueOf((String) from).toLocalDate(); + return (U) java.sql.Date.valueOf(s).toLocalDate(); } catch (IllegalArgumentException e1) { try { - return (U) LocalDate.parse((String) from); + return (U) LocalDate.parse(s); } catch (DateTimeParseException e2) { return null; @@ -1682,6 +1682,38 @@ final class Convert { return s; } + static final String patchIso8601Date(String s) { + + // [#11485] Trino produces a non-ISO 8601 "UTC" suffix, instead of "Z" + if (s.endsWith(" UTC")) + s = s.replace(" UTC", "Z"); + + int l = s.length(); + int d1 = s.indexOf('-'); + int d2 = s.indexOf('-', d1 + 1); + int ss = s.indexOf(' ', d2 + 1); + int st = s.indexOf('T', d2 + 1); + int sx = Math.max(ss, st); + + if (d1 == -1 || d2 == -1) + return s; + + // [#12547] Support year numbers with more or less than 4 digits + // [#13786] Be lenient with PostgreSQL style abbreviated time stamp literals + else if (sx == -1) + if (l - d2 < 3 || d2 - d1 < 3) + return padLead4(s, d1) + '-' + + padMid2(s, d1, d2) + '-' + + padMid2(s, d2); + else + return s; + + else + return padLead4(s, d1) + '-' + + padMid2(s, d1, d2) + '-' + + padMid2(s, d2, sx); + } + private static final String padLead2(String s, int i1) { return leftPad(s.substring(0, i1), 2, '0'); } diff --git a/jOOQ/src/main/java/org/jooq/impl/Internal.java b/jOOQ/src/main/java/org/jooq/impl/Internal.java index 6129e4dc4f..a4a41fdac3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Internal.java +++ b/jOOQ/src/main/java/org/jooq/impl/Internal.java @@ -48,6 +48,7 @@ import static org.jooq.tools.StringUtils.isBlank; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -1031,4 +1032,94 @@ public final class Internal { public static final int javaVersion() { return JAVA_VERSION.get(); } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final Object[] convert(Object[] values, Field[] fields) { + return Convert.convert(values, fields); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final Object[] convert(Object[] values, Class[] types) { + return Convert.convert(values, types); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final U[] convertArray(Object[] from, Converter converter) throws DataTypeException { + return Convert.convertArray(from, converter); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final Object[] convertArray(Object[] from, Class toClass) throws DataTypeException { + return Convert.convertArray(from, toClass); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final U[] convertCollection(Collection from, Class to) { + return Convert.convertCollection(from, to); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final U convert(Object from, Converter converter) throws DataTypeException { + return Convert.convert(from, converter); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final T convert(Object from, Class toClass) throws DataTypeException { + return Convert.convert(from, toClass); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final List convert(Collection collection, Class type) throws DataTypeException { + return Convert.convert(collection, type); + } + + /** + * [#11898] [#16044] This method just acts as a bridge to internal API from + * the deprecated-for-removal {@link org.jooq.tools.Convert} utility. Do not + * reuse these methods. + */ + @Deprecated(forRemoval = true) + public static final List convert(Collection collection, Converter converter) throws DataTypeException { + return Convert.convert(collection, converter); + } } diff --git a/jOOQ/src/main/java/org/jooq/tools/Convert.java b/jOOQ/src/main/java/org/jooq/tools/Convert.java index 8388395c19..de9a7c2286 100644 --- a/jOOQ/src/main/java/org/jooq/tools/Convert.java +++ b/jOOQ/src/main/java/org/jooq/tools/Convert.java @@ -37,34 +37,11 @@ */ package org.jooq.tools; -import static java.time.temporal.ChronoField.INSTANT_SECONDS; -import static java.time.temporal.ChronoField.MILLI_OF_DAY; -import static java.time.temporal.ChronoField.MILLI_OF_SECOND; -import static org.jooq.ContextConverter.scoped; -import static org.jooq.impl.Internal.arrayType; -import static org.jooq.impl.Internal.converterContext; -import static org.jooq.tools.StringUtils.leftPad; -import static org.jooq.tools.reflect.Reflect.accessible; -import static org.jooq.tools.reflect.Reflect.wrapper; -import static org.jooq.types.Unsigned.ubyte; -import static org.jooq.types.Unsigned.uint; -import static org.jooq.types.Unsigned.ulong; -import static org.jooq.types.Unsigned.ushort; - import java.io.File; -import java.io.StringReader; -import java.io.StringWriter; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.math.BigDecimal; -import java.math.BigInteger; import java.net.URI; import java.net.URL; import java.nio.ByteBuffer; import java.sql.Date; -import java.sql.ResultSet; -import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; @@ -73,55 +50,20 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.OffsetTime; -import java.time.Year; -import java.time.format.DateTimeParseException; import java.time.temporal.Temporal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.UUID; -import java.util.regex.Pattern; -// ... import org.jooq.Converter; -import org.jooq.ConverterContext; import org.jooq.ConverterProvider; import org.jooq.DataType; -import org.jooq.EnumType; import org.jooq.Field; -import org.jooq.JSON; -import org.jooq.JSONB; -import org.jooq.Param; -import org.jooq.QualifiedRecord; -import org.jooq.Record; -import org.jooq.Result; -import org.jooq.SQLDialect; -import org.jooq.XML; import org.jooq.exception.DataTypeException; -import org.jooq.impl.AbstractContextConverter; -import org.jooq.impl.IdentityConverter; -import org.jooq.tools.jdbc.MockArray; -import org.jooq.tools.jdbc.MockResultSet; -import org.jooq.tools.reflect.Reflect; -import org.jooq.types.DayToSecond; -import org.jooq.types.UByte; -import org.jooq.types.UInteger; -import org.jooq.types.ULong; -import org.jooq.types.UShort; -import org.jooq.types.YearToMonth; -import org.jooq.types.YearToSecond; -import org.jooq.util.postgres.PostgresUtils; -import org.jooq.util.xml.jaxb.InformationSchema; - -import jakarta.xml.bind.JAXB; +import org.jooq.impl.Internal; /** * Utility methods for type conversions @@ -143,8 +85,6 @@ import jakarta.xml.bind.JAXB; @Deprecated(forRemoval = true, since = "3.15") public final class Convert { - private static final JooqLogger log = JooqLogger.getLogger(Convert.class); - /** * All string values that can be transformed into a boolean true value. */ @@ -155,31 +95,6 @@ public final class Convert { */ public static final Set FALSE_VALUES; - /** - * A UUID pattern for UUIDs with or without hyphens - */ - private static final Pattern UUID_PATTERN = Pattern.compile("(\\p{XDigit}{8})-?(\\p{XDigit}{4})-?(\\p{XDigit}{4})-?(\\p{XDigit}{4})-?(\\p{XDigit}{12})"); - - /** - * The Jackson ObjectMapper or Gson instance, if available. - */ - private static final Object JSON_MAPPER; - - /** - * The Jackson ObjectMapper::readValue or Gson::fromJson method, if available. - */ - private static final Method JSON_READ_METHOD; - - /** - * The Jackson ObjectMapper::writeValueToString or Gson::toJson method, if available. - */ - private static final Method JSON_WRITE_METHOD; - - /** - * Whether a JAXB implementation is available. - */ - private static final boolean JAXB_AVAILABLE; - static { Set trueValues = new HashSet<>(); Set falseValues = new HashSet<>(); @@ -216,53 +131,6 @@ public final class Convert { TRUE_VALUES = Collections.unmodifiableSet(trueValues); FALSE_VALUES = Collections.unmodifiableSet(falseValues); - - Object jsonMapper = null; - Method jsonReadMethod = null; - Method jsonWriteMethod = null; - boolean jaxbAvailable = false; - - try { - Class klass = Class.forName("com.fasterxml.jackson.databind.ObjectMapper"); - - jsonMapper = klass.getDeclaredConstructor().newInstance(); - jsonReadMethod = klass.getMethod("readValue", String.class, Class.class); - jsonWriteMethod = klass.getMethod("writeValueAsString", Object.class); - log.debug("Jackson is available"); - } - catch (Exception e1) { - log.debug("Jackson not available", e1.getMessage()); - - try { - Class klass = Class.forName("com.google.gson.Gson"); - - jsonMapper = klass.getDeclaredConstructor().newInstance(); - jsonReadMethod = klass.getMethod("fromJson", String.class, Class.class); - jsonWriteMethod = klass.getMethod("toJson", Object.class); - log.debug("Gson is available"); - } - catch (Exception e2) { - log.debug("Gson not available", e2.getMessage()); - } - } - - JSON_MAPPER = jsonMapper; - JSON_READ_METHOD = jsonReadMethod; - JSON_WRITE_METHOD = jsonWriteMethod; - - try { - JAXB.marshal(new InformationSchema(), new StringWriter()); - jaxbAvailable = true; - log.debug("JAXB is available"); - } - - // [#10145] Depending on whether jOOQ is modularised or not, this can also - // be a NoClassDefFoundError. - catch (Throwable t) { - log.debug("JAXB not available", t.getMessage()); - } - - JAXB_AVAILABLE = jaxbAvailable; } /** @@ -271,22 +139,7 @@ public final class Convert { * This converts values[i] to fields[i].getType() */ public static final Object[] convert(Object[] values, Field[] fields) { - if (values == null) - return null; - - // [#1005] Convert values from the VALUES clause to appropriate - // values as specified by the INTO clause's column list. - Object[] result = new Object[values.length]; - - // TODO [#1008] Should fields be cast? Check this with - // appropriate integration tests - for (int i = 0; i < values.length; i++) - if (values[i] instanceof Field) - result[i] = values[i]; - else - result[i] = convert(values[i], fields[i].getType()); - - return result; + return Internal.convert(values, fields); } /** @@ -295,23 +148,9 @@ public final class Convert { * This converts values[i] to types[i] */ public static final Object[] convert(Object[] values, Class[] types) { - if (values == null) - return null; - - // [#1005] Convert values from the VALUES clause to appropriate - // values as specified by the INTO clause's column list. - Object[] result = new Object[values.length]; - - // TODO [#1008] Should fields be cast? Check this with - // appropriate integration tests - for (int i = 0; i < values.length; i++) - if (values[i] instanceof Field) - result[i] = values[i]; - else - result[i] = convert(values[i], types[i]); - - return result; + return Internal.convert(values, types); } + /** * Convert an array into another one using a converter *

@@ -324,18 +163,8 @@ public final class Convert { * @return A converted array * @throws DataTypeException - When the conversion is not possible */ - @SuppressWarnings("unchecked") public static final U[] convertArray(Object[] from, Converter converter) throws DataTypeException { - if (from == null) - return null; - - Object[] arrayOfT = convertArray(from, converter.fromType()); - Object[] arrayOfU = (Object[]) Array.newInstance(converter.toType(), from.length); - - for (int i = 0; i < arrayOfT.length; i++) - arrayOfU[i] = convert(arrayOfT[i], converter); - - return (U[]) arrayOfU; + return Internal.convertArray(from, converter); } /** @@ -354,34 +183,12 @@ public final class Convert { * @return A converted array * @throws DataTypeException - When the conversion is not possible */ - @SuppressWarnings("unchecked") public static final Object[] convertArray(Object[] from, Class toClass) throws DataTypeException { - if (from == null) - return null; - else if (!toClass.isArray()) - return convertArray(from, arrayType(toClass)); - else if (toClass == from.getClass()) - return from; - else { - final Class toComponentType = toClass.getComponentType(); - - if (from.length == 0) - return Arrays.copyOf(from, from.length, (Class) toClass); - else if (from[0] != null && from[0].getClass() == toComponentType) - return Arrays.copyOf(from, from.length, (Class) toClass); - else { - final Object[] result = (Object[]) Array.newInstance(toComponentType, from.length); - - for (int i = 0; i < from.length; i++) - result[i] = convert(from[i], toComponentType); - - return result; - } - } + return Internal.convertArray(from, toClass); } public static final U[] convertCollection(Collection from, Class to){ - return new ConvertAll(to).from(from, converterContext()); + return Internal.convertCollection(from, to); } /** @@ -392,29 +199,8 @@ public final class Convert { * @return The target type object * @throws DataTypeException - When the conversion is not possible */ - @SuppressWarnings("unchecked") public static final U convert(Object from, Converter converter) throws DataTypeException { - - // [#5865] [#6799] [#11099] This leads to significant performance improvements especially when - // used from MockResultSet, which is likely to host IdentityConverters - if (converter instanceof IdentityConverter) - return (U) from; - else - return convert0(from, converter); - } - - /** - * Conversion type-safety - */ - @SuppressWarnings("unchecked") - private static final U convert0(Object from, Converter converter) throws DataTypeException { - Class fromType = converter.fromType(); - - if (fromType == Object.class) - return scoped(converter).from((T) from, converterContext()); - - ConvertAll convertAll = new ConvertAll<>(fromType); - return scoped(converter).from(convertAll.from(from, converterContext()), converterContext()); + return Internal.convert(from, converter); } /** @@ -481,12 +267,8 @@ public final class Convert { * @return The converted object * @throws DataTypeException - When the conversion is not possible */ - @SuppressWarnings("unchecked") public static final T convert(Object from, Class toClass) throws DataTypeException { - if (from != null && from.getClass() == toClass) - return (T) from; - else - return convert0(from, new ConvertAll(toClass)); + return Internal.convert(from, toClass); } /** @@ -500,7 +282,7 @@ public final class Convert { * @see #convert(Object, Class) */ public static final List convert(Collection collection, Class type) throws DataTypeException { - return convert(collection, new ConvertAll<>(type)); + return Internal.convert(collection, type); } /** @@ -514,1014 +296,6 @@ public final class Convert { * @see #convert(Object, Converter) */ public static final List convert(Collection collection, Converter converter) throws DataTypeException { - return convert0(collection, converter); - } - - /** - * Type safe conversion - */ - private static final List convert0(Collection collection, Converter converter) throws DataTypeException { - ConvertAll all = new ConvertAll<>(converter.fromType()); - List result = new ArrayList<>(collection.size()); - - for (Object o : collection) - result.add(convert(all.from(o, converterContext()), converter)); - - return result; - } - - /** - * No instances - */ - private Convert() {} - - /** - * The converter to convert them all. - */ - static final class ConvertAll extends AbstractContextConverter { - - private final Class toClass; - - @SuppressWarnings("unchecked") - ConvertAll(Class toClass) { - super(Object.class, (Class) toClass); - - this.toClass = toClass; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public U from(Object from, ConverterContext scope) { - if (from == null) { - - // [#936] If types are converted to primitives, the result must not - // be null. Return the default value instead - if (toClass.isPrimitive()) { - - // Characters default to the "zero" character - if (toClass == char.class) - return (U) Character.valueOf((char) 0); - - // All others can be converted from (int) 0 - else - return convert(0, toClass); - } - - else if (toClass == Optional.class) - return (U) Optional.empty(); - else - return null; - } - else { - final Class fromClass = from.getClass(); - final Class wrapperTo; - final Class wrapperFrom; - - // No conversion - if (toClass == fromClass) - return (U) from; - - // [#2535] Simple up-casting can be done early - // [#1155] ... up-casting includes (toClass == Object.class) - else if (toClass.isAssignableFrom(fromClass)) - return (U) from; - - // [#6790] No conversion for primitive / wrapper conversions - else if ((wrapperTo = wrapper(toClass)) == (wrapperFrom = wrapper(fromClass))) - return (U) from; - - // Regular checks - else if (fromClass == byte[].class) { - - // [#5824] UUID's most significant bits in byte[] are first - if (toClass == UUID.class) { - ByteBuffer b = ByteBuffer.wrap((byte[]) from); - long mostSigBits = b.getLong(); - long leastSigBits = b.getLong(); - return (U) new UUID(mostSigBits, leastSigBits); - } - - // [#11700] [#11772] R2DBC uses ByteBuffer instead of byte[] - else if (toClass == ByteBuffer.class) - return (U) ByteBuffer.wrap((byte[]) from); - - // [#5569] Binary data is expected to be in JVM's default encoding - else - return convert(new String((byte[]) from), toClass); - } - else if (fromClass.isArray()) { - Object[] fromArray = (Object[]) from; - - // [#3062] [#5796] Default collections if no specific collection type was requested - if (Collection.class.isAssignableFrom(toClass) && - toClass.isAssignableFrom(ArrayList.class)) - return (U) new ArrayList<>(Arrays.asList(fromArray)); - else if (Collection.class.isAssignableFrom(toClass) && - toClass.isAssignableFrom(LinkedHashSet.class)) - return (U) new LinkedHashSet<>(Arrays.asList(fromArray)); - - // [#3443] Conversion from Object[] to JDBC Array - else if (toClass == java.sql.Array.class) - return (U) new MockArray(null, fromArray, fromClass); - else - return (U) convertArray(fromArray, toClass); - } - - // [#11560] Results wrapped in ResultSet - else if (Result.class.isAssignableFrom(fromClass) && toClass == ResultSet.class) { - return (U) new MockResultSet((Result) from); - } - - // [#3062] Default collections if no specific collection type was requested - else if (Collection.class.isAssignableFrom(fromClass) - - && (toClass == java.sql.Array.class || toClass.isArray()) - ) { - Object[] fromArray = ((Collection) from).toArray(); - - // [#3443] [#10704] Conversion from Object[] to JDBC Array - if (toClass == java.sql.Array.class) - return (U) new MockArray(null, fromArray, fromClass); - else - return (U) convertArray(fromArray, toClass); - } - else if (toClass == Optional.class) - return (U) Optional.of(from); - - // All types can be converted into String - else if (toClass == String.class) { - if (from instanceof EnumType) - return (U) ((EnumType) from).getLiteral(); - - return (U) from.toString(); - } - - // [#5569] It should be possible, at least, to convert an empty string to an empty (var)binary. - else if (toClass == byte[].class) { - - // [#5824] UUID's most significant bits in byte[] are first - if (from instanceof UUID) { - ByteBuffer b = ByteBuffer.wrap(new byte[16]); - b.putLong(((UUID) from).getMostSignificantBits()); - b.putLong(((UUID) from).getLeastSignificantBits()); - return (U) b.array(); - } - else if (from instanceof ByteBuffer) - return (U) ((ByteBuffer) from).array(); - else - return (U) from.toString().getBytes(); - } - - // Various number types are converted between each other via String - else if (wrapperTo == Byte.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Byte.valueOf(((Number) from).byteValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Byte.valueOf((byte) 1) : Byte.valueOf((byte) 0)); - - try { - String fromString = from.toString().trim(); - Integer asInt = Ints.tryParse(fromString); - return (U) Byte.valueOf(asInt != null ? asInt.byteValue() : new BigDecimal(fromString).byteValue()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - else if (wrapperTo == Short.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Short.valueOf(((Number) from).shortValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Short.valueOf((short) 1) : Short.valueOf((short) 0)); - - try { - String fromString = from.toString().trim(); - Integer asInt = Ints.tryParse(fromString); - return (U) Short.valueOf(asInt != null ? asInt.shortValue() : new BigDecimal(fromString).shortValue()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - else if (wrapperTo == Integer.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Integer.valueOf(((Number) from).intValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Integer.valueOf(1) : Integer.valueOf(0)); - - try { - String fromString = from.toString().trim(); - Integer asInt = Ints.tryParse(fromString); - return (U) Integer.valueOf(asInt != null ? asInt.intValue() : new BigDecimal(fromString).intValue()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - else if (wrapperTo == Long.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Long.valueOf(((Number) from).longValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Long.valueOf(1L) : Long.valueOf(0L)); - - if (wrapperFrom == Year.class) - return (U) (Long) (long) ((Year) from).getValue(); - - if (java.util.Date.class.isAssignableFrom(fromClass)) - return (U) Long.valueOf(((java.util.Date) from).getTime()); - - if (Temporal.class.isAssignableFrom(fromClass)) - return (U) Long.valueOf(millis((Temporal) from)); - - try { - String fromString = from.toString().trim(); - Long asLong = Longs.tryParse(fromString); - return (U) Long.valueOf(asLong != null ? asLong.longValue() : new BigDecimal(fromString).longValue()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - - // ... this also includes unsigned number types - else if (toClass == UByte.class) { - try { - if (Number.class.isAssignableFrom(fromClass)) - return (U) ubyte(((Number) from).shortValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? ubyte(1) : ubyte(0)); - - String fromString = from.toString().trim(); - Integer asInt = Ints.tryParse(fromString); - return (U) ubyte(asInt != null ? asInt.shortValue() : new BigDecimal(fromString).shortValue()); - } - catch (NumberFormatException e) { - return null; - } - } - else if (toClass == UShort.class) { - try { - if (Number.class.isAssignableFrom(fromClass)) - return (U) ushort(((Number) from).intValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? ushort(1) : ushort(0)); - - String fromString = from.toString().trim(); - Integer asInt = Ints.tryParse(fromString); - return (U) ushort(asInt != null ? asInt.intValue() : new BigDecimal(fromString).intValue()); - } - catch (NumberFormatException e) { - return null; - } - } - else if (toClass == UInteger.class) { - try { - if (Number.class.isAssignableFrom(fromClass)) - return (U) uint(((Number) from).longValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? uint(1) : uint(0)); - - String fromString = from.toString().trim(); - Long asLong = Longs.tryParse(fromString); - return (U) uint(asLong != null ? asLong.longValue() : new BigDecimal(fromString).longValue()); - } - catch (NumberFormatException e) { - return null; - } - } - else if (toClass == ULong.class) { - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? ulong(1) : ulong(0)); - - if (java.util.Date.class.isAssignableFrom(fromClass)) - return (U) ulong(((java.util.Date) from).getTime()); - - if (Temporal.class.isAssignableFrom(fromClass)) - return (U) ulong(millis((Temporal) from)); - - try { - String fromString = from.toString().trim(); - // tryParse() will return null in case of overflow - Long asLong = Longs.tryParse(fromString); - return asLong != null ? (U) ulong(asLong.longValue()) : (U) ulong(new BigDecimal(fromString).toBigInteger()); - } - catch (NumberFormatException e) { - return null; - } - } - - // ... and floating point / fixed point types - else if (wrapperTo == Float.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Float.valueOf(((Number) from).floatValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Float.valueOf(1.0f) : Float.valueOf(0.0f)); - - try { - return (U) Float.valueOf(from.toString().trim()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - else if (wrapperTo == Double.class) { - if (Number.class.isAssignableFrom(fromClass)) - return (U) Double.valueOf(((Number) from).doubleValue()); - - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Double.valueOf(1.0) : Double.valueOf(0.0)); - - try { - return (U) Double.valueOf(from.toString().trim()); - } - catch (NumberFormatException e) { - return Reflect.initValue(toClass); - } - } - else if (toClass == BigDecimal.class) { - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? BigDecimal.ONE : BigDecimal.ZERO); - - try { - return (U) new BigDecimal(from.toString().trim()); - } - catch (NumberFormatException e) { - return null; - } - } - else if (toClass == BigInteger.class) { - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? BigInteger.ONE : BigInteger.ZERO); - - try { - return (U) new BigDecimal(from.toString().trim()).toBigInteger(); - } - catch (NumberFormatException e) { - return null; - } - } - else if (toClass == Year.class) { - if (Number.class.isAssignableFrom(wrapperFrom)) - return (U) Year.of((((Number) from).intValue())); - - try { - return (U) Year.parse(from.toString().trim()); - } - catch (DateTimeParseException e) { - return null; - } - } - else if (wrapperTo == Boolean.class) { - String s = from.toString().toLowerCase().trim(); - - if (TRUE_VALUES.contains(s)) - return (U) Boolean.TRUE; - else if (FALSE_VALUES.contains(s)) - return (U) Boolean.FALSE; - else - return (U) (toClass == Boolean.class ? null : false); - } - else if (wrapperTo == Character.class) { - if (wrapperFrom == Boolean.class) - return (U) (((Boolean) from) ? Character.valueOf('1') : Character.valueOf('0')); - - if (from.toString().length() < 1) - return Reflect.initValue(toClass); - - return (U) Character.valueOf(from.toString().charAt(0)); - } - - // URI types can be converted from strings - else if (fromClass == String.class && toClass == URL.class) { - try { - return (U) new URI(from.toString()).toURL(); - } - catch (Exception e) { - return null; - } - } - // Date types can be converted among each other - else if (java.util.Date.class.isAssignableFrom(fromClass)) { - - // [#12225] Avoid losing precision if possible - if (Timestamp.class == fromClass) - if (LocalDateTime.class == toClass) - return (U) ((Timestamp) from).toLocalDateTime(); - else - return toDate(((Timestamp) from).getTime(), ((Timestamp) from).getNanos(), toClass); - else if (Date.class == fromClass && LocalDate.class == toClass) - return (U) ((Date) from).toLocalDate(); - else if (Time.class == fromClass && LocalTime.class == toClass) - return (U) ((Time) from).toLocalTime(); - else - return toDate(((java.util.Date) from).getTime(), toClass); - } - else if (Temporal.class.isAssignableFrom(fromClass)) { - - // [#12225] Avoid losing precision if possible - if (LocalDateTime.class == fromClass && Timestamp.class == toClass) - return (U) Timestamp.valueOf((LocalDateTime) from); - else if (LocalDateTime.class == fromClass && Temporal.class.isAssignableFrom(toClass)) - return toDate(((LocalDateTime) from).toInstant(OffsetTime.now().getOffset()).toEpochMilli(), ((LocalDateTime) from).getNano(), toClass); - else if (LocalDate.class == fromClass && Date.class == toClass) - return (U) Date.valueOf((LocalDate) from); - else if (LocalTime.class == fromClass && Time.class == toClass) - return (U) Time.valueOf((LocalTime) from); - else if (OffsetDateTime.class == fromClass && (Timestamp.class == toClass || Temporal.class.isAssignableFrom(toClass))) - return toDate(((OffsetDateTime) from).toInstant().toEpochMilli(), ((OffsetDateTime) from).getNano(), toClass); - else if (Instant.class == fromClass && (Timestamp.class == toClass || Temporal.class.isAssignableFrom(toClass))) - return toDate(((Instant) from).toEpochMilli(), ((Instant) from).getNano(), toClass); - else - return toDate(convert(from, Long.class), toClass); - } - - // Long may also be converted into a date type - else if (wrapperFrom == Long.class && java.util.Date.class.isAssignableFrom(toClass)) { - return toDate((Long) from, toClass); - } - else if (wrapperFrom == Long.class && Temporal.class.isAssignableFrom(toClass)) { - return toDate((Long) from, toClass); - } - - // [#1501] Strings can be converted to java.sql.Date - else if (fromClass == String.class && toClass == java.sql.Date.class) { - try { - return (U) java.sql.Date.valueOf((String) from); - } - catch (IllegalArgumentException e) { - return null; - } - } - - // [#1501] Strings can be converted to java.sql.Date - else if (fromClass == String.class && toClass == java.sql.Time.class) { - try { - return (U) java.sql.Time.valueOf(patchFractionalSeconds(patchIso8601Time((String) from))); - } - catch (IllegalArgumentException e) { - return null; - } - } - - // [#1501] Strings can be converted to java.sql.Date - else if (fromClass == String.class && toClass == java.sql.Timestamp.class) { - try { - return (U) java.sql.Timestamp.valueOf(patchIso8601Timestamp((String) from, false)); - } - catch (IllegalArgumentException e) { - return null; - } - } - else if (fromClass == String.class && toClass == LocalDate.class) { - - // Try "lenient" ISO date formats first - try { - return (U) java.sql.Date.valueOf((String) from).toLocalDate(); - } - catch (IllegalArgumentException e1) { - try { - return (U) LocalDate.parse((String) from); - } - catch (DateTimeParseException e2) { - return null; - } - } - } - - else if (fromClass == String.class && toClass == LocalTime.class) { - try { - return (U) LocalTime.parse(patchIso8601Time((String) from)); - } - catch (DateTimeParseException e2) { - return null; - } - } - - else if (fromClass == String.class && toClass == OffsetTime.class) { - - // Try "local" ISO date formats first - try { - return (U) java.sql.Time.valueOf((String) from).toLocalTime().atOffset(OffsetTime.now().getOffset()); - } - catch (IllegalArgumentException e1) { - try { - return (U) OffsetTime.parse((String) from); - } - catch (DateTimeParseException e2) { - return null; - } - } - } - - else if (fromClass == String.class && toClass == LocalDateTime.class) { - try { - return (U) LocalDateTime.parse(patchIso8601Timestamp((String) from, true)); - } - catch (DateTimeParseException e2) { - return null; - } - } - - else if (fromClass == String.class && toClass == OffsetDateTime.class) { - - // Try "local" ISO date formats first - try { - return (U) java.sql.Timestamp.valueOf((String) from).toLocalDateTime().atOffset(OffsetDateTime.now().getOffset()); - } - catch (IllegalArgumentException e1) { - try { - return (U) OffsetDateTime.parse((String) from); - } - catch (DateTimeParseException e2) { - return null; - } - } - } - - else if (fromClass == String.class && toClass == Instant.class) { - - // Try "local" ISO date formats first - try { - return (U) java.sql.Timestamp.valueOf((String) from).toLocalDateTime().atOffset(OffsetDateTime.now().getOffset()).toInstant(); - } - catch (IllegalArgumentException e1) { - try { - return (U) Instant.parse((String) from); - } - catch (DateTimeParseException e2) { - return null; - } - } - } - - // [#14437] [#14713] Interval conversions - else if (fromClass == String.class && toClass == YearToMonth.class) { - - // Try our own standard SQL implementation first - YearToMonth r = YearToMonth.valueOf((String) from); - if (r != null) - return (U) r; - - // If that failed, try the H2 specific format - if (((String) from).startsWith("INTERVAL")) { - try { - r = ((Param) scope.dsl().parser().parseField((String) from)).getValue(); - return (U) r; - } - catch (Exception ignore) {} - } - - // If that failed, try the PostgreSQL specific formats - try { - return (U) PostgresUtils.toYearToMonth(from); - } - catch (Exception e) { - return null; - } - } - else if (fromClass == String.class && toClass == DayToSecond.class) { - - // Try our own standard SQL implementation first - DayToSecond r = DayToSecond.valueOf((String) from); - if (r != null) - return (U) r; - - // If that failed, try the H2 specific format - if (((String) from).startsWith("INTERVAL")) { - try { - r = ((Param) scope.dsl().parser().parseField((String) from)).getValue(); - return (U) r; - } - catch (Exception ignore) {} - } - - // If that failed, try the PostgreSQL specific formats - try { - return (U) PostgresUtils.toDayToSecond(from); - } - catch (Exception e) { - return null; - } - } - else if (fromClass == String.class && toClass == YearToSecond.class) { - - // Try our own standard SQL implementation first - YearToSecond r = YearToSecond.valueOf((String) from); - if (r != null) - return (U) r; - - // If that failed, try the PostgreSQL specific formats - try { - return (U) PostgresUtils.toYearToSecond(from); - } - catch (Exception e) { - return null; - } - } - - // [#1448] [#6255] [#5720] To Enum conversion - else if (java.lang.Enum.class.isAssignableFrom(toClass) && (fromClass == String.class || from instanceof Enum || from instanceof EnumType)) { - try { - String fromString = - (fromClass == String.class) ? (String) from - : (from instanceof EnumType) ? ((EnumType) from).getLiteral() - : ((Enum) from).name(); - - if (fromString == null) - return null; - - if (EnumType.class.isAssignableFrom(toClass)) { - for (Object value : toClass.getEnumConstants()) - if (fromString.equals(((EnumType) value).getLiteral())) - return (U) value; - - return null; - } - else { - return (U) java.lang.Enum.valueOf((Class) toClass, fromString); - } - - } - catch (IllegalArgumentException e) { - return null; - } - } - - // [#1624] UUID data types can be read from Strings - else if (fromClass == String.class && toClass == UUID.class) { - try { - return (U) parseUUID((String) from); - } - catch (IllegalArgumentException e) { - return null; - } - } - - // [#8943] JSON data types can be read from Strings - else if (fromClass == String.class && toClass == JSON.class) { - return (U) JSON.valueOf((String) from); - } - - // [#8943] JSONB data types can be read from Strings - else if (fromClass == String.class && toClass == JSONB.class) { - return (U) JSONB.valueOf((String) from); - } - - // [#10072] Out of the box Jackson JSON mapping support - else if (fromClass == JSON.class && JSON_MAPPER != null) { - try { - return (U) JSON_READ_METHOD.invoke(JSON_MAPPER, ((JSON) from).data(), toClass); - } - catch (Exception e) { - throw new DataTypeException("Error while mapping JSON to POJO using Jackson", e); - } - } - - // [#10072] Out of the box Jackson JSON mapping support - else if (fromClass == JSONB.class && JSON_MAPPER != null) { - try { - return (U) JSON_READ_METHOD.invoke(JSON_MAPPER, ((JSONB) from).data(), toClass); - } - catch (Exception e) { - throw new DataTypeException("Error while mapping JSON to POJO using Jackson", e); - } - } - - // [#11213] Workaround for a problem when Jackson or Gson do not know - // the generic List type because toClass has its generics erased - else if (Map.class.isAssignableFrom(fromClass) && JSON_MAPPER != null) { - try { - return (U) JSON_READ_METHOD.invoke(JSON_MAPPER, JSON_WRITE_METHOD.invoke(JSON_MAPPER, from), toClass); - } - catch (Exception e) { - throw new DataTypeException("Error while mapping JSON to POJO using Jackson", e); - } - } - - // [#10072] Out of the box JAXB mapping support - else if (fromClass == XML.class && JAXB_AVAILABLE) { - try { - return JAXB.unmarshal(new StringReader(((XML) from).data()), toClass); - } - catch (Exception e) { - throw new DataTypeException("Error while mapping XML to POJO using JAXB", e); - } - } - - // [#3023] Record types can be converted using the supplied Configuration's - // RecordMapperProvider - else if (Record.class.isAssignableFrom(fromClass)) { - Record record = (Record) from; - return record.into(toClass); - } - - else if (Struct.class.isAssignableFrom(fromClass)) { - Struct struct = (Struct) from; - - if (QualifiedRecord.class.isAssignableFrom(toClass)) { - try { - QualifiedRecord record = ((QualifiedRecord) toClass.getDeclaredConstructor().newInstance()); - record.from(struct.getAttributes()); - return (U) record; - } - catch (Exception e) { - throw new DataTypeException("Cannot convert from " + fromClass + " to " + toClass, e); - } - } - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if (Collection.class.isAssignableFrom(fromClass) && Collection.class.isAssignableFrom(toClass)) { - return copyCollection(fromClass, (Collection) from); - } - - // TODO [#2520] When RecordUnmappers are supported, they should also be considered here - - // [#10229] Try public, single argument, applicable constructors first - for (Constructor constructor : toClass.getConstructors()) { - Class[] types = constructor.getParameterTypes(); - - // [#11183] Prevent StackOverflowError when recursing into UDT POJOs - if (types.length == 1 && types[0] != toClass) { - try { - return (U) constructor.newInstance(convert(from, types[0])); - } - - // Throw exception further down instead - catch (Exception ignore) {} - } - } - - // [#10229] Try private, single argument, applicable constructors - for (Constructor constructor : toClass.getDeclaredConstructors()) { - Class[] types = constructor.getParameterTypes(); - - // [#11183] Prevent StackOverflowError when recursing into UDT POJOs - if (types.length == 1 && types[0] != toClass) { - try { - return (U) accessible(constructor).newInstance(convert(from, types[0])); - } - - // Throw exception further down instead - catch (Exception ignore) {} - } - } - } - - throw fail(from, toClass); - } - - @SuppressWarnings("unchecked") - private final U copyCollection(Class fromClass, Collection collection) { - try { - Collection c; - - if (!toClass.isInterface()) - c = (Collection) toClass.newInstance(); - else if (Set.class.isAssignableFrom(toClass)) - c = new LinkedHashSet<>(); - else - c = new ArrayList<>(); - - c.addAll(collection); - return (U) c; - } - catch (Exception e) { - throw new DataTypeException("Cannot convert from " + fromClass + " to " + toClass, e); - } - } - - static final Pattern P_FRACTIONAL_SECONDS = Pattern.compile("^(\\d+:\\d+:\\d+)\\.\\d+$"); - - static final String patchFractionalSeconds(String string) { - - // [#15478] java.sql.Time doesn't support them - return string.length() > 8 - ? P_FRACTIONAL_SECONDS.matcher(string).replaceFirst("$1") - : string; - } - - static final String patchIso8601Time(String s) { - int l = s.length(); - int c1 = s.indexOf(':'); - - if (c1 >= 0) { - int c2 = s.indexOf(':', c1 + 1); - - if (c2 == -1) - return padLead2(s, c1) + ':' + padMid2(s, c1) + ":00"; - else if (l < 8 || c2 != l - 3 || c1 != l - 6) - return padLead2(s, c1) + ':' + padMid2(s, c1, c2) + ':' + padMid2(s, c2); - } - - // [#12158] Support Db2's 15.30.45 format - else if ((c1 = s.indexOf('.')) >= 0) { - return patchIso8601Time(s.replace('.', ':')); - } - - return s; - } - - static final String patchIso8601Timestamp(String s, boolean t) { - - // [#11485] Trino produces a non-ISO 8601 "UTC" suffix, instead of "Z" - if (s.endsWith(" UTC")) - s = s.replace(" UTC", "Z"); - - int l = s.length(); - int d1 = s.indexOf('-'); - int d2 = s.indexOf('-', d1 + 1); - int ss = s.indexOf(' ', d2 + 1); - int st = s.indexOf('T', d2 + 1); - int sx = Math.max(ss, st); - int c1 = s.indexOf(':', sx + 1); - int c2 = s.indexOf(':', c1 + 1); - - if (d1 == -1 || d2 == -1) - return s; - - // [#12547] Support year numbers with more or less than 4 digits - // [#13786] Be lenient with PostgreSQL style abbreviated time stamp literals - else if (sx == -1) - return padLead4(s, d1) + '-' - + padMid2(s, d1, d2) + '-' - + padMid2(s, d2) - + (t ? "T00:00:00" : " 00:00:00"); - else if (c2 == -1) - return padLead4(s, d1) + '-' - + padMid2(s, d1, d2) + '-' - + padMid2(s, d2, sx) - + (t ? 'T' : ' ') - + padMid2(s, sx, c1) + ':' - + padMid2(s, c1) + ":00"; - - // [#13786] TODO: This doesn't pad seconds in the presence of fractional seconds or time zones - else if (t == (st == -1) || l - c2 < 3 || c2 - c1 < 3 || c1 - sx < 3 || sx - d2 < 3 || d2 - d1 < 3) - return padLead4(s, d1) + '-' - + padMid2(s, d1, d2) + '-' - + padMid2(s, d2, sx) - + (t ? 'T' : ' ') - + padMid2(s, sx, c1) + ':' - + padMid2(s, c1, c2) + ':' - + padMid2(s, c2); - else - return s; - } - - private static final String padLead2(String s, int i1) { - return leftPad(s.substring(0, i1), 2, '0'); - } - - private static final String padLead4(String s, int i1) { - return leftPad(s.substring(0, i1), 4, '0'); - } - - private static final String padMid2(String s, int i1) { - return leftPad(s.substring(i1 + 1), 2, '0'); - } - - private static final String padMid2(String s, int i1, int i2) { - return leftPad(s.substring(i1 + 1, i2), 2, '0'); - } - - @Override - public Object to(U to, ConverterContext scope) { - return to; - } - - /** - * Convert a long timestamp (millis) to any date type. - */ - private static X toDate(long time, Class toClass) { - return toDate(time, 0, toClass); - } - - /** - * Convert a long timestamp (millis) with nanos adjustment to any date - * type. - */ - @SuppressWarnings("unchecked") - private static X toDate(long time, int nanos, Class toClass) { - if (toClass == Date.class) - return (X) new Date(time); - else if (toClass == Time.class) - return (X) new Time(time); - else if (toClass == Timestamp.class) - return (X) toTimestamp(time, nanos); - else if (toClass == java.util.Date.class) - return (X) new java.util.Date(time); - else if (toClass == Calendar.class) { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(time); - return (X) calendar; - } - else if (toClass == LocalDate.class) - return (X) new Date(time).toLocalDate(); - else if (toClass == LocalTime.class) - return (X) new Time(time).toLocalTime(); - else if (toClass == OffsetTime.class) - return (X) new Time(time).toLocalTime().atOffset(OffsetTime.now().getOffset()); - else if (toClass == LocalDateTime.class) - return (X) toTimestamp(time, nanos).toLocalDateTime(); - else if (toClass == OffsetDateTime.class) - return (X) toTimestamp(time, nanos).toLocalDateTime().atOffset(OffsetDateTime.now().getOffset()); - else if (toClass == Instant.class) - if (nanos == 0L) - return (X) Instant.ofEpochMilli(time); - else - return (X) Instant.ofEpochSecond(time / 1000L, nanos); - - throw fail(time, toClass); - } - - private static Timestamp toTimestamp(long time, int nanos) { - if (nanos == 0L) - return new Timestamp(time); - - Timestamp ts = new Timestamp(time / 1000L * 1000L); - ts.setNanos(nanos); - return ts; - } - - - private static final long millis(Temporal temporal) { - - // java.sql.* temporal types: - if (temporal instanceof LocalDate) - return Date.valueOf((LocalDate) temporal).getTime(); - else if (temporal instanceof LocalTime) - return Time.valueOf((LocalTime) temporal).getTime(); - else if (temporal instanceof LocalDateTime) - return Timestamp.valueOf((LocalDateTime) temporal).getTime(); - - // OffsetDateTime - else if (temporal.isSupported(INSTANT_SECONDS)) - return 1000 * temporal.getLong(INSTANT_SECONDS) + temporal.getLong(MILLI_OF_SECOND); - - // OffsetTime - else if (temporal.isSupported(MILLI_OF_DAY)) - return temporal.getLong(MILLI_OF_DAY); - - throw fail(temporal, Long.class); - } - - /** - * Some databases do not implement the standard very well. Specifically, - * {@link SQLDialect#SYBASE} seems to omit hyphens - */ - private static final UUID parseUUID(String string) { - if (string == null) - return null; - else if (string.contains("-")) - return UUID.fromString(string); - else - return UUID.fromString(UUID_PATTERN.matcher(string).replaceAll("$1-$2-$3-$4-$5")); - } - - private static DataTypeException fail(Object from, Class toClass) { - String message = "Cannot convert from " + from + " (" + from.getClass() + ") to " + toClass; - - // [#10072] [#11023] Some mappings may not have worked because of badly set up classpaths - if ((from instanceof JSON || from instanceof JSONB) && JSON_MAPPER == null) - return new DataTypeException(message + ". Check your classpath to see if Jackson or Gson is available to jOOQ."); - else if (from instanceof XML && !JAXB_AVAILABLE) - return new DataTypeException(message + ". Check your classpath to see if JAXB is available to jOOQ."); - else - return new DataTypeException(message); - } + return Internal.convert(collection, converter); } }