REOPENED - BATCH-970: JdbcCursorItemReader executes outside of main transaction
added non-recursive implementations of javax.sql.Wrapper methods to the ExtendedConnectionDataSourceProxy to keep both JDK 5 and 6 happy
This commit is contained in:
@@ -26,47 +26,60 @@ import java.sql.SQLException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.jdbc.datasource.ConnectionProxy;
|
||||
import org.springframework.jdbc.datasource.DataSourceUtils;
|
||||
import org.springframework.jdbc.datasource.SmartDataSource;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link SmartDataSource} that is capable of keeping a single JDBC Connection
|
||||
* which is NOT closed after each use even if {@link Connection#close()} is called.
|
||||
* Implementation of {@link SmartDataSource} that is capable of keeping a single
|
||||
* JDBC Connection which is NOT closed after each use even if
|
||||
* {@link Connection#close()} is called.
|
||||
*
|
||||
* The connection can be kept open over multiple transactions when used together with any of Spring's
|
||||
* {@link org.springframework.transaction.PlatformTransactionManager} implementations.
|
||||
* The connection can be kept open over multiple transactions when used together
|
||||
* with any of Spring's
|
||||
* {@link org.springframework.transaction.PlatformTransactionManager}
|
||||
* implementations.
|
||||
*
|
||||
* <p>Loosely based on the SingleConnectionDataSource implementation in Spring Core. Intended
|
||||
* to be used with the {@link JdbcCursorItemReader} to provide a connection that remains
|
||||
* open across transaction boundaries, It remains open for the life of the cursor, and can be
|
||||
* shared with the main transaction of the rest of the step processing.
|
||||
*
|
||||
* <p>Once close suppression has been turned on for a connection, it will be returned for the first
|
||||
* {@link #getConnection()} call. Any subsequent calls to {@link #getConnection()} will retrieve a
|
||||
* new connection from the wrapped {@link DataSource} until the {@link DataSourceUtils} queries
|
||||
* whether the connection should be closed or not by calling {@link #shouldClose(Connection)} for
|
||||
* the close-suppressed {@link Connection}. At that point the cycle starts over again, and the next
|
||||
* {@link #getConnection()} call will have the {@link Connection} that is being close-suppressed
|
||||
* returned. This allows the use of the close-suppressed {@link Connection} to be the main
|
||||
* {@link Connection} for an extended data access process. The close suppression is turned off by
|
||||
* calling {@link #stopCloseSuppression(Connection)}.
|
||||
* <p>
|
||||
* Loosely based on the SingleConnectionDataSource implementation in Spring
|
||||
* Core. Intended to be used with the {@link JdbcCursorItemReader} to provide a
|
||||
* connection that remains open across transaction boundaries, It remains open
|
||||
* for the life of the cursor, and can be shared with the main transaction of
|
||||
* the rest of the step processing.
|
||||
*
|
||||
* <p>This class is not multi-threading capable.
|
||||
* <p>
|
||||
* Once close suppression has been turned on for a connection, it will be
|
||||
* returned for the first {@link #getConnection()} call. Any subsequent calls to
|
||||
* {@link #getConnection()} will retrieve a new connection from the wrapped
|
||||
* {@link DataSource} until the {@link DataSourceUtils} queries whether the
|
||||
* connection should be closed or not by calling
|
||||
* {@link #shouldClose(Connection)} for the close-suppressed {@link Connection}.
|
||||
* At that point the cycle starts over again, and the next
|
||||
* {@link #getConnection()} call will have the {@link Connection} that is being
|
||||
* close-suppressed returned. This allows the use of the close-suppressed
|
||||
* {@link Connection} to be the main {@link Connection} for an extended data
|
||||
* access process. The close suppression is turned off by calling
|
||||
* {@link #stopCloseSuppression(Connection)}.
|
||||
*
|
||||
* <p>
|
||||
* This class is not multi-threading capable.
|
||||
*
|
||||
* <p>
|
||||
* The connection returned will be a close-suppressing proxy instead of the
|
||||
* physical {@link Connection}. Be aware that you will not be able to cast this
|
||||
* to a native <code>OracleConnection</code> or the like anymore; you need to
|
||||
* use a {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor}.
|
||||
*
|
||||
* <p>The connection returned will be a close-suppressing proxy instead of the physical
|
||||
* {@link Connection}. Be aware that you will not be able to cast this to a native
|
||||
* <code>OracleConnection</code> or the like anymore; you need to use a
|
||||
* {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor}.
|
||||
*
|
||||
* @author Thomas Risberg
|
||||
* @see #getConnection()
|
||||
* @see java.sql.Connection#close()
|
||||
* @see DataSourceUtils#releaseConnection
|
||||
* @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor
|
||||
*/
|
||||
public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
public class ExtendedConnectionDataSourceProxy implements SmartDataSource, InitializingBean {
|
||||
|
||||
/** Provided DataSource */
|
||||
private DataSource dataSource;
|
||||
@@ -87,7 +100,8 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that takes as a parameter with the {&link DataSource} to be wrapped.
|
||||
* Constructor that takes as a parameter with the {&link DataSource} to be
|
||||
* wrapped.
|
||||
*/
|
||||
public ExtendedConnectionDataSourceProxy(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
@@ -114,9 +128,11 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status of close suppression being activated for a given {@link Connection}
|
||||
* Return the status of close suppression being activated for a given
|
||||
* {@link Connection}
|
||||
*
|
||||
* @param connection the {@link Connection} that the close suppression status is requested for
|
||||
* @param connection the {@link Connection} that the close suppression
|
||||
* status is requested for
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean isCloseSuppressionActive(Connection connection) {
|
||||
@@ -125,23 +141,25 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
return connection.equals(closeSuppressedConnection) ? true : false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param connection the {@link Connection} that close suppression is requested for
|
||||
* @param connection the {@link Connection} that close suppression is
|
||||
* requested for
|
||||
*/
|
||||
public void startCloseSuppression(Connection connection) {
|
||||
synchronized (this.connectionMonitor) {
|
||||
closeSuppressedConnection = connection;
|
||||
if(TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
borrowedConnection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param connection the {@link Connection} that close suppression should be turned off for
|
||||
* @param connection the {@link Connection} that close suppression should be
|
||||
* turned off for
|
||||
*/
|
||||
public void stopCloseSuppression(Connection connection) {
|
||||
synchronized (this.connectionMonitor) {
|
||||
@@ -149,7 +167,7 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
borrowedConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
synchronized (this.connectionMonitor) {
|
||||
return initConnection(null, null);
|
||||
@@ -204,29 +222,27 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given Connection with a proxy that delegates every method call to it
|
||||
* but suppresses close calls.
|
||||
* Wrap the given Connection with a proxy that delegates every method call
|
||||
* to it but suppresses close calls.
|
||||
* @param target the original Connection to wrap
|
||||
* @return the wrapped Connection
|
||||
*/
|
||||
protected Connection getCloseSuppressingConnectionProxy(Connection target) {
|
||||
return (Connection) Proxy.newProxyInstance(
|
||||
ConnectionProxy.class.getClassLoader(),
|
||||
new Class[] {ConnectionProxy.class},
|
||||
new CloseSuppressingInvocationHandler(target, this));
|
||||
return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(),
|
||||
new Class[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(target, this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invocation handler that suppresses close calls on JDBC Connections until the associated instance of the
|
||||
* ExtendedConnectionDataSourceProxy determines the connection should actually be closed.
|
||||
* Invocation handler that suppresses close calls on JDBC Connections until
|
||||
* the associated instance of the ExtendedConnectionDataSourceProxy
|
||||
* determines the connection should actually be closed.
|
||||
*/
|
||||
private static class CloseSuppressingInvocationHandler implements InvocationHandler {
|
||||
|
||||
private final Connection target;
|
||||
|
||||
|
||||
private final ExtendedConnectionDataSourceProxy dataSource;
|
||||
|
||||
|
||||
public CloseSuppressingInvocationHandler(Connection target, ExtendedConnectionDataSourceProxy dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
this.target = target;
|
||||
@@ -244,8 +260,9 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
return new Integer(System.identityHashCode(proxy));
|
||||
}
|
||||
else if (method.getName().equals("close")) {
|
||||
// Handle close method: don't pass the call on if we are suppressing close calls.
|
||||
if (dataSource.completeCloseCall((Connection)proxy)) {
|
||||
// Handle close method: don't pass the call on if we are
|
||||
// suppressing close calls.
|
||||
if (dataSource.completeCloseCall((Connection) proxy)) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
@@ -254,7 +271,8 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
}
|
||||
else if (method.getName().equals("getTargetConnection")) {
|
||||
// Handle getTargetConnection method: return underlying Connection.
|
||||
// Handle getTargetConnection method: return underlying
|
||||
// Connection.
|
||||
return this.target;
|
||||
}
|
||||
|
||||
@@ -267,5 +285,39 @@ public class ExtendedConnectionDataSourceProxy implements SmartDataSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs only a 'shallow' non-recursive check of self's and delegate's
|
||||
* class to retain Java 5 compatibility.
|
||||
*/
|
||||
public boolean isWrapperFor(Class<?> iface) throws SQLException {
|
||||
if (iface.isAssignableFrom(SmartDataSource.class) || iface.isAssignableFrom(dataSource.getClass())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either self or delegate (in this order) if one of them can be
|
||||
* cast to supplied parameter class. Does *not* support recursive unwrapping
|
||||
* of the delegate to retain Java 5 compatibility.
|
||||
*/
|
||||
public <T> T unwrap(Class<T> iface) throws SQLException {
|
||||
if (iface.isAssignableFrom(SmartDataSource.class)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
T casted = (T) this;
|
||||
return casted;
|
||||
}
|
||||
else if (iface.isAssignableFrom(dataSource.getClass())) {
|
||||
@SuppressWarnings("unchecked")
|
||||
T casted = (T) dataSource;
|
||||
return casted;
|
||||
}
|
||||
throw new SQLException("Unsupported class " + iface.getSimpleName());
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(dataSource);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.springframework.batch.item.database;
|
||||
import static org.easymock.EasyMock.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -16,6 +17,7 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DataSourceUtils;
|
||||
import org.springframework.jdbc.datasource.SmartDataSource;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
@@ -31,24 +33,24 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
DataSource ds = createMock(DataSource.class);
|
||||
|
||||
expect(ds.getConnection()).andReturn(con); // con1
|
||||
con.close();
|
||||
con.close();
|
||||
expect(ds.getConnection()).andReturn(con); // con2
|
||||
con.close();
|
||||
|
||||
expect(ds.getConnection()).andReturn(con); // con3
|
||||
|
||||
expect(ds.getConnection()).andReturn(con); // con3
|
||||
con.close(); // con3
|
||||
expect(ds.getConnection()).andReturn(con); // con4
|
||||
expect(ds.getConnection()).andReturn(con); // con4
|
||||
con.close(); // con4
|
||||
|
||||
replay(ds);
|
||||
replay(con);
|
||||
|
||||
|
||||
final ExtendedConnectionDataSourceProxy csds = new ExtendedConnectionDataSourceProxy(ds);
|
||||
|
||||
|
||||
Connection con1 = csds.getConnection();
|
||||
Connection con2 = csds.getConnection();
|
||||
assertNotSame("shouldn't be the same connection", con1, con2);
|
||||
|
||||
|
||||
assertTrue("should be able to close connection", csds.shouldClose(con1));
|
||||
con1.close();
|
||||
assertTrue("should be able to close connection", csds.shouldClose(con2));
|
||||
@@ -71,27 +73,27 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
con3.close();
|
||||
assertTrue("should be able to close connection", csds.shouldClose(con4));
|
||||
con4.close();
|
||||
|
||||
|
||||
verify(ds);
|
||||
verify(con);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testOperationWithDirectCloseCall() throws SQLException {
|
||||
Connection con = createMock(Connection.class);
|
||||
DataSource ds = createMock(DataSource.class);
|
||||
|
||||
expect(ds.getConnection()).andReturn(con); // con1
|
||||
con.close();
|
||||
con.close();
|
||||
expect(ds.getConnection()).andReturn(con); // con2
|
||||
con.close();
|
||||
|
||||
|
||||
replay(ds);
|
||||
replay(con);
|
||||
|
||||
|
||||
final ExtendedConnectionDataSourceProxy csds = new ExtendedConnectionDataSourceProxy(ds);
|
||||
|
||||
|
||||
Connection con1 = csds.getConnection();
|
||||
csds.startCloseSuppression(con1);
|
||||
Connection con1_1 = csds.getConnection();
|
||||
@@ -108,7 +110,7 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
con1.close();
|
||||
assertTrue("should be able to close connection", csds.shouldClose(con2));
|
||||
con2.close();
|
||||
|
||||
|
||||
verify(ds);
|
||||
verify(con);
|
||||
|
||||
@@ -116,7 +118,7 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
|
||||
@Test
|
||||
public void testSupressOfCloseWithJdbcTemplate() throws Exception {
|
||||
|
||||
|
||||
Connection con = createMock(Connection.class);
|
||||
DataSource ds = createMock(DataSource.class);
|
||||
Statement stmt = createMock(Statement.class);
|
||||
@@ -175,7 +177,7 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
replay(stmt);
|
||||
replay(con);
|
||||
replay(ds);
|
||||
|
||||
|
||||
final ExtendedConnectionDataSourceProxy csds = new ExtendedConnectionDataSourceProxy();
|
||||
csds.setDataSource(ds);
|
||||
PlatformTransactionManager tm = new DataSourceTransactionManager(csds);
|
||||
@@ -183,39 +185,35 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
final TransactionTemplate tt2 = new TransactionTemplate(tm);
|
||||
tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
final JdbcTemplate template = new JdbcTemplate(csds);
|
||||
|
||||
|
||||
Connection connection = DataSourceUtils.getConnection(csds);
|
||||
csds.startCloseSuppression(connection);
|
||||
tt.execute(
|
||||
new TransactionCallback() {
|
||||
tt.execute(new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select baz from bar");
|
||||
template.queryForList("select foo from bar");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
tt.execute(new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select ham from foo");
|
||||
tt2.execute(new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select baz from bar");
|
||||
template.queryForList("select foo from bar");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
tt.execute(
|
||||
new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select ham from foo");
|
||||
tt2.execute(
|
||||
new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select 1 from eggs");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
template.queryForList("select more, ham from foo");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
tt.execute(
|
||||
new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select spam from ham");
|
||||
template.queryForList("select 1 from eggs");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
template.queryForList("select more, ham from foo");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
tt.execute(new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
template.queryForList("select spam from ham");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
csds.stopCloseSuppression(connection);
|
||||
DataSourceUtils.releaseConnection(connection, csds);
|
||||
template.queryForList("select egg from bar");
|
||||
@@ -226,4 +224,112 @@ public class ExtendedConnectionDataSourceProxyTests {
|
||||
verify(ds);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void delegateIsRequired() throws Exception {
|
||||
|
||||
ExtendedConnectionDataSourceProxy tested = new ExtendedConnectionDataSourceProxy(null);
|
||||
tested.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unwrapForUnsupportedInterface() throws Exception {
|
||||
|
||||
ExtendedConnectionDataSourceProxy tested = new ExtendedConnectionDataSourceProxy(new DataSourceStub());
|
||||
|
||||
assertFalse(tested.isWrapperFor(Unsupported.class));
|
||||
|
||||
try {
|
||||
tested.unwrap(Unsupported.class);
|
||||
fail();
|
||||
}
|
||||
catch (SQLException expected) {
|
||||
// this would be the correct behavior in a Java6-only recursive implementation
|
||||
// assertEquals(DataSourceStub.UNWRAP_ERROR_MESSAGE, expected.getMessage());
|
||||
assertEquals("Unsupported class " + Unsupported.class.getSimpleName(), expected.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unwrapForSupportedInterface() throws Exception {
|
||||
|
||||
DataSourceStub ds = new DataSourceStub();
|
||||
ExtendedConnectionDataSourceProxy tested = new ExtendedConnectionDataSourceProxy(ds);
|
||||
|
||||
assertTrue(tested.isWrapperFor(Supported.class));
|
||||
assertEquals(ds, tested.unwrap(Supported.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unwrapForSmartDataSource() throws Exception {
|
||||
|
||||
ExtendedConnectionDataSourceProxy tested = new ExtendedConnectionDataSourceProxy(new DataSourceStub());
|
||||
|
||||
assertTrue(tested.isWrapperFor(DataSource.class));
|
||||
assertEquals(tested, tested.unwrap(DataSource.class));
|
||||
|
||||
assertTrue(tested.isWrapperFor(SmartDataSource.class));
|
||||
assertEquals(tested, tested.unwrap(SmartDataSource.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface implemented by the wrapped DataSource
|
||||
*/
|
||||
private static interface Supported {
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface *not* implemented by the wrapped DataSource
|
||||
*/
|
||||
private static interface Unsupported {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub for a wrapped DataSource that implements additional interface. Its
|
||||
* purpose is testing of {@link DataSource#isWrapperFor(Class)} and
|
||||
* {@link DataSource#unwrap(Class)} methods.
|
||||
*/
|
||||
private static class DataSourceStub implements DataSource, Supported {
|
||||
|
||||
private static final String UNWRAP_ERROR_MESSAGE = "supplied type is not implemented by this class";
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public PrintWriter getLogWriter() throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public int getLoginTimeout() throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void setLogWriter(PrintWriter out) throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void setLoginTimeout(int seconds) throws SQLException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public boolean isWrapperFor(Class<?> iface) throws SQLException {
|
||||
if (iface.equals(Supported.class) || (iface.equals(DataSource.class))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T unwrap(Class<T> iface) throws SQLException {
|
||||
if (iface.equals(Supported.class) || iface.equals(DataSource.class)) {
|
||||
return (T) this;
|
||||
}
|
||||
throw new SQLException(UNWRAP_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user