org.tentackle.dbms.DbObjectClassVariables Maven / Gradle / Ivy
/*
* Tentackle - https://tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.dbms;
import org.tentackle.common.FileHelper;
import org.tentackle.dbms.rmi.AbstractDbObjectRemoteDelegate;
import org.tentackle.log.Logger;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.SessionUtilities;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
/**
* Holds static class variables for classes derived from AbstractDbObject.
* This is a "singleton per class".
*
* @param the persistence type
* @author harald
*/
public class DbObjectClassVariables
> {
private static final Logger LOGGER = Logger.get(DbObjectClassVariables.class);
// all classes register here:
private static final Map>> CLASSMAP = new HashMap<>();
/**
* Creates a classvariable for a db object.
*
* @param clazz is the class of the derived AbstractDbObject
* @param classId the unique class id, 0 if abstract
* @param tableName is the SQL tablename
* @param the persistence type
*/
public static
> DbObjectClassVariables
create(Class
clazz, int classId, String tableName) {
return DbClassVariablesFactory.getInstance().dbPoCv(clazz, classId, tableName);
}
/**
* Creates a classvariable for a db object.
* Classid and tablename are derived from mapped services in META-INF.
*
* @param clazz is the class of the derived AbstractDbObject
* @param
the persistence type
*/
public static
> DbObjectClassVariables
create(Class
clazz) {
return DbClassVariablesFactory.getInstance().dbPoCv(clazz);
}
/**
* Gets the classvariables for a given tablename.
*
* @param
the persistence type
* @param tableName is the database tablename
* @return the classvariables or null if no such tablename
*/
@SuppressWarnings("unchecked")
public static
> DbObjectClassVariables
getVariables(String tableName) {
synchronized(CLASSMAP) {
return (DbObjectClassVariables
) CLASSMAP.get(tableName);
}
}
/**
* Registers a classvariables instance.
*
* @param
the persistence type
* @param variables the classvariables to register
*/
public static
> void putVariables(DbObjectClassVariables
variables) {
if (variables.tableName != null) {
synchronized(CLASSMAP) {
@SuppressWarnings("unchecked")
DbObjectClassVariables
classVar = (DbObjectClassVariables
) CLASSMAP.get(variables.tableName);
if (classVar != null) {
/*
* Using an extended class with the same tablename is okay.
* This is because the class hierarchy will register along the extension path.
* In all other cases the tablename must be unique.
*/
if (!classVar.clazz.isAssignableFrom(variables.clazz)) {
throw new PersistenceException(
"classvariables for '" + variables.tableName + "' already registered, old=" + classVar.clazz.getName() +
", new=" + variables.clazz.getName());
}
}
else {
CLASSMAP.put(variables.tableName, variables);
}
}
}
}
protected record ForeignReference(Method[] methods, int priority, String methodPath) implements Comparable {
/**
* Sorted by priority + method.
*/
@Override
public int compareTo(ForeignReference o) {
int rv = priority - o.priority;
if (rv == 0) {
rv = methodPath.compareTo(o.methodPath);
}
return rv;
}
}
/**
* Referencing classes and their isReferencing-methods.
*/
protected final Set foreignReferences = new TreeSet<>();
/**
* the class.
*/
public final Class clazz;
/**
* The unique class id.
*/
public final int classId;
/**
* the base-classname.
*/
public final String classBaseName;
/**
* database table name.
*/
public final String tableName;
/**
* ID for the remote delegate for this class.
*/
public int remoteDelegateId;
/**
* Number of columns in a resultset.
*/
public int columnCount;
/**
* true if prepared statements should always be prepared (i.e. if
* the statement is changing for some reasons, e.g. the tablename).
*/
public boolean alwaysPrepare;
/**
* prepared statement ID for select().
*/
public final StatementId selectObjectStatementId = new StatementId();
/**
* prepared statement ID for selectAll().
*/
public final StatementId selectAllObjectsStatementId = new StatementId();
/**
* prepared statement ID for selectAllIdSerial().
*/
public final StatementId selectAllIdSerialStatementId = new StatementId();
/**
* prepared statement ID for selectForUpdate().
*/
public final StatementId selectForUpdateStatementId = new StatementId();
/**
* prepared statement ID for selectSerial().
*/
public final StatementId selectSerialStatementId = new StatementId();
/**
* prepared statement ID for selectMaxId().
*/
public final StatementId selectMaxIdStatementId = new StatementId();
/**
* prepared statement ID for selectMaxTableSerial().
*/
public final StatementId selectMaxTableSerialStatementId = new StatementId();
/**
* prepared statement ID for insert().
*/
public final StatementId insertStatementId = new StatementId();
/**
* prepared statement ID for update().
*/
public final StatementId updateStatementId = new StatementId();
/**
* prepared statement ID for delete().
*/
public final StatementId deleteStatementId = new StatementId();
/**
* prepared statement ID for dummyUpdate().
*/
public final StatementId dummyUpdateStatementId = new StatementId();
/**
* prepared statement ID for updateSerial().
*/
public final StatementId updateSerialStatementId = new StatementId();
/**
* prepared statement ID for updateSerial(long).
*/
public final StatementId updateAndSetSerialStatementId = new StatementId();
/**
* prepared statement ID for updateTableSerial().
*/
public final StatementId updateTableSerialStatementId = new StatementId();
/**
* prepared statement ID for updateSerialAndTableSerial().
*/
public final StatementId updateSerialAndTableSerialStatementId = new StatementId();
/**
* prepared statement ID for selectExpiredTableSerials().
*/
public final StatementId selectExpiredTableSerials1StatementId = new StatementId();
public final StatementId selectExpiredTableSerials2StatementId = new StatementId();
/**
* prepared statement ID for selectObjectsWithExpiredTableSerials
*/
public final StatementId selectObjectsWithExpiredTableSerialsStatementId = new StatementId();
/**
* table serial expiration backlog.
*/
public final TableSerialExpirationBacklog expirationBacklog;
/**
* the source for obtaining a new ID.
*/
public IdSource idSource;
/**
* class properties.
*/
private Properties properties;
/**
* Constructs a class variable.
* Throws IllegalStateException if already constructed.
*
* @param clazz is the class of the derived AbstractDbObject
* @param classId the unique class id, 0 if abstract
* @param tableName is the SQL tablename
*/
public DbObjectClassVariables(Class
clazz, int classId, String tableName) {
this.clazz = clazz;
this.classId = classId;
this.tableName = tableName;
classBaseName = clazz.getSimpleName();
expirationBacklog = createBacklog();
// register and check extension hierarchy
putVariables(this);
}
/**
* Constructs a class variable.
* Throws IllegalStateException if already constructed.
* Classid and tablename are derived from META-INF.
*
* @param clazz is the class of the derived AbstractDbObject
*/
public DbObjectClassVariables(Class
clazz) {
this(clazz,
SessionUtilities.getInstance().determineClassId(clazz),
SessionUtilities.getInstance().determineTablename(clazz));
}
/**
* Creates the table serial backlog.
*
* @return the backlog
*/
protected TableSerialExpirationBacklog createBacklog() {
return new TableSerialExpirationBacklog();
}
/**
* Lazily gets the properties.
* Loads and initially processes the properties.
*
* @return the properties, never null
*/
public Properties getProperties() {
if (properties == null) {
// load properties, if any from file or resource
String propertiesName = clazz.getName().replace('.', '/');
try {
properties = FileHelper.loadProperties(propertiesName);
}
catch (FileNotFoundException nf) {
// missing props is okay and default
}
catch (IOException io) {
LOGGER.severe("could not load class variable properties from " + propertiesName, io);
}
finally {
if (properties == null) {
properties = new Properties();
}
}
}
return properties;
}
/**
* Get the IdSource.
*
* If the IdSource is null, the method will look for
* a property file which is the classname + ".properties".
* The property file must contain an idsource-line
* similar to that in Db.properties.
* If there is no such file, the source from the db will be used.
*
* @param db the session
* @return the idSource
*/
public synchronized IdSource getIdSource(Db db) {
if (idSource == null) { // setup IdSource once for this class
// check property "idsource = ..."
String idConfig = getProperty(Db.IDSOURCE);
if (idConfig == null) {
// no such property: use default id source from Db
idSource = db.getDefaultIdSource();
}
else {
// create configurator and source from property
idSource = IdSourceConfigurator.getInstance().configure(db, idConfig);
}
}
return idSource;
}
/**
* Determine the referencing priority.
*
* The default implementation returns 0.
* Provided to be overridden.
*
* @param clazz the referencing class
* @return the priority
*/
protected int determineReferencePriority(Class extends AbstractDbObject>> clazz) {
// all objects get the same priority by default
return 0;
}
/**
* Adds a PDO class that is referencing the class of this class variable holder.
*
* The referencing class must provide a static method isReferencing<ClassBasename>
,
* while ClassBaseName
is the {@link #classBaseName} of the referenced class.
* The method must provide the objectId as its only parameter and is usually generated
* by the ???IsReferencing-wurblets.
* The method name can also contain dots to denote a chain of methods to invoke.
*
* @param clazz the class to add
* @param methodPath the method path name, null if default "isReferencing<ClassBaseName>"
* @return true if added, false if already in map
* @see #removeReferencingClass
*/
public boolean addReferencingClass(Class extends AbstractDbObject>> clazz, String methodPath) {
ForeignReference ref = createForeignReference(clazz, methodPath);
synchronized(foreignReferences) {
return foreignReferences.add(ref);
}
}
/**
* Removes a PDO class that is referencing the class from this class variable holder.
*
* @param clazz the class to be removed
* @param methodPath the method path name, null if default
* @return true if removed, false if not in map
* @see #addReferencingClass
*/
public boolean removeReferencingClass(Class extends AbstractDbObject>> clazz, String methodPath) {
ForeignReference ref = createForeignReference(clazz, methodPath);
synchronized(foreignReferences) {
return foreignReferences.remove(ref);
}
}
private ForeignReference createForeignReference(Class extends AbstractDbObject>> clazz, String methodPath) {
if (methodPath == null) {
methodPath = "isReferencing" + classBaseName;
}
try {
StringTokenizer stok = new StringTokenizer(methodPath, ".");
List methods = new ArrayList<>();
Class> methodClass = clazz;
while (stok.hasMoreTokens()) {
String methodName = stok.nextToken();
Method method;
if (stok.hasMoreTokens()) { // getter method to another object
method = methodClass.getMethod(methodName); // no args
methodClass = determineImplementingClassOfReturnType(method.getReturnType());
}
else { // isReferencingMethod
try {
method = methodClass.getMethod(methodName, Long.TYPE);
}
catch (NoSuchMethodException e1) {
// try Long instead of long
method = methodClass.getMethod(methodName, Long.class);
}
}
int modifiers = method.getModifiers();
if (Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
throw new PersistenceException(methodClass.getName() + "." + method.getName() + " must be public and non-static");
}
methods.add(method);
}
return new ForeignReference(methods.toArray(new Method[0]), determineReferencePriority(clazz), methodPath);
}
catch (NoSuchMethodException ex) {
throw new PersistenceException("please add an *IsReferencing-wurblet to generate " + clazz.getName() + "." + methodPath, ex);
}
}
/**
* Determines the implementing class for a type returned by a method.
*
* @param clazz the method's returned class
* @return the implementing class
*/
protected Class> determineImplementingClassOfReturnType(Class> clazz) {
return clazz; // same in low-level persistence layer
}
/**
* Determines whether the class of this variable holder is referenced.
*
* @param db the session
* @param id the object id to test whether it is referenced by any other objects
* @return true if referenced
*/
public boolean isReferenced(Db db, long id) {
return isReferencedImpl(db, id, foreignReferences);
}
/**
* Determines whether the class of this variable holder is referenced.
*
* @param db the session
* @param id the object id to test whether it is referenced by any other objects
* @param foreignReferences the foreign references
* @return true if referenced
*/
protected boolean isReferencedImpl(Db db, long id, Set foreignReferences) {
for (ForeignReference ref : foreignReferences) {
try {
@SuppressWarnings("unchecked")
Object obj = AbstractDbObject.newInstance(db, (Class>) ref.methods[0].getDeclaringClass());
for (int methodIndex = 0; methodIndex < ref.methods.length; methodIndex++) {
if (methodIndex < ref.methods.length - 1) {
obj = ref.methods[methodIndex].invoke(obj);
}
else if (Boolean.TRUE.equals(ref.methods[methodIndex].invoke(obj, id))) {
return true;
}
}
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new PersistenceException("couldn't invoke " + ref.methodPath, ex);
}
}
return false;
}
/**
* Gets the delegateId of the class, i.e. subclass of AbstractDbObject.
* If the remoteDelegateId is 0, it will be prepared
* The delegateId is unique for each class. It is valid only in remote connections and
* is the same for all remote sessions.
* The RMI-server creates a delegate for each subclass of AbstractDbObject (DbObjectRemoteDelegateImpl resp.)
*
* @return the delegate id
*/
public int getRemoteDelegateId() {
if (remoteDelegateId == 0) {
remoteDelegateId = Db.prepareRemoteDelegate(clazz);
}
return remoteDelegateId;
}
/**
* Gets the RemoteDelegate for the class and session.
*
* @param db the session
* @return the delegate
*/
@SuppressWarnings("unchecked")
public AbstractDbObjectRemoteDelegate getRemoteDelegate(Db db) {
return (AbstractDbObjectRemoteDelegate
) db.getRemoteDelegate(getRemoteDelegateId());
}
/**
* Gets a class property.
*
* @param key the property key
* @return the property value
*/
public String getProperty(String key) {
return getProperties().getProperty(key);
}
/**
* Sets a class property.
*
* @param key the property key
* @param value the property value
* @return the old property value, null if new
*/
public String putProperty(String key, String value) {
return (String) getProperties().put(key, value);
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(clazz.getName());
if (tableName != null) {
buf.append("/'").append(tableName).append("'");
}
return buf.toString();
}
}