[#910] Add ExecuteListener extension to allow for overriding exception

translator to handle vendor-specific error codes
This commit is contained in:
Lukas Eder 2012-05-01 14:39:40 +02:00
parent eaf66c19d2
commit 97e8fbc9f0
20 changed files with 226 additions and 37 deletions

View File

@ -53,7 +53,6 @@ import java.util.List;
import java.util.Queue;
import org.jooq.ExecuteContext;
import org.jooq.ExecuteListener;
import org.jooq.ExecuteType;
import org.jooq.Field;
import org.jooq.Result;
@ -61,6 +60,7 @@ import org.jooq.TableRecord;
import org.jooq.UpdatableRecord;
import org.jooq.conf.Settings;
import org.jooq.conf.SettingsTools;
import org.jooq.impl.DefaultExecuteListener;
import org.jooq.impl.Factory;
import org.jooq.test.BaseTest;
import org.jooq.test.jOOQAbstractTest;
@ -92,6 +92,26 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
super(delegate);
}
@Test
public void testExecuteListenerCustomException() throws Exception {
Factory create = create(new Settings()
.withExecuteListeners(CustomExceptionListener.class.getName()));
try {
create.fetch("invalid sql");
}
catch (E e) {
assertEquals("ERROR", e.getMessage());
}
}
public static class CustomExceptionListener extends DefaultExecuteListener {
@Override
public void exception(ExecuteContext ctx) {
ctx.exception(new E("ERROR"));
}
}
@Test
public void testExecuteListenerOnResultQuery() throws Exception {
Factory create = create(new Settings()
@ -129,7 +149,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
assertEquals(2, result.size());
}
public static class ResultQueryListener implements ExecuteListener {
public static class ResultQueryListener extends DefaultExecuteListener {
// A counter that is incremented in callback methods
private static int callbackCount = 0;
@ -412,6 +432,18 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
}
}
static class E extends RuntimeException {
/**
* Generated UID
*/
private static final long serialVersionUID = 594781555404278995L;
public E(String message) {
super(message);
}
}
@Test
public void testExecuteListenerOnBatchSingle() {
if (!executePreparedStatements(create().getSettings())) {
@ -450,7 +482,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
assertEquals(14, BatchSingleListener.end);
}
public static class BatchSingleListener implements ExecuteListener {
public static class BatchSingleListener extends DefaultExecuteListener {
// A counter that is incremented in callback methods
private static int callbackCount = 0;
@ -672,7 +704,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
assertEquals(20, BatchMultipleListener.end);
}
public static class BatchMultipleListener implements ExecuteListener {
public static class BatchMultipleListener extends DefaultExecuteListener {
// A counter that is incremented in callback methods
private static int callbackCount = 0;

View File

@ -413,7 +413,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
context.statement().setInt(context.nextIndex(), 2);
}
catch (SQLException e) {
throw translate("CustomCondition.bind", getSQL(), e);
throw translate(getSQL(), e);
}
}
};
@ -443,7 +443,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
context.statement().setInt(context.nextIndex(), 3);
}
catch (SQLException e) {
throw translate("CustomCondition.bind", getSQL(), e);
throw translate(getSQL(), e);
}
}
};

View File

@ -1487,6 +1487,11 @@ public abstract class jOOQAbstractTest<
new ExecuteListenerTests(this).testExecuteListenerOnResultQuery();
}
@Test
public void testExecuteListenerCustomException() throws Exception {
new ExecuteListenerTests(this).testExecuteListenerCustomException();
}
@Test
public void testExecuteListenerOnBatchSingle() throws Exception {
new ExecuteListenerTests(this).testExecuteListenerOnBatchSingle();

View File

@ -38,8 +38,10 @@ package org.jooq;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.jooq.conf.StatementType;
import org.jooq.exception.DataAccessException;
/**
* A context object for {@link Query} execution passed to registered
@ -104,8 +106,9 @@ public interface ExecuteContext extends Configuration {
String sql();
/**
* Override the SQL statement that is being executed. This may have no
* effect, if called at the wrong moment.
* Override the SQL statement that is being executed.
* <p>
* This may have no effect, if called at the wrong moment.
*
* @see ExecuteListener#renderEnd(ExecuteContext)
* @see ExecuteListener#prepareStart(ExecuteContext)
@ -133,8 +136,9 @@ public interface ExecuteContext extends Configuration {
Connection getConnection();
/**
* Override the {@link Connection} that is being used for execution. This
* may have no effect, if called at the wrong moment.
* Override the {@link Connection} that is being used for execution.
* <p>
* This may have no effect, if called at the wrong moment.
*
* @see ExecuteListener#start(ExecuteContext)
*/
@ -161,8 +165,9 @@ public interface ExecuteContext extends Configuration {
PreparedStatement statement();
/**
* Override the {@link PreparedStatement} that is being executed. This may
* have no effect, if called at the wrong moment.
* Override the {@link PreparedStatement} that is being executed.
* <p>
* This may have no effect, if called at the wrong moment.
*
* @see ExecuteListener#prepareEnd(ExecuteContext)
* @see ExecuteListener#bindStart(ExecuteContext)
@ -176,8 +181,9 @@ public interface ExecuteContext extends Configuration {
ResultSet resultSet();
/**
* Override the {@link ResultSet} that is being fetched. This may have no
* effect, if called at the wrong moment.
* Override the {@link ResultSet} that is being fetched.
* <p>
* This may have no effect, if called at the wrong moment.
*
* @see ExecuteListener#executeEnd(ExecuteContext)
* @see ExecuteListener#fetchStart(ExecuteContext)
@ -205,4 +211,31 @@ public interface ExecuteContext extends Configuration {
* Calling this has no effect. It is being used by jOOQ internally.
*/
void result(Result<?> result);
/**
* The {@link RuntimeException} being thrown.
*/
RuntimeException exception();
/**
* Override the {@link RuntimeException} being thrown.
* <p>
* This may have no effect, if called at the wrong moment.
*/
void exception(RuntimeException e);
/**
* The {@link SQLException} that was thrown by the database.
*/
SQLException sqlException();
/**
* Override the {@link SQLException} being thrown.
* <p>
* Any <code>SQLException</code> will be wrapped by jOOQ using an unchecked
* {@link DataAccessException}. To have jOOQ throw your own custom
* {@link RuntimeException}, use {@link #exception(RuntimeException)}
* instead. This may have no effect, if called at the wrong moment.
*/
void sqlException(SQLException e);
}

View File

@ -38,6 +38,7 @@ package org.jooq;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
@ -56,11 +57,11 @@ import org.jooq.tools.StopWatchListener;
* <code>Settings</code> to
* {@link Factory#Factory(java.sql.Connection, SQLDialect, Settings)}. Advanced
* <code>ExecuteListeners</code> can also provide custom implementations of
* {@link Connection}, {@link PreparedStatement} and {@link ResultSet} to jOOQ
* in apropriate methods. For convenience, consider extending
* {@link DefaultExecuteListener} instead of implementing this interface. This
* will prevent compilation errors in future versions of jOOQ, when this
* interface might get new methods.
* {@link Connection}, {@link PreparedStatement}, {@link ResultSet},
* {@link SQLException} or {@link RuntimeException} to jOOQ in apropriate
* methods. For convenience, consider extending {@link DefaultExecuteListener}
* instead of implementing this interface. This will prevent compilation errors
* in future versions of jOOQ, when this interface might get new methods.
* <p>
* The following table explains how every type of statement / operation invokes
* callback methods in the correct order for all registered
@ -220,6 +221,15 @@ import org.jooq.tools.StopWatchListener;
* <td>Yes, 1x</td>
* <td>Yes, 1x</td>
* </tr>
* <tr>
* <td> {@link #exception(ExecuteContext)}</td>
* <td>Maybe, 1x</td>
* <td>Maybe, 1x</td>
* <td>Maybe, 1x</td>
* <td>Maybe, 1x</td>
* <td>Maybe, 1x</td>
* <td>Maybe, 1x</td>
* </tr>
* </table>
* <br/>
* <h3>Legend:</h3>
@ -236,7 +246,8 @@ import org.jooq.tools.StopWatchListener;
* </ol>
* <p>
* If nothing is specified, the default is to use {@link LoggerListener} and
* {@link StopWatchListener} as the only event listeners.
* {@link StopWatchListener} as the only event listeners, as configured in
* {@link Settings#isExecuteLogging()}
*
* @author Lukas Eder
*/
@ -808,7 +819,7 @@ public interface ExecuteListener {
void fetchEnd(ExecuteContext ctx);
/**
* Called at the end of the execution lifecycle..
* Called at the end of the execution lifecycle.
* <p>
* Available attributes from <code>ExecuteContext</code>:
* <ul>
@ -840,7 +851,7 @@ public interface ExecuteListener {
* Note that the <code>Statement</code> is already closed!</li>
* <li> {@link ExecuteContext#resultSet()}: The <code>ResultSet</code> that
* was fetched or <code>null</code>, if no result set was fetched. Note that
* if any <code>ResultSet</code> is already closed!</li>
* the <code>ResultSet</code> may already be closed!</li>
* <li> {@link ExecuteContext#record()}: The last <code>Record</code> that
* was fetched or null if no records were fetched.</li>
* <li> {@link ExecuteContext#result()}: The last set of records that were
@ -848,4 +859,50 @@ public interface ExecuteListener {
* </ul>
*/
void end(ExecuteContext ctx);
/**
* Called in the event of an exception at any moment of the execution
* lifecycle.
* <p>
* Available attributes from <code>ExecuteContext</code>:
* <ul>
* <li> {@link ExecuteContext#getConnection()}: The connection used for
* execution</li>
* <li> {@link ExecuteContext#configuration()}: The execution configuration</li>
* <li> {@link ExecuteContext#query()}: The <code>Query</code> object, if a
* jOOQ query is being executed or <code>null</code> otherwise</li>
* <li> {@link ExecuteContext#routine()}: The <code>Routine</code> object, if
* a jOOQ routine is being executed or <code>null</code> otherwise</li>
* <li> {@link ExecuteContext#sql()}: The rendered <code>SQL</code> statement
* that is about to be executed, or <code>null</code> if the
* <code>SQL</code> statement is unknown..</li>
* <li> {@link ExecuteContext#statement()}: The
* <code>PreparedStatement</code> that is about to be executed, or
* <code>null</code> if no statement is known to jOOQ. This can be any of
* the following: <br/>
* <br/>
* <ul>
* <li>A <code>java.sql.PreparedStatement</code> from your JDBC driver when
* a jOOQ <code>Query</code> is being executed as
* {@link StatementType#PREPARED_STATEMENT}</li>
* <li>A <code>java.sql.Statement</code> from your JDBC driver wrapped in a
* <code>java.sql.PreparedStatement</code> when your jOOQ <code>Query</code>
* is being executed as {@link StatementType#STATIC_STATEMENT}</li>
* <li>A <code>java.sql.CallableStatement</code> when you are executing a
* jOOQ <code>Routine</code></li>
* </ul>
* Note that the <code>Statement</code> may be closed!</li>
* <li> {@link ExecuteContext#resultSet()}: The <code>ResultSet</code> that
* was fetched or <code>null</code>, if no result set was fetched. Note that
* the <code>ResultSet</code> may already be closed!</li>
* <li> {@link ExecuteContext#record()}: The last <code>Record</code> that
* was fetched or null if no records were fetched.</li>
* <li> {@link ExecuteContext#result()}: The last set of records that were
* fetched or null if no records were fetched.</li>
* <li> {@link ExecuteContext#exception()}: The {@link RuntimeException} that
* is about to be thrown</li>
* <li> {@link ExecuteContext#sqlException()}: The {@link SQLException} that
* was thrown by the database</li> </ul>
*/
void exception(ExecuteContext ctx);
}

View File

@ -146,7 +146,7 @@ abstract class AbstractBindContext extends AbstractContext<BindContext> implemen
return bindValue0(value, type);
}
catch (SQLException e) {
throw Util.translate("DefaultBindContext.bindValue", null, e);
throw Util.translate(null, e);
}
}

View File

@ -147,7 +147,9 @@ abstract class AbstractQuery extends AbstractQueryPart implements Query {
return result;
}
catch (SQLException e) {
throw Util.translate("AbstractQuery.execute", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
finally {
if (!keepStatementOpen()) {

View File

@ -281,8 +281,19 @@ abstract class AbstractQueryPart implements QueryPartInternal, AttachableInterna
/**
* Internal convenience method
*
* @deprecated - 2.3.0 - Do not reuse
*/
@SuppressWarnings("unused")
@Deprecated
protected final DataAccessException translate(String task, String sql, SQLException e) {
return Util.translate(task, sql, e);
return translate(sql, e);
}
/**
* Internal convenience method
*/
protected final DataAccessException translate(String sql, SQLException e) {
return Util.translate(sql, e);
}
}

View File

@ -329,7 +329,9 @@ public abstract class AbstractRoutine<T> extends AbstractSchemaProviderQueryPart
return 0;
}
catch (SQLException e) {
throw translate("AbstractRoutine.executeCallableStatement", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
finally {
Util.safeClose(listener, ctx);

View File

@ -93,7 +93,9 @@ class BatchMultiple implements Batch {
return result;
}
catch (SQLException e) {
throw Util.translate("BatchMultiple.execute", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
finally {
Util.safeClose(listener, ctx);

View File

@ -131,7 +131,9 @@ class BatchSingle implements BatchBindStep {
return result;
}
catch (SQLException e) {
throw Util.translate("BatchSingle.execute", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
finally {
Util.safeClose(listener, ctx);

View File

@ -1267,7 +1267,9 @@ class CursorImpl<R extends Record> implements Cursor<R> {
}
}
catch (SQLException e) {
throw Util.translate("Cursor.fetch", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
// Conveniently close cursors and underlying objects after the last

View File

@ -39,6 +39,7 @@ import java.sql.Blob;
import java.sql.Clob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@ -68,7 +69,7 @@ class DefaultExecuteContext extends AbstractConfiguration implements ExecuteCont
*/
private static final long serialVersionUID = -6653474082935089963L;
// Persistent attributes
// Persistent attributes (repeatable)
private final Query query;
private final Routine<?> routine;
private String sql;
@ -76,16 +77,18 @@ class DefaultExecuteContext extends AbstractConfiguration implements ExecuteCont
private final Query[] batchQueries;
private final String[] batchSQL;
// Transient attributes
// Transient attributes (created afresh per execution)
private transient PreparedStatement statement;
private transient ResultSet resultSet;
private transient Record record;
private transient Result<?> result;
private transient SQLException sqlException;
private transient RuntimeException exception;
// ------------------------------------------------------------------------
// XXX: Static utility methods for handling blob / clob lifecycle
// ------------------------------------------------------------------------
private static final ThreadLocal<List<Blob>> BLOBS = new ThreadLocal<List<Blob>>();
private static final ThreadLocal<List<Clob>> CLOBS = new ThreadLocal<List<Clob>>();
@ -335,4 +338,25 @@ class DefaultExecuteContext extends AbstractConfiguration implements ExecuteCont
public final Result<?> result() {
return result;
}
@Override
public final RuntimeException exception() {
return exception;
}
@Override
public final void exception(RuntimeException e) {
this.exception = e;
}
@Override
public final SQLException sqlException() {
return sqlException;
}
@Override
public final void sqlException(SQLException e) {
this.sqlException = e;
exception(Util.translate(sql(), e));
}
}

View File

@ -96,4 +96,7 @@ public class DefaultExecuteListener implements ExecuteListener {
@Override
public void end(ExecuteContext ctx) {}
@Override
public void exception(ExecuteContext ctx) {}
}

View File

@ -187,4 +187,11 @@ class ExecuteListeners implements ExecuteListener {
listener.end(ctx);
}
}
@Override
public final void exception(ExecuteContext ctx) {
for (ExecuteListener listener : listeners) {
listener.exception(ctx);
}
}
}

View File

@ -1238,7 +1238,9 @@ public class Factory implements FactoryOperations {
return new CursorImpl<Record>(ctx, listener, fields).fetch();
}
catch (SQLException e) {
throw Util.translate("Factory.fetch", ctx.sql(), e);
ctx.sqlException(e);
listener.exception(ctx);
throw ctx.exception();
}
}

View File

@ -434,7 +434,7 @@ class LoaderImpl<R extends TableRecord<R>> implements
// SQLExceptions originating from rollbacks or commits are always fatal
// They are propagated, and not swallowed
catch (SQLException e) {
throw Util.translate("LoaderImpl.executeCSV", null, e);
throw Util.translate(null, e);
}
finally {
reader.close();

View File

@ -119,7 +119,7 @@ class MetaDataFieldProvider implements FieldProvider, Serializable {
}
}
catch (SQLException e) {
throw Util.translate("MetaFieldProvider.init", null, e);
throw Util.translate(null, e);
}
meta = null;

View File

@ -491,8 +491,8 @@ final class Util {
/**
* Translate a {@link SQLException} to a {@link DataAccessException}
*/
static final DataAccessException translate(String task, String sql, SQLException e) {
String message = task + "; SQL [" + sql + "]; " + e.getMessage();
static final DataAccessException translate(String sql, SQLException e) {
String message = "SQL [" + sql + "]; " + e.getMessage();
return new DataAccessException(message, e);
}

View File

@ -127,4 +127,9 @@ public class StopWatchListener implements ExecuteListener {
public void end(ExecuteContext ctx) {
watch.splitDebug("Finishing");
}
@Override
public void exception(ExecuteContext ctx) {
watch.splitDebug("Exception");
}
}