AssociationSyncher.java
/*
** Module : AssociationSyncher.java
** Abstract : Synchronizes records across foreign key associations
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20061113 @31136 Created initial version. Abstract base class
** for classes which synchronize records across
** foreign key associations.
** 002 ECF 20061114 @31145 Disabled prohibitively expensive operations.
** This effectively removes all synchronization
** except setting the foreign DMO in a local DMO
** when a legacy key property in that local DMO
** is changed.
** 003 ECF 20070416 @33018 Integrated user interrupt handling.
** 004 ECF 20070704 @34357 Closed session flush loophole. A pending flush
** is now forced with the ChangeBroker after an
** association is updated.
** 005 ECF 20070911 @35282 Replaced database name string with Database
** information object. Required for non-local
** database connections.
** 006 ECF 20071219 @36576 Changed acquireLock() signature. Allow lock
** type to be specified.
** 007 CA 20080815 @39450 Support API change in DBUtils.
** 008 GES 20090421 @41823 Matched utility class package change.
** 009 ECF 20131029 Import change, minor housekeeping.
** 010 ECF 20160202 Replaced Apache commons logging with J2SE logging.
** 011 ECF 20200906 New ORM implementation.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.util.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.logging.CentralLogger;
/**
* The abstract base class for subclasses which synchronize either end of a
* foreign key association, based upon legacy key properties being changed in
* the record on the opposite end of the association. In the pre-conversion
* application, there are no formal, foreign keys. Such associations between
* records are defined less stringently; the association between two records
* is determined dynamically at runtime. With the introduction of formal
* foreign keys into the schema, it is necessary to mimic this very late
* binding association by dynamically updating foreign keys as the values of
* legacy key columns change. In the P2J environment, a DMO joined to
* another DMO by a foreign key contains a reference to that DMO (and
* associated getter and setter methods). It is these references which are
* updated by this class and its subclasses.
*/
abstract class AssociationSyncher
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(AssociationSyncher.class.getName());
/** Object which abstracts methods to assist in managing the foreign DMO */
protected final Helper helper;
/** Method used to set foreign DMO into local DMO */
protected final Method foreignSetter;
/** Name of table for which temporary record locks must be acquired */
protected final String lockTable;
/** Record buffer being monitored for property changes */
protected final RecordBuffer buffer;
/** Set of temporary locks which are acquired to make updates */
private final Set<TempLock> tempLocks = new HashSet<>();
/**
* Constructor.
*
* @param buffer
* Record buffer containing the DMO whose property change(s)
* will trigger synchronization attempts.
* @param info
* Foreign relation descriptor object.
* @param lockClass
* DMO class for which temporary record locks must be acquired.
*/
AssociationSyncher(RecordBuffer buffer, RelationInfo info, Class<? extends Record> lockClass)
{
this.buffer = buffer;
lockTable = DatabaseManager.getBackingTableName(lockClass);
buffer.addLegacyForeignKeys(info.getForeignProperties());
if (info.isUnique())
{
helper = new OneToOneHelper(info);
}
else
{
helper = new OneToManyHelper(info);
}
String ifaceName = info.getForeignInterfaceName();
String method = null;
try
{
// store foreign record setter method
Class<?> localIface = info.getLocalInterface();
method = "set" + ifaceName + "Record";
Class<?>[] sig = new Class<?>[] { info.getForeignInterface() };
foreignSetter = localIface.getDeclaredMethod(method, sig);
}
catch (NoSuchMethodException exc)
{
throw new IllegalArgumentException(
"Invalid DMO class: missing foreign setter [" +
info.getLocalInterfaceName() +
"." +
method +
"]");
}
}
/**
* Perform record synchronization. This method is optimized to avoid the
* overhead of performing a database query if not absolutely necessary.
*
* @param dirtyProperties
* The set of properties which have been modified, thereby
* triggering the synchronization request.
*
* @throws PersistenceException
* if any error occurs resolving the foreign record.
*/
public final void attemptSynch(Set<String> dirtyProperties)
throws PersistenceException
{
if (buffer.getCurrentRecord() == null)
{
return;
}
Set<String> properties = getLegacyKeys();
Iterator<String> iter = dirtyProperties.iterator();
while (iter.hasNext())
{
// If any legacy key property has been updated, a synch is needed.
if (properties.contains(iter.next()))
{
doSynch();
break;
}
}
}
/**
* Retrieve the set of property names which represent the columns which
* comprise the legacy key joining two records. Changes to these
* properties trigger synchronization of the two ends of a foreign key
* association. A set is returned because legacy keys are often
* composites of multiple columns.
*
* @return Set of property names comprising the legacy key.
*/
protected abstract Set<String> getLegacyKeys();
/**
* Subclasses which need to remove any existing association linkages
* before the new association is applied must implement this method.
*
* @param newLocalDMOs
* Set of zero or more local DMOs which are about to be updated
* with the new association.
* @param newForeignDMO
* Foreign DMO which is about to be updated with the new
* association.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
protected abstract void preSynch(Set<Record> newLocalDMOs, Record newForeignDMO)
throws IllegalAccessException,
InvocationTargetException;
/**
* Retrieve the set of DMOs which comprise the local end of the foreign
* association. In cases where the foreign end of the association is
* driving the synchronization, these will represent the "new" local
* records which are resolved, based upon the new property values in one
* of the foreign record's legacy keys.
* <p>
* The implementing class should obtain an exclusive lock on each record
* returned, using the {@link #acquireLock} method to ensure it is
* properly released or downgraded upon completion of synchronization.
* This is not necessary for records already exclusively locked.
* <p>
* If the association is one-to-one, the set will contain zero or one
* records.
* <p>
* If the association is one-to-many, the set may contain zero or more
* records.
*
* @return Set of newly resolved local DMOs. The set may be empty, but
* it will not be <code>null</code>.
*
* @throws PersistenceException
* if there are any errors retrieving DMOs from the database.
*/
protected abstract Set<Record> retrieveLocalDMOs()
throws PersistenceException;
/**
* Retrieve the DMO, if any, at the foreign end of the association. In
* cases where the local end of the association is driving the
* synchronization, this will represent the "new" foreign record which is
* resolved, based upon the new property values in one of the local
* record's legacy keys.
* <p>
* The implementing class should obtain an exclusive lock on the record
* returned, using the {@link #acquireLock} method to ensure it is
* properly released or downgraded upon completion of synchronization.
* This is not necessary if the record is already exclusively locked.
*
* @return Foreign DMO, or <code>null</code> if none was available.
*
* @throws PersistenceException
* if there are any errors retrieving the DMO from the database.
*/
protected abstract Record retrieveForeignDMO()
throws PersistenceException;
/**
* Acquire a temporary lock of the specified type for the given primary key
* ID. The table in which the record with the specified ID resides was
* stored during construction of this object. The lock is released when it
* is no longer needed.
*
* @param id
* ID of the record to lock.
* @param lockType
* Lock type to acquire for the record.
*
* @throws LockUnavailableException
* if a NO-WAIT lock type variant is requested, but the lock
* cannot be acquired immediately.
*/
protected final void acquireLock(Long id, LockType lockType)
throws LockUnavailableException
{
boolean sharedDb = !buffer.getDmoInfo().multiTenant;
TempLock lock = new TempLock(buffer.getPersistence(), lockType, lockTable, id, sharedDb);
tempLocks.add(lock);
}
/**
* Perform foreign association synchronization. This will update the
* appropriate DMO references on both ends of a bidirectional, foreign key
* association.
*
* @throws PersistenceException
* if any error occurs resolving records.
*/
private void doSynch()
throws PersistenceException
{
try
{
Set<Record> localDMOs = retrieveLocalDMOs();
Record foreignDMO = retrieveForeignDMO();
// preSynch(localDMOs, foreignDMO);
// Update the local DMO(s) referenced by the foreign end of the association.
// helper.replace(foreignDMO, localDMOs);
// Update the foreign DMO referenced by the local end(s) of the association.
// ChangeBroker broker = ChangeBroker.get();
// Persistence persistence = buffer.getPersistence();
Iterator<Record> iter = localDMOs.iterator();
while (iter.hasNext())
{
Record next = iter.next();
// Update the foreign record in the local DMO.
foreignSetter.invoke(next, foreignDMO);
// Force a pending flush for the updated DMO type.
// Class<? extends Record> dmoClass = next.getClass();
// broker.forcePendingFlush(persistence, dmoClass);
}
}
catch (IllegalAccessException exc)
{
// Programming error.
throw new RuntimeException(exc);
}
catch (InvocationTargetException exc)
{
DBUtils.handleException(buffer.getDatabase(), exc);
// Programming error.
throw new RuntimeException(exc);
}
catch (PersistenceException exc)
{
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
buffer.describe() + ": Error synchronizing foreign association",
exc);
}
exc.fillInStackTrace();
throw exc;
}
finally
{
// Release temporary locks.
Iterator<TempLock> iter = tempLocks.iterator();
while (iter.hasNext())
{
TempLock lock = iter.next();
lock.reset();
}
tempLocks.clear();
}
}
/**
* An interface which abstracts a helper API for managing a foreign DMO.
* The abstraction hides the implementation differences between DMOs which
* participate in a one-to-one association versus those that participate
* in a one-to-many (many-to-one) association.
*/
protected interface Helper
{
/**
* Retrieve the set of local DMOs stored in a foreign DMO for this
* association.
*
* @param foreignDMO
* Foreign DMO from which to extract the set of local DMOs.
*
* @return Set of local DMOs referenced by the given foreign DMO. The
* set may be empty, but it will not be <code>null</code>.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public abstract Set<Record> getLocalDMOs(Record foreignDMO)
throws IllegalAccessException,
InvocationTargetException;
/**
* Replace the set of local DMOs stored in a foreign DMO for this
* association.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMOs
* The set of local DMOs which must replace the existing set.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public abstract void replace(Record foreignDMO, Set<Record> localDMOs)
throws IllegalAccessException,
InvocationTargetException;
/**
* Remove the given local DMO from the set of local DMOs stored in a
* foreign DMO for this association.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMO
* The local DMO to be removed from the existing set.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public abstract void remove(Record foreignDMO, Record localDMO)
throws IllegalAccessException,
InvocationTargetException;
}
/**
* An implementation of the <code>Helper</code> interface which assumes
* that there is no more than one record on either end of the association.
* This has implications for the methods used to retrieve and update the
* foreign record's reference to the local end of the association.
*/
protected final class OneToOneHelper
implements Helper
{
/** Method used to get local DMO from foreign DMO */
private final Method getter;
/** Method used to set local DMO into foreign DMO */
private final Method setter;
/**
* Constructor. Looks up reflective methods used to get and set the
* foreign DMO's reference to a local DMO.
*
* @param info
* Object which contains information regarding the nature of
* the one-to-one, foreign association.
*/
OneToOneHelper(RelationInfo info)
{
String method = null;
String ifaceName = info.getLocalInterfaceName();
try
{
Class<? extends DataModelObject> foreignIface = info.getForeignInterface();
// store inverse (i.e., local) record getter method
StringBuilder buf = new StringBuilder("get");
buf.append(ifaceName);
buf.append("Record");
method = buf.toString();
getter = foreignIface.getDeclaredMethod(method, (Class<?>[]) null);
// store inverse (i.e., local) record setter method
buf.setLength(0);
buf.append("set");
buf.append(ifaceName);
buf.append("Record");
method = buf.toString();
Class<?>[] sig = new Class[] { info.getLocalInterface() };
setter = foreignIface.getDeclaredMethod(method, sig);
}
catch (NoSuchMethodException exc)
{
throw new IllegalArgumentException(
"Invalid DMO class: missing inverse getter or setter [" +
info.getForeignInterfaceName() +
"." +
method +
"]");
}
}
/**
* Retrieve the set of local DMOs stored in a foreign DMO for this
* association. Since this helper implementation manages a one-to-one
* association, the set will never contain more than one record.
*
* @param foreignDMO
* Foreign DMO from which to extract the set of local DMOs.
*
* @return Set of local DMOs referenced by the given foreign DMO. The
* set may be empty, but it will not be <code>null</code>.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public Set<Record> getLocalDMOs(Record foreignDMO)
throws IllegalAccessException,
InvocationTargetException
{
Record dmo = (Record) getter.invoke(foreignDMO, (Object[]) null);
return (dmo == null) ? new EmptySet<>() : Collections.singleton(dmo);
}
/**
* Replace the local DMO stored in a foreign DMO for this association
* with the contents of the given set.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMOs
* The set of local DMOs which must replace the existing set.
* Since this helper implementation manages a one-to-one
* association, this set must not have more than one element.
* However, it may be empty, in which case the local DMO
* reference in the foreign DMO will be nulled out.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
* @throws IllegalArgumentException
* if <code>localDMOs</code> contains more than one element.
*/
public void replace(Record foreignDMO, Set<Record> localDMOs)
throws IllegalAccessException,
InvocationTargetException
{
if (foreignDMO == null)
{
return;
}
Record localDMO = null;
Iterator<Record> iter = localDMOs.iterator();
if (iter.hasNext())
{
localDMO = iter.next();
}
if (iter.hasNext())
{
throw new IllegalArgumentException(
"Cannot set multiple DMOs using method " + setter.getName());
}
// update the local record in the foreign DMO. Foreign keys are never constrained to be
// not-null, so a null parameter is OK.
setter.invoke(foreignDMO, localDMO);
}
/**
* Remove the reference to the given local DMO from the given foreign
* DMO for this association. This implementation actually ignores the
* <code>localDMO</code> argument and assumes the intent of the caller
* is simply to remove the reference entirely.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMO
* The local DMO to be removed from the existing set.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public void remove(Record foreignDMO, Record localDMO)
throws IllegalAccessException,
InvocationTargetException
{
// Create a set containing only a null reference and pass it to the
// updateLocal method. This makes a simplifying assumption that
// localDMO is the current, local DMO set in the foreign record.
Set<Record> localDMOs = Collections.singleton(null);
replace(foreignDMO, localDMOs);
}
}
/**
* An implementation of the <code>Helper</code> interface which assumes
* a one-to-many association from a foreign DMO to zero or more local
* DMOs. This has implications for the methods used to retrieve and
* update the foreign record's reference to the local end of the
* association.
*/
protected final class OneToManyHelper
implements Helper
{
/** Object containing information about the nature of the association */
private final RelationInfo info;
/** Method used to retrieve set of local DMOs from foreign DMO */
private final Method getter;
/** Method used to set local DMO set into foreign DMO */
private Method setter = null;
/**
* Constructor. Looks up reflective method used to get the set of
* local DMOs referenced by the foreign end of the association.
*
* @param info
* Object which contains information regarding the nature of
* the one-to-many, foreign association.
*/
OneToManyHelper(RelationInfo info)
{
this.info = info;
String method = null;
String ifaceName = info.getLocalInterfaceName();
try
{
// store inverse (i.e., local) records getter method
Class<? extends DataModelObject> foreignIface = info.getForeignInterface();
method = "get" + ifaceName + "Set";
Class<?>[] sig = new Class[] {};
getter = foreignIface.getDeclaredMethod(method, sig);
}
catch (NoSuchMethodException exc)
{
throw new IllegalArgumentException(
"Invalid DMO class: missing inverse getter [" +
info.getForeignInterfaceName() +
"." +
method +
"]");
}
}
/**
* Retrieve the set of local DMOs stored in a foreign DMO for this
* association.
*
* @param foreignDMO
* Foreign DMO from which to extract the set of local DMOs.
*
* @return Set of local DMOs referenced by the given foreign DMO. The
* set may be empty, but it will not be <code>null</code>.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public Set<Record> getLocalDMOs(Record foreignDMO)
throws IllegalAccessException,
InvocationTargetException
{
Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);
return (set == null) ? new EmptySet<>() : set;
}
/**
* Replace the set of local DMOs stored in a foreign DMO for this
* association.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMOs
* The set of local DMOs which must replace the existing set.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public void replace(Record foreignDMO, Set<Record> localDMOs)
throws IllegalAccessException,
InvocationTargetException
{
if (foreignDMO == null)
{
return;
}
// retrieve from the foreign DMO the current set of local DMOs which reference it
Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);
if (set != null)
{
set.retainAll(localDMOs);
}
else
{
if (setter == null)
{
String ifaceName = info.getLocalInterfaceName();
String method = null;
try
{
// store inverse (i.e., local) record setter method
Class<? extends DataModelObject> foreignIface = info.getForeignInterface();
method = "set" + ifaceName + "Set";
Class<?>[] sig = new Class[] { Set.class };
setter = foreignIface.getDeclaredMethod(method, sig);
}
catch (NoSuchMethodException exc)
{
throw new IllegalArgumentException(
"Invalid DMO class: missing inverse setter [" +
info.getForeignInterfaceName() +
"." +
method +
"]");
}
}
setter.invoke(foreignDMO, localDMOs);
}
}
/**
* Remove the given local DMO from the set of local DMOs stored in a
* foreign DMO for this association.
*
* @param foreignDMO
* Foreign DMO which stores a reference to local DMOs.
* @param localDMO
* The local DMO to be removed from the existing set.
*
* @throws IllegalAccessException
* if an access error occurs while invoking a method using
* reflection.
* @throws InvocationTargetException
* if a method invoked using reflection throws an exception.
*/
public void remove(Record foreignDMO, Record localDMO)
throws IllegalAccessException,
InvocationTargetException
{
// retrieve from the foreign DMO the current set of local DMOs which reference it
Set<Record> set = (Set<Record>) getter.invoke(foreignDMO, (Object[]) null);
if (set != null)
{
set.remove(localDMO);
}
}
}
}