org.firebirdsql.event.FBEventManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaybird Show documentation
Show all versions of jaybird Show documentation
JDBC Driver for the Firebird RDBMS
/*
* Public Firebird Java API.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.firebirdsql.event;
import org.firebirdsql.gds.EventHandle;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.impl.GDSFactory;
import org.firebirdsql.gds.impl.GDSType;
import org.firebirdsql.gds.ng.*;
import org.firebirdsql.gds.ng.listeners.DefaultDatabaseListener;
import org.firebirdsql.jdbc.FBSQLException;
import org.firebirdsql.jdbc.FirebirdConnection;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import org.firebirdsql.util.SQLExceptionChainBuilder;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* An {@link org.firebirdsql.event.EventManager} implementation to listen for database events.
*
* @author Gabriel Reid
* @author Mark Rotteveel
* @author Vasiliy Yashkov
*/
public class FBEventManager implements EventManager {
private static final Logger log = LoggerFactory.getLogger(FBEventManager.class);
private final GDSType gdsType;
private FbDatabase fbDatabase;
private final IConnectionProperties connectionProperties;
private final EventManagerBehaviour eventManagerBehaviour;
private volatile boolean connected = false;
private final Map> listenerMap = Collections.synchronizedMap(new HashMap>());
private final Map handlerMap = Collections.synchronizedMap(new HashMap());
private final BlockingQueue eventQueue = new LinkedBlockingQueue<>();
private EventDispatcher eventDispatcher;
private Thread dispatchThread;
private volatile long waitTimeout = 1000;
@SuppressWarnings("UnusedDeclaration")
public FBEventManager() {
this(GDSFactory.getDefaultGDSType());
}
public FBEventManager(GDSType gdsType) {
this.gdsType = gdsType;
connectionProperties = new FbConnectionProperties();
eventManagerBehaviour = new DefaultEventManagerBehaviour();
}
/**
* Constructs an event manager using an existing connection.
*
* @param connection Connection that unwraps to {@link FirebirdConnection}.
*/
private FBEventManager(Connection connection) throws SQLException {
this.fbDatabase = connection.unwrap(FirebirdConnection.class).getFbDatabase();
gdsType = null;
connectionProperties = fbDatabase.getConnectionProperties().asImmutable();
// NOTE In this implementation, we don't take into account pooled connections that might be closed while
// the FbDatabase instance remains in use. This means that at the moment, it is possible that the event manager
// can remain in use for longer than the Connection.
fbDatabase.addDatabaseListener(new DefaultDatabaseListener() {
@Override
public void detaching(FbDatabase database) {
try {
if (!isConnected()) return;
try {
disconnect();
} catch (SQLException e) {
log.error("Exception on disconnect of event manager on connection detaching.", e);
}
} finally {
database.removeDatabaseListener(this);
fbDatabase = null;
}
}
});
eventManagerBehaviour = new ManagedEventManagerBehaviour();
}
/**
* Creates an {@link EventManager} for a connection.
*
* The created event manager does not allow setting the properties and will instead
* throw {@link UnsupportedOperationException} for the setters.
*
*
* The returned instance is not necessarily an implementation of {@link FBEventManager}.
*
*
* @param connection
* A connection that unwraps to {@link org.firebirdsql.jdbc.FirebirdConnection}
* @return An event manager
* @throws SQLException
* When {@code connection} does not unwrap to {@link org.firebirdsql.jdbc.FirebirdConnection}
* @since 3.0.7
*/
public static EventManager createFor(Connection connection) throws SQLException {
return new FBEventManager(connection);
}
@Override
public void connect() throws SQLException {
if (connected) {
throw new IllegalStateException("Connect called while already connected");
}
eventManagerBehaviour.connectDatabase();
connected = true;
eventDispatcher = new EventDispatcher();
dispatchThread = new Thread(eventDispatcher);
dispatchThread.setDaemon(true);
dispatchThread.start();
}
@Override
public void close() throws SQLException {
if (connected) {
disconnect();
}
}
@Override
public void disconnect() throws SQLException {
if (!connected) {
throw new IllegalStateException("Disconnect called while not connected");
}
SQLExceptionChainBuilder chain = new SQLExceptionChainBuilder<>();
try {
try {
for (String eventName : new HashSet<>(handlerMap.keySet())) {
try {
unregisterListener(eventName);
} catch (SQLException e) {
chain.append(e);
} catch (Exception e) {
chain.append(new SQLException(e));
}
}
} finally {
handlerMap.clear();
listenerMap.clear();
try {
eventManagerBehaviour.disconnectDatabase();
} catch (SQLException e) {
chain.append(e);
}
connected = false;
}
} finally {
eventDispatcher.stop();
dispatchThread.interrupt();
// join the thread and wait until it dies
try {
dispatchThread.join();
} catch (InterruptedException ex) {
chain.append(new FBSQLException(ex));
} finally {
eventDispatcher = null;
dispatchThread = null;
}
}
if (chain.hasException()) throw chain.getException();
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public void setUser(String user) {
connectionProperties.setUser(user);
}
@Override
public String getUser() {
return connectionProperties.getUser();
}
@Override
public void setPassword(String password) {
connectionProperties.setPassword(password);
}
@Override
public String getPassword() {
return connectionProperties.getPassword();
}
@Override
public void setDatabase(String database) {
connectionProperties.setDatabaseName(database);
}
@Override
public String getDatabase() {
return connectionProperties.getDatabaseName();
}
@Override
public String getHost() {
return connectionProperties.getServerName();
}
@Override
public void setHost(String host) {
connectionProperties.setServerName(host);
}
@Override
public int getPort() {
return connectionProperties.getPortNumber();
}
@Override
public void setPort(int port) {
connectionProperties.setPortNumber(port);
}
@Override
public WireCrypt getWireCrypt() {
return connectionProperties.getWireCrypt();
}
@Override
public void setWireCrypt(WireCrypt wireCrypt) {
connectionProperties.setWireCrypt(wireCrypt);
}
@Override
public String getDbCryptConfig() {
return connectionProperties.getDbCryptConfig();
}
@Override
public void setDbCryptConfig(String dbCryptConfig) {
connectionProperties.setDbCryptConfig(dbCryptConfig);
}
@Override
public String getAuthPlugins() {
return connectionProperties.getAuthPlugins();
}
@Override
public void setAuthPlugins(String authPlugins) {
connectionProperties.setAuthPlugins(authPlugins);
}
@Override
public long getWaitTimeout() {
return waitTimeout;
}
@Override
public void setWaitTimeout(long waitTimeout) {
this.waitTimeout = waitTimeout;
}
public void addEventListener(String eventName, EventListener listener) throws SQLException {
if (!connected) {
throw new IllegalStateException("Can't add event listeners to disconnected EventManager");
}
if (listener == null || eventName == null) {
throw new NullPointerException();
}
synchronized (listenerMap) {
if (!listenerMap.containsKey(eventName)) {
registerListener(eventName);
listenerMap.put(eventName, new HashSet());
}
Set listenerSet = listenerMap.get(eventName);
listenerSet.add(listener);
}
}
public void removeEventListener(String eventName, EventListener listener) throws SQLException {
if (eventName == null || listener == null) {
throw new NullPointerException();
}
Set listenerSet = listenerMap.get(eventName);
if (listenerSet != null) {
listenerSet.remove(listener);
if (listenerSet.isEmpty()) {
listenerMap.remove(eventName);
unregisterListener(eventName);
}
}
}
public int waitForEvent(String eventName) throws InterruptedException, SQLException {
return waitForEvent(eventName, 0);
}
public int waitForEvent(String eventName, final int timeout) throws InterruptedException, SQLException {
if (!connected) {
throw new IllegalStateException("Can't wait for events with disconnected EventManager");
}
if (eventName == null) {
throw new NullPointerException();
}
final Object lock = new Object();
OneTimeEventListener listener = new OneTimeEventListener(lock);
try {
synchronized (lock) {
addEventListener(eventName, listener);
lock.wait(timeout);
}
} finally {
removeEventListener(eventName, listener);
}
return listener.getEventCount();
}
private void registerListener(String eventName) throws SQLException {
GdsEventHandler handler = new GdsEventHandler(eventName);
handlerMap.put(eventName, handler);
handler.register();
}
private void unregisterListener(String eventName) throws SQLException {
GdsEventHandler handler = handlerMap.get(eventName);
try {
if (handler != null) handler.unregister();
} finally {
handlerMap.remove(eventName);
}
}
private interface EventManagerBehaviour {
void connectDatabase() throws SQLException;
void disconnectDatabase() throws SQLException;
}
/**
* Default behaviour where the event manager owns the connection.
*/
private class DefaultEventManagerBehaviour implements EventManagerBehaviour {
@Override
public void connectDatabase() throws SQLException {
FbDatabaseFactory databaseFactory = GDSFactory.getDatabaseFactoryForType(gdsType);
fbDatabase = databaseFactory.connect(connectionProperties);
fbDatabase.attach();
}
@Override
public void disconnectDatabase() throws SQLException {
fbDatabase.close();
}
}
/**
* Behaviour where the lifetime of the connection used by the event manager is managed elsewhere.
*/
private class ManagedEventManagerBehaviour implements EventManagerBehaviour {
@Override
public void connectDatabase() throws SQLException {
// using existing connection
if (fbDatabase == null) {
// fbDatabase has already detached
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_notConnectedToServer)
.toFlatSQLException();
}
}
@Override
public void disconnectDatabase() {
// fbDatabase will be closed where it was opened
}
}
class GdsEventHandler implements org.firebirdsql.gds.EventHandler {
private final EventHandle eventHandle;
private boolean initialized = false;
private volatile boolean cancelled = false;
public GdsEventHandler(String eventName) throws SQLException {
eventHandle = fbDatabase.createEventHandle(eventName, this);
}
public synchronized void register() throws SQLException {
if (cancelled) {
throw new IllegalStateException("Trying to register a cancelled event handler");
}
fbDatabase.queueEvent(eventHandle);
}
public synchronized void unregister() throws SQLException {
if (cancelled) return;
fbDatabase.cancelEvent(eventHandle);
cancelled = true;
}
@Override
public synchronized void eventOccurred(EventHandle eventHandle) {
if (!cancelled) {
try {
fbDatabase.countEvents(eventHandle);
} catch (SQLException e) {
String message = "Exception processing event counts";
log.warn(message + ": " + e + "; see debug level for stacktrace");
log.debug(message, e);
}
if (initialized && !cancelled) {
eventQueue.add(new DatabaseEventImpl(eventHandle.getEventName(), eventHandle.getEventCount()));
} else {
initialized = true;
}
try {
register();
} catch (SQLException e) {
String message = "Exception registering for event";
log.warn(message + ": " + e + "; see debug level for stacktrace");
log.debug(message, e);
}
}
}
}
class EventDispatcher implements Runnable {
private volatile boolean running = true;
public void stop() {
running = false;
}
@Override
public void run() {
DatabaseEvent event;
while (running) {
try {
event = eventQueue.poll(waitTimeout, TimeUnit.MILLISECONDS);
if (event == null) continue;
synchronized (listenerMap) {
Set listenerSet = listenerMap.get(event.getEventName());
if (listenerSet != null) {
for (EventListener listener : listenerSet) {
listener.eventOccurred(event);
}
}
}
} catch (InterruptedException ie) {
// Ignore interruption; continue if not explicitly stopped
}
}
}
}
}
class OneTimeEventListener implements EventListener {
private int eventCount = -1;
private final Object lock;
public OneTimeEventListener(Object lock) {
this.lock = lock;
}
@Override
public void eventOccurred(DatabaseEvent event) {
if (eventCount == -1) {
eventCount = event.getEventCount();
}
synchronized (lock) {
lock.notifyAll();
}
}
public int getEventCount() {
return eventCount;
}
}
class DatabaseEventImpl implements DatabaseEvent {
private int eventCount;
private String eventName;
public DatabaseEventImpl(String eventName, int eventCount) {
this.eventName = eventName;
this.eventCount = eventCount;
}
@Override
public int getEventCount() {
return this.eventCount;
}
@Override
public String getEventName() {
return this.eventName;
}
@Override
public String toString() {
return "DatabaseEvent['" + eventName + " * " + eventCount + "]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy