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

org.tentackle.dbms.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.dbms;

import org.tentackle.common.Constants;
import org.tentackle.common.ExceptionHelper;
import org.tentackle.common.FileHelper;
import org.tentackle.daemon.Scavenger;
import org.tentackle.dbms.rmi.DbRemoteDelegate;
import org.tentackle.dbms.rmi.RemoteDbConnection;
import org.tentackle.dbms.rmi.RemoteDbSession;
import org.tentackle.dbms.rmi.RemoteDelegate;
import org.tentackle.io.ReconnectionPolicy;
import org.tentackle.io.Reconnector;
import org.tentackle.io.SocketFactoryFactory;
import org.tentackle.io.SocketFactoryType;
import org.tentackle.log.Logger;
import org.tentackle.log.LoggerFactory;
import org.tentackle.misc.Provider;
import org.tentackle.reflect.ReflectionHelper;
import org.tentackle.session.DefaultSessionTaskDispatcher;
import org.tentackle.session.LoginFailedException;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.RemoteSession;
import org.tentackle.session.SavepointHandle;
import org.tentackle.session.Session;
import org.tentackle.session.SessionCloseHandler;
import org.tentackle.session.SessionClosedException;
import org.tentackle.session.SessionInfo;
import org.tentackle.session.SessionPool;
import org.tentackle.session.SessionTaskDispatcher;
import org.tentackle.session.SessionUtilities;
import org.tentackle.sql.Backend;
import org.tentackle.sql.BackendException;
import org.tentackle.sql.BackendInfo;

import java.io.FileNotFoundException;
import java.io.Serializable;
import java.lang.ref.WeakReference;
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.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;


/**
 * A persistence session.
 * 

* Each thread must use its own session. If threads must share the same session, access to the session * must be synchronized at the application-level! *

* Sessions are an abstraction for a connection between a client * and a server, whereas "server" is not necessarily a database server, * because sessions can be either local or remote.
* A local session 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 session and a physical connection. This is up * to the connection manager. * Local sessions are used in client applications running in 2-tier mode * or in application servers.
* Remote sessions 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 session, 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 session. *

* Local sessions 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 sessions via JDBC the file contains at least the following properties: * *

 * url=jdbc-url
 *
 * Example:
 * url=jdbc:postgresql://gonzo.krake.local/erp
 * 
* * Local sessions 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 sessions 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: *

    *
  • * dynamically loaded JDBC driver *
    driver=org.postgresql.Driver:jar:file:/usr/share/java/postgresql.jar!/
    *
  • *
  • * 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 sessions 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 sessions, the properties (session info) will be sent to the server. * This can be used to set some application specific options or to tune the session. * * @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 sessions via WeakReferences, so the * session still will be finalized when the session isn't used anymore. * The set of sessions is used to enhance the diagnostic utilities. */ private static final Set> SESSIONS = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** * Registers this session. */ private void registerSession() { SESSIONS.add(new WeakReference<>(this)); } /** * Unregisters this session. *

* Also cleans up the set by removing unreferenced or closed sessions. */ private void unregisterSession() { for (Iterator> iter = SESSIONS.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); Db refDb = ref.get(); // if closed or unreferenced or the session to remove if (refDb == null || !refDb.isOpen() || refDb == this) { // == is okay here iter.remove(); } } } /** * Gets a list of all currently open sessions. * * @return the list of open sessions */ public static Collection getAllOpenSessions() { List dbList = new ArrayList<>(); for (Iterator> iter = SESSIONS.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 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 session 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 sessionId; // unique session ID private int sessionGroupId; // ID of the session group, 0 = none private volatile 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 session if this is a cloned one private volatile 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 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 = session is closed because it is treated as crashed private volatile Thread ownerThread; // the exclusive owner thread private WeakReference dispatcherRef; // task dispatcher for asynchroneous tasks private Map applicationProperties; // application-specific properties private ReconnectionPolicy reconnectionPolicy; // reconnection policy, null if none (default) // 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 session. * * @param conMgr the connection manager if local connection (ignored if remote) * @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); } catch (FileNotFoundException e1) { 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 PersistenceException("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 session. * * @return the instance number */ @Override public final int getInstanceNumber() { return instanceNumber; } /** * Gets the session name.
* Consists of the instance number, the connection id and the connection group id. * * @return the session name */ @Override public String getName() { StringBuilder buf = new StringBuilder(); buf.append("Db"); buf.append(instanceNumber); buf.append('c'); buf.append(sessionId); if (sessionGroupId > 0) { buf.append('g'); buf.append(sessionGroupId); } 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); ManagedConnection mc = con; if (mc != null) { buf.append('['); buf.append(mc.getName()); buf.append(']'); } return buf.toString(); } /** * Compares two session instances.
* We simply use the unique instanceNumber.
* Because the instanceNumber is unique, we don't need to override equals/hash. * In fact, we must not override equals because it may be used in WeakHashMaps, where * the object reference counts, for example. * * @param session the session to compare this session 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; } /** * Gets the connection manager for this session. * * @return the connection manager */ public ConnectionManager getConnectionManager() { return conMgr; } /** * Sets the pool manager.
* The method is invoked from a SessionPool when the session is created. * * @param sessionPool the session pool, null = not pooled */ public void setPool(SessionPool sessionPool) { this.dbPool = sessionPool; } /** * Gets the pool manager. * * @return the session pool, null = not pooled */ @Override public SessionPool getPool() { return dbPool; } /** * Checks whether this session 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 session is used in a pool. * * @param poolId the ID given by the pool (> 0), 0 = not used (free session), -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 session is readonly. * * @return true if readonly. */ public boolean isReadOnly() { return readOnly; } /** * Sets the session 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; } /** * Asserts session 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 session 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 session 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 sessions"); } } /** * asserts that this is a remote connection */ public void assertRemote() { if (!isRemote()) { throw new PersistenceException(this, "operation not allowed for local sessions"); } } /** * 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 session is still in use. * Whenever a {@link StatementWrapper} or {@link PreparedStatementWrapper} is used * (i.e executeQuery or executeUpdate), the session 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 session'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; SessionUtilities.getInstance().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; } @Override @SuppressWarnings("unchecked") public T setProperty(String key, T value) { if (isRemote()) { try { return rdel.setProperty(key, value); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } // else: local session synchronized (this) { return (T) getApplicationProperties().put(key, value); } } @Override @SuppressWarnings("unchecked") public T getProperty(String key) { if (isRemote()) { try { return rdel.getProperty(key); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } // else: local session synchronized (this) { return (T) getApplicationProperties().get(key); } } /** * Lazy getter for application properties. * * @return the properties, never null */ private Map getApplicationProperties() { if (applicationProperties == null) { applicationProperties = new HashMap<>(); } return applicationProperties; } /** * 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) { return (LoginFailedException) 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 sessionId = rdel.getSessionId(); } else { // direct connection to database sessionId = conMgr.login(this); } // fresh connections are always in autoCommit mode autoCommit = true; transaction = null; setupIdSource(); if (!sessionInfo.isImmutable()) { sessionInfo.setSince(System.currentTimeMillis()); } alive = true; // all sedsions start alive! if (!isCloned()) { LOGGER.info("session {0} opened", this); } registerSession(); } // warning causes catch (Exception e) { throw handleConnectException(e); } } /** * Re-opens the session. */ public void reOpen() { if (isOpen()) { try { close(); } catch (PersistenceException ex) { // nothing to log } } clearMembers(); unregisterSession(); open(); if (sessionGroupId > 0) { groupWith(sessionGroupId); } } /** * Enables the automatic reconnection capability.
* If the backend is shutdown or not available for some other reason, * the application will retry to connect periodically. * * @param blocking true if reconnection blocks the current thread * @param millis the milliseconds between connection retries */ public void enableReconnection(boolean blocking, long millis) { reconnectionPolicy = DbUtilities.getInstance().createReconnectionPolicy(this, blocking, millis); } /** * Disables the automatic reconnection capability. */ public void disableReconnection() { reconnectionPolicy = null; } /** * Reconnects the session if a reconnection policy is defined. * * @return true if reconnected, false if reconnection disabled or non-blocking in background */ public boolean optionallyReconnect() { if (reconnectionPolicy != null) { Reconnector.getInstance().submit(reconnectionPolicy); return reconnectionPolicy.isBlocking(); } return false; } /** * 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 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) { // don't rcon.logout if crashed, because connection may block. // remote side must timeout anyway. (or has already timed out) if (!crashed) { rcon.logout(rses); } LOGGER.info("remote session {0} closed", this); } } else { if (sessionId > 0) { ManagedConnection c = con; if (c != null) { // if attached try { c.closePreparedStatements(true); // cleanup all pending statements } catch (Exception ex) { LOGGER.warning("closing prepared statements failed for " + this, ex); } } conMgr.logout(this); LOGGER.info("session {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 session failed", e); } finally { // whatever happened: make it unusable clearMembers(); // clear members for re-open unregisterSession(); } } } /** * Cleanup if unreferenced connection is still open. *

* {@inheritDoc} */ @Override protected void finalize() throws Throwable { try { if (isOpen()) { LOGGER.warning("closing unreferenced open session: " + this); close(); } } catch (Exception ex) { try { LOGGER.severe("closing unreferenced session '" + this + "' failed in finalizer"); } catch (Exception ex2) { // don't stop finalization if just the logging failed } } finally { super.finalize(); } } /** * Sets the crash flag.
* The session may be marked as crashed to reduce logging of succeeding errors. * * @param crashed the crash flag */ public 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 boolean isCrashed() { return crashed; } /** * Gets the connection state. * * @return true if session is open, else false */ @Override public boolean isOpen() { if (isRemote()) { return rdel != null; } else { return sessionId > 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; } /** * 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(String txName, Provider txe) throws E { if (txName == null) { // lambdas don't provide an enclosing method, because they are not inner classes. String clsName = getClass().getName(); for (StackTraceElement se: new Exception().getStackTrace()) { if (!clsName.equals(se.getClassName())) { txName = ReflectionHelper.getClassBaseName(se.getClassName()) + "#" + se.getMethodName(); break; } } if (txName == null) { // 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.get(); commit(txVoucher); return rv; } catch (Throwable 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(Provider txe) throws E { return transaction(null, txe); } /** * 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) { return begin(txName, false); } @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 transaction voucher (!= 0) if a new transaction was begun, else 0 */ private long begin(String txName, boolean fromRemote) { assertOpen(); assertOwnerThread(); long txVoucher = 0; if (isRemote()) { if (autoCommit) { try { txVoucher = rdel.begin(txName); if (txVoucher != 0) { autoCommit = false; // we are now within a tx } else { throw new PersistenceException(this, "server transaction already running"); } } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } // else: only the outermost transaction is initiated by the remote client // there is no transaction nesting level at the client side! // as a result: the nesting level at the server side is max. 1 } else { alive = true; immediatelyRolledBack = 0; // the first begin clears all pending immediate rollbacks() ... if (setAutoCommit(false)) { // new transaction assertNoTxRunning(); modificationLogList = null; transaction = new DbTransaction(this, txName, fromRemote); txVoucher = transaction.getTxVoucher(); } else { // already running transaction assertTxRunning(); transaction.incrementTxLevel(); // just increment the nesting level } } LOGGER.fine("{0} {1}", transaction, txVoucher != 0 ? " begun" : " already running"); return txVoucher; } /** * Creates an unnamed savepoint in the current transaction. * * @return the savepoint handle unique within current transaction */ @Override public SavepointHandle setSavepoint() { if (isRemote()) { try { return rdel.setSavepoint(); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); ManagedConnection mc = connection(); Savepoint savepoint = mc.setSavepoint(); try { SavepointHandle handle = new SavepointHandle(savepoint.getSavepointId()); transaction.addSavepoint(handle, savepoint); return handle; } catch (SQLException sqx) { throw mc.createFromSqlException("setting unnamed savepoint failed", sqx); } } } /** * Creates a savepoint with the given name in the current transaction. * * @param name the savepoint name * * @return the savepoint handle unique within current transaction */ @Override public SavepointHandle setSavepoint(String name) { if (isRemote()) { try { return rdel.setSavepoint(name); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); ManagedConnection mc = connection(); Savepoint savepoint = mc.setSavepoint(name); try { SavepointHandle handle = new SavepointHandle(savepoint.getSavepointName()); transaction.addSavepoint(handle, savepoint); return handle; } catch (SQLException sqx) { throw mc.createFromSqlException("setting named savepoint '" + name + "' failed", sqx); } } } /** * Undoes all changes made after the given Savepoint object was set. * * @param handle the savepoint handle */ @Override public void rollback(SavepointHandle handle) { if (isRemote()) { try { rdel.rollback(handle); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); Savepoint savepoint = transaction.removeSavepoint(handle); if (savepoint == null) { throw new PersistenceException(this, "no such savepoint to rollback: " + handle); } connection().rollback(savepoint); } } /** * Removes the specified Savepoint and subsequent Savepoint objects from the current * transaction. * * @param handle the savepoint handle */ @Override public void releaseSavepoint(SavepointHandle handle) { if (isRemote()) { try { rdel.releaseSavepoint(handle); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); Savepoint savepoint = transaction.removeSavepoint(handle); if (savepoint == null) { throw new PersistenceException(this, "no such savepoint to release: " + handle); } connection().releaseSavepoint(savepoint); } } /** * Returns whethe a transaction with savepoints is currently running. * * @return true if savepoints present */ public boolean isTxWithSavepoints() { return transaction != null && transaction.isWithSavepoints(); } /** * 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; } } /** * 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(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(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) { assertOpen(); assertOwnerThread(); boolean committed = false; if (isRemote()) { if (txVoucher != 0) { try { if (autoCommit) { throw new PersistenceException(this, "no client transaction running"); } committed = rdel.commit(txVoucher); if (committed) { autoCommit = true; // now outside tx again } else { throw new PersistenceException(this, "no server transaction running"); } } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } } else { alive = 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; } @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) { assertOpen(); assertOwnerThread(); boolean rolledBack = false; if (isRemote()) { if (txVoucher != 0) { try { if (autoCommit) { throw new PersistenceException(this, "no client transaction running"); } rolledBack = rdel.rollback(txVoucher, withLog); if (rolledBack) { autoCommit = true; // now outside tx again } else { throw new PersistenceException(this, "no server transaction running"); } } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } } else { alive = true; if (transaction == null) { // allow pending rollbacks after rollbackImmediately if (immediatelyRolledBack > 0) { immediatelyRolledBack--; LOGGER.fine("pending immediate rollback counter decremented -> no physical rollback"); } else { LOGGER.fine("{0}: no transaction running -> no physical rollback", this); } rolledBack = txVoucher != 0; } else { 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(); ManagedConnection c = con; if (c != null) { c.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); } } } return rolledBack; } /** * Rolls back the current transaction, if pending.
* Used to cleanup in case of an exception or alike. *

* Only applicable to local sessions. *

* Applications should use {@link #rollback(long)}.
* Invocations of this method will be logged as a warning. * * @param cause the cause of the rollback, may be null * @return true if rolled back */ public boolean rollbackImmediately(Throwable cause) { assertNotRemote(); try { // cancel all pending statements (usually max. 1) ManagedConnection c = con; if (c != null) { c.cancelRunningStatements(); } if (transaction != null) { LOGGER.warning("*** immediate rollback for {0} ***", this); int currentTxLevel = transaction.getTxLevel(); immediatelyRolledBack = 0; transaction.invalidateTxLevel(); // avoid misleading warnings // log statements only if the root-cause is a persistence exception boolean withLog = ExceptionHelper.extractException(PersistenceException.class, true, cause) != null; if (!rollback(transaction.getTxVoucher(), withLog)) { throw new PersistenceException(this, transaction + " not rolled back despite valid voucher"); } transaction = null; // remember the txLevel for pending rollback()s, if any will arive from the application immediatelyRolledBack = currentTxLevel; forceDetached(); return true; } else { // not within a transaction: cleanup forced forceDetached(); return false; } } catch (RuntimeException rex) { try { ManagedConnection c = con; if (c != null && !c.isClosed()) { // check low level transaction state boolean connectionInTransaction; try { connectionInTransaction = c.getConnection().getAutoCommit(); } catch (SQLException ex) { LOGGER.warning("cannot determine transaction state of " + c + ": 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(c + " still in transaction: performing low-level rollback"); try { c.getConnection().rollback(); c.getConnection().setAutoCommit(true); } catch (SQLException ex) { LOGGER.warning("low-level connection rollback failed for " + c, ex); } try { // this may yield some additional information c.logAndClearWarnings(); } catch (PersistenceException ex) { LOGGER.warning("clear warnings failed for " + c, ex); } } c.setDead(true); // don't use this connection anymore! LOGGER.warning("connection " + c + " marked dead"); } } finally { 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. */ void setConnection(ManagedConnection con) { this.con = con; } /** * Gets the current connection. * * @return the connection, null = not attached */ ManagedConnection getConnection() { return con; } /** * Asserts that that the connection is attached and returns it. * * @return the attached connection */ ManagedConnection connection() { ManagedConnection c = con; if (c == null) { throw new PersistenceException(this, "not attached"); } return c; } /** * Detach the session.
* Used in execption handling to cleanup all pending statements and result sets. */ void forceDetached() { conMgr.forceDetach(this); } /** * Gets the session id. * This is a unique number assigned to this session by the ConnectionManager. * * @return the session id, 0 = session is new and not connected so far */ @Override public int getSessionId() { return sessionId; } /** * Clears the session ID.
* Used by connection managers only. * Package scope! */ void clearSessionId() { sessionId = 0; } /** * 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 != sessionGroupId) { if (number != 0) { LOGGER.info("{0} joined group {1}", this, number); } else { LOGGER.info("{0} removed from group {1}", this, sessionGroupId); } } sessionGroupId = number; } @Override public int getSessionGroupId() { return sessionGroupId; } @Override public int groupWith(int sessionId) { if (sessionId <= 0) { throw new PersistenceException(this, "invalid session id " + sessionId); } assertOpen(); if (isRemote()) { try { sessionGroupId = rdel.groupWith(sessionId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { if (sessionGroupId > 0 && sessionId != sessionGroupId) { throw new PersistenceException(this, "session to join group " + sessionId + " already belongs to group " + sessionGroupId); } setSessionGroupId(sessionId); for (WeakReference dbRef: SESSIONS) { Db db = dbRef.get(); if (db != null && db.sessionId == sessionId) { if (db.sessionGroupId > 0 && sessionId != db.sessionGroupId) { throw new PersistenceException(db, "session already belongs to group " + db.sessionGroupId); } db.setSessionGroupId(sessionId); break; } } } return sessionGroupId; } /** * Attaches the session. *

* Notice: package scope! */ void attach() { assertOpen(); assertOwnerThread(); if (conMgr == null) { throw new PersistenceException(this, "no connection manager"); } if (isPooled() && getPoolId() == 0) { throw new PersistenceException(this, "illegal attempt to attach a pooled session which is not in use"); } conMgr.attach(this); } /** * Detaches the session. *

* Notice: package scope! */ void detach() { if (sessionId > 0) { assertOwnerThread(); conMgr.detach(this); } // else: session is already closed (may happen during cleanup) } /** * Sets the exclusive owner thread. *

* Allows to detect other threads accidently using this session.
* 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 session as soon as they * are instatiated! The session 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 StatementWrapper createStatement (int resultSetType, int resultSetConcurrency) { assertNotRemote(); attach(); try { StatementWrapper stmt = connection().createStatement(resultSetType, resultSetConcurrency); stmt.markReady(); return stmt; } catch (RuntimeException rex) { detach(); throw rex; } } /** * 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 session to a connection. * The session 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 session */ public PreparedStatementWrapper getPreparedStatement(StatementKey stmtKey, boolean alwaysPrepare, int resultSetType, int resultSetConcurrency, Supplier sqlSupplier) { assertNotRemote(); attach(); try { PreparedStatementWrapper stmt = connection().getPreparedStatement( stmtKey, alwaysPrepare, resultSetType, resultSetConcurrency, sqlSupplier); stmt.markReady(); // mark ready for being used once return stmt; } catch (RuntimeException rex) { detach(); throw rex; } } /** * 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 session */ 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 session to a connection. * The session 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 session */ public PreparedStatementWrapper createPreparedStatement(String sql, int resultSetType, int resultSetConcurrency) { assertNotRemote(); attach(); try { PreparedStatementWrapper stmt = connection().createPreparedStatement(null, sql, resultSetType, resultSetConcurrency); stmt.markReady(); // mark ready for being used once return stmt; } catch (RuntimeException rex) { detach(); throw rex; } } /** * 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 session */ 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 sessions 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); connection().commit(); } if (!autoCommit) { // starting a tx attach(); } connection().setAutoCommit(autoCommit); if (autoCommit) { // ending a tx detach(); } this.autoCommit = autoCommit; LOGGER.finer("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 session. * * @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 session's local data for close/clone */ private void clearMembers() { if (isRemote()) { // cloning a remote session involves creating an entire new session via open()! // notice that a remote session is always open (otherwise it wouldn't be remote ;-)) rdel = null; rses = null; rcon = null; delegates = null; // returning to GC will also GC on server-side (if invoked from close()) } else { defaultIdSource = null; transaction = null; } sessionId = 0; ownerThread = null; if (sessionInfo != null && !sessionInfo.isImmutable()) { sessionInfo.setSince(0); // not logged in anymore } } /** * Clones a logical session. *

* Connections may be cloned, which results in a new session using * the cloned userinfo of the original session. * If the old session is already open, the new session will be opened as well. * If the old session is closed, the cloned session 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()); } newDb.clearMembers(); newDb.sessionGroupId = 0; 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.sessionId = conMgr.login(newDb); // new connections always start with autocommit on! newDb.autoCommit = true; newDb.transaction = null; newDb.setupIdSource(); } } LOGGER.info("session {0} cloned from {1}, state={2}", newDb, this, newDb.isOpen() ? "open" : "closed"); newDb.registerSession(); return newDb; } catch (Exception e) { throw handleConnectException(e); } } /** * Gets the original session if this session is cloned. * * @return the orginal session, null if this session 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 session 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 session. * * @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 session, i.e. the remote session. * Thus, delegates are unique per class AND session! * 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) session 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) session doing * certain tasks that should not be logged. * The state will be handed over to the remote session 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 session 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(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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy