// Copyright 2010 NexJ Systems Inc. This software is licensed under the terms of the Eclipse Public License 1.0
package nexj.core.persistence.sql;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DataTruncation;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import nexj.core.meta.Attribute;
import nexj.core.meta.Component;
import nexj.core.meta.Metaclass;
import nexj.core.meta.Metadata;
import nexj.core.meta.MetadataException;
import nexj.core.meta.Primitive;
import nexj.core.meta.TypeConversionException;
import nexj.core.meta.UnaryFunction;
import nexj.core.meta.persistence.AttributeMapping;
import nexj.core.meta.persistence.ClassMapping;
import nexj.core.meta.persistence.Key;
import nexj.core.meta.persistence.PersistenceMapping;
import nexj.core.meta.persistence.Schema;
import nexj.core.meta.persistence.SchemaUpgrade;
import nexj.core.meta.persistence.sql.Column;
import nexj.core.meta.persistence.sql.Index;
import nexj.core.meta.persistence.sql.IndexColumn;
import nexj.core.meta.persistence.sql.RelationalClassDenorm;
import nexj.core.meta.persistence.sql.RelationalClassMapping;
import nexj.core.meta.persistence.sql.RelationalDatabase;
import nexj.core.meta.persistence.sql.RelationalDatabaseFragment;
import nexj.core.meta.persistence.sql.RelationalMapping;
import nexj.core.meta.persistence.sql.RelationalPrimitiveDenorm;
import nexj.core.meta.persistence.sql.RelationalPrimitiveMapping;
import nexj.core.meta.persistence.sql.RelationalSchema;
import nexj.core.meta.persistence.sql.Table;
import nexj.core.meta.persistence.sql.upgrade.RelationalSchemaUpgrade;
import nexj.core.meta.persistence.sql.upgrade.RelationalSchemaUpgradeState;
import nexj.core.meta.upgrade.UpgradeState;
import nexj.core.persistence.CalendarFactory;
import nexj.core.persistence.Converter;
import nexj.core.persistence.Cursor;
import nexj.core.persistence.DeadlockException;
import nexj.core.persistence.DuplicateKeyException;
import nexj.core.persistence.Field;
import nexj.core.persistence.GenericPersistenceAdapter;
import nexj.core.persistence.LockTimeoutException;
import nexj.core.persistence.OID;
import nexj.core.persistence.OIDGenerator;
import nexj.core.persistence.OIDHolder;
import nexj.core.persistence.Operator;
import nexj.core.persistence.PersistenceException;
import nexj.core.persistence.Query;
import nexj.core.persistence.QueryTimeoutException;
import nexj.core.persistence.SchemaVersion;
import nexj.core.persistence.Source;
import nexj.core.persistence.ValueRangeException;
import nexj.core.persistence.Work;
import nexj.core.persistence.operator.AggregateOperator;
import nexj.core.persistence.operator.AttributeOperator;
import nexj.core.persistence.operator.FunctionOperator;
import nexj.core.persistence.sql.SQLGenerator.OperatorAppender;
import nexj.core.runtime.Instance;
import nexj.core.runtime.InstanceList;
import nexj.core.runtime.UnitOfWork;
import nexj.core.runtime.ValidationException;
import nexj.core.scripting.Pair;
import nexj.core.util.Binary;
import nexj.core.util.Cancellable;
import nexj.core.util.HashHolder;
import nexj.core.util.Logger;
import nexj.core.util.PropertyMap;
import nexj.core.util.StringId;
import nexj.core.util.StringUtil;
import nexj.core.util.TZ;
import nexj.core.util.Undefined;
/**
* SQL persistence adapter.
*/
public abstract class SQLAdapter extends GenericPersistenceAdapter implements Cancellable
{
// constants
protected final static Bind BIND_STRING = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.VARCHAR);
}
else
{
stmt.setString(nOrdinal + 1, (String)value);
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
return rs.getString(nOrdinal + 1);
}
};
protected final static Bind BIND_CHAR = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.CHAR);
}
else
{
stmt.setString(nOrdinal + 1, (String)value);
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
String s = rs.getString(nOrdinal + 1);
if (s != null)
{
int i = s.length();
while (i > 0 && s.charAt(i - 1) == ' ')
{
--i;
}
if (i < s.length())
{
s = s.substring(0, i);
}
}
return s;
}
};
protected final static Bind BIND_CLOB = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.CLOB);
}
else
{
stmt.setString(nOrdinal + 1, (String)value);
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
Clob clob = rs.getClob(nOrdinal + 1);
if (clob == null)
{
return null;
}
return clob.getSubString(1, (int)clob.length());
}
};
protected final static Bind BIND_BINARY = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.VARBINARY);
}
else
{
stmt.setBytes(nOrdinal + 1, ((Binary)value).getData());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
byte[] data = rs.getBytes(nOrdinal + 1);
if (data == null)
{
return null;
}
return new Binary(data);
}
};
protected final static Bind BIND_BLOB = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.BLOB);
}
else
{
stmt.setBytes(nOrdinal + 1, ((Binary)value).getData());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
Blob blob = rs.getBlob(nOrdinal + 1);
if (blob == null)
{
return null;
}
return new Binary(blob.getBytes(1L, (int)blob.length()));
}
};
protected final static Bind BIND_INTEGER = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.INTEGER);
}
else
{
stmt.setInt(nOrdinal + 1, ((Number)value).intValue());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
int n = rs.getInt(nOrdinal + 1);
if (rs.wasNull())
{
return null;
}
return Primitive.createInteger(n);
}
};
protected final static Bind BIND_LONG = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.BIGINT);
}
else
{
stmt.setLong(nOrdinal + 1, ((Number)value).longValue());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
long l = rs.getLong(nOrdinal + 1);
if (rs.wasNull())
{
return null;
}
return Primitive.createLong(l);
}
};
protected final static Bind BIND_DECIMAL = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.DECIMAL);
}
else if (value instanceof BigDecimal)
{
stmt.setBigDecimal(nOrdinal + 1, (BigDecimal)value);
}
else
{
stmt.setBigDecimal(nOrdinal + 1, Primitive.toDecimal(value));
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
BigDecimal dec = rs.getBigDecimal(nOrdinal + 1);
return (dec == null) ? null : dec.stripTrailingZeros();
}
};
protected final static Bind BIND_FLOAT = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.REAL);
}
else
{
stmt.setFloat(nOrdinal + 1, ((Number)value).floatValue());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
float f = rs.getFloat(nOrdinal + 1);
if (rs.wasNull())
{
return null;
}
return Primitive.createFloat(f);
}
};
protected final static Bind BIND_DOUBLE = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.DOUBLE);
}
else
{
stmt.setDouble(nOrdinal + 1, ((Number)value).doubleValue());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
double d = rs.getDouble(nOrdinal + 1);
if (rs.wasNull())
{
return null;
}
return Primitive.createDouble(d);
}
};
protected final static Bind BIND_TIMESTAMP = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.TIMESTAMP);
}
else
{
stmt.setTimestamp(nOrdinal + 1, (Timestamp)value, adapter.getCalendar());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
return rs.getTimestamp(nOrdinal + 1, adapter.getCalendar());
}
};
protected final static Bind BIND_BOOLEAN = new Bind()
{
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.BIT);
}
else
{
stmt.setBoolean(nOrdinal + 1, ((Boolean)value).booleanValue());
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
boolean b = rs.getBoolean(nOrdinal + 1);
if (rs.wasNull())
{
return null;
}
return Boolean.valueOf(b);
}
};
protected static Method s_unwrap;
protected static Method s_getUnderlyingConnection;
protected static Class s_wsJDBCConnection;
protected static Method s_getNativeConnection;
static
{
try
{
s_unwrap = // Java 1.6 java.sql.Wrapper
Class.forName("java.sql.Wrapper").getDeclaredMethod("unwrap", new Class[]{Class.class});
}
catch (Throwable t)
{
}
try
{
s_getUnderlyingConnection =
Class.forName("org.jboss.resource.adapter.jdbc.WrappedConnection")
.getDeclaredMethod("getUnderlyingConnection", null);
}
catch (Throwable t)
{
}
try
{
s_wsJDBCConnection = Class.forName("com.ibm.ws.rsadapter.jdbc.WSJdbcConnection");
s_getNativeConnection =
Class.forName("com.ibm.ws.rsadapter.jdbc.WSJdbcUtil")
.getDeclaredMethod("getNativeConnection", new Class[]{s_wsJDBCConnection});
}
catch (Throwable t)
{
}
}
// attributes
/**
* The maximum batch data size in bytes (non-positive for unlimited).
*/
protected int m_nMaxBatchDataSize = 65536;
/**
* The unicode string flag.
*/
protected boolean m_bUnicode = true;
/**
* The literal bind flag. Set to true to disable bind parameters and use
* literal binds in all SQL select queries.
*/
protected boolean m_bLiteral;
/**
* True if debug logging is enabled.
*/
protected boolean m_bDebug;
// associations
/**
* The connection factory.
*/
protected SQLConnectionFactory m_connectionFactory;
/**
* The SQL hook.
*/
protected SQLHook m_sqlHook;
/**
* The SQL work item lookup key.
*/
protected SQLWork m_workKey;
/**
* The calendar for timestamp conversion.
*/
protected Calendar m_calendar;
/**
* The statement being executed.
*/
protected Statement m_statement;
/**
* Log statement list for the deferred exception logger.
*/
protected List m_logList;
/**
* Cached supported operator visitor.
*/
protected SupportedOperatorVisitor m_sopVisitor;
/**
* Bind factory array: BindFactory[nPrimitiveOrdinal].
*/
protected final static BindFactory[] s_bindFactoryArray = new BindFactory[Primitive.MAX_COUNT];
static
{
s_bindFactoryArray[Primitive.STRING_ORDINAL] = new BindFactory()
{
public Bind create(Column column)
{
if (column.getAllocation() == Column.LOCATOR)
{
return BIND_CLOB;
}
if (column.getAllocation() == Column.FIXED)
{
return BIND_CHAR;
}
return BIND_STRING;
}
public Bind create(Primitive type)
{
return BIND_STRING;
}
};
s_bindFactoryArray[Primitive.BINARY_ORDINAL] = new BindFactory()
{
public Bind create(Column column)
{
if (column.getAllocation() == Column.LOCATOR)
{
return BIND_BLOB;
}
return BIND_BINARY;
}
public Bind create(Primitive type)
{
return BIND_BINARY;
}
};
s_bindFactoryArray[Primitive.INTEGER_ORDINAL] = new SimpleBindFactory(BIND_INTEGER);
s_bindFactoryArray[Primitive.LONG_ORDINAL] = new SimpleBindFactory(BIND_LONG);
s_bindFactoryArray[Primitive.DECIMAL_ORDINAL] = new SimpleBindFactory(BIND_DECIMAL);
s_bindFactoryArray[Primitive.FLOAT_ORDINAL] = new SimpleBindFactory(BIND_FLOAT);
s_bindFactoryArray[Primitive.DOUBLE_ORDINAL] = new SimpleBindFactory(BIND_DOUBLE);
s_bindFactoryArray[Primitive.TIMESTAMP_ORDINAL] = new BindFactory()
{
public Bind create(final Column column)
{
if (column.isTimeZoned())
{
return new Bind()
{
protected Calendar m_calendar = ((CalendarFactory)column.getConverter().getInstance(null)).createCalendar();
public void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException
{
if (value == null)
{
stmt.setNull(nOrdinal + 1, Types.TIMESTAMP);
}
else
{
stmt.setTimestamp(nOrdinal + 1, (Timestamp)value, m_calendar);
}
}
public Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException
{
return rs.getTimestamp(nOrdinal + 1, m_calendar);
}
};
}
return BIND_TIMESTAMP;
}
public Bind create(Primitive type)
{
return BIND_TIMESTAMP;
}
};
s_bindFactoryArray[Primitive.BOOLEAN_ORDINAL] = new SimpleBindFactory(BIND_BOOLEAN);
s_bindFactoryArray[Primitive.ANY_ORDINAL] = new BindFactory()
{
public Bind create(Column column)
{
throw new MetadataException("err.meta.columnBindType", new Object[]{Primitive.ANY.getName()});
}
public Bind create(Primitive type)
{
throw new MetadataException("err.meta.columnBindType", new Object[]{Primitive.ANY.getName()});
}
};
}
/**
* 2d array of converters: Converter[nFromTypeOrdinal * Primitive.MAX_COUNT + nToTypeOrdinal].
*/
protected final static Converter[] s_converterArray = new Converter[Primitive.MAX_COUNT * Primitive.MAX_COUNT];
static
{
for (int i = 0; i < Primitive.MAX_COUNT; ++i)
{
for (int k = 0; k < Primitive.MAX_COUNT; ++k)
{
if (i != k)
{
Primitive fromType = Primitive.get(i);
Primitive toType = Primitive.get(k);
UnaryFunction forward = toType.findConverter(fromType);
if (forward != null)
{
UnaryFunction inverse = fromType.findConverter(toType);
if (inverse != null)
{
s_converterArray[i * Primitive.MAX_COUNT + k] =
new SQLGenericConverter(fromType, toType, forward, inverse,
Primitive.isOrderPreserved(fromType, toType));
}
}
}
}
}
}
/**
* Retrieve OperatorAppender suitable for use of the specific operator with this adapter.
* @param operator The operator to retrieve the appender for.
* @return The preferred operator appender on null of no override required.
*/
public OperatorAppender findOperatorAppender(Operator operator)
{
return null;
}
/**
* The class logger.
*/
private final static Logger s_logger = Logger.getLogger(SQLAdapter.class);
/**
* The deferred exception SQL logger.
*/
private final static Logger s_xlogger = Logger.getLogger(SQLAdapter.class.getName() + ".exception");
// constructors
/**
* Constructs the adapter.
*/
protected SQLAdapter()
{
m_bDebug = s_logger.isDebugEnabled();
if (!m_bDebug)
{
m_bDebug = s_xlogger.isDebugEnabled();
if (m_bDebug)
{
m_logList = new ArrayList();
}
}
}
// operations
/**
* Sets the connection factory.
* @param connectionFactory The connection factory to set.
*/
public void setConnectionFactory(SQLConnectionFactory connectionFactory)
{
m_connectionFactory = connectionFactory;
}
/**
* @return The connection factory.
*/
public SQLConnectionFactory getConnectionFactory()
{
return m_connectionFactory;
}
/**
* Sets the SQL hook.
* @param sqlHook The SQL hook to set.
*/
public void setSQLHook(SQLHook sqlHook)
{
m_sqlHook = sqlHook;
}
/**
* @return The SQL hook.
*/
public SQLHook getSQLHook()
{
return m_sqlHook;
}
/**
* Sets the maximum batch data size in bytes.
* @param nMaxBatchDataSize The maximum batch data size in bytes to set (non-positive for unlimited).
*/
public void setMaxBatchDataSize(int nMaxBatchDataSize)
{
m_nMaxBatchDataSize = nMaxBatchDataSize;
}
/**
* @return The maximum batch data size in bytes (non-positive for unlimited).
*/
public int getMaxBatchDataSize()
{
return m_nMaxBatchDataSize;
}
/**
* Sets the unicode string flag.
* @param bUnicode The unicode string flag to set.
*/
public void setUnicode(boolean bUnicode)
{
m_bUnicode = bUnicode;
}
/**
* @return The unicode string flag.
*/
public boolean isUnicode()
{
return m_bUnicode;
}
/**
* Sets the literal bind flag.
* @param bLiteral True to disable bind parameters and use literal binds
* in all SQL select queries.
*/
public void setLiteral(boolean bLiteral)
{
m_bLiteral = bLiteral;
}
/**
* Gets the literal bind flag.
*
* @return The literal bind flag; true to disable bind parameters and use
* literal binds in all SQL select queries.
*/
public boolean isLiteral()
{
return m_bLiteral;
}
/**
* @return True if debug logging is enabled.
*/
public final boolean isDebug()
{
return m_bDebug;
}
/**
* Can the specified query use a shared SQL connection.
* @param query The query to check.
* @return The specific query can use a share an SQL connection with other queries.
*/
protected boolean isSharedConnection(Query query)
{
return true;
}
/**
* Gets an SQL connection resource.
* Each invocation of this method must be paired with a decRef() on the resource.
* @return An SQL connection resource for the current UOW.
* @throws SQLException if an error occurs.
*/
public SQLConnection getConnection() throws SQLException
{
return getConnection(null, true);
}
/**
* Gets an SQL connection resource.
* Each invocation of this method must be paired with a decRef() on the resource.
* @param fragment The data source fragment. Can be null.
* @param bShareable True if the connection is shareable.
* @return An SQL connection resource for the current UOW.
* @throws SQLException if an error occurs.
*/
public SQLConnection getConnection(RelationalDatabaseFragment fragment, boolean bShareable) throws SQLException
{
String sFragmentName = (fragment != null) ? fragment.getName() :
m_context.getUnitOfWork().getFragmentName(m_bFragmented);
SQLConnection sqlcon = (SQLConnection)m_context.getUnitOfWork()
.findResource(this, sFragmentName, null, bShareable);
if (sqlcon == null)
{
Connection con = ((fragment == null) ? m_connectionFactory :
((SQLConnectionFactory)fragment.getConnectionFactory().getInstance(m_context))).getConnection(this);
if (m_sqlHook != null)
{
con = new SQLHookConnection(con, m_sqlHook);
}
sqlcon = new SQLConnection(this, con);
sqlcon.setFragmentName(sFragmentName);
sqlcon.setShareable(bShareable);
m_context.getUnitOfWork().addResource(sqlcon);
}
sqlcon.incRef();
return sqlcon;
}
/**
* Closes an SQL connection.
* @param connection The connection to close. Can be null.
*/
public void close(Connection connection)
{
if (connection != null)
{
try
{
connection.close();
}
catch (SQLException e)
{
s_logger.debug("Unable to close the SQL connection", e);
}
}
}
/**
* Closes an SQL statement.
* @param stmt The statement to close. Can be null.
*/
public void close(Statement stmt)
{
if (stmt != null)
{
try
{
stmt.close();
}
catch (SQLException e)
{
s_logger.debug("Unable to close the SQL statement", e);
}
}
}
/**
* Closes a result set.
* @param rs The result set to close. Can be null.
*/
public void close(ResultSet rs)
{
if (rs != null)
{
try
{
rs.close();
}
catch (SQLException e)
{
s_logger.debug("Unable to close the result set", e);
}
}
}
/**
* Cancels an SQL statement.
* @param stmt The statement to cancel. Can be null.
*/
public void cancel(Statement stmt)
{
if (stmt != null)
{
try
{
stmt.cancel();
}
catch (SQLException e)
{
s_logger.debug("Unable to cancel the SQL statement", e);
}
}
}
/**
* Cancels the currently executing statement.
* @see Cancellable#cancel()
*/
public void cancel()
{
Statement stmt;
synchronized (this)
{
stmt = m_statement;
}
cancel(stmt);
}
/**
* Creates and initializes a Statement.
* @param connection The Connection on which the Statement should be created.
* @param sSQL The SQL to execute.
* @param query The Query being executed.
* @return The Statement to execute.
* @throws SQLException If an error occurs.
*/
public PreparedStatement prepareStatement(Connection connection, String sSQL, Query query) throws SQLException
{
return connection.prepareStatement(sSQL);
}
/**
* Executes an SQL query.
* @param stmt The statement to execute.
* @see PreparedStatement#executeQuery();
*/
public ResultSet executeQuery(PreparedStatement stmt) throws SQLException
{
Cancellable cancellableSaved = null;
try
{
synchronized (this)
{
m_statement = stmt;
}
cancellableSaved = m_context.setCancellable(this);
return stmt.executeQuery();
}
finally
{
m_context.setCancellable(cancellableSaved);
synchronized (this)
{
m_statement = null;
}
}
}
/**
* Executes an SQL update.
* @param stmt The statement to execute.
* @return The affected record count.
* @see PreparedStatement#executeUpdate();
*/
public int executeUpdate(PreparedStatement stmt) throws SQLException
{
Cancellable cancellableSaved = null;
try
{
synchronized (this)
{
m_statement = stmt;
}
cancellableSaved = m_context.setCancellable(this);
return stmt.executeUpdate();
}
finally
{
m_context.setCancellable(cancellableSaved);
synchronized (this)
{
m_statement = null;
}
}
}
/**
* Executes an SQL batch.
* @param stmt The statement to execute.
* @return Affected record counts and status information.
* @see Statement#executeBatch();
*/
public int[] executeBatch(Statement stmt) throws SQLException
{
Cancellable cancellableSaved = null;
try
{
synchronized (this)
{
m_statement = stmt;
}
cancellableSaved = m_context.setCancellable(this);
return stmt.executeBatch();
}
finally
{
m_context.setCancellable(cancellableSaved);
synchronized (this)
{
m_statement = null;
}
}
}
/**
* @see nexj.core.persistence.PersistenceMapper#mapQuery(nexj.core.persistence.Query)
*/
public void mapQuery(Query query)
{
if (query.isRoot())
{
query.optimizeJoins();
}
SQLGenerator generator = new SQLGenerator(query, this);
query.setGenerator(generator);
generator.addMappings();
}
/**
* @see nexj.core.persistence.PersistenceAdapter#openCursor(nexj.core.persistence.Query)
*/
public Cursor openCursor(Query query)
{
return new SQLCursor(query);
}
/**
* @see nexj.core.persistence.PersistenceAdapter#execute(nexj.core.persistence.Work[], int, int)
*/
public void execute(Work[] workArray, int nStart, int nEnd)
{
SQLWork work = null;
SQLConnection connection = null;
PreparedStatement stmt = null;
long lStartTime = 0;
long lEndTime = 0;
int nBatch = nStart;
int nDataSize = 0;
int nTotalSize = 0;
boolean bBatchable = true;
boolean bDebug = s_logger.isDebugEnabled();
try
{
clearLog();
for (;;)
{
if (stmt != null)
{
boolean bExecute = (nStart >= nEnd || !isBatchSupported());
if (!bExecute)
{
bExecute = !bBatchable;
bBatchable = ((SQLWork)workArray[nStart]).isBatchable();
if (bBatchable)
{
nDataSize = ((SQLWork)workArray[nStart]).getDataSize();
}
bExecute |= !bBatchable;
if (!bExecute)
{
bExecute = (workArray[nStart].compareTo(workArray[nBatch]) != 0);
if (!bExecute)
{
bExecute |= m_nMaxBatchDataSize >= 0 && m_nMaxBatchDataSize - nTotalSize < nDataSize;
}
}
}
if (bExecute)
{
if (nStart - nBatch > 1)
{
stmt.addBatch();
}
if (bDebug)
{
lStartTime = System.currentTimeMillis();
}
work.execute(stmt, workArray, nBatch, nStart);
if (bDebug)
{
lEndTime = System.currentTimeMillis();
s_logger.debug("SQL execution time: " + (lEndTime - lStartTime) + " ms");
}
nBatch = nStart;
nTotalSize = 0;
close(stmt);
stmt = null;
}
nTotalSize += nDataSize;
}
if (nStart >= nEnd)
{
break;
}
if (work == null && isBatchSupported())
{
work = (SQLWork)workArray[nStart];
bBatchable = work.isBatchable();
if (bBatchable)
{
nDataSize = work.getDataSize();
}
}
work = (SQLWork)workArray[nStart++];
if (stmt == null)
{
String sSQL = work.getSQL();
log(sSQL);
if (connection == null)
{
connection = getConnection();
}
if (work.isCallable())
{
stmt = connection.getConnection().prepareCall(sSQL);
}
else
{
stmt = connection.getConnection().prepareStatement(sSQL);
}
}
else
{
stmt.addBatch();
}
if (bDebug)
{
int nIndex = nStart - nBatch - 1;
if (nIndex != 0)
{
s_logger.debug("--- Batch " + nIndex + " ---");
}
}
work.prepare();
work.bind(stmt, (SQLWork)workArray[nBatch]);
if (isBatchSupported() && bBatchable)
{
nDataSize = work.getDataSize();
}
}
}
catch (SQLException e)
{
flushLog(e);
throw getException(e, workArray, nBatch, nStart);
}
finally
{
close(stmt);
if (connection != null)
{
connection.decRef();
}
clearLog();
}
}
/**
* @see nexj.core.persistence.PersistenceAdapter#addDependency(nexj.core.runtime.UnitOfWork, nexj.core.persistence.Work, nexj.core.runtime.Instance, nexj.core.meta.persistence.Key, nexj.core.meta.persistence.Key, boolean)
*/
public void addDependency(UnitOfWork uow, Work work, Instance instance, Key dstKey, Key srcKey, boolean bSuccessor)
{
if (instance.getUnitOfWork() != uow)
{
return;
}
int nType;
switch (instance.getState())
{
case Instance.NEW:
nType = SQLWork.INSERT;
break;
case Instance.DIRTY:
nType = SQLWork.UPDATE;
break;
case Instance.DELETED:
if (instance.getOID() != null)
{
nType = SQLWork.DELETE;
break;
}
default:
return;
}
Table table;
if (bSuccessor)
{
table = ((Index)dstKey).getTable();
}
else
{
table = ((RelationalMapping)instance.getPersistenceMapping()).getPrimaryTable();
}
SQLWork sqlWork = findWork(uow, instance, table);
if (sqlWork == null)
{
sqlWork = addWork(uow, nType, instance, table);
if (bSuccessor)
{
Table primaryTable = ((RelationalMapping)instance.getPersistenceMapping()).getPrimaryTable();
// Add a secondary table as successor to the primary one
if (table != primaryTable)
{
if (instance.getOID() != null)
{
sqlWork.setOID();
}
else
{
getWork(uow, nType, instance, primaryTable).addSuccessor(sqlWork, table.getPrimaryKey(), primaryTable.getPrimaryKey());
}
}
}
}
if (bSuccessor)
{
if (nType == SQLWork.INSERT)
{
work.addSuccessor(sqlWork, dstKey, srcKey);
}
}
else
{
sqlWork.addSuccessor(work, dstKey, srcKey);
}
}
/**
* @see nexj.core.persistence.PersistenceAdapter#addWork(nexj.core.runtime.UnitOfWork, nexj.core.runtime.Instance)
*/
public void addWork(UnitOfWork uow, Instance instance)
{
switch (instance.getState())
{
case Instance.NEW:
addInsert(uow, instance);
break;
case Instance.DIRTY:
addUpdate(uow, instance);
break;
case Instance.DELETED:
addDelete(uow, instance);
break;
default:
throw new IllegalStateException();
}
}
/**
* @see nexj.core.persistence.GenericPersistenceAdapter#addPrimitiveWork(nexj.core.runtime.UnitOfWork, nexj.core.runtime.Instance, nexj.core.persistence.Work, nexj.core.meta.persistence.AttributeMapping, java.lang.Object)
*/
protected Work addPrimitiveWork(UnitOfWork uow, Instance instance,
Work primaryWork, AttributeMapping attributeMapping, Object value)
{
RelationalMapping mapping = (RelationalMapping)instance.getPersistenceMapping();
RelationalPrimitiveMapping primitiveMapping = (RelationalPrimitiveMapping)attributeMapping;
Column column = primitiveMapping.getColumn();
int nType = primaryWork.getType();
SQLWork work = null;
if (nType == SQLWork.UPDATE || value != null || column.getTable() == mapping.getPrimaryTable())
{
work = findWork(uow, instance, column.getTable());
if (work == null)
{
if (nType == SQLWork.UPDATE)
{
((SQLUpdate)primaryWork).touch();
}
work = addWork(uow, nType, instance, column.getTable());
primaryWork.addSuccessor(work, column.getTable().getPrimaryKey(), mapping.getObjectKey());
}
work.setValue(column, column.getValueType().convert(value));
}
return work;
}
/**
* @see nexj.core.persistence.GenericPersistenceAdapter#addClassWork(nexj.core.runtime.UnitOfWork, nexj.core.runtime.Instance, nexj.core.persistence.Work, nexj.core.meta.persistence.ClassMapping)
*/
protected Work addClassWork(UnitOfWork uow, Instance instance,
Work primaryWork, ClassMapping assocMapping)
{
PersistenceMapping mapping = instance.getPersistenceMapping();
RelationalClassMapping relAssocMapping = (RelationalClassMapping)assocMapping;
SQLWork work = findWork(uow, instance, relAssocMapping.getSourceKey().getTable());
if (work == null)
{
if (primaryWork.getType() == SQLWork.UPDATE)
{
((SQLUpdate)primaryWork).touch();
}
work = addWork(uow, primaryWork.getType(), instance, relAssocMapping.getSourceKey().getTable());
primaryWork.addSuccessor(work, relAssocMapping.getSourceKey().getTable().getPrimaryKey(), mapping.getObjectKey());
}
return work;
}
/**
* @see nexj.core.persistence.GenericPersistenceAdapter#isCreateSuccessor(nexj.core.persistence.Work, nexj.core.meta.persistence.ClassMapping, boolean)
*/
protected boolean isCreateSuccessor(Work work, ClassMapping assocMapping, boolean bOID)
{
SQLWork primaryWork = (SQLWork)work;
if (assocMapping.getKey(false).isObjectKeyPart())
{
if (bOID)
{
return true;
}
if (primaryWork.isIdentity())
{
return true;
}
if (assocMapping.getKey(true).isObjectKeyPart())
{
return false;
}
throw new PersistenceException("err.persistence.requiredOID",
new Object[]{assocMapping.getPersistenceMapping().getMetaclass().getName()});
}
return false;
}
/**
* Adds insert work items to the unit of work.
* @param uow The unit of work.
* @param instance The instance for which to add the work items.
*/
protected void addInsert(UnitOfWork uow, Instance instance)
{
RelationalMapping relMapping = (RelationalMapping)instance.getPersistenceMapping();
Table primaryTable = relMapping.getPrimaryTable();
SQLWork primaryWork = getWork(uow, SQLWork.INSERT, instance, primaryTable);
OID oid = instance.getOID();
if (oid != null)
{
primaryWork.setOID();
}
else
{
Component component = relMapping.getKeyGenerator();
if (component == RelationalMapping.KEY_GEN_IDENTITY)
{
primaryWork.setIdentity(true);
}
else if (component != null)
{
oid = ((OIDGenerator)component.getInstance(uow.getInvocationContext())).generateOID(instance, this);
Object[] values = oid.getValueArray();
Index pk = primaryTable.getPrimaryKey();
int nCount = pk.getIndexColumnCount();
if (nCount != values.length)
{
throw new PersistenceException("err.persistence.oidValueCount",
new Object[]{component.getName(), Primitive.createInteger(nCount),
Primitive.createInteger(values.length)});
}
for (int i = 0; i < nCount; ++i)
{
values[i] = pk.getIndexColumn(i).getColumn().getValueType().convert(values[i]);
}
instance.setOID(oid);
primaryWork.setOID();
}
else
{
Metaclass metaclass = instance.getMetaclass();
for (int i = 0, n = 0, nCount = metaclass.getInstanceAttributeCount(); i < nCount; ++i)
{
Attribute attribute = metaclass.getInstanceAttribute(i);
AttributeMapping mapping = relMapping.getAttributeMapping(attribute);
int nPrimaryColCount = primaryTable.getPrimaryKey().getIndexColumnCount();
if (mapping instanceof RelationalPrimitiveMapping)
{
Column column = ((RelationalPrimitiveMapping)mapping).getColumn();
if (column.isPrimary())
{
primaryWork.setValue(column, column.getValueType().convert(instance.getValue(i)));
if (++n >= nPrimaryColCount)
{
break;
}
}
}
else if (mapping instanceof RelationalClassMapping)
{
Index index = ((RelationalClassMapping)mapping).getSourceKey();
if (index.isObjectKeyPart())
{
Object value = instance.getValue(i);
if (value instanceof OIDHolder)
{
oid = ((OIDHolder)value).getOID();
if (oid != null )
{
int nIndexColCount = index.getIndexColumnCount();
if (nIndexColCount == oid.getCount())
{
for (int k = 0; k < nIndexColCount; ++k)
{
Column column = index.getIndexColumn(k).getColumn();
if (column.isPrimary())
{
primaryWork.setValue(column, column.getValueType().convert(oid.getValue(k)));
if (++n >= nPrimaryColCount)
{
break;
}
}
}
}
}
}
}
}
}
oid = primaryWork.computeOID();
if (oid != null)
{
instance.setOID(oid);
}
}
}
addCreateDependencies(uow, instance, primaryWork);
}
/**
* Adds update work items to the unit of work.
* @param uow The unit of work.
* @param instance The instance for which to add the work items.
*/
protected void addUpdate(UnitOfWork uow, Instance instance)
{
RelationalMapping relMapping = (RelationalMapping)instance.getPersistenceMapping();
Table primaryTable = relMapping.getPrimaryTable();
SQLUpdate work = (SQLUpdate)getWork(uow, SQLWork.UPDATE, instance, primaryTable);
work.setupLocking();
addUpdateDependencies(uow, instance, work);
}
/**
* Adds deletion work items to the unit of work.
* @param uow The unit of work.
* @param instance The instance for which to add the work items.
*/
protected void addDelete(UnitOfWork uow, Instance instance)
{
assert instance.getOID() != null;
RelationalMapping relMapping = (RelationalMapping)instance.getPersistenceMapping();
SQLWork primaryWork = getWork(uow, SQLWork.DELETE, instance, relMapping.getPrimaryTable());
primaryWork.setOID();
primaryWork.setupLocking();
int nCount = relMapping.getTableCount();
for (int i = 1; i < nCount; ++i)
{
SQLWork work = getWork(uow, SQLWork.DELETE, instance, relMapping.getTable(i));
work.setOID();
primaryWork.addSuccessor(work, null, null);
}
}
/**
* @see nexj.core.persistence.PersistenceAdapter#addDenorm(nexj.core.runtime.UnitOfWork, nexj.core.runtime.Instance, nexj.core.meta.persistence.AttributeMapping)
*/
public void addDenorm(UnitOfWork uow, Instance instance, AttributeMapping mapping) throws PersistenceException
{
Attribute attribute = mapping.getAttribute();
Object value = instance.getValueDirect(attribute.getOrdinal());
if (mapping instanceof RelationalPrimitiveMapping)
{
for (int i = 0, n = mapping.getDenormCount(); i < n; ++i)
{
RelationalPrimitiveDenorm denorm = (RelationalPrimitiveDenorm)mapping.getDenorm(i);
Column column = denorm.getColumn();
SQLWork work = findWork(uow, instance, column.getTable());
if (work != null)
{
work.setValue(column, value);
}
}
}
else
{
Instance assoc = null;
if (value instanceof InstanceList)
{
InstanceList list = (InstanceList)value;
if (!list.isEmpty())
{
assoc = list.getInstance(0);
}
}
else
{
assoc = (Instance)value;
}
for (int i = 0, n = mapping.getDenormCount(); i < n; ++i)
{
RelationalClassDenorm denorm = (RelationalClassDenorm)mapping.getDenorm(i);
Key dstKey = ((RelationalClassMapping)denorm.getMapping()).getDestinationKey();
SQLWork work = findWork(uow, instance, denorm.getSourceKey().getTable());
if (work != null)
{
if (assoc != null && assoc.getOID() == null)
{
addDependency(uow, work, assoc, denorm.getSourceKey(), dstKey, true);
}
else
{
work.setKeyValue(denorm.getSourceKey(), dstKey, assoc);
}
}
}
}
}
/**
* Finds an SQL work item in the unit of work.
* @param uow The unit of work.
* @param instance The instance for which to find the work item.
* @param table The table for which to find the work item.
* @return The found work item, or null if not found.
*/
public SQLWork findWork(UnitOfWork uow, Instance instance, Table table)
{
if (m_workKey == null)
{
m_workKey = new SQLWork(instance, table, this)
{
public int getType()
{
return -1;
}
public String getSQL()
{
return null;
}
public void bind(PreparedStatement stmt, SQLWork proto)
{
}
public boolean isBatchable()
{
return false;
}
public void execute(PreparedStatement stmt, Work[] workArray, int nStart, int nEnd) throws SQLException
{
}
};
}
else
{
m_workKey.setData(instance, table);
}
return (SQLWork)uow.findWork(m_workKey);
}
/**
* Gets an SQL work item in the unit of work, or creates a new one of not found.
* @param uow The unit of work.
* @param nType The work item type.
* @param instance The instance for which to find the work item.
* @param table The table for which to find the work item.
* @return The work item.
*/
public SQLWork getWork(UnitOfWork uow, int nType, Instance instance, Table table)
{
SQLWork work = findWork(uow, instance, table);
if (work == null)
{
work = addWork(uow, nType, instance, table);
}
return work;
}
/**
* Factory method to create a work item of a given type and add it to the UOW.
* @param uow The unit of work.
* @param nType The work item type.
* @param instance The instance for which to find the work item.
* @param table The table for which to find the work item.
* @return The created work item.
*/
protected SQLWork addWork(UnitOfWork uow, int nType, Instance instance, Table table)
{
SQLWork work;
switch (nType)
{
case SQLWork.INSERT:
work = new SQLInsert(instance, table, this);
break;
case SQLWork.UPDATE:
work = new SQLUpdate(instance, table, this);
break;
case SQLWork.DELETE:
work = new SQLDelete(instance, table, this);
break;
default:
throw new IllegalArgumentException();
}
uow.addWork(work);
return work;
}
/**
* Gets a default type converter.
* @param fromType The type from which to convert.
* @param toType The type to which to convert.
* @throws TypeConversionException if the conversion is not supported.
*/
public Converter getDefaultConverter(Primitive fromType, Primitive toType) throws TypeConversionException
{
Converter converter = s_converterArray[fromType.getOrdinal() * Primitive.MAX_COUNT + toType.getOrdinal()];
if (converter == null)
{
throw new TypeConversionException(toType);
}
return converter;
}
/**
* Gets a type converter for the specified column.
* @param type The type to which to convert.
* @param column The column
* @return The converter instance, or null if no conversion is needed.
*/
protected final Converter getConverter(Primitive type, Column column)
{
if (column.getConverter() == null)
{
type = getConversionMapper().getType(type);
Primitive colType = getConversionMapper().getType(column.getType());
if (type != colType)
{
return getDefaultConverter(colType, type);
}
return null;
}
return (Converter)column.getConverter().getInstance(null);
}
/**
* Adds the field columns to a set.
* @param field The field.
* @param columnSet The destination column set.
*/
protected void addColumns(Field field, Set columnSet)
{
RelationalPrimitiveMapping mapping = (RelationalPrimitiveMapping)field.getAttributeMapping();
if (mapping != null)
{
columnSet.add(mapping.getColumn());
}
else
{
Object item = field.getItem();
if (item instanceof Column)
{
columnSet.add(item);
}
else if (item instanceof Field[])
{
Field[] fieldArray = (Field[])item;
for (int i = 0; i < fieldArray.length; ++i)
{
addColumns(fieldArray[i], columnSet);
}
}
}
}
/**
* @see nexj.core.persistence.PersistenceAdapter#isUnique(nexj.core.persistence.Query, java.util.List)
*/
public boolean isUnique(Query query, List sourceList)
{
int nCount = sourceList.size();
Set columnSet = new HashHolder(nCount);
for (int nSource = 0; nSource < nCount; ++nSource)
{
Source source = (Source)sourceList.get(nSource);
if (source instanceof Field)
{
addColumns((Field)source, columnSet);
}
else
{
Query assoc = source.getQuery();
if (assoc.getParent() == query)
{
Index index = (Index)query.getKey(false);
for (int i = 0, n = index.getIndexColumnCount(); i < n; ++i)
{
columnSet.add(index.getIndexColumn(i).getColumn());
}
}
}
}
if (!columnSet.isEmpty())
{
Set tableSet = new HashHolder(columnSet.size() >> 2);
for (Iterator itr = columnSet.iterator(); itr.hasNext();)
{
tableSet.add(((Column)itr.next()).getTable());
}
for (Iterator itr = tableSet.iterator(); itr.hasNext();)
{
Table table = (Table)itr.next();
loop:
for (int nIndex = 0, nIndexCount = table.getIndexCount(); nIndex < nIndexCount; ++nIndex)
{
Index index = table.getIndex(nIndex);
if (index.isUnique() &&
(index.getType() == Index.BTREE || index.getType() == Index.CLUSTER))
{
for (int i = 0, n = index.getIndexColumnCount(); i < n; ++i)
{
if (!columnSet.contains(index.getIndexColumn(i).getColumn()))
{
continue loop;
}
}
return true;
}
}
}
}
return false;
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getQuery(nexj.core.persistence.Source)
*/
public Query getQuery(Source source)
{
return ((SQLJoin)source.getMapping()).query;
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getFields(Source)
*/
public final Field[] getFields(Source source)
{
if (source instanceof Query)
{
return (Field[])((Query)source).getFieldItem();
}
return (Field[])source.getItem();
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getValues(nexj.core.persistence.OID, Source)
*/
public Object[] getValues(OID oid, Source source) throws PersistenceException
{
if (source instanceof Query)
{
Query query = (Query)source;
if (query.getAttribute() != null)
{
Key key = query.getKey(true);
if (key.isObjectKeyPart())
{
Field[] fieldArray = getFields(source);
int nCount = fieldArray.length;
if (nCount != oid.getCount() && nCount <= key.getPartCount())
{
Object[] valueArray = new Object[nCount];
for (int i = 0; i < nCount; ++i)
{
int nOrdinal = key.getObjectKeyPartOrdinal(i);
if (nOrdinal >= oid.getCount())
{
return oid.getValueArray();
}
valueArray[i] = oid.getValue(nOrdinal);
}
return valueArray;
}
}
}
}
return oid.getValueArray();
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getValue(OID, nexj.core.persistence.Source)
*/
public Object getValue(OID oid, Source source)
{
if (source instanceof Field)
{
Column column = null;
Field field = (Field)source;
RelationalPrimitiveMapping mapping = (RelationalPrimitiveMapping)field.getAttributeMapping();
if (mapping != null)
{
column = mapping.getColumn();
}
else if (field.getItem() instanceof Column)
{
column = (Column)field.getItem();
}
if (column != null && column.isPrimary())
{
IndexColumn indexColumn = ((Index)((RelationalMapping)source.getQuery()
.getPersistenceMapping()).getObjectKey()).findIndexColumn(column);
if (indexColumn != null && indexColumn.getOrdinal() < oid.getCount())
{
return oid.getValue(indexColumn.getOrdinal());
}
}
}
else
{
Query query = source.getQuery();
if (query.getParent() != null)
{
Key key = query.getKey(false);
if (key.isObjectKeyPart())
{
int nCount = key.getPartCount();
Object[] valueArray = new Object[nCount];
for (int i = 0; i < nCount; ++i)
{
int nOrdinal = key.getObjectKeyPartOrdinal(i);
if (nOrdinal >= oid.getCount())
{
return Undefined.VALUE;
}
valueArray[i] = oid.getValue(nOrdinal);
}
return new OID(valueArray);
}
}
}
return Undefined.VALUE;
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getValue(PropertyMap, nexj.core.persistence.Source)
*/
public Object getValue(PropertyMap instance, Source source)
{
if (instance instanceof OIDHolder)
{
OID oid = ((OIDHolder)instance).getOID();
if (oid == null)
{
return Undefined.VALUE;
}
Object value = getValue(oid, source);
if (value != Undefined.VALUE)
{
return value;
}
}
Attribute attribute = source.getAttribute();
if (attribute != null && !source.isInverse())
{
Object value = instance.findValue(attribute.getName(), Undefined.VALUE);
if (value != Undefined.VALUE)
{
return value;
}
}
if (source instanceof Field)
{
Column column = null;
RelationalPrimitiveMapping primitiveMapping = (RelationalPrimitiveMapping)source.getAttributeMapping();
if (primitiveMapping != null)
{
column = primitiveMapping.getColumn();
}
else
{
Object item = source.getItem();
if (item instanceof Column)
{
column = (Column)item;
}
else if (item instanceof Field[])
{
Object[] valueArray = null;
Field[] fieldArray = (Field[])item;
for (int i = 0; i < fieldArray.length; ++i)
{
Object value = getValue(instance, fieldArray[i]);
if (value == Undefined.VALUE)
{
valueArray = null;
break;
}
if (valueArray == null)
{
valueArray = new Object[fieldArray.length];
}
valueArray[i] = value;
}
if (valueArray != null)
{
return new OID(valueArray);
}
}
}
if (column != null)
{
PersistenceMapping persistenceMapping = ((SQLJoin)source.getMapping()).query.getPersistenceMapping();
if (persistenceMapping != null)
{
Metaclass metaclass = persistenceMapping.getMetaclass();
assert metaclass.isUpcast(m_context.getMetadata().getMetaclass(instance.getClassName()));
// TODO: Optimize with precomputed metadata
for (int nAttrOrdinal = 0, nAttrCount = metaclass.getInstanceAttributeCount();
nAttrOrdinal < nAttrCount; ++nAttrOrdinal)
{
attribute = metaclass.getInstanceAttribute(nAttrOrdinal);
AttributeMapping mapping = persistenceMapping.getAttributeMapping(attribute);
if (mapping instanceof RelationalPrimitiveMapping)
{
if (column == ((RelationalPrimitiveMapping)mapping).getColumn())
{
Object value = instance.findValue(attribute.getName(), Undefined.VALUE);
if (value != Undefined.VALUE)
{
return column.getValueType().convert(value);
}
}
}
else if (mapping instanceof RelationalClassMapping)
{
RelationalClassMapping classMapping = (RelationalClassMapping)mapping;
if (classMapping.isInner())
{
IndexColumn indexColumn = classMapping.getSourceKey().findIndexColumn(column);
if (indexColumn != null)
{
Object value = instance.findValue(attribute.getName(), Undefined.VALUE);
if (value == null)
{
return null;
}
if (value instanceof OIDHolder)
{
OID oid = ((OIDHolder)value).getOID();
if (oid != null && indexColumn.getOrdinal() < oid.getCount())
{
return oid.getValue(indexColumn.getOrdinal());
}
}
}
}
}
}
}
}
}
return Undefined.VALUE;
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getOID(nexj.core.util.PropertyMap, java.lang.Object)
*/
public OID getOID(PropertyMap instance, Object item)
{
Field[] fieldArray = (Field[])item;
Object[] valueArray = new Object[fieldArray.length];
for (int i = 0; i < fieldArray.length; ++i)
{
if ((valueArray[i] = getValue(instance, fieldArray[i])) == Undefined.VALUE)
{
return null;
}
}
return new OID(valueArray);
}
/**
* @see nexj.core.persistence.PersistenceAdapter#validate(nexj.core.meta.persistence.AttributeMapping, java.lang.Object)
*/
public void validate(AttributeMapping mapping, Object value) throws ValidationException
{
if (mapping instanceof RelationalPrimitiveMapping && value != null)
{
Column column = ((RelationalPrimitiveMapping)mapping).getColumn();
int nPrecision;
switch (column.getType().getOrdinal())
{
case Primitive.INTEGER_ORDINAL:
nPrecision = column.getPrecision();
switch (nPrecision)
{
case 1:
int nValue = ((Number)toBind(column, column.getValueType().convert(value))).intValue();
if (nValue < 0 || nValue > 255)
{
throw new ValidationException("err.validation.numberRange",
new Object[]
{
new StringId(mapping.getAttribute().getCaption()),
new StringId(mapping.getAttribute().getMetaclass().getCaption()),
Primitive.ZERO_INTEGER,
Primitive.createInteger(255)
});
}
break;
case 2:
nValue = ((Number)toBind(column, column.getValueType().convert(value))).intValue();
if (nValue < Short.MIN_VALUE || nValue > Short.MAX_VALUE)
{
throw new ValidationException("err.validation.numberRange",
new Object[]
{
new StringId(mapping.getAttribute().getCaption()),
new StringId(mapping.getAttribute().getMetaclass().getCaption()),
Primitive.createInteger(Short.MIN_VALUE),
Primitive.createInteger(Short.MAX_VALUE)
});
}
break;
}
break;
case Primitive.DECIMAL_ORDINAL:
nPrecision = column.getPrecision(getMaxDecimalPrecision());
int nScale = column.getScale(getMaxDecimalPrecision());
BigDecimal decMax = BigDecimal.TEN.pow(nPrecision - nScale);
if (((BigDecimal)toBind(column, column.getValueType().convert(value))).abs().setScale(nScale,
RoundingMode.HALF_UP).compareTo(decMax) >= 0)
{
if (nScale > 0)
{
decMax = decMax.subtract(BigDecimal.ONE.divide(BigDecimal.TEN.pow(nScale)));
}
throw new ValidationException("err.validation.numberRange",
new Object[]
{
new StringId(mapping.getAttribute().getCaption()),
new StringId(mapping.getAttribute().getMetaclass().getCaption()),
decMax.negate(),
decMax
});
}
break;
case Primitive.TIMESTAMP_ORDINAL:
long lTime = ((Timestamp)toBind(column, column.getValueType().convert(value))).getTime();
if (lTime < getMinTime() || lTime > getMaxTime())
{
throw new ValidationException("err.validation.dateRange",
new Object[]
{
new StringId(mapping.getAttribute().getCaption()),
new StringId(mapping.getAttribute().getMetaclass().getCaption()),
new Timestamp(getMinTime()),
new Timestamp(getMaxTime())
});
}
break;
}
}
}
/**
* Validate schema Unicode flag matches DB state (if available).
* @param schema the Schema to validate.
* @param rs The resultset to validate.
* @param nColumn The column number (1 based) in resultset to validate.
* @return TRUE == is a unicode column
* FALSE == is not a unicode column
* null == unknown (e.g. number column or column not present)
* @throws SQLException On DB IO error.
*/
protected abstract Boolean isUnicode(RelationalSchema schema, ResultSet rs, int nColumn)
throws SQLException;
/**
* @return The maximum precision of decimal numbers.
*/
protected int getMaxDecimalPrecision()
{
return 38;
}
/**
* @return The minimum supported timestamp value.
*/
protected abstract long getMinTime();
/**
* @return The minimum supported timestamp value.
*/
protected abstract long getMaxTime();
/**
* @see nexj.core.persistence.PersistenceAdapter#isSupported(nexj.core.persistence.Operator, int)
*/
public boolean isSupported(Operator op, int nFlags)
{
if (SQLGenerator.isSupported(op))
{
if ((nFlags & Operator.NORMALIZE_EXPRESSION) != 0)
{
return true;
}
if (m_sopVisitor == null)
{
m_sopVisitor = new SupportedOperatorVisitor();
}
return m_sopVisitor.isSupported(op);
}
return false;
}
/**
* Determines if an operator is supported in a table join.
* This is restriction is in addition to isSupported().
* @param query The joined query.
* @param op The operator to test.
* @return True if the operator is supported.
*/
public boolean isJoinSupported(Query query, Operator op)
{
if (op instanceof AttributeOperator)
{
Source source = ((AttributeOperator)op).getSource();
return !(source instanceof Field) || source.getMapping() == query.getMapping();
}
return true;
}
/**
* Determines if the adapter is compatible with the connection.
* @param con The JDBC connection.
* @param db The relational database. Can be null.
* @return True if the adapter is compatible with the connection.
*/
protected boolean isCompatible(Connection con, RelationalDatabase db) throws SQLException
{
if (db != null && !getClass().equals(db.getAdapter().getClassObject()))
{
return false;
}
String sDriver = (db == null) ? null : db.getDriver();
if (StringUtil.isEmpty(sDriver))
{
return true;
}
int i = sDriver.indexOf('.');
if (i < 0)
{
return true;
}
int n = 1;
if (i <= 2)
{
n = 3;
}
else if ("biz com edu net org gov mil int".indexOf(sDriver.substring(0, i)) >= 0)
{
n = 2;
}
for (++i; n != 0; --n)
{
int k = sDriver.indexOf('.', i);
if (k < 0)
{
i = sDriver.length();
break;
}
i = k + 1;
}
return sDriver.regionMatches(0, unwrap(con).getClass().getName(), 0, i);
}
/**
* @see nexj.core.persistence.PersistenceAdapter#getVersion(nexj.core.meta.persistence.Schema)
*/
public SchemaVersion getVersion(Schema schema) throws PersistenceException
{
if (schema == null)
{
return null;
}
RelationalDatabase db = (RelationalDatabase)schema.getDataSource();
Table table = ((RelationalSchema)schema).getVersionTable();
Metadata metadata = m_context.getMetadata();
if (metadata.getNamespace() == null || table == null)
{
return null;
}
Column namespaceColumn = table.getColumn("namespace");
Column versionColumn = table.getColumn("version");
Column stepColumn = table.getColumn("step");
Column upgradableColumn = table.getColumn("upgradable");
Column testColumn = table.getColumn("test");
StringBuffer buf = new StringBuffer(128);
buf.append("select ");
buf.append(namespaceColumn.getQuotedName());
buf.append(", ");
buf.append(versionColumn.getQuotedName());
buf.append(", ");
buf.append(stepColumn.getQuotedName());
buf.append(", ");
buf.append(upgradableColumn.getQuotedName());
buf.append(", ");
buf.append(testColumn.getQuotedName());
buf.append(" from ");
buf.append(table.getQuotedName());
String sSQL = buf.toString();
SQLConnection connection = null;
PreparedStatement stmt = null;
try
{
connection = getConnection();
log(sSQL);
Connection con = connection.getConnection();
if (!isCompatible(con, db))
{
String sDBName = con.getMetaData().getDatabaseProductName();
String sDBVersion = con.getMetaData().getDatabaseProductVersion();
throw new PersistenceException("err.persistence.adapterMismatch",
new Object[]{db.getName(), db.getAdapter().getName(),
(sDBVersion.contains(sDBName)) ? sDBVersion : (sDBName + " " + sDBVersion)});
}
stmt = con.prepareStatement(sSQL);
ResultSet rs = executeQuery(stmt);
if (!rs.next())
{
throw new PersistenceException("err.persistence.noStorageVersion",
new Object[]{db.getName()});
}
SchemaVersion version = new SchemaVersion();
version.setNamespace(Primitive.toString(getBind(namespaceColumn).getValue(rs, 0, this)));
version.setVersion(Primitive.toString(getBind(versionColumn).getValue(rs, 1, this)));
Integer step = Primitive.toInteger(getBind(stepColumn).getValue(rs, 2, this));
version.setStep((step == null) ? -1 : step.intValue());
Boolean upgradable = Primitive.toBoolean(getBind(upgradableColumn).getValue(rs, 3, this));
version.setUpgradable((upgradable == null) ? false : upgradable.booleanValue());
Boolean test = Primitive.toBoolean(getBind(testColumn).getValue(rs, 4, this));
version.setTest((test == null) ? false : test.booleanValue());
// has to be done while still have data in ResultSet on DB2
Boolean unicode = isUnicode((RelationalSchema)schema, rs, 1); // 1 == "namespace" column
boolean bUnicode = db.isUnicode();
if (unicode == null)
{
throw new PersistenceException(
"err.persistence.sql.unicodeUnknown",
new Object[]{db.getName(), Boolean.valueOf(bUnicode)});
}
if (unicode.booleanValue() != bUnicode)
{
throw new PersistenceException(
"err.persistence.sql.unicodeMismatch",
new Object[]{db.getName(), Boolean.valueOf(bUnicode)});
}
if (rs.next())
{
throw new PersistenceException("err.persistence.ambiguousStorageVersion",
new Object[]{db.getName()});
}
return version;
}
catch (SQLException e)
{
throw new PersistenceException("err.persistence.checkVersion",
new Object[]{db.getName()}, e);
}
finally
{
close(stmt);
if (connection != null)
{
connection.decRef();
}
}
}
/**
* @see nexj.core.persistence.PersistenceAdapter#setVersion(nexj.core.meta.persistence.Schema, nexj.core.persistence.SchemaVersion)
*/
public void setVersion(Schema schema, SchemaVersion version) throws PersistenceException
{
if (version.getNamespace() == null ||
version.getVersion() == null)
{
throw new NullPointerException();
}
if (!version.isUpgradable())
{
throw new IllegalStateException();
}
if (schema == null)
{
return;
}
Table table = ((RelationalSchema)schema).getVersionTable();
if (table == null)
{
return;
}
Column namespaceColumn = table.getColumn("namespace");
Column versionColumn = table.getColumn("version");
Column stepColumn = table.getColumn("step");
StringBuffer buf = new StringBuffer(128);
buf.append("update ");
buf.append(table.getQuotedName());
buf.append(" set ");
buf.append(versionColumn.getQuotedName());
buf.append(" = ");
appendBind(buf, 0);
buf.append(", ");
buf.append(stepColumn.getQuotedName());
buf.append(" = ");
appendBind(buf, 1);
buf.append(" where ");
buf.append(namespaceColumn.getQuotedName());
buf.append(" = ");
appendBind(buf, 2);
String sSQL = buf.toString();
SQLConnection connection = null;
PreparedStatement stmt = null;
try
{
connection = getConnection();
if (isDebug())
{
log(sSQL);
logBindValue(0, version.getVersion());
logBindValue(1, Primitive.createInteger(version.getStep()));
logBindValue(2, version.getNamespace());
}
stmt = connection.getConnection().prepareStatement(sSQL);
getBind(versionColumn).setValue(stmt, 0, version.getVersion(), this);
getBind(stepColumn).setValue(stmt, 1, Primitive.createInteger(version.getStep()), this);
getBind(namespaceColumn).setValue(stmt, 2, version.getNamespace(), this);
if (executeUpdate(stmt) != 1)
{
throw new PersistenceException("err.persistence.noStorageNamespace",
new Object[]{schema.getDataSource().getName(), version.getNamespace()});
}
}
catch (SQLException e)
{
throw new PersistenceException("err.persistence.setVersion",
new Object[]{schema.getDataSource().getName()}, e);
}
finally
{
close(stmt);
if (connection != null)
{
connection.decRef();
}
}
}
/**
* Obtains the original SQL connection.
* @param connection The connection wrapper.
* @return The original SQL connection.
*/
protected static Connection unwrap(Connection connection) throws SQLException
{
if (connection == null)
{
return null;
}
if (connection instanceof SQLHookConnection)
{
return unwrap(((SQLHookConnection)connection).getConnection());
}
if (connection instanceof nexj.core.rpc.sql.ra.SQLConnection)
{
return ((nexj.core.rpc.sql.ra.SQLConnection)connection).getConnection();
}
try
{
if (s_getUnderlyingConnection != null &&
s_getUnderlyingConnection.getDeclaringClass().isAssignableFrom(connection.getClass()))
{
return (Connection)s_getUnderlyingConnection.invoke(connection, null);
}
if (s_getNativeConnection != null &&
s_wsJDBCConnection.isAssignableFrom(connection.getClass()))
{
return (Connection)s_getNativeConnection.invoke(null, new Object[]{connection});
}
// Note: Class.isAssignableFrom(...) returns incorrect results under Sun JVM
// i.e. class compiled under Java 1.5 not implementing a Java 1.6 interface will be marked
// as implementing the Java 1.6 interface when run via a Sun Java 1.6 JVM
// e.g. java.sql.Wrapper from JRE1.6 not implemented on jBoss4 WrappedStatement
if (s_unwrap != null && // Java 1.6 java.sql.Wrapper
s_unwrap.getDeclaringClass().isAssignableFrom(connection.getClass()))
{
return (Connection)s_unwrap.invoke(connection, new Object[]{Connection.class});
}
}
catch (Exception e)
{
SQLException x;
if (e instanceof InvocationTargetException &&
((InvocationTargetException)e).getTargetException() instanceof SQLException)
{
x = (SQLException)((InvocationTargetException)e).getTargetException();
}
else
{
x = new SQLException("Unable to unwrap connection");
x.initCause(e);
}
throw x;
}
return connection;
}
/**
* @see nexj.core.persistence.PersistenceAdapter#upgrade(nexj.core.meta.persistence.SchemaUpgrade, nexj.core.meta.upgrade.UpgradeState, nexj.core.persistence.SchemaVersion)
*/
public void upgrade(SchemaUpgrade upgrade, UpgradeState state, SchemaVersion version) throws PersistenceException
{
SQLConnection connection = null;
try
{
connection = getConnection();
SQLSchemaManager manager = createSchemaManager((RelationalDatabase)upgrade.getDataSource());
RelationalSchemaUpgradeState rstate = (RelationalSchemaUpgradeState)state;
manager.setSQLAppender(new SQLSchemaManager.SQLConnectionAppender(connection.getConnection()));
for (;;)
{
int nStep = version.getStep();
if (nStep < 0)
{
break;
}
manager.upgrade((RelationalSchemaUpgrade)upgrade, rstate, version);
if (version.getStep() == nStep)
{
throw new IllegalStateException();
}
setVersion(rstate.getSchema(), version);
}
}
catch (SQLException e)
{
throw getException(e, null, 0, 0);
}
catch (RuntimeException e)
{
throw new PersistenceException(
"err.persistence.upgrade.apply",
new Object[]{
version.getNamespace(),
version.getVersion(),
Primitive.createInteger(version.getStep())},
e);
}
finally
{
if (connection != null)
{
connection.decRef();
}
}
}
/**
* Gets SQL statement to execute when a connection is first established.
* @return The initial SQL string; null to issue no initial statement.
*/
public String getInitialSQL()
{
return null;
}
/**
* Gets SQL statement to execute to test that the connection is still valid
* after it has been retrieved out of a connection pool.
* @return The test SQL string.
*/
public abstract String getTestSQL();
/**
* @return The calendar for timestamp conversion.
*/
protected Calendar getCalendar()
{
if (m_calendar == null)
{
m_calendar = Calendar.getInstance(TZ.UTC, Locale.ENGLISH);
}
return m_calendar;
}
/**
* Template method to return a bind factory from a primitive type.
* @param type The primitive type.
* @return The bind factory.
*/
protected BindFactory getBindFactory(Primitive type)
{
return s_bindFactoryArray[type.getOrdinal()];
}
/**
* Gets the bind for a given column.
* @param column The column for which to get the bind.
* @return The bind.
*/
public Bind getBind(Column column)
{
return getBindFactory(getConversionMapper().getType(column.getType())).create(column);
}
/**
* Gets the bind for a given primitive type.
* @param type The primitive type for which to get the bind.
* @return The bind.
*/
public Bind getBind(Primitive type)
{
return getBindFactory(getConversionMapper().getType(type)).create(type);
}
/**
* Gets the internal representation of a given constant.
* @param type The constant type.
* @param value The constant value.
* @return The converted constant.
*/
public Object getInternal(Primitive type, Object value)
{
Primitive toType = getConversionMapper().getType(type);
if (toType == type)
{
return value;
}
return toType.getConverter(type).invoke(value);
}
/**
* Converts a primitive value to a column value type.
* @param column The table column.
* @param value The value to convert.
* @return The converted value.
*/
public final Object toBind(Column column, Object value)
{
Converter converter = getConverter(column.getValueType(), column);
if (converter == null)
{
return value;
}
return converter.getInverseFunction().invoke(value);
}
/**
* Converts a bind primitive value to a column value type.
* @param column The table column.
* @param value The value to convert.
* @return The converted value.
*/
public final Object toValue(Column column, Object value)
{
Converter converter = getConverter(column.getValueType(), column);
if (converter == null)
{
return value;
}
return converter.getForwardFunction().invoke(value);
}
/**
* Determines if the output columns should be aliased to ensure their uniqueness.
* @param The top level query.
* @return True if the columns should be aliased.
*/
public boolean isColumnAliased(Query query)
{
return false;
}
/**
* Appends any additional output fields to a string buffer.
* @param buf The string buffer.
* @param query The query providing the output fields.
* @param gen The SQL generator.
*/
public void appendExtraOutputFields(StringBuffer buf, Query query, SQLGenerator gen)
{
}
/**
* Appends a bind placeholder to a string buffer.
* @param buf The destination buffer.
* @param nOrdinal The placeholder ordinal number (0-based).
*/
public void appendBind(StringBuffer buf, int nOrdinal)
{
buf.append('?');
}
/**
* Appends a bind placeholder to a string buffer.
* Used where column type is unknown from immediate query context.
* Override in adapters that need to know the bind type.
* @param buf The destination buffer.
* @param nOrdinal The placeholder ordinal number (0-based).
* @param type The value type.
* @param value The actual value to be bound to the statement (null == unknown)
*/
public void appendBind(StringBuffer buf, int nOrdinal, Primitive type, Object value)
{
appendBind(buf, nOrdinal);
}
/**
* Appends a bind placeholder to a string buffer.
* Used where column type is unknown from immediate query context.
* Override in adapters that need to know the bind type.
* @param buf The destination buffer.
* @param nOrdinal The placeholder ordinal number (0-based).
* @param column The column the value is meant for.
*/
public void appendBind(StringBuffer buf, int nOrdinal, Column column)
{
appendBind(buf, nOrdinal);
}
/**
* Appends a case-converted bind placeholder to a string buffer.
* @param buf The destination buffer.
* @param nOrdinal The placeholder ordinal number (0-based).
* @param column The column the value is meant for.
*/
public void appendCaseConvertedBind(StringBuffer buf, int nOrdinal, Column column)
{
throw new UnsupportedOperationException();
}
/**
* Appends a case-converted literal to a string buffer.
* @param buf The destination buffer.
* @param sLiteral The literal to append.
*/
public void appendCaseConvertedLiteral(StringBuffer buf, String sLiteral)
{
throw new UnsupportedOperationException();
}
/**
* Converts a wildcard expression to a database like pattern.
* @param sPattern The wildcard expression (with * and ?).
*/
public String getLikePattern(String sPattern)
{
int nCount = sPattern.length();
StringBuffer patBuf = new StringBuffer(nCount + 8);
for (int i = 0; i < nCount; ++i)
{
char ch = sPattern.charAt(i);
switch (ch)
{
case '*':
patBuf.append('%');
break;
case '?':
patBuf.append('_');
break;
case '%':
case '_':
case '\\':
patBuf.append('\\');
patBuf.append(ch);
break;
default:
if (isLikeReservedChar(ch))
{
patBuf.append('\\'); // char defined in appendLikeEscape(StringBuffer)
}
patBuf.append(ch);
break;
}
}
return patBuf.toString();
}
/**
* Does the argument represent a reserved character in a 'LIKE' statement pattern.
* The characters '%', '_', and '/' are assumed to be reserved.
* @param ch The character to examine.
* @return Is this character reserved.
*/
protected boolean isLikeReservedChar(char ch)
{
return '[' == ch || ']' == ch;
}
/**
* Appends a like escape statement to a string buffer.
* @param buf The destination buffer.
*/
public void appendLikeEscape(StringBuffer buf)
{
buf.append(" escape '\\'");
}
/**
* Appends the beginning of a like statement to a string buffer.
* @param buf The destination buffer.
*/
public void appendLikeStatement(StringBuffer buf)
{
buf.append(" like ");
}
/**
* Appends the full-text search statement (e.g. "contains") that returns a
* double precision floating point score [0..1] of each result,
* or null for a non-matching record.
* @param buf The buffer to append to.
* @param sAlias The alias of the table containing the column.
* @param column The column to do the full-text search on (same as for getMatchJoin()).
* @param join The joined table SQLJoin struct (null if return from getMatchJoin() == null).
* @param expression The full-text search expression to search for (same as for getMatchJoin()).
*/
public abstract void appendMatchStatement(StringBuffer buf, String sAlias, Column column,
SQLJoin join, Pair expression);
/**
* Gets the columns that should be prepended to an ORDER BY clause
* to trick the database server into using an index in lieu of
* performing a sort.
*
* @return The columns to prepend; null to prepend nothing.
*/
public OrderByPrefix getOrderByPrefix(Query query)
{
return null;
}
/**
* Appends the sort direction (asc/desc) to a string buffer.
* @param buf The destination buffer.
* @param op The sort operand.
* @param bAscending True if the sort direction is ascending.
*/
public void appendSortDirection(StringBuffer buf, Operator op, boolean bAscending)
{
if (!bAscending)
{
buf.append(" desc");
}
}
/**
* Appends a sort order conversion function prefix.
* @param buf The destination buffer.
* @param op The operand to convert.
* @return The suffix to append or null.
*/
public String appendSortPrefix(StringBuffer buf, Operator op)
{
return null;
}
/**
* Appends a group order conversion function prefix.
* @param buf The destination buffer.
* @param op The operand to convert.
* @return The suffix to append or null.
*/
public String appendGroupPrefix(StringBuffer buf, Operator op)
{
return null;
}
/**
* Appends a comparison conversion function prefix.
* @param buf The destination buffer.
* @param op The operand to convert.
* @return The suffix to append or null.
*/
public String appendComparisonPrefix(StringBuffer buf, Operator op)
{
return null;
}
/**
* Appends a boolean-to-numerical expression conversion prefix.
* @param buf The destination buffer.
* @param op The operand to convert.
* @return The suffix to append or null.
*/
public String appendBooleanPrefix(StringBuffer buf, Operator op)
{
buf.append("(case when ");
return " then 1 else 0 end)";
}
/**
* Appends a string-length function prefix.
* @param buf The destination buffer.
* @param op The string-length operator.
* @return The suffix to append.
*/
public abstract String appendStringLengthPrefix(StringBuffer buf, FunctionOperator op);
/**
* Appends a substring function prefix.
* @param buf The destination buffer.
* @param op The substring operator.
* @return The suffix to append.
*/
public abstract String appendSubstringPrefix(StringBuffer buf, FunctionOperator op);
/**
* Appends a count aggregate function prefix.
* @param buf The destination buffer.
* @param op The count operator.
* @return The suffix to append.
*/
public String appendCountPrefix(StringBuffer buf, AggregateOperator op)
{
buf.append("count(");
return ")";
}
/**
* Appends a sum aggregate function prefix.
* @param buf The destination buffer.
* @param op The sum operator.
* @return The suffix to append.
*/
public String appendSumPrefix(StringBuffer buf, AggregateOperator op)
{
buf.append("sum(");
return ")";
}
/**
* Appends an average aggregate function prefix.
* @param buf The destination buffer.
* @param op The average operator.
* @return The suffix to append.
*/
public String appendAveragePrefix(StringBuffer buf, AggregateOperator op)
{
buf.append("avg(");
return ")";
}
/**
* Appends a minimum aggregate function prefix.
* @param buf The destination buffer.
* @param op The minimum operator.
* @return The suffix to append.
*/
public String appendMinimumPrefix(StringBuffer buf, AggregateOperator op)
{
buf.append("min(");
return ")";
}
/**
* Appends a maximum aggregate function prefix.
* @param buf The destination buffer.
* @param op The maximum operator.
* @return The suffix to append.
*/
public String appendMaximumPrefix(StringBuffer buf, AggregateOperator op)
{
buf.append("max(");
return ")";
}
/**
* Appends an ungroup aggregate function prefix.
* @param buf The destination buffer.
* @return The suffix to append.
*/
public String appendUngroupPrefix(StringBuffer buf)
{
buf.append("min(");
return ")";
}
/**
* Appends a sort, comparison etc suffix to a string buffer.
* @param buf The destination buffer.
* @param sSuffix The suffix to append.
*/
public void appendSuffix(StringBuffer buf, String sSuffix)
{
if (sSuffix != null)
{
buf.append(sSuffix);
}
}
/**
* Determines if the operator is case-converted.
* @param op The operator to test.
* @return True if the operator is case-converted.
*/
public boolean isCaseConverted(Operator op)
{
return false;
}
/**
* Determines if the field is case-converted.
* @param field The field to test.
* @return True if the field is case-converted.
*/
public boolean isCaseConverted(Field field)
{
return false;
}
/**
* Determines if the column is case-converted.
* @param column The column to test.
* @return True if the column is case-converted.
*/
public boolean isCaseConverted(Column column)
{
return false;
}
/**
* Appends a case-converted column name.
* @param buf The destination buffer.
* @param column The column to append.
*/
public void appendCaseConvertedColumn(StringBuffer buf, Column column)
{
throw new UnsupportedOperationException();
}
/**
* Determines if a value can be used as literal.
* Some databases have limitations on the maximum length of a literal.
* @param type The value primitive type.
* @param value The value.
* @return True if the value can be used as a literal.
*/
public abstract boolean isLiteral(Primitive type, Object value);
/**
* Append a literal SQL sanitized string to the buffer (i.e. all quotes escaped)
* @param buf The buffer to append to.
* @param sValue The value to sanitize and append.
*/
protected void appendLiteral(StringBuffer buf, String sValue)
{
buf.append('\'');
for (int i = 0, nLength = sValue.length(); i < nLength; ++i)
{
char ch = sValue.charAt(i);
if (ch == '\'')
{
buf.append('\'');
}
buf.append(ch);
}
buf.append('\'');
}
/**
* Appends an SQL literal to a string buffer.
* @param buf The output buffer.
* @param column The column the value is meant to be used with.
* @param value The value to append.
*/
public void appendLiteral(StringBuffer buf, Column column, Object value)
{
appendLiteral(buf, column.getType(), value);
}
/**
* Appends an SQL literal to a string buffer.
* @param buf The output buffer.
* @param type The value type.
* @param value The value to append.
*/
public abstract void appendLiteral(StringBuffer buf, Primitive type, Object value);
/**
* Appends the specified SQL type conversion expression to a string buffer.
* @param buf The destination buffer.
* @param op The operand to convert - a Field or Operator.
* @param fromType The type from which to convert.
* @param type The type to which to convert.
* @param gen The SQLGenerator instance.
*/
public abstract void appendTypeConversion(StringBuffer buf, Object op,
Primitive fromType, Primitive type, SQLGenerator gen);
/**
* Appends a prefix hint to the SQL clause: HINT select ...
* @param buf The query output buffer.
* @param query The root query node.
*/
public abstract void appendPrefixHint(StringBuffer buf, Query query);
/**
* Appends an infix hint to the SQL clause: select HINT ...
* @param buf The query output buffer.
* @param query The root query node.
*/
public abstract void appendInfixHint(StringBuffer buf, Query query);
/**
* Appends a suffix hint to the SQL clause: select ... where ... order by ... HINT
* @param buf The query output buffer.
* @param query The root query node.
*/
public abstract void appendSuffixHint(StringBuffer buf, Query query);
/**
* Appends a table hint to the SQL clause: select ... from table_1 as alias_1 HINT ...
* @param buf The query output buffer.
* @param join The table join.
* @param query The join query node.
*/
public abstract void appendTableHint(StringBuffer buf, SQLJoin join, Query query);
/**
* Appends an identity output prefix.
* @param buf The statement output buffer.
* @param work The work item.
*/
public abstract void appendIdentityPrefix(StringBuffer buf, SQLInsert work);
/**
* Appends an identity column to the SQL insert statement column list.
* @param buf The statement output buffer.
* @param work The work item.
* @return True if anything has been appended.
*/
public abstract boolean appendIdentityColumn(StringBuffer buf, SQLInsert work);
/**
* Appends an identity value to the SQL insert statement value list.
* @param buf The statement output buffer.
* @param work The work item.
* @return True if anything has been appended.
*/
public abstract boolean appendIdentityValue(StringBuffer buf, SQLInsert work);
/**
* Appends an SQL statement outputting the last generated identity value for a given table.
* @param buf The statement output buffer.
* @param work The work item.
* @return True if the resulting statement should be prepared as callable.
*/
public abstract boolean appendIdentitySuffix(StringBuffer buf, SQLInsert work);
/**
* Binds an identity output parameter.
* @param stmt The prepared statement.
* @param work The SQL insert work item.
* @throws SQLException if a JDBC error occurs.
*/
public abstract void bindIdentity(PreparedStatement stmt, SQLInsert work) throws SQLException;
/**
* Gets the identity value from an executed statement.
* @param stmt The prepared statement.
* @param column The column storing the value.
* @param work The SQL insert work item.
* @throws SQLException if a JDBC error occurs.
*/
public abstract Object getIdentityValue(PreparedStatement stmt, Column column, SQLInsert work) throws SQLException;
/**
* Appends an SQL block start for an update statement with a conditional check.
* @param buf The statement output buffer.
* @return False if conditional statements are not supported.
*/
public abstract boolean appendNoRowsBlock(StringBuffer buf);
/**
* Appends the beginning of an SQL conditional that checks if no rows have been updated.
* @param buf The statement output buffer.
*/
public abstract void appendNoRowsStart(StringBuffer buf);
/**
* Appends the end of an SQL conditional that checks if no rows have been updated.
* @param buf The statement output buffer.
*/
public abstract void appendNoRowsEnd(StringBuffer buf);
/**
* @return True if batch mode is supported by the driver.
*/
public abstract boolean isBatchSupported();
/**
* @return True if the driver returns real batch update counts,
* as opposed to just success status.
*/
public abstract boolean isBatchUpdateCountSupported();
/**
* Determines if a work item is batchable.
* @param work The work item.
* @return True if the work item is batchable.
*/
public abstract boolean isBatchable(SQLWork work);
/**
* @return True if the driver requires fetching column values
* in the order of query column declaration.
* The official SQL Server driver has this bug.
*/
public boolean isOrderedColumnFetch()
{
return false;
}
/**
* @return The join syntax, one of the SQLGenerator.JOIN_* constants.
*/
public int getJoinSyntax()
{
return SQLGenerator.JOIN_ANSI;
}
/**
* @return The maximum count of elements in a list (e.g. in (expr1, ..., exprN)).
*/
public int getMaxListSize()
{
return Integer.MAX_VALUE;
}
/**
* @return The maximum supported count of bind parameters.
*/
public int getMaxBindCount()
{
return Integer.MAX_VALUE;
}
/**
* Rounds up a list size to facilitate SQL statement caching.
* @param nSize The list size.
* @param nBind The bind parameter count.
* @return The rounded up list size.
*/
protected int roundUpListSize(int nSize, int nBindCount)
{
int nMask = 1;
int nRoundedSize = nSize;
while (nRoundedSize > 0)
{
nRoundedSize >>>= 1;
nMask <<= 1;
}
nMask = (nMask - 1) >>> 4;
nRoundedSize = nSize + nMask & ~nMask;
return (getMaxBindCount() - nRoundedSize < nBindCount) ? nSize : nRoundedSize;
}
/**
* Rounds up the maximum row count to facilitate SQL statement caching.
* @param The maximum row count.
* @return The rounded up count.
*/
protected int roundUpMaxCount(int nMaxCount)
{
if (nMaxCount <= 8)
{
return nMaxCount;
}
return (nMaxCount + 0x07) & ~0x07;
}
/**
* Converts an SQLException to a subclass of PersistenceException.
* @param e The SQLException to convert.
* @param workArray The array containing the work items. Can be null.
* @param nStart The start index.
* @param nEnd The end index (exclusive).
* @return The converted exception.
*/
public PersistenceException getException(SQLException e, Work[] workArray, int nStart, int nEnd)
{
if (isQueryTimeoutException(e))
{
return new QueryTimeoutException(e);
}
if (isDuplicateKeyException(e))
{
Instance firstInstance = null;
Metaclass commonMetaclass = null;
if (workArray != null)
{
for (int i = nStart; i < nEnd; i++)
{
Instance instance = workArray[i].getInstance();
Metaclass metaclass = instance.getLazyMetaclass();
if (commonMetaclass == null || metaclass.isUpcast(commonMetaclass))
{
commonMetaclass = metaclass;
firstInstance = instance;
}
else if (!commonMetaclass.isUpcast(metaclass)) // Metaclasses of instances do not have a common parent; just use root for the first one as a guess.
{
commonMetaclass = instance.getLazyMetaclass().getPersistenceRoot();
break;
}
}
}
return new DuplicateKeyException(firstInstance,
(firstInstance == null) ? null :
getUniqueKeys(getDuplicateKeyName(e),
(RelationalMapping)firstInstance.getPersistenceMapping()), e);
}
if (isDateRangeException(e))
{
throw new ValueRangeException("err.persistence.dateRange",
new Object[]{new Timestamp(getMinTime()), new Timestamp(getMaxTime())}, e);
}
if (e instanceof DataTruncation)
{
throw new ValueRangeException("err.persistence.valueRange", e);
}
if(isDeadlockException(e))
{
return new DeadlockException(e);
}
if (isLockTimeoutException(e))
{
return new LockTimeoutException(e);
}
return new PersistenceException("err.persistence.sql", e);
}
/**
* Determines the unique keys for a given index in a relational mapping.
* @param sIndexName The name of the index for which to determine the unique keys.
* @param mapping The relational mapping.
* @return A list of unique keys.
*/
private Pair getUniqueKeys(String sIndexName, RelationalMapping mapping)
{
if (sIndexName != null)
{
for (int nTable = 0; nTable < mapping.getTableCount(); ++nTable)
{
Table table = mapping.getTable(nTable);
for (int nIndex = 0, nIndexCount = table.getIndexCount(); nIndex < nIndexCount; ++nIndex)
{
Index index = table.getIndex(nIndex);
if (indexNameMatches(index, sIndexName))
{
if (index.isUnique() && index.getIndexColumnCount() != 0)
{
Pair keys = mapping.getUniqueKeys(index);
if (keys != null)
{
return (Pair)keys.getHead();
}
}
return null;
}
}
}
}
return null;
}
/**
* Checks if a given vendor-specific SQL exception is a data range exception.
* @param e The SQL exception.
* @return True if this is a data range exception.
*/
protected abstract boolean isDateRangeException(SQLException e);
/**
* Checks if a given vendor-specific SQL exception is a query timeout exception.
* @param e The SQL exception.
* @return True if this is a query timeout exception.
*/
protected abstract boolean isQueryTimeoutException(SQLException e);
/**
* Checks if a given vendor-specific SQL exception is a duplicate key exception.
* @param e The SQL exception.
* @return true If this is a duplicate key exception.
*/
protected abstract boolean isDuplicateKeyException(SQLException e);
/**
* Extracts a key name from a vendor-specific SQL exception; its message could be localized.
* @param e An exception for which isDuplicateKeyException() is true.
* @return The duplicate key name, or null if not found.
*/
protected abstract String getDuplicateKeyName(SQLException e);
/**
* Matches a given index to its name in the persistent store.
* This name is the one generated in SQLSchemaManager.getIndexName() class.
* @param index The index to match.
* @param sPhysicalName The physical index name.
* @return True if the index name matches.
* @see nexj.core.persistence.sql.SQLSchemaManager#getIndexName(String, String, String, boolean, boolean)
*/
protected abstract boolean indexNameMatches(Index index, String sPhysicalName);
/**
* Checks if a given vendor-specific SQL exception is a deadlock exception.
* @param e The SQL exception.
* @return true If this is a deadlock exception.
*/
protected abstract boolean isDeadlockException(SQLException e);
/**
* Checks if a given vendor-specific SQL exception is a lock timeout exception.
* @param e The SQL exception.
* @return true If this is a lock timeout exception.
*/
protected abstract boolean isLockTimeoutException(SQLException e);
/**
* Matches a given index to its name in the persistent store.
* @param sMetadataName Name of the key in metadata.
* @param nMetaStart Starting position of the metadata name to be compared to the persistent key name.
* @param sPhysicalName Name of the key in a persistent store.
* @param sPrefix Physical name prefix. May be null.
* @param bCaseInsensitive Whether a case insensitive comparison is required.
* @return True if the index names in metadata and in a persistent store match.
*/
protected boolean indexNameMatches(String sMetadataName, int nMetaStart, String sPhysicalName, String sPrefix, boolean bCaseInsensitive)
{
int i = 0;
if (sPrefix != null)
{
if (!sPhysicalName.regionMatches(bCaseInsensitive, 0, sPrefix, 0, sPrefix.length()))
{
return false;
}
i += sPrefix.length();
}
if (sMetadataName.length() - nMetaStart != sPhysicalName.length() - i)
{
return false;
}
for (int k = 0; i < sPhysicalName.length(); ++i, ++k)
{
char chMeta = sMetadataName.charAt(k + nMetaStart);
char chPhys = sPhysicalName.charAt(i);
if (chPhys == '_')
{
if (chMeta != '_' && chMeta != '.')
{
return false;
}
}
else
{
if (bCaseInsensitive)
{
if (Character.toLowerCase(chMeta) != Character.toLowerCase(chPhys))
{
return false;
}
}
else
{
if (chMeta != chPhys)
{
return false;
}
}
}
}
return true;
}
/**
* Creates an SQLSchemaManager instance.
* @return The schema manager instance.
*/
public abstract SQLSchemaManager createSchemaManager();
/**
* Creates an SQLSchemaManager instance and initializes it with
* the current fragment of the specified data source.
* @param database The database. Can be null.
* @return The schema manager instance.
*/
public SQLSchemaManager createSchemaManager(RelationalDatabase database)
{
SQLSchemaManager manager = createSchemaManager();
if (database != null)
{
manager.setFragment((RelationalDatabaseFragment)database
.getFragment((m_context == null) ? null : m_context.getFragmentName()));
}
return manager;
}
/**
* Logs a message.
* @param message The message to log.
*/
public final void log(Object message)
{
if (m_bDebug)
{
if (m_logList != null)
{
m_logList.add(message);
}
else
{
s_logger.debug(message);
}
}
}
/**
* Logs a bind value.
* @param nOrdinal The bind value ordinal number.
* @param value The bind value.
*/
public final void logBindValue(int nOrdinal, Object value)
{
if (m_bDebug)
{
if (m_logList != null)
{
m_logList.add(Primitive.createInteger(nOrdinal));
m_logList.add(value);
}
else
{
StringBuffer buf = new StringBuffer(64);
appendBindValueLog(buf, nOrdinal, value);
s_logger.debug(buf);
}
}
}
/**
* Appends a bind value log to a string buffer.
* @param logger The destination logger.
* @param nOrdinal The bind value ordinal number.
* @param value The bind value.
*/
public void appendBindValueLog(StringBuffer buf, int nOrdinal, Object value)
{
buf.append("Bind[");
buf.append(nOrdinal);
buf.append("] = ");
if (value instanceof String)
{
buf.append('\'');
buf.append(value);
buf.append('\'');
}
else if (value instanceof Binary)
{
((Binary)value).appendTo(buf);
}
else
{
buf.append(Primitive.toString(value));
}
}
/**
* Logs the deferred log statements and resets the log.
* @param t Optional exception to log. Can be null.
*/
public final void flushLog(Throwable t)
{
if (m_logList != null)
{
StringBuffer buf = null;
int nLastBind = -1;
int nBatch = 0;
for (int i = 0, nCount = m_logList.size(); i < nCount; ++i)
{
Object message = m_logList.get(i);
if (message instanceof Integer && i < nCount - 1)
{
if (buf == null)
{
buf = new StringBuffer(64);
}
int nBind = ((Integer)message).intValue();
if (nBind <= nLastBind)
{
buf.setLength(0);
buf.append("--- Batch ");
buf.append(++nBatch);
buf.append("---");
s_xlogger.debug(buf);
}
buf.setLength(0);
appendBindValueLog(buf, nBind, m_logList.get(++i));
s_xlogger.debug(buf);
nLastBind = nBind;
}
else
{
s_xlogger.debug(message);
nLastBind = -1;
nBatch = 0;
}
}
m_logList.clear();
if (t != null)
{
m_context.getMachine().updateStackTrace(t);
s_xlogger.debug("Database error", t);
}
}
}
/**
* Resets the deferred exception log.
*/
public final void clearLog()
{
if (m_logList != null)
{
m_logList.clear();
}
}
// inner classes
/**
* SQL bind adapter.
*/
public interface Bind
{
/**
* Sets a bind value on a prepared statement.
* @param stmt The prepared statement.
* @param nOrdinal The bind variable number (0-based).
* @param value The value to set. Can be null.
* @param adapter The persistence adapter.
*/
void setValue(PreparedStatement stmt, int nOrdinal, Object value, SQLAdapter adapter) throws SQLException;
/**
* Gets a bind value from a result set.
* @param rs The result set.
* @param nOrdinal The column number (0-based).
* @param adapter The persistence adapter.
* @return The value. Can be null.
*/
Object getValue(ResultSet rs, int nOrdinal, SQLAdapter adapter) throws SQLException;
}
/**
* Factory for creating binds for a particular data type.
*/
protected interface BindFactory
{
/**
* Creates a bind for a given column.
* @param column The column.
* @return The created bind.
*/
Bind create(Column column);
/**
* Creates a bind for a primitive type.
* @param type The primitive type.
* @return The created bind.
*/
Bind create(Primitive type);
}
/**
* BindFactory that does not contain any logic to create the bind.
*/
protected final static class SimpleBindFactory implements BindFactory
{
/**
* The bind to return from the factory methods.
*/
private Bind m_bind;
/**
* Creates a bind factory.
* @param bind The bind to return from the factory methods.
*/
public SimpleBindFactory(Bind bind)
{
m_bind = bind;
}
/**
* @see nexj.core.meta.persistence.sql.Column.BindFactory#create(nexj.core.meta.persistence.sql.Column)
*/
public Bind create(Column column)
{
return m_bind;
}
/**
* @see nexj.core.meta.persistence.sql.Column.BindFactory#create(nexj.core.meta.Primitive)
*/
public Bind create(Primitive type)
{
return m_bind;
}
}
/**
* Encapsulates columns that should be prepended to an ORDER BY clause
* to trick the database server into using an index in lieu of
* performing a sort.
*/
protected static class OrderByPrefix
{
// attributes
/**
* The number of columns to prepend, starting with the first
* column in the index.
*/
protected int m_nColumnCount;
/**
* Indicates whether or not the sort order of the columns to
* be prepended should be reversed to match the sort order
* of the first column of the ORDER BY clause.
*
* E.g. if index is on (A ASC, B DESC, C ASC) and the
* existing ORDER BY clause is "C DESC", then the new
* clause should be "A DESC, B ASC, C DESC".
*/
protected boolean m_bSortDirectionReversed;
// associations
/**
* The index from which the columns to prepend should be taken.
*/
protected Index m_index;
/**
* The query mapping for the specified index.
*/
protected Object m_mapping;
// constructors
/**
* Creates a new encapsulation of the columns that should be prepended to
* an ORDER BY clause to trick the database server into using an
* index in lieu of performing a sort.
*
* @param index The index from which the columns to prepend should be taken.
* @param nColumnCount The number of columns to prepend.
* @param bSortDirectionReversed True to invert the sort orders of the columns
* being prepended.
* @param mapping The index mapping.
*/
public OrderByPrefix(
Index index, int nColumnCount, boolean bSortDirectionReversed, Object mapping)
{
m_index = index;
m_mapping = mapping;
m_nColumnCount = nColumnCount;
m_bSortDirectionReversed = bSortDirectionReversed;
}
// operations
/**
* Gets the number of columns to prepend. Columns will be prepended
* starting from the first column in the index.
*
* @return The number of columns to prepend to the ORDER BY clause.
*/
protected int getColumnCount()
{
return m_nColumnCount;
}
/**
* Indicates whether or not the sort order of the columns to
* be prepended should be reversed to match the sort order
* of the first column of the ORDER BY clause.
*
* @return True to invert the sort orders of the columns being prepended;
* false to leave them as-is.
*/
protected boolean isSortDirectionReversed()
{
return m_bSortDirectionReversed;
}
/**
* Gets the index from which the columns to prepend will be taken.
*
* @return The index from which the columns to prepend will be taken.
*/
protected Index getIndex()
{
return m_index;
}
/**
* @return The query mapping for the specified index.
*/
protected Object getMapping()
{
return m_mapping;
}
}
/**
* Return a table representing a join required for the match (e.g. CONTAINS) operation.
* The table must have its primary index set to the appropriate column names.
* @param column The column to do the full-text search on.
* @param expression The full-text search expression to search for.
* @return The table required in join or null if none required.
*/
public Table getMatchJoin(Column column, Pair expression)
{
return null;
}
/**
* Sets proper fetch size for a prepared statement
* @param stmt The statement to modify
* @param query Query in use for statement
* @throws SQLException if statement modification fails
*/
public void setFetchSize(PreparedStatement stmt, Query query) throws SQLException
{
if (query.getMaxCount() >= 0)
{
stmt.setFetchSize(Math.min(256, query.getMaxCount() + query.getOffset()));
}
}
/**
* Visitor for determining whether a given operator is supported.
*/
protected static class SupportedOperatorVisitor implements Operator.Visitor
{
// attributes
/**
* The operator parent.
*/
protected Operator m_parent;
// operations
/**
* Determines if an operator is supported by the database.
* @param op The operator to test.
* @return True if the operator is supported.
*/
public boolean isSupported(Operator op)
{
m_parent = op;
try
{
return op.visit(this, Operator.VISIT_PREORDER);
}
finally
{
m_parent = null;
}
}
/**
* @see nexj.core.persistence.Operator.Visitor#isEligible(nexj.core.persistence.Operator)
*/
public boolean isEligible(Operator op)
{
return op.getParent() == m_parent;
}
/**
* @see nexj.core.persistence.Operator.Visitor#visit(nexj.core.persistence.Operator)
*/
public boolean visit(Operator op)
{
if (op instanceof AttributeOperator && op.getParent() == m_parent)
{
AttributeOperator aop = (AttributeOperator)op;
if (!aop.isNoConversion() && aop.getSource() instanceof Field)
{
Field field = (Field)aop.getSource();
if (field.getAttribute() != null)
{
AttributeMapping mapping = field.getAttributeMapping();
if (mapping instanceof RelationalPrimitiveMapping &&
((RelationalPrimitiveMapping)mapping).getColumn().isPostConverted())
{
return false;
}
}
}
}
return true;
}
}
}