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

org.tentackle.pdo.AbstractPersistentObject Maven / Gradle / Ivy

There is a newer version: 21.16.1.0
Show newest version
/**
 * 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> 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> 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> 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> 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 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 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 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> getPersistenceClass(Class clazz) { Class persistenceClass = Pdo.create(clazz).getPersistenceDelegate().getClass(); if (AbstractPersistentObject.class.isAssignableFrom(persistenceClass)) { return (Class>) 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[] 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"); } } }