FQLBundle.java

/*
** Module   : FQLBundle.java
** Abstract : FQL data necessary for random access query navigation
**
** Copyright (c) 2005-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051007   @23166 Created initial version. A bundle of HQL statements and method objects created by
**                           an HQLHelper and used by a RandomAccessQuery.
** 002 ECF 20060317   @25098 Added substitution indices feature. This array contains the index mapping of
**                           query substitution parameters, which may have been reordered during HQL statement
**                           preprocessing.
** 003 ECF 20080330   @37733 Code cleanup. Integrated generics. Replaced StringBuffer with StringBuilder.
** 004 ECF 20080516   @38499 Added getStatements() method and made class public.
** 005 ECF 20080606   @38604 Added support for dirty database HQL. Changed getStatements() to
**                           getDirtyStatements(). Added freeze() method to conserve a little memory.
** 006 ECF 20081009   @40076 Integrated ParameterIndices. This object replaces a simple array of integers.
** 007 GES 20090424   @41939 Import change.
** 008 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 009 ECF 20200906          New ORM implementation.
** 010 OM  20221103          Renamed class to FQLBundle.
*/

/*
** 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 com.goldencode.util.*;

/**
 * An assemblage of the various FQL statements, corresponding, reflective
 * methods, and an index of the runtime query substitution parameters, which
 * are necessary to satisfy a specific record retrieval request for a random
 * access query.
 * <p>
 * Each bundle contains a list of one or more FQL statements which are
 * executed in sequence by a {@link RandomAccessQuery} instance to obtain
 * a result for a specific record retrieval (e.g., first, last, next, etc.)
 * request.  These statements include as their restriction criteria (i.e.,
 * where clause), the query's original, base where clause (if any), augmented
 * with sequentially less-specific restriction criteria composed using the
 * query's sort criteria.  If any parameterized substitutions are necessary
 * to supply these statements with data extracted from the current record,
 * the appropriate list of "getter" methods is supplied by the bundle as
 * well.
 * <p>
 * To satisfy the random access query's single-record retrieval semantic,
 * each statement is executed in turn, until a non-null result is returned.
 * For each statement, as many of the getter methods are executed against the
 * current record DMO, as are necessary to provided the substitutions
 * required for that statement (for the augmented portion only).  These
 * values are added to the existing parameter array (if any) for the base
 * where clause, at the time the statement is executed.
 * <p>
 * For example, given the following query criteria and a request to retrieve
 * the "next" record...
 * <pre>
 *    DMO:        Customer
 *    alias:      customer
 *    where:      customer.number = ? and customer.creditLimit = ?
 *    order by:   customer.name asc
 * </pre>
 * <p>
 * ...the FQL bundle might contain the following list of FQL statements...
 * <pre>
 * 1) from Customer as customer
 *    where (customer.number = ? and customer.creditLimit = ?)
 *      and (customer.name = ? and customer.recid &gt; ?)
 *
 * 2) from Customer as customer
 *    where (customer.number = ? and customer.creditLimit = ?)
 *      and (customer.name &gt; ?)
 * </pre>
 * <p>
 * ...and the following list of DMO "getter" methods...
 * <pre>
 * 1) Customer.getName()
 * 2) Customer.primaryKey()
 * </pre>
 * <p>
 * The methods are only used to satisfy the <code>customer.name</code> and
 * <code>customer.id</code> substitutions; the <code>customer.number</code>
 * and <code>customer.creditLimit</code> parameter substitution values must
 * be provided by the client code which creates the query.
 * <p>
 * A bundle is unaware of the navigation/retrieval mode with which it has
 * been tasked (e.g., first, last, next, etc.);  it is simply a container
 * for the information described above.
 *
 * @see  FQLHelper
 */
public final class FQLBundle
{
   /** DMO getter methods used to get sort criteria parameter values */
   private List<Method> getters = new ArrayList<>();
   
   /** FQL statements to be executed to satisfy a record retrieval request */
   private List<String> statements = new ArrayList<>();
   
   /** Parallel set of FQL statements for dirty database use */
   private List<String> dirtyStatements = null;
   
   /** Mapping of indices in the array of query substitution parameters */
   private ParameterIndices paramIndices = null;
   
   /**
    * Default constructor.
    */
   FQLBundle()
   {
   }
   
   /**
    * Get an unmodifiable view on the list of FQL statements in this bundle,
    * which are for dirty database use.  If no dirty statements have been
    * stored in this bundle, the list of primary database FQL statements is
    * returned.
    *
    * @return  Unmodifiable list of FQL statements using dirty database's dialect.
    */
   public List<String> getDirtyStatements()
   {
      return dirtyStatements;
   }
   
   /**
    * Get a string representation of the internal state of this object,
    * primarily for debugging purposes.
    *
    * @return  String representing this object's state.
    */
   public String toString()
   {
      String sep = System.getProperty("line.separator");
      StringBuilder buf = new StringBuilder(sep);
      
      buf.append("PRIMARY STATEMENTS").append(sep);
      Iterator<String> stmtIter = statements.iterator();
      while (stmtIter.hasNext())
      {
         buf.append(StringHelper.indent(stmtIter.next(), 3)).append(sep);
      }
      
      if (dirtyStatements != null)
      {
         buf.append("DIRTY STATEMENTS").append(sep);
         stmtIter = dirtyStatements.iterator();
         while (stmtIter.hasNext())
         {
            buf.append(StringHelper.indent(stmtIter.next(), 3)).append(sep);
         }
      }
      
      buf.append("GETTERS").append(sep);
      Iterator<Method> getterIter = getters.iterator();
      while (getterIter.hasNext())
      {
         buf.append("   ").append(getterIter.next()).append(sep);
      }
      
      return buf.toString();
   }
   
   /**
    * Get an iterator on the list of FQL statements in this bundle.
    *
    * @return  Iterator of FQL statements.
    */
   Iterator<String> statements()
   {
      return statements.iterator();
   }
   
   /**
    * Get the number of FQL statements stored in this bundle.
    *
    * @return  Statement list size.
    */
   int statementsSize()
   {
      return statements.size();
   }
   
   /**
    * Retrieve the FQL statement at a particular index in the list.
    *
    * @param   index
    *          Index of the required statement.
    * @param   dirty
    *          <code>true</code> to retrieve statement for dirty database
    *          dialect;  <code>false</code> to get primary statement.
    *
    * @return  FQL statement at the specified index.
    *
    * @throws  ArrayIndexOutOfBoundsException
    *          if <code>index</code> is invalid.
    */
   String getStatement(int index, boolean dirty)
   {
      if (dirty && dirtyStatements == null)
      {
         throw new IllegalStateException(
            "No dirty FQL statements available");
      }
      
      List<String> list = (dirty ? dirtyStatements : statements);
      
      return list.get(index);
   }
   
   /**
    * Replace the FQL statement at the specified index with a new statement.
    *
    * @param   index
    *          Index at which to replace statement.
    * @param   statement
    *          New FQL statement.
    * @param   dirty
    *          <code>true</code> to set statement for dirty database dialect;
    *          <code>false</code> to set primary statement.
    *
    * @throws  ArrayIndexOutOfBoundsException
    *          if <code>index</code> is invalid.
    */
   void setStatement(int index, String statement, boolean dirty)
   {
      if (dirty && dirtyStatements == null)
      {
         throw new IllegalStateException("No dirty FQL statements available");
      }
      
      List<String> list = (dirty ? dirtyStatements : statements);
      
      list.set(index, statement);
   }
   
   /**
    * Add an FQL statement to the end of the list of primary or dirty
    * statements stored in this bundle.
    *
    * @param   statement
    *          FQL statement to add.
    * @param   dirty
    *          <code>true</code> to add statement for dirty database dialect;
    *          <code>false</code> to add primary statement.
    */
   void addStatement(String statement, boolean dirty)
   {
      if (dirty)
      {
         if (dirtyStatements == null)
         {
            dirtyStatements = new ArrayList<>();
         }
         dirtyStatements.add(statement);
      }
      else
      {
         statements.add(statement);
      }
   }
   
   /**
    * Return the number of DMO getter methods stored in this bundle.
    *
    * @return  A nonnegative number.
    */
   int gettersSize()
   {
      return getters.size();
   }
   
   /**
    * Get an iterator on the DMO getter methods, if any, stored in this
    * bundle.
    *
    * @return  Iterator of getter method objects.
    */
   Iterator<Method> getters()
   {
      return getters.iterator();
   }
   
   /**
    * Add a DMO getter method to the end of the list of methods stored in
    * this bundle.
    * 
    * @param   getter
    *          Accessor method.
    */
   void addGetter(Method getter)
   {
      getters.add(getter);
   }
   
   /**
    * Get the <code>ParameterIndices</code> object associated with this
    * bundle.  This object maintains an array of zero-based indices into the
    * array of query substitution arguments for the base (i.e., unaugmented)
    * where clause, as it exists <i>after</i> preprocessing is complete.  This
    * additional level of indirection is necessary because the query
    * substitution placeholders may have been reordered within the FQL where
    * clause expression during the FQL preprocessing rewrite step, and some
    * parameters may have been inlined into the where clause.
    *
    * @return  An object which provides a mapping of indices into the query
    *          substitution parameter list.
    */
   ParameterIndices getParameterIndices()
   {
      return paramIndices;
   }
   
   /**
    * Set the <code>ParameterIndices</code> object associated with this
    * bundle.  This object maintains an array of zero-based indices into the
    * array of query substitution arguments for the base (i.e., unaugmented)
    * where clause, as it exists <i>after</i> preprocessing is complete.  This
    * additional level of indirection is necessary because the query
    * substitution placeholders may have been reordered within the FQL where
    * clause expression during the FQL preprocessing rewrite step, and some
    * parameters may have been inlined into the where clause.
    *
    * @param   paramIndices
    *          An object which provides a mapping of indices into the query
    *          substitution parameter list.
    */
   void setParameterIndices(ParameterIndices paramIndices)
   {
      this.paramIndices = paramIndices;
   }
   
   /**
    * Freeze this bundle so that its internal data structures can no longer be
    * modified.
    */
   void freeze()
   {
      ((ArrayList<String>) statements).trimToSize();
      statements = Collections.unmodifiableList(statements);
      
      if (dirtyStatements == null)
      {
         dirtyStatements = statements;
      }
      else
      {
         ((ArrayList<String>) dirtyStatements).trimToSize();
         dirtyStatements = Collections.unmodifiableList(dirtyStatements);
      }
      
      ((ArrayList<Method>) getters).trimToSize();
      getters = Collections.unmodifiableList(getters);
   }
}