AbstractJoin.java
/*
** Module : AbstractJoin.java
** Abstract : Abstract base class for natural join helpers
**
** Copyright (c) 2006-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060309 @25000 Created initial version. Abstract base class for
** natural join helper implementations.
** 002 ECF 20060803 @28381 Fixed parameter type defect. Was using the wrong
** end of the join for parameter type in a non-
** dereference situation.
** 003 SVL 20080421 @38073 Added support for multiple query substitution
** parameters.
** 004 CA 20080618 @38866 Save the inverse buffer for later checks.
** 005 ECF 20080822 @39559 Added isServerJoin(). Reports whether join occurs
** at database server or within P2J runtime
** environment.
** 006 ECF 20090716 @43210 Added RecordBuffer.initialize() calls to c'tor.
** 007 SVL 20120331 Upgraded to Hibernate 4.
** 008 SVL 20140210 Use HQLExpression instead of HQL string.
** 009 ECF 20150330 Added support for cross-schema joins.
** 010 ECF 20150801 Added getUnresolvedParameters().
** 011 SVL 20160626 If join is missing, add it at runtime.
** 012 ECF 20171220 Change to error handling.
** 013 ECF 20200906 New ORM implementation.
** 014 OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them.
** OM 20201007 Optimizations by using DmoMeta data instead of map lookups.
** VVT 20210106 Fixed: NPE in isIndexedIgnoreCase(). See #5062.
** ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to
** prevent NPE in proxy case.
** CA 20220520 The index field lookup must be case-insensitive, when building the relation.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** 015 OM 20230404 The join navigation key may contain null/unknown values.
** 016 OM 20230630 Added support for the newly observed PARENT_ID_RELATION between joined tables.
** 017 OM 20230911 Added full dynamic support for bidirectional PARENT_ID_RELATION between joins.
*/
/*
** 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 com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import java.io.*;
import java.util.*;
/**
* The abstract base class for dynamic and server-side join helpers. Join
* helpers are used by query implementations to:
* <ul>
* <li>generate FQL where clause snippets which join records using foreign relations;
* <li>provide query substitution parameters if the FQL snippet requires them at query execution time.
* </ul>
* <p>
* Instances of this class are enclosed within query objects and are used at
* query prepare time and at query execution time to augment the query with
* the necessary join semantics.
*
* @see DynamicJoin
* @see ServerJoin
*/
abstract class AbstractJoin
{
/** The index of fql expression for {@code null} fields. */
public static final int NULL_FIELD = 0;
/** The index of fql expression for {@code not null} fields. */
public static final int NOT_NULL_FIELD = 1;
/** FQL where clause snippet used to perform the join */
private final FQLExpression[][] fql;
/** DMO interface for the inverse (non-local) end of the join */
private final Class<? extends DataModelObject> inverseIface;
/** Hibernate types of query substitution parameters (null if none) */
private final List<FqlType> parameterTypes;
/** Buffer to which this component should join via a foreign relation. */
protected final BufferReference inverse;
/**
* Constructor which invokes abstract initialization methods in subclass
* implementations.
*
* @param local
* Local record buffer (the referring end of the join).
* @param inverse
* Inverse record buffer (the referent end of the join).
*/
AbstractJoin(RecordBuffer local, RecordBuffer inverse)
{
local.initialize();
inverse.initialize();
// look up relation information
Class<? extends DataModelObject> localIface = local.getDMOInterface();
this.inverseIface = inverse.getDMOInterface();
this.inverse = inverse;
DmoMeta inverseInfo = inverse.getDmoInfo();
DmoMeta localInfo = local.getDmoInfo();
RelationInfo info = localInfo.getRelation(inverseIface);
boolean dereference = (info != null);
String parentChildRelationField = null;
if (!dereference)
{
info = inverseInfo.getRelation(localIface);
}
// if relation doesn't exist, add it at runtime
if (info == null)
{
try
{
Set<String> commonFields = TableMapper.getCommonFields(local, inverse);
P2JIndex localIndex = TableMapper.findJoiningIndex(local, commonFields);
P2JIndex inverseIndex = TableMapper.findJoiningIndex(inverse, commonFields);
boolean localHasLeadingIndex = false;
if (localIndex != null)
{
if (inverseIndex == null)
{
// the left table contains the primary key
localHasLeadingIndex = true;
}
else
{
// in this case, both tables had a unique index containing one or more common
// fields. Must analyze further...
Set<String> set1 = TableMapper.getIndexFieldNames(localIndex);
Set<String> set2 = TableMapper.getIndexFieldNames(inverseIndex);
commonFields.retainAll(set1);
commonFields.retainAll(set2);
boolean match1 = set1.equals(commonFields);
boolean match2 = set2.equals(commonFields);
if (match1 && !match2)
{
localHasLeadingIndex = true;
}
else if (!match1 && !match2)
{
// it is ambiguous which end represents the primary and which the foreign end
// of this relation. Probably requires manual inspection and a hint.
raiseJoinError(local, inverse, false);
}
}
}
else if (inverseIndex == null)
{
Property property = null;
if (!local.isTemporary() && !inverse.isTemporary())
{
// "FIND <parent> OF <child>", based on child having a "FIELD <parent>-recid AS RECID" field
// [local] is <parent> and [inverse] is <child>
property = inverseInfo.byLegacyName(localInfo.legacyTable.toLowerCase() + "-recid");
if (property == null)
{
property = localInfo.byLegacyName(inverseInfo.legacyTable.toLowerCase() + "-recid");
localHasLeadingIndex = false;
}
else
{
localHasLeadingIndex = true;
}
}
if (property == null)
{
raiseJoinError(local, inverse, !inverse.isTemporary());
}
else
{
parentChildRelationField = property.legacy;
}
}
RecordBuffer referring = localHasLeadingIndex ? inverse : local;
RecordBuffer referent = localHasLeadingIndex ? local : inverse;
P2JIndex relationIndex = localHasLeadingIndex ? localIndex : inverseIndex;
Set<String> indexFieldNames = relationIndex == null
? new HashSet<>(Collections.singletonList(parentChildRelationField))
: TableMapper.getIndexFieldNames(relationIndex);
info = new RelationInfo(referring.getDMOInterface(),
referring.getDMOImplementationClass(),
referent.getDMOInterface(),
referent.getDMOImplementationClass(),
referent.getSchema());
Map<String, String> legacyToPropReferring =
TableMapper.getLegacyFieldNameMap(referring).getLegacyField2Name();
Map<String, String> legacyToPropReferent =
TableMapper.getLegacyFieldNameMap(referent).getLegacyField2Name();
for (String fieldName : indexFieldNames)
{
fieldName = fieldName.toLowerCase();
String referringProp = legacyToPropReferring.get(fieldName);
if (referringProp == null)
{
referringProp = Session.PK; //"recid";
}
String referentProp = legacyToPropReferent.get(fieldName);
if (referentProp == null)
{
referentProp = Session.PK; //"recid";
}
Boolean ignoreCase = referent.getDmoInfo().isIndexedIgnoreCase(referentProp);
info.addPropertyData(referringProp, referentProp, ignoreCase != null && ignoreCase);
}
dereference = !localHasLeadingIndex;
DmoMetadataManager.putDynamicRelation(info);
}
catch (PersistenceException e)
{
throw new IllegalArgumentException(
"Cannot determine join type between " + local.getDMOInterface().getName() +
" and " + inverse.getDMOInterface().getName(), e);
}
}
// Initialize FQL.
this.fql = generateFQL(local, inverse, info, dereference);
// Store parameter type.
this.parameterTypes = getParameterTypes(info, dereference);
// Remaining setup, if any.
setup(local, inverse, info, dereference);
}
/**
* Return the inverse buffer used with this join, if any.
*
* @return The inverse buffer used with this join.
*/
public BufferReference getInverse()
{
return inverse;
}
/**
* Get a list of unresolved field reference parameters for this join.
*
* @return An empty list. Subclasses should override this implementation if a non-empty
* parameter list is required.
*/
public List<FieldReference> getUnresolvedParameters()
{
return Collections.emptyList();
}
/**
* Report whether the join represented by this object takes place at the
* database server, or within runtime code.
*
* @return <code>true</code> if the join happens at the server;
* <code>false</code> if it happens in the runtime.
*/
protected abstract boolean isServerJoin();
/**
* Generate the set of FQL where clause snippets which will be inserted into the overall FQL statement
* executed by the enclosing query.
*
* @param local
* Local record buffer (the referring end of the join).
* @param inverse
* Inverse record buffer (the referent end of the join).
* @param info
* Relation descriptor object.
* @param dereference
* {@code true} if the local DMO contains a reference to the foreign record, which must be
* dereferenced in the FQL query; {@code false} if the local DMO <i>is</i> the foreign record,
* referenced by the inverse DMO.
*
* @return FQL where clause snippets used to perform the join. A bi-dimensional array of clauses are used.
* The first dimension is the size of the join (aka the number of fields paired) and the second is
* always two: the first (NULL_FIELD) is used when the joined field is {@code null} and the second
* (NOT_NULL_FIELD) is used when the join field is not unknown.
*/
protected abstract FQLExpression[][] generateFQL(RecordBuffer local,
RecordBuffer inverse,
RelationInfo info,
boolean dereference);
/**
* Determine the Hibernate type of the query substitution parameters, if any. The parameter(s)
* generally will be a FQL type of the implementation class of the inverse DMO or the types of
* DMO fields.
*
* @param info
* Relation descriptor object.
* @param dereference
* {@code true} if the local DMO contains a reference to the foreign record, which
* must be dereferenced in the FQL query; {@code false} if the local DMO <i>is</i> the
* foreign record, referenced by the inverse DMO.
*
* @return The list of appropriate FQL types, or {@code null} if no query substitution
* parameters are used for this join.
*/
protected abstract List<FqlType> getParameterTypes(RelationInfo info, boolean dereference);
/**
* Perform any additional setup necessary for a particular join helper
* implementation. This base implementation does nothing.
*
* @param local
* Local record buffer (the referring end of the join).
* @param inverse
* Inverse record buffer (the referent end of the join).
* @param info
* Relation descriptor object.
* @param dereference
* <code>true</code> if the local DMO contains a reference to the
* foreign record, which must be dereferenced in the FQL query;
* <code>false</code> if the local DMO <i>is</i> the foreign
* record, referenced by the inverse DMO.
*/
protected void setup(RecordBuffer local,
RecordBuffer inverse,
RelationInfo info,
boolean dereference)
{
}
/**
* Throw {@link ErrorConditionException} that signals that the specified tables cannot be joined.
*
* @param left
* Record buffer which represents the left table in the join.
* @param right
* Record buffer which represents the right table in the join.
* @param parentIsPersistent
* Depending on this parameter, a specific error is raised: if {@code true} then error 230,
* otherwise the pair 855 and 1832 are raised.
*
* @throws ErrorConditionException
* If {@code parentIsPersistent} then error 230, containing the text
* {@code '<Table1> of <Table2>'. Index fields of table 1 must be fields in table 2.}.
* Otherwise, {@code Unknown database name .} and
* {@code ** Workfile names cannot have database qualifiers.} are raised.
*/
protected void raiseJoinError(RecordBuffer left, RecordBuffer right, boolean parentIsPersistent)
{
if (parentIsPersistent)
{
ErrorManager.recordOrThrowError(
new int[] {855, 1832},
new String[] {
"Unknown database name . (855)",
"** Workfile names cannot have database qualifiers. (1832)"
}, false, true, false);
}
else
{
String msg = left.getLegacyName() + " of " + right.getLegacyName() + ". " +
"Index fields of table 1 must be fields in table 2";
ErrorManager.recordOrThrowError(230, msg);
}
}
/**
* Get the values of DMO instance fields or the DMO instance itself which
* will be used as the query substitution parameter(s). This base
* implementation returns <code>null</code>. Subclasses which need a
* parameter must override this method.
*
* @return <code>null</code>.
*/
List<Serializable> getParameters()
{
return null;
}
/**
* Get the DMO interface for the inverse (non-local) end of the join.
*
* @return Inverse DMO interface.
*/
Class<? extends DataModelObject> getInverseInterface()
{
return inverseIface;
}
/**
* Get the FQLs for the where clause snippet used to perform the join. Since this part of the predicate
* will nnt be preprocessed using {@code FQLPreprocessor} and the join fields of the inverse may contain
* unknown values, a bi-dimensional array of clauses are used. The first dimension is the size of the join
* (aka the number of fields paired) and the second is always two: the first (0) is used when the joined
* field is {@code null} and the second (1) is used when the join field is not unknown.
* <p>
* The enclosing query embeds these phrases into the overall FQL statement before executing the query based
* on the actual values of the inverse buffer.
*
* @return FQL where clause snippets used to perform the join. A bi-dimensional array of clauses are used.
* The first dimension is the size of the join (aka the number of fields paired) and the second is
* always two: the first (NULL_FIELD) is used when the joined field is {@code null} and the second
* (NOT_NULL_FIELD) is used when the join field is not unknown.
*/
FQLExpression[][] getFQL()
{
return fql;
}
/**
* Get the type of the query substitution parameters, if any, which was
* determined at construction.
*
* @return The list of appropriate Hibernate types, or <code>null</code>
* if no query substitution parameters are used for this join.
*/
List<FqlType> getParameterTypes()
{
return parameterTypes;
}
}