org.tentackle.pdo.AbstractPersistentObject Maven / Gradle / Ivy
/**
* Tentackle - http://www.tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.pdo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.tentackle.app.AbstractApplication;
import org.tentackle.common.Constants;
import org.tentackle.common.Timestamp;
import org.tentackle.misc.DateHelper;
import org.tentackle.misc.IdentifiableMap;
import org.tentackle.misc.ImmutableException;
import org.tentackle.misc.ScrollableResource;
import org.tentackle.misc.TrackedArrayList;
import org.tentackle.misc.TrackedList;
import org.tentackle.pdo.rmi.AbstractPersistentObjectRemoteDelegate;
import org.tentackle.persist.AbstractDbObject;
import org.tentackle.persist.Db;
import org.tentackle.persist.ModificationLog;
import org.tentackle.persist.PreparedStatementWrapper;
import org.tentackle.persist.ResultSetCursor;
import org.tentackle.persist.ResultSetWrapper;
import org.tentackle.persist.rmi.RemoteResultSetCursor;
import org.tentackle.security.Permission;
import org.tentackle.security.SecurityException;
import org.tentackle.security.SecurityFactory;
import org.tentackle.security.SecurityResult;
import org.tentackle.sql.Backend;
import org.tentackle.validate.ValidationFailedException;
import org.tentackle.validate.ValidationResult;
import org.tentackle.validate.ValidationScope;
import org.tentackle.validate.ValidationScopeFactory;
import org.tentackle.validate.ValidationUtilities;
import org.tentackle.validate.scope.ChangeableScope;
import org.tentackle.validate.scope.MandatoryScope;
import org.tentackle.validate.scope.PersistenceScope;
import static org.tentackle.persist.AbstractDbObject.CN_ID;
import static org.tentackle.persist.AbstractDbObject.CN_SERIAL;
import static org.tentackle.persist.AbstractDbObject.CN_TABLESERIAL;
import static org.tentackle.sql.Backend.SQL_ALLSTAR;
import static org.tentackle.sql.Backend.SQL_AND;
import static org.tentackle.sql.Backend.SQL_COMMA;
import static org.tentackle.sql.Backend.SQL_EQUAL;
import static org.tentackle.sql.Backend.SQL_EQUAL_PAR;
import static org.tentackle.sql.Backend.SQL_EQUAL_PAR_COMMA;
import static org.tentackle.sql.Backend.SQL_EQUAL_ZERO;
import static org.tentackle.sql.Backend.SQL_FROM;
import static org.tentackle.sql.Backend.SQL_GREATER_PAR;
import static org.tentackle.sql.Backend.SQL_ISNULL;
import static org.tentackle.sql.Backend.SQL_LEFT_PARENTHESIS;
import static org.tentackle.sql.Backend.SQL_LESS_PAR;
import static org.tentackle.sql.Backend.SQL_LIKE_PAR;
import static org.tentackle.sql.Backend.SQL_OR;
import static org.tentackle.sql.Backend.SQL_ORDERBY;
import static org.tentackle.sql.Backend.SQL_PLUS_ONE;
import static org.tentackle.sql.Backend.SQL_RIGHT_PARENTHESIS;
import static org.tentackle.sql.Backend.SQL_SELECT;
import static org.tentackle.sql.Backend.SQL_SET;
import static org.tentackle.sql.Backend.SQL_UPDATE;
import static org.tentackle.sql.Backend.SQL_WHERE;
import static org.tentackle.sql.Backend.SQL_WHEREALL;
/**
* AbstractApplication database object.
* Extends {@link AbstractDbObject} with features necessary in nearly all
* desktop enterprise applications.
*
* In contrast to AbstractDbObject, which is associated to a {@link Db},
* an AbstractPersistentObject is associated to a {@link DomainContext}.
*
* @param the PDO class (interface)
* @param the persistence implementation class
* @author harald
*/
public abstract class AbstractPersistentObject, P extends AbstractPersistentObject>
extends AbstractDbObject implements PersistentObject {
private static final long serialVersionUID = 1921941642168910515L;
private transient boolean contextImmutable; // true if domain context cannot be changed anymore
private T pdo; // the pdo instance this is a delegate for
private transient T copiedObject; // != null if this object is a "copy" of copiedObject
private boolean persistable = true; // true if persistable (default)
@Persistent("userId of token lock holder")
private long editedBy; // if != 0 : holds the userId of token holder
@Persistent("time since token lock given to user")
private Timestamp editedSince; // if editedBy!=0: time since token given to user, if 0: time since token released
@Persistent("time when token lock expires")
private Timestamp editedExpiry; // time when token expires
/**
* all persistent objects have a "NormText", i.e. a phonetically normalized text which can be searched
* made protected here, so all subclasses can directly access it.
*/
@Persistent("normalized text")
private String normText;
/**
* Optional id of the root entity.
* Improves performance in selects in composite entities.
*/
@Persistent("root entity id")
private long rootId;
/**
* Optional class id of the root entity.
* Improves performance in selects in composite entities
* if the components belong to different kinds of root entities.
*/
@Persistent("root entity class id")
private int rootClassId;
private transient boolean validated; // true if validated (transient because we don't trust remote clients)
private boolean renewTokenLock; // true if renew token lock once(!) upon next persistent operation
// for PdoCache
private transient long cacheAccessCount; // access counter (if caching strategy is MOU)
private transient long cacheAccessTime; // last access time (if caching strategy is LRU)
private transient boolean expired; // true = object is expired in all caches it belongs to
// snapshot
private transient List snapshots; // taken snapshots, null if none
private transient Timestamp editedSinceSnapshot;
private transient Timestamp editedExpirySnapshot;
protected transient boolean objectIsSnapshot; // true if this is a snapshot
/** name of the editedBy column. */
public static final String CN_EDITEDBY = org.tentackle.common.Constants.CN_EDITEDBY;
/** name of the editedBy attribute. */
public static final String AN_EDITEDBY = org.tentackle.common.Constants.AN_EDITEDBY;
/** name of the editedExpiry column. */
public static final String CN_EDITEDEXPIRY = org.tentackle.common.Constants.CN_EDITEDEXPIRY;
/** name of the editedExpiry attribute. */
public static final String AN_EDITEDEXPIRY = org.tentackle.common.Constants.AN_EDITEDEXPIRY;
/** name of the editedSince column. */
public static final String CN_EDITEDSINCE = org.tentackle.common.Constants.CN_EDITEDSINCE;
/** name of the editedSince attribute. */
public static final String AN_EDITEDSINCE = org.tentackle.common.Constants.AN_EDITEDSINCE;
/** name of the normText column. */
public static final String CN_NORMTEXT = org.tentackle.common.Constants.CN_NORMTEXT;
/** name of the normText attribute. */
public static final String AN_NORMTEXT = org.tentackle.common.Constants.AN_NORMTEXT;
/** name of root-ID column. */
public static final String CN_ROOTID = org.tentackle.common.Constants.CN_ROOTID;
/** name of root-ID attribute. */
public static final String AN_ROOTID = org.tentackle.common.Constants.AN_ROOTID;
/** name of rootclass-ID column. */
public static final String CN_ROOTCLASSID = org.tentackle.common.Constants.CN_ROOTCLASSID;
/** name of rootclass-ID attribute. */
public static final String AN_ROOTCLASSID = org.tentackle.common.Constants.AN_ROOTCLASSID;
// transaction names
/** saveImpl copy in context **/
public static final String TX_SAVE_COPY_IN_CONTEXT = "save copy in context";
/** delete all in context **/
public static final String TX_DELETE_ALL_IN_CONTEXT = "delete all in context";
/** transfer edited by **/
public static final String TX_TRANSFER_TOKENLOCK = "transfer token lock";
/**
* Creates an application database object.
*
* @param pdo the persistent domain object this is a delegate for
* @param context the database context
*/
public AbstractPersistentObject(T pdo, DomainContext context) {
this.pdo = pdo;
setDomainContext(context);
}
/**
* Creates an application database object without a domain context
* for a given connection.
* Note: the application must set the context.
*
* @param pdo the persistent domain object this is a delegate for
* @param session the session (must be an instance of {@link Session}).
*/
public AbstractPersistentObject(T pdo, Session session) {
super((Db) session);
this.pdo = pdo;
}
/**
* Creates an application database object without a database context.
* Note: the application must set the context.
*
* @param pdo the persistent domain object this is a delegate for
*/
public AbstractPersistentObject(T pdo) {
super();
this.pdo = pdo;
}
/**
* Creates an application database object without a database context.
*/
public AbstractPersistentObject() {
super();
}
@Override
public DomainDelegate getDomainDelegate() {
return pdo.getDomainDelegate();
}
@Override
public T getPdo() {
return pdo;
}
@Override
public T pdo() {
return pdo;
}
@Override
public void setPdo(T pdo) {
this.pdo = pdo;
}
@Override
public P clone() {
P obj = super.clone();
obj.setPdo(null);
return obj;
}
@Override
@SuppressWarnings("unchecked")
public PersistentObject clonePersistentObject() {
return clone();
}
/**
* Finds a snapshot method.
* Snapshot methods ({@link #createAttributesInSnapshot}, {@link #createComponentsInSnapshot},
* {@link #revertAttributesToSnapshot} and {@link #revertComponentsToSnapshot}) take the snapshot
* object with the correct implementation type and are implemented at some implementation class.
* This method finds the most specific snapshot-method along the inheritance hierarchy.
*
* @param methodName the snapshot method name
* @param clazz the implementation class
* @return the method
* @throws NoSuchMethodException if no such method found
*/
protected Method findSnapshotMethod(String methodName, Class> clazz) throws NoSuchMethodException {
while(clazz != null) {
try {
Method method = clazz.getDeclaredMethod(methodName, clazz);
if (!method.isAccessible()) {
// generated methods are usually protected or private, make them accessible
method.setAccessible(true);
}
return method;
}
catch (NoSuchMethodException ex) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException(methodName);
}
/**
* Creates a snapshot of this object.
*
* The method must be implemented.
* The default implementation just throws PersistenceException.
*
* @return the snapshot
*/
@Override
public T createSnapshot() {
T snapshot = getPdo().clonePdo();
PersistentObject persistentSnapshot = snapshot.getPersistenceDelegate();
// find implementing method (not the create...InSnapshot below!)
Class> persistentClass = persistentSnapshot.getClass();
try {
Method createAttributesInSnapshotMethod = findSnapshotMethod("createAttributesInSnapshot", persistentClass);
Method createComponentsInSnapshotMethod = findSnapshotMethod("createComponentsInSnapshot", persistentClass);
createAttributesInSnapshotMethod.invoke(this, persistentSnapshot);
createComponentsInSnapshotMethod.invoke(this, persistentSnapshot);
addSnapshot(snapshot);
return snapshot;
}
catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new PersistenceException(this, "creating snapshot incorrectly implemented", ex);
}
}
/**
* Updates the attributes in snapshot object.
* The snapshot object is assumed to be a clone of this object.
*
* @param snapshot the snapshot
*/
protected void createAttributesInSnapshot(AbstractPersistentObject snapshot) {
// Important: no snapshot cause of inheritance problems
super.createAttributesInSnapshot(snapshot);
snapshot.objectIsSnapshot = true;
if (editedSince != null) {
snapshot.editedSinceSnapshot = (Timestamp) editedSince.clone();
}
if (editedExpiry != null) {
snapshot.editedExpirySnapshot = (Timestamp) editedExpiry.clone();
}
}
/**
* Updates the components in snapshot object.
The snapshot object is assumed to be a clone of this object.
*
* @param snapshot the snapshot
*/
protected void createComponentsInSnapshot(AbstractPersistentObject snapshot) {
// Important: no snapshot cause of inheritance problems
// default implementation does nothing
}
/**
* Reverts the state of this object to given snapshot.
*
* The method must be implemented.
* The default implementation just throws PersistenceException.
*
* @param snapshot the snapshot to revert to
*/
@Override
public void revertToSnapshot(T snapshot) {
if (snapshot != null) {
assertValidSnapshot(snapshot);
PersistentObject persistentSnapshot = snapshot.getPersistenceDelegate();
// find implementing method (not the create...InSnapshot below!)
Class> persistentClass = persistentSnapshot.getClass();
try {
Method revertAttributesToSnapshotMethod = findSnapshotMethod("revertAttributesToSnapshot", persistentClass);
Method revertComponentsToSnapshotMethod = findSnapshotMethod("revertComponentsToSnapshot", persistentClass);
revertAttributesToSnapshotMethod.invoke(this, persistentSnapshot);
revertComponentsToSnapshotMethod.invoke(this, persistentSnapshot);
}
catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new PersistenceException(this, "reverting snapshot incorrectly implemented", ex);
}
}
}
/**
* Copies all attributes from a snapshot object back to this object.
*
* @param snapshot the snapshot object
*/
@SuppressWarnings("unchecked")
protected void revertAttributesToSnapshot(AbstractPersistentObject snapshot) {
// Important: no snapshot cause of inheritance problems
super.revertAttributesToSnapshot(snapshot);
contextImmutable = snapshot.contextImmutable;
copiedObject = (T) snapshot.copiedObject;
editedBy = snapshot.editedBy;
editedSince = snapshot.editedSince;
if (editedSince != null) {
editedSince.setTime(snapshot.editedSinceSnapshot.getTime());
}
editedExpiry = snapshot.editedExpiry;
if (editedExpiry != null) {
editedExpiry.setTime(snapshot.editedExpirySnapshot.getTime());
}
renewTokenLock = snapshot.renewTokenLock;
cacheAccessCount = snapshot.cacheAccessCount;
cacheAccessTime = snapshot.cacheAccessTime;
expired = snapshot.expired;
snapshots = snapshot.snapshots;
normText = snapshot.normText;
validated = snapshot.validated;
persistable = snapshot.persistable;
}
/**
* Reverts all components of this object to a given snapshot.
*
* @param snapshot the snapshot object
*/
protected void revertComponentsToSnapshot(AbstractPersistentObject snapshot) {
// Important: no snapshot cause of inheritance problems
// default implementation does nothing
}
/**
* Returns whether this is a snapshot.
*
* @return true if this is a snapshot
*/
@Override
public boolean isSnapshot() {
return objectIsSnapshot;
}
/**
* Gets all snapshots.
*
* @return the list of snapshots for this object, null if none taken so far
*/
@Override
public List getSnapshots() {
return snapshots;
}
/**
* Adds a snapshot to the list of snapshots.
*
* @param snapshot the snapshot to add
*/
protected void addSnapshot(T snapshot) {
if (snapshots == null) {
snapshots = new ArrayList<>();
}
for (T sh: snapshots) {
// we cannot use contains() because all snaphots have the same object ID
if (sh == snapshot) {
throw new PersistenceException(this, "snapshot already added");
}
}
snapshots.add(snapshot);
}
/**
* Asserts that given snapshot is valid for this object.
*
* @param snapshot the snapshot
*/
protected void assertValidSnapshot(T snapshot) {
if (!snapshot.isSnapshot()) {
throw new PersistenceException(this, "not a snapshot");
}
if (snapshots != null) {
for (T sh : snapshots) {
// we cannot use contains() because all snaphots have the same object ID
if (sh == snapshot) {
return;
}
}
}
throw new PersistenceException(this, "invalid snapshot");
}
/**
* Asserts that the PDO is not cached.
*/
protected void assertNotCached() {
if (isCached()) {
throw new PersistenceException(this, "object is a cached");
}
}
/**
* {@inheritDoc}
*
* Overridden due to snapshots are immutable.
*/
@Override
protected void assertMutable() {
super.assertMutable();
if (isSnapshot()) {
throw new PersistenceException(this, new ImmutableException("object is a snapshot"));
}
}
@Override
public boolean isPersistable() {
return persistable && super.isPersistable() &&
getDomainContext() != null && // context may be used in security checks!
!isAbstract();
}
/**
* Sets the local persistable flag.
*
* The implementation maintains an additional flag to disable persistabilty.
*
* @param persistable true if persistable, false if not persistable
*/
public void setPersistable(boolean persistable) {
this.persistable = persistable;
}
/**
* {@inheritDoc}
*
* Overridden to check {@link #assertMutable()}.
*/
@Override
protected void assertPersistable() {
super.assertPersistable();
assertMutable();
}
/**
* {@inheritDoc}
*
* The default is false. Override if this is a root-entity.
*/
@Override
public boolean isRootEntity() {
return false;
}
@Override
public boolean isRootIdProvided() {
return false;
}
@Override
public long getRootId() {
return rootId;
}
@Override
public void setRootId(long rootId) {
this.rootId = rootId;
}
@Override
public boolean isRootClassIdProvided() {
return false;
}
@Override
public int getRootClassId() {
return rootClassId;
}
@Override
public void setRootClassId(int rootClassId) {
this.rootClassId = rootClassId;
}
@Override
public boolean isNormTextProvided() {
return false;
}
/**
* Asserts that entity provides a normtext.
*/
protected void assertNormTextProvided() {
if (!isNormTextProvided()) {
throw new PersistenceException(this, "entity does not provide a normtext column");
}
}
/**
* Sets the normtext.
* @param normText the normtext
*/
@Override
public void setNormText (String normText) {
assertMutable();
if (!Objects.equals(this.normText, normText)) {
setModified(true);
}
this.normText = normText;
}
/**
* Gets the normtext.
*
* @return the normtext
*/
@Override
public String getNormText() {
return normText;
}
// ---------------- implements DomainContextDependable -------------------------
/**
* Returns whether the domain context is immutable.
*
* @return true if context cannot be changed
*/
@Override
public boolean isDomainContextImmutable() {
return contextImmutable;
}
/**
* Sets the immutable flag of the domain context.
*
* @param contextImmutable true if context cannot be changed
*/
@Override
public void setDomainContextImmutable(boolean contextImmutable) {
this.contextImmutable = contextImmutable;
}
/**
* Asserts that the domain context is mutable.
*/
protected void assertDomainContextMutable() {
if (isDomainContextImmutable()) {
throw new PersistenceException(this, "domain context is immutable");
}
}
/**
* {@inheritDoc}
*
* Setting the context will also set the session and context id.
*/
@Override
public void setDomainContext(DomainContext context) {
if (getDomainContext() != context) {
if (context == null) {
throw new IllegalArgumentException("domain context cannot be cleared to null");
}
assertDomainContextMutable();
if (isRootEntity()) {
// if this is a root entity: replace with root context
setSessionHolder(context.getRootContext(pdo));
}
else {
setSessionHolder(context);
}
determineContextId();
}
}
/**
* {@inheritDoc}
*
* The default implementation just returns the context.
* Subclasses may override this with a covariant method.
*/
@Override
public DomainContext getDomainContext() {
return (DomainContext) getSessionHolder();
}
/**
* {@inheritDoc}
*
* The default implementation does nothing (object living in a context
* not depending on another object).
*/
@Override
public void determineContextId() {
}
/**
* {@inheritDoc}
*
* The default implementation returns -1.
*/
@Override
public long getContextId() {
return -1;
}
/**
* Gets the SQL code for the context condition.
*
* @return null if no condition (default)
*/
public String getSqlContextCondition() {
return null;
}
/**
* Gets the additional condition to be used in the WHERE clause for the classid.
*
* Example: return " AND " + CN_CLASSID + "=?";
*
*
* @return the condition, null if none necessary
*/
public String getSqlClassIdCondition() {
if (isClassIdRequiredInWhereClause()) {
return Backend.SQL_AND + getTopSuperTableAlias() + '.' + CN_CLASSID + Backend.SQL_EQUAL_PAR;
}
return null;
}
/**
* {@inheritDoc}
*
* The default implementation returns the PDO's DomainContext.
*/
@Override
public DomainContext getBaseContext() {
return getDomainContext();
}
/**
* {@inheritDoc}
*
* The default implementation just returns a new {@link DomainContext}.
*/
@Override
public DomainContext createValidContext() {
Session session = Session.getCurrentSession();
if (session != null) {
session = null; // use the thread-local session for the new context
}
else {
session = getSession();
}
return Pdo.createDomainContext(session);
}
// ---------------- end DomainContextDependable ---------------------------------
/**
* Gets the application oriented class variables for this object.
* Class variables for classes derived from AbstractPersistentObject are kept in an
* instance of {@link PersistentObjectClassVariables}.
*
* @return the class variables
* @see AbstractDbObject#getClassVariables
*/
@Override
public PersistentObjectClassVariables getClassVariables() {
throw new PersistenceException(this, "classvariables undefined for " + getClass());
}
@Override
public String getTableName () {
return getClassVariables().getTableName();
}
/**
* Gets the table alias.
*
* @return the alias, null if class does not map to a database table
*/
public String getTableAlias () {
return getClassVariables().getTableAlias();
}
/**
* Gets the full column name with optional table alias.
*
* @param name the short name
* @return the full name
*/
public String getColumnName(String name) {
return getClassVariables().getColumnName(name);
}
/**
* Gets the alias for the topmost super class.
*
* @return the alias
*/
public String getTopSuperTableAlias() {
return getClassVariables().getTopSuperClassVariables().getTableAlias();
}
/**
* Gets the tablename for the topmost super class.
*
* @return the alias
*/
public String getTopSuperTableName() {
return getClassVariables().getTopSuperClassVariables().getTableName();
}
/**
* Gets the pdo-class.
*
* @return the class of the associated PDO
*/
public Class getPdoClass() {
return getClassVariables().pdoClass;
}
/**
* Returns whether this an abstract PDO.
* Abstract PDOs are real objects but cannot be persisted.
* They may be used to retrieve data for the concrete implementations, however.
*
* @return true if abstract PDO
*/
@Override
public boolean isAbstract() {
return getClassVariables().isAbstract();
}
/**
* Asserts that this is not an abstract entity class.
*/
public void assertNotAbstract() {
if (isAbstract()) {
throw new PersistenceException(this, "operation not allowed for abstract entities");
}
}
/**
* Gets the classid.
*
* @return the classid
* @throws PersistenceException if not valid
*/
public int getValidClassId() {
assertNotAbstract();
return super.getClassId();
}
/**
* Returns whether the classid needs to be inluded in the WHERE-clause.
* Applies only to leafs with SINGLE inheritance.
*
* @return true if include classid
*/
public boolean isClassIdRequiredInWhereClause() {
return false;
}
/**
* Returns whether an explicit id alias is necessary in joins.
* This applies to MULTI-table inherited PDOs only (abstracts and leafs).
*
* @return true if need explicit id alias
*/
public boolean isExplicitIdAliasRequiredInJoins() {
return false;
}
/**
* Tells whether this object is composite (i.e. has composite relations).
* The method is overridden by the PdoRelations-wurblet if at least one
* relation is flagged as "composite".
* The default implementation returns false.
*
* @return true if object is composite, false if not.
*/
@Override
public boolean isComposite() {
return false;
}
/**
* Loads all components.
*
* The method is used to transfer a copy of an object between tiers including all
* composite object relations recursively.
* Furthermore, it can be used to get a map of all components.
* By default the method throws a PersistenceException telling that it is not implemented
* if isComposite() returns true.
*
* @param onlyLoaded true if return only already loaded components (lazy composite relations)
* @return the map of all components, including this object.
*/
public IdentifiableMap extends PersistentDomainObject>> loadComponents(boolean onlyLoaded) {
if (isComposite()) {
throw new PersistenceException("method not implemented");
}
// else: this is not a composite, just return me
return new IdentifiableMap<>(this.getPdo());
}
/**
* Adds the components of this object to a map.
*
* @param components the component map
* @param onlyLoaded true if return only already loaded component (lazy composite relations)
* @return the number of components added
*/
public int addComponents(IdentifiableMap> components, boolean onlyLoaded) {
return components.add(this.getPdo()) == null ? 1 : 0;
}
/**
* Adds the components of a collection to a map.
*
* @param components the components map
* @param objects the collection of objects to add along with their components
* @param onlyLoaded true if return only already loaded components (lazy composite relations)
* @return the number of components added
*/
@SuppressWarnings("unchecked")
public int addComponents(IdentifiableMap> components, Collection extends PersistentDomainObject>> objects, boolean onlyLoaded) {
int count = 0;
for (PersistentDomainObject> object: objects) {
count += ((AbstractPersistentObject,?>) object.getPersistenceDelegate()).addComponents(components, onlyLoaded);
}
return count;
}
/**
* Deletes this object and all its components without any further processing.
* Same as {@link #deletePlain()} but with components.
*/
public void deletePlainWithComponents() {
assertNotRemote();
if (isComposite()) {
throw new PersistenceException(this, "local part of the method not implemented");
}
else {
deletePlain();
}
}
/**
* Deletes plain with components all objects of a collection.
*
* @param objects the collection of PDOs
* @return the number of objects processed, < 0 if failed (number*(-1)-1)
*/
public int deletePlainWithComponents(Collection extends PersistentDomainObject>> objects) {
int count = 0;
if (objects != null && objects.size() > 0) {
Session session = null;
long txVoucher = 0;
try {
for (PersistentDomainObject> obj: objects) {
if (obj != null) {
if (session == null) {
session = obj.getSession();
if (session == null) {
throw new PersistenceException(obj, "db is null");
}
txVoucher = session.begin("delete plain with components collection");
}
else if(obj.getSession() != session) {
// must be the same instance of Db!
throw new PersistenceException(obj, "unexpected db connection: " + obj.getSession() + ", expected: " + session);
}
((AbstractPersistentObject,?>) obj.getPersistenceDelegate()).deletePlainWithComponents();
count++;
}
}
if (session != null) {
session.commit(txVoucher);
}
}
catch (RuntimeException rex) {
if (session != null) {
session.rollback(txVoucher);
}
throw rex;
}
}
return count;
}
/**
* Inserts this object and all its components without any further processing.
* Same as {@link #insertPlain()} but with components.
*/
@SuppressWarnings("unchecked")
public void insertPlainWithComponents() {
assertNotRemote();
if (isComposite()) {
throw new PersistenceException(this, "local part of the method not implemented");
}
else {
insertPlain();
}
}
/**
* Inserts plain with components all objects of a collection.
*
* @param objects the collection of PDOs
* @return the number of objects processed
*/
public int insertPlainWithComponents(Collection extends PersistentDomainObject>> objects) {
int count = 0;
if (objects != null && objects.size() > 0) {
Session session = null;
long txVoucher = 0;
try {
for (PersistentDomainObject> obj: objects) {
if (obj != null) {
if (session == null) {
session = obj.getSession();
if (session == null) {
throw new PersistenceException(obj, "db is null");
}
txVoucher = session.begin("insert plain with components collection");
}
else if(obj.getSession() != session) {
// must be the same instance of Db!
throw new PersistenceException(obj, "unexpected db connection: " + obj.getSession() + ", expected: " + session);
}
((AbstractPersistentObject,?>) obj.getPersistenceDelegate()).insertPlainWithComponents();
count++;
}
}
if (session != null) {
session.commit(txVoucher);
}
}
catch (RuntimeException rex) {
if (session != null) {
session.rollback(txVoucher);
}
throw rex;
}
}
return count;
}
/**
* Checks whether this object (if saved) would violate any
* unique constraints.
*
* The method is usually used by the presentation layer
* to check for duplicates.
* The default implementation invokes findByUniqueDomainKey(getUniqueDomainKey())
* and throws {@link UnsupportedOperationException} if one of those methods are not implemented
* (which is the default).
*
* @return the duplicate object, null if no duplicate
*/
@Override
public T findDuplicate() {
T thisPdo = getPdo();
Object key = thisPdo.getUniqueDomainKey();
if (key != null) {
T otherPdo = thisPdo.findByUniqueDomainKey(key);
if (otherPdo != null && !otherPdo.equals(thisPdo)) {
return otherPdo;
}
}
return null;
}
/**
* Determines whether object is cacheable or not.
* The default implementation always returns true, but apps
* can use this as a filter.
* @return true if object is cacheable
*/
@Override
public boolean isCacheable() {
return true;
}
/**
* Checks whether object has been marked expired.
* Expired objects will be reloaded from the database by
* the cache when the object is retrieved again.
* @return true if object is expired (in cache)
*/
@Override
public boolean isExpired() {
return expired;
}
/**
* Sets this object's expiration flag.
* @param expired true if object is expired
*/
@Override
public void setExpired(boolean expired) {
this.expired = expired;
}
/**
* Gets the last cache access time.
* @return the last cache access time
*/
@Override
public long getCacheAccessTime() {
return cacheAccessTime;
}
/**
* Gets the cache access count.
*
* @return the access count
*/
@Override
public long getCacheAccessCount() {
return cacheAccessCount;
}
/**
* mark cache access (count and set current system-time)
*/
@Override
public void markCacheAccess() {
cacheAccessCount++;
cacheAccessTime = System.currentTimeMillis();
editedBy = 0; // tokenlock does not apply to cached objects!
}
/**
* @return true if object is cached
*/
@Override
public boolean isCached() {
return cacheAccessCount > 0;
}
@Override
public SecurityResult getSecurityResult(Permission permission) {
return SecurityFactory.getInstance().getSecurityManager().evaluate(getBaseContext(), permission, getClassId(), getId());
}
@Override
public boolean isPermissionAccepted(Permission permission) {
return getSecurityResult(permission).isAccepted();
}
/**
* Creates a new object as a copy of the current object in another
* (or the same) database context.
* The new context must be of the same class as the old one.
* Subclasses must override this method if they contain links to
* other objects (via IDs) that may have changed too (depending on
* the context).
* The default implementation just clones the object, sets the context
* and sets the original object (see {@link #getCopiedObject}).
*
* @param otherContext the database context
* @return the created object
*/
@Override
public T createCopyInContext(DomainContext otherContext) {
// first we have to clonePdo() ourself. This gives a "new" object in the same context
P apo = clone();
// set the new context
apo.setDomainContext(otherContext);
// createPdo a new PDO
T obj = Pdo.create(getPdoClass(), apo);
// set the origin of the copy (so apps may check this)
copiedObject = pdo;
// .. to be followed by ID-replacement code in subclasses (they MUST super.createCopyInContext())!!
return obj;
}
/**
* Gets the original object if this object was created by {@link #createCopyInContext}.
*
* @return the object or null if this object is not a copy
*/
@Override
public T getCopiedObject() {
return copiedObject;
}
/**
* Same as {@link #createCopyInContext} but saves the new object within a transaction.
*
* @param otherContext the other domain context
* @return the created object
*/
public T saveCopyInContext(DomainContext otherContext) {
long txVoucher = beginTx(TX_SAVE_COPY_IN_CONTEXT);
try {
// createPdo the new object
T obj = createCopyInContext(otherContext);
obj.save();
getSession().commit(txVoucher);
return obj;
}
catch (RuntimeException ex) {
getSession().rollback(txVoucher);
if (ex instanceof PersistenceException) {
((PersistenceException) ex).updateDbObject(this);
throw ex;
}
else {
throw new PersistenceException(this, ex);
}
}
}
/**
* Selects an object according to a template object.
* Useful to find corresponding objects in another context.
* The default implementation loads the same object by its ID.
* Should be overwridden to select by some other unique key.
*
* @param template the template object
* @return the object if found, else null.
*/
public T selectByTemplate(AbstractPersistentObject template) {
return select(template.getId());
}
/**
* Creates a "fast copy" of this object.
*
* The method is provided as a fast alternative to deep cloning
* via reflection or serialization.
*
* All database-attributes are copied in such a way that
* each attribute of the new object can be changed without modifying
* the state of the original object. This also applies to all related
* perstent objects along the object graph. Non-persistent objects or any other
* non-database attributes are initialized according to the object's
* constructor (i.e. will usually be initialized to false, 0 or null, respectively).
*
The method will createPdo a new object (no clonePdo!) and set all
attributes the same way as if the object has been loaded from storage.
* Notice that it differs semantically from createCopyInContext() as
* copy() will copy the identity (ID) from the source object as well and
* will retain the context.
* Thus, upon return we're talking about another instance of the "same" object
* with respect to what the database considers as "same".
*
* A copied object is always not modified and not deleted,
* as it is when loaded from the database.
*
* The default implementation throws a PersistenceException, i.e. the method
* must be overridden. See the PdoCopy wurblet for an implementation.
*
* @return the copied object
*/
public T copy() {
throw new PersistenceException(this, "not implemented (consider @wurblet PdoCopy)");
}
/**
* Adds the eager joins to the created SQL.
*
* @return the processed code
*/
public JoinedSelect getEagerJoinedSelect() {
List> eagerJoins = getEagerJoins();
return eagerJoins != null && !eagerJoins.isEmpty() ? new JoinedSelect<>(eagerJoins) : null;
}
/**
* {@inheritDoc}
*
* Overridden to implement inheritance and adjust the domain context.
*/
@Override
@SuppressWarnings("unchecked")
public P readFromResultSetWrapper(ResultSetWrapper rs) {
P po = super.readFromResultSetWrapper(rs);
if (po.isAbstract()) {
// create the concrete persistence implementation according to the class id
int classId = po.getClassId();
if (classId == 0) {
throw new PersistenceException(po, "class id is 0 in " + po.toGenericString());
}
String className = PdoUtilities.getInstance().getPdoClassName(classId);
if (className == null) {
throw new PersistenceException(po, "no such PDO class for classId " + classId + " in " + po.toGenericString());
}
po = (P) Pdo.create(className, getDomainContext()).getPersistenceDelegate();
po = po.readFromResultSetWrapper(rs); // invoke the concrete implementation
}
if (po.getContextId() >= 0 || po.getDomainContext() == null) {
// pdo depends on a valid context id or no context at all
long domainContextId = po.getDomainContext() == null ? -1 : po.getDomainContext().getContextId();
if (domainContextId != po.getContextId()) {
// create valid context if context-IDs don't match or there is no domaincontext at all
po.setDomainContext(po.createValidContext());
}
}
return po;
}
/**
* Executes the query for a prepared statement and adds the results to a list.
*
* @param st the query statement
* @param js the joined select configuration, null if no joins
* @param list the list
*/
public void executeQueryToList(PreparedStatementWrapper st, JoinedSelect js, List list) {
assertRootContextIsAccepted();
try (ResultSetWrapper rs = st.executeQuery()) {
executeQueryToList(rs, js, list);
}
}
/**
* Executes the query for a prepared statement and adds the results to a list.
*
* @param rs the result set wrapper
* @param js the joined select configuration, null if no joins
* @param list the list
*/
public void executeQueryToList(ResultSetWrapper rs, JoinedSelect js, List list) {
if (js == null) {
while (rs.next()) {
P nextPo = newInstance();
nextPo = nextPo.readFromResultSetWrapper(rs);
T nextPdo = Pdo.create(nextPo.getPdoClass(), nextPo);
nextPdo = derivePdoFromPo(nextPdo, nextPo);
if (nextPdo != null) {
list.add(nextPdo);
}
}
}
else {
js.initialize(list);
while (rs.next()) {
readJoinedRow(rs, js);
}
}
}
/**
* Executes the query for a prepared statement and returns the results in a list.
*
* @param st the query statement
* @param js the joined select configuration, null if no joins
* @return the list
*/
public List executeListQuery(PreparedStatementWrapper st, JoinedSelect js) {
List list = new ArrayList<>();
executeQueryToList(st, js, list);
return list;
}
/**
* Executes the query for a prepared statement and returns the results in a list.
*
* @param st the query statement
* @return the list
*/
public List executeListQuery(PreparedStatementWrapper st) {
return executeListQuery(st, null);
}
/**
* Executes the query for a prepared statement and returns the results in a tracked list.
*
* @param st the query statement
* @param js the joined select configuration, null if no joins
* @return the tracked list
*/
public TrackedList executeTrackedListQuery(PreparedStatementWrapper st, JoinedSelect js) {
TrackedList list = new TrackedArrayList<>();
executeQueryToList(st, js, list);
list.setModified(false);
return list;
}
/**
* Executes the query for a prepared statement and returns the results in a tracked list.
*
* @param st the query statement
*
* @return the tracked list
*/
public TrackedList executeTrackedListQuery(PreparedStatementWrapper st) {
return executeTrackedListQuery(st, null);
}
/**
* Returns the eager joins for this PDO.
*
* @return the list of eager joins, null if none
*/
public List> getEagerJoins() {
List> eagerJoins = null;
PersistentObjectClassVariables super T, ? super P> cv = getClassVariables();
while (cv != null) {
if (cv.eagerJoins != null && !cv.eagerJoins.isEmpty()) {
if (eagerJoins == null) {
eagerJoins = new ArrayList<>();
}
eagerJoins.addAll(cv.eagerJoins);
}
cv = cv.superClassVariables;
}
return eagerJoins;
}
/**
* Reads the next row from a result set within a joined select.
*
* @param js the joined select
* @param rs the result set
*/
@SuppressWarnings("unchecked")
public void readJoinedRow(ResultSetWrapper rs, JoinedSelect js) {
rs.startSkip();
P nextPo = newInstance();
nextPo = nextPo.readFromResultSetWrapper(rs);
if (nextPo != null) {
T nextPdo;
if (js.currentPdo() == null || js.currentPdo().getPersistenceDelegate().getId() != nextPo.getId()) {
// create a new PDO
nextPdo = Pdo.create(nextPo.getPdoClass(), nextPo);
nextPdo = derivePdoFromPo(nextPdo, nextPo);
T knownPdo = js.nextPdo(nextPdo);
if (knownPdo != nextPdo) {
// already in list
nextPdo = knownPdo;
nextPo = (P) nextPdo.getPersistenceDelegate();
}
}
else {
// stay within same PDO
nextPdo = js.currentPdo();
}
// skip to first join
rs.skip(nextPo.getColumnCount());
for (Join super T,?> join: js.getJoins()) {
PersistentDomainObject joinedPdo = join.createJoinedPdo(nextPdo);
AbstractPersistentObject joinedPo = (AbstractPersistentObject) joinedPdo.getPersistenceDelegate();
if (rs.getObject(rs.findColumn(CN_ID)) != null) {
// only if not all columns null.
// This is the case if ID is null (which is not nullable)
// -> this one is valid:
joinedPo = joinedPo.readFromResultSetWrapper(rs);
if (joinedPo != null &&
(join.getLastJoinedPdo() == null ||
join.getLastJoinedPdo().getPersistenceDelegate().getId() != joinedPo.getId())) {
joinedPdo = joinedPo.derivePdoFromPo(joinedPdo, joinedPo);
((Join) join).join(nextPdo, joinedPdo);
}
}
// skip to next join
int columnCount = joinedPo != null ? joinedPo.getColumnCount() : 0;
if (join.isExplicitIdAliasRequired()) {
columnCount++;
}
rs.skip(columnCount);
}
}
}
/**
* Executes a query for a prepared statement and returns the first PDO.
*
* @param st the query statement
* @param js the joined select configuration, null if no joins
* @return the PDO
*/
public T executeFirstPdoQuery(PreparedStatementWrapper st, JoinedSelect js) {
assertRootContextIsAccepted();
T pDo = null;
try (ResultSetWrapper rs = st.executeQuery()) {
if (js == null) {
if (rs.next()) {
P po = readFromResultSetWrapper(rs);
pDo = derivePdoFromPo(pdo, po);
}
}
else {
// joined: until end of resultset or first PDO completed
js.initialize(null);
while (pDo == null && rs.next()) {
readJoinedRow(rs, js);
pDo = js.getLastPdo(); // != null if js.currentPdo() already contains second PDO
}
if (pDo == null) { // readJoinedRow() returned false -> eof
pDo = js.currentPdo();
}
}
return pDo;
}
}
/**
* Executes a query for a prepared statement and returns the first PDO.
*
* @param st the query statement
* @return the PDO
*/
public T executeFirstPdoQuery(PreparedStatementWrapper st) {
return executeFirstPdoQuery(st, null);
}
/**
* Loads a PDO from the database by its unique ID.
*
* @param id is the object id
* @param locked true if select FOR UPDATE
*
* @return object if loaded, null if no such object
*/
public T select(long id, boolean locked) {
T obj = null;
if (id > 0) {
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
obj = getRemoteDelegate().select(context, id, locked);
configureRemoteObject(context, obj);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
JoinedSelect js = getEagerJoinedSelect();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().selectObjectStatementId,
() -> {
StringBuilder sql = new StringBuilder(createSelectSql(locked));
if (js != null) {
js.createJoinedSql(pdo, sql);
}
return sql.toString();
}
);
int ndx = 1;
if (js != null) {
ndx = js.setClassIdsInStatement(st, ndx);
}
st.setLong(ndx, id);
obj = executeFirstPdoQuery(st, js);
}
}
return obj;
}
/**
* Loads a PDO from the database by its unique ID.
*
* @param id is the object id
*
* @return object if loaded, null if no such object
*/
@Override
public T select(long id) {
return select(id, false);
}
/**
* Reloads the object.
* Note: to make sure that any lazy inits are cleared,
* the returned object is always a new object.
*
* @return the object if reloaded, else null (never this)
*/
@Override
public T reload() {
return Pdo.create(getPdo()).select(getId());
}
/**
* Reloads the object with a write lock.
* Note: to make sure that any lazy inits are cleared,
* the returned object is always a new object.
*
* @return the object if reloaded, else null (never this)
*/
@Override
public T reloadLocked() {
return Pdo.create(getPdo()).selectLocked(getId());
}
/**
* Loads and write-locks a PDO from the database by its unique ID.
*
* @param id is the object id
*
* @return object if loaded, null if no such object
*/
@Override
public T selectLocked(long id) {
return select(id, true);
}
/**
* Derive the concrete PDO from a given PO.
*
* @param pdo the original persistent domain object
* @param po the effective persistent object from readFromResultSetWrapper
* @return the persistent domain object, null if no read permission
*/
public T derivePdoFromPo(T pdo, P po) {
T derivedPdo;
if (po == pdo.getPersistenceDelegate()) {
derivedPdo = pdo; // same type
}
else {
if (pdo.isAbstract()) {
// some subtype
derivedPdo = po.getPdo();
}
else {
throw new PersistenceException(this, "misconfigured PDO inheritance");
}
}
DomainContext context = getDomainContext();
if (po.isRootEntity()) {
// set the root-context for sure
po.setSessionHolder(context.getRootContext(derivedPdo)); // no setDomainContext!
// check always because context wasn't checked by assertRootContextIsAccepted
// but simply created along with the PDO!
if (!po.isReadAllowed()) {
return null;
}
}
else {
// optional security checks for components (the root context is already checked by assertRootContextIsAccepted)
if (po.getRootId() != 0 && po.getRootId() != context.getRootId() ||
po.getRootClassId() != 0 && po.getRootClassId() != context.getRootClassId()) {
// fast check failed!
if (po.getRootId() != 0 && po.getRootClassId() != 0) {
// po provides both the rootClassId and the rootId -> check that explicitly
// (useful for deep links)
SecurityResult sr = SecurityFactory.getInstance().getSecurityManager().evaluate(
context, SecurityFactory.getInstance().getReadPermission(), po.getRootClassId(), po.getRootId());
if (sr.isAccepted()) {
return derivedPdo;
}
// no permission for root-entity -> component does not exist as the root does for this user
return null;
}
throw new SecurityException(po, "unexpected root context " +
context.getRootClassId() + "[" + context.getRootId() + "], expected " +
po.getRootClassId() + "[" + po.getRootId() + "]");
}
}
return derivedPdo;
}
/**
* Checks the root context agains the security rules.
*/
public void assertRootContextIsAccepted() {
DomainContext context = getDomainContext();
if (context.isRootContext()) {
SecurityResult result = SecurityFactory.getInstance().getSecurityManager().
evaluate(context, SecurityFactory.getInstance().getReadPermission(),
context.getRootClassId(), context.getRootId());
if (!result.isAccepted()) {
throw new SecurityException(this, result.explain(
"no read permission for root-entity " + context.getRootClassId() + "[" + context.getRootId() + "]"));
}
}
// non-root contexts are always accepted.
// however: they must select root-entities!
// any attempt to select non-roots will fail in deriveFromPdo above!
}
@Override
public StringBuilder createSelectAllInnerSql() {
StringBuilder sql = new StringBuilder();
String alias = getTableAlias();
sql.append(alias).append('.').append(SQL_ALLSTAR);
sql.append(SQL_FROM);
sql.append(getTableName()).append(getBackend().sqlAsBeforeTableAlias()).append(alias);
sql.append(SQL_WHEREALL);
return sql;
}
@Override
public StringBuilder createSelectAllIdSerialInnerSql() {
StringBuilder sql = new StringBuilder();
String alias = getTopSuperTableAlias();
sql.append(alias).append('.').append(CN_ID);
sql.append(SQL_COMMA);
sql.append(alias).append('.').append(CN_SERIAL);
sql.append(SQL_FROM);
sql.append(getTableName()).append(getBackend().sqlAsBeforeTableAlias()).append(alias);
sql.append(SQL_WHEREALL);
return sql;
}
@Override
public StringBuilder createSelectAllByIdInnerSql() {
StringBuilder sql = createSelectAllInnerSql();
sql.append(SQL_AND);
sql.append(getTopSuperTableAlias()).append('.').append(CN_ID);
sql.append(SQL_EQUAL_PAR);
return sql;
}
/**
* Creates the inner sql text to select the ID field.
*
* Returns something like:
*
* "id FROM xytable WHERE 1=1"
*
*
* @param classVariables the classvariables
* @return the sql text
*/
public StringBuilder createSelectIdInnerSql(PersistentObjectClassVariables super T, ? super P> classVariables) {
StringBuilder sql = new StringBuilder();
String tName = classVariables.getTableName();
String tAlias = classVariables.getTableAlias();
sql.append(tAlias).append('.').append(CN_ID);
sql.append(SQL_FROM);
sql.append(tName).append(getBackend().sqlAsBeforeTableAlias()).append(tAlias);
sql.append(SQL_WHEREALL);
return sql;
}
@Override
public StringBuilder createSelectIdInnerSql() {
return createSelectIdInnerSql(getClassVariables());
}
/**
* Creates the SQL code for select by normtext.
*
* @return the sql code
*/
public String createSelectByNormTextSql() {
StringBuilder sql = new StringBuilder(SQL_SELECT);
sql.append(createSelectAllInnerSql());
String condition = getSqlClassIdCondition();
if (condition != null) {
sql.append(condition);
}
condition = getSqlContextCondition();
if (condition != null) {
sql.append(condition);
}
sql.append(SQL_AND);
sql.append(getTopSuperTableAlias()).append('.').append(CN_NORMTEXT);
sql.append(SQL_LIKE_PAR);
String orderSuffix = orderBy();
if (orderSuffix != null) {
sql.append(SQL_ORDERBY);
sql.append(orderSuffix);
}
return sql.toString();
}
/**
* Creates the SQL code for the selectSerial statement.
*
* @return the sql code
*/
@Override
public String createSelectSerialSql() {
return SQL_SELECT + CN_SERIAL + SQL_FROM + getTopSuperTableName() + SQL_WHERE + CN_ID + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the selectMaxId statement.
*
* @return the sql code
*/
@Override
public String createSelectMaxIdSql() {
return SQL_SELECT + getBackend().sqlFunction(Backend.SQL_MAX, CN_ID) + SQL_FROM + getTopSuperTableName();
}
/**
* Creates the SQL code for the selectMaxTableSerial statement.
*
* @return the sql code
*/
@Override
public String createSelectMaxTableSerialSql() {
return SQL_SELECT + getBackend().sqlFunction(Backend.SQL_MAX, CN_TABLESERIAL) + SQL_FROM + getTopSuperTableName();
}
/**
* Creates the SQL code for the dummy update statement.
* Useful get an exclusive lock within a transaction.
*
* @return the sql code
*/
@Override
public String createDummyUpdateSql() {
return SQL_UPDATE + getTopSuperTableName() + SQL_SET + CN_ID + SQL_EQUAL + CN_ID + SQL_WHERE + CN_ID + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the serial update statement.
*
* @return the sql code
*/
@Override
public String createUpdateSerialSql() {
return SQL_UPDATE + getTopSuperTableName() + SQL_SET + CN_SERIAL + SQL_EQUAL + CN_SERIAL + SQL_PLUS_ONE + SQL_WHERE +
CN_ID + SQL_EQUAL_PAR + SQL_AND + CN_SERIAL + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the serial + tableSerial update statement.
*
* @return the sql code
*/
@Override
public String createUpdateSerialAndTableSerialSql() {
return SQL_UPDATE + getTopSuperTableName() + SQL_SET + CN_SERIAL + SQL_EQUAL + CN_SERIAL + Backend.SQL_PLUS_ONE + Backend.SQL_COMMA +
CN_TABLESERIAL + SQL_EQUAL_PAR + SQL_WHERE +
CN_ID + SQL_EQUAL_PAR + SQL_AND + CN_SERIAL + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the first statement to select expired table serials.
*
* @return the sql code
*/
@Override
public String createSelectExpiredTableSerials1Sql() {
// sort by tableserial+id to return pairs in deterministic order for same tableserials
return SQL_SELECT + CN_ID + SQL_COMMA + CN_TABLESERIAL + SQL_FROM + getTopSuperTableName() + SQL_WHERE +
CN_TABLESERIAL + SQL_GREATER_PAR + SQL_ORDERBY + CN_TABLESERIAL + SQL_COMMA + CN_ID;
}
/**
* Creates the SQL code for the second statement to select expired table serials.
*
* @return the sql code
*/
@Override
public String createSelectExpiredTableSerials2Sql() {
// sort by tableserial+id to return pairs in deterministic order for same tableserials
return SQL_SELECT + CN_ID + SQL_COMMA + CN_TABLESERIAL + SQL_FROM + getTopSuperTableName() + SQL_WHERE +
CN_TABLESERIAL + SQL_GREATER_PAR + SQL_AND + CN_TABLESERIAL + Backend.SQL_LESSOREQUAL_PAR +
SQL_ORDERBY + CN_TABLESERIAL + SQL_COMMA + CN_ID;
}
/**
* Gets the result set for the normtext query (where normtext like ?)
* for use in lists.
* If the given normtext is null or just "%" the method falls back
* to {@link #resultAll}.
*
* @param normText the normtext to search for
* @param js the optional join config, null if no joins
* @return the result set
* @see #resultByNormTextCursor
*/
public ResultSetWrapper resultByNormText(String normText, JoinedSelect js) {
if (normText == null || "%".equals(normText)) {
return resultAll(js);
}
assertRootContextIsAccepted();
assertNormTextProvided();
getSession().assertNotRemote();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().selectByNormTextStatementId,
() -> {
StringBuilder sql = new StringBuilder(createSelectByNormTextSql());
if (js != null) {
js.createJoinedSql(pdo, sql);
}
return sql.toString();
}
);
int ndx = 1;
if (js != null) {
ndx = js.setClassIdsInStatement(st, ndx);
}
if (isClassIdRequiredInWhereClause()) {
st.setInt(ndx++, getClassId());
}
long contextId = getContextId();
if (contextId >= 0) {
st.setLong(ndx++, contextId);
}
st.setString(ndx, normText);
return st.executeQuery();
}
/**
* Gets the result set for the normtext query (where normtext like ?)
* for use in cursors.
* Cursors differ from lists because they need {@link ResultSet#TYPE_SCROLL_INSENSITIVE}
* set.
* If the given normtext is null or just "%" the method falls back
* to {@link #resultAll}.
*
* @param normText the normtext to search for
* @param js the optional join config, null if no joins
* @return the result set
* @see #resultByNormText
*/
public ResultSetWrapper resultByNormTextCursor(String normText, JoinedSelect js) {
if (normText == null || "%".equals(normText)) { // NOI18N
return resultAllCursor(js);
}
assertRootContextIsAccepted();
assertNormTextProvided();
getSession().assertNotRemote();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().selectByNormTextCursorStatementId,
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY,
() -> {
StringBuilder sql = new StringBuilder(createSelectByNormTextSql());
if (js != null) {
js.createJoinedSql(pdo, sql);
sql.append(SQL_ORDERBY).append(getColumnName(CN_ID));
}
return sql.toString();
}
);
int ndx = 1;
if (js != null) {
ndx = js.setClassIdsInStatement(st, ndx);
}
if (isClassIdRequiredInWhereClause()) {
st.setInt(ndx++, getClassId());
}
long contextId = getContextId();
if (contextId >= 0) {
st.setLong(ndx++, contextId);
}
st.setString(ndx, normText);
return st.executeQuery();
}
/**
* Selects all objects with a given normtext as a list.
*
* @param normText the normtext
* @return the list of objects, never null
* @see #resultByNormText
*/
@Override
@SuppressWarnings("unchecked")
public List selectByNormText(String normText) {
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
List list = getRemoteDelegate().selectByNormText(context, normText);
configureRemoteObjects(context, list);
return list;
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
JoinedSelect js = getEagerJoinedSelect();
List list = new ArrayList<>();
try (ResultSetWrapper rs = resultByNormText(normText, js)) {
executeQueryToList(rs, js, list);
return list;
}
}
/**
* Selects all objects with a given normtext as a cursor.
*
* @param normText the normtext
* @return the cursor
* @see #resultByNormTextCursor
*/
@Override
public ScrollableResource selectByNormTextAsCursor(String normText) {
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
RemoteResultSetCursor remoteCursor = getRemoteDelegate().selectByNormTextAsCursor(context, normText);
return new ResultSetCursor<>(context, remoteCursor);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
JoinedSelect js = getEagerJoinedSelect();
return new ResultSetCursor<>(pdo, resultByNormTextCursor(normText, js), js);
}
/**
* Creates SQL code for select all.
* Appends context condition and order by clause if configured.
*
* @return the sql code
*/
public String createSelectAllSql() {
StringBuilder sql = new StringBuilder(SQL_SELECT);
sql.append(createSelectAllInnerSql());
String condition = getSqlClassIdCondition();
if (condition != null) {
sql.append(condition);
}
condition = getSqlContextCondition();
if (condition != null) {
sql.append(condition);
}
String orderSuffix = orderBy();
if (orderSuffix != null) {
sql.append(SQL_ORDERBY);
sql.append(orderSuffix);
}
return sql.toString();
}
/**
* Gets the result set for all objects in the current context
* for use in lists.
*
* @param js the optional join config, null if no joins
* @return the result set
* @see #resultAllCursor
*/
public ResultSetWrapper resultAll(JoinedSelect js) {
getSession().assertNotRemote();
assertRootContextIsAccepted();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().selectAllStatementId,
() -> {
StringBuilder sql = new StringBuilder(createSelectAllSql());
if (js != null) {
js.createJoinedSql(pdo, sql);
}
return sql.toString();
}
);
int ndx = 1;
if (js != null) {
ndx = js.setClassIdsInStatement(st, ndx);
}
if (isClassIdRequiredInWhereClause()) {
st.setInt(ndx++, getClassId());
}
long contextId = getContextId();
if (contextId >= 0) {
st.setLong(ndx, contextId);
}
return st.executeQuery();
}
/**
* Gets the result set for all objects in the current context
* for use in cursors.
* Cursors differ from lists because they need {@link ResultSet#TYPE_SCROLL_INSENSITIVE} set.
*
* @param js the optional join config, null if no joins
* @return the result set
* @see #resultAll
*/
public ResultSetWrapper resultAllCursor(JoinedSelect js) {
getSession().assertNotRemote();
assertRootContextIsAccepted();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().selectAllCursorStatementId,
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY,
() -> {
StringBuilder sql = new StringBuilder(createSelectAllSql());
if (js != null) {
js.createJoinedSql(pdo, sql);
sql.append(SQL_ORDERBY).append(getColumnName(CN_ID));
}
return sql.toString();
}
);
int ndx = 1;
if (js != null) {
ndx = js.setClassIdsInStatement(st, ndx);
}
if (isClassIdRequiredInWhereClause()) {
st.setInt(ndx++, getClassId());
}
long contextId = getContextId();
if (contextId >= 0) {
st.setLong(ndx, contextId);
}
return st.executeQuery();
}
/**
* Selects all records in current context as a list.
*
* @return the list of objects, never null
* @see #resultAll
*/
@SuppressWarnings("unchecked")
@Override
public List selectAll() {
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
List list = getRemoteDelegate().selectAll(context);
configureRemoteObjects(context, list);
return list;
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
JoinedSelect js = getEagerJoinedSelect();
List list = new ArrayList<>();
try (ResultSetWrapper rs = resultAll(js)) {
executeQueryToList(rs, js, list);
return list;
}
}
/**
* Selects all records in current context as a cursor
*
* @return the cursor
* @see #resultAllCursor
*/
@Override
public ScrollableResource selectAllAsCursor() {
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
return new ResultSetCursor<>(context, getRemoteDelegate().selectAllAsCursor(context));
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
JoinedSelect js = getEagerJoinedSelect();
return new ResultSetCursor<>(pdo, resultAllCursor(js), js);
}
/**
* Gets the cache.
* The default implementation returns null.
* Must be overridden to enable optimization features with RMI servers.
*
* @return the cache, null if uncached
*/
@Override
public PdoCache getCache() {
return null;
}
/**
* Expires the cache according to the serial numbers.
*
* If objects of this class are cached, the
* cache must be expired on updates, etc...
* Furthermore, if there is a cache, isCountingModification() MUST return true,
* in order for countModification() to invalidate the cache for the local JVM.
* Classes with a cache must override this method!
* The implementation with the PdoCache should look like this:
*
* cache.expire(maxSerial);
*
* while "cache" has been declared by the wurblet PdoCache.
*
* @param maxSerial is the new tableSerial this object will get
* @return true if cache invalidated, false if there is no cache
* @see PdoCache
*/
public boolean expireCache(long maxSerial) {
return false;
}
/**
* Gets the object via cache.
* If there is no cache (i.e. the method is not overridden),
* the default implementation just loads from the db.
*
* @param id the uniue object ID
* @return the object, null if no such object
* @see #selectObject(long)
*/
@Override
public T selectCached(long id) {
return select(id);
}
/**
* Gets the object via cache only.
* If there is no cache (i.e. the method is not overridden),
* the default implementation just loads from the db.
*
* @param id the uniue object ID
* @return the object, null if no such object
* @see #selectObject(long)
*/
@Override
public T selectCachedOnly(long id) {
return select(id);
}
/**
* Gets all objects in context via cache.
* If there is no cache (i.e. the method is not overridden),
* the default implementation gets it from the db.
*
* @return the list, never null
* @see #selectAll()
*/
@Override
public List selectAllCached() {
return selectAll();
}
@Override
public List selectAllForCache() {
List list;
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
list = getRemoteDelegate().selectAllForCache(context);
configureRemoteObjects(context, list);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
list = selectAll();
}
return list;
}
@Override
public T selectForCache(long id) {
T obj;
if (getSession().isRemote()) {
try {
DomainContext context = getDomainContext();
obj = getRemoteDelegate().selectForCache(context, id);
configureRemoteObject(context, obj);
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
obj = select(id);
}
return obj;
}
/**
* {@inheritDoc}
*
* Overridden to expire the cache if object is using the tableserial.
*/
@Override
public long countModification () {
long tableSerial = super.countModification();
if (tableSerial >= 0) {
expireCache(tableSerial);
}
return tableSerial;
}
/**
* Adds a PDO class that is referencing this PDO clazz.
*
* @param the PDO type
* @param clazz the class to add
* @param methodName a method name, null if default
* @return true if added, false if already in map
* @see #removeReferencingClass
*/
public > boolean addReferencingClass(Class clazz, String methodName) {
return getClassVariables().addReferencingClass(getPersistenceClass(clazz), methodName);
}
/**
* Removes a PDO class that is referencing this PDO clazz.
*
* @param the PDO type
* @param clazz the class to add
* @param methodName a method name, null if default
* @return true if removed, false if not in map
* @see #removeReferencingClass
*/
public > boolean removeReferencingClass(Class clazz, String methodName) {
return getClassVariables().removeReferencingClass(getPersistenceClass(clazz), methodName);
}
/**
* Gets the persistence class from a pdo class.
*
* Throws a PersistenceException if the persistence class is not an {@link AbstractPersistentObject}.
*
* @param the PDO type
* @param clazz the pdo class
* @return the persistence class
*/
@SuppressWarnings("unchecked")
public > Class extends AbstractPersistentObject,?>> getPersistenceClass(Class clazz) {
Class> persistenceClass = Pdo.create(clazz).getPersistenceDelegate().getClass();
if (AbstractPersistentObject.class.isAssignableFrom(persistenceClass)) {
return (Class extends AbstractPersistentObject,?>>) persistenceClass;
}
throw new PersistenceException(this, "persistence class not compatible: " + persistenceClass);
}
/**
* {@inheritDoc}
*
* Overridden because remote method requires a {@link DomainContext} instead of just the db-connection.
*/
@Override
public boolean isReferenced() {
if (getSession().isRemote()) {
if (isNew()) {
// new objects are never referenced because they simply don't exist in the db!
// so we can saveImpl a roundtrip here
return false;
}
else {
try {
return getRemoteDelegate().isReferenced(getDomainContext(), getId());
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
}
else {
// local model
return super.isReferenced();
}
}
/**
* Searches for a "pattern" in this object.
* The default implementation looks for the pattern in the normtext.
*
* @param pattern the pattern to search for
* @return true if this object "contains" the pattern
*/
@Override
public boolean containsPattern (String pattern) {
return getNormText() != null && pattern != null && getNormText().contains(pattern);
}
/**
* Gets the natural ordering to be added in WHERE-clauses following "ORDER BY ".
* The wurblets will use it if --sort option set.
* Example:
*
* return CN_ID + "," + CN_PRINTED + " DESC";
*
* For a single field with sort ascending returning the fieldname is sufficient.
* The default is null, i.e. no order-by-clause will be added.
*
* @return the order by appendix string, null if no order by
*/
public String orderBy() {
return null;
}
/**
* {@inheritDoc}
*
* Overridden due to security check.
*/
@Override
public void initModification(char modType) {
super.initModification(modType);
clearTokenLock();
}
/**
* {@inheritDoc}
*
* Overridden to clear the renewTokenLock flag.
*/
@Override
public void finishModification(char modType) {
super.finishModification(modType);
setRenewTokenLockRequested(false);
}
/**
* {@inheritDoc}
*
* Ovewritten to get the token lock removed, if any.
*/
@Override
public void finishNotUpdated(char modType) {
if (getTokenLockTimeout() > 0) {
if (isRenewTokenLockRequested()) {
// renew token (keep old editedSince)
if (getEditedSince() == null) {
setEditedSince(DateHelper.now());
}
updateTokenLock(DateHelper.now(getTokenLockTimeout()), getContextUserId(), getEditedSince());
}
else {
// release token
updateTokenLock(null);
}
}
setRenewTokenLockRequested(false);
}
/**
* Gets the user id from the current context, i.e. userinfo.
*
* Note: invokes the errorhandler if some exception or userId is 0.
*
* @return the userId
*/
public long getContextUserId() {
long userId = getDomainContext().getSessionInfo().getUserId();
if (userId == 0) {
throw new PersistenceException(this, "userId is 0");
}
else {
return userId;
}
}
/**
* Clears or renews the token lock.
*
* If {@link #isRenewTokenLockRequested()} is true, the token will be renewed (prolonged
* if existing) or created (if not existing).
* The operation is only done in memory, not in persistent storage.
*/
public void clearTokenLock() {
if (isRenewTokenLockRequested() && getTokenLockTimeout() > 0) {
setEditedBy(getContextUserId());
setEditedExpiry(DateHelper.now(getTokenLockTimeout()));
if (getEditedSince() == null) {
setEditedSince(DateHelper.now());
}
}
else {
setEditedBy(0);
setEditedExpiry(null);
}
}
/**
* Sets whether to apply or renew the token lock on insert or update.
*
* By default, the token lock (if set) will be cleared upon insert or update. However, sometimes it is
* necessary to keep it, renew it respectively.
* Setting this flag to true will renew the token upon the next persistence operation. Afterwards the flag will be
* cleared.
*
* @param renewTokenLock true to renew the token (once)
*/
public void setRenewTokenLockRequested(boolean renewTokenLock) {
this.renewTokenLock = renewTokenLock;
}
/**
* Determines whether to renew the token lock on saveImpl or update.
*
* @return true to renew the token (once), false to clear token (default)
*/
public boolean isRenewTokenLockRequested() {
return renewTokenLock;
}
@Override
protected boolean isUpdateNecessary() {
/**
* if editedBy or renewTokenLock is set, the token lock must be updated
* even if the attributes were not changed.
*/
return super.isUpdateNecessary() || getEditedBy() != 0 || isRenewTokenLockRequested();
}
/**
* {@inheritDoc}
*
* Overridden to clear the token lock if remote session.
*/
@Override
public void updateObject() {
super.updateObject();
if (getSession().isRemote()) {
clearTokenLock();
}
}
/**
* {@inheritDoc}
*
* Overridden to clear the token lock if remote session.
*/
@Override
public void insertObject() {
super.insertObject();
if (getSession().isRemote()) {
clearTokenLock();
}
}
@Override
public void newId() {
super.newId();
if (!isRootEntity()) {
/**
* Is a component.
* Retrieve the root-entity and classid if configured
*/
DomainContext context = getDomainContext();
if (isRootIdProvided()) {
setRootId(context.getRootId());
}
if (isRootClassIdProvided()) {
setRootClassId(context.getRootClassId());
}
}
}
/**
* Updates the root context.
* Method is used after deserialization.
*
* @return true if root context set, false if this is not a root-entity
*/
public boolean updateRootContext() {
if (isRootEntity()) {
DomainContext context = getDomainContext();
// switch root context if not already done
context.getRootContext(pdo);
return true;
}
return false;
}
/**
* Configures the remotely retrieved object.
*
* @param context the local domain context
* @param obj the object
*/
public void configureRemoteObject(DomainContext context, T obj) {
if (obj != null) {
// do that without invocation handling (faster)
@SuppressWarnings("unchecked")
AbstractPersistentObject po = (AbstractPersistentObject) obj.getPersistenceDelegate();
if (po.isRootEntity()) {
// root entities get their own root-context
po.setSession(context.getSession());
po.setSessionHolder(po.getDomainContext().getRootContext(obj));
}
else {
// component
DomainContext poContext = po.getDomainContext();
if (poContext.getRootId() == context.getRootId() &&
poContext.getRootClassId() == context.getRootClassId()) {
// component that refers to the same root entity: use same context instance
po.setDomainContext(context);
}
// else: use a different context instance
}
// loaded from database: not modified yet and hence no validation pending
po.validated = true;
}
}
/**
* Configures the remotely retrieved objects.
*
* @param context the local domain context
* @param objects the objects to configure
*/
public void configureRemoteObjects(DomainContext context, Collection objects) {
if (objects != null) {
for (T obj: objects) {
configureRemoteObject(context, obj);
}
}
}
/**
* Asserts that the po's context is a root context.
*/
protected void assertRootContext() {
if (!getDomainContext().isRootContext()) {
/**
* This will also prevent components from being saved alone,
* i.e. not from within their root entity.
* (possible only in servers)
*/
throw new PersistenceException(this, "unexpected non-root domain context");
}
}
/**
* Marks all objects in a list to be deleted.
* This method is provided to mark components in PDOs only.
* This method must not be used from within the application!
*
* @param the pdo type
* @param pdos the objects to mark deleted
*/
protected > void markDeleted(Collection pdos) {
if (pdos != null) {
for (X pDo: pdos) {
if (pDo != null) {
AbstractPersistentObject po = (AbstractPersistentObject) pDo.getPersistenceDelegate();
if (!po.isNew()) {
po.markDeleted();
}
}
}
}
}
/**
* Marks an object to be deleted.
* This method is provided to mark components in PDOs only.
* This method must not be used from within the application!
*
* @param pdo the pdo to mark deleted
*/
protected void markDeleted(PersistentDomainObject> pdo) {
if (pdo != null) {
((AbstractPersistentObject) pdo.getPersistenceDelegate()).markDeleted();
}
}
/**
* Deletes a List of objects.
* This method is provided to mark components in PDOs only.
* This method must not be used from within the application!
*
* @param the pdo type
* @param pdos the list of object to delete
*/
protected > void delete(Collection pdos) {
if (pdos != null) {
Session session = null;
for (X pDo: pdos) {
if (pDo != null) {
AbstractPersistentObject po = (AbstractPersistentObject) pDo.getPersistenceDelegate();
if (!po.isVirgin()) {
if (session == null) {
session = po.getSession();
if (session == null) {
throw new PdoRuntimeException(pDo, "session is null");
}
}
else if (po.getSession() != session) {
// must be the same instance of Db!
throw new PdoRuntimeException(pDo, "unexpected session: " + po.getSession() + ", expected: " + session);
}
po.deleteImpl();
}
}
}
if (pdos instanceof TrackedList>) {
((TrackedList>) pdos).setModified(false);
}
}
}
/**
* Deletes a PDO.
* This method is provided to save components in PDOs only.
* This method must not be used from within the application!
*
* @param pdo the pdo to mark deleted
*/
protected void delete(PersistentDomainObject> pdo) {
if (pdo != null) {
((AbstractPersistentObject) pdo.getPersistenceDelegate()).deleteImpl();
}
}
/**
* Deletes all objects in oldList that are not in newList.
* This method is provided to save components in PDOs only.
* This method must not be used from within the application!
*
* @param the PDO type
* @param oldCollection the list of objects stored in db
* @param newCollection the new list of objects
*/
protected > void deleteMissingInCollection(Collection oldCollection,
Collection newCollection) {
if (oldCollection != null && oldCollection.size() > 0) {
Session session = null;
for (X pDo: oldCollection) {
if (pDo != null) {
AbstractPersistentObject po = (AbstractPersistentObject) pDo.getPersistenceDelegate();
if (!po.isVirgin() && (newCollection == null || !newCollection.contains(pDo))) {
if (session == null) {
session = po.getSession();
if (session == null) {
throw new PersistenceException(pDo, "session is null");
}
}
else if(po.getSession() != session) {
// must be the same instance of Db!
throw new PersistenceException(pDo, "unexpected session: " + po.getSession() + ", expected: " + session);
}
po.deleteImpl();
}
}
}
}
}
/**
* Checks if the po is a root entity.
*/
public void assertRootEntity() {
if (!isRootEntity()) {
throw new PersistenceException(this, "not a root-entity");
}
}
@Override
public void delete() {
assertRootEntity();
if (!getSession().isRemote()) {
assertWritePermission(); // check only once at server side
}
deleteImpl();
}
/**
* Implementation of delete bypassing the invocation handler.
*/
protected void deleteImpl() {
if (getSession().isRemote()) {
assertPersistable();
preparePending = true;
try {
prepareDelete();
getRemoteDelegate().deleteImpl(pdo); // this will do delete() in server, i.e. all checks
setPersistable(false); // pdo cannot be used anymore
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
deleteObject();
}
}
@Override
public void save() {
assertRootEntity();
if (!getSession().isRemote()) {
assertWritePermission(); // check only once at server side
}
saveImpl();
}
/**
* Implementation of save bypassing the invocation handler.
*/
protected void saveImpl() {
if (getSession().isRemote()) {
assertPersistable();
// execute in 3-tier client
clearOnRemoteSave();
preparePending = true;
prepareSave();
try {
getRemoteDelegate().saveImpl(pdo);
setPersistable(false); // pdo cannot be used anymore
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
if (!isValidated()) {
validate();
}
super.saveObject();
}
}
/**
* Saves a list of PDOs.
* This method is provided to save components in PDOs only.
* Assumes that a transaction is already running.
* This method must not be used from within the application!
*
* @param the pdo type
* @param pdos the list to save
* @param modifiedOnly true if only modified objects are saved
*/
@SuppressWarnings("unchecked")
protected > void save(Collection pdos, boolean modifiedOnly) {
if (pdos != null) {
Session session = null;
for (X pDo: pdos) {
if (pDo != null) {
AbstractPersistentObject po = (AbstractPersistentObject) pDo.getPersistenceDelegate();
if (session == null) {
session = po.getSession();
if (session == null) {
throw new PdoRuntimeException(pDo, "session is null");
}
}
else if (po.getSession() != session) {
// must be the same instance!
throw new PdoRuntimeException(pDo, "unexpected session: " + po.getSession() + ", expected: " + session);
}
if (po.isPersistable()) {
if (!modifiedOnly || po.isModified()) {
po.saveImpl();
}
}
else {
// po not persistable anymore?
if (!po.isNew()) {
// already stored on disk: remove it!
po.deleteImpl();
}
}
}
}
if (pdos instanceof TrackedList>) {
((TrackedList>) pdos).setModified(false);
}
}
}
/**
* Saves a PDO.
* This method is provided to save components in PDOs only.
* This method must not be used from within the application!
*
* @param pdo the pdo to save
*/
protected void save(PersistentDomainObject> pdo) {
if (pdo != null) {
((AbstractPersistentObject) pdo.getPersistenceDelegate()).saveImpl();
}
}
@Override
public T persist() {
assertRootEntity();
if (!getSession().isRemote()) {
assertWritePermission(); // check only once at server side
}
return persistImpl();
}
@Override
public T persistTokenLocked() {
setRenewTokenLockRequested(true);
return persist();
}
/**
* Implementation of persist bypassing the invocation handler.
*
* @return the persisted PDO
*/
protected T persistImpl() {
if (getSession().isRemote()) {
assertPersistable();
// execute in 3-tier client
clearOnRemoteSave();
preparePending = true;
prepareSave();
/**
* We cannot invoke super.persistObject because we cannot
* change the reference to the persistent object in the PDO proxy object.
* Hence, we must transfer the whole PDO proxy and update the context/session on return.
*/
try {
T persistedPdo = getRemoteDelegate().persistImpl(pdo);
configureRemoteObject(getDomainContext(), persistedPdo);
return persistedPdo;
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
if (!isValidated()) {
validate();
}
if (super.persistObject() != this) {
throw new PersistenceException(this, "local persist does not return same object");
}
return pdo;
}
}
/**
* Determines whether in updates of composite objects unmodified objects in the
* update path get at least the serial updated or are not touched at all.
* The default is to leave unmodified objects untouched, except root entities.
* However, in some applications it is necessary to update the master object if some of its childs are updated (usually
* to trigger something, e.g. a cache-update).
* The default implementation returns true if this is a root entity.
*
* @return true if update serial even if object is unchanged
* @see #updateObject
*/
@Override
public boolean isUpdatingSerialEvenIfNotModified() {
return isRootEntity();
}
/**
* Checks whether some of the objects in the list are modified.
* This method is provided to save components in PDOs only.
* Assumes that a transaction is already running.
* This method must not be used from within the application!
*
* @param the PDO type
* @param pdos the objects
* @return true if modified
*/
protected > boolean isModified(Collection pdos) {
// TrackedArrayLists are modified if elements added, replaced or removed
if (pdos instanceof TrackedList> &&
((TrackedList>) pdos).isModified()) {
return true;
}
// check attributes
if (pdos != null) {
for (X pDo: pdos) {
if (pDo != null) {
AbstractPersistentObject po = (AbstractPersistentObject) pDo.getPersistenceDelegate();
if (po.isModified()) {
return true;
}
}
}
}
return false;
}
/**
* By default objects don't need to include the editedBy/Since/Expiry columns in the
* database table.
* Override this method if object contains such columns, i.e. global model option [TOKENLOCK] set.
*
* @return true if object is using the edited lock columns, false if not.
*/
@Override
public boolean isTokenLockProvided() {
return false;
}
/**
* Asserts that entity provides a normtext.
*/
protected void assertTokenLockProvided() {
if (!isTokenLockProvided()) {
throw new PersistenceException(this, "entity does not provide the edited-token columns");
}
}
/**
* Gets the expiration in milliseconds of the "being-edited-token" if
* this object should request such a token when being edited.
*
* @return the timespan in ms, 0 = no token required.
*/
@Override
public long getTokenLockTimeout() {
// if token lock columns are provided the default timeout is 5 minutes
return isTokenLockProvided() ? Constants.MINUTE_MS * 5 : 0;
}
/**
* Creates the SQL code for the {@link #updateTokenLock} statement.
*
* @return the sql code
*/
public String createUpdateTokenLockSql() {
assertTokenLockProvided();
return SQL_UPDATE + getTopSuperTableName() + SQL_SET +
CN_EDITEDBY + SQL_EQUAL_PAR_COMMA +
CN_EDITEDSINCE + SQL_EQUAL_PAR_COMMA +
CN_EDITEDEXPIRY + SQL_EQUAL_PAR + SQL_WHERE +
CN_ID + SQL_EQUAL_PAR + SQL_AND + SQL_LEFT_PARENTHESIS +
CN_EDITEDBY + SQL_EQUAL_PAR + SQL_OR + // the current user holds the token
CN_EDITEDBY + SQL_EQUAL_ZERO + SQL_OR + // no one holding the token
CN_EDITEDEXPIRY + SQL_LESS_PAR + SQL_OR + // token expired
CN_EDITEDEXPIRY + SQL_ISNULL + SQL_RIGHT_PARENTHESIS; // or not set at all (pathologic case)
}
/**
* Creates the SQL code for the {@link #updateTokenLock} statement with mod counting.
*
* @return the sql code
*/
public String createUpdateTokenLockWithCountSql() {
assertTokenLockProvided();
return SQL_UPDATE + getTopSuperTableName() + SQL_SET +
CN_SERIAL + SQL_EQUAL + CN_SERIAL + SQL_PLUS_ONE + SQL_COMMA +
CN_TABLESERIAL + SQL_EQUAL_PAR_COMMA +
CN_EDITEDBY + SQL_EQUAL_PAR_COMMA +
CN_EDITEDSINCE + SQL_EQUAL_PAR_COMMA +
CN_EDITEDEXPIRY + SQL_EQUAL_PAR + SQL_WHERE +
CN_ID + SQL_EQUAL_PAR + SQL_AND + SQL_LEFT_PARENTHESIS +
CN_EDITEDBY + SQL_EQUAL_PAR + SQL_OR + // the current user holds the token
CN_EDITEDBY + SQL_EQUAL_ZERO + SQL_OR + // no one holding the token
CN_EDITEDEXPIRY + SQL_LESS_PAR + SQL_OR + // token expired
CN_EDITEDEXPIRY + SQL_ISNULL + SQL_RIGHT_PARENTHESIS; // or not set at all (pathologic case)
}
/**
* Creates the SQL code for the {@link #updateTokenLockOnly} statement.
*
* @return the sql code
*/
public String createUpdateTokenLockOnlySql() {
assertTokenLockProvided();
return SQL_UPDATE + getTopSuperTableName() + SQL_SET +
CN_EDITEDBY + SQL_EQUAL_PAR_COMMA +
CN_EDITEDSINCE + SQL_EQUAL_PAR_COMMA +
CN_EDITEDEXPIRY + SQL_EQUAL_PAR + SQL_WHERE +
CN_ID + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the select in {@link #updateTokenLock} statement.
*
* @return the sql code
*/
public String createSelectTokenLockSql() {
assertTokenLockProvided();
return SQL_SELECT + CN_EDITEDBY + SQL_COMMA + CN_EDITEDSINCE + SQL_COMMA + CN_EDITEDEXPIRY +
SQL_FROM + getTopSuperTableName() + SQL_WHERE + CN_ID + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the {@link #transferTokenLock} statement without tableserial.
*
* @return the sql code
*/
public String createTransferTokenLockSql() {
assertTokenLockProvided();
return SQL_UPDATE + getTopSuperTableName() + SQL_SET +
CN_SERIAL + SQL_EQUAL + CN_SERIAL + SQL_PLUS_ONE + SQL_COMMA +
CN_EDITEDBY + SQL_EQUAL_PAR +
SQL_WHERE + CN_ID + SQL_EQUAL_PAR + SQL_AND + CN_SERIAL + SQL_EQUAL_PAR;
}
/**
* Creates the SQL code for the {@link #transferTokenLock} statement with tableserial.
*
* @return the sql code
*/
public String createTransferTokenLockWithTableSerialSql() {
assertTokenLockProvided();
return SQL_UPDATE + getTopSuperTableName() + SQL_SET +
CN_SERIAL + SQL_EQUAL + CN_SERIAL + SQL_PLUS_ONE + SQL_COMMA +
CN_EDITEDBY + SQL_EQUAL_PAR_COMMA +
CN_TABLESERIAL + SQL_EQUAL_PAR +
SQL_WHERE + CN_ID + SQL_EQUAL_PAR + SQL_AND + CN_SERIAL + SQL_EQUAL_PAR;
}
/**
* Gets the id of the user currently editing this object.
*
* @return the id or 0 if not being edited currently.
*/
@Override
public long getEditedBy() {
return editedBy;
}
/**
* Sets the user editing this object.
* Does *NOT* alter isModified() and does not require the object to be mutable.
*
* @param editedBy the id of the user, 0 to clear.
*
*/
@Override
public void setEditedBy(long editedBy) {
this.editedBy = editedBy;
}
/**
* Requests an edited by lock.
*/
@Override
public void requestTokenLock() {
updateTokenLock(DateHelper.now(getTokenLockTimeout()));
}
/**
* Releases an edited by lock.
* Use this method if a PDO needs to be unlocked without being persisted.
*/
@Override
public void releaseTokenLock() {
updateTokenLock(null);
}
/**
* Checks whether this object is token locked (editedBy != 0) and
* the lock is not expired.
*
* @return true if locked by any user
*/
@Override
public boolean isTokenLocked() {
return getEditedBy() != 0 && getEditedExpiry() != null && getEditedExpiry().getTime() > System.currentTimeMillis();
}
/**
* Checks whether this object is edited locked by given user.
*
* @param userId the user's ID
* @return true locked
*/
@Override
public boolean isTokenLockedBy(long userId) {
return isTokenLocked() && getEditedBy() == userId;
}
/**
* Checks whether this object is edited locked by the current user.
*
* @return true if locked
*/
@Override
public boolean isTokenLockedByMe() {
return isTokenLockedBy(getDomainContext().getSessionInfo().getUserId());
}
/**
* Gets the time since when this object is being edited.
*
* @return the time, null if not being edited.
*/
@Override
public Timestamp getEditedSince() {
return editedSince;
}
/**
* Sets the time since when this object is being edited.
* Does *NOT* alter isModified() and does not require the object to be mutable.
*
* @param editedSince the time, null to clear.
*/
@Override
public void setEditedSince(Timestamp editedSince) {
this.editedSince = editedSince;
}
/**
* Gets the time since when this object is being edited.
*
* @return the time, null if not being edited.
*/
@Override
public Timestamp getEditedExpiry() {
return editedExpiry;
}
/**
* Sets the time when the token should expire.
* Does *NOT* alter isModified() and does not require the object to be mutable.
*
* @param editedExpiry the expiration time, null to clear.
*/
@Override
public void setEditedExpiry(Timestamp editedExpiry) {
this.editedExpiry = editedExpiry;
}
/**
* Gets the object associated to the id of the editedBy-attribute.
* This is usually the id of a persistent Object implementing the concept
* of a user, group, role or whatever. The default implementation
* invokes {@link AbstractApplication#getUser}.
*
* @return the user object, null if no user or id was 0
*/
@Override
public > U getTokenLockObject() {
AbstractApplication application = AbstractApplication.getRunningApplication();
return application == null ? null : application.getUser(getDomainContext(), getEditedBy());
}
/**
* Sets the user/group/role-object editing this object.
*
* @param obj the object, null to clear.
*/
@Override
public > void setTokenLockObject(U obj) {
setEditedBy(obj == null ? 0 : obj.getId());
}
public boolean isCountingModificationForTokenLock() {
return false;
}
/**
* Updates token lock columns in db-record.
* Will *NOT* log modification and *NOT* update the serial-counter!
* Must be called from within application where appropriate.
*
* 2 cases:
*
* If expiry == 0, the "token" is released.
* True is returned if operation was successful and editedBy will hold 0 while editedSince
* holds the current (release) time. False is returned if token could not
* be released and nothing is changed. This usually is the case when another
* user holds the token (and indicates some logic errors in the application, btw.)
* EditedBy and editedSince are updated in the object to reflect the current values
* in the database.
* Notice: releasing an already released token is not considered to be an error.
* This will simply update the release timestamp.
*
* If expiry > 0, a new token for the current user is requested.
* True is returned if the token could be exclusively acquired. EditedBy and
* editedSince are updated accordingly.
* If false: the object is in use by another user and editedSince and editedId
* holds this user and his timestamp.
* Notice: requesting an already requested token will simply renew the token.
*
* The method does not check getTokenLockTimeout()! This is due to the application.
* Applications should use updateTokenLock(tokenExpiry). This is an internal implementation only.
*
* @param tokenExpiry holds the time the token will expire. Null to release token.
* @param userId is the current user (unused if tokenExpiry is null)
* @param curTime is the current system time
*/
public void updateTokenLock(Timestamp tokenExpiry, long userId, Timestamp curTime) {
if (getSession().isRemote()) {
try {
TokenLock token = getRemoteDelegate().updateTokenLock(getId(), tokenExpiry, userId, curTime);
token.applyTo(this);
}
catch (RemoteException e) {
RuntimeException rex = PersistenceException.createFromRemoteException(this, e);
if (rex instanceof LockException) {
LockException lx = (LockException) rex;
TokenLock token = lx.getTokenLock();
if (token != null) {
token.applyTo(this);
}
}
throw rex;
}
}
else {
assertNotCached();
assertPersistable();
assertRootEntity();
assertWritePermission();
if (getSession().isPersistenceOperationAllowed(this, ModificationLog.UPDATE)) {
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().updateTokenLockStatementId,
this::createUpdateTokenLockSql);
long newUser = tokenExpiry != null ? userId : 0;
st.setLong(1, newUser);
st.setTimestamp(2, curTime);
st.setTimestamp(3, tokenExpiry);
st.setLong(4, getId());
st.setLong(5, userId);
st.setTimestamp(6, curTime);
if (st.executeUpdate() == 1) {
// update was successful
setEditedBy(newUser);
setEditedSince(curTime);
setEditedExpiry(tokenExpiry);
}
else {
// no success: another user is currently holding the token
st = getPreparedStatement(getClassVariables().selectTokenLockStatementId, this::createSelectTokenLockSql);
st.setLong(1, getId());
try (ResultSetWrapper rs = st.executeQuery()) {
if (rs.next()) {
setEditedBy(rs.getLong(CN_EDITEDBY));
setEditedSince(rs.getTimestamp(CN_EDITEDSINCE));
setEditedExpiry(rs.getTimestamp(CN_EDITEDEXPIRY));
throw new LockException(getSession(), new TokenLock(editedBy, editedSince, editedExpiry));
}
else {
throw new NotFoundException(this, "could not retrieve token lock");
}
}
}
}
}
}
/**
* Updates editing info in db-record (if feature enabled).
* Will *NOT* log modification and *NOT* update the serial-counter!
* Must be called from within application where appropriate. See PdoEditDialog.
*
* 2 cases:
*
* If expiry == 0, the "token" is released.
* True is returned if operation was successful and editedBy will hold 0 while editedSince
* holds the current (release) time. False is returned if token could not
* be released and nothing is changed. This usually is the case when another
* user holds the token (and indicates some logic errors in the application, btw.)
* EditedBy and editedSince are updated in the object to reflect the current values
* in the database.
* Notice: releasing an already released token is not considered to be an error.
* This will simply update the release timestamp.
*
* If expiry > 0, a new token for the current user is requested.
* True is returned if the token could be exclusively acquired. EditedBy and
* editedSince are updated accordingly.
* If false: the object is in use by another user and editedSince and editedId
* holds this user and his timestamp.
* Notice: requesting an already requested token will simply renew the token.
*
* The method does not check getTokenLockTimeout()! This is due to the application.
*
* @param tokenExpiry holds the time the token will expire. Null to release token.
*/
public void updateTokenLock(Timestamp tokenExpiry) {
updateTokenLock(tokenExpiry, getDomainContext().getSessionInfo().getUserId(), DateHelper.now());
}
/**
* Update the editedBy-attributes to persistent storage.
* No check is done whether locked or not and there is no serial
* update and no modlog. Used by daemons to cleanup.
*/
public void updateTokenLockOnly() {
if (getSession().isRemote()) {
try {
getRemoteDelegate().updateTokenLockOnly(getId(), getEditedBy(), getEditedSince(), getEditedExpiry());
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
assertNotCached();
assertPersistable();
assertRootEntity();
assertWritePermission();
if (getSession().isPersistenceOperationAllowed(this, ModificationLog.UPDATE)) {
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().updateTokenLockOnlyStatementId,
this::createUpdateTokenLockOnlySql);
st.setLong(1, getEditedBy());
st.setTimestamp(2, getEditedSince());
st.setTimestamp(3, getEditedExpiry());
st.setLong(4, getId());
assertThisRowAffected(st.executeUpdate());
}
}
}
@Override
public T transferTokenLock(long userId) {
if (getSession().isRemote()) {
try {
T tPdo = getRemoteDelegate().transferTokenLock(pdo, userId);
configureRemoteObject(getDomainContext(), tPdo);
return tPdo;
}
catch (RemoteException e) {
throw PersistenceException.createFromRemoteException(this, e);
}
}
else {
assertNotCached();
assertPersistable();
assertRootEntity();
assertWritePermission();
if (getSession().isPersistenceOperationAllowed(this, ModificationLog.UPDATE)) {
// within a TX cause of countModification to invalidateCache
long txVoucher = getSession().begin(TX_TRANSFER_TOKENLOCK);
try {
boolean withTableSerial = isTableSerialProvided();
PreparedStatementWrapper st = getPreparedStatement(getClassVariables().transferTokenLockStatementId,
() -> withTableSerial ? createTransferTokenLockWithTableSerialSql() : createTransferTokenLockSql());
long newTableSerial = countModification(); // this will rollback and terminate on error
if (withTableSerial) {
setTableSerial(newTableSerial);
}
int ndx = 0;
st.setLong(++ndx, userId);
if (withTableSerial) {
st.setLong(++ndx, getTableSerial());
}
st.setLong(++ndx, getId());
st.setLong(++ndx, getSerial());
assertThisRowAffected(st.executeUpdate());
setSerial(getSerial() + 1); // was incremented
if (userId != 0) {
// set/refresh the expiry
updateTokenLock(DateHelper.now(getTokenLockTimeout()), userId, DateHelper.now());
}
getSession().commit(txVoucher);
}
catch (RuntimeException ex) {
setPersistable(false);
getSession().rollback(txVoucher);
if (ex instanceof PersistenceException) {
((PersistenceException) ex).updateDbObject(this);
throw ex;
}
else {
throw new PersistenceException(this, ex);
}
}
}
return pdo;
}
}
/**
* Gets the optional transient data object.
* The default implementation does nothing.
*
* @return the transient data, null if none.
*/
@Override
public Object getTransientData() {
return null;
}
/**
* Sets the optional transient data object.
* Sometimes, e.g. when objects need to be reloaded from storage, all non-persistent
* data attached to the object would be lost. In such cases the framework invokes
* getTransientData() and setTransientData() to keep the non-persistent attributes.
* The default implementation does nothing.
*
* @param data the transient data
*/
@Override
public void setTransientData(Object data) {
}
/**
* {@inheritDoc}
*
* Overridden due to covariance.
*/
@Override
@SuppressWarnings("unchecked")
public AbstractPersistentObjectRemoteDelegate getRemoteDelegate() {
return (AbstractPersistentObjectRemoteDelegate) super.getRemoteDelegate();
}
/**
* {@inheritDoc}
*
* The default scopes are: {@link PersistenceScope}, {@link MandatoryScope} and {@link ChangeableScope}.
*/
@Override
@SuppressWarnings("unchecked")
public Class extends ValidationScope>[] getDefaultScopes() {
return new Class[] { PersistenceScope.class, MandatoryScope.class, ChangeableScope.class };
}
@Override
public List validate(String validationPath, ValidationScope scope) {
return ValidationUtilities.getInstance().validate(getPdo(), validationPath, scope);
}
@Override
public void validate() {
validated = false;
String validationPath = ValidationUtilities.getInstance().getDefaultValidationPath(pdo);
List results = validate(validationPath, ValidationScopeFactory.getInstance().getPersistenceScope());
if (ValidationUtilities.getInstance().hasFailed(results)) {
throw new ValidationFailedException(
"validation of " + pdo.toGenericString() + " as " + validationPath + " failed:\n"
+ ValidationUtilities.getInstance().resultsToString(results), results);
}
validated = true;
}
@Override
public boolean isValidated() {
return validated;
}
@Override
public void setModified(boolean modified) {
super.setModified(modified);
validated = !modified;
}
/**
* Determines whether the application is allowed to read this PDO.
* Makes no sense to publish in PersistentObject because PDOs without read permissions
* are not read from the database at all.
*
* @return true if allowed
*/
public boolean isReadAllowed() {
return getClassVariables().isReadAllowed(this);
}
@Override
public boolean isWriteAllowed() {
return getClassVariables().isWriteAllowed(this);
}
@Override
public boolean isViewAllowed() {
return getClassVariables().isViewAllowed(this);
}
@Override
public boolean isEditAllowed() {
return getClassVariables().isEditAllowed(this);
}
/**
* Checks write permission for this object.
*
* @throws SecurityException if no write permission
*/
protected void assertWritePermission() {
if (!isWriteAllowed()) {
throw new SecurityException(this, "no write permission");
}
}
/**
* Checks read permission for this object.
*
* @throws SecurityException if no read permission
*/
protected void assertReadPermission() {
if (!isReadAllowed()) {
throw new SecurityException(this, "no read permission");
}
}
}