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