ForeignResolver.java
/*
** Module : ForeignResolver.java
** Abstract : Helper object for foreign relation joins
**
** Copyright (c) 2006-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- -JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060302 @25012 Created initial version. Resolves foreign
** records using legacy keys.
** 002 ECF 20060713 @28045 Use new DBUtils class.
** 003 ECF 20061114 @31137 Modified to support expanded foreign
** association synchronization. Also embedded
** rtrim() function into character queries to
** match database indices.
** 004 ECF 20070629 @35298 Minor optimization. Replaced StringBuffer
** with StringBuilder.
** 005 CA 20080819 @39460 Support API change in FieldReference.
** 006 SVL 20140210 Use HQLExpression instead of HQL string.
** 007 ECF 20160202 Replaced Apache commons logging with J2SE logging.
** 008 OM 20160603 Fixed rtrimming to space character only.
** 009 ECF 20200906 New ORM implementation.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** 010 SR 20230510 Matched persistence.list() new signature.
** 011 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.util.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.logging.*;
/**
* Resolves records on either end of a foreign key association, based upon
* the values of one or more properties in a DMO, where those properties
* represent the legacy "foreign key" in the pre-conversion application.
* This allows a record's foreign key to be kept in synch with changes to
* these legacy key properties. This is necessary because a true foreign key
* is only added to a table as part of the conversion process; the legacy
* application knows nothing of these new keys, so the runtime must keep them
* in synch as the legacy key values change.
*/
final class ForeignResolver
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(ForeignResolver.class.getName());
/** Information about the primary/foreign key relation */
private final RelationInfo info;
/** FQL query statement */
private final String fql;
/** Field references used to retrieve query substitution values */
private final ArrayList<FieldReference> fieldRefs = new ArrayList<>();
/** Does resolver walk the foreign key association from the foreign end? */
private final boolean inverse;
/** The associated database */
private final Database database;
/**
* Constructor.
*
* @param info
* Descriptor of the relation between the local and foreign DMOs.
* @param buffer
* A record buffer which represents the "local" (i.e., referring)
* DMO if <code>inverse</code> is <code>false</code>, or which
* represents the foreign (i.e., referent) DMO if
* <code>inverse</code> is <code>true</code>.
* @param inverse
* <code>true</code> to construct an object which resolves the
* "local" (i.e., referring) DMO(s) given a foreign (i.e.,
* referent) DMO; <code>false</code> to construct an object
* which resolves the foreign DMO given a local DMO.
*/
ForeignResolver(RelationInfo info, RecordBuffer buffer, boolean inverse)
{
this.info = info;
this.database = buffer.getDatabase(); // TODO: OM: caching Database obj?
this.inverse = inverse;
this.fql = generateFQL(info);
Iterator<String> propIter = inverse
? info.foreignProperties()
: info.localProperties();
createFieldRefs(buffer.getDMOInterface(), propIter);
}
/**
* Resolve a foreign record using the values of one or more legacy key
* properties in <code>buffer</code>'s current record.
*
* @param buffer
* The record buffer containing the current, local record for
* which a related, foreign record must be found.
*
* @return The primary key ID of the foreign record associated with the
* current value(s) of the legacy key(s) in <code>buffer</code>'s
* current record, or <code>null</code> if none was found.
*
* @throws PersistenceException
* if any error occurs querying the foreign record from the
* database.
* @throws IllegalStateException
* if the current record in the buffer is <code>null</code>, or
* if this object has been configured to retrieve the record for
* the inverse, foreign key association.
*/
Long resolveForeign(RecordBuffer buffer)
throws PersistenceException
{
if (inverse)
{
throw new IllegalStateException(
"Resolver configured to resolve inverse foreign association");
}
Record local = buffer.getCurrentRecord();
if (local == null)
{
throw new IllegalStateException("Cannot resolve foreign DMO for a null record");
}
Persistence persistence = buffer.getPersistence();
Object[] args = getArguments(local);
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("FQL: " + fql);
StringBuilder buf = new StringBuilder("PARMS: ");
int len = args.length;
if (len == 0)
{
buf.append("N/A");
}
for (int k = 0; k < len; k++)
{
if (k > 0)
{
buf.append(", ");
}
buf.append(args[k]);
}
LOG.fine(buf.toString());
buf.setLength(0);
buf.append("LOCAL DMO: ");
buf.append(local);
buf.append(" [");
buf.append(local.primaryKey());
buf.append("]");
LOG.fine(buf.toString());
}
List<Long> list = persistence.list(fql, args, 0, 0);
Long id = list == null ? null : list.get(0);
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("FOREIGN DMO: " + (id != null ? id : "N/A"));
}
return id;
}
/**
* Resolve zero or more records which refer via foreign key association to
* the record currently stored in {@code buffer}, using the values
* of one or more legacy key properties in that record.
*
* @param buffer
* The record buffer containing the current, foreign record for
* which one or more related, "local" records must be found.
*
* @return A list of zero or more primary key IDs for the record(s), if
* any, associated with the current value(s) of the legacy key(s)
* in {@code buffer}'s current record.
*
* @throws PersistenceException
* if any error occurs querying the database.
* @throws IllegalStateException
* if the current record in the buffer is {@code null}, or
* if this object has not been configured to retrieve the record
* for the inverse, foreign key association..
*/
List<Long> resolveLocal(RecordBuffer buffer)
throws PersistenceException
{
if (!inverse)
{
throw new IllegalStateException(
"Resolver not configured to resolve inverse foreign association");
}
Record dmo = buffer.getCurrentRecord();
if (dmo == null)
{
throw new IllegalStateException("Cannot resolve local DMO for a null record");
}
Persistence persistence = buffer.getPersistence();
Object[] args = getArguments(dmo);
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("FQL: " + fql);
StringBuilder buf = new StringBuilder("PARMS: ");
int len = args.length;
if (len == 0)
{
buf.append("N/A");
}
for (int k = 0; k < len; k++)
{
if (k > 0)
{
buf.append(", ");
}
buf.append(args[k]);
}
LOG.fine(buf.toString());
buf.setLength(0);
buf.append("FOREIGN DMO: ");
buf.append(dmo);
buf.append(" [");
buf.append(dmo.primaryKey());
buf.append("]");
LOG.fine(buf.toString());
}
List<Long> list = persistence.list(fql, args, 0, 0);
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("LOCAL DMO(s): " + list);
}
return list;
}
/**
* Get the descriptor object which contains information about the relation
* associated with this resolver.
*
* @return Relation descriptor.
*/
RelationInfo getInfo()
{
return info;
}
/**
* Get the current values of all legacy key properties for this relation,
* from the given record. These will become query substitution values
* when searching for the foreign record.
*
* @param dmo
* Record currently stored in the record buffer.
*
* @return Array of current, legacy key property values.
*/
private Object[] getArguments(Record dmo)
{
Object[] args = new Object[fieldRefs.size()];
Iterator<FieldReference> iter = fieldRefs.iterator();
for (int i = 0; iter.hasNext(); i++)
{
FieldReference next = iter.next();
args[i] = next.get(dmo);
}
return args;
}
/**
* Generate the FQL statement which will be used for record resolution,
* using the information stored in the relation descriptor.
*
* @param info
* Relation descriptor.
*
* @return FQL query statement.
*/
private String generateFQL(RelationInfo info)
{
String alias = inverse ? info.getLocalAlias() : info.getForeignAlias();
FQLExpression buf = new FQLExpression("select ");
buf.append(alias);
buf.append("." + Session.PK + " from ");
buf.append(inverse ? info.getLocalClassName() : info.getForeignClassName());
buf.append(" as ");
buf.append(alias);
buf.append(" where ");
Iterator<String> iter = inverse
? info.localProperties()
: info.foreignProperties();
for (int i = 0; iter.hasNext(); i++)
{
if (i > 0)
{
buf.append(" and ");
}
String prop = iter.next();
// TODO: support computed columns
if (info.isIgnoreCase(prop))
{
buf.append("upper(rtrim(");
buf.append(alias);
buf.append(".");
buf.append(prop);
buf.append(")) = upper(rtrim(", true);
buf.append("))");
}
else
{
buf.append(alias);
buf.append(".");
buf.append(prop);
buf.append(" = ", true);
}
}
return buf.toFinalExpression();
}
/**
* Construct one {@link FieldReference} object for each legacy key
* property which will be used as a query substitution parameter during
* record resolution. Store these objects in a list for later access.
*
* @param dmoIface
* Business interface which the field references will invoke to
* retrieve property values.
* @param propIter
* Iterator on the names of those DMO properties which form the
* legacy foreign key.
*/
private void createFieldRefs(Class<? extends DataModelObject> dmoIface,
Iterator<String> propIter)
{
while (propIter.hasNext())
{
String prop = propIter.next();
fieldRefs.add(new FieldReference(database, dmoIface, prop));
}
fieldRefs.trimToSize();
}
}