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 - 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.
* 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 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. * * @param commitRunnable the runnable to register * @return the handle */ public DbTransactionHandle registerCommitTxRunnable(CommitTxRunnable commitRunnable) { 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. * * @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