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

org.tentackle.dbms.ModificationLog 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.DateHelper;
import org.tentackle.common.Freezable;
import org.tentackle.common.ParameterString;
import org.tentackle.common.RemoteMethod;
import org.tentackle.common.StringHelper;
import org.tentackle.common.Timestamp;
import org.tentackle.dbms.rmi.ModificationLogRemoteDelegate;
import org.tentackle.log.Logger;
import org.tentackle.misc.Canonicalizer;
import org.tentackle.session.ClassId;
import org.tentackle.session.NotFoundException;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.Persistent;
import org.tentackle.session.Session;
import org.tentackle.session.SessionUtilities;
import org.tentackle.session.TableName;
import org.tentackle.sql.Backend;

import java.io.Serial;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

/*
 * @> $mapfile
 *
 * # modification table for async coupling
 * name := $classname
 * id := $classid
 * table := $tablename
 *
 * [remote]
 *
 * ## attributes
 * long                     objectId         objectid         object id
 * int                      objectClassId    classid          object class id
 * String(192)              objectClassName  classname        object classname (if classid == 0)
 * long                     txId             txid             transaction id (optional)
 * String(64)               txName           txname           transaction name (optional) [TRIMWRITE]
 * ModificationType   modType          modtype          modification type
 * Timestamp                when             modtime          time of event
 * long                     userId           userid           user id
 * String                   message          message          optional informational or error message [NOMETHOD]
 * Timestamp                processed        processed        processing time [MAPNULL]
 *
 * ## indexes
 * index next := processed, id
 * index txid := txid
 * index object := objectid, classid, processed
 * index user := userid, processed
 *
 * @<
 */


/**
 * Logging for object modifications.
* * Modifications of PDOs can be logged to a so-called modification log.
* Such modlogs can be used for asynchronous database coupling, higher level replication, etc... *

* Note: the txId is only valid (> 0) if the session has {@link Db#isLogModificationTxEnabled}, * i.e. begin and commit transaction events are logged as well. If the {@link IdSource} of the modlog is * transaction-based, transactions will not overlap in the table because obtaining * the id for the modlog is part of the transaction. However, if the idSource is * remote, transactions may overlap! * In such cases the txid is necessary to separate the modlog sequences into * discrete transactions. (see the PoolKeeper project) */ @ClassId(/**/2/**/) // @wurblet < Inject $classid @TableName(/**/"modlog"/**/) // @wurblet < Inject --string $tablename public class ModificationLog extends AbstractDbObject { @Serial private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.get(ModificationLog.class); /** Variables common to all instances of {@link ModificationLog}. */ public static final DbObjectClassVariables CLASSVARIABLES = DbObjectClassVariables.create(ModificationLog.class); // @wurblet fieldlenghts ColumnNames --model=$mapfile ////GEN-BEGIN:fieldlenghts /** database column name for 'objectId'. */ public static final String CN_OBJECTID = "objectid"; /** database column name for 'objectClassId'. */ public static final String CN_OBJECTCLASSID = "classid"; /** database column name for 'objectClassName'. */ public static final String CN_OBJECTCLASSNAME = "classname"; /** database column name for 'txId'. */ public static final String CN_TXID = "txid"; /** database column name for 'txName'. */ public static final String CN_TXNAME = "txname"; /** database column name for 'modType'. */ public static final String CN_MODTYPE = "modtype"; /** database column name for 'when'. */ public static final String CN_WHEN = "modtime"; /** database column name for 'userId'. */ public static final String CN_USERID = "userid"; /** database column name for 'message'. */ public static final String CN_MESSAGE = "message"; /** database column name for 'processed'. */ public static final String CN_PROCESSED = "processed"; ////GEN-END:fieldlenghts // @wurblet fieldnames ColumnLengths --model=$mapfile ////GEN-BEGIN:fieldnames /** maximum number of characters for 'objectClassName'. */ int CL_OBJECTCLASSNAME = 192; /** maximum number of characters for 'txName'. */ int CL_TXNAME = 64; ////GEN-END:fieldnames /** * The {@link AbstractDbObject} the log belongs to. null = unknown. Speeds up {@link #getObject} * in distributed applications (see the poolkeeper framework). *

* If the lazyObject is a PersistentObject, it's getPdo() method will hold the PDO. */ protected ModificationLoggable lazyObject; /** * Message parameters (lazy) */ protected ParameterString messageParameters; // @wurblet declare Declare --model=$mapfile ////GEN-BEGIN:declare /** object id. */ private long objectId; /** object class id. */ private int objectClassId; /** object classname (if classid == 0). */ private String objectClassName; /** transaction id (optional). */ private long txId; /** transaction name (optional). */ private String txName; /** modification type. */ private ModificationType modType; /** time of event. */ private Timestamp when; /** user id. */ private long userId; /** optional informational or error message. */ private String message; /** processing time. */ private Timestamp processed; ////GEN-END:declare /** * Creates an empty modification log for a given session. * Useful for reading the log or as an RMI-proxy. * * @param db the session */ public ModificationLog(Db db) { super(db); } /** * Creates a modification log for a given session and modification type.
* * @param db the session * @param modType is the modification type (BEGIN or COMMIT) */ public ModificationLog(Db db, ModificationType modType) { this(db); this.modType = modType; txName = db.getTxName(); txId = db.getLogModificationTxId(); when = DateHelper.now(); userId = db.getSessionInfo().getUserId(); } /** * Creates a modification log from an object. * * @param object is the logged object * @param modType is the modification type (INSERT, UPDATE...) */ @SuppressWarnings("rawtypes") public ModificationLog(ModificationLoggable object, ModificationType modType) { this((Db) object.getSession(), modType); if (modType == DbModificationType.BEGIN || modType == DbModificationType.COMMIT) { throw new PersistenceException(this, "illegal BEGIN or COMMIT in object logging"); } objectId = object.getId(); /* * The modlog's serial should reflect the serial of the object. * The modlog is inserted _after_ the object has been modified in the db. * Because the modlog's serial will be incremented during save(), we need to subtract 1 * from the serial. However, during update, the serial will be incremented _after_ * creating the modlog (see AbstractDbObject.updateObject()), so we need to subtract 1 only * for the other modlog types. */ setSerial(object.getSerial() - (modType == DbModificationType.UPDATE ? 0 : 1)); if (object instanceof AbstractDbObject) { objectClassId = ((AbstractDbObject) object).getClassId(); } if (objectClassId == 0) { objectClassName = object.getClass().getName(); } if (modType == DbModificationType.INSERT || modType == DbModificationType.UPDATE) { // keep object for RMI transfers (not DELETE as this will be loaded on the servers side) lazyObject = object; } } /** * Creates a modlog from another modlog, but a different type. * * @param template the modlog template * @param modType is the modification type (INSERT, UPDATE...) */ public ModificationLog(ModificationLog template, ModificationType modType) { super(template.getSession()); objectId = template.objectId; objectClassId = template.objectClassId; txId = template.txId; txName = template.txName; when = template.when; userId = template.userId; message = template.message; this.modType = modType; } /** * Creates an empty modlog. */ public ModificationLog() { super(); } @Override public DbObjectClassVariables getClassVariables() { return CLASSVARIABLES; } // @wurblet methods MethodsImpl --model=$mapfile --noif ////GEN-BEGIN:methods @Override public ModificationLogRemoteDelegate getRemoteDelegate() { return (ModificationLogRemoteDelegate) super.getRemoteDelegate(); } @Override public void getFields(ResultSetWrapper rs) { super.getFields(rs); if (rs.configureSection(CLASSVARIABLES)) { rs.configureColumn(CN_OBJECTID); rs.configureColumn(CN_OBJECTCLASSID); rs.configureColumn(CN_OBJECTCLASSNAME); rs.configureColumn(CN_TXID); rs.configureColumn(CN_TXNAME); rs.configureColumn(CN_MODTYPE); rs.configureColumn(CN_WHEN); rs.configureColumn(CN_USERID); rs.configureColumn(CN_MESSAGE); rs.configureColumn(CN_PROCESSED); rs.configureColumn(CN_ID); rs.configureColumn(CN_SERIAL); } objectId = rs.getLong(); objectClassId = rs.getInt(); objectClassName = rs.getString(); txId = rs.getLong(); txName = rs.getString(); modType = ModificationType.toInternal(rs.getChar()); when = rs.getTimestamp(); userId = rs.getLong(); message = rs.getString(); processed = rs.getTimestamp(true); setId(rs.getLong()); setSerial(rs.getLong()); } @Override public int setFields(PreparedStatementWrapper st) { int ndx = super.setFields(st); st.setLong(++ndx, objectId); st.setInt(++ndx, objectClassId); st.setString(++ndx, objectClassName); st.setLong(++ndx, txId); st.setString(++ndx, StringHelper.trim(txName, 64)); st.setChar(++ndx, modType == null ? ModificationType.getDefault().toExternal() : modType.toExternal()); st.setTimestamp(++ndx, when); st.setLong(++ndx, userId); st.setString(++ndx, message); st.setTimestamp(++ndx, processed, true); st.setLong(++ndx, getId()); st.setLong(++ndx, getSerial()); return ndx; } @Override public String createInsertSql(Backend backend) { return Backend.SQL_INSERT_INTO + getTableName() + Backend.SQL_LEFT_PARENTHESIS + CN_OBJECTID + Backend.SQL_COMMA + CN_OBJECTCLASSID + Backend.SQL_COMMA + CN_OBJECTCLASSNAME + Backend.SQL_COMMA + CN_TXID + Backend.SQL_COMMA + CN_TXNAME + Backend.SQL_COMMA + CN_MODTYPE + Backend.SQL_COMMA + CN_WHEN + Backend.SQL_COMMA + CN_USERID + Backend.SQL_COMMA + CN_MESSAGE + Backend.SQL_COMMA + CN_PROCESSED + Backend.SQL_COMMA + CN_ID + Backend.SQL_COMMA + CN_SERIAL + Backend.SQL_INSERT_VALUES + Backend.SQL_PAR_COMMA.repeat(11) + Backend.SQL_PAR + Backend.SQL_RIGHT_PARENTHESIS; } @Override public String createUpdateSql(Backend backend) { return Backend.SQL_UPDATE + getTableName() + Backend.SQL_SET + CN_OBJECTID + Backend.SQL_EQUAL_PAR_COMMA + CN_OBJECTCLASSID + Backend.SQL_EQUAL_PAR_COMMA + CN_OBJECTCLASSNAME + Backend.SQL_EQUAL_PAR_COMMA + CN_TXID + Backend.SQL_EQUAL_PAR_COMMA + CN_TXNAME + Backend.SQL_EQUAL_PAR_COMMA + CN_MODTYPE + Backend.SQL_EQUAL_PAR_COMMA + CN_WHEN + Backend.SQL_EQUAL_PAR_COMMA + CN_USERID + Backend.SQL_EQUAL_PAR_COMMA + CN_MESSAGE + Backend.SQL_EQUAL_PAR_COMMA + CN_PROCESSED + Backend.SQL_EQUAL_PAR_COMMA + CN_SERIAL + Backend.SQL_EQUAL + CN_SERIAL + Backend.SQL_PLUS_ONE + Backend.SQL_WHERE + CN_ID + Backend.SQL_EQUAL_PAR + Backend.SQL_AND + CN_SERIAL + Backend.SQL_EQUAL_PAR; } /** * Gets the attribute objectId. * * @return object id */ public long getObjectId() { return objectId; } /** * Sets the attribute objectId. * * @param objectId object id */ public void setObjectId(long objectId) { assertMutable(); this.objectId = objectId; } /** * Gets the attribute objectClassId. * * @return object class id */ public int getObjectClassId() { return objectClassId; } /** * Sets the attribute objectClassId. * * @param objectClassId object class id */ public void setObjectClassId(int objectClassId) { assertMutable(); this.objectClassId = objectClassId; } /** * Gets the attribute objectClassName. * * @return object classname (if classid == 0) */ public String getObjectClassName() { return objectClassName; } /** * Sets the attribute objectClassName. * * @param objectClassName object classname (if classid == 0) */ public void setObjectClassName(String objectClassName) { assertMutable(); this.objectClassName = objectClassName; } /** * Gets the attribute txId. * * @return transaction id (optional) */ public long getTxId() { return txId; } /** * Sets the attribute txId. * * @param txId transaction id (optional) */ public void setTxId(long txId) { assertMutable(); this.txId = txId; } /** * Gets the attribute txName. * * @return transaction name (optional) */ public String getTxName() { return txName; } /** * Sets the attribute txName. * * @param txName transaction name (optional) */ public void setTxName(String txName) { assertMutable(); this.txName = txName; } /** * Gets the attribute modType. * * @return modification type */ public ModificationType getModType() { return modType; } /** * Sets the attribute modType. * * @param modType modification type */ public void setModType(ModificationType modType) { assertMutable(); this.modType = modType; } /** * Gets the attribute when. * * @return time of event */ public Timestamp getWhen() { return when; } /** * Sets the attribute when. * * @param when time of event */ public void setWhen(Timestamp when) { Timestamp.setUTC(when, false); Freezable.freeze(when); assertMutable(); this.when = when; } /** * Gets the attribute userId. * * @return user id */ public long getUserId() { return userId; } /** * Sets the attribute userId. * * @param userId user id */ public void setUserId(long userId) { assertMutable(); this.userId = userId; } /* * no accessor methods for message. * optional informational or error message */ /** * Gets the attribute processed. * * @return processing time */ public Timestamp getProcessed() { return processed; } /** * Sets the attribute processed. * * @param processed processing time */ public void setProcessed(Timestamp processed) { Timestamp.setUTC(processed, false); Freezable.freeze(processed); assertMutable(); this.processed = processed; } ////GEN-END:methods /** * Canonicalize the strings in this modlog.
* Used to reduce communication bandwidth when sending larger collections of modlogs via RMI. * * @param stringCanonicalizer the canonicalizer for the strings, null if none * @param objectCanonicalizer the canonicalizer for the lazy object, null if none */ public void canonicalize(Canonicalizer stringCanonicalizer, Canonicalizer objectCanonicalizer) { if (stringCanonicalizer != null) { txName = stringCanonicalizer.canonicalize(txName); message = stringCanonicalizer.canonicalize(message); messageParameters = null; // force rebuild } if (objectCanonicalizer != null) { lazyObject = objectCanonicalizer.canonicalize(lazyObject); } } /** * {@inheritDoc}.
* Overridden to set the db in lazyObject too (if unmarshalled from remote db) */ @Override public void setSession(Session session) { super.setSession(session); if (lazyObject != null) { lazyObject.setSession(session); } } /** * Clears the lazyObject. * Necessary for replaying modlogs that should not copy the lazyObject * to a remote db. */ public void clearLazyObject() { lazyObject = null; } @Override public ModificationLog readFromResultSetWrapper(ResultSetWrapper rs) { ModificationLog log = super.readFromResultSetWrapper(rs); // clear hidden attributes in case modlog is used more than once for resultSet...() log.clearLazyObject(); log.messageParameters = null; return log; } /** * Returns whether this modlog belongs to the current transaction. * * @return true if part of current transaction */ public boolean isLogOfTransaction() { return getSession().isTxRunning(); } /** * Returns whether the modlog refers to the PDO at the destination side during replay. * * @return true if refer to destination db */ public boolean isDestinationReferringLog() { return modType == DbModificationType.DELETE; } /** * {@inheritDoc} *

* Overridden to check for deferred logging. */ @Override public void saveObject() { Db db = getSession(); if (db.isRemote()) { ModificationLoggable oldLazyObject = lazyObject; lazyObject = null; // don't transfer the lazyObject to the remote server super.saveObject(); lazyObject = oldLazyObject; // restore lazyObject } else { if (isLogOfTransaction()) { if (db.isLogModificationDeferred()) { newId(); // obtain an ID to trigger BEGIN only once setSerial(getSerial() + 1); // increment serial as if it has been saved } else { super.saveObject(); } db.pushModificationLogOfTransaction(this); } else { super.saveObject(); } } } /** * Gets the message parameters. * * @return the parameters, never null * @throws ParseException if parameters are malformed */ public ParameterString getMessageParameters() throws ParseException { if (messageParameters == null) { messageParameters = new ParameterString(getMessage()); } return messageParameters; } /** * Sets the message parameters. *

* Updates the message field as well. * * @param messageParameters the message parameters */ public void setMessageParameters(ParameterString messageParameters) { this.messageParameters = messageParameters; this.message = messageParameters == null ? null : messageParameters.toString(); } /** * Gets a message parameter. * * @param name the parameter's name * @return the parameter's value * @throws ParseException if parsing the message failed */ public String getMessageParameter(String name) throws ParseException { return getMessageParameters().getParameter(name); } /** * Sets a message parameter. *

* Updates the message field as well. * * @param name the parameter's name * @param value the parameter's value * @throws ParseException if parsing the message failed */ public void setMessageParameter(String name, String value) throws ParseException { getMessageParameters().setParameter(name, value); this.message = messageParameters.toString(); } @Override public String toString() { // I: 242222 U2104 T8988112/BlahDomain#move 2016-11-12 14:33:10.000 org.example.Blah[34243/4] "masterData, serverId=404" StringBuilder buf = new StringBuilder(); buf.append(modType).append(": ").append(getId()); if (objectId == 0) { buf.append('/').append(getSerial()); } buf.append(" U").append(userId); if (txId != 0 || txName != null) { buf.append(' '); if (txId != 0) { buf.append('T').append(txId).append('/'); } if (txName != null) { buf.append(txName); } } if (when != null) { buf.append(' ').append(when); } if (objectId != 0) { buf.append(' '); if (objectClassId != 0) { buf.append(SessionUtilities.getInstance().getClassName(objectClassId)); } else if (objectClassName != null) { buf.append(objectClassName); } buf.append('[').append(objectId).append('/').append(getSerial()).append(']'); } else if (objectClassName != null) { buf.append(' ').append(objectClassName); } if (message != null) { buf.append(" \"").append(message).append("\""); } return buf.toString(); } /** * Gets the db object referenced by this ModificationLog.
* The object is lazily cached if the given session equals * the session of this modlog. * * @param the expected returned type * @param session is the session from which to load the object. * @return the object or null if not found */ @SuppressWarnings("unchecked") public T getObject(Session session) { if (lazyObject != null && lazyObject.getSession().equals(session)) { return (T) lazyObject; // already lazily cached for requested session } String className = objectClassName != null ? objectClassName : SessionUtilities.getInstance().getClassName(objectClassId); if (className == null) { throw new PersistenceException(this, "unknown class id " + objectClassId); } try { if (session.isRemote()) { LOGGER.warning("inefficient remote object load while processing " + this); } Class clazz = Class.forName(className); lazyObject = loadObject(session, clazz, objectId); return (T) lazyObject; } catch (ClassNotFoundException | RuntimeException ex) { throw new PersistenceException(this, "can't load object " + className + "[" + objectId + "]", ex); } } /** * Gets the object referenced by this ModificationLog.
* The object is lazily cached. * * @param the expected returned type * @return the object or null if not found. */ public T getObject() { return getObject(getSession()); } /** * Selects the next record to process.
* This is the first unprocessed modlog with the lowest ID. * * @return the modlog, null if no unprocessed log found * * @wurblet selectFirstUnprocessed DbSelectUnique --model=$mapfile processed:=:null +id */ ////GEN-BEGIN:selectFirstUnprocessed public ModificationLog selectFirstUnprocessed() { Db db = getSession(); if (db.isRemote()) { try { ModificationLog obj = getRemoteDelegate().selectFirstUnprocessed(); db.applyTo(obj); return obj; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_FIRST_UNPROCESSED_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setTimestamp(ndx++, null, true); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { if (rs.next()) { return readFromResultSetWrapper(rs); } return null; // not found } } private static final StatementId SELECT_FIRST_UNPROCESSED_STMT = new StatementId(); ////GEN-END:selectFirstUnprocessed /** * Selects the unprocessed modlogs as a result set.
* * @return the resultset * * @wurblet resultSetUnprocessed DbSelectList --model=$mapfile --resultset processed:=:null +id */ ////GEN-BEGIN:resultSetUnprocessed public ResultSetWrapper resultSetUnprocessed() { Db db = getSession(); PreparedStatementWrapper st = getPreparedStatement(RESULT_SET_UNPROCESSED_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx, null, true); return st.executeQuery(); } private static final StatementId RESULT_SET_UNPROCESSED_STMT = new StatementId(); ////GEN-END:resultSetUnprocessed /** * Selects the next record to process greater than a given id. * * @param id the modlog id * @return the modlog, null if no unprocessed log found * * @wurblet selectFirstUnprocessedGreater DbSelectUnique --model=$mapfile id:> processed:=:null +id */ ////GEN-BEGIN:selectFirstUnprocessedGreater public ModificationLog selectFirstUnprocessedGreater(long id) { Db db = getSession(); if (db.isRemote()) { try { ModificationLog obj = getRemoteDelegate().selectFirstUnprocessedGreater(id); db.applyTo(obj); return obj; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_FIRST_UNPROCESSED_GREATER_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_ID); sql.append(Backend.SQL_GREATER_PAR); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setLong(ndx++, id); st.setTimestamp(ndx++, null, true); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { if (rs.next()) { return readFromResultSetWrapper(rs); } return null; // not found } } private static final StatementId SELECT_FIRST_UNPROCESSED_GREATER_STMT = new StatementId(); ////GEN-END:selectFirstUnprocessedGreater /** * Selects the first modlog since a given modification time.
* * @param when the starting modification time * @return the modlog if any exists * * @wurblet resultSetSince DbSelectList --model=$mapfile --resultset when:>= +id */ ////GEN-BEGIN:resultSetSince public ResultSetWrapper resultSetSince(Timestamp when) { Db db = getSession(); PreparedStatementWrapper st = getPreparedStatement(RESULT_SET_SINCE_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_WHEN); sql.append(Backend.SQL_GREATEROREQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx, when); return st.executeQuery(); } private static final StatementId RESULT_SET_SINCE_STMT = new StatementId(); ////GEN-END:resultSetSince /** * Selects the first modlog with an ID greater than given ID. * * @param id the given ID * @return the modlog if any exists * * @wurblet selectGreaterId DbSelectUnique --model=$mapfile id:> +id */ ////GEN-BEGIN:selectGreaterId public ModificationLog selectGreaterId(long id) { Db db = getSession(); if (db.isRemote()) { try { ModificationLog obj = getRemoteDelegate().selectGreaterId(id); db.applyTo(obj); return obj; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_GREATER_ID_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_ID); sql.append(Backend.SQL_GREATER_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setLong(ndx++, id); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { if (rs.next()) { return readFromResultSetWrapper(rs); } return null; // not found } } private static final StatementId SELECT_GREATER_ID_STMT = new StatementId(); ////GEN-END:selectGreaterId /** * Selects the first modlog with an ID greater than given ID. * * @param id the given ID * @return the modlog if any exists * * @wurblet resultSetGreaterId DbSelectList --model=$mapfile --resultset id:> +id */ ////GEN-BEGIN:resultSetGreaterId public ResultSetWrapper resultSetGreaterId(long id) { Db db = getSession(); PreparedStatementWrapper st = getPreparedStatement(RESULT_SET_GREATER_ID_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_ID); sql.append(Backend.SQL_GREATER_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setLong(ndx, id); return st.executeQuery(); } private static final StatementId RESULT_SET_GREATER_ID_STMT = new StatementId(); ////GEN-END:resultSetGreaterId /** * Selects the last processed modlog.
* This is the last processed modlog with the highest ID. * * @return the modlog, null if no processed log found * * @wurblet selectLastProcessed DbSelectUnique --model=$mapfile processed:!=:null -id */ ////GEN-BEGIN:selectLastProcessed public ModificationLog selectLastProcessed() { Db db = getSession(); if (db.isRemote()) { try { ModificationLog obj = getRemoteDelegate().selectLastProcessed(); db.applyTo(obj); return obj; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_LAST_PROCESSED_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_NOTEQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTDESC); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setTimestamp(ndx++, null, true); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { if (rs.next()) { return readFromResultSetWrapper(rs); } return null; // not found } } private static final StatementId SELECT_LAST_PROCESSED_STMT = new StatementId(); ////GEN-END:selectLastProcessed /** * Gets the modlogs pending for a given object. * * @param objectClassId the object's class ID * @param objectId the object's ID * @return the list of modlogs * * @wurblet selectByObject DbSelectList --model=$mapfile processed:=:null objectClassId objectId +id */ ////GEN-BEGIN:selectByObject public List selectByObject(int objectClassId, long objectId) { Db db = getSession(); if (db.isRemote()) { try { List list = getRemoteDelegate().selectByObject(objectClassId, objectId); db.applyTo(list); return list; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(db, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_BY_OBJECT_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTCLASSID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, null, true); st.setInt(ndx++, objectClassId); st.setLong(ndx, objectId); try (ResultSetWrapper rs = st.executeQuery()) { List list = new ArrayList<>(); boolean derived = getClass() != ModificationLog.class; while (rs.next()) { ModificationLog obj = derived ? newInstance() : new ModificationLog(db); list.add(obj.readFromResultSetWrapper(rs)); } return list; } } private static final StatementId SELECT_BY_OBJECT_STMT = new StatementId(); ////GEN-END:selectByObject /** * Selects all logs pending for a given user and type. * * @param userId the user ID * @param modType the modlog type * @return the modlogs * * @wurblet selectByUserAndType DbSelectList --model=$mapfile processed:=:null userId modType */ ////GEN-BEGIN:selectByUserAndType public List selectByUserAndType(long userId, ModificationType modType) { Db db = getSession(); if (db.isRemote()) { try { List list = getRemoteDelegate().selectByUserAndType(userId, modType); db.applyTo(list); return list; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(db, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_BY_USER_AND_TYPE_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_USERID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_MODTYPE); sql.append(Backend.SQL_EQUAL_PAR); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, null, true); st.setLong(ndx++, userId); st.setChar(ndx, modType == null ? ModificationType.getDefault().toExternal() : modType.toExternal()); try (ResultSetWrapper rs = st.executeQuery()) { List list = new ArrayList<>(); boolean derived = getClass() != ModificationLog.class; while (rs.next()) { ModificationLog obj = derived ? newInstance() : new ModificationLog(db); list.add(obj.readFromResultSetWrapper(rs)); } return list; } } private static final StatementId SELECT_BY_USER_AND_TYPE_STMT = new StatementId(); ////GEN-END:selectByUserAndType /** * Checks if there are pending logs for a given user. * * @param userId the user ID * @return true if there are logs * * @wurblet isReferencingUser DbIsReferencing --model=$mapfile processed:=:null userId */ ////GEN-BEGIN:isReferencingUser public boolean isReferencingUser(long userId) { if (getSession().isRemote()) { try { return getRemoteDelegate().isReferencingUser(userId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } PreparedStatementWrapper st = getPreparedStatement(IS_REFERENCING_USER_STMT, b -> { StringBuilder sql = createSelectIdInnerSql(); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_USERID); sql.append(Backend.SQL_EQUAL_PAR); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setTimestamp(ndx++, null, true); st.setLong(ndx++, userId); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { return rs.next(); } } private static final StatementId IS_REFERENCING_USER_STMT = new StatementId(); ////GEN-END:isReferencingUser /** * Checks if there are pending logs for a given object. * * @param objectClassId the object's class ID * @param objectId the object's ID * @return true if there are logs * * @wurblet isReferencingObject DbIsReferencing --model=$mapfile processed:=:null objectClassId objectId */ ////GEN-BEGIN:isReferencingObject public boolean isReferencingObject(int objectClassId, long objectId) { if (getSession().isRemote()) { try { return getRemoteDelegate().isReferencingObject(objectClassId, objectId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } PreparedStatementWrapper st = getPreparedStatement(IS_REFERENCING_OBJECT_STMT, b -> { StringBuilder sql = createSelectIdInnerSql(); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTCLASSID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTID); sql.append(Backend.SQL_EQUAL_PAR); b.buildSelectSql(sql, false, 1, 0); return sql.toString(); } ); int ndx = getBackend().setLeadingSelectParameters(st, 1, 0); st.setTimestamp(ndx++, null, true); st.setInt(ndx++, objectClassId); st.setLong(ndx++, objectId); getBackend().setTrailingSelectParameters(st, ndx, 1, 0); try (ResultSetWrapper rs = st.executeQuery()) { return rs.next(); } } private static final StatementId IS_REFERENCING_OBJECT_STMT = new StatementId(); ////GEN-END:isReferencingObject /** * Updates the processing timestamp by objectclass + objectid + serial less or equal than some value.
* Used to mark modlogs already processed for a given object. (poolkeeper) * * @param processed the new processing timestamp * @param objectClassId the object class ID * @param objectId the object id * @param modType the modification type * @param serial the object serial * @return the number of modlogs updated */ // @wurblet updateByObjectTypeSerial DbUpdateBy --model=$mapfile \ // processed:=:null objectClassId objectId modType serial:<= | processed ////GEN-BEGIN:updateByObjectTypeSerial public int updateByObjectTypeSerial(Timestamp processed, int objectClassId, long objectId, ModificationType modType, long serial) { if (getSession().isRemote()) { try { return getRemoteDelegate().updateByObjectTypeSerial(processed, objectClassId, objectId, modType, serial); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(UPDATE_BY_OBJECT_TYPE_SERIAL_STMT, b -> { StringBuilder sql = createSqlUpdate(); sql.append(CN_PROCESSED).append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_WHEREALL); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTCLASSID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_MODTYPE); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_SERIAL); sql.append(Backend.SQL_LESSOREQUAL_PAR); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, processed, true); st.setTimestamp(ndx++, null, true); st.setInt(ndx++, objectClassId); st.setLong(ndx++, objectId); st.setChar(ndx++, modType == null ? ModificationType.getDefault().toExternal() : modType.toExternal()); st.setLong(ndx, serial); return st.executeUpdate(); } private static final StatementId UPDATE_BY_OBJECT_TYPE_SERIAL_STMT = new StatementId(); ////GEN-END:updateByObjectTypeSerial /** * Deletes by objectclass + objectid + serial less or equal than some value.
* Used to remove modlogs already processed for a given object. (poolkeeper) * * @param objectClassId the object class ID * @param objectId the object id * @param modType the modification type * @param serial the object serial * @return the number of objects removed */ // @wurblet deleteByObjectTypeSerial DbDeleteBy --model=$mapfile \ // processed:=:null objectClassId objectId modType serial:<= ////GEN-BEGIN:deleteByObjectTypeSerial public int deleteByObjectTypeSerial(int objectClassId, long objectId, ModificationType modType, long serial) { if (getSession().isRemote()) { try { return getRemoteDelegate().deleteByObjectTypeSerial(objectClassId, objectId, modType, serial); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(DELETE_BY_OBJECT_TYPE_SERIAL_STMT, b -> { StringBuilder sql = createDeleteAllSql(); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTCLASSID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_OBJECTID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_MODTYPE); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_SERIAL); sql.append(Backend.SQL_LESSOREQUAL_PAR); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, null, true); st.setInt(ndx++, objectClassId); st.setLong(ndx++, objectId); st.setChar(ndx++, modType == null ? ModificationType.getDefault().toExternal() : modType.toExternal()); st.setLong(ndx, serial); return st.executeUpdate(); } private static final StatementId DELETE_BY_OBJECT_TYPE_SERIAL_STMT = new StatementId(); ////GEN-END:deleteByObjectTypeSerial /** * Selects the transaction.
* * @param txId the transaction id * @return the modlogs of the transaction sorted by id * * @wurblet selectByTxId DbSelectList --model=$mapfile txId --bounded +id */ ////GEN-BEGIN:selectByTxId public List selectByTxId(long txId) { Db db = getSession(); if (db.isRemote()) { try { List list = getRemoteDelegate().selectByTxId(txId); db.applyTo(list); return list; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(db, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_BY_TX_ID_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND); sql.append(CN_TXID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setLong(ndx, txId); try (ResultSetWrapper rs = st.executeQuery()) { List list = new ArrayList<>(); boolean derived = getClass() != ModificationLog.class; while (rs.next()) { ModificationLog obj = derived ? newInstance() : new ModificationLog(db); list.add(obj.readFromResultSetWrapper(rs)); } return list; } } private static final StatementId SELECT_BY_TX_ID_STMT = new StatementId(); ////GEN-END:selectByTxId /** * Updates the processing timestamp of a transaction for all unprocessed modlogs. * * @param processed the new processing timestamp, null to set unprocessed * @param txId the transaction id * @return the number of updated modlogs * * @wurblet updateUnprocessedByTxId DbUpdateBy --model=$mapfile txId processed:=:null | processed */ ////GEN-BEGIN:updateUnprocessedByTxId public int updateUnprocessedByTxId(Timestamp processed, long txId) { if (getSession().isRemote()) { try { return getRemoteDelegate().updateUnprocessedByTxId(processed, txId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(UPDATE_UNPROCESSED_BY_TX_ID_STMT, b -> { StringBuilder sql = createSqlUpdate(); sql.append(CN_PROCESSED).append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_WHEREALL); sql.append(Backend.SQL_AND); sql.append(CN_TXID); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, processed, true); st.setLong(ndx++, txId); st.setTimestamp(ndx, null, true); return st.executeUpdate(); } private static final StatementId UPDATE_UNPROCESSED_BY_TX_ID_STMT = new StatementId(); ////GEN-END:updateUnprocessedByTxId /** * Deletes a transaction from the modlogs. * * @param txId the transaction id * @return the number of deleted modlogs * * @wurblet deleteByTxId DbDeleteBy --model=$mapfile txId */ ////GEN-BEGIN:deleteByTxId public int deleteByTxId(long txId) { if (getSession().isRemote()) { try { return getRemoteDelegate().deleteByTxId(txId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(DELETE_BY_TX_ID_STMT, b -> { StringBuilder sql = createDeleteAllSql(); sql.append(Backend.SQL_AND); sql.append(CN_TXID); sql.append(Backend.SQL_EQUAL_PAR); return sql.toString(); } ); int ndx = 1; st.setLong(ndx, txId); return st.executeUpdate(); } private static final StatementId DELETE_BY_TX_ID_STMT = new StatementId(); ////GEN-END:deleteByTxId /** * Deletes processed modlogs.
* Used to limit the backlog for recovery. *

* Note: does not honour any transaction boundaries, so the first already processed * transaction may be incomplete. * * @param processed the maximum processing date * @return the number of deleted modlogs */ // @wurblet deleteProcessed DbDeleteBy --model=$mapfile processed:!=:null processed:<= ////GEN-BEGIN:deleteProcessed public int deleteProcessed(Timestamp processed) { if (getSession().isRemote()) { try { return getRemoteDelegate().deleteProcessed(processed); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(DELETE_PROCESSED_STMT, b -> { StringBuilder sql = createDeleteAllSql(); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_NOTEQUAL_PAR); sql.append(Backend.SQL_AND); sql.append(CN_PROCESSED); sql.append(Backend.SQL_LESSOREQUAL_PAR); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, null, true); st.setTimestamp(ndx, processed, true); return st.executeUpdate(); } private static final StatementId DELETE_PROCESSED_STMT = new StatementId(); ////GEN-END:deleteProcessed /** * Selects all modlogs that are unprocessed or modified up to a given timestamp.
* Used to replay modlogs after a crash. * * @param processed the minimum processing date, null = all * @return the modlogs * * @wurblet selectUpTo DbSelectList --model=$mapfile processed:=:null or processed:>= +id */ ////GEN-BEGIN:selectUpTo public List selectUpTo(Timestamp processed) { Db db = getSession(); if (db.isRemote()) { try { List list = getRemoteDelegate().selectUpTo(processed); db.applyTo(list); return list; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(db, e); } } PreparedStatementWrapper st = getPreparedStatement(SELECT_UP_TO_STMT, b -> { StringBuilder sql = createSelectAllInnerSql(b); sql.append(Backend.SQL_AND).append(Backend.SQL_LEFT_PARENTHESIS); sql.append(CN_PROCESSED); sql.append(Backend.SQL_EQUAL_PAR); sql.append(Backend.SQL_OR); sql.append(CN_PROCESSED); sql.append(Backend.SQL_GREATEROREQUAL_PAR); sql.append(Backend.SQL_RIGHT_PARENTHESIS); sql.append(Backend.SQL_ORDERBY) .append(CN_ID).append(Backend.SQL_SORTASC); b.buildSelectSql(sql, false, 0, 0); return sql.toString(); } ); int ndx = 1; st.setTimestamp(ndx++, null, true); st.setTimestamp(ndx, processed, true); try (ResultSetWrapper rs = st.executeQuery()) { List list = new ArrayList<>(); boolean derived = getClass() != ModificationLog.class; while (rs.next()) { ModificationLog obj = derived ? newInstance() : new ModificationLog(db); list.add(obj.readFromResultSetWrapper(rs)); } return list; } } private static final StatementId SELECT_UP_TO_STMT = new StatementId(); ////GEN-END:selectUpTo @RemoteMethod public List selectTxIds(long fromId, long upToId) { // @wurblet selectTxIds RemoteMethod --model=$mapfile ////GEN-BEGIN:selectTxIds if (getSession().isRemote()) { try { return getRemoteDelegate().selectTxIds(fromId, upToId); } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(this, e); } } ////GEN-END:selectTxIds PreparedStatementWrapper st = getPreparedStatement(SELECT_TX_IDS_STMT, b -> Backend.SQL_SELECT + "DISTINCT(" + CN_TXID + ") FROM " + getTableName() + Backend.SQL_WHERE + CN_ID + ">=? AND " + CN_ID + "<=?" ); st.setLong(1, fromId); st.setLong(2, upToId); try (ResultSetWrapper rs = st.executeQuery()) { List txIds = new ArrayList<>(); while (rs.next()) { txIds.add(rs.getALong(1)); } return txIds; } } private static final StatementId SELECT_TX_IDS_STMT = new StatementId(); /** * Updates the processing timestamp. * * @param processed the processing timestamp, null to mark unprocessed */ public void updateProcessed(Timestamp processed) { if (getSession().isRemote()) { // invoke remote method try { getRemoteDelegate().updateProcessed(processed, this); this.processed = processed; return; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(UPDATE_PROCESSES_STMTID, b -> Backend.SQL_UPDATE + getTableName() + Backend.SQL_SET + CN_PROCESSED + Backend.SQL_EQUAL_PAR + Backend.SQL_WHERE + CN_ID + Backend.SQL_EQUAL_PAR ); st.setTimestamp(1, processed, true); st.setLong(2, getId()); assertThisRowAffected(st.executeUpdate()); this.processed = processed; } private static final StatementId UPDATE_PROCESSES_STMTID = new StatementId(); /** * Updates the processing timestamp and adds a comment to the message field. * * @param processed the processing timestamp, null to mark unprocessed * @param comment the comment to add to the message, null if none * @throws ParseException if message does not contain a valid parameter string */ public void updateDiagnostics(Timestamp processed, String comment) throws ParseException { String parStr; if (comment != null) { ParameterString ps = new ParameterString(message); ps.setParameter("comment", comment); parStr = ps.toString(); } else { parStr = message; // unchanged } if (getSession().isRemote()) { // invoke remote method try { getRemoteDelegate().updateDiagnostics(processed, comment, this); this.processed = processed; this.message = parStr; return; } catch (RemoteException e) { throw PersistenceException.createFromRemoteException(getSession(), e); } } // else: local mode PreparedStatementWrapper st = getPreparedStatement(UPDATE_DIAGNOSTICS_STMTID, b -> Backend.SQL_UPDATE + getTableName() + Backend.SQL_SET + CN_PROCESSED + Backend.SQL_EQUAL_PAR_COMMA + CN_MESSAGE + Backend.SQL_EQUAL_PAR + Backend.SQL_WHERE + CN_ID + Backend.SQL_EQUAL_PAR ); int ndx = 1; st.setTimestamp(ndx++, processed, true); st.setString(ndx++, parStr); st.setLong(ndx, getId()); assertThisRowAffected(st.executeUpdate()); this.processed = processed; this.message = parStr; } private static final StatementId UPDATE_DIAGNOSTICS_STMTID = new StatementId(); /** * Gets the message. * * @return optional informational or error message */ @Persistent(comment = "optional informational or error message") public String getMessage() { return message; } /** * Sets the message. *

* The lazy {@link #messageParameters} are cleared. * * @param message optional informational or error message */ public void setMessage(String message) { assertMutable(); this.message = message; messageParameters = null; // force rebuild } /** * Applies a modification to another db.
* The method is not static to allow overriding (e.g. to extend with more transaction types). * Method is invoked within a tx, so no begin/commit/rollback necessary. *

* The modlog to replay is passed as an argument because the replaying modlog is used * to replay a list of modlogs and may perform some housekeeping and maintain state. * * @param modlog the modification log to replay * @param toDb the db the logs will be applied to * @param lenient true if ignore errors such as already existing objects or wrong serial numbers, * false for exact replay */ public void replay(ModificationLog modlog, Session toDb, boolean lenient) { LOGGER.fine("replaying modlog {0} to {1}", modlog, toDb); // load the object (lazily during rmi, because the object is already switched to local db) boolean retrieveFromDestination = modlog.isDestinationReferringLog(); Session db = retrieveFromDestination ? toDb : modlog.getSession(); AbstractDbObject object = modlog.getObject(db); if (object == null) { handleMissingObject(modlog, toDb); return; // if no exception } if (!retrieveFromDestination) { // switch to other db object.setSession(toDb); } try { // perform any preprocessing replayInitModification(modlog, object); if (modlog.getModType() == DbModificationType.INSERT) { replayInsert(modlog, object, lenient); } else if (modlog.getModType() == DbModificationType.UPDATE) { replayUpdate(modlog, object, lenient); } else if (modlog.getModType() == DbModificationType.DELETE) { replayDelete(modlog, object, lenient); } else { throw new PersistenceException(modlog, "illegal modType " + modlog.getModType() + " for replay"); } // perform any post-processing replayFinishModification(modlog, object); } finally { if (!retrieveFromDestination) { object.setSession(modlog.getSession()); } } } /** * Perform preprocessing for replay. * * @param modlog the modification log * @param object the object to insert */ public void replayInitModification(ModificationLog modlog, AbstractDbObject object) { try { object.initModification(modlog.getModType()); } catch (RuntimeException re) { LOGGER.severe("replaying init-modification failed for " + modlog, re); throw re; } } /** * Perform postprocessing for replay. * * @param modlog the modification log * @param object the object to insert */ public void replayFinishModification(ModificationLog modlog, AbstractDbObject object) { try { object.finishModification(modlog.getModType()); } catch (RuntimeException re) { LOGGER.severe("replaying finish modification failed for " + modlog, re); throw re; } } /** * Replay an insert. * * @param modlog the modification log * @param object the object to insert * @param lenient true if lenient update in case object already exists */ public void replayInsert(ModificationLog modlog, AbstractDbObject object, boolean lenient) { object.setModificationLog(modlog); long oldSerial = object.getSerial(); // keep old serial because of Canonicalizer try { // set the modlog's serial (usually 1), in case multiple updates follow object.setSerial(modlog.getSerial()); if (lenient || object.isReplayedLeniently(DbModificationType.INSERT)) { // check if the object already exists. We must avoid a constraint exception // as this would abort the transaction. long persistedSerial = object.selectSerial(object.getId()); if (persistedSerial > 0) { LOGGER.info("lenient insert of {0} hanged to update with serial {1}", object.toGenericString(), modlog.getSerial()); object.setSerial(persistedSerial); object.updatePlain(); if (object.getSerial() != modlog.getSerial()) { object.updateSerial(modlog.getSerial()); } return; } // else: does not exist -> continue with insert } object.insertPlain(); } catch (RuntimeException re) { LOGGER.severe("replaying update failed for " + modlog, re); throw re; } finally { object.setSerial(oldSerial); } } /** * Replay an update. * * @param modlog the modification log * @param object the object to update * @param lenient true if tolerate wrong serials, false if serial must match */ public void replayUpdate(ModificationLog modlog, AbstractDbObject object, boolean lenient) { long oldSerial = object.getSerial(); // keep old serial for finally below object.setModificationLog(modlog); try { if (object.getSerial() > modlog.getSerial()) { // update only the serial object.setSerial(modlog.getSerial() - 1); object.updateSerial(); } else { if (object.getSerial() < modlog.getSerial()) { String msg = "unexpected modlog serial " + modlog.getSerial() + " > object serial " + object.getSerial(); if (lenient || object.isReplayedLeniently(DbModificationType.UPDATE)) { // take serial from object instead of modlog LOGGER.info("{0} -> update forced in lenient mode", msg); } else { throw new PersistenceException(modlog, msg); } } // this is the final update object.setSerial(object.getSerial() - 1); object.updatePlain(); } } catch (NotFoundException nfx) { // NotFoundException didn't terminate the transaction because only no rows // affected but no constraint violation raised -> we can continue if in lenient mode long persistedSerial = nfx.getPersistedSerial(); object.setSerial(oldSerial); if (persistedSerial == -1) { LOGGER.severe("replaying update failed for " + modlog + " because " + object.toGenericString() + " does not exist in " + object.getSession(), nfx); if (lenient || object.isReplayedLeniently(DbModificationType.UPDATE)) { LOGGER.info("lenient mode -> insert {0}", object.toGenericString()); object.insertPlain(); } else { throw nfx; } } else if (persistedSerial > 0) { LOGGER.severe("replaying update failed for " + modlog + " because " + object.toGenericString() + " exists with wrong serial " + persistedSerial + " in " + object.getSession(), nfx); if (lenient || object.isReplayedLeniently(DbModificationType.UPDATE)) { LOGGER.info("lenient mode -> update {0}", object.toGenericString()); object.setSerial(persistedSerial); object.updatePlain(); if (object.getSerial() != modlog.getSerial()) { object.updateSerial(modlog.getSerial()); } } else { throw nfx; } } else { LOGGER.severe("replaying update failed for " + modlog + " because no rows affected", nfx); throw nfx; } } catch (RuntimeException rex) { LOGGER.severe("replaying update failed for " + modlog, rex); throw rex; } finally { object.setSerial(oldSerial); } } /** * Replay a delete. * * @param modlog the modification log * @param object the object to delete * @param lenient true if ignore missing object, false if object must exist */ public void replayDelete(ModificationLog modlog, AbstractDbObject object, boolean lenient) { object.setModificationLog(modlog); long oldSerial = object.getSerial(); // keep old serial for finally below try { // set the modlog's serial, in case insert and further updates follow object.setSerial(modlog.getSerial()); object.deletePlain(); } catch (NotFoundException nfx) { long persistedSerial = nfx.getPersistedSerial(); object.setSerial(oldSerial); if (persistedSerial == -1) { LOGGER.severe("replaying delete failed for " + modlog + " because " + object.toGenericString() + " does not exist in " + object.getSession(), nfx); if (lenient || object.isReplayedLeniently(DbModificationType.DELETE)) { // object does not exist at all LOGGER.info("lenient mode -> ignored!"); } else { throw nfx; } } else if (persistedSerial > 0) { LOGGER.severe("replaying delete failed for " + modlog + " because " + object.toGenericString() + " exists with wrong serial " + persistedSerial + " in " + object.getSession(), nfx); if (lenient || object.isReplayedLeniently(DbModificationType.DELETE)) { LOGGER.info("lenient mode -> delete " + object.toGenericString()); object.setSerial(persistedSerial); object.deletePlain(); } else { throw nfx; } } } catch (RuntimeException re) { LOGGER.severe("replaying delete failed for " + modlog, re); throw re; } finally { object.setSerial(oldSerial); } } /** * Replay state shared between consecutive invocations of {@link #replay}.
* This is just a DTO. No further logic. */ public static class ReplayState implements Serializable { @Serial private static final long serialVersionUID = 1L; /** saved logModificationAllowed. */ public boolean oldLogModificationAllowed; /** transaction voucher. */ public long txVoucher; /** the transaction id if BEGIN modlog found. */ public long pendingTxId; /** the IDs of the replayed modlogs of this chunk. */ public long[] modlogIDs; /** true if this is the first block. */ public final boolean first; /** true if this is the last block. */ public boolean last; /** * Creates an initial state. */ public ReplayState() { first = true; } } /** * Replays a list of modlogs within a single transaction.
* It will also create new txId if the modlogs are copied. * The method is not static to allow overriding (e.g. to extend with more transaction types). *

* There may be several consecutive invocations within the same transaction. * This allows some feedback during long-running large modlog-lists. * * @param state the state of the preceding invocation, never null * @param modList the list of log objects from the source db * @param copyLog true to copy the logs as well * @param lenient true if lenient, else strict mode * @param toDb the db the logs will be applied to * * @return the replay state, never null * * @throws PersistenceException if replay failed and transaction rolled back */ public ReplayState replay(ReplayState state, List modList, boolean copyLog, boolean lenient, Db toDb) { if (state.first) { state.oldLogModificationAllowed = toDb.isLogModificationAllowed(); toDb.setLogModificationAllowed(false); // don't log twice! state.txVoucher = toDb.begin("replay"); } try { for (ModificationLog log: modList) { // replay object's modification if (log.getModType() != DbModificationType.BEGIN && log.getModType() != DbModificationType.COMMIT) { // ignore BEGIN/COMMIT replay(log, toDb, lenient); // replay next modlog } if (copyLog) { LOGGER.fine("copying modlog {0} to {1}", log, toDb); log.clearLazyObject(); // no more needed, and don't change db in lazyObject and don't transfer to remote db if (log.getModType() == DbModificationType.BEGIN) { state.pendingTxId = log.getId(); } Session oldSession = log.getSession(); log.setSession(toDb); log.setId(0); log.newId(); // force to get a new ID for toDb! if (state.pendingTxId != 0) { log.setTxId(state.pendingTxId); // set the txId if a BEGIN was found } log.insertPlain(); if (log.getModType() == DbModificationType.COMMIT) { state.pendingTxId = 0; // clear pending id } log.setSession(oldSession); } } if (state.last) { toDb.commit(state.txVoucher); toDb.setLogModificationAllowed(state.oldLogModificationAllowed); } state.modlogIDs = new long[modList.size()]; int ndx = 0; for (ModificationLog log: modList) { state.modlogIDs[ndx++] = log.getId(); } return state; } catch (RuntimeException e) { toDb.rollback(state.txVoucher); toDb.setLogModificationAllowed(state.oldLogModificationAllowed); if (e instanceof PersistenceException) { throw e; } else { throw new PersistenceException("replay modList failed", e); } } } /** * Loads the object from storage. * * @param session the session * @param clazz the object class * @param id the object id * @return the object, null if not found */ protected ModificationLoggable loadObject(Session session, Class clazz, long id) { return (ModificationLoggable) DbUtilities.getInstance().selectObject(session, clazz, id, true); } /** * Handles the case when an object to replay is not found. * * @param modlog the original modlog * @param toSession the destination db */ protected void handleMissingObject(ModificationLog modlog, Session toSession) { throw new PersistenceException(modlog.getModType() == DbModificationType.DELETE ? modlog.getSession() : toSession, "object " + SessionUtilities.getInstance().getClassName(modlog.getObjectClassId()) + "[" + modlog.getObjectId() + "] does not exist"); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy