jooq/jOOQ/src/main/java/org/jooq/impl/Val.java
2012-12-22 09:46:27 +01:00

628 lines
22 KiB
Java

/**
* Copyright (c) 2009-2012, Lukas Eder, lukas.eder@gmail.com
* All rights reserved.
*
* This software is licensed to you under the Apache License, Version 2.0
* (the "License"); You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* . Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* . Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* . Neither the name "jOOQ" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.jooq.impl;
import static java.lang.Integer.toOctalString;
import static java.util.Arrays.asList;
import static org.jooq.SQLDialect.ASE;
import static org.jooq.SQLDialect.CUBRID;
import static org.jooq.SQLDialect.DB2;
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.INGRES;
import static org.jooq.SQLDialect.MYSQL;
import static org.jooq.SQLDialect.ORACLE;
import static org.jooq.SQLDialect.POSTGRES;
import static org.jooq.SQLDialect.SQLITE;
import static org.jooq.SQLDialect.SQLSERVER;
import static org.jooq.SQLDialect.SYBASE;
import static org.jooq.tools.StringUtils.leftPad;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Arrays;
import org.jooq.ArrayRecord;
import org.jooq.BindContext;
import org.jooq.Converter;
import org.jooq.DataType;
import org.jooq.EnumType;
import org.jooq.Param;
import org.jooq.RenderContext;
import org.jooq.SQLDialect;
import org.jooq.UDTRecord;
import org.jooq.tools.StringUtils;
import org.jooq.types.Interval;
/**
* @author Lukas Eder
*/
class Val<T> extends AbstractField<T> implements Param<T> {
private static final long serialVersionUID = 6807729087019209084L;
private static final char[] HEX = "0123456789abcdef".toCharArray();
private final String paramName;
private T value;
private boolean inline;
Val(T value, DataType<T> type) {
this(value, type, null);
}
Val(T value, DataType<T> type, String paramName) {
super(name(value, paramName), type);
this.paramName = paramName;
this.value = value;
}
/**
* A utility method that generates a field name.
* <p>
* <ul>
* <li>If <code>paramName != null</code>, take <code>paramName</code></li>
* <li>Otherwise, take the string value of <code>value</code></li>
* </ul>
*/
private static String name(Object value, String paramName) {
return paramName == null ? String.valueOf(value) : paramName;
}
// ------------------------------------------------------------------------
// XXX: Field API
// ------------------------------------------------------------------------
@Override
public final void toSQL(RenderContext context) {
// Casting can be enforced or prevented
switch (context.castMode()) {
case NEVER:
toSQL(context, getValue(), getType());
return;
case ALWAYS:
toSQLCast(context);
return;
case SOME:
// This dialect must cast
if (context.cast()) {
toSQLCast(context);
}
// In some cases, we should still cast
else if (shouldCast(context)) {
toSQLCast(context);
}
else {
toSQL(context, getValue(), getType());
}
return;
}
// See if we "should" cast, to stay on the safe side
if (shouldCast(context)) {
toSQLCast(context);
}
// Most RDBMS can infer types for bind values
else {
toSQL(context, getValue(), getType());
}
}
private boolean shouldCast(RenderContext context) {
// In default mode, casting is only done when parameters are NOT inlined
if (!isInline(context)) {
// Generated enums should not be cast...
if (!(getValue() instanceof EnumType)) {
switch (context.getDialect()) {
// These dialects can hardly detect the type of a bound constant.
case DB2:
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:
case SYBASE: {
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 (getDataType().isInterval()) {
switch (context.getDialect()) {
case ORACLE:
case POSTGRES:
return true;
}
}
return false;
}
/**
* Render the bind variable including a cast, if necessary
*/
private void toSQLCast(RenderContext context) {
DataType<T> dataType = getDataType(context);
DataType<T> type = dataType.getSQLDataType();
SQLDialect dialect = context.getDialect();
// [#822] Some RDBMS need precision / scale information on BigDecimals
if (getValue() != null && getType() == BigDecimal.class && asList(CUBRID, DB2, DERBY, FIREBIRD, HSQLDB).contains(dialect)) {
// Add precision / scale on BigDecimals
int scale = ((BigDecimal) getValue()).scale();
int precision = scale + ((BigDecimal) getValue()).precision();
// Firebird's max precision is 18
if (dialect == FIREBIRD) {
precision = Math.min(precision, 18);
}
toSQLCast(context, dataType, 0, precision, scale);
}
// [#1028] Most databases don't know an OTHER type (except H2, HSQLDB).
else if (SQLDataType.OTHER == type) {
// If the bind value is set, it can be used to derive the cast type
if (value != null) {
toSQLCast(context, DefaultDataType.getDataType(dialect, value.getClass()), 0, 0, 0);
}
// [#632] [#722] Current integration tests show that Ingres and
// Sybase can do without casting in most cases.
else if (asList(INGRES, SYBASE).contains(dialect)) {
context.sql(getBindVariable(context));
}
// 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(context, DefaultDataType.getDataType(dialect, 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
// [#1130] TODO type can be null for ARRAY types, etc.
else if (dialect == POSTGRES && (type == null || !type.isTemporal())) {
toSQL(context, getValue(), getType());
}
// [#1727] VARCHAR types should be cast to their actual lengths in some
// dialects
else if ((type == SQLDataType.VARCHAR || type == SQLDataType.CHAR) && asList(FIREBIRD).contains(dialect)) {
toSQLCast(context, dataType, getValueLength(), 0, 0);
}
// In all other cases, the bind variable can be cast normally
else {
toSQLCast(context, dataType, dataType.length(), dataType.precision(), dataType.scale());
}
}
private int getValueLength() {
String string = (String) getValue();
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 void toSQLCast(RenderContext context, DataType<?> type, int length, int precision, int scale) {
context.keyword("cast(");
toSQL(context, getValue(), getType());
context.keyword(" as ")
.sql(type.length(length).precision(precision, scale).getCastTypeName(context))
.sql(")");
}
/**
* Get a bind variable, depending on value of
* {@link RenderContext#namedParams()}
*/
private final String getBindVariable(RenderContext context) {
if (context.namedParams()) {
int index = context.nextIndex();
if (StringUtils.isBlank(getParamName())) {
return ":" + index;
}
else {
return ":" + getName();
}
}
else {
return "?";
}
}
/**
* Inlining abstraction
*/
private void toSQL(RenderContext context, Object val) {
if (val == null) {
toSQL(context, val, Object.class);
}
else {
toSQL(context, val, val.getClass());
}
}
/**
* Inlining abstraction
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void toSQL(RenderContext context, Object val, Class<?> type) {
SQLDialect dialect = context.getDialect();
// [#650] Check first, if we have a converter for the supplied type
Converter<?, ?> converter = DataTypes.converter(type);
if (converter != null) {
val = ((Converter) converter).to(val);
type = converter.fromType();
}
if (isInline(context)) {
if (val == null) {
context.keyword("null");
}
else if (type == Boolean.class) {
// [#1153] Some dialects don't support boolean literals
// TRUE and FALSE
if (asList(ASE, DB2, FIREBIRD, ORACLE, SQLSERVER, SQLITE, SYBASE).contains(dialect)) {
context.sql(((Boolean) val) ? "1" : "0");
}
else {
context.keyword(val.toString());
}
}
// [#1154] Binary data cannot always be inlined
else if (type == byte[].class) {
byte[] binary = (byte[]) val;
if (asList(ASE, SQLSERVER, SYBASE).contains(dialect)) {
context.sql("0x")
.sql(convertBytesToHex(binary));
}
else if (dialect == DB2) {
context.keyword("blob")
.sql("(X'")
.sql(convertBytesToHex(binary))
.sql("')");
}
else if (asList(DERBY, H2, HSQLDB, INGRES, MYSQL, SQLITE).contains(dialect)) {
context.sql("X'")
.sql(convertBytesToHex(binary))
.sql("'");
}
else if (asList(ORACLE).contains(dialect)) {
context.keyword("hextoraw('")
.sql(convertBytesToHex(binary))
.sql("')");
}
else if (dialect == POSTGRES) {
context.sql("E'")
.sql(convertBytesToPostgresOctal(binary))
.keyword("'::bytea");
}
// This default behaviour is used in debug logging for dialects
// that do not support inlining binary data
else {
context.sql("X'")
.sql(convertBytesToHex(binary))
.sql("'");
}
}
// Interval extends Number, so let Interval come first!
else if (Interval.class.isAssignableFrom(type)) {
context.sql("'")
.sql(val.toString())
.sql("'");
}
else if (Number.class.isAssignableFrom(type)) {
context.sql(val.toString());
}
// [#1156] Date/Time data types should be inlined using JDBC
// escape syntax
else if (type == Date.class) {
// The SQLite JDBC driver does not implement the escape syntax
// [#1253] SQL Server and Sybase do not implement date literals
if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) {
context.sql("'").sql(val.toString()).sql("'");
}
// [#1253] Derby doesn't support the standard literal
else if (dialect == DERBY) {
context.keyword("date('").sql(val.toString()).sql("')");
}
// Most dialects implement SQL standard date literals
else {
context.keyword("date '").sql(val.toString()).sql("'");
}
}
else if (type == Timestamp.class) {
// The SQLite JDBC driver does not implement the escape syntax
// [#1253] SQL Server and Sybase do not implement timestamp literals
if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) {
context.sql("'").sql(val.toString()).sql("'");
}
// [#1253] Derby doesn't support the standard literal
else if (dialect == DERBY) {
context.keyword("timestamp('").sql(val.toString()).sql("')");
}
// CUBRID timestamps have no fractional seconds
else if (dialect == CUBRID) {
context.keyword("datetime '").sql(val.toString()).sql("'");
}
// Most dialects implement SQL standard timestamp literals
else {
context.keyword("timestamp '").sql(val.toString()).sql("'");
}
}
else if (type == Time.class) {
// The SQLite JDBC driver does not implement the escape syntax
// [#1253] SQL Server and Sybase do not implement time literals
if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) {
context.sql("'").sql(val.toString()).sql("'");
}
// [#1253] Derby doesn't support the standard literal
else if (dialect == DERBY) {
context.keyword("time('").sql(val.toString()).sql("')");
}
// [#1253] Oracle doesn't know time literals
else if (dialect == ORACLE) {
context.keyword("timestamp '1970-01-01 ").sql(val.toString()).sql("'");
}
// Most dialects implement SQL standard time literals
else {
context.keyword("time '").sql(val.toString()).sql("'");
}
}
else if (type.isArray()) {
// H2 renders arrays as rows
if (dialect == H2) {
context.sql(Arrays.toString((Object[]) val).replaceAll("\\[([^]]*)\\]", "($1)"));
}
// By default, render HSQLDB / POSTGRES syntax
else {
context.keyword("ARRAY")
.sql(Arrays.toString((Object[]) val));
}
}
else if (ArrayRecord.class.isAssignableFrom(type)) {
context.sql(val.toString());
}
else if (EnumType.class.isAssignableFrom(type)) {
toSQL(context, ((EnumType) val).getLiteral());
}
else if (UDTRecord.class.isAssignableFrom(type)) {
context.sql("[UDT]");
}
// Known fall-through types:
// - Blob, Clob (both not supported by jOOQ)
// - String
else {
context.sql("'")
.sql(val.toString().replace("'", "''"))
.sql("'");
}
}
// In Postgres, some additional casting must be done in some cases...
// TODO: Improve this implementation with [#215] (cast support)
else if (dialect == SQLDialect.POSTGRES) {
// Postgres needs explicit casting for array types
if (type.isArray() && byte[].class != type) {
context.sql(getBindVariable(context));
context.sql("::");
context.keyword(DefaultDataType.getDataType(dialect, type).getCastTypeName(context));
}
// ... and also for enum types
else if (EnumType.class.isAssignableFrom(type)) {
context.sql(getBindVariable(context));
// [#968] Don't cast "synthetic" enum types (note, val can be null!)
String name = ((EnumType) type.getEnumConstants()[0]).getName();
if (!StringUtils.isBlank(name)) {
context.sql("::");
context.literal(name);
}
}
else {
context.sql(getBindVariable(context));
}
}
else {
context.sql(getBindVariable(context));
}
}
@Override
public final void bind(BindContext context) {
// [#1302] Bind value only if it was not explicitly forced to be inlined
if (!isInline()) {
context.bindValue(getValue(), getType());
}
}
// ------------------------------------------------------------------------
// XXX: Param API
// ------------------------------------------------------------------------
@Override
public final void setValue(T value) {
setConverted(value);
}
@Override
public final void setConverted(Object value) {
this.value = getDataType().convert(value);
}
@Override
public final T getValue() {
return value;
}
@Override
public final String getParamName() {
return paramName;
}
@Override
public final void setInline(boolean inline) {
this.inline = inline;
}
@Override
public final boolean isInline() {
return inline;
}
private final boolean isInline(RenderContext context) {
return isInline() || context.inline();
}
/**
* 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);
}
/**
* Postgres uses octals instead of hex encoding
*/
private static final String convertBytesToPostgresOctal(byte[] binary) {
StringBuilder sb = new StringBuilder();
for (byte b : binary) {
sb.append("\\\\");
sb.append(leftPad(toOctalString(b), 3, '0'));
}
return sb.toString();
}
}