org.tentackle.dbms.ModificationLog Maven / Gradle / Ivy
Show all versions of tentackle-database Show documentation
/*
* 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 extends ModificationLog> selectByTxId(long txId) {
Db db = getSession();
if (db.isRemote()) {
try {
List extends ModificationLog> 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 extends ModificationLog> 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");
}
}