All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.persist.Db Maven / Gradle / Ivy

There is a newer version: 21.16.1.0
Show newest version
/**
 * 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy