# 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;
       }
    }