org.tentackle.persist.Db Maven / Gradle / Ivy
/**
* Tentackle - http://www.tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.persist;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.net.URI;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.tentackle.common.Constants;
import org.tentackle.common.ExceptionHelper;
import org.tentackle.common.FileHelper;
import org.tentackle.daemon.Scavenger;
import org.tentackle.io.SocketFactoryFactory;
import org.tentackle.io.SocketFactoryType;
import org.tentackle.log.Logger;
import org.tentackle.log.Logger.Level;
import org.tentackle.log.LoggerFactory;
import org.tentackle.log.LoggerOutputStream;
import org.tentackle.pdo.BackendException;
import org.tentackle.pdo.DefaultSessionTaskDispatcher;
import org.tentackle.pdo.LoginFailedException;
import org.tentackle.pdo.Pdo;
import org.tentackle.pdo.PersistenceException;
import org.tentackle.pdo.RemoteSession;
import org.tentackle.pdo.Session;
import org.tentackle.pdo.SessionCloseHandler;
import org.tentackle.pdo.SessionClosedException;
import org.tentackle.pdo.SessionInfo;
import org.tentackle.pdo.SessionPool;
import org.tentackle.pdo.SessionTaskDispatcher;
import org.tentackle.pdo.TransactionEnvelope;
import org.tentackle.persist.rmi.DbRemoteDelegate;
import org.tentackle.persist.rmi.RemoteDbConnection;
import org.tentackle.persist.rmi.RemoteDbSession;
import org.tentackle.persist.rmi.RemoteDelegate;
import org.tentackle.reflect.ReflectionHelper;
import org.tentackle.sql.Backend;
import org.tentackle.sql.BackendInfo;
/**
* A logical database connection.
*
* Db connections are an abstraction for a connection between a client
* and a server, whereas "server" is not necessarily a database server,
* because Db connections can be either local or remote.
* A local Db talks to a database backend via a {@link ConnectionManager}
* which is responsible for the physical JDBC-connection. However, there
* is no 1:1 relationship between a Db and a physical connection. This is up
* to the connection manager.
* Local Db connections are used in client applications running in 2-tier mode
* or in application servers.
* Remote Db connections are connected to a Tentackle application server
* via RMI. Client applications running in 3-tier mode (more precise n-tier, with n ≥ 3)
* use remote connections.
* With the abstraction of a logical Db connection, Tentackle applications
* are able to run both in 2- or n-tier mode without a single modification
* to the source code. It's just a small change in a config file.
*
* The configuration is achieved by a property file, which is located
* by the {@link SessionInfo} passed to the Db.
*
* Local connections can be either a DataSource bound via JNDI or a direct
* JDBC connection. JNDI is used in application servers, for example running
* a JRuby on Rails application from within Glassfish (see tentackle-web).
* JDBC connections are for standalone 2-tier applications or Tentackle application servers.
*
* For local connections via JDBC the file contains at least the following properties:
*
*
* driver=jdbc-driver
* url=jdbc-url
*
* Example:
* driver=org.postgresql.Driver
* url=jdbc:postgresql://gonzo.krake.local/erp
*
*
* Local connections via JNDI need only the url. The url starts with
* "jndi:" and is followed by the JNDI-name. The optional database backend type is
* only necessary if it cannot be determined from the connection's metadata.
*
*
* url=jndi:name[:database-type]
*
* Example:
* url=jndi:jdbc/erpPool
*
*
* For remote connections only one line is required at minimum:
*
*
* url=rmi://hostname[:port]/service
*
* Example:
* url=rmi://gonzo.krake.local:28004/ErpServer
*
*
* The port is optional and defaults to 1099 (the default RMI registry).
*
* Optionally, the following properties can be defined:
*
* For local connections via JDBC:
*
* -
* A predefined user and password
*
* user=fixed-user
* password=fixed-password
*
*
* -
* By default the credentials used to connect to the backend are the same as the ones in the session info.
* However, if a technical user must be used, the configuration for the backend may be defined in another
* property file.
*
* backendinfo=property-file
*
*
*
*
* For local connections via JDBC or JNDI:
*
* -
* Each object persistable to the database (database object for short)
* gets a unique object ID which is generated
* by an {@link IdSource}. The default source is {@link ObjectId}.
*
* idsource=id-source-descriptor
*
* See {@link IdSourceConfigurator} for details.
*
*
*
* For remote connections, the properties (session info) will be sent to the server.
* This can be used to set some application specific options or to tune the connection.
*
* @author harald
*/
public class Db implements Session, Cloneable {
/**
* property key for IdSource.
*/
public static final String PROPERTY_IDSOURCE = "idsource";
/**
* The property key for the socket factory.
*/
public static final String SOCKET_FACTORY = "socketfactory";
/**
* The property key for the SSL cipher suites.
*/
public static final String CIPHER_SUITES = "ciphersuites";
/**
* The property key for the SSL protocols.
*/
public static final String PROTOCOLS = "protocols";
/**
* logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(Db.class);
/**
* Global close handlers.
*/
private static final Set GLOBAL_CLOSE_HANDLERS =
Collections.newSetFromMap(new ConcurrentHashMap<>());
/**
* Registers a global close handler.
*
* @param closeHandler the handler
* @return true if added, false if already registered
*/
public static boolean registerGlobalCloseHandler(SessionCloseHandler closeHandler) {
return GLOBAL_CLOSE_HANDLERS.add(closeHandler);
}
/**
* Unregisters a global close handler.
*
* @param closeHandler the handler
* @return true if removed, false if not registered
*/
public static boolean unregisterGlobalCloseHandler(SessionCloseHandler closeHandler) {
return GLOBAL_CLOSE_HANDLERS.remove(closeHandler);
}
/**
* We keep an internal set of all open Db-sessions via WeakReferences, so the
* Db-session still will be finalized when the Db isn't used anymore.
* The set of Db-sessions is used to enhance the diagnostic utilities.
*/
private static final Set> DB_SET =
Collections.newSetFromMap(new ConcurrentHashMap<>());
/**
* Registers a Db.
*
* @param db the db to register
*/
private static void registerDb(Db db) {
DB_SET.add(new WeakReference<>(db));
}
/**
* Unregisters a Db.
*
* Also cleans up the set by removing unreferenced or closed db sessions.
*
* @param db the db to unregister
*/
private static void unregisterDb(Db db) {
for (Iterator> iter = DB_SET.iterator(); iter.hasNext(); ) {
WeakReference ref = iter.next();
Db refDb = ref.get();
// if closed or unreferenced or the db to remove
if (refDb == null || !refDb.isOpen() || refDb == db) { // == is okay here
iter.remove();
}
}
}
/**
* Gets a list of all currently open Db sessions.
*
* @return the list of open Db sessions
*/
public static Collection getAllOpenDb() {
List dbList = new ArrayList<>();
for (Iterator> iter = DB_SET.iterator(); iter.hasNext(); ) {
WeakReference ref = iter.next();
Db refDb = ref.get();
if (refDb != null && refDb.isOpen()) {
dbList.add(refDb);
}
else {
iter.remove();
}
}
return dbList;
}
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); // global instance counter
// for RMI remote connections (all Db)
private static Class>[] remoteClasses; // the classes Objects for the delegates provide service for
private static int nextDelegateId; // next handle per class
private int instanceNumber; // each Db gets a unique instance number
private BackendInfo backendInfo; // the backend information
private String idConfig; // configuration of IdSource
private final ConnectionManager conMgr; // connection manager for local connections
private int conId; // local connection ID
private int groupConId; // ID of the connection group, 0 = none
private ManagedConnection con; // connection if attached, null = detached
private SessionPool dbPool; // != null if this Db is managed by a SessionPool
private int poolId; // the poolid if dbPool != null
private SessionInfo sessionInfo; // user information
private Db clonedFromDb; // the original db if this is a cloned one
private boolean autoCommit; // true if autocommit on, else false
private boolean countModificationAllowed = true; // allow modification count
private boolean logModificationAllowed = true; // allow modification log
private final Set closeHandlers = new HashSet<>(); // close handlers
private boolean readOnly; // true if database is readonly
private IdSource defaultIdSource; // default ID-Source for db-connection (if not overridden in derivates)
private int fetchSize; // default fetchsize, 0 = drivers default
private int maxRows; // maximum rows per resultset, 0 = no limit
private boolean logModificationTxEnabled; // true if log transaction begin/commit in modification log
private long logModificationTxId; // transaction id. !=0 if 'commit' is pending in modification log
private boolean logModificationDeferred; // true if log to memory rather than dbms
private List modificationLogList; // in-memory modification-log for current transaction
private DbTransaction transaction; // the running transaction
private int immediatelyRolledBack; // txLevel > 0 if a transaction has been rolled back by rollbackImmediately (only for local Db)
private int immediatelyCommitted; // txLevel > 0 if a transaction has been committed by commitImmediately (only for local Db)
private volatile boolean alive; // true = connection is still in use, false = connection has timed out
private long keepAliveInterval; // the auto keep alive interval in ms, 0 = no keep alive
private boolean crashed; // true = db is closed because it is treated as crashed
private volatile Thread ownerThread; // the exclusive owner thread
private WeakReference dispatcherRef; // task dispatcher for asynchroneous tasks
// for RMI remote connections
private RMIClientSocketFactory csf; // client socket factory, null if system default
private RemoteDbConnection rcon; // != null if remote connection established to RMI-server
private RemoteDbSession rses; // remote database session if logged in to RMI-server
private DbRemoteDelegate rdel; // remote database delegate
private RemoteDelegate[] delegates; // the delegates per session
/**
* Creates an instance of a logical db connection.
*
* @param conMgr the connection manager to use for this db, unused for local connections
* @param sessionInfo user information
*/
public Db(ConnectionManager conMgr, SessionInfo sessionInfo) {
instanceNumber = INSTANCE_COUNTER.incrementAndGet();
setSessionInfo(sessionInfo);
// establish connection to the database server
try {
// load configuration settings
Properties sessionProperties = sessionInfo.getProperties();
Properties backendProperties = sessionProperties; // backend properties defaults to session properties
String technicalBackendInfoName = sessionInfo.getProperties().getProperty(Constants.BACKEND_TECHNICAL_INFO);
if (technicalBackendInfoName != null) {
// programmatically predefined (usually technical) backend info
try {
backendProperties = FileHelper.loadProperties(technicalBackendInfoName, false);
}
catch (FileNotFoundException e1) {
// try as resource
try {
backendProperties = FileHelper.loadProperties(technicalBackendInfoName, true);
}
catch (FileNotFoundException e2) {
throw new PersistenceException("technical backend properties '" + technicalBackendInfoName + "' not found");
}
}
}
try {
backendInfo = new BackendInfo(backendProperties);
}
catch (BackendException bex) {
throw new PersistenceException(this, "invalid configuration in " + sessionInfo.getPropertiesName(), bex);
}
if (backendInfo.isRemote()) {
this.conMgr = null; // no connection manager for remote connections
SocketFactoryType factoryType = SocketFactoryType.parse(backendProperties.getProperty(SOCKET_FACTORY));
csf = SocketFactoryFactory.getInstance().createClientSocketFactory(null, factoryType);
LOGGER.info("using client socket factory {0}", csf);
}
else {
if (conMgr == null) {
throw new NullPointerException("connection manager required for local connections");
}
this.conMgr = conMgr;
}
String val = sessionProperties.getProperty(PROPERTY_IDSOURCE);
if (val != null) {
idConfig = val;
}
}
catch (PersistenceException rex) {
throw rex;
}
catch (Exception ex) {
throw new PersistenceException(this, "database configuration failed", ex);
}
}
/**
* Gets the unique instance number of this Db.
*
* @return the instance number
*/
@Override
public final int getInstanceNumber() {
return instanceNumber;
}
/**
* Gets the db name.
* Consists of the instance number, the connection id and the connection group id.
*
* @return the db name
*/
@Override
public String getName() {
StringBuilder buf = new StringBuilder();
buf.append("Db");
buf.append(instanceNumber);
buf.append('c');
buf.append(conId);
if (groupConId > 0) {
buf.append('g');
buf.append(groupConId);
}
return buf.toString();
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(getName());
if (conMgr != null || dbPool != null) {
buf.append('{');
if (conMgr != null) {
buf.append(conMgr.getName());
}
if (conMgr != null && dbPool != null) {
buf.append('/');
}
if (dbPool != null) {
buf.append(dbPool.getName());
}
buf.append('}');
}
buf.append(':');
buf.append(backendInfo);
if (con != null) {
buf.append('[');
buf.append(con.getName());
buf.append(']');
}
return buf.toString();
}
/**
* Compares two db instances.
* Implemented for trees, caches, etc... because they need a Comparable.
* We simply use the unique instanceNumber.
*
* @param session the session to compare this db with
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
@Override
public int compareTo(Session session) {
return instanceNumber - ((Db) session).instanceNumber;
}
@Override
public int hashCode() {
int hash = 3;
hash = 43 * hash + this.instanceNumber;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Db other = (Db) obj;
return this.instanceNumber == other.instanceNumber;
}
/**
* Gets the connection manager for this Db.
*
* @return the connection manager
*/
@Override
public ConnectionManager getSessionManager() {
return conMgr;
}
/**
* Sets the pool manager.
* The method is invoked from a SessionPool when the Db is created.
*
* @param sessionPool the db pool, null = not pooled
*/
public void setPool(SessionPool sessionPool) {
this.dbPool = sessionPool;
}
/**
* Gets the pool manager.
*
* @return the db pool, null = not pooled
*/
@Override
public SessionPool getPool() {
return dbPool;
}
/**
* Checks whether this Db is pooled.
*
* @return true if pooled, false if not pooled
*/
@Override
public boolean isPooled() {
return dbPool != null;
}
/**
* Sets the pool id.
* The method is invoked from a SessionPool when the Db is used in a pool.
*
* @param poolId the ID given by the pool (> 0), 0 = not used (free Db), -1 if removed from pool
*/
public void setPoolId(int poolId) {
this.poolId = poolId;
}
/**
* Gets the poolid.
*
* @return the pool id
*/
public int getPoolId() {
return poolId;
}
/**
* Returns whether Db is readonly.
*
* @return true if readonly.
*/
public boolean isReadOnly() {
return readOnly;
}
/**
* Sets the db to readonly.
*
* If a database is readonly, no updates, deletes or inserts
* are possible. Any attempt to do so results in a persistence exception.
*
* @param readOnly true if readonly
*/
public void setReadOnly(boolean readOnly) {
assertOpen();
if (isRemote()) {
try {
rdel.setReadOnly(readOnly);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
this.readOnly = readOnly;
}
/**
* Gets the remote connection object.
*
* @return the connection object, null if local
*/
public RemoteDbConnection getRemoteConnection() {
return rcon;
}
/**
* Gets the remote session object.
* @return the session, null if local
*/
public RemoteDbSession getRemoteDbSession() {
return rses;
}
/**
* Creates a physical low level connection to the local db server via JDBC
* using this Db (authentication by userinfo or fixed user/password).
*
* @return the low level JDBC connection
* @throws SQLException if connection failed
*/
public Connection connect() throws SQLException {
Connection connection = null;
try {
connection = backendInfo.connect();
if (!connection.getAutoCommit()) {
connection.setAutoCommit(true); // set to autocommit mode for sure
}
return connection;
}
catch (SQLException sqx) {
LOGGER.severe("connection to " + backendInfo + " failed");
if (connection != null) {
connection.close();
}
throw sqx;
}
}
/**
* Asserts db is not used by another thread than the ownerthread.
*/
public void assertOwnerThread() {
Thread ownrThread = getOwnerThread();
if (ownrThread != null) {
Thread currentThread = Thread.currentThread();
if (ownrThread != currentThread && !Scavenger.isScavenger(currentThread)) {
throw new PersistenceException(this,
"illegal attempt to use Db owned by " + ownrThread +
" by " + currentThread.getName());
}
}
}
/**
* Asserts that the database is open.
*/
public void assertOpen() {
try {
if (!isOpen()) {
if (getPool() != null) {
throw new SessionClosedException(this, "database session already closed by pool " + getPool() +
"! application still holding a reference to this Db after returning it to the pool!");
}
throw new SessionClosedException(this, "database session is closed");
}
}
catch (RuntimeException rex) {
handleExceptionForScavenger(rex);
}
}
/**
* Asserts that a transaction with the given voucher is running.
*/
public void assertTxRunning() {
try {
if (transaction == null) {
throw new PersistenceException(this, "no transaction running");
}
}
catch (RuntimeException rex) {
handleExceptionForScavenger(rex);
}
}
/**
* Asserts that no transaction is running.
*/
public void assertNoTxRunning() {
try {
if (transaction != null) {
throw new PersistenceException(this, transaction + " still pending");
}
}
catch (RuntimeException rex) {
handleExceptionForScavenger(rex);
}
}
/**
* asserts that this is not a remote connection
*/
public void assertNotRemote() {
if (isRemote()) {
throw new PersistenceException(this, "operation not allowed for remote db connections");
}
}
/**
* asserts that this is a remote connection
*/
public void assertRemote() {
if (!isRemote()) {
throw new PersistenceException(this, "operation not allowed for local db connections");
}
}
/**
* Logs exception if scavenger thread, else throws it.
*
* @param rex the runtime exception
*/
private void handleExceptionForScavenger(RuntimeException rex) {
Thread currentThread = Thread.currentThread();
if (Scavenger.isScavenger(currentThread)) {
LOGGER.warning("exception ignored by scavenger " + currentThread, rex);
}
else {
throw rex;
}
}
/**
* Checks whether the db connection is still in use.
* Whenever a {@link StatementWrapper} or {@link PreparedStatementWrapper} is used
* (i.e executeQuery or executeUpdate), the db connection is set to be alive.
* Some other thread may clear this flag regulary and check whether it has
* been set in the meantime.
*
* @return true if connection still in use, false if not used since last setAlive(false).
*/
@Override
public boolean isAlive() {
assertOpen();
if (isRemote()) {
try {
return rdel.isAlive();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
return alive;
}
/**
* Sets the db connection's alive state.
*
* @param alive is true to signal it's alive, false to clear
*/
@Override
public void setAlive(boolean alive) {
assertOpen();
if (isRemote()) {
try {
rdel.setAlive(alive);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
this.alive = alive;
}
}
@Override
public long getKeepAliveInterval() {
return keepAliveInterval;
}
@Override
public void setKeepAliveInterval(long keepAliveInterval) {
if (this.keepAliveInterval != keepAliveInterval) {
this.keepAliveInterval = keepAliveInterval;
Pdo.keepAliveIntervalChanged(this);
}
}
/**
* Sets the ID-Source configuration.
*
* @param idConfig the configuration
*/
public void setIdConfiguration(String idConfig) {
this.idConfig = idConfig;
}
/**
* Gets the ID-Source configuration.
*
* @return the configuration
*/
public String getIdSourceConfiguration() {
return idConfig;
}
/**
* Gets the dispatcher for this session.
* The dispatcher can be used to submit asynchroneous tasks in a serial manner.
* The returned dispatcher is configured to shutdown if idle for a given timeout.
*
* @return the dispatcher
*/
@Override
public synchronized SessionTaskDispatcher getDispatcher() {
SessionTaskDispatcher dispatcher = null;
if (dispatcherRef != null) {
dispatcher = dispatcherRef.get();
if (dispatcher == null) {
dispatcherRef = null; // GC'd
}
}
if (dispatcher == null || !dispatcher.isAlive()) {
dispatcher = new DefaultSessionTaskDispatcher("Task Dispatcher for " + getUrl());
dispatcher.setShutdownIdleTimeout(300000); // shutdown if idle for more than 5 minutes
dispatcherRef = new WeakReference<>(dispatcher);
dispatcher.start();
}
return dispatcher;
}
/**
* Logs an exception.
*
* @param e the exception head, null if none
* @param msg the optional message, null if none
* @param logger the logger
* @param level the logging level
*/
public void logException(Logger logger, Level level, String msg, Throwable e) {
if (e == null) {
// avoid "Nullpointer Exception" cause it isn't one!
e = new Exception("unknown (exception was null)");
}
try (PrintStream ps = new PrintStream(new LoggerOutputStream(logger, level))) {
ps.println();
if (msg != null) {
ps.println(msg);
}
SQLException sqlEx = ExceptionHelper.extractException(SQLException.class, true, e);
msg = ">>>DB>>>> " + this;
if (sqlEx != null) {
msg += "\n>>>SQL>>> " + sqlEx.getMessage() +
"\n>>>Code>> " + sqlEx.getErrorCode() +
"\n>>>State> " + sqlEx.getSQLState();
}
ps.println(msg);
e.printStackTrace(ps);
}
}
/**
* Checks for a dead communications link.
* If the link is down, for example the database server closed it,
* the connection is marked dead.
*
* @param ex the sql exception
* @return true if connection is dead
* @see ManagedConnection#setDead(boolean)
*/
public boolean checkForDeadLink(SQLException ex) {
if (getBackend().isCommunicationLinkException(ex)) {
// some severe comlink error, probably closed by server
ManagedConnection mc = getConnection();
if (mc != null) {
// if connection still attached: mark it dead!
mc.setDead(true);
LOGGER.severe("managed connection " + mc + " marked dead", ex);
return true;
}
}
return false;
}
/**
* Configures the ID source if not remote.
*/
private void setupIdSource() {
if (!isRemote()) {
if (idConfig != null) {
setDefaultIdSource(IdSourceConfigurator.getInstance().configure(this, idConfig));
}
else {
IdSource idSource = IdSourceConfigurator.getInstance().configure(this, null);
setDefaultIdSource(idSource);
}
}
}
/**
* Handles a connect exception (in open or clone).
* The method returns a PersistenceException.
*/
private PersistenceException handleConnectException(Exception e) {
if (e instanceof RemoteException) {
// translate RemoteException to real exception
e = PersistenceException.createFromRemoteException(this, (RemoteException) e);
}
if (e instanceof LoginFailedException) {
LOGGER.warning("remote login failed", e);
return (LoginFailedException) e;
}
// severe error: no new login attempt anymore
logException(LOGGER, Level.WARNING, "login failed", e);
if (e instanceof PersistenceException) {
return (PersistenceException) e;
}
return new PersistenceException(this, "connection error", e);
}
/**
* Open a database connection.
* If the login failes due to wrong passwords
* or denied access by the application server,
* the method {@link #handleConnectException} is invoked.
*/
@Override
public void open () {
if (isOpen()) {
throw new PersistenceException(this, "session is already open");
}
if (getPoolId() == -1) {
throw new PersistenceException(this, "db was closed and removed from pool " + getPool() +", cannot be re-opened");
}
try {
if (isRemote()) {
// get connection to RMI-server
if (csf != null) {
String rmiUrl = backendInfo.getUrl();
URI uri = new URI(rmiUrl);
String host = uri.getHost();
int port = uri.getPort();
String path = uri.getPath();
if (path.startsWith("/")) {
path = path.substring(1);
}
LOGGER.info("connecting to {0}:{1}/{2} using {3}", host, port, path, csf);
Registry registry = LocateRegistry.getRegistry(host, port, csf);
try {
rcon = (RemoteDbConnection) registry.lookup(path);
}
catch (NotBoundException nx) {
StringBuilder buf = new StringBuilder();
buf.append("bound services =");
String[] services = registry.list();
if (services != null) {
for (String service: services) {
buf.append(" '").append(service).append("'");
}
}
LOGGER.warning(buf.toString());
throw nx;
}
}
else {
rcon = (RemoteDbConnection) Naming.lookup(backendInfo.getUrl());
}
/**
* Check that server matches expected version.
* The server checks the client's version in rcon.login() below.
* So, we have a double check.
*/
sessionInfo.checkServerVersionInfo(rcon.getServerVersionInfo());
// get session
rses = rcon.login(sessionInfo); // throws exception if login denied
// get delegate
rdel = rses.getDbRemoteDelegate();
// get the remote connection id
conId = rdel.getSessionId();
}
else {
// direct connection to database
conId = conMgr.login(this);
}
// fresh connections are always in autoCommit mode
autoCommit = true;
transaction = null;
setupIdSource();
sessionInfo.setSince(System.currentTimeMillis());
alive = true; // all db start alive!
if (!isCloned()) {
LOGGER.info("connection {0} established", this);
}
registerDb(this);
}
// warning causes
catch (Exception e) {
throw handleConnectException(e);
}
}
/**
* Clears all passwords (stored in char[]-arrays) so
* that they are no more visible in memory.
*/
public void clearPassword() {
sessionInfo.clearPassword();
backendInfo.clearPassword();
}
/**
* Registers a close handler.
*
* @param closeHandler the handler
* @return true if added, false if already registered
*/
public boolean registerCloseHandler(SessionCloseHandler closeHandler) {
return closeHandlers.add(closeHandler);
}
/**
* Unregisters a close handler.
*
* @param closeHandler the handler
* @return true if removed, false if not registered
*/
public boolean unregisterCloseHandler(SessionCloseHandler closeHandler) {
return closeHandlers.remove(closeHandler);
}
@Override
public synchronized void close () {
if (isOpen()) {
try {
for (SessionCloseHandler closeHandler : closeHandlers) {
try {
closeHandler.beforeClose(this);
}
catch (Exception ex) {
LOGGER.warning("closehandler.beforeClose failed for " + this, ex);
}
}
for (SessionCloseHandler closeHandler : GLOBAL_CLOSE_HANDLERS) {
try {
closeHandler.beforeClose(this);
}
catch (Exception ex) {
LOGGER.warning("global closehandler.beforeClose failed for " + this, ex);
}
}
if (isRemote()) {
if (rcon != null && rses != null) {
rcon.logout(rses); // close the session
LOGGER.info("remote db {0} closed", this);
}
}
else {
if (conId > 0) {
if (con != null) {
try {
con.closePreparedStatements(true); // cleanup all pending statements
}
catch (Exception ex) {
LOGGER.warning("closing prepared statements failed for " + this, ex);
}
}
conMgr.logout(this);
LOGGER.info("db {0} closed", this);
}
}
for (SessionCloseHandler closeHandler : closeHandlers) {
try {
closeHandler.afterClose(this);
}
catch (Exception ex) {
LOGGER.warning("closehandler.afterClose failed for " + this, ex);
}
}
for (SessionCloseHandler closeHandler : GLOBAL_CLOSE_HANDLERS) {
try {
closeHandler.afterClose(this);
}
catch (Exception ex) {
LOGGER.warning("global closehandler.afterClose failed for " + this, ex);
}
}
}
catch (Exception e) {
throw new PersistenceException(this, "closing db failed", e);
}
finally {
// whatever happened: make it unusable
clearMembers(this); // clear members for re-open
unregisterDb(this);
}
}
}
/**
* finalizer if connection is broken
*/
@Override
protected void finalize() throws Throwable {
try {
if (isOpen()) {
LOGGER.warning("closing unreferenced open db: " + this);
}
close(); // cleanup for sure
}
catch (Exception ex) {
try {
LOGGER.severe("closing unreferenced db '" + this + "' failed in finalizer");
}
catch (Exception ex2) {
// don't stop finalization if just the logging failed
}
}
finally {
super.finalize();
}
}
/**
* Sets the crash flag.
* The db may be marked as crashed to reduce logging of succeeding errors.
* May be invoked from any thread.
*
* @param crashed the crash flag
*/
public synchronized void setCrashed(boolean crashed) {
this.crashed = crashed;
}
/**
* Gets the crash flag.
* May be invoked from any thread.
*
* @return true if Db is marked as crashed
*/
public synchronized boolean isCrashed() {
return crashed;
}
/**
* Gets the connection state.
*
* @return true if db is open, else false
*/
@Override
public boolean isOpen() {
if (isRemote()) {
return rdel != null;
}
else {
return conId > 0;
}
}
/**
* Transactions get a unique transaction number by
* counting the transactions per Db instance.
*
* @return the current transaction counter, 0 if no transaction in progress
*/
public long getTxNumber() {
return transaction == null ? 0 : transaction.getTxNumber();
}
/**
* Gets the optional transaction name.
* Useful to distinguish transactions in logModification or alike.
* The tx-name is cleared after commit or rollback.
*
* @return the transaction name, null if no transaction in progress
*/
@Override
public String getTxName() {
return transaction == null ? null : transaction.getTxName();
}
/**
* Gets the transaction nesting level.
*
* @return the nesting level, 0 if no transaction in progress
*/
public int getTxLevel() {
return transaction == null ? 0 : transaction.getTxLevel();
}
/**
* Marks the txLevel invalid.
*
* Will suppress any checks and warnings.
*/
public void invalidateTxLevel() {
if (transaction != null) {
transaction.invalidateTxLevel();
}
}
/**
* Returns whether the txLevel is valid.
*
* @return true if valid
*/
public boolean isTxLevelValid() {
return transaction != null && transaction.isTxLevelValid();
}
/**
* Gets the pending txLevel after an immediate rollback.
*
* @return > 0 if there was an immediate rollback
*/
public int getImmediateRollbackTxLevel() {
return immediatelyRolledBack;
}
/**
* Gets the pending txLevel after an immediate commit.
*
* @return > 0 if there was an immediate commit
*/
public int getImmediateCommitTxLevel() {
return immediatelyCommitted;
}
/**
* Sets the optional transaction object.
* By default, whenever a transaction is initiated by a persistence operation of
* a {@link AbstractDbObject}, that object becomes the "parent" of the
* transaction.
* The {@code txObject} is mainly used for logging and enhanced auditing (partial history) during transactions.
* The {@code txObject} is cleared at the end of the transaction.
*
* @param txObject the transaction object, null to clear
*/
public void setTxObject(AbstractDbObject> txObject) {
assertTxRunning();
transaction.setTxObject(txObject);
}
/**
* Gets the optional transaction object.
*
* @return the transaction object, null if none
*/
public AbstractDbObject> getTxObject() {
assertTxRunning();
return transaction.getTxObject();
}
@Override
public T transaction(TransactionEnvelope txe, String txName) {
if (txName == null) {
Method method = txe.getClass().getEnclosingMethod();
if (method != null) {
// inner class
txName = ReflectionHelper.getClassBaseName(method.getDeclaringClass()) + "#" + method.getName();
}
else {
// lambdas don't provide an enclosing method, because they are not inner classes.
// can't figure out method name -> just use the classname (better than nothing...)
txName = ReflectionHelper.getClassBaseName(txe.getClass());
int ndx = txName.indexOf('$'); // cut useless info $$Lambda$0/123456789
if (ndx > 0) {
txName = txName.substring(0, ndx);
}
}
}
long txVoucher = begin(txName);
try {
T rv = txe.run();
commit(txVoucher);
return rv;
}
catch (RuntimeException ex) {
try {
if (txVoucher != 0 && ExceptionHelper.extractException(PersistenceException.class, true, ex) != null) {
// log only if cause is a PersistenceException
rollback(txVoucher);
}
else {
rollbackSilently(txVoucher);
}
}
catch (RuntimeException rex) {
LOGGER.severe("rollback failed", rex);
}
throw ex;
}
}
@Override
public T transaction(TransactionEnvelope txe) {
return transaction(txe, null);
}
/**
* Starts a transaction.
* Does nothing if a transaction is already running!
*
* @param txName is the optional transaction name, null if none
*
* @return the transaction voucher (!= 0) if a new transaction was begun, else 0
*/
@Override
public long begin(String txName) {
DbTransaction tx = begin(txName, false);
return tx != null ? tx.getTxVoucher() : 0;
}
@Override
public long begin() {
return begin(null);
}
/**
* Starts a transaction.
* Just increments the transaction level if a transaction is already running.
*
* @param txName is the optional transaction name, null if none
* @param fromRemote true if invocation from remote client via {@link DbRemoteDelegate#begin}
*
* @return the DbTransaction if a new transaction was begun, else null
*/
private DbTransaction begin(String txName, boolean fromRemote) {
LOGGER.finer("begin transaction requested on {0} from {1}, txName={2}",
this, (fromRemote ? "remote client" : "local"), txName);
assertOpen();
assertOwnerThread();
DbTransaction tx = null;
if (isRemote()) {
try {
tx = rdel.begin(txName);
if (tx != null) {
// new transaction begun
tx.setSession(this);
autoCommit = false; // we are now within a tx
}
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
alive = true;
immediatelyRolledBack = 0; // the first begin clears all pending immediate rollbacks() ...
immediatelyCommitted = 0; // ... and commits()
if (setAutoCommit(false)) {
// new transaction
assertNoTxRunning();
modificationLogList = null;
tx = transaction = new DbTransaction(this, txName, fromRemote);
}
else {
// already running transaction
assertTxRunning();
transaction.incrementTxLevel(); // just increment the nesting level
}
}
LOGGER.fine("{0} {1}", transaction, tx != null ? " begun" : " already running");
return tx;
}
/**
* Checks whether current transaction was initiated by a remote client.
*
* @return true if tx is a remote client transaction
*/
public boolean isClientTx() {
assertTxRunning();
return transaction.getTxVoucher() < 0;
}
/**
* Creates a begin modification log if necessary.
*/
public void logBeginTx() {
if (logModificationTxEnabled && logModificationTxId == 0 && isTxRunning()) {
ModificationLog log = ModificationLogFactory.getInstance().createModificationLog(this, ModificationLog.BEGIN);
log.reserveId();
log.setTxId(log.getId());
log.saveObject();
logModificationTxId = log.getTxId();
}
}
/**
* Creates a commit modification log if necessary.
*/
public void logCommitTx() {
if (logModificationTxId != 0) {
// only if BEGIN already created
if (logModificationTxEnabled) {
ModificationLogFactory.getInstance().createModificationLog(this, ModificationLog.COMMIT).saveObject();
}
logModificationTxId = 0;
}
}
/**
* Gets the number of objects modified since the last begin().
* The method is provided to check whether objects have been
* modified at all.
*
* @return the number of modified objects, 0 if none modified or no transaction running
*/
public int getUpdateCount() {
return transaction == null ? 0 : transaction.getUpdateCount();
}
/**
* Add to updateCount.
*
* @param count the number of updates to add
*/
void addToUpdateCount(int count) {
if (transaction != null) {
transaction.addToUpdateCount(count);
}
}
/**
* Registers a {@link PersistenceVisitor} to be invoked just before
* performing a persistence operation.
* Notice that the visitor must be registered within the transaction, i.e. after {@link #begin()}.
* The visitors are automatically unregistered at the end of a transaction
* (commit or rollback).
*
* @param visitor the visitor to register
* @return a handle uniquely referring to the visitor
* @see #isPersistenceOperationAllowed(org.tentackle.persist.AbstractDbObject, char)
*/
public DbTransactionHandle registerPersistenceVisitor(PersistenceVisitor visitor) {
if (isRemote()) {
try {
return rdel.registerPersistenceVisitor(visitor);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
assertTxRunning();
return transaction.registerPersistenceVisitor(visitor);
}
}
/**
* Unegisters a {@link PersistenceVisitor}.
*
* @param handle the visitor's handle to unregister
* @return the visitor if removed, null if not registered
*/
public PersistenceVisitor unregisterPersistenceVisitor(DbTransactionHandle handle) {
if (isRemote()) {
try {
return rdel.unregisterPersistenceVisitor(handle);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.unregisterPersistenceVisitor(handle) : null;
}
}
/**
* Gets the currently registered persistence visitors.
*
* @return the visitors, null or empty if none
*/
public Collection getPersistenceVisitors() {
if (isRemote()) {
try {
return rdel.getPersistenceVisitors();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.getPersistenceVisitors() : null;
}
}
/**
* Checks whether a persistence operation is allowed.
* This is determined by consulting the {@link PersistenceVisitor}s.
*
* @param object the persistence object
* @param modType the modification type
* @return true if allowed
* @see #registerPersistenceVisitor(org.tentackle.persist.PersistenceVisitor)
*/
public boolean isPersistenceOperationAllowed(AbstractDbObject> object, char modType) {
return transaction == null || transaction.isPersistenceOperationAllowed(object, modType);
}
/**
* Registers a {@link CommitTxRunnable} to be invoked just before
* committing a transaction.
*
* @param commitRunnable the runnable to register
* @return the handle
*/
public DbTransactionHandle registerCommitTxRunnable(CommitTxRunnable commitRunnable) {
if (isRemote()) {
try {
return rdel.registerCommitTxRunnable(commitRunnable);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
assertTxRunning();
return transaction.registerCommitTxRunnable(commitRunnable);
}
}
/**
* Unregisters a {@link CommitTxRunnable}.
*
* @param handle the runnable's handle to unregister
* @return the runnable, null if not registered
*/
public CommitTxRunnable unregisterCommitTxRunnable(DbTransactionHandle handle) {
if (isRemote()) {
try {
return rdel.unregisterCommitTxRunnable(handle);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.unregisterCommitTxRunnable(handle) : null;
}
}
/**
* Gets the currently registered commit runnables.
*
* @return the runnables, null or empty if none
*/
public Collection getCommitTxRunnables() {
if (isRemote()) {
try {
return rdel.getCommitTxRunnables();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.getCommitTxRunnables() : null;
}
}
/**
* Registers a {@link RollbackTxRunnable} to be invoked just before
* rolling back a transaction.
*
* @param rollbackRunnable the runnable to register
* @return the handle
*/
public DbTransactionHandle registerRollbackTxRunnable(RollbackTxRunnable rollbackRunnable) {
if (isRemote()) {
try {
return rdel.registerRollbackTxRunnable(rollbackRunnable);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
assertTxRunning();
return transaction.registerRollbackTxRunnable(rollbackRunnable);
}
}
/**
* Unregisters a {@link RollbackTxRunnable}.
*
* @param handle the runnable's handle to unregister
* @return the runnable, null if not registered
*/
public RollbackTxRunnable unregisterRollbackTxRunnable(DbTransactionHandle handle) {
if (isRemote()) {
try {
return rdel.unregisterRollbackTxRunnable(handle);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.unregisterRollbackTxRunnable(handle) : null;
}
}
/**
* Gets the currently registered rollback runnables.
*
* @return the runnables, null or empty if none
*/
public Collection getRollbackTxRunnables() {
if (isRemote()) {
try {
return rdel.getRollbackTxRunnables();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return transaction != null ? transaction.getRollbackTxRunnables() : null;
}
}
/**
* Commits a transaction if the corresponding begin() has started it.
* The corresponding begin() is determined by the txVoucher parameter.
* If it fits to the issued voucher the tx will be committed.
* If it is 0, nothing will happen.
* In all other cases an exception will be thrown.
*
* @param txVoucher the transaction voucher, 0 if do nothing (nested tx)
*
* @return true if committed, false if nested tx
*/
@Override
public boolean commit(long txVoucher) {
LOGGER.finer("committing {0}", transaction);
assertOpen();
assertOwnerThread();
boolean committed = false;
if (isRemote()) {
try {
committed = rdel.commit(txVoucher);
if (committed) {
transaction = null;
autoCommit = true; // now outside tx again
}
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
alive = true;
// allow pending rollbacks after commitImmediately
if (immediatelyCommitted > 0) {
assertNoTxRunning();
immediatelyCommitted--;
if (immediatelyCommitted < 0) {
LOGGER.warning(this + ": too many commits after commitImmediately, ignored");
}
return true;
}
assertTxRunning();
if (txVoucher != 0) {
if (!autoCommit) {
// within a transaction: commit!
logCommitTx();
if (transaction.isTxLevelValid() && transaction.getTxLevel() != 1) {
LOGGER.warning(this + ": txLevel=" + transaction.getTxLevel() + ", should be 1");
}
transaction.invokeCommitTxRunnables();
setAutoCommit(true); // according to the specs: this will commit!
logModificationDeferred = false;
transaction = null;
modificationLogList = null;
committed = true;
}
else {
throw new PersistenceException(this, "transaction ended unexpectedly before commit (valid voucher)");
}
}
else {
// no voucher, no change (this is ok)
transaction.decrementTxLevel(); // just decrement the tx level
}
}
LOGGER.fine("{0} {1}", transaction, committed ? " committed" : " nesting level decremented");
return committed;
}
/**
* Commits the current transaction, if pending.
*
* Applications should use {@link #commit(long)}.
* Invocations of this method will be logged as a warning.
*
* @return true if committed
*/
public boolean commitImmediately() {
if (transaction != null) {
int currentTxLevel = transaction.getTxLevel();
if(!isRemote()) {
immediatelyCommitted = 0;
immediatelyRolledBack = 0;
}
transaction.invalidateTxLevel(); // avoid misleading warnings
if (!commit(transaction.getTxVoucher())) {
throw new PersistenceException(this, transaction + " not committed despite valid voucher");
}
transaction = null;
LOGGER.warning("*** immediate commit for " + this + " ***");
if (!isRemote()) {
// keep the txLevel for pending commit()s, if any will arrive from the application
immediatelyCommitted = currentTxLevel;
}
return true;
}
else {
return false;
}
}
@Override
public boolean rollback(long txVoucher) {
return rollback(txVoucher, true);
}
@Override
public boolean rollbackSilently(long txVoucher) {
return rollback(txVoucher, false);
}
/**
* Rolls back a transaction if the corresponding begin() has started it.
*
* @param txVoucher the transaction voucher, 0 if ignore (nested tx)
* @param withLog true if log via INFO
* @return true if tx was really rolled back, false if not.
*/
public boolean rollback(long txVoucher, boolean withLog) {
LOGGER.finer("rolling back {0}", transaction);
assertOpen();
assertOwnerThread();
boolean rolledBack = false;
if (isRemote()) {
try {
rolledBack = rdel.rollback(txVoucher, withLog);
if (rolledBack) {
transaction = null;
autoCommit = true; // now outside tx again
}
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
alive = true;
// allow pending rollbacks after rollbackImmediately
if (immediatelyRolledBack > 0) {
assertNoTxRunning();
immediatelyRolledBack--;
if (immediatelyRolledBack < 0) {
LOGGER.warning(this + ": too many rollbacks after rollbackImmediately, ignored");
}
LOGGER.fine("pending immediate rollback counter decremented -> no physical rollback");
return true;
}
if (transaction != null) {
if (txVoucher != 0) {
// begin() started a new tx
if (!autoCommit) {
// within a transaction
if (transaction.isTxLevelValid() && transaction.getTxLevel() != 1) {
LOGGER.warning(this + ": txLevel=" + transaction.getTxLevel() + ", should be 1");
}
transaction.invokeRollbackTxRunnables();
if (con != null) {
con.rollback(withLog); // avoid a commit ...
setAutoCommit(true); // ... in setAutoCommit
}
else {
LOGGER.warning("rollback too late: connection already detached");
autoCommit = true;
}
LOGGER.fine("{0} rolled back", transaction);
logModificationDeferred = false;
modificationLogList = null;
logModificationTxId = 0;
transaction = null;
rolledBack = true;
}
else {
throw new PersistenceException(this, "transaction ended unexpectedly before rollback (valid voucher)");
}
}
else {
// no voucher, no change (this is ok)
transaction.decrementTxLevel();
LOGGER.fine("{0} nesting level decremented -> no physical rollback", transaction);
}
}
else {
LOGGER.fine("no transaction running -> no physical rollback");
}
}
return rolledBack;
}
/**
* Rolls back the current transaction, if pending.
*
* Applications should use {@link #rollback(long)}.
* Invocations of this method will be logged as a warning.
*
* @return true if rolled back
*/
@Override
public boolean rollbackImmediately() {
try {
// cancel all pending statements (usually max. 1)
if (con != null) {
con.cancelRunningStatements();
}
if (transaction != null) {
int currentTxLevel = transaction.getTxLevel();
if(!isRemote()) {
immediatelyCommitted = 0;
immediatelyRolledBack = 0;
}
transaction.invalidateTxLevel(); // avoid misleading warnings
if (!rollback(transaction.getTxVoucher())) {
throw new PersistenceException(this, transaction + " not rolled back despite valid voucher");
}
transaction = null;
LOGGER.warning("*** immediate rollback for " + this + " ***");
if (!isRemote()) {
// remember the txLevel for pending rollback()s, if any will arive from the application
immediatelyRolledBack = currentTxLevel;
}
return true;
}
else {
// not within a transaction: cleanup forced
forceDetached();
return false;
}
}
catch(RuntimeException rex) {
if (con != null && !con.isClosed()) {
// check low level transaction state
boolean connectionInTransaction;
try {
connectionInTransaction = con.getConnection().getAutoCommit();
}
catch (SQLException ex) {
LOGGER.warning("cannot determine transaction state of " + con + ": assume transaction still running", ex);
connectionInTransaction = true;
}
if (connectionInTransaction) {
/**
* If the physical connection is still running a transaction,
* we need a low-level rollback and mark the connection dead
* to avoid further use by the application.
*/
LOGGER.warning(con + " still in transaction: performing low-level rollback");
try {
con.getConnection().rollback();
con.getConnection().setAutoCommit(true);
}
catch (SQLException ex) {
LOGGER.warning("low-level connection rollback failed for " + con, ex);
}
try {
// this may yield some additional information
con.logAndClearWarnings();
}
catch (PersistenceException ex) {
LOGGER.warning("clear warnings failed for " + con, ex);
}
}
con.setDead(true); // don't use this connection anymore!
LOGGER.warning("connection " + con + " marked dead");
}
handleExceptionForScavenger(rex);
return transaction != null;
}
}
/**
* Sets the current connection.
* This method is package scope and invoked whenever a connection
* is attached or detached to/from a Db by the ConnectionManager.
*/
synchronized void setConnection(ManagedConnection con) {
this.con = con;
}
/**
* Gets the current connection.
*
* @return the connection, null = not attached
*/
synchronized ManagedConnection getConnection() {
return con;
}
/**
* Detach the db.
* Used in execption handling to cleanup all pending statements and result sets.
*/
public synchronized void forceDetached() {
if (con != null) {
conMgr.forceDetach(this);
}
}
/**
* Gets the connection id.
* This is a unique number assigned to this Db by the ConnectionManager.
*
* @return the connection id, 0 = Db is new and not connected so far
*/
@Override
public int getSessionId() {
return conId;
}
/**
* Sets the group number for this db.
*
* This is an optional number describing groups of db-connections,
* which is particulary useful in rmi-servers: if one connection
* fails, all others should be closed as well.
* Groups are only meaningful for local db-connections, i.e.
* for remote dbs the group instance refers to that of the rmi-server.
*
* @param number is the group number, 0 = no group
*/
public void setSessionGroupId(int number) {
assertOpen();
if (isRemote()) {
try {
rdel.setSessionGroupId(number);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
if (number != groupConId) {
if (number != 0) {
LOGGER.info("{0} joined group {1}", this, number);
}
else {
LOGGER.info("{0} removed from group {1}", this, groupConId);
}
}
groupConId = number;
}
@Override
public int getSessionGroupId() {
assertOpen();
if (isRemote()) {
try {
return rdel.getSessionGroupId();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
return groupConId;
}
@Override
public int groupWith(int sessionId) {
if (sessionId <= 0) {
throw new PersistenceException(this, "invalid session id " + sessionId);
}
assertOpen();
if (isRemote()) {
try {
groupConId = rdel.groupWith(sessionId);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
if (groupConId > 0 && sessionId != groupConId) {
throw new PersistenceException(this, "session to join group " + sessionId + " already belongs to group " + groupConId);
}
setSessionGroupId(sessionId);
for (WeakReference dbRef: DB_SET) {
Db db = dbRef.get();
if (db != null && db.conId == sessionId) {
if (db.groupConId > 0 && sessionId != db.groupConId) {
throw new PersistenceException(db, "session already belongs to group " + db.groupConId);
}
db.setSessionGroupId(sessionId);
break;
}
}
}
return groupConId;
}
/**
* Attaches the connection.
*
* Notice: package scope!
*/
synchronized void attach() {
assertOpen();
assertOwnerThread();
if (conMgr == null) {
throw new PersistenceException(this, "no connection manager");
}
if (isPooled() && getPoolId() == 0) {
// no SessionPool.getDb() on this db: possible closed RemoteSession?
throw new PersistenceException(this, "illegal attempt to attach a pooled Db which is not in use");
}
conMgr.attach(this);
}
/**
* Detaches the connection.
*
* Notice: package scope!
*/
synchronized void detach() {
if (conId > 0) {
assertOwnerThread();
conMgr.detach(this);
}
// else: Db is already closed (may happen during cleanup)
}
/**
* Sets the exclusive owner thread.
*
* Allows to detect other threads accidently using this db.
* Caution: don't forget to clear!
*
* @param ownerThread the owner thread, null to clear
*/
@Override
public void setOwnerThread(Thread ownerThread) {
this.ownerThread = ownerThread;
}
/**
* Gets the owner thread.
*
* @return the exclusive thread, null if none
*/
@Override
public Thread getOwnerThread() {
return ownerThread;
}
/**
* Creates a non-prepared statement.
*
* Non-prepared statements attach the db as soon as they
* are instatiated! The db is detached after executeUpdate or after
* executeQuery when its result set is closed.
*
* @param resultSetType a result set type; one of
* ResultSet.TYPE_FORWARD_ONLY
,
* ResultSet.TYPE_SCROLL_INSENSITIVE
, or
* ResultSet.TYPE_SCROLL_SENSITIVE
* @param resultSetConcurrency a concurrency type; one of
* ResultSet.CONCUR_READ_ONLY
or
* ResultSet.CONCUR_UPDATABLE
* @return the statement wrapper
*/
public synchronized StatementWrapper createStatement (int resultSetType, int resultSetConcurrency) {
assertNotRemote();
attach();
StatementWrapper stmt = con.createStatement(resultSetType, resultSetConcurrency);
stmt.markReady();
return stmt;
}
/**
* Creates a non-prepared statement.
*
* @param resultSetType a result set type; one of
* ResultSet.TYPE_FORWARD_ONLY
,
* ResultSet.TYPE_SCROLL_INSENSITIVE
, or
* ResultSet.TYPE_SCROLL_SENSITIVE
* @return a new Statement
object that will generate
* ResultSet
objects with the given type and
* concurrency CONCUR_READ_ONLY
*/
public StatementWrapper createStatement (int resultSetType) {
return createStatement(resultSetType, ResultSet.CONCUR_READ_ONLY);
}
/**
* Creates a non-prepared statement.
*
* @return a new Statement
object that will generate
* ResultSet
objects with type TYPE_FORWARD_ONLY and
* concurrency CONCUR_READ_ONLY
*/
public StatementWrapper createStatement () {
return createStatement(ResultSet.TYPE_FORWARD_ONLY);
}
/**
* Gets the prepared statement.
*
* Getting the prepared statement will attach the db to a connection.
* The db will be detached after executeUpdate() or executeQuery when its
* result set is closed.
*
* @param stmtKey the statement key
* @param alwaysPrepare true if always do a physical prepare
* @param resultSetType one of ResultSet.TYPE_...
* @param resultSetConcurrency one of ResultSet.CONCUR_...
* @param sqlSupplier the SQL supplier
*
* @return the prepared statement for this db
*/
public synchronized PreparedStatementWrapper getPreparedStatement(
StatementKey stmtKey, boolean alwaysPrepare, int resultSetType, int resultSetConcurrency, Supplier sqlSupplier) {
assertNotRemote();
attach();
PreparedStatementWrapper stmt = con.getPreparedStatement(stmtKey, alwaysPrepare, resultSetType, resultSetConcurrency, sqlSupplier);
stmt.markReady(); // mark ready for being used once
return stmt;
}
/**
* Gets the prepared statement.
* Uses {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#CONCUR_READ_ONLY}.
*
* @param stmtKey the statement key
* @param alwaysPrepare true if always do a physical prepare
* @param sqlSupplier the SQL supplier
*
* @return the prepared statement for this db
*/
public PreparedStatementWrapper getPreparedStatement(StatementKey stmtKey, boolean alwaysPrepare,
Supplier sqlSupplier) {
return getPreparedStatement(stmtKey, alwaysPrepare, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, sqlSupplier);
}
/**
* Creates a one-shot prepared statement.
*
* Getting the prepared statement will attach the db to a connection.
* The db will be detached after executeUpdate() or executeQuery() when its
* result set is closed.
*
* @param sql the SQL code
* @param resultSetType one of ResultSet.TYPE_...
* @param resultSetConcurrency one of ResultSet.CONCUR_...
*
* @return the prepared statement for this db
*/
public synchronized PreparedStatementWrapper createPreparedStatement(String sql, int resultSetType, int resultSetConcurrency) {
assertNotRemote();
attach();
PreparedStatementWrapper stmt = con.createPreparedStatement(null, sql, resultSetType, resultSetConcurrency);
stmt.markReady(); // mark ready for being used once
return stmt;
}
/**
* Creates a one-shot prepared statement.
* Uses {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#CONCUR_READ_ONLY}.
*
* @param sql the SQL code
*
* @return the prepared statement for this db
*/
public PreparedStatementWrapper createPreparedStatement(String sql) {
return Db.this.createPreparedStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
}
/**
* Sets autoCommit feature.
*
* The method is provided for local db connections only.
* Applications must use begin/commit instead.
* Furthermore, the database will be attached and detached.
*
* The method differs from the JDBC-method: a commit is *NOT* issued
* if the autoCommit boolean value wouldn't change.
* This allows nested setAutoCommit(false) in large transactions.
*
* @param autoCommit the new commit value
* @return the old(!) value of autoCommit
*/
private boolean setAutoCommit (boolean autoCommit) {
if (this.autoCommit != autoCommit) {
if (autoCommit && getBackend().sqlRequiresExtraCommit()) {
// some dbms need a commit before setAutoCommit(true);
con.commit();
}
if (!autoCommit) {
// starting a tx
attach();
}
con.setAutoCommit(autoCommit);
if (autoCommit) {
// ending a tx
detach();
}
this.autoCommit = autoCommit;
LOGGER.finest("physically setAutoCommit({0})", autoCommit);
return !autoCommit; // was toggled
}
else {
return autoCommit; // not changed
}
}
/**
* Gets the current transaction state.
* Technically, a transaction is running if the autocommit is turned off.
*
* @return true if a transaction is running
*/
@Override
public boolean isTxRunning() {
return !autoCommit;
}
/**
* Gets the current user info.
*
* @return the UserInfo
*/
@Override
public SessionInfo getSessionInfo() {
return sessionInfo;
}
/**
* Sets the userInfo.
* This will *NOT* send the session to the remote server
* if this is a remote db.
*
* @param sessionInfo the session info
*/
public void setSessionInfo (SessionInfo sessionInfo) {
this.sessionInfo = sessionInfo;
}
/**
* Gets the backend info.
*
* @return the backend info
*/
public BackendInfo getBackendInfo() {
return backendInfo;
}
@Override
public String getUrl() {
return backendInfo.getUrl();
}
/**
* clears the db local data for close/clone
*/
private static void clearMembers(Db db) {
if (db.isRemote()) {
// cloning a remote connection involves creating an entire new connection via open()!
// notice that a remote db is always open (otherwise it wouldn't be remote ;-))
db.rdel = null;
db.rses = null;
db.rcon = null;
db.delegates = null;
// returning to GC will also GC on server-side (if invoked from close())
}
else {
db.defaultIdSource = null;
db.transaction = null;
}
db.groupConId = 0;
db.conId = 0;
db.ownerThread = null;
if (db.sessionInfo != null && !db.sessionInfo.isImmutable()) {
db.sessionInfo.setSince(0); // not logged in anymore
}
}
/**
* Clones a logical connection.
*
* Connections may be cloned, which results in a new connection using
* the cloned userinfo of the original connection.
* If the old db is already open, the new db will be opened as well.
* If the old db is closed, the cloned db will be closed.
*
* @return the cloned Db
*/
@Override
public Db clone() {
try {
Db newDb = (Db) super.clone();
if (sessionInfo != null) { // we need a new UserInfo cause some things may be stored here!
newDb.setSessionInfo(sessionInfo.clone());
}
clearMembers(newDb);
newDb.instanceNumber = INSTANCE_COUNTER.incrementAndGet();
newDb.clonedFromDb = this;
if (isRemote()) {
newDb.open();
}
else {
if (isOpen()) {
if (newDb.sessionInfo != null) {
newDb.getSessionInfo().setSince(System.currentTimeMillis());
}
newDb.conId = conMgr.login(newDb);
// new connections always start with autocommit on!
newDb.autoCommit = true;
newDb.transaction = null;
newDb.setupIdSource();
}
}
LOGGER.info("connection {0} cloned from {1}, state={2}", newDb, this, newDb.isOpen() ? "open" : "closed");
registerDb(newDb);
return newDb;
}
catch (Exception e) {
throw handleConnectException(e);
}
}
/**
* Gets the original db if this db is cloned.
*
* @return the orginal db, null if this db is not cloned.
*/
public Db getClonedFromDb() {
return clonedFromDb;
}
/**
* Clears the cloned state.
* Useful if the information is no longer needed.
*/
public void clearCloned() {
this.clonedFromDb = null;
}
/**
* Gets the cloned state.
*
* @return true if this db is cloned
*/
public boolean isCloned() {
return clonedFromDb != null;
}
/**
* Gets the default fetchsize
*
* @return the default fetchSize.
*/
public int getFetchSize() {
return fetchSize;
}
/**
* Sets the default fetchsize for all "wrapped" statements
* (PreparedStatementWrapper and StatementWrapper)
*
* @param fetchSize the new default fetchSize
*
*/
public void setFetchSize(int fetchSize) {
this.fetchSize = fetchSize;
}
/**
* gets the maximum number of rows in resultsets.
*
* @return the max rows, 0 = no limit
*/
public int getMaxRows() {
return maxRows;
}
/**
* sets the maximum number of rows in resultsets.
* @param maxRows the max rows, 0 = no limit (default)
*
*/
public void setMaxRows(int maxRows) {
this.maxRows = maxRows;
}
/**
* Gets the type of the logical connection.
*
* @return true if remote, false if local
*/
@Override
public boolean isRemote() {
return backendInfo.isRemote();
}
@Override
public RemoteSession getRemoteSession() {
assertRemote();
return RemoteSessionFactory.getInstance().create(rses);
}
/**
* Prepares a {@link RemoteDelegate}.
*
* The delegates for the AbstractDbObject-classes "live" in the db, i.e. the remote session.
* Thus, delegates are unique per class AND db!
* In order for the AbstractDbObject-derived classes to quickly map to the corresponding delegate,
* the delegates get a unique handle, i.e. an index to an array of delegates, which in
* turn is unique among ALL sessions.
*
* @param clazz is the AbstractDbObject-class
* @return the handle
*/
public synchronized static int prepareRemoteDelegate (Class> clazz) {
if (remoteClasses == null) {
remoteClasses = new Class>[16]; // start with a reasonable size
}
if (nextDelegateId >= remoteClasses.length) {
Class>[] old = remoteClasses;
remoteClasses = new Class>[old.length << 1]; // double size
System.arraycopy(old, 0, remoteClasses, 0, old.length);
}
remoteClasses[nextDelegateId++] = clazz;
return nextDelegateId; // start at 1
}
/**
* Gets the remote delegate by its id.
*
* @param delegateId is the handle for the delegate
* @return the delegate for this session
*/
public RemoteDelegate getRemoteDelegate (int delegateId) {
assertRemote(); // only allowed on remote connections!
delegateId--; // starting from 0
if (delegateId < 0 || delegateId >= remoteClasses.length) {
throw new PersistenceException(this, "delegate handle out of range");
}
// enlarge if necessary
if (delegates == null) {
delegates = new RemoteDelegate[remoteClasses.length];
for (int i=0; i < delegates.length; i++) {
delegates[i] = null;
}
}
if (delegateId >= delegates.length) {
RemoteDelegate[] old = delegates;
delegates = new RemoteDelegate[remoteClasses.length];
System.arraycopy(old, 0, delegates, 0, old.length);
// set the rest to null
for (int i=old.length; i < delegates.length; i++) {
delegates[i] = null;
}
}
// check if delegate already fetched from RMI-server
if (delegates[delegateId] == null) {
// we need to prepare it
assertOpen();
try {
RemoteDelegate delegate = rses.getRemoteDelegate(remoteClasses[delegateId].getName());
delegates[delegateId] = delegate;
return delegate;
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
// already created
return delegates[delegateId];
}
/**
* Creates the remote delegate.
*
* @param className the classname
* @return the delegate
* @throws RemoteException if creating the delegate failed
*/
public RemoteDelegate createRemoteDelegate(String className) throws RemoteException {
assertOpen();
return rses.getRemoteDelegate(className);
}
/**
* Checks whether objects are allowed to count modifications.
* The default is true.
*
* @return true if objects are allowed to count modifications
*/
public boolean isCountModificationAllowed() {
return countModificationAllowed;
}
/**
* Defines whether objects are allowed to count modifications.
* Useful to turn off modcount for a special (temporary) connection doing
* certain tasks that should not be counted.
*
* @param countModificationAllowed true if allowed, false if turned off
*
*/
public void setCountModificationAllowed(boolean countModificationAllowed) {
assertOpen();
if (isRemote()) {
try {
rdel.setCountModificationAllowed(countModificationAllowed);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
this.countModificationAllowed = countModificationAllowed;
}
/**
* Sets the modification allowed state.
* Useful to turn off modlog for a special (temporary) connection doing
* certain tasks that should not be logged.
* The state will be handed over to the remote db-connection as well.
*
* @param logModificationAllowed true to allow, false to deny
*/
public void setLogModificationAllowed(boolean logModificationAllowed) {
assertOpen();
if (isRemote()) {
try {
rdel.setLogModificationAllowed(logModificationAllowed);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
this.logModificationAllowed = logModificationAllowed;
}
/**
* Gets the state of logModificationAllowed.
*
* @return true if modification logging is allowed (default).
*/
public boolean isLogModificationAllowed() {
return logModificationAllowed;
}
/**
* Sets the modification logging deferred state for the current transaction.
* In deferred mode the {@link ModificationLog}s are not written to the database.
* Instead they are kept in memory and can be processed later via
* {@link #popModificationLogsOfTransaction()}.
* The state will be handed over to the remote db-connection as well.
*
* Note: the deferred state will be reset to false after each begin/commit/rollback.
*
* Important: if the deferred state is changed within a transaction and there are
* already modifications made, i.e. modlogs were created, the deferred state will
* *not* be changed! Furthermore, if the deferred state is changed to false.
* the modlogs are removed from the database (but kept in memory).
* As a consequence, the in-memory modlogs will always contain the whole transaction
* or nothing. Only {@link #popModificationLogsOfTransaction()} removes the logs
* collected so far or the end of the transaction.
*
* @param logModificationDeferred true to defer logs for the rest of current transaction, false to undefer
*/
public void setLogModificationDeferred(boolean logModificationDeferred) {
assertOpen();
if (isRemote()) {
try {
this.logModificationDeferred = rdel.setLogModificationDeferred(logModificationDeferred);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
if (this.logModificationDeferred != logModificationDeferred &&
isTxRunning() && modificationLogList != null && !modificationLogList.isEmpty()) {
// deferred changed within transaction and modlogs already created
if (logModificationDeferred) {
// remove already persisted modlogs from database
for (ModificationLog log: modificationLogList) {
log.deleteObject();
log.unmarkDeleted();
}
}
else {
// don't turn off until end of transaction!
return;
}
}
this.logModificationDeferred = logModificationDeferred;
}
}
/**
* Gets the state for logModificationDeferred.
*
* @return true if modification logging is deferred. Default is not deferred.
*/
public boolean isLogModificationDeferred() {
return logModificationDeferred;
}
/**
* Pushes a {@link ModificationLog} to the list of modlogs of the current transaction.
*
* Notice: only allowed for local databases!
*
* @param log the modlog
* @see #popModificationLogsOfTransaction()
*/
public void pushModificationLogOfTransaction(ModificationLog log) {
if (modificationLogList == null) {
modificationLogList = new ArrayList<>();
}
modificationLogList.add(log);
}
/**
* Returns the {@link ModificationLog}s of the current transaction.
* Upon return the list of logs is cleared.
*
* Works for local and remote databases.
*
* @return the logs, null = no logs
* @see #pushModificationLogOfTransaction(org.tentackle.persist.ModificationLog)
*/
public List popModificationLogsOfTransaction() {
assertOpen();
List list = modificationLogList;
if (isRemote()) {
try {
list = rdel.popModificationLogsOfTransaction();
applyTo(list);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
modificationLogList = null;
}
return list;
}
/**
* Gets the default {@link IdSource}.
*
* @return the idSource
*/
public IdSource getDefaultIdSource() {
return defaultIdSource;
}
/**
* Set the default {@link IdSource}.
* This is the source that is used to generate unique object IDs
* if classes did not configure their own source.
*
* @param idSource New value of property idSource.
*/
public void setDefaultIdSource(IdSource idSource) {
this.defaultIdSource = idSource;
}
/**
* Returns the current transaction id from the last BEGIN modification log.
* The tx-ID is only available if logModificationTx is true.
*
* @return the tx ID, 0 if no transaction is pending.
*/
public long getLogModificationTxId() {
assertOpen();
if (isRemote()) {
try {
return rdel.getLogModificationTxId();
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
return logModificationTxId;
}
}
/**
* Sets the transaction id.
* Normally the tx-ID is derived from the id of the BEGIN-modlog
* so it's not necessary to invoke this method from an application.
* (Poolkeeper's replication layer will do so!)
*
* @param logModificationTxId the transaction ID
*/
public void setLogModificationTxId(long logModificationTxId) {
assertOpen();
if (isRemote()) {
try {
rdel.setLogModificationTxId(logModificationTxId);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
this.logModificationTxId = logModificationTxId;
}
}
/**
* Gets the value of logModificationTx.
*
* @return true if transaction begin/end is logged in the modification log, false if not (default)
*/
public boolean isLogModificationTxEnabled() {
return logModificationTxEnabled;
}
/**
* Turn transaction-logging on or off. Default is off.
* With enabled logging the transactions are logged in the {@link ModificationLog}
* as well.
*
* @param logModificationTxEnabled true to turn on transaction logging.
*/
public void setLogModificationTxEnabled(boolean logModificationTxEnabled) {
assertOpen();
if (isRemote()) {
try {
rdel.setLogModificationTxEnabled(logModificationTxEnabled);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
this.logModificationTxEnabled = logModificationTxEnabled;
}
/**
* Gets the database backend.
*
* @return the backend
*/
public Backend getBackend() {
return backendInfo.getBackend();
}
/**
* Gets the name of the remote server application.
*
* @return the remote name
*/
public String getRemoteName() {
assertRemote();
try {
return getRemoteDbSession().getServerName();
}
catch (RemoteException re) {
throw PersistenceException.createFromRemoteException(this, re);
}
}
}