/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Other licenses: * ----------------------------------------------------------------------------- * Commercial licenses for this work are available. These replace the above * ASL 2.0 and offer limited warranties, support, maintenance, and commercial * database integrations. * * For more information, please visit: http://www.jooq.org/licenses * * * * * * * * * * * * * */ package org.jooq.impl; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; // ... // ... import static org.jooq.SQLDialect.CUBRID; // ... import static org.jooq.SQLDialect.DERBY; import static org.jooq.SQLDialect.FIREBIRD; import static org.jooq.SQLDialect.H2; // ... import static org.jooq.SQLDialect.HSQLDB; // ... // ... import static org.jooq.SQLDialect.MARIADB; import static org.jooq.SQLDialect.MYSQL; // ... import static org.jooq.SQLDialect.POSTGRES; // ... import static org.jooq.SQLDialect.SQLITE; // ... // ... // ... import static org.jooq.conf.ParamType.INLINED; import static org.jooq.impl.DSL.cast; import static org.jooq.impl.DSL.inline; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.using; import static org.jooq.impl.DefaultExecuteContext.localTargetConnection; import static org.jooq.impl.Keywords.K_ARRAY; import static org.jooq.impl.Keywords.K_AS; import static org.jooq.impl.Keywords.K_BLOB; import static org.jooq.impl.Keywords.K_CAST; import static org.jooq.impl.Keywords.K_DATE; import static org.jooq.impl.Keywords.K_DATETIME; import static org.jooq.impl.Keywords.K_FALSE; import static org.jooq.impl.Keywords.K_HOUR_TO_SECOND; import static org.jooq.impl.Keywords.K_NULL; import static org.jooq.impl.Keywords.K_TIME; import static org.jooq.impl.Keywords.K_TIMESTAMP; import static org.jooq.impl.Keywords.K_TIMESTAMP_WITH_TIME_ZONE; import static org.jooq.impl.Keywords.K_TIME_WITH_TIME_ZONE; import static org.jooq.impl.Keywords.K_TRUE; import static org.jooq.impl.Keywords.K_YEAR_TO_DAY; import static org.jooq.impl.Keywords.K_YEAR_TO_FRACTION; import static org.jooq.impl.Tools.attachRecords; import static org.jooq.impl.Tools.getMappedUDTName; import static org.jooq.impl.Tools.needsBackslashEscaping; import static org.jooq.tools.jdbc.JDBCUtils.safeClose; import static org.jooq.tools.jdbc.JDBCUtils.safeFree; import static org.jooq.tools.jdbc.JDBCUtils.wasNull; import static org.jooq.tools.reflect.Reflect.on; import static org.jooq.util.postgres.PostgresUtils.toPGArrayString; import static org.jooq.util.postgres.PostgresUtils.toPGInterval; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; // ... import org.jooq.Attachable; import org.jooq.Binding; import org.jooq.BindingGetResultSetContext; import org.jooq.BindingGetSQLInputContext; import org.jooq.BindingGetStatementContext; import org.jooq.BindingRegisterContext; import org.jooq.BindingSQLContext; import org.jooq.BindingSetSQLOutputContext; import org.jooq.BindingSetStatementContext; import org.jooq.Configuration; import org.jooq.Context; import org.jooq.Converter; import org.jooq.Converters; import org.jooq.DataType; import org.jooq.EnumType; import org.jooq.Field; import org.jooq.Record; import org.jooq.RenderContext; import org.jooq.Result; import org.jooq.Row; import org.jooq.SQLDialect; import org.jooq.Schema; import org.jooq.Scope; import org.jooq.UDTRecord; import org.jooq.exception.ControlFlowSignal; import org.jooq.exception.DataTypeException; import org.jooq.exception.MappingException; import org.jooq.exception.SQLDialectNotSupportedException; import org.jooq.tools.Convert; import org.jooq.tools.JooqLogger; import org.jooq.tools.jdbc.JDBCUtils; import org.jooq.tools.jdbc.MockArray; import org.jooq.tools.jdbc.MockResultSet; import org.jooq.types.DayToSecond; import org.jooq.types.Interval; import org.jooq.types.UByte; import org.jooq.types.UInteger; import org.jooq.types.ULong; import org.jooq.types.UNumber; import org.jooq.types.UShort; import org.jooq.types.YearToMonth; import org.jooq.util.postgres.PostgresUtils; /** * @author Lukas Eder */ public class DefaultBinding<T, U> implements Binding<T, U> { static final JooqLogger log = JooqLogger.getLogger(DefaultBinding.class); private static final char[] HEX = "0123456789abcdef".toCharArray(); /** * Generated UID */ private static final long serialVersionUID = -198499389344950496L; final Class<T> type; final Converter<T, U> converter; @Deprecated // TODO: This type boolean should not be passed standalone to the // constructor. Find a better design final boolean isLob; public DefaultBinding(Converter<T, U> converter) { this(converter, false); } DefaultBinding(Converter<T, U> converter, boolean isLob) { this.type = converter.fromType(); this.converter = converter; this.isLob = isLob; } @SuppressWarnings({ "rawtypes", "unchecked" }) static final <T, X, U> Binding<T, U> newBinding(final Converter<X, U> converter, final DataType<T> type, final Binding<T, X> binding) { final Binding<T, U> theBinding; if (converter == null && binding == null) { theBinding = (Binding) type.getBinding(); } else if (converter == null) { theBinding = (Binding) binding; } else if (binding == null) { theBinding = (Binding) new DefaultBinding<X, U>(converter, type.isLob()); } else { theBinding = new Binding<T, U>() { /** * Generated UID */ private static final long serialVersionUID = 8912340791845209886L; final Converter<T, U> theConverter = Converters.of(binding.converter(), converter); @Override public Converter<T, U> converter() { return theConverter; } @Override public void sql(BindingSQLContext<U> ctx) throws SQLException { binding.sql(ctx.convert(converter)); } @Override public void register(BindingRegisterContext<U> ctx) throws SQLException { binding.register(ctx.convert(converter)); } @Override public void set(BindingSetStatementContext<U> ctx) throws SQLException { binding.set(ctx.convert(converter)); } @Override public void set(BindingSetSQLOutputContext<U> ctx) throws SQLException { binding.set(ctx.convert(converter)); } @Override public void get(BindingGetResultSetContext<U> ctx) throws SQLException { binding.get(ctx.convert(converter)); } @Override public void get(BindingGetStatementContext<U> ctx) throws SQLException { binding.get(ctx.convert(converter)); } @Override public void get(BindingGetSQLInputContext<U> ctx) throws SQLException { binding.get(ctx.convert(converter)); } }; } return theBinding; } @Override public Converter<T, U> converter() { return converter; } @Override public void sql(BindingSQLContext<U> ctx) { T converted = converter.to(ctx.value()); // Casting can be enforced or prevented switch (ctx.render().castMode()) { case NEVER: toSQL(ctx, converted); return; case ALWAYS: toSQLCast(ctx, converted); return; } // See if we "should" cast, to stay on the safe side if (shouldCast(ctx, converted)) { toSQLCast(ctx, converted); } // Most RDBMS can infer types for bind values else { toSQL(ctx, converted); } } private final boolean shouldCast(BindingSQLContext<U> ctx, T converted) { // In default mode, casting is only done when parameters are NOT inlined if (ctx.render().paramType() != INLINED) { // Generated enums should not be cast... if (!(converted instanceof EnumType)) { switch (ctx.family()) { // These dialects can hardly detect the type of a bound constant. case DERBY: case FIREBIRD: // These dialects have some trouble, when they mostly get it right. case H2: case HSQLDB: // [#1261] There are only a few corner-cases, where this is // really needed. Check back on related CUBRID bugs case CUBRID: // [#1029] Postgres and [#632] Sybase need explicit casting // in very rare cases. case POSTGRES: { return true; } } } } // [#566] JDBC doesn't explicitly support interval data types. To be on // the safe side, always cast these types in those dialects that support // them if (Interval.class.isAssignableFrom(type)) { switch (ctx.family()) { case POSTGRES: return true; } } return false; } /** * Render the bind variable including a cast, if necessary */ private final void toSQLCast(BindingSQLContext<U> ctx, T converted) { DataType<T> dataType = DefaultDataType.getDataType(ctx.dialect(), type); DataType<T> sqlDataType = dataType.getSQLDataType(); SQLDialect family = ctx.family(); // [#822] Some RDBMS need precision / scale information on BigDecimals if (converted != null && type == BigDecimal.class && asList(CUBRID, DERBY, FIREBIRD, HSQLDB).contains(family)) { // Add precision / scale on BigDecimals int scale = ((BigDecimal) converted).scale(); int precision = ((BigDecimal) converted).precision(); // [#5323] BigDecimal precision is always 1 for BigDecimals smaller than 1.0 if (scale >= precision) precision = scale + 1; toSQLCast(ctx, converted, dataType, 0, precision, scale); } // [#1028] Most databases don't know an OTHER type (except H2, HSQLDB). else if (SQLDataType.OTHER == sqlDataType) { // If the bind value is set, it can be used to derive the cast type if (converted != null) { toSQLCast(ctx, converted, DefaultDataType.getDataType(family, converted.getClass()), 0, 0, 0); } // [#632] [#722] Current integration tests show that Ingres and // Sybase can do without casting in most cases. else if (asList().contains(family)) { ctx.render().sql(ctx.variable()); } // Derby and DB2 must have a type associated with NULL. Use VARCHAR // as a workaround. That's probably not correct in all cases, though else { toSQLCast(ctx, converted, DefaultDataType.getDataType(family, String.class), 0, 0, 0); } } // [#1029] Postgres generally doesn't need the casting. Only in the // above case where the type is OTHER // [#1125] Also with temporal data types, casting is needed some times // [#4338] ... specifically when using JSR-310 types // [#1130] TODO type can be null for ARRAY types, etc. else if (asList(POSTGRES).contains(family) && (sqlDataType == null || !sqlDataType.isTemporal())) { toSQL(ctx, converted); } // [#1727] VARCHAR types should be cast to their actual lengths in some // dialects else if ((sqlDataType == SQLDataType.VARCHAR || sqlDataType == SQLDataType.CHAR) && asList(FIREBIRD).contains(family)) { toSQLCast(ctx, converted, dataType, getValueLength((String) converted), 0, 0); } // In all other cases, the bind variable can be cast normally else { toSQLCast(ctx, converted, dataType, dataType.length(), dataType.precision(), dataType.scale()); } } private static final int getValueLength(String string) { if (string == null) { return 1; } else { int length = string.length(); // If non 7-bit ASCII characters are present, multiply the length by // 4 to be sure that even UTF-32 collations will fit. But don't use // larger numbers than Derby's upper limit 32672 for (int i = 0; i < length; i++) { if (string.charAt(i) > 127) { return Math.min(32672, 4 * length); } } return Math.min(32672, length); } } private final void toSQLCast(BindingSQLContext<U> ctx, T converted, DataType<?> dataType, int length, int precision, int scale) { ctx.render().visit(K_CAST).sql('('); toSQL(ctx, converted); ctx.render().sql(' ').visit(K_AS).sql(' ') .sql(dataType.length(length).precision(precision, scale).getCastTypeName(ctx.configuration())) .sql(')'); } /** * Inlining abstraction */ @SuppressWarnings({ "unchecked", "rawtypes" }) private final void toSQL(BindingSQLContext<U> ctx, Object val) { SQLDialect family = ctx.family(); RenderContext render = ctx.render(); if (render.paramType() == INLINED) { // [#2223] Some type-casts in this section may seem unnecessary, e.g. // ((Boolean) val).toString(). They have been put in place to avoid // accidental type confusions where type != val.getClass(), and thus // SQL injection may occur if (val == null) { render.visit(K_NULL); } else if (type == Boolean.class) { // [#1153] Some dialects don't support boolean literals TRUE and FALSE if (asList(FIREBIRD, SQLITE).contains(family)) { render.sql(((Boolean) val) ? "1" : "0"); } else { render.visit(((Boolean) val) ? K_TRUE : K_FALSE); } } // [#1154] Binary data cannot always be inlined else if (type == byte[].class) { byte[] binary = (byte[]) val; if (asList().contains(family)) { render.sql("0x") .sql(convertBytesToHex(binary)); } else if (asList(DERBY, H2, HSQLDB, MARIADB, MYSQL, SQLITE).contains(family)) { render.sql("X'") .sql(convertBytesToHex(binary)) .sql('\''); } else if (asList().contains(family)) { render.sql("hextoraw('") .sql(convertBytesToHex(binary)) .sql("')"); } else if (family == POSTGRES) { render.sql("E'") .sql(PostgresUtils.toPGString(binary)) .sql("'::bytea"); } // This default behaviour is used in debug logging for dialects // that do not support inlining binary data else { render.sql("X'") .sql(convertBytesToHex(binary)) .sql('\''); } } // Interval extends Number, so let Interval come first! else if (Interval.class.isAssignableFrom(type)) { render.sql('\'') .sql(escape(val, render)) .sql('\''); } // [#5249] Special inlining of special floating point values else if (Double.class.isAssignableFrom(type) && ((Double) val).isNaN()) { if (POSTGRES == family) render.visit(inline("NaN")).sql("::float8"); else render.sql(((Number) val).toString()); } // [#5249] Special inlining of special floating point values else if (Float.class.isAssignableFrom(type) && ((Float) val).isNaN()) { if (POSTGRES == family) render.visit(inline("NaN")).sql("::float4"); else render.sql(((Number) val).toString()); } else if (Number.class.isAssignableFrom(type)) { render.sql(((Number) val).toString()); } // [#1156] DATE / TIME inlining is very vendor-specific else if (Tools.isDate(type)) { Date date = getDate(type, val); // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement date literals if (asList(SQLITE).contains(family)) { render.sql('\'').sql(escape(date, render)).sql('\''); } // [#1253] Derby doesn't support the standard literal else if (family == DERBY) { render.visit(K_DATE).sql("('").sql(escape(date, render)).sql("')"); } // [#3648] Circumvent a MySQL bug related to date literals else if (family == MYSQL) { render.sql("{d '").sql(escape(date, render)).sql("'}"); } // Most dialects implement SQL standard date literals else { render.visit(K_DATE).sql(" '").sql(escape(date, render)).sql('\''); } } else if (Tools.isTimestamp(type)) { Timestamp ts = getTimestamp(type, val); // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement timestamp literals if (asList(SQLITE).contains(family)) { render.sql('\'').sql(escape(ts, render)).sql('\''); } // [#1253] Derby doesn't support the standard literal else if (family == DERBY) { render.visit(K_TIMESTAMP).sql("('").sql(escape(ts, render)).sql("')"); } // CUBRID timestamps have no fractional seconds else if (family == CUBRID) { render.visit(K_DATETIME).sql(" '").sql(escape(ts, render)).sql('\''); } // [#3648] Circumvent a MySQL bug related to date literals else if (family == MYSQL) { render.sql("{ts '").sql(escape(ts, render)).sql("'}"); } // Most dialects implement SQL standard timestamp literals else { render.visit(K_TIMESTAMP).sql(" '").sql(escape(ts, render)).sql('\''); } } else if (Tools.isTime(type)) { Time time = getTime(type, val); // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement time literals if (asList(SQLITE).contains(family)) { render.sql('\'').sql(new SimpleDateFormat("HH:mm:ss").format(time)).sql('\''); } // [#1253] Derby doesn't support the standard literal else if (family == DERBY) { render.visit(K_TIME).sql("('").sql(escape(time, render)).sql("')"); } // [#3648] Circumvent a MySQL bug related to date literals else if (family == MYSQL) { render.sql("{t '").sql(escape(time, render)).sql("'}"); } // Most dialects implement SQL standard time literals else { render.visit(K_TIME).sql(" '").sql(escape(time, render)).sql('\''); } } else if (type == OffsetDateTime.class) { String string = format((OffsetDateTime) val); // Some dialects implement SQL standard time literals { render.visit(K_TIMESTAMP_WITH_TIME_ZONE).sql(" '").sql(escape(string, render)).sql('\''); } } else if (type == OffsetTime.class) { String string = format((OffsetTime) val); // Some dialects implement SQL standard time literals { render.visit(K_TIME_WITH_TIME_ZONE).sql(" '").sql(escape(string, render)).sql('\''); } } else if (type.isArray()) { String separator = ""; // H2 renders arrays as rows if (family == H2) { render.sql('('); for (Object o : ((Object[]) val)) { render.sql(separator); new DefaultBinding<Object, Object>(Converters.identity((Class) type.getComponentType()), isLob).sql(new DefaultBindingSQLContext<Object>(ctx.configuration(), ctx.data(), ctx.render(), o)); separator = ", "; } render.sql(')'); } else if (family == POSTGRES) { render.visit(cast(inline(PostgresUtils.toPGArrayString((Object[]) val)), type)); } // By default, render HSQLDB / POSTGRES syntax else { render.visit(K_ARRAY); render.sql('['); for (Object o : ((Object[]) val)) { render.sql(separator); new DefaultBinding<Object, Object>(Converters.identity((Class) type.getComponentType()), isLob).sql(new DefaultBindingSQLContext<Object>(ctx.configuration(), ctx.data(), ctx.render(), o)); separator = ", "; } render.sql(']'); // [#3214] Some PostgreSQL array type literals need explicit casting if (family == POSTGRES && EnumType.class.isAssignableFrom(type.getComponentType())) { pgRenderEnumCast(render, type); } } } else if (EnumType.class.isAssignableFrom(type)) { String literal = ((EnumType) val).getLiteral(); if (literal == null) { new DefaultBinding<Object, Object>(Converters.identity((Class) String.class), isLob).sql(new DefaultBindingSQLContext<Object>(ctx.configuration(), ctx.data(), ctx.render(), literal)); } else { new DefaultBinding<Object, Object>(Converters.identity((Class) String.class), isLob).sql(new DefaultBindingSQLContext<Object>(ctx.configuration(), ctx.data(), ctx.render(), literal)); } } else if (UDTRecord.class.isAssignableFrom(type)) { render.sql("[UDT]"); } // Known fall-through types: // - Blob, Clob (both not supported by jOOQ) // - String // - UUID else { render.sql('\'') .sql(escape(val, render), true) .sql('\''); } } // In Postgres, some additional casting must be done in some cases... else if (family == SQLDialect.POSTGRES) { // Postgres needs explicit casting for enum (array) types if (EnumType.class.isAssignableFrom(type) || (type.isArray() && EnumType.class.isAssignableFrom(type.getComponentType()))) { render.sql(ctx.variable()); pgRenderEnumCast(render, type); } // ... and also for other array types else if (type.isArray() && byte[].class != type) { render.sql(ctx.variable()); render.sql("::"); render.sql(DefaultDataType.getDataType(family, type).getCastTypeName(render.configuration())); } else { render.sql(ctx.variable()); } } else { render.sql(ctx.variable()); } } private static final Time getTime(Class<?> t, Object val) { return t == Time.class ? (Time) val : Time.valueOf((LocalTime) val) ; } private static final Timestamp getTimestamp(Class<?> t, Object val) { return t == Timestamp.class ? (Timestamp) val : Timestamp.valueOf((LocalDateTime) val) ; } private static final Date getDate(Class<?> t, Object val) { return t == Date.class ? (Date) val : Date.valueOf((LocalDate) val) ; } /** * Escape a string literal by replacing <code>'</code> by <code>''</code>, and possibly also backslashes. */ private static final String escape(Object val, Context<?> context) { String result = val.toString(); if (needsBackslashEscaping(context.configuration())) result = result.replace("\\", "\\\\"); return result.replace("'", "''"); } /** * Convert a byte array to a hex encoded string. * * @param value the byte array * @return the hex encoded string */ private static final String convertBytesToHex(byte[] value) { return convertBytesToHex(value, value.length); } /** * Convert a byte array to a hex encoded string. * * @param value the byte array * @param len the number of bytes to encode * @return the hex encoded string */ private static final String convertBytesToHex(byte[] value, int len) { char[] buff = new char[len + len]; char[] hex = HEX; for (int i = 0; i < len; i++) { int c = value[i] & 0xff; buff[i + i] = hex[c >> 4]; buff[i + i + 1] = hex[c & 0xf]; } return new String(buff); } @SuppressWarnings("unchecked") @Override public void register(BindingRegisterContext<U> ctx) throws SQLException { Configuration configuration = ctx.configuration(); int sqlType = DefaultDataType.getDataType(ctx.dialect(), type).getSQLType(ctx.configuration()); if (log.isTraceEnabled()) log.trace("Registering variable " + ctx.index(), "" + type); switch (configuration.family()) { default: { ctx.statement().registerOutParameter(ctx.index(), sqlType); break; } } } @SuppressWarnings("unchecked") @Override public void set(BindingSetStatementContext<U> ctx) throws SQLException { Configuration configuration = ctx.configuration(); SQLDialect dialect = ctx.dialect(); T value = converter.to(ctx.value()); if (log.isTraceEnabled()) if (value != null && value.getClass().isArray() && value.getClass() != byte[].class) log.trace("Binding variable " + ctx.index(), Arrays.asList((Object[]) value) + " (" + type + ")"); else log.trace("Binding variable " + ctx.index(), value + " (" + type + ")"); // Setting null onto a prepared statement is subtly different for every // SQL dialect. See the following section for details if (value == null) { int sqlType = DefaultDataType.getDataType(dialect, type).getSQLType(configuration); // [#1126] Oracle's UDTs need to be bound with their type name if (UDTRecord.class.isAssignableFrom(type)) { ctx.statement().setNull(ctx.index(), sqlType, Tools.getMappedUDTName(configuration, (Class<UDTRecord<?>>) type)); } // [#1225] [#1227] TODO Put this logic into DataType // Some dialects have trouble binding binary data as BLOB else if (asList(POSTGRES).contains(configuration.family()) && sqlType == Types.BLOB) { ctx.statement().setNull(ctx.index(), Types.BINARY); } // All other types can be set to null if the JDBC type is known else if (sqlType != Types.OTHER) { ctx.statement().setNull(ctx.index(), sqlType); } // [#729] In the absence of the correct JDBC type, try setObject else { ctx.statement().setObject(ctx.index(), null); } } else { Class<?> actualType = type; // Try to infer the bind value type from the actual bind value if possible. if (actualType == Object.class) { actualType = value.getClass(); } if (actualType == Blob.class) { ctx.statement().setBlob(ctx.index(), (Blob) value); } else if (actualType == Boolean.class) { ctx.statement().setBoolean(ctx.index(), (Boolean) value); } else if (actualType == BigDecimal.class) { if (asList(SQLITE).contains(dialect.family())) { ctx.statement().setString(ctx.index(), value.toString()); } else { ctx.statement().setBigDecimal(ctx.index(), (BigDecimal) value); } } else if (actualType == BigInteger.class) { if (asList(SQLITE).contains(dialect.family())) { ctx.statement().setString(ctx.index(), value.toString()); } else { ctx.statement().setBigDecimal(ctx.index(), new BigDecimal((BigInteger) value)); } } else if (actualType == Byte.class) { ctx.statement().setByte(ctx.index(), (Byte) value); } else if (actualType == byte[].class) { ctx.statement().setBytes(ctx.index(), (byte[]) value); } else if (actualType == Clob.class) { ctx.statement().setClob(ctx.index(), (Clob) value); } else if (actualType == Double.class) { ctx.statement().setDouble(ctx.index(), (Double) value); } else if (actualType == Float.class) { ctx.statement().setFloat(ctx.index(), (Float) value); } else if (actualType == Integer.class) { ctx.statement().setInt(ctx.index(), (Integer) value); } else if (actualType == Long.class) { ctx.statement().setLong(ctx.index(), (Long) value); } else if (actualType == Short.class) { ctx.statement().setShort(ctx.index(), (Short) value); } else if (actualType == String.class) { ctx.statement().setString(ctx.index(), (String) value); } // There is potential for trouble when binding date time as such // ------------------------------------------------------------- else if (Tools.isDate(actualType)) { Date date = getDate(actualType, value); if (dialect == SQLITE) { ctx.statement().setString(ctx.index(), date.toString()); } else { ctx.statement().setDate(ctx.index(), date); } } else if (Tools.isTime(actualType)) { Time time = getTime(actualType, value); if (dialect == SQLITE) { ctx.statement().setString(ctx.index(), time.toString()); } else { ctx.statement().setTime(ctx.index(), time); } } else if (Tools.isTimestamp(actualType)) { Timestamp timestamp = getTimestamp(actualType, value); if (dialect == SQLITE) { ctx.statement().setString(ctx.index(), timestamp.toString()); } else { ctx.statement().setTimestamp(ctx.index(), timestamp); } } else if (actualType == OffsetTime.class) { String string = format((OffsetTime) value); ctx.statement().setString(ctx.index(), string); } else if (actualType == OffsetDateTime.class) { ctx.statement().setString(ctx.index(), format((OffsetDateTime) value)); } // [#566] Interval data types are best bound as Strings else if (actualType == YearToMonth.class) { if (dialect.family() == POSTGRES) { ctx.statement().setObject(ctx.index(), toPGInterval((YearToMonth) value)); } else { ctx.statement().setString(ctx.index(), value.toString()); } } else if (actualType == DayToSecond.class) { if (dialect.family() == POSTGRES) { ctx.statement().setObject(ctx.index(), toPGInterval((DayToSecond) value)); } else { ctx.statement().setString(ctx.index(), value.toString()); } } else if (actualType == UByte.class) { ctx.statement().setShort(ctx.index(), ((UByte) value).shortValue()); } else if (actualType == UShort.class) { ctx.statement().setInt(ctx.index(), ((UShort) value).intValue()); } else if (actualType == UInteger.class) { ctx.statement().setLong(ctx.index(), ((UInteger) value).longValue()); } else if (actualType == ULong.class) { ctx.statement().setBigDecimal(ctx.index(), new BigDecimal(value.toString())); } else if (actualType == UUID.class) { switch (dialect.family()) { // [#1624] Some JDBC drivers natively support the // java.util.UUID data type case H2: case POSTGRES: { ctx.statement().setObject(ctx.index(), value); break; } // Most databases don't have such a type. In this case, jOOQ // emulates the type default: { ctx.statement().setString(ctx.index(), value.toString()); break; } } } // The type byte[] is handled earlier. byte[][] can be handled here else if (actualType.isArray()) { switch (dialect.family()) { case POSTGRES: { ctx.statement().setString(ctx.index(), toPGArrayString((Object[]) value)); break; } case HSQLDB: { Object[] a = (Object[]) value; Class<?> t = actualType; // [#2325] [#5823] Cannot bind UUID[] type in HSQLDB. // See also: https://sourceforge.net/p/hsqldb/bugs/1466 if (actualType == UUID[].class) { a = Convert.convertArray(a, byte[][].class); t = byte[][].class; } ctx.statement().setArray(ctx.index(), new MockArray(dialect, a, t)); break; } case H2: { ctx.statement().setObject(ctx.index(), value); break; } default: throw new SQLDialectNotSupportedException("Cannot bind ARRAY types in dialect " + dialect); } } else if (EnumType.class.isAssignableFrom(actualType)) { ctx.statement().setString(ctx.index(), ((EnumType) value).getLiteral()); } else { ctx.statement().setObject(ctx.index(), value); } } } @Override public void set(BindingSetSQLOutputContext<U> ctx) throws SQLException { Configuration configuration = ctx.configuration(); T value = converter.to(ctx.value()); if (value == null) { ctx.output().writeObject(null); } else if (type == Blob.class) { ctx.output().writeBlob((Blob) value); } else if (type == Boolean.class) { ctx.output().writeBoolean((Boolean) value); } else if (type == BigInteger.class) { ctx.output().writeBigDecimal(new BigDecimal((BigInteger) value)); } else if (type == BigDecimal.class) { ctx.output().writeBigDecimal((BigDecimal) value); } else if (type == Byte.class) { ctx.output().writeByte((Byte) value); } else if (type == byte[].class) { // [#1327] Oracle cannot serialise BLOBs as byte[] to SQLOutput // Use reflection to avoid dependency on OJDBC if (isLob) { Blob blob = null; try { blob = on("oracle.sql.BLOB").call("createTemporary", on(ctx.output()).call("getSTRUCT") .call("getJavaSqlConnection").get(), false, on("oracle.sql.BLOB").get("DURATION_SESSION")).get(); blob.setBytes(1, (byte[]) value); ctx.output().writeBlob(blob); } finally { DefaultExecuteContext.register(blob); } } else { ctx.output().writeBytes((byte[]) value); } } else if (type == Clob.class) { ctx.output().writeClob((Clob) value); } else if (type == Date.class) { Date date = (Date) value; ctx.output().writeDate(date); } else if (type == Double.class) { ctx.output().writeDouble((Double) value); } else if (type == Float.class) { ctx.output().writeFloat((Float) value); } else if (type == Integer.class) { ctx.output().writeInt((Integer) value); } else if (type == Long.class) { ctx.output().writeLong((Long) value); } else if (type == Short.class) { ctx.output().writeShort((Short) value); } else if (type == String.class) { // [#1327] Oracle cannot serialise CLOBs as String to SQLOutput // Use reflection to avoid dependency on OJDBC if (isLob) { Clob clob = null; try { clob = on("oracle.sql.CLOB").call("createTemporary", on(ctx.output()).call("getSTRUCT") .call("getJavaSqlConnection").get(), false, on("oracle.sql.CLOB").get("DURATION_SESSION")).get(); clob.setString(1, (String) value); ctx.output().writeClob(clob); } finally { DefaultExecuteContext.register(clob); } } else { ctx.output().writeString((String) value); } } else if (type == Time.class) { ctx.output().writeTime((Time) value); } else if (type == Timestamp.class) { ctx.output().writeTimestamp((Timestamp) value); } else if (type == YearToMonth.class) { ctx.output().writeString(value.toString()); } else if (type == DayToSecond.class) { ctx.output().writeString(value.toString()); } // else if (type.isArray()) { // stream.writeArray(value); // } else if (UNumber.class.isAssignableFrom(type)) { ctx.output().writeString(value.toString()); } else if (type == UUID.class) { ctx.output().writeString(value.toString()); } else if (EnumType.class.isAssignableFrom(type)) { ctx.output().writeString(((EnumType) value).getLiteral()); } else if (UDTRecord.class.isAssignableFrom(type)) { ctx.output().writeObject((UDTRecord<?>) value); } else { throw new UnsupportedOperationException("Type " + type + " is not supported"); } } @SuppressWarnings("unchecked") @Override public void get(BindingGetResultSetContext<U> ctx) throws SQLException { T result = null; if (type == Blob.class) { result = (T) ctx.resultSet().getBlob(ctx.index()); } else if (type == Boolean.class) { result = (T) wasNull(ctx.resultSet(), Boolean.valueOf(ctx.resultSet().getBoolean(ctx.index()))); } else if (type == BigInteger.class) { // The SQLite JDBC driver doesn't support BigDecimals if (ctx.configuration().dialect() == SQLDialect.SQLITE) { result = Convert.convert(ctx.resultSet().getString(ctx.index()), (Class<T>) BigInteger.class); } else { BigDecimal b = ctx.resultSet().getBigDecimal(ctx.index()); result = (T) (b == null ? null : b.toBigInteger()); } } else if (type == BigDecimal.class) { // The SQLite JDBC driver doesn't support BigDecimals if (ctx.configuration().dialect() == SQLDialect.SQLITE) { result = Convert.convert(ctx.resultSet().getString(ctx.index()), (Class<T>) BigDecimal.class); } else { result = (T) ctx.resultSet().getBigDecimal(ctx.index()); } } else if (type == Byte.class) { result = (T) wasNull(ctx.resultSet(), Byte.valueOf(ctx.resultSet().getByte(ctx.index()))); } else if (type == byte[].class) { result = (T) ctx.resultSet().getBytes(ctx.index()); } else if (type == Clob.class) { result = (T) ctx.resultSet().getClob(ctx.index()); } else if (type == Date.class) { result = (T) getDate(ctx.family(), ctx.resultSet(), ctx.index()); } else if (type == Double.class) { result = (T) wasNull(ctx.resultSet(), Double.valueOf(ctx.resultSet().getDouble(ctx.index()))); } else if (type == Float.class) { result = (T) wasNull(ctx.resultSet(), Float.valueOf(ctx.resultSet().getFloat(ctx.index()))); } else if (type == Integer.class) { result = (T) wasNull(ctx.resultSet(), Integer.valueOf(ctx.resultSet().getInt(ctx.index()))); } else if (type == LocalDate.class) { result = (T) localDate(getDate(ctx.family(), ctx.resultSet(), ctx.index())); } else if (type == LocalTime.class) { result = (T) localTime(getTime(ctx.family(), ctx.resultSet(), ctx.index())); } else if (type == LocalDateTime.class) { result = (T) localDateTime(getTimestamp(ctx.family(), ctx.resultSet(), ctx.index())); } else if (type == Long.class) { result = (T) wasNull(ctx.resultSet(), Long.valueOf(ctx.resultSet().getLong(ctx.index()))); } else if (type == OffsetTime.class) { result = (T) offsetTime(ctx.resultSet().getString(ctx.index())); } else if (type == OffsetDateTime.class) { result = (T) offsetDateTime(ctx.resultSet().getString(ctx.index())); } else if (type == Short.class) { result = (T) wasNull(ctx.resultSet(), Short.valueOf(ctx.resultSet().getShort(ctx.index()))); } else if (type == String.class) { result = (T) ctx.resultSet().getString(ctx.index()); } else if (type == Time.class) { result = (T) getTime(ctx.family(), ctx.resultSet(), ctx.index()); } else if (type == Timestamp.class) { result = (T) getTimestamp(ctx.family(), ctx.resultSet(), ctx.index()); } else if (type == YearToMonth.class) { if (ctx.family() == POSTGRES) { Object object = ctx.resultSet().getObject(ctx.index()); result = (T) (object == null ? null : PostgresUtils.toYearToMonth(object)); } else { String string = ctx.resultSet().getString(ctx.index()); result = (T) (string == null ? null : YearToMonth.valueOf(string)); } } else if (type == DayToSecond.class) { if (ctx.family() == POSTGRES) { Object object = ctx.resultSet().getObject(ctx.index()); result = (T) (object == null ? null : PostgresUtils.toDayToSecond(object)); } else { String string = ctx.resultSet().getString(ctx.index()); result = (T) (string == null ? null : DayToSecond.valueOf(string)); } } else if (type == UByte.class) { result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UByte.class); } else if (type == UShort.class) { result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UShort.class); } else if (type == UInteger.class) { result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UInteger.class); } else if (type == ULong.class) { result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), ULong.class); } else if (type == UUID.class) { switch (ctx.family()) { // [#1624] Some JDBC drivers natively support the // java.util.UUID data type case H2: case POSTGRES: { result = (T) ctx.resultSet().getObject(ctx.index()); break; } // Most databases don't have such a type. In this case, jOOQ // emulates the type default: { result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UUID.class); break; } } } // The type byte[] is handled earlier. byte[][] can be handled here else if (type.isArray()) { switch (ctx.family()) { case POSTGRES: { result = pgGetArray(ctx, ctx.resultSet(), type, ctx.index()); break; } default: // Note: due to a HSQLDB bug, it is not recommended to call rs.getObject() here: // See https://sourceforge.net/tracker/?func=detail&aid=3181365&group_id=23316&atid=378131 result = (T) convertArray(ctx.resultSet().getArray(ctx.index()), (Class<? extends Object[]>) type); break; } } else if (EnumType.class.isAssignableFrom(type)) { result = (T) getEnumType((Class<EnumType>) type, ctx.resultSet().getString(ctx.index())); } else if (Record.class.isAssignableFrom(type)) { switch (ctx.family()) { case POSTGRES: result = (T) pgNewRecord(type, null, ctx.resultSet().getObject(ctx.index())); break; default: result = (T) ctx.resultSet().getObject(ctx.index(), typeMap(type, ctx.configuration())); break; } } else if (Result.class.isAssignableFrom(type)) { ResultSet nested = (ResultSet) ctx.resultSet().getObject(ctx.index()); result = (T) DSL.using(ctx.configuration()).fetch(nested); } else { result = (T) unlob(ctx.resultSet().getObject(ctx.index())); } // [#4372] Attach records if possible / required if (result instanceof Attachable && attachRecords(ctx.configuration())) ((Attachable) result).attach(ctx.configuration()); ctx.value(converter.from(result)); } private static final LocalDate localDate(Date date) { return date == null ? null : date.toLocalDate(); } private static final LocalTime localTime(Time time) { return time == null ? null : time.toLocalTime(); } private static final LocalDateTime localDateTime(Timestamp timestamp) { return timestamp == null ? null : timestamp.toLocalDateTime(); } private static final Pattern LENIENT_OFFSET_PATTERN = Pattern.compile( "(?:(\\d{4}-\\d{2}-\\d{2})[T ])?(\\d{2}:\\d{2}(:\\d{2})?(?:\\.\\d+)?)(?: +)?(([+-])(\\d)?(\\d)(:\\d{2})?)?"); private static final OffsetTime offsetTime(String string) { return string == null ? null : OffsetTime.parse(preparse(string, false)); } private static final OffsetDateTime offsetDateTime(String string) { return string == null ? null : OffsetDateTime.parse(preparse(string, true)); } private static final String preparse(String formatted, boolean includeDate) { Matcher m = LENIENT_OFFSET_PATTERN.matcher(formatted); if (m.find()) { StringBuilder sb = new StringBuilder(); String group1 = m.group(1); if (includeDate && group1 != null) { sb.append(group1); // [#4338] SQL supports the alternative ISO 8601 date format, where a // whitespace character separates date and time. java.time does not sb.append('T'); } sb.append(m.group(2)); if (m.group(3) == null) sb.append(":00"); if (m.group(4) != null) { sb.append(m.group(5)); String group6 = m.group(6); String group8 = m.group(8); // [#4965] Oracle might return a single-digit hour offset (and some spare space) sb.append(group6 == null ? "0" : group6); sb.append(m.group(7)); // [#4338] [#5180] [#5776] PostgreSQL is more lenient regarding the offset format sb.append(group8 == null ? ":00" : group8); } else { sb.append("+00:00"); } return sb.toString(); } // Probably a bug, let OffsetDateTime or OffsetTime report it else { return formatted; } } private static final String replaceZ(String format) { // Replace the ISO standard Z character for UTC, as some databases don't like that return format.replace("Z", "+00:00"); } private static final String format(OffsetTime val) { return replaceZ(val.format(DateTimeFormatter.ISO_OFFSET_TIME)); } private static final String format(OffsetDateTime val) { // Remove the ISO standard T character, as some databases don't like that String format = val.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); return replaceZ(format.substring(0, 10) + ' ' + format.substring(11)); } @SuppressWarnings("unchecked") @Override public void get(BindingGetStatementContext<U> ctx) throws SQLException { T result = null; if (type == Blob.class) { result = (T) ctx.statement().getBlob(ctx.index()); } else if (type == Boolean.class) { result = (T) wasNull(ctx.statement(), Boolean.valueOf(ctx.statement().getBoolean(ctx.index()))); } else if (type == BigInteger.class) { BigDecimal d = ctx.statement().getBigDecimal(ctx.index()); result = (T) (d == null ? null : d.toBigInteger()); } else if (type == BigDecimal.class) { result = (T) ctx.statement().getBigDecimal(ctx.index()); } else if (type == Byte.class) { result = (T) wasNull(ctx.statement(), Byte.valueOf(ctx.statement().getByte(ctx.index()))); } else if (type == byte[].class) { result = (T) ctx.statement().getBytes(ctx.index()); } else if (type == Clob.class) { result = (T) ctx.statement().getClob(ctx.index()); } else if (Tools.isDate(type)) { result = (T) ctx.statement().getDate(ctx.index()); if (result != null && type == LocalDate.class) result = (T) ((Date) result).toLocalDate(); } else if (type == Double.class) { result = (T) wasNull(ctx.statement(), Double.valueOf(ctx.statement().getDouble(ctx.index()))); } else if (type == Float.class) { result = (T) wasNull(ctx.statement(), Float.valueOf(ctx.statement().getFloat(ctx.index()))); } else if (type == Integer.class) { result = (T) wasNull(ctx.statement(), Integer.valueOf(ctx.statement().getInt(ctx.index()))); } else if (type == Long.class) { result = (T) wasNull(ctx.statement(), Long.valueOf(ctx.statement().getLong(ctx.index()))); } else if (type == Short.class) { result = (T) wasNull(ctx.statement(), Short.valueOf(ctx.statement().getShort(ctx.index()))); } else if (type == String.class) { result = (T) ctx.statement().getString(ctx.index()); } else if (Tools.isTime(type)) { result = (T) ctx.statement().getTime(ctx.index()); if (result != null && type == LocalTime.class) result = (T) ((Time) result).toLocalTime(); } else if (Tools.isTimestamp(type)) { result = (T) ctx.statement().getTimestamp(ctx.index()); if (result != null && type == LocalDateTime.class) result = (T) ((Timestamp) result).toLocalDateTime(); } else if (type == OffsetTime.class) { result = (T) offsetTime(ctx.statement().getString(ctx.index())); } else if (type == OffsetDateTime.class) { result = (T) offsetDateTime(ctx.statement().getString(ctx.index())); } else if (type == YearToMonth.class) { if (ctx.family() == POSTGRES) { Object object = ctx.statement().getObject(ctx.index()); result = (T) (object == null ? null : PostgresUtils.toYearToMonth(object)); } else { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : YearToMonth.valueOf(string)); } } else if (type == DayToSecond.class) { if (ctx.family() == POSTGRES) { Object object = ctx.statement().getObject(ctx.index()); result = (T) (object == null ? null : PostgresUtils.toDayToSecond(object)); } else { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : DayToSecond.valueOf(string)); } } else if (type == UByte.class) { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : UByte.valueOf(string)); } else if (type == UShort.class) { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : UShort.valueOf(string)); } else if (type == UInteger.class) { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : UInteger.valueOf(string)); } else if (type == ULong.class) { String string = ctx.statement().getString(ctx.index()); result = (T) (string == null ? null : ULong.valueOf(string)); } else if (type == UUID.class) { switch (ctx.family()) { // [#1624] Some JDBC drivers natively support the // java.util.UUID data type case H2: case POSTGRES: { result = (T) ctx.statement().getObject(ctx.index()); break; } // Most databases don't have such a type. In this case, jOOQ // emulates the type default: { result = (T) Convert.convert(ctx.statement().getString(ctx.index()), UUID.class); break; } } } // The type byte[] is handled earlier. byte[][] can be handled here else if (type.isArray()) { result = (T) convertArray(ctx.statement().getObject(ctx.index()), (Class<? extends Object[]>)type); } else if (EnumType.class.isAssignableFrom(type)) { result = (T) getEnumType((Class<EnumType>) type, ctx.statement().getString(ctx.index())); } else if (Record.class.isAssignableFrom(type)) { switch (ctx.family()) { case POSTGRES: result = (T) pgNewRecord(type, null, ctx.statement().getObject(ctx.index())); break; default: result = (T) ctx.statement().getObject(ctx.index(), typeMap(type, ctx.configuration())); break; } } else if (Result.class.isAssignableFrom(type)) { ResultSet nested = (ResultSet) ctx.statement().getObject(ctx.index()); result = (T) DSL.using(ctx.configuration()).fetch(nested); } else { result = (T) ctx.statement().getObject(ctx.index()); } // [#4372] Attach records if possible / required if (result instanceof Attachable && attachRecords(ctx.configuration())) ((Attachable) result).attach(ctx.configuration()); ctx.value(converter.from(result)); } static final Map<String, Class<?>> typeMap(Class<?> type, Configuration configuration) { return typeMap(type, configuration, new HashMap<String, Class<?>>()); } @SuppressWarnings("unchecked") static final Map<String, Class<?>> typeMap(Class<?> type, Configuration configuration, Map<String, Class<?>> result) { try { if (UDTRecord.class.isAssignableFrom(type)) { Class<UDTRecord<?>> t = (Class<UDTRecord<?>>) type; result.put(getMappedUDTName(configuration, t), t); UDTRecord<?> r = t.newInstance(); for (Field<?> field : r.getUDT().fields()) typeMap(field.getType(), configuration, result); } } catch (Exception e) { throw new MappingException("Error while collecting type map", e); } return result; } @SuppressWarnings("unchecked") @Override public void get(BindingGetSQLInputContext<U> ctx) throws SQLException { T result = null; if (type == Blob.class) { result = (T) ctx.input().readBlob(); } else if (type == Boolean.class) { result = (T) wasNull(ctx.input(), Boolean.valueOf(ctx.input().readBoolean())); } else if (type == BigInteger.class) { BigDecimal d = ctx.input().readBigDecimal(); result = (T) (d == null ? null : d.toBigInteger()); } else if (type == BigDecimal.class) { result = (T) ctx.input().readBigDecimal(); } else if (type == Byte.class) { result = (T) wasNull(ctx.input(), Byte.valueOf(ctx.input().readByte())); } else if (type == byte[].class) { // [#1327] Oracle cannot deserialise BLOBs as byte[] from SQLInput if (isLob) { Blob blob = null; try { blob = ctx.input().readBlob(); result = (T) (blob == null ? null : blob.getBytes(1, (int) blob.length())); } finally { safeFree(blob); } } else { result = (T) ctx.input().readBytes(); } } else if (type == Clob.class) { result = (T) ctx.input().readClob(); } else if (type == Date.class) { result = (T) ctx.input().readDate(); } else if (type == Double.class) { result = (T) wasNull(ctx.input(), Double.valueOf(ctx.input().readDouble())); } else if (type == Float.class) { result = (T) wasNull(ctx.input(), Float.valueOf(ctx.input().readFloat())); } else if (type == Integer.class) { result = (T) wasNull(ctx.input(), Integer.valueOf(ctx.input().readInt())); } else if (type == Long.class) { result = (T) wasNull(ctx.input(), Long.valueOf(ctx.input().readLong())); } else if (type == Short.class) { result = (T) wasNull(ctx.input(), Short.valueOf(ctx.input().readShort())); } else if (type == String.class) { result = (T) ctx.input().readString(); } else if (type == Time.class) { result = (T) ctx.input().readTime(); } else if (type == Timestamp.class) { result = (T) ctx.input().readTimestamp(); } else if (type == YearToMonth.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : YearToMonth.valueOf(string)); } else if (type == DayToSecond.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : DayToSecond.valueOf(string)); } else if (type == UByte.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : UByte.valueOf(string)); } else if (type == UShort.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : UShort.valueOf(string)); } else if (type == UInteger.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : UInteger.valueOf(string)); } else if (type == ULong.class) { String string = ctx.input().readString(); result = (T) (string == null ? null : ULong.valueOf(string)); } else if (type == UUID.class) { result = (T) Convert.convert(ctx.input().readString(), UUID.class); } // The type byte[] is handled earlier. byte[][] can be handled here else if (type.isArray()) { Array array = ctx.input().readArray(); result = (T) (array == null ? null : array.getArray()); } else if (EnumType.class.isAssignableFrom(type)) { result = (T) getEnumType((Class<EnumType>) type, ctx.input().readString()); } else if (UDTRecord.class.isAssignableFrom(type)) { result = (T) ctx.input().readObject(); } else { result = (T) unlob(ctx.input().readObject()); } ctx.value(converter.from(result)); } /** * [#2534] Extract <code>byte[]</code> or <code>String</code> data from a * LOB, if the argument is a lob. */ private static final Object unlob(Object object) throws SQLException { if (object instanceof Blob) { Blob blob = (Blob) object; try { return blob.getBytes(1, (int) blob.length()); } finally { JDBCUtils.safeFree(blob); } } else if (object instanceof Clob) { Clob clob = (Clob) object; try { return clob.getSubString(1, (int) clob.length()); } finally { JDBCUtils.safeFree(clob); } } return object; } @SuppressWarnings("unchecked") private static final <E extends EnumType> E getEnumType(Class<? extends E> type, String literal) { try { EnumType[] list = Tools.enums(type); for (EnumType e : list) if (e.getLiteral().equals(literal)) return (E) e; } catch (Exception e) { throw new DataTypeException("Unknown enum literal found : " + literal); } return null; } private static final Object[] convertArray(Object array, Class<? extends Object[]> type) throws SQLException { if (array instanceof Object[]) { return Convert.convert(array, type); } else if (array instanceof Array) { return convertArray((Array) array, type); } return null; } private static final Object[] convertArray(Array array, Class<? extends Object[]> type) throws SQLException { if (array != null) { return Convert.convert(array.getArray(), type); } return null; } private static final Date getDate(SQLDialect family, ResultSet rs, int index) throws SQLException { // SQLite's type affinity needs special care... if (family == SQLDialect.SQLITE) { String date = rs.getString(index); return date == null ? null : new Date(parse(Date.class, date)); } else { return rs.getDate(index); } } private static final Time getTime(SQLDialect family, ResultSet rs, int index) throws SQLException { // SQLite's type affinity needs special care... if (family == SQLDialect.SQLITE) { String time = rs.getString(index); return time == null ? null : new Time(parse(Time.class, time)); } else { return rs.getTime(index); } } private static final Timestamp getTimestamp(SQLDialect family, ResultSet rs, int index) throws SQLException { // SQLite's type affinity needs special care... if (family == SQLDialect.SQLITE) { String timestamp = rs.getString(index); return timestamp == null ? null : new Timestamp(parse(Timestamp.class, timestamp)); } else { return rs.getTimestamp(index); } } private static final Pattern WHITESPACE = Pattern.compile(" "); private static final long parse(Class<? extends java.util.Date> type, String date) throws SQLException { // Try reading a plain number first try { return Long.valueOf(date); } // If that fails, try reading a formatted date catch (NumberFormatException e) { if (type == Timestamp.class) return Timestamp.valueOf(date).getTime(); // Dates may come with " 00:00:00". This is safely trimming time information if (type == Date.class) return Date.valueOf(WHITESPACE.split(date)[0]).getTime(); if (type == Time.class) return Time.valueOf(date).getTime(); throw new SQLException("Could not parse date " + date, e); } } // ------------------------------------------------------------------------- // XXX: The following section has been added for Postgres UDT support. The // official Postgres JDBC driver does not implement SQLData and similar // interfaces. Instead, a string representation of a UDT has to be parsed // ------------------------------------------------------------------------- private static final <T> T pgFromString(Class<T> type, String string) { return pgFromString(Converters.identity(type), string); } @SuppressWarnings("unchecked") private static final <T> T pgFromString(Converter<?, T> converter, String string) { Class<T> type = converter.toType(); if (string == null) { return null; } else if (type == Blob.class) { // Not supported } else if (type == Boolean.class) { return (T) Convert.convert(string, Boolean.class); } else if (type == BigInteger.class) { return (T) new BigInteger(string); } else if (type == BigDecimal.class) { return (T) new BigDecimal(string); } else if (type == Byte.class) { return (T) Byte.valueOf(string); } else if (type == byte[].class) { return (T) PostgresUtils.toBytes(string); } else if (type == Clob.class) { // Not supported } else if (type == Date.class) { return (T) Date.valueOf(string); } else if (type == Double.class) { return (T) Double.valueOf(string); } else if (type == Float.class) { return (T) Float.valueOf(string); } else if (type == Integer.class) { return (T) Integer.valueOf(string); } else if (type == Long.class) { return (T) Long.valueOf(string); } else if (type == Short.class) { return (T) Short.valueOf(string); } else if (type == String.class) { return (T) string; } else if (type == Time.class) { return (T) Time.valueOf(string); } else if (type == Timestamp.class) { return (T) Timestamp.valueOf(string); } else if (type == LocalTime.class) { return (T) LocalTime.parse(string); } else if (type == LocalDate.class) { return (T) LocalDate.parse(string); } else if (type == LocalDateTime.class) { return (T) LocalDateTime.parse(string); } else if (type == OffsetTime.class) { return (T) offsetTime(string); } else if (type == OffsetDateTime.class) { return (T) offsetDateTime(string); } else if (type == UByte.class) { return (T) UByte.valueOf(string); } else if (type == UShort.class) { return (T) UShort.valueOf(string); } else if (type == UInteger.class) { return (T) UInteger.valueOf(string); } else if (type == ULong.class) { return (T) ULong.valueOf(string); } else if (type == UUID.class) { return (T) UUID.fromString(string); } else if (type.isArray()) { return (T) pgNewArray(type, string); } else if (EnumType.class.isAssignableFrom(type)) { return (T) getEnumType((Class<EnumType>) type, string); } else if (Record.class.isAssignableFrom(type)) { return (T) pgNewRecord(type, null, string); } else if (type == Object.class) { return (T) string; } // [#4964] [#6058] Recurse only if we have a meaningful converter, not the identity converter, // which would cause a StackOverflowError, here! else if (type != converter.fromType()) { Converter<Object, T> c = (Converter<Object, T>) converter; return c.from(pgFromString(c.fromType(), string)); } throw new UnsupportedOperationException("Class " + type + " is not supported"); } /** * Create a UDT record from a PGobject * <p> * Unfortunately, this feature is very poorly documented and true UDT * support by the PostGreSQL JDBC driver has been postponed for a long time. * * @param object An object of type PGobject. The actual argument type cannot * be expressed in the method signature, as no explicit * dependency to postgres logic is desired * @return The converted {@link UDTRecord} */ @SuppressWarnings("unchecked") static final Record pgNewRecord(Class<?> type, Field<?>[] fields, final Object object) { if (object == null) { return null; } return Tools.newRecord(true, (Class<Record>) type, fields) .operate(new RecordOperation<Record, RuntimeException>() { @Override public Record operate(Record record) { List<String> values = PostgresUtils.toPGObject(object.toString()); Row row = record.fieldsRow(); for (int i = 0; i < row.size(); i++) { pgSetValue(record, row.field(i), values.get(i)); } return record; } }); } /** * Workarounds for the unimplemented Postgres JDBC driver features */ @SuppressWarnings("unchecked") private static final <T> T pgGetArray(Scope ctx, ResultSet rs, Class<T> type, int index) throws SQLException { // Get the JDBC Array and check for null. If null, that's OK Array array = null; try { array = rs.getArray(index); if (array == null) { return null; } // Try fetching a Java Object[]. That's gonna work for non-UDT types try { // [#5633] Special treatment for this type. // [#5586] [#5613] TODO: Improve PostgreSQL array deserialisation. if (byte[][].class == type) throw new ControlFlowSignal("GOTO the next array deserialisation strategy"); else return (T) convertArray(array, (Class<? extends Object[]>) type); } // This might be a UDT (not implemented exception...) catch (Exception e) { List<Object> result = new ArrayList<Object>(); ResultSet arrayRs = null; // Try fetching the array as a JDBC ResultSet try { arrayRs = array.getResultSet(); while (arrayRs.next()) { DefaultBindingGetResultSetContext<T> out = new DefaultBindingGetResultSetContext<T>(ctx.configuration(), ctx.data(), arrayRs, 2); new DefaultBinding<T, T>(Converters.identity((Class<T>) type.getComponentType()), false).get(out); result.add(out.value()); } } // That might fail too, then we don't know any further... catch (Exception fatal) { String string = null; try { string = rs.getString(index); } catch (SQLException ignore) {} log.error("Cannot parse array", string, fatal); return null; } finally { safeClose(arrayRs); } return (T) convertArray(result.toArray(), (Class<? extends Object[]>) type); } } finally { safeFree(array); } } /** * Create an array from a String * <p> * Unfortunately, this feature is very poorly documented and true UDT * support by the PostGreSQL JDBC driver has been postponed for a long time. * * @param string A String representation of an array * @return The converted array */ private static final Object[] pgNewArray(Class<?> type, String string) { if (string == null) { return null; } try { Class<?> component = type.getComponentType(); List<String> values = PostgresUtils.toPGArray(string); if (values.isEmpty()) { return (Object[]) java.lang.reflect.Array.newInstance(component, 0); } else { Object[] result = (Object[]) java.lang.reflect.Array.newInstance(component, values.size()); for (int i = 0; i < values.size(); i++) result[i] = pgFromString(type.getComponentType(), values.get(i)); return result; } } catch (Exception e) { throw new DataTypeException("Error while creating array", e); } } static final <T> void pgSetValue(Record record, Field<T> field, String value) { record.set(field, pgFromString(field.getConverter(), value)); } private static final void pgRenderEnumCast(RenderContext render, Class<?> type) { @SuppressWarnings("unchecked") Class<? extends EnumType> enumType = (Class<? extends EnumType>) ( type.isArray() ? type.getComponentType() : type); // [#968] Don't cast "synthetic" enum types (note, val can be null!) // [#4427] Java Enum agnostic implementation will work for Scala also EnumType[] enums = Tools.enums(enumType); if (enums == null || enums.length == 0) throw new IllegalArgumentException("Not a valid EnumType : " + type); Schema schema = enums[0].getSchema(); if (schema != null) { render.sql("::"); schema = using(render.configuration()).map(schema); if (schema != null && TRUE.equals(render.configuration().settings().isRenderSchema())) { render.visit(schema); render.sql('.'); } render.visit(name(enums[0].getName())); } if (type.isArray()) render.sql("[]"); } // ----------------------------------------------------------------------------------------------------------------- // Object API // ----------------------------------------------------------------------------------------------------------------- @Override public String toString() { return "DefaultBinding [type=" + type + ", converter=" + converter + "]"; } }