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

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

The newest version!
/*
 * Tentackle - https://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.EncryptedProperties;
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.RMISocketFactoryFactory;
import org.tentackle.io.RMISocketFactoryType;
import org.tentackle.io.ReconnectionPolicy;
import org.tentackle.io.Reconnector;
import org.tentackle.log.Logger;
import org.tentackle.misc.Holder;
import org.tentackle.misc.Provider;
import org.tentackle.reflect.ReflectionHelper;
import org.tentackle.session.BackendConfiguration;
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.session.TransactionIsolation;
import org.tentackle.session.TransactionWritability;
import org.tentackle.session.VersionIncompatibleException;
import org.tentackle.sql.Backend;
import org.tentackle.sql.BackendException;
import org.tentackle.sql.BackendInfo;
import org.tentackle.sql.BackendInfoFactory;
import org.tentackle.sql.ScriptRunnerResult;

import java.io.IOException;
import java.io.Serializable;
import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
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.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

/**
 * 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[|backend-type]
 *
 * Example:
 * url=jdbc:postgresql://gonzo.krake.local/erp
 * 
* * The backend type is optional and overrides the default resolution via URL. *

*
* 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[|backend-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).
* The initial socket factory type must correspond to the server's configuration (see {@link org.tentackle.dbms.rmi.RmiServer}). * If this is not the system's default (plain, unencrypted, uncompressed), it must be set via {@code socketfactory}. * Optionally, {@code ciphersuites} and {@code protocols} can be set (again, see {@link org.tentackle.dbms.rmi.RmiServer}). *

* 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
     *  
    * If the password begins with a {@code ~} and the application provides a {@link org.tentackle.common.Cryptor}, * the string following the {@code ~} is considered to be encrypted. If an unencrypted password must begin with a {@code ~}, * use {@code ~~} instead. *
  • *
  • * 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
     *  
    *
  • *
  • * Automatic JDBC batching can be enabled via: *
     *  batchsize=size
     *  
    * if the size is > 1. The persistence layer collects matching statements and executes them within batches. * See {@link DbBatch}. *
  • *
*

* 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 IDSOURCE = "idsource"; /** * Property key for the socket factory. */ public static final String SOCKET_FACTORY = "socketfactory"; /** * Property key for the SSL cipher suites. */ public static final String CIPHER_SUITES = "ciphersuites"; /** * Property key for the SSL protocols. */ public static final String PROTOCOLS = "protocols"; /** * Property key to enable batching for this session. */ public static final String BATCHSIZE = "batchsize"; private static final Logger LOGGER = Logger.get(Db.class); /** * Global close handlers. */ private static final Set GLOBAL_CLOSE_HANDLERS = ConcurrentHashMap.newKeySet(); /** * 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 = ConcurrentHashMap.newKeySet(); /** * 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() { resources.unregisterSession(this); } /** * 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; } /** * Gets an open session by its ID. * * @param sessionId the session ID * @param url the session URL * @return the session, null if no such open session * @see Session#getSessionId() */ public static Db getOpenSession(int sessionId, String url) { for (Iterator> iter = SESSIONS.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); Db refDb = ref.get(); if (refDb != null && refDb.isOpen()) { if (refDb.getSessionId() == sessionId && Objects.equals(refDb.getUrl(), url)) { return refDb; } } else { iter.remove(); } } return null; } private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); // global session instance counter // for RMI remote connections private static Class[] remoteClasses; // the classes the delegates provide service for private static int nextDelegateId; // next handle per class private static final Cleaner CLEANER = Cleaner.create(); // instead of deprecated finalize() // crashed remote sessions are closed from a separate thread to avoid blocking (TCP stack, whatever) private static final AtomicInteger REMOTE_CLOSING_COUNTER = new AtomicInteger(); private static final ExecutorService REMOTE_CLOSING_EXECUTOR = Executors.newCachedThreadPool(runnable -> { Thread t = new Thread(runnable, "Remote Session Closer(" + REMOTE_CLOSING_COUNTER.incrementAndGet() + ")"); t.setDaemon(true); // don't inhibit termination of JVM! return t; }); /** * Holds the resources to be cleaned up. */ private static class Resources implements Runnable { private String name; // the name of the session private final ConnectionManager conMgr; // connection manager for local connections private DbRemoteDelegate rdel; // remote database delegate private RemoteDbConnection rcon; // != null if remote connection established to RMI-server private RemoteDbSession rses; // remote database session if logged in to RMI-server private int sessionId; // the session ID private volatile ManagedConnection con; // connection if attached, null = detached private boolean crashed; // true = session is closed because it is treated as crashed private final Set closeHandlers; // close handlers private Db me; // != null if programmatic cleanup (close) Resources(ConnectionManager conMgr, int sessionId, DbRemoteDelegate rdel, RemoteDbConnection rcon, RemoteDbSession rses) { this.conMgr = conMgr; this.sessionId = sessionId; this.rdel = rdel; this.rcon = rcon; this.rses = rses; closeHandlers = new HashSet<>(); } @Override public void run() { try { if (isOpen()) { try { if (me != null) { for (SessionCloseHandler closeHandler : closeHandlers) { try { closeHandler.beforeClose(me); } catch (RuntimeException ex) { LOGGER.warning("SessionCloseHandler.beforeClose failed for " + me, ex); } } for (SessionCloseHandler closeHandler : GLOBAL_CLOSE_HANDLERS) { try { closeHandler.beforeClose(me); } catch (RuntimeException ex) { LOGGER.warning("global SessionCloseHandler.beforeClose failed for " + me, ex); } } } // else: cleanup unreferenced session. If unref, there cannot be anyone be interested in // this event, because nobody has a reference to it. if (rcon != null && rses != null) { if (crashed) { // don't rses.close() if crashed, because connection may block! // do it from a background thread instead. // remote side must time out anyway. (or has already timed out) // this is especially useful if this is a server running against another server. REMOTE_CLOSING_EXECUTOR.submit(() -> remoteClose(rses)); } else { rses.close(); } if (me != null) { DbUtilities.getInstance().closeGroupsOfSession(me); LOGGER.info("remote session {0} closed", name); } else { LOGGER.warning("unreferenced remote session {0} cleaned up", name); } } else if (sessionId > 0) { ManagedConnection c = con; if (c != null) { // if attached try { c.closePreparedStatements(true); // cleanup all pending statements } catch (RuntimeException ex) { LOGGER.warning("closing prepared statements failed for " + name, ex); } } if (me != null) { DbUtilities.getInstance().closeGroupsOfSession(me); conMgr.logout(me); LOGGER.info("session {0} closed", name); } else { conMgr.cleanup(sessionId, c); LOGGER.warning("unreferenced session {0} cleaned up", name); } } if (me != null) { for (SessionCloseHandler closeHandler : closeHandlers) { try { closeHandler.afterClose(me); } catch (RuntimeException ex) { LOGGER.warning("SessionCloseHandler.afterClose failed for " + me, ex); } } for (SessionCloseHandler closeHandler : GLOBAL_CLOSE_HANDLERS) { try { closeHandler.afterClose(me); } catch (RuntimeException ex) { LOGGER.warning("global SessionCloseHandler.afterClose failed for " + me, ex); } } } } catch (PersistenceException pe) { throw pe; } catch (RemoteException | RuntimeException e) { LOGGER.severe("closing session " + name + " failed", e); } finally { // whatever happened: make it unusable clearMembers(); // clear members for re-open if (me != null) { me.clearMembers(); unregisterSession(me); // no harm if me==null, since session refs are weak } } } } finally { me = null; } } private void remoteClose(RemoteDbSession remoteDbSession) { LOGGER.warning("closing remote session " + remoteDbSession); try { remoteDbSession.close(); } catch (RemoteException e) { LOGGER.warning("remote closing failed", e); } } boolean isOpen() { return sessionId > 0 || rdel != null; } void clearMembers() { rdel = null; rses = null; rcon = null; sessionId = 0; } void unregisterSession(Db session) { 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 == session) { // == is okay here iter.remove(); } } } } private final BackendInfo backendInfo; // the backend information private Resources resources; // resources to be cleaned up (not final due to reopen) private Cleanable cleanable; // to clean up the connection (not final due to reopen) private int instanceNumber; // each session gets a unique instance number private String idConfig; // configuration of IdSource private int sessionGroupId; // ID of the session group, 0 = none private int exportedSessionGroupId; // session group exported to the remote client, 0 = none private SessionPool dbPool; // != null if this Db is managed by a SessionPool private int poolId; // the poolid if dbPool != null private SessionInfo sessionInfo; // session 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 boolean readOnly; // true if database is readonly private IdSource defaultIdSource; // default ID-Source (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 Deque> postCommits; // post commits private boolean postCommitsRunning; // flag to inhibit registering post commits while running them 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 volatile Thread ownerThread; // the exclusive owner thread private WeakReference dispatcherRef; // task dispatcher for asynchronous tasks private Map applicationProperties; // application-specific properties private ReconnectionPolicy reconnectionPolicy; // reconnection policy, null if none (default) private int batchSize; // >1 if JDBC batching should be used for certain prepared statements // for RMI remote connections private RMIClientSocketFactory csf; // client socket factory, null if system default private RemoteDelegate[] delegates; // the delegates per session /** * Creates an instance of a logical session.
* If the login fails due to wrong passwords * or denied access by the application server, * the method {@link #handleConnectException} is invoked. * * @param conMgr the connection manager if local connection (ignored if remote) * @param sessionInfo session information */ public Db(ConnectionManager conMgr, SessionInfo sessionInfo) { instanceNumber = INSTANCE_COUNTER.incrementAndGet(); setSessionInfo(sessionInfo); // establish connection to the database server try { // load configuration settings EncryptedProperties sessionProperties = sessionInfo.getProperties(); EncryptedProperties backendProperties = sessionProperties; // backend properties defaults to session properties String technicalBackendInfoName = sessionInfo.getProperties().getPropertyIgnoreCase(Constants.BACKEND_INFO); if (technicalBackendInfoName != null) { if (technicalBackendInfoName.startsWith(Constants.BACKEND_INFO_USER) || technicalBackendInfoName.startsWith(Constants.BACKEND_INFO_SYSTEM)) { int ndx = technicalBackendInfoName.indexOf(':'); if (ndx >= 0 && ndx < technicalBackendInfoName.length() - 1) { String configName = technicalBackendInfoName.substring(ndx + 1); boolean systemPrefs = technicalBackendInfoName.startsWith(Constants.BACKEND_INFO_SYSTEM); if (sessionInfo.getApplicationName() != null) { BackendConfiguration backendConfiguration = BackendConfiguration.getBackendConfigurations( sessionInfo.getApplicationName(), systemPrefs).get(configName); if (backendConfiguration != null) { backendProperties = new EncryptedProperties(); DbUtilities.getInstance().applyBackendConfiguration(backendConfiguration, backendProperties); } else { LOGGER.warning("no such backend configuration: @{0}:{1} for {2} -> fallback to session properties", systemPrefs ? "system" : "user", configName, sessionInfo.getApplicationName()); } } else { LOGGER.warning("cannot load backend configuration @{0}:{1} due to missing application name -> fallback to session properties", systemPrefs ? "system" : "user", configName); } } // else: user didn't select a backendConfiguration -> continue with default settings from properties } else if (!technicalBackendInfoName.equals(sessionProperties.getName())) { // programmatically predefined (usually technical) backend info from _another_ property file try { backendProperties = FileHelper.loadProperties(technicalBackendInfoName); } catch (IOException e1) { throw new PersistenceException("technical backend properties '" + technicalBackendInfoName + "' could not be loaded", e1); } } } try { backendInfo = BackendInfoFactory.getInstance().create(backendProperties); } catch (BackendException bex) { throw new PersistenceException(this, "invalid configuration in " + sessionInfo.getPropertiesName(), bex); } if (backendInfo.isRemote()) { conMgr = null; // no connection manager for remote connections RMISocketFactoryType factoryType = RMISocketFactoryType.parse(backendProperties.getPropertyIgnoreCase(SOCKET_FACTORY)); csf = RMISocketFactoryFactory.getInstance().createClientSocketFactory(null, factoryType); } else { if (conMgr == null) { throw new PersistenceException("connection manager required for local connections"); } String val = backendProperties.getPropertyIgnoreCase(BATCHSIZE); if (val != null) { batchSize = Integer.parseInt(val); } } String val = sessionProperties.getPropertyIgnoreCase(IDSOURCE); if (val != null) { idConfig = val; } open(conMgr); } catch (PersistenceException rex) { throw rex; } catch (RuntimeException ex) { throw new PersistenceException(this, "database configuration failed", ex); } } /** * Opens the session. * * @param conMgr the connection manager, null if remote */ private void open(ConnectionManager conMgr) { try { int sessionId; // the local or remote session ID DbRemoteDelegate rdel = null; // remote database delegate RemoteDbConnection rcon = null; // != null if remote connection established to RMI-server RemoteDbSession rses = null; // remote database session if logged in to RMI-server if (backendInfo.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.checkServerVersion(rcon.getServerVersion()); // 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 sessions start alive! resources = new Resources(conMgr, sessionId, rdel, rcon, rses); resources.name = toString(); cleanable = CLEANER.register(this, resources); registerSession(); if (!isCloned()) { LOGGER.info("session {0} opened", this); } } catch (MalformedURLException | URISyntaxException | NotBoundException | RemoteException | VersionIncompatibleException ex) { throw handleConnectException(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); if (resources != null) { buf.append('c').append(resources.sessionId); } if (sessionGroupId > 0) { buf.append('g'); buf.append(sessionGroupId); } return buf.toString(); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getName()); // according to management order: pool | connection manager | managed connection | physical connection if (dbPool != null) { buf.append('|').append(dbPool.getName()); } ManagedConnection mc = null; if (resources != null) { mc = resources.con; if (resources.conMgr != null) { buf.append('|').append(resources.conMgr.getName()); } } if (backendInfo != null) { buf.append('|').append(backendInfo); } if (mc != null) { buf.append('|').append(mc.getName()); } if (isTxRunning() && transaction != null) { // during open autoCommit is still false buf.append('|').append(transaction); } return buf.toString(); } /** * Compares two session instances.
* Since the instance number is unique, it is used for the comparison. *

* Notice that this method, as well as equals and hashCode are marked * as final, since they must not be overridden! * * @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 final int compareTo(Session session) { return instanceNumber - ((Db) session).instanceNumber; } @Override public final boolean equals(Object obj) { return super.equals(obj); } @Override public final int hashCode() { return super.hashCode(); } /** * Gets the connection manager for this session. * * @return the connection manager */ public ConnectionManager getConnectionManager() { return resources.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. *

* Notice that this flag is valid for the session, not for the connection, as the physical connection may change over time! * * @param readOnly true if readonly */ public void setReadOnly(boolean readOnly) { assertOpen(); if (isRemote()) { try { resources.rdel.setReadOnly(readOnly); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } this.readOnly = readOnly; } /** * Gets the batch size if batching is enabled. * * @return the batch size, ≤ 1 if disabled * @see DbBatch */ public int getBatchSize() { return batchSize; } /** * Sets the batch size for JDBC auto batching. * * @param batchSize the batch size, ≤ 1 to disable * @see DbBatch */ public void setBatchSize(int batchSize) { this.batchSize = batchSize; } /** * Gets the remote connection object. * * @return the connection object, null if local */ public RemoteDbConnection getRemoteConnection() { return resources.rcon; } /** * Gets the remote session object. * @return the session, null if local */ public RemoteDbSession getRemoteDbSession() { return resources.rses; } /** * Runs an SQL script. * * @param txName the transaction name * @param script the SQL script * @return the results */ public List runScript(String txName, String script) { return transaction(txName, () -> { // connection is safely attached only within a transaction return getBackend().createScriptRunner(connection().getConnection()).run(script); }); } /** * 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 a pooled session is requested from the pool (in use). */ public void assertRequestedIfPooled() { if (isPooled() && getPoolId() == 0) { throw new PersistenceException(this, "illegal attempt to use a pooled session which was already returned to the pool"); } } /** * 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 session */ public void assertNotRemote() { if (isRemote()) { throw new PersistenceException(this, "operation not allowed for remote sessions"); } } /** * asserts that this is a remote session */ 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 regularly 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(); 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(); this.alive = alive; if (alive) { if (isRemote()) { // only for alive=true -> to remote side (remote times out on its own if false) try { resources.rdel.setAlive(true); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { DbTransaction tx = transaction; if (tx != null) { tx.setAlive(true); } } } } @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 asynchronous tasks in a serial manner. * The returned dispatcher is configured to shut down 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 resources.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 resources.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); } @Override public void reOpen() { if (isOpen()) { try { close(); } catch (PersistenceException ex) { // nothing to log } } clearMembers(); unregisterSession(); open(resources.conMgr); 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; } @Override public boolean registerCloseHandler(SessionCloseHandler closeHandler) { return resources.closeHandlers.add(closeHandler); } @Override public boolean unregisterCloseHandler(SessionCloseHandler closeHandler) { return resources.closeHandlers.remove(closeHandler); } @Override public void close() { if (isOpen()) { LOGGER.fine("closing session {0}", this); resources.me = this; // programmatic close cleanable.clean(); } } /** * 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) { resources.crashed = crashed; } /** * Gets the crash flag.
* May be invoked from any thread. * * @return true if Db is marked as crashed */ public boolean isCrashed() { return resources.crashed; } /** * Gets the connection state. * * @return true if session is open, else false */ @Override public boolean isOpen() { return resources.isOpen(); } /** * 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; } @Override public T transaction(String txName, Provider txe) throws E { if (txName == null) { Holder sh = new Holder<>(); // we cannot modify txName from within the lambda below StackWalker.getInstance(Set.of(), 3).forEach(s -> { if (sh.get() == null && !Db.class.getName().equals(s.getClassName())) { sh.accept(ReflectionHelper.getClassBaseName(s.getClassName()) + "#" + s.getMethodName()); } }); if (sh.get() == null) { // call tree too long? -> just use the classname (better than nothing...) txName = txe.getClass().getSimpleName(); int ndx = txName.indexOf('$'); // cut useless info $$Lambda$0/123456789 if (ndx > 0) { txName = txName.substring(0, ndx); } } else { txName = sh.get(); } } long txVoucher = begin(txName); try { T rv = txe.get(); commit(txVoucher); return rv; } catch (Throwable t) { try { if (txVoucher == 0 || SessionUtilities.getInstance().isSilentRollbackSufficient(t)) { rollbackSilently(txVoucher); } else { rollback(txVoucher); } } catch (RuntimeException rex) { LOGGER.severe("rollback failed after " + t.getClass().getSimpleName() + " (" + t.getMessage() + ")", rex); } throw t; } } @Override public T transaction(Provider txe) throws E { return transaction(null, txe); } /** * Creates a transaction. * * @param txName the transaction name, null if <unnamed> * @param fromRemote true if initiated from remote client * @return the transaction */ protected DbTransaction createTransaction(String txName, boolean fromRemote) { return DbTransactionFactory.getInstance().create(this, txName, fromRemote); } @Override public long begin(String txName, TransactionIsolation transactionIsolation, TransactionWritability transactionWritability) { return begin(txName, false, transactionIsolation, transactionWritability); } @Override public long begin(String txName) { return begin(txName, false, null, null); } @Override public long begin() { return begin(null, false, null, 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} * @param transactionIsolation the transaction isolation level, null or {@link TransactionIsolation#DEFAULT} if default * @param transactionWritability the transaction writabilty, null or {@link TransactionWritability#DEFAULT} if default * * @return the transaction voucher (!= 0) if a new transaction was begun, else 0 */ private long begin(String txName, boolean fromRemote, TransactionIsolation transactionIsolation, TransactionWritability transactionWritability) { assertOpen(); assertOwnerThread(); long txVoucher = 0; if (isRemote()) { if (autoCommit) { try { txVoucher = resources.rdel.begin(txName, transactionIsolation, transactionWritability); 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! // However, if remote methods are invoked within a tx, those methods running // at the server may increase the nesting level > 1. } else { alive = true; immediatelyRolledBack = 0; // the first begin clears all pending immediate rollbacks() ... if (setAutoCommit(c -> { // new transaction assertNoTxRunning(); if (transactionIsolation != null && transactionIsolation != TransactionIsolation.DEFAULT) { c.setTransactionIsolation(transactionIsolation.getLevel()); } if (transactionWritability != null && transactionWritability != TransactionWritability.DEFAULT) { c.setReadOnly(transactionWritability.getWritable()); } })) { modificationLogList = null; transaction = createTransaction(txName, fromRemote); txVoucher = transaction.getTxVoucher(); LOGGER.fine("{0}: begin transaction, voucher {1}", this, txVoucher); } else { // already running transaction assertTxRunning(); if (transactionIsolation != null && transactionIsolation != TransactionIsolation.DEFAULT && connection().getTransactionIsolation() != transactionIsolation.getLevel()) { throw new PersistenceException(this, "cannot change a running transaction's isolation from " + TransactionIsolation.valueOf(connection().getTransactionIsolation()) + " to " + transactionIsolation); } if (transactionWritability != null && transactionWritability != TransactionWritability.DEFAULT && transactionWritability.getWritable() != connection().isReadOnly()) { throw new PersistenceException(this, connection().isReadOnly() ? "cannot change a running read-only transaction to read-write" : "cannot change a running read-write transaction to read-only"); } transaction.incrementTxLevel(txName); // just increment the nesting level LOGGER.fine("{0}: nested begin", this); } } 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 resources.rdel.setSavepoint(); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); executeBatch(); 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 resources.rdel.setSavepoint(name); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); executeBatch(); 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 { resources.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 { resources.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 whether 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 {@link DbModificationType#BEGIN} modification log, if necessary. */ public void logBeginTx() { if (logModificationTxEnabled && logModificationTxId == 0 && isTxRunning()) { ModificationLog log = ModificationLogFactory.getInstance().createModificationLog(this, DbModificationType.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, DbModificationType.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, ModificationType) */ public DbTransactionHandle registerPersistenceVisitor(PersistenceVisitor visitor) { if (isRemote()) { try { return resources.rdel.registerPersistenceVisitor(visitor); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { assertTxRunning(); return transaction.registerPersistenceVisitor(visitor); } } /** * Unregisters 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 resources.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 resources.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, ModificationType modType) { return transaction == null || transaction.isPersistenceOperationAllowed(object, modType); } @Override public void postCommit(Consumer postCommit) { if (isTxRunning()) { if (postCommitsRunning) { throw new PersistenceException(this, "post-commits cannot be registered from within another post-commit"); } if (postCommits == null) { postCommits = new ArrayDeque<>(); } postCommits.push(postCommit); } else { throw new PersistenceException(this, "post-commits can only be registered from within a running transaction"); } } /** * Runs the post commits in reverse order of registration.
* Post commits are registered for the session, not the transaction, * i.e. they are not transferred via RMI to the middle tier like {@link CommitTxRunnable}s or {@link RollbackTxRunnable}s. */ protected void runPostCommits() { try { postCommitsRunning = true; if (postCommits != null) { for (Consumer postCommit : postCommits) { postCommit.accept(this); } postCommits = null; } } finally { postCommitsRunning = false; } } /** * Registers a {@link CommitTxRunnable} to be invoked just before * committing a transaction.
* The order of execution corresponds to the order of registration.
* Runnables may create other runnables up to {@link DbTransaction#maxTxRunnableDepth} recursion depth. * * @param commitRunnable the runnable to register * @return the handle */ public DbTransactionHandle registerCommitTxRunnable(CommitTxRunnable commitRunnable) { /* Since CommitTxRunnables (and RollbackTxRunnables) are always executed at the local session's side, they need to be Serializable in order to be transferred from the remote client to the server. That's why an annotation like @PreCommit, similar to @PostCommit, wouldn't work since we cannot transfer method references, which are by definition not serializable. That's why commit- and rollback-runnables are only available at the persistence implementation layer and not at the PDO interface or Session interface layer. */ if (isRemote()) { try { return resources.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 resources.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 resources.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.
* The order of execution corresponds to the order of registration.
* Runnables may create other runnables up to {@link DbTransaction#maxTxRunnableDepth} recursion depth. * * @param rollbackRunnable the runnable to register * @return the handle */ public DbTransactionHandle registerRollbackTxRunnable(RollbackTxRunnable rollbackRunnable) { if (isRemote()) { try { return resources.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 resources.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 resources.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 to 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) { // ignore remote nesting levels try { if (autoCommit) { throw new PersistenceException(this, "no client transaction running"); } committed = resources.rdel.commit(txVoucher); if (committed) { autoCommit = true; // now outside tx again runPostCommits(); } 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! LOGGER.fine("{0}: commit transaction, voucher {1}", this, txVoucher); transaction.assertValidTxVoucher(txVoucher); if (transaction.isTxLevelValid() && transaction.getTxLevel() != 1) { LOGGER.warning(this + ": txLevel=" + transaction.getTxLevel() + ", should be 1"); } transaction.commit(); logCommitTx(); setAutoCommit(null); // according to the specs: this will commit! logModificationDeferred = false; long txNo = getTxNumber(); transaction = null; modificationLogList = null; committed = true; DbUtilities.getInstance().notifyCommit(this, txNo); runPostCommits(); } else { throw new PersistenceException(this, "transaction ended unexpectedly before commit, valid voucher " + txVoucher); } } else { // no voucher, no change (this is ok) LOGGER.fine("{0}: nested commit", this); transaction.decrementTxLevel(); // just decrement the tx level } } 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) { // ignore remote nesting levels try { if (autoCommit) { throw new PersistenceException(this, "no client transaction running"); } rolledBack = resources.rdel.rollback(txVoucher, withLog); if (rolledBack) { autoCommit = true; // now outside tx again postCommits = null; } 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) { // matching begin() started a new tx if (!autoCommit) { // within a transaction LOGGER.fine("{0}: rollback transaction, voucher {1}", this, txVoucher); transaction.assertValidTxVoucher(txVoucher); if (transaction.isTxLevelValid() && transaction.getTxLevel() != 1) { LOGGER.warning(this + ": txLevel=" + transaction.getTxLevel() + ", should be 1"); } transaction.rollback(); ManagedConnection c = resources.con; if (c != null) { c.rollback(withLog); // avoid a commit ... setAutoCommit(null); // ... in setAutoCommit } else { LOGGER.warning("rollback too late: connection already detached"); autoCommit = true; } logModificationDeferred = false; modificationLogList = null; logModificationTxId = 0; long txNo = getTxNumber(); transaction = null; postCommits = null; rolledBack = true; DbUtilities.getInstance().notifyRollback(this, txNo); } else { throw new PersistenceException(this, "transaction ended unexpectedly before rollback (valid voucher)"); } } else { // no voucher, no change (this is ok) LOGGER.fine("{0}: nested rollback", this); transaction.decrementTxLevel(); } } } return rolledBack; } /** * Rolls back the current transaction, if pending.
* Used to clean up 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 */ public void rollbackImmediately(Throwable cause) { assertNotRemote(); try { // cancel all pending statements (usually max. 1) ManagedConnection c = resources.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 if (!rollback(transaction.getTxVoucher(), !SessionUtilities.getInstance().isSilentRollbackSufficient(cause))) { throw new PersistenceException(this, transaction + " not rolled back despite valid voucher"); } transaction = null; postCommits = null; // remember the txLevel for pending rollback()s, if any will arrive from the application immediatelyRolledBack = currentTxLevel; } // else: not within a transaction: cleanup forced forceDetached(); } catch (RuntimeException rex) { try { ManagedConnection c = resources.con; if (c != null && !c.isClosed()) { // check low level transaction state boolean connectionInTransaction = false; try { connectionInTransaction = c.getConnection().getAutoCommit(); } catch (SQLException ex) { c.checkForDeadLink(ex); if (!c.isDead()) { 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); long txNo = getTxNumber(); if (txNo != 0) { DbUtilities.getInstance().notifyRollback(this, txNo); } } 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! } } finally { handleExceptionForScavenger(rex); } } } /** * 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) { resources.con = con; } /** * Gets the current connection. * * @return the connection, null = not attached */ ManagedConnection getConnection() { return resources.con; } /** * Gets the currently attached connection. *

* Throws {@link PersistenceException} if no connection attached. *

* WARNING: this method is provided for internal use only and should not be used by applications! * * @return the attached connection, never null */ public ManagedConnection connection() { ManagedConnection c = resources.con; if (c == null) { throw new PersistenceException(this, "not attached"); } return c; } /** * Detach the session.
* Used in exception handling to clean up all pending statements and result sets. */ void forceDetached() { resources.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 resources.sessionId; } /** * Clears the session ID.
* Used by connection managers only. * Package scope! */ void clearSessionId() { resources.sessionId = 0; } /** * Sets the group number for this session.
* * This is an optional number describing groups of sessions, * which is particularly useful in RMI-servers: if one connection * fails, all others should be closed as well. * Groups are only meaningful for local sessions, i.e. * for remote sessions the group instance refers to that of the RMI-server. * * @param groupId is the group number, 0 = no group */ public void setSessionGroupId(int groupId) { assertOpen(); if (groupId != sessionGroupId) { if (groupId != 0) { if (sessionGroupId != 0) { throw new PersistenceException(this, "session already belongs to group " + sessionGroupId + ", cannot change to group " + groupId); } LOGGER.info("{0} joined group {1}", this, groupId); } else { LOGGER.info("{0} removed from group {1}", this, sessionGroupId); } sessionGroupId = groupId; } } @Override public int getSessionGroupId() { return sessionGroupId; } /** * Gets the session group exported to remote clients. * * @return the exported group, 0 if not a client group */ public int getExportedSessionGroupId() { return exportedSessionGroupId; } /** * Sets the session group exported to remote clients. * * @param groupId the exported group */ public void setExportedSessionGroupId(int groupId) { assertOpen(); if (groupId != exportedSessionGroupId) { if (groupId != 0) { if (exportedSessionGroupId != 0) { throw new PersistenceException(this, "session already belongs to exported group " + exportedSessionGroupId + ", cannot change to exported group " + groupId); } LOGGER.info("{0} joined exported group {1}", this, groupId); } else { LOGGER.info("{0} removed from exported group {1}", this, exportedSessionGroupId); } exportedSessionGroupId = groupId; } } @Override public void groupWith(int sessionId) { assertOpen(); if (isRemote()) { try { Db session2 = getOpenSession(sessionId, getUrl()); if (session2 == null) { throw new PersistenceException(this, "no such local session with ID " + sessionId); } resources.rdel.groupWith(sessionId); setSessionGroupId(sessionId); session2.setSessionGroupId(sessionId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } else { DbUtilities.getInstance().addToSessionGroup(this, sessionId, false); } } /** * Attaches the session. *

* Notice: package scope! */ void attach() { assertOpen(); assertOwnerThread(); assertRequestedIfPooled(); if (resources.conMgr == null) { throw new PersistenceException(this, "no connection manager"); } resources.conMgr.attach(this); } /** * Detaches the session. *

* Notice: package scope! */ void detach() { if (resources.sessionId > 0) { assertOwnerThread(); resources.conMgr.detach(this); } // else: session is already closed (may happen during cleanup) } /** * Detaches the session after exception. */ void detachSafely() { try { detach(); } catch (RuntimeException rx) { LOGGER.warning("ignored exception: " + rx.getMessage()); // just log the reason w/o stacktrace } } /** * Sets the exclusive owner thread. *

* Allows detecting other threads accidentally 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 instantiated! 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) { detachSafely(); 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, SqlSupplier 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) { detachSafely(); 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, SqlSupplier 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 sqlSupplier the SQL code supplier * @param resultSetType one of ResultSet.TYPE_... * @param resultSetConcurrency one of ResultSet.CONCUR_... * * @return the prepared statement for this session */ public PreparedStatementWrapper createPreparedStatement(SqlSupplier sqlSupplier, int resultSetType, int resultSetConcurrency) { assertNotRemote(); attach(); try { ManagedConnection c = connection(); PreparedStatementWrapper stmt = c.createPreparedStatement(null, sqlSupplier.get(c.getBackend()).toString(), resultSetType, resultSetConcurrency); stmt.markReady(); // mark ready for being used once return stmt; } catch (RuntimeException rex) { detachSafely(); throw rex; } } /** * Creates a one-shot prepared statement.
* Uses {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#CONCUR_READ_ONLY}. * * @param sqlSupplier the SQL code supplier * * @return the prepared statement for this session */ public PreparedStatementWrapper createPreparedStatement(SqlSupplier sqlSupplier) { return createPreparedStatement(sqlSupplier, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } /** * Adds a statement with its related PDO to the current batch.
* Does nothing if there is no batched transaction running.
*

* IMPORTANT: only insert, update or delete statements that refer to a single row * are allowed for auto batching! * * @param modType the statement type * @param statement the statement to batch * @param object the object to insert, update or delete * @param rootClassId the root class ID */ @SuppressWarnings("rawtypes") public void addToBatch(ModificationType modType, PreparedStatementWrapper statement, AbstractDbObject object, int rootClassId) { DbBatch batch = getBatch(); if (batch != null) { batch.add(modType, statement, object, rootClassId); } } /** * Executes all pending batched statements.
* Does nothing if there is no batched transaction running. */ public void executeBatch() { DbBatch batch = getBatch(); if (batch != null) { batch.execute(false); } } /** * Gets the current batch. * * @return the batch, null if no batched transaction or no transaction at all */ private DbBatch getBatch() { return transaction != null ? transaction.getBatch() : null; } /** * 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 txStarter connection consumer invoked immediately when a transaction is begun (sets autocommit to false), null for autocommit = true * @return the old(!) value of autoCommit */ private boolean setAutoCommit (Consumer txStarter) { boolean autoCommit = txStarter == null; if (this.autoCommit != autoCommit) { if (autoCommit) { if (getBackend().isExtraCommitRequired()) { // some dbms need a commit before setAutoCommit(true); connection().commit(); } } else { // starting a tx attach(); txStarter.accept(connection()); } 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 session info. * * @return the session info */ @Override public SessionInfo getSessionInfo() { return sessionInfo; } /** * Sets the session info.
* 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 ;-)) delegates = null; // returning to GC will also GC on server-side (if invoked from close()) } else { defaultIdSource = null; transaction = null; logModificationTxId = 0; logModificationDeferred = false; modificationLogList = null; immediatelyRolledBack = 0; } postCommits = null; postCommitsRunning = false; ownerThread = null; autoCommit = true; if (sessionInfo != null && !sessionInfo.isImmutable()) { sessionInfo.setSince(0); // not logged in anymore } if (resources != null) { resources.clearMembers(); } } @Override public Db clone() { return clone(null); // super.clone() is invoked in clone(String) below } @Override public Db clone(String sessionName) { assertOpen(); if (isPooled()) { throw new PersistenceException(this, "pooled sessions must not be cloned"); } try { Db newDb = (Db) super.clone(); if (sessionInfo != null) { // we need a new session info cause some things may be stored here! SessionInfo clonedInfo = sessionInfo.clone(); if (sessionName != null) { clonedInfo.setSessionName(sessionName); } newDb.setSessionInfo(clonedInfo); } newDb.resources = null; newDb.clearMembers(); newDb.sessionGroupId = 0; newDb.exportedSessionGroupId = 0; newDb.instanceNumber = INSTANCE_COUNTER.incrementAndGet(); newDb.clonedFromDb = this; newDb.dispatcherRef = null; newDb.open(resources.conMgr); LOGGER.info("session {0} cloned from {1}", newDb, this); newDb.registerSession(); return newDb; } catch (CloneNotSupportedException | RuntimeException e) { throw handleConnectException(e); } } /** * Gets the original session if this session is cloned. * * @return the original 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(resources.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 static synchronized 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! assertRequestedIfPooled(); // same as in attach(), but for remote session pools 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]; Arrays.fill(delegates, 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 = resources.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 resources.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 modification counting 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 { resources.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 { resources.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 un-defer */ public void setLogModificationDeferred(boolean logModificationDeferred) { assertOpen(); if (isRemote()) { try { this.logModificationDeferred = resources.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 = resources.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 resources.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. * * @param logModificationTxId the transaction ID */ public void setLogModificationTxId(long logModificationTxId) { assertOpen(); if (isRemote()) { try { resources.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 { resources.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